Commit graph

2611 commits

Author SHA1 Message Date
Bryan Helmkamp
0845c331cb
Default Daytona auto-stop to 120 minutes
Omitting autoStopInterval from the create-sandbox request inherits
Daytona's server-side default of 15 idle minutes. Daytona counts
inactivity from the last sandbox interaction, and LLM inference never
touches the sandbox, so a single long inference call is enough for the
sandbox to auto-stop mid-run: a workflow failed exactly this way, with
the sandbox entering its stop transition 15 minutes after the last
command while the agent was still thinking.

Send an explicit 120-minute default when lifecycle.auto_stop is unset.
That clears any realistic inference call while still reclaiming
sandboxes leaked by a dead worker. An explicit auto_stop = "0s" still
disables auto-stop entirely.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-20 17:26:13 -04:00
Scott Werner
2168d902f0
Merge pull request #762 from fabro-sh/refactor/shared-run-spec-test-fixture
Some checks are pending
Rust / Format (push) Waiting to run
Rust / Clippy (push) Waiting to run
Rust / Generated Docs (push) Waiting to run
Rust / Test (Linux) (push) Waiting to run
Rust / Test (macOS) (push) Waiting to run
Add a shared RunSpec test fixture so additive fields stop churning tests
2026-08-19 17:55:59 -04:00
Scott Werner
1898031d74 Make RunSpec example a checked doctest 2026-08-19 17:47:25 -04:00
Scott Werner
eea868647f
Merge pull request #760 from fabro-sh/codex/blob-roundtrip-tests
Test blob offloads through production hydration
2026-08-19 17:44:19 -04:00
Scott Werner
19aa5940ea Add a shared RunSpec test fixture and adopt it
`RunSpec` has 13 fields and no `Default`, so every test that needed one
spelled out all 13 even when it cared about one or two. That put 64
hand-rolled `RunSpec { .. }` literals in `lib/`, and made a single
additive field cost a mechanical edit at roughly 30 sites.

Add `test_run_spec()` to `fabro-types`' feature-gated `test_support`
module: fixed `fixtures::RUN_1`, default settings, a minimal `test`
graph, `test_run_provenance()`, and every optional field unset. Tests
now spread it and only spell out what they assert on.

Adopt it at the 13 literals where the spread removes real duplication,
including the crate-local `test_run_spec` helpers in `fabro-store` and
`fabro-workflow`, which are now defined in terms of the shared fixture.
Tests that populate every field on purpose — the exhaustive `RunSpec`
serde round-trip in particular — keep spelling it out.

No production code and no behavior changes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 16:50:37 -04:00
Scott Werner
facc6a02f2 Test blob offloads through production hydration 2026-08-19 14:10:02 -04:00
Scott Werner
7b47ef2d05 Cover missing SQLite blob reads 2026-08-18 17:43:22 -04:00
Scott Werner
01efe7c883 Document BlobBackend as a transitional enum
Mark the Slate arm as temporary and record that the SQLite arm's
verified-read and hash-conflict semantics are the intended end state,
so the dual-backend enum reads as a rollout vehicle rather than a
permanent abstraction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-18 17:41:29 -04:00
Scott Werner
cc16362528 Add SQLite blob store foundation 2026-08-18 17:41:29 -04:00
Scott Werner
2575ab85fc
Merge pull request #747 from fabro-sh/codex/blob-hash-vocabulary
Some checks failed
Rust / Format (push) Waiting to run
Rust / Clippy (push) Waiting to run
Rust / Generated Docs (push) Waiting to run
Rust / Test (Linux) (push) Waiting to run
Rust / Test (macOS) (push) Waiting to run
TypeScript / Typecheck (push) Has been cancelled
TypeScript / Test (push) Has been cancelled
TypeScript / Build (push) Has been cancelled
Unify blob hash vocabulary
2026-08-18 15:55:43 -04:00
Scott Werner
3e6b23ce76 Keep loaded workflow-version closures out of implicit copies
LoadedWorkflowVersionClosure owns every file of every version in the
dependency graph, so an advertised Clone invites accidental deep copies
of the whole set. Drop the derive until a consumer needs owned copies.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-18 13:02:54 -04:00
Scott Werner
1e29347227 Cover rejection of broken transitive includes under file run goals
The positive run-goal tests only asserted fixture shape, so a regression
that stopped pushing the file-goal template root would keep them green
while broken nested includes were silently accepted. Pin the rejection
path directly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-18 13:02:54 -04:00
Scott Werner
2f2097be54 Anchor run-goal template validation at the version entrypoint
Create-time validation of workflow.toml run goals anchored includes at
workflow.toml for inline goals and at the goal file's directory for file
goals, while the run engine inlines the effective goal into the
entrypoint graph and renders it under the entrypoint's template source.
That divergence rejected layouts `fabro run` executes fine and accepted
layouts that fail at render time. Anchor both goal forms at the
entrypoint so validation matches the runtime, and pin the anchor with a
nested-entrypoint test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-18 13:02:54 -04:00
Scott Werner
5a5cfbdaa0 Parse template dependencies whose paths collide with discovery roots
Batched dependency discovery pre-seeded roots into the path-keyed result
map and reused that map as the traversal-dedup set, so a loaded include
target whose path matched a root (e.g. a goal template including the
graph file that anchors an inline prompt) was recorded but never parsed,
silently accepting invalid template content that per-root discovery used
to reject. Dedup traversal on the full (path, root, content) occurrence
instead, which also stops re-parsing identical duplicate roots.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-18 13:02:25 -04:00
Scott Werner
408cd2f745 Simplify workflow-version closure validation and loading
- Replace the discarded dependency-closure map in put/get with a
  visitor-based walk so only get_closure retains loaded versions
- Hold the closure root structurally in LoadedWorkflowVersionClosure
  instead of asserting its presence in the map with expect()
- Drop the visited-set parameter that guarded against impossible
  content-address cycles
- Move template-discovery error source-name extraction into
  TemplateDiscoveryError::source_name() where the variants are owned
- Collapse repeated TemplateSource construction into a TemplateRoots
  collector and share the config file-reference validation pipeline
  between dockerfile and run-goal references
- Deduplicate test helpers (version_id, version_with_goal_file,
  impl Into<String> config fixtures)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-18 13:02:11 -04:00
Scott Werner
63025bb748 Close workflow goals over version dependencies 2026-08-18 13:01:03 -04:00
Scott Werner
a0845d8346
Merge pull request #756 from fabro-sh/refactor/graph-reference-kind
Classify graph attributes with a graph-only reference kind
2026-08-18 12:57:32 -04:00
Scott Werner
0d6d2392e6
Merge pull request #755 from fabro-sh/fix/template-dependency-discovery
Harden template dependency discovery
2026-08-18 12:28:11 -04:00
Scott Werner
8dfbfb9aa5 Classify graph attributes with a graph-only reference kind
reference_kind_for_attribute returned the full ReferenceKind, which
includes the config-sourced Dockerfile kind the classifier can never
yield, so the shared graph walker carried a silent `continue` and an
`unreachable!` for impossible kinds; each new config-sourced kind widens
those filler arms, and a classifier extension that reuses an existing
kind would be dropped by the walker without validation, visitation, or a
compiler error. Return a GraphReferenceKind subset instead (converting
into ReferenceKind for validation), making the walker's matches total
with every arm meaningful.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 17:58:46 -04:00
Scott Werner
9459ce1d04 Attribute template discovery errors to their source by construction
TemplateDiscoveryError only named a failing source through the Display
strings of its variants: parse and load failures forwarded transparently
to inner errors whose source naming varies (parent for some load
failures, the child path for dynamic dependencies, nothing for I/O
faults), so consumers that need the failing template's path had to
string-round-trip error messages. Carry the parent path on every
variant, exposing a total source_path() accessor, and render parse and
load failures with a parent-naming message above the preserved source
chain.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 17:53:13 -04:00
Scott Werner
679bc6701b Parse template dependencies whose paths collide with discovery roots
Dependency discovery pre-seeded roots into the path-keyed result map
and reused that map as the traversal-dedup set, so a loaded include
target whose path matched a root was recorded but never parsed (an
include chain that reaches the file anchoring a root silently skips its
content), and a second root occurrence at an already-seeded path was
dropped without parsing. Dedup traversal on the full
(path, root, content) occurrence instead, so every distinct authored
occurrence is parsed exactly once and identical duplicates parse once.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 17:51:46 -04:00
Scott Werner
14cc56b25f Remove stale env-interpolation promises from docs
Config {{ env.NAME }} interpolation was removed workspace-wide (tokens
still parse only to fail with a migration message), but several doc
comments and the server-secrets strategy doc still presented it as a
live mechanism, including run goal file paths where the new
workflow-version validation now makes the contradiction user-visible.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 17:36:53 -04:00
Scott Werner
95b511128f Align SHA-256 hash casing contracts 2026-08-17 17:31:15 -04:00
Scott Werner
46d4a1e5c8 Inline the blob_hash_from_response alias
It was a one-line passthrough to parse_blob_ref with a single caller,
leaving two names for the same operation; every other consumer calls
parse_blob_ref directly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 13:56:19 -04:00
Scott Werner
3524cd76d4 Generate the blob-field snapshot filters from a field list
The manifest_blob and definition_blob filter entries were copy-paste
twins that had to be edited identically; build them from one loop like
the elapsed-ms filters above so the pattern and placeholder cannot
drift apart.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 13:56:19 -04:00
Scott Werner
80b99e9b7b Drop duplicated blob-hash rewrites from the attach normalizer
The [BLOB_HASH] placeholder was defined both here and in the shared
json_snapshot_filters regexes, which had to be edited in lockstep. The
fabro_json_snapshot! macro always applies the shared filters to the
rendered string, so the normalizer copies were redundant.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 13:56:19 -04:00
Scott Werner
ae5c734299 Probe sandbox locality once per context resolution pass
materialize_blob_ref checked is_local_execution for every blob
reference, but the sandbox and run directory are invariant across a
resolution pass, so each check after the first was a redundant (and on
Docker/Daytona, remote) round-trip. The check is now memoized in a
per-pass SandboxLocality threaded through resolve_execution_value.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 13:56:19 -04:00
Scott Werner
af522d1aae Share the blob cache across dump Json and Text hydration
hydrate_referenced_blobs_with_reader kept a per-call blob cache for the
Json entries but the Text branch bypassed it, so offloaded stage
responses (referenced by both checkpoint values and response.md) were
fetched twice per dump. Both branches now hydrate through the shared
cache, and a test pins the single-fetch behavior.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 13:56:19 -04:00
Scott Werner
88b2a01af8 Type the blob-write response hash as fabro_types::BlobHash
Promote BlobHash to a named OpenAPI schema with the ^[0-9a-f]{64}$
pattern, reference it from WriteBlobResponse.hash and the blobHash path
parameter, and map it to fabro_types::BlobHash via with_replacement.
The server now serializes the domain type directly and the client gets
a parsed BlobHash by construction, removing the to_string/parse adapter
pair across the wire boundary. Adds the JSON-parity test required for
new replacements.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 13:56:19 -04:00
Scott Werner
f1c00a167e Rename blob read parameters from id to blob_hash
Finish the blob-hash vocabulary unification at the defining signatures:
RunStoreBackend::read_blob, RunStoreHandle, LocalRunStoreBackend, the
HTTP backend impl, RunDatabase::read_blob, and BlobStore::read/exists
all said `id`, which kept re-teaching the old vocabulary at every impl
site and inlay hint.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 13:56:19 -04:00
Scott Werner
a52e2c3334 Bump API spec version to 0.2.0 for the blob-write wire break
The WriteBlobResponse field rename (id -> hash) is a breaking change to
the wire contract with no compatibility shim, so signal it in the spec
version. There is no runtime version handshake; clients generated from
the older spec fail on the missing field until rebuilt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 13:56:19 -04:00
Scott Werner
bf4265e1b8 Unify blob hash vocabulary 2026-08-17 13:56:19 -04:00
Scott Werner
a045ea4cb0 Remove the unused list_blobs API from fabro-store
RunDatabase::list_blobs and BlobStore::list have had no production
callers since the store-dump export switched from enumerating the whole
blob namespace to hydrating only referenced blob refs. The semantics
have also gone stale: blobs now live in one content-addressed store
shared across run handles, so list_blobs on a per-run handle returned
every blob from every run, inviting exactly the per-run-enumeration
misuse the old dump loop would be today.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 12:02:10 -04:00
Scott Werner
8c3ff6216c Revert "Serialize workflow-version canonical bytes once at construction"
This reverts commit 8090d7030984862564a929ee9264e93911014e00.

The cached canonical field was optimizing an unmeasured path: without
the (deferred) O(closure) dependency re-validation multiplier, the
repeated serialization is microseconds for realistic versions. Compute
canonical bytes on demand like the environment, automation, and MCP
stores do, rather than carrying a serde-skipped cache field, a
construction bootstrap, and doubled memory for it. Purely in-memory:
stored blobs and version IDs are unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 16:34:52 -04:00
Scott Werner
180330c117 Remove unused WorkflowPath::parent and is_ancestor_of
Neither method has callers anywhere in the workspace: resolve_reference
splits on '/' directly, and the path-collision validator now checks
ancestor prefixes against a path set. parent() also constructed Self
without going through validate(), so dropping it removes an unvalidated
construction path from the wire type's public API.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 16:34:52 -04:00
Scott Werner
75fb1e3a1d Prove workflow-version ID JSON parity with the OpenAPI schema
The round-trip fixtures only used empty workflow_dependencies, so no
WorkflowVersionId value ever appeared on the wire in a fabro-api
assertion and CreateWorkflowVersionResponse had no coverage at all.
Put a real 64-hex id in the fixture, round-trip the response type, and
pin serialization to the schema's ^[0-9a-f]{64}$ pattern including
lowercase normalization of case-insensitive input.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 16:34:52 -04:00
Scott Werner
cc711027da Parse workflow version IDs case-insensitively
WorkflowVersionId bolted a lowercase-only byte scan onto BlobHash
parsing, giving the same 64-hex concept two parse behaviors across
entry points. Identity is the decoded 32-byte digest and canonical
serialization always emits lowercase, so accepting either case on
input is lossless — the stored-blob canonicality check still rejects
non-canonical bytes independently. Delegate straight to BlobHash.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 16:34:52 -04:00
Scott Werner
20c9fba0b1 Serialize workflow-version canonical bytes once at construction
WorkflowVersion::new serialized the whole version just to enforce the
size limit and threw the bytes away, the store re-serialized them to
write the blob, and every read re-serialized a third time for the
canonicality comparison. Cache the canonical bytes on the struct at
construction (skipped during serde) and expose them as an infallible
borrow; the now-unconstructable InvalidShape store error variant goes
away with it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 16:34:52 -04:00
Scott Werner
e688bd9876 Return 422 for invalid workflow-version dependencies
DependencyInvalid fell through to the curated 500 even though the
OpenAPI contract promises 422 workflow_version_dependency_not_found for
an absent, invalid, or non-canonical dependency. Route it to that
response alongside DependencyNotFound; the top-level message only names
the caller-supplied path and id, so no internal chain leaks. Drop the
InvalidVersion/InvalidShape arms, which were unreachable from the only
call site.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 16:34:52 -04:00
Scott Werner
79e44262aa Detect workflow path collisions hidden by sort order
The adjacent-pair scan over the byte-sorted path list missed
file/directory collisions whenever a sibling path sorted between the
ancestor and its descendant (any byte below '/' after the shared
prefix, e.g. "assets.txt" between "assets" and "assets/item.txt").
Replace it with an exhaustive ancestor-prefix lookup over a path set,
which also catches equal paths across files and workflow dependencies.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 16:34:52 -04:00
Scott Werner
178320e7a5 Add immutable workflow version resource
Add the WorkflowVersion domain resource with exactly entrypoint, files,
and workflow_dependencies, plus strict WorkflowPath validation and
deterministic canonical raw JSON. Semantic validation of graph imports,
templates, file references, workflow.toml rules, Dockerfile paths, and
exact child-workflow dependency bindings lives in the new
fabro-workflow-version crate, which validates the complete stored
dependency closure through the shared blob store before writing a root.
The authenticated create-only POST /api/v1/workflow-versions endpoint
ships with its OpenAPI contract, Rust type replacements, and generated
TypeScript client.

Squashed from the resource commits of the original combined branch;
the walker unification this builds on landed separately.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 16:34:52 -04:00
Scott Werner
a76e2d7ddc Test graph goal filenames with at prefix 2026-08-13 15:52:54 -04:00
Scott Werner
13755d7c2b Unify the workflow graph reference walkers
Move the static-reference vocabulary out of fabro-workflow so every
consumer shares one definition: ReferenceKind, AttributeScope, and
reference_kind_for_attribute land in fabro-types::graph, and
validate_static_reference plus a new visit_graph_references walker land
in fabro-template. The manifest bundler drops its ad-hoc graph scan and
walks references through the shared walker.

Unifying the walkers forces three semantic alignments, each matching
what the engine actually executes rather than what the old scanners
happened to match:

- stack.child_dotfile is no longer classified as a child-workflow
  reference; the engine never resolved it as one.
- import and stack.child_workflow only count at node scope; graph- and
  edge-level occurrences were scanned but never executed.
- @@-escaped goals flow through the shared walker's escape handling
  instead of the bundler's own prefix stripping.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 14:34:23 -04:00
Scott Werner
09c6bd836b Rename SettingsLayer::image_layers to environment_images
Also add environment_images_mut and adopt it in the run compiler's
Dockerfile resolution, replacing the hand-rolled iteration over named
environments plus the run environment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 14:34:23 -04:00
Scott Werner
3226d845bc
Merge pull request #726 from fabro-sh/codex/extract-working-tree-collector
Some checks are pending
Rust / Format (push) Waiting to run
Rust / Clippy (push) Waiting to run
Rust / Generated Docs (push) Waiting to run
Rust / Test (Linux) (push) Waiting to run
Rust / Test (macOS) (push) Waiting to run
Extract workflow bundling from manifest assembly
2026-08-12 14:01:31 -04:00
Scott Werner
04f45b7c6b Restore lexical root path normalization and simplify bundler internals
Route the root workflow through collect_workflow_entry so relative root
arguments are lexically normalized before reading, matching the pre-refactor
behavior: `..` segments no longer resolve through symlinks to a file other
than the one the manifest key names, and `~`-prefixed references are
rejected again. Adds a symlink regression test for the root argument.

Also:
- collect_workflow_entry/collect_workflow_location return the manifest key,
  so bundle() no longer recomputes the root key
- hold one FilesystemTemplateStore on the bundler instead of rebuilding it
  per template reference
- drop the unused Clone derive on WorkflowScanInput
- replace the hand-rolled JSON literal in the characterization test with an
  insta snapshot per the testing strategy
- share one write_file fixture helper between the lib and bundler test
  modules
- remove the bundler git-push test; the bundler has no git code path, so the
  test could not fail

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 13:44:18 -04:00
Scott Werner
62ed7cb8a2 Rename RunBlobId to BlobHash 2026-08-12 11:33:41 -04:00
Scott Werner
2ee6109006 Warn on malformed blob keys and drop structural sharing test
Skipping a malformed key under blobs/sha256 during listing now emits a
warn! so operators get a signal when the CAS namespace contains garbage,
matching the projection-cache warmup skip path. Also removes the
runs_share_database_blob_store test, which asserted Arc pointer identity
of internal wiring rather than any observable behavior.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 11:26:35 -04:00
Scott Werner
f773a24758 Simplify run handle construction and blob store test fixtures
- Collapse RunDatabase::open_writer/open_reader wrappers into one
  pub(crate) build, with a Database::open_run_database helper that
  gathers the shared-store dependencies in one place
- Stop fetching the blob store on open_run's active-cache hit path
- Share a raw-db test fixture between the two BlobStore raw-key tests
- Evict the cached writer in open_run_reader_is_read_only so the test
  exercises the real reader construction path

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 10:51:19 -04:00
Scott Werner
10499e707c Share one blob store across run handles 2026-08-10 15:32:47 -04:00