Remote acquisition walked the entire depth-1 checkout and failed on any
symlink that dangled or resolved outside the root, even when the link
was nowhere near the selected workflow. Submodule-style dangling links
and links into the host are common in workflow repositories and made
--workflow-git fail where the same commit collected fine locally. The
bundler already root-checks every file it opens; the only unchecked
reads were the selected TOML (or a graph selector's sibling TOML) during
location resolution. Check those in collect_workflow_versions and drop
the O(repo) walk. walkdir stays a dev-dependency for the dump tests.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The replaced manifest builder resolved the run's repository identity from
the workflow's run.scm settings before falling back to the checkout's
origin. The new standalone derivation always used the checkout's origin,
so a fork checkout of a workflow that names its upstream repository
silently targeted the fork and pushed there.
Read the run.scm layer from the resolved workflow.toml and project.toml
(or from the inline workflow.toml bytes) and pass it through both the CLI
and the standalone run-tool adapter. When the configured repository is
not the checkout's origin, nothing can be proven about it, so derivation
now fails with a message naming that mismatch instead of the generic
"push the commit" hint.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Standalone fabro_run_create ignored the environment's provider and always
produced a Git target or failed, so a Local environment with no explicit
target was rejected by admission and a directory without Git metadata
hard-failed, while fabro run derived a folder target and a none target
for the same inputs.
Move the CLI's provider-aware derivation into fabro-manifest as a shared
helper with a typed error, and have the standalone adapter look up the
selected environment and call it. The helper also distinguishes a failed
remote query from an unpublished commit, so an offline ls-remote no
longer reports "push the commit and try again" when the branch is
already on the origin.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Inline workflow sources were routed through the checkout-selector
collector, which rewrites any extensionless relative path to a
.fabro/workflows/<name>/workflow.toml lookup. A supplied entrypoint such
as "review" therefore failed with "workflow was not found" even though
its bytes were in the file map.
Add a dedicated inline collector in fabro-manifest that treats the
entrypoint as an exact key, checks the file paths for filesystem
collisions before staging anything, and stages the bytes in a private
temporary root only for the duration of collection. The server adapter
now delegates to it instead of staging files itself.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Deduplicate shared-filesystem capability checks and simplify workflow-source dispatch and types. Move Git observation and local package collection onto spawn_blocking, and flush inline workflow files before collection.
Simplify validated source and input types, derive inline size-limit messages from shared constants, add target schema-parity coverage, and remove dead producer pass-through parameters.
The packager logged the full packaging error chain at WARN. That chain
embeds caller-supplied workflow and prompt source: the graph parser's
diagnostic includes the unparsed remainder and the TOML parser prints
the offending line. The logging strategy prohibits user file contents
in tracing events at every level, and this adapter runs inside
`fabro mcp` and run workers at the default filter.
Log the collector error's own path-only message at DEBUG, since a
malformed request is an expected input error, together with the
entrypoint and file count. Wrap the blocking-task join error with
`context` instead of interpolating it. A test installs a TRACE-level
subscriber around the blocking path and checks that the fixture's
source marker, which the full chain does contain, never reaches the
log.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Supplied files are staged on the host filesystem and WorkflowLocation
probes the fixed sibling name `workflow.toml` there. A request that
supplied `Workflow.toml` beside its graph therefore attached the config
on a case-insensitive host (and then failed the not-supplied check),
while the identical request on ext4 registered a version with no
config. The outcome of a content-addressed registration depended on
the server's filesystem.
After collection, every version is checked against the supplied map:
when no exact sibling `workflow.toml` was supplied, no supplied key may
alias that name under the same case and normalization rules the tool
already applies to supplied keys. A supplied sibling config still
attaches only to the graph it selects, matching checkouts, since
several graphs may share one directory.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Every packaging failure collapsed into one generic message, so an LLM
caller that omitted a child workflow, referenced a prompt with the
wrong case, or exceeded the canonical size limit could not tell what to
fix. The collector's error type already separates variants whose
messages carry only paths and counts from the ones whose sources quote
supplied content.
collect_supplied_workflow_versions now returns the typed collector
error, with new variants for a referenced file missing from the
package root, a collected file the caller did not supply, and staging
I/O failures. The bundler reports missing files with their
package-relative path so the collector can recognize them. The packager
renders the full cause chain for path-only variants and stops at the
last path-only level, plus a hint, for graph, TOML, and template
failures whose diagnostics quote source.
The tool-side raw byte total remains a cheap lower bound; the canonical
limit now surfaces with its own message instead of the generic one.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ServerWorkflowVersionPackager was a pure adapter over
fabro_manifest::collect_supplied_workflow_versions that touched no
server state, yet it lived in fabro-server and was imported from there
by the standalone MCP server and the CLI run worker. fabro-manifest can
depend on fabro-tool without a cycle, so the adapter now lives beside
the collector as SuppliedWorkflowVersionPackager and fabro-server no
longer exports a non-server module for it.
The adapter also cloned every version's file map out of a closure it
already owned. CollectedWorkflowClosure::into_versions hands the
versions over by value inside the blocking task instead.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
WorkflowLocation dispatches any `.toml` path to the config loader, so a
supplied entrypoint such as `sub/run.toml` was accepted, its graph
became the version entrypoint, and the config file was registered under
its own name. Runtime only reads WorkflowVersion::config_path(), the
fixed sibling `workflow.toml`, so the version's goal, environment, and
Dockerfile settings were silently dropped on every run.
In workflow-version projection, reject a config whose collected path is
not the graph's sibling `workflow.toml`. This applies to every caller
that packages versions, including `fabro run <dir>/other.toml`, which
previously registered the config and then ignored it; failing at
packaging replaces a silent drop. Manifest bundling for the legacy run
path does not project versions and is unchanged.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
validate_workflow_source_paths ran inside workflow_files for every
collected version, including the pre-existing checkout callers behind
automation materialization and `fabro run`. A repository on a
case-sensitive filesystem whose graph legitimately references two paths
that differ only by case or Unicode normalization packaged before this
branch and would have started failing.
The check is also redundant for the supplied-content path that
motivated it: the tool request validates the full key set before
staging, and the supplied collector confines collected keys to that
set. Remove it from the collector so existing callers are unchanged.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The pre-resolution containment check in the version bundler was a
no-op: ManifestPath::from_absolute happily returns a `..`-prefixed path
for locations outside the package root, so an escaping
stack.child_workflow reference reached WorkflowLocation resolution,
which probes and parses config files on the host before the real
containment check in read_package_file ran. The request still failed,
but the TOML parser's diagnostic quoted the host file.
Check that the normalized reference stays under the package root before
resolving it, and extend the supplied-workflow test to plant malformed
host files that any parser would quote.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Move supplied-content packaging into fabro-manifest beside the checkout
collector, and narrow the injected seam to a packager that returns the
dependency-ordered closure so ClientBackend registers versions with the
client it already owns.
Validate the tool input once through a ValidatedWorkflowVersionCreate
newtype, matching the other tools, instead of re-validating at three
layers. Reuse the fabro-types unique-map deserializer and the shared
"not available" error helper, derive budget messages from the limit
constants, and render the tool result through the shared summary+JSON
path used by sibling tools.
Share one extension dispatch between WorkflowLocation::resolve and
from_exact_path, compute the bundler's normalized reference once, key
path-collision checks by a Cow so the canonical exact check no longer
allocates, and log the full packaging error chain before returning the
curated tool message. Replace the hand-rolled axum test server with
httpmock and declare the new unicode dependencies at the workspace.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Workflow LLM handlers build lithos requests, bill from lithos usage and
cost, and classify failures from lithos `ErrorKind`. Model resolution and
fallback use the fabro-llm selection and catalog helpers. Validation
rules read the lithos catalog, and store fixtures use the new
`BilledModelUsage` shape.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Share canonicalize_location and resolve_existing_workflow_location
between the local package resolver and the version collector, drop the
redundant package-root pre-check and the PackageFileReadError enum in
favor of anyhow context, and read HEAD's SHA from git2 instead of a
separate rev-parse subprocess.
Make GitRunTargetObservation a plain struct, replace the repo-info
tuple with a named struct, tighten the closure view trait, avoid
deep-cloning the root workflow during collected validation, remove the
unused into_closure accessor, and dedupe test helpers.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Automations now store an environment_id that must reference an enabled
Docker or Daytona environment. Each trigger fire resolves the current
environment definition and snapshots its settings into the run, and
deleting an environment still referenced by an automation is rejected
with a conflict.
Existing automations are backfilled conservatively: a compatible
environment named default is selected when present, otherwise the sole
compatible environment. Anything ambiguous is left incomplete and cannot
run until an operator selects an environment in the web UI.
Scheduler failures are recorded on the automation as last_error and
cleared after the next successful scheduled run.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Share the RunIntent shape between the scheduler and the API trigger via
AutomationRunMaterialized::into_run_intent, drop the pass-through
packaging wrappers and the unreachable VersionIdMismatch error, and move
the config-path and version-ID derivations onto WorkflowVersion so the
server, validator, and collector stop re-deriving them.
The collector now owns the collected sources (moving file contents
instead of cloning them), resolves the workflow location once, and shares
the not-found probe with build_run_manifest. The bundler reads a goal
file once instead of twice.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Apply cleanup review findings on the model stylesheet template branch:
- Move the root-only stylesheet rule into visit_graph_references via a
GraphPosition parameter, so the bundler and workflow-version stop
re-implementing the entrypoint guard with duplicated match arms
- Let ModelStylesheetTemplateTransform build its own template store and
skip the pass entirely when the graph has no stylesheet; drop its dead
Transform impl and the template_render_store re-export
- Parse fix-message namespaces with the typed Namespace enum, share the
vars/goal fix strings with script_interpolation_fix, and replace the
attribute_name magic-string check with a restricted-namespace fix the
stylesheet transform sets on its own render target
- Drop template_render_store's content parameter; the store's render
always overwrites it before rendering
- Trim redundant tests and add a transform_options() helper in
pipeline/validate.rs tests
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FBHEs42qNHDeKmsqTSDSQ
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>
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>
Apply cleanup review findings on the collector extraction:
- Deduplicate the lexical path-normalization loop: normalize_absolute_path
now delegates to lexically_normalize_access_path, and it plus
manifest_path_from_absolute live in working_tree.rs so the module
dependency points one way (projection -> collector). Drop the redundant
re-normalization in collect_bundled_file.
- Extract collect_bundled_template_includes to replace the copy-pasted
goal/prompt template-closure sequence, seed_config_document for the
duplicated config seeding, and read_source_input for the duplicated
config reader closures (with the user-settings is_file check hoisted).
- Replace ~100 lines of trivial getters on the Collected* output structs
with pub(super) fields; keep the CollectedPath newtype encapsulated.
- Assemble the manifest by value, moving collected sources into the wire
types instead of deep-copying every file a second time; drop two full
DraftDocument clones that only satisfied the borrow checker; stop
recomputing manifest paths per file in template-dependency verification.
- Resolve the root workflow once in assemble_current_manifest, removing an
unreachable duplicate error path; flatten single-use CollectionNamespace
into a finalize_documents free function.
No behavior change; fabro-manifest tests, clippy, and fmt pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The manifest builder's best-effort pre-run push converted every result
into a PreRunPushOutcome that was serialized into GitContext, expanded
into five OpenAPI union arms, and generated into API clients — but no
production path ever read it; every field read was a test.
Delete the concept while preserving the behavior:
- Drop the PreRunPushOutcome enum and GitContext.push_outcome from
fabro-types; GitContext keeps origin_url, branch, optional sha, and
dirty, which remain real execution inputs and provenance.
- Rename the manifest outcome builder to push_manifest_branch_best_effort,
a side-effect-only helper with the same decision rules: skip without an
origin, skip on configured-repository mismatch, skip when the branch is
already synced, otherwise push noninteractively and discard the result
without failing manifest creation or logging raw Git stderr.
- Prove the push through repository state instead of the deleted enum: a
branch ahead of a local bare origin is pushed during manifest build, a
mismatched configured repository is not, and a failing remote helper
still cannot fail manifest creation.
- Remove push_outcome from GitContext in OpenAPI, delete the five-arm
union schemas, and drop the fabro-api type replacement and re-export.
- Keep one regression proving historical run.created events with a nested
push_outcome still deserialize through ordinary unknown-field tolerance
and reserialize to the reduced shape. No migration or event rewrite.
Old JSON carrying the removed field stays readable. Newly generated
clients omit a field older servers required, so new-client-to-old-server
compatibility is intentionally not promised for this pre-1.0 contract.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Drop ManifestTarget.identifier (the raw token the user typed) and
ManifestGoal.path (the original goal-file path) from the OpenAPI
manifest schema, the Rust manifest builder, the regenerated Rust and
TypeScript client types, and every canonical test fixture. Neither
field had a production reader: the server selects the workflow by
target.path and consumes only the resolved goal type and text.
Target path, goal type/text, manifest versioning, and submitted-byte
persistence are unchanged. Old request bodies that still carry the
removed properties remain accepted through unknown-field tolerance,
pinned by a dedicated public-route regression test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>