Commit graph

249 commits

Author SHA1 Message Date
Scott Werner
2d292c28f8
Merge pull request #783 from fabro-sh/codex/sqlite-blob-startup-activation
Activate verified SQLite blob storage at server startup
2026-08-24 16:32:59 -04:00
Scott Werner
fb833294bf Remove stale auth-session test import 2026-08-24 14:40:38 -04:00
Bryan Helmkamp
c042b9abdc
Merge remote-tracking branch 'origin/main' into feature/bounded-agent-tool-output
# Conflicts:
#	lib/components/fabro-sandbox/src/clone_source.rs
2026-08-24 14:08:04 -04:00
Scott Werner
0e580f0a43 Adapt run intents to activated blob storage
Use the synchronous blob authority established at server startup and remove the obsolete per-request store-open error path.
2026-08-24 14:07:51 -04:00
Scott Werner
776e719383 Harden SQLite blob activation safety
Keep VACUUM snapshots private until permissions and durability are established. Refuse to recreate a missing rollback backup after import has begun, and preserve secondary cleanup failures in startup logs.
2026-08-24 14:02:35 -04:00
Scott Werner
f71d077221 Register the SQLite blob activation bridge as a server migration
The activation module described itself as a temporary compatibility
bridge but bypassed the structure the migrations strategy prescribes: no
dated migrations/ file, no src/migrations.rs registry entry, no
REMOVAL_DEADLINE, and no removal_deadline log field. The strategy doc's
removal checklist (grep REMOVAL_DEADLINE, explicit registry ordering)
would never have surfaced it, letting the bridge silently outlive its
window as a second, parallel migration mechanism in serve.rs.

The module now lives at migrations/2026082301_sqlite_blob_activation.rs,
is registered and re-exported through src/migrations.rs like the two
existing server migrations, carries a REMOVAL_DEADLINE eligibility floor
(removal still requires the evidence and explicit approval in the module
docs), and logs removal_deadline on every activation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 14:02:35 -04:00
Scott Werner
5629dcd7d0 Make snapshot and backup publication durable across power loss
Neither the pre-activation backup nor the pre-migration snapshot fsynced
the staged file contents or the parent directory around the publishing
rename. A crash after the import committed could lose the retained
'.pre-blob-activation.bak' (whose directory entry was never made
durable), and the next activation would then write a new backup that
already contains the imported blobs, silently breaking the documented
pre-activation rollback boundary; a torn staging file could likewise
wedge later boots in backup validation.

write_snapshot_to_staging now syncs the staged file before handing it to
the caller, and both publishers sync the destination's parent directory
after their rename (fabro-db on a blocking task, activation inside its
existing blocking publication task).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 14:02:35 -04:00
Scott Werner
e067de9382 Share one SQLite snapshot-staging helper between fabro-db and activation
create_backup re-implemented the staging half of fabro-db's
pre-migration snapshot (remove stale staging file, UTF-8 check,
VACUUM INTO, private permissions), and remove_file_if_exists and
set_private_permissions had been made pub precisely to hand-copy that
sequence. Any future hardening of snapshot staging would have had to
land in two crates and could drift.

fabro-db now exposes write_snapshot_to_staging with a typed
SnapshotStagingError; both the pre-migration snapshot and the
pre-activation backup stage through it, and the hand-copied helpers are
private again. The publish halves stay separate on purpose: migrations
overwrite their snapshot, activation publishes with persist_noclobber
plus integrity validation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 14:02:35 -04:00
Scott Werner
31c7a670d5 Continue startup when the final WAL truncate checkpoint reports busy
PRAGMA wal_checkpoint(TRUNCATE) returning busy=1 aborted server startup.
Any external reader that outlives the pool's five-second busy timeout (a
replication agent, a backup tool, an operator sqlite3 shell) would crash
the boot, and a supervisor restart would loop into the same abort while
the reader persisted, over a condition that threatens no data integrity.

A busy truncate now logs a warning and startup continues; a later
checkpoint truncates the WAL once the reader is gone. Adds the
failure-path coverage the relocated checkpoint lost: a held read
snapshot blocks the truncate and activation still succeeds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 14:02:35 -04:00
Scott Werner
e1abecc9f4 Skip the blob activation disk preflight when no mount matches the database
available_space_for_path returning None aborted startup with a fatal
UnknownFilesystem error, even on a fresh install with zero legacy rows.
Hosts with tmpfs or squashfs roots, network-filesystem data paths, or an
unreadable mount table would fail every boot with no operator override,
while the resource sampler already treats the identical condition as
benign (supported: false) and keeps running.

The preflight now logs a warning and is skipped when free space cannot
be determined; the import, verification, and integrity checks still run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 14:02:35 -04:00
Scott Werner
bccc5750a4 Size the blob activation disk preflight to the remaining import work
The preflight demanded ~1.5x the full legacy inventory bytes free on
every startup, with no credit for rows already imported. Because the
first activation itself consumes about twice the legacy bytes (the
SQLite copy plus the retained backup) and the legacy keyspace stays in
place for the whole retention window, a successfully activated server
could fall below the requirement and become unable to restart until an
operator freed space the server would never write.

The legacy inventory now checks each row's hash against the SQLite blobs
table and reports pending rows and bytes, and the preflight requires
1.5x only the pending bytes plus the backup reserve and fixed headroom.
A warm restart with nothing left to import needs only the headroom.
Also updates the server operations doc for this and for the
verification pass now running only on boots that import rows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 14:02:35 -04:00
Scott Werner
94e3d46754 Cut redundant blob scans and hashing from server startup
Startup previously scanned the legacy SlateDB keyspace three times and
SHA-256-hashed every value in each pass (inventory, import,
verification), then read and rehashed every row of the live SQLite blobs
table — on every boot, even a warm restart with nothing to import. With
a large object-store-backed legacy keyspace that makes restart time
proportional to total blob bytes for the whole retention window.

The inventory pass now only validates key shapes and sizes the keyspace;
digests are still validated by the import pass before any row persists.
The independent verification sweep now runs only on boots whose import
actually inserted rows: the import pass itself byte-compares every
already-present legacy row each boot, so a no-op restart is already
fully cross-checked without a third scan or a full-table rehash.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 14:02:35 -04:00
Scott Werner
b56aee570c Pass the SQLite pool directly to the legacy blob import and verification
import_legacy_blobs_into and verify_legacy_blobs_in took a &BlobStore and
extracted its pool through sqlite_pool_for_legacy_import, an Option that
was statically always Some in production (the None arm existed only for
the test-only Slate backend). That accessor forced a clippy
unnecessary_wraps suppression and two WrongTargetBackend error variants
no production caller could ever hit, and the activation path round-tripped
a pool it already owned through a BlobStore it had just built.

Both functions now take &SqlitePool, deleting the accessor, the
suppression, both unreachable variants, and their rejection test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 14:02:35 -04:00
Scott Werner
eb54a8d0f8 Forward fabro-store/test-support through dependent test-support features
fabro-workflow's and fabro-server's src/test_support.rs import
fabro_store::test_support, but their test-support features never enabled
fabro-store/test-support. Workspace builds passed only through feature
unification from other members' dev-dependencies, while per-crate builds
such as `cargo check -p fabro-cli --tests` or
`cargo check -p fabro-server --features test-support` failed with E0432.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 14:02:35 -04:00
Scott Werner
d65785d888 Simplify blob activation and share the test store fixture
Blob activation cleanups:
- Reuse fabro-db's append_to_path, remove_file_if_exists, and
  set_private_permissions instead of local duplicates.
- Return the store directly from activate_blob_storage; the report
  wrapper existed only to be logged internally and then discarded.
- Collapse compute_disk_preflight to return the required free bytes
  instead of echoing its inputs back through a struct.
- Deduplicate the "exactly one ok row" PRAGMA integrity_check protocol
  into one executor-generic helper used by the backup and live checks.
- Skip re-validating a freshly published backup; the staging copy was
  validated immediately before the atomic rename, so only a
  concurrently published file needs its own validation.
- Replace the manual anyhow wrapping plus duplicate error log in
  serve.rs with a plain .context(), matching other startup errors.
- Extract the disk-candidate enumeration in resource_sampler.rs that
  available_space_for_path had copy-pasted from sample_disk_resources.

Test fixture cleanups:
- Route all hand-assembled Database::new(..., test_blob_store()) test
  fixtures (32 sites) through fabro_store::test_support::test_database,
  and make that helper infallible instead of returning an unconditional
  Ok.
- Install the test blob schema from fabro_db::BLOBS_MIGRATION_SQL via a
  test-support-gated optional dependency instead of a four-level
  relative include_str! into fabro-db's migrations directory.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 14:02:35 -04:00
Scott Werner
2814c1fd45 Activate verified SQLite blob storage 2026-08-24 14:02:34 -04:00
Bryan Helmkamp
1e284c625e
Simplify bounded tool output capture
Apply cleanups from a reuse/simplification/efficiency review of the
bounded-tool-output changes:

- Share one MAX_RUN_EVENT_BODY_BYTES constant in fabro-types; the server
  body limit, the agent's serialized-output reservation, and the event
  headroom test all derive from it.
- Rework truncation.rs around one split_head_tail helper: drop the
  hand-rolled ceil_char_boundary (std's is stable), the duplicate
  truncate_plain_output splitter and its dead Tail arm, and the
  head_bytes field with its sentinel values.
- Return Cow from preview_tool_output and take retain_tool_output's
  input by value, so untruncated output crosses the pipeline without
  full copies. Measure serialized JSON size with a counting writer
  instead of materializing the payload.
- Reuse fabro-llm's byte-token estimate (now public) instead of a third
  copy of the 4-bytes-per-token heuristic.
- Take retain_tool_result's ToolResult by value and mutate content in
  place; extract the triplicated error retain-emit-truncate block into
  finish_error_result.
- Share the shell retain-and-record sequence between the native and
  kimi shell tools as retain_shell_output.
- Move OutputCaptureBuffer::into_parts to reuse the head allocation,
  skip the buffer round-trip in replay_exec_result when output fits,
  and replace daytona's byte-iterator suffix matching with contiguous
  slice comparisons behind one retained_slices accessor.
- Make SessionBoundEmitter's fields private.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TK3QTWQHiXhRbFwTr57LzX
2026-08-24 13:56:16 -04:00
Scott Werner
856e2fadd8 Harden run-intent workflow-closure lowering
Cap closure expansion at 256 distinct workflow mounts. Mounts are keyed
by rebased path, so a small chain of stored versions that mounts a
shared dependency along two paths per level expands exponentially; a
single authenticated create request could stall the server before any
error was returned. The check also bounds the recursion depth.

Resolve file-form run goals through the certified version: expose
ValidatedWorkflowVersion::resolved_goal_file_content, which reuses the
exact grammar store validation certified, and drop the parallel
resolution (and its unreachable-for-stored-versions error variants) the
server had re-implemented. The certified entrypoint-presence invariant
replaces the MissingEntrypoint error the same way.

Destructure both environment layer types without `..` when pinning
server environment authority, so a new server-owned field becomes a
compile-time decision instead of silently escaping the pin.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 13:02:50 -04:00
Scott Werner
087d48f0c7 Sharpen run-intent admission error responses
Lowering and compiler rejections now carry the top-level error message
in the 422 detail, matching the diagnostic depth the legacy manifest
lane already returns for identical defects; the full source chain stays
in the server log.

Pre-persistence store failures stop claiming run_persistence_failed:
credential-store reads return credential_store_error and run-variable
snapshots return variable_store_error, so alerting keyed on codes
triages the failing subsystem instead of a persistence outage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 13:02:24 -04:00
Scott Werner
5606940aaf Parse create-run bodies strictly per admission lane
Both lanes now deserialize the raw request bytes directly instead of
round-tripping through a serde_json::Value, which silently collapsed
duplicate JSON keys to last-key-wins on the legacy manifest lane and
stripped line/column locations from manifest parse errors.

When neither lane accepts the body, attribution now recognizes a
defective manifest by its required keys, so a legacy manifest carrying a
stray workflow_version_id keeps its 400 manifest error instead of being
misrouted to a 422 run_intent_invalid describing a schema the caller
never used.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 13:02:14 -04:00
Bryan Helmkamp
6e19fb2eec
Reserve event space for serialized tool output 2026-08-24 13:00:58 -04:00
Bryan Helmkamp
401acb6cdf
Record tool output byte counts 2026-08-24 12:46:27 -04:00
Scott Werner
b51b80e16e Preserve run creation error context 2026-08-24 12:22:38 -04:00
Scott Werner
fc822ab9b0 Fix CLI RunSpec test fixtures 2026-08-24 12:17:47 -04:00
Scott Werner
96c6de0ab9 Trim create-run request parsing overhead
The create-run dispatcher deep-cloned the parsed JSON body once to
attempt the RunIntent shape and again for the RunManifest fallback,
so every legacy manifest request paid two full copies of a body that
carries entire workflow bundles. Deserialize both shapes from a
reference to the parsed value instead; routing and error attribution
are unchanged.

Also bind the lowered goal slot once in inline_goal_file rather than
re-navigating the settings layer and asserting the goal is still there.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 11:55:44 -04:00
Scott Werner
6b65f2a6af Share the create-run pipeline tail between both admission lanes
The intent and legacy-manifest create handlers each carried a full copy
of the same post-admission sequence: LLM readiness resolution, graph
compilation and model pinning, persistence, summary read, managed-run
registration, title-generation spawn, and the 201 response. The copies
had already drifted on when the run ID is resolved (before compilation
in one lane, after in the other).

Extract one finalize_created_run tail, with a small CreatedRunErrorStyle
carrying each lane's pinned error mapping and log lines so the wire
contracts are unchanged. Both lanes now resolve identity before
compilation and share the parent-link validation, which lets the
PinnedRun copy of PreparedRun's identity accessors be deleted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 11:55:44 -04:00
Scott Werner
18d98794ae Move Git-target validation onto RunTarget in fabro-types
The Git-target grammar (slug, branch, and SHA rules plus the derived
origin URL) was implemented twice with no shared code path: once in
server admission and again in sandbox start, so the two could drift and
disagree about which persisted targets are valid.

Own it once as RunTarget::validate() in fabro-types, next to the
primitives it uses, returning the canonical target together with its
derived GitContext projection. Admission consumes it directly, and the
start path re-derives the expected clone source from the same rules
before checking the persisted projection against it. The start path now
also moves the derived strings into the sandbox spec instead of cloning
them.

While reordering admission around the shared validator, run the pure,
in-memory checks (target grammar, environment id) before the blob-store
closure fetch and lowering so malformed requests no longer pay for
version-store I/O.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 11:55:44 -04:00
Scott Werner
040bc6c043 Add version-backed run intent creation 2026-08-24 11:55:43 -04:00
Scott Werner
87b49a8527 Merge main into feat/refresh-tokens-sqlite
Preserve the SQLite auth-session release notes alongside main's July 26 fixes and retain all current changelog navigation entries. Make the refresh-token rotation timestamp assertion deterministic after the merged suite exposed its wall-clock race.
2026-08-24 11:04:10 -04:00
Bryan Helmkamp
3217a05aad
Merge origin/main into feat/venice-search-provider 2026-08-21 21:04:28 -04:00
Bryan Helmkamp
88ed2ac9a3
refactor(search): select backend from available credentials 2026-08-21 20:09:52 -04:00
Bryan Helmkamp
cfa8ae92c0
style: apply pinned rustfmt 2026-08-21 19:34:38 -04:00
Bryan Helmkamp
45e06d2a6e
Merge pull request #775 from fabro-sh/claude/additional-github-repositories
Additional GitHub repository access
2026-08-21 18:55:31 -04:00
Bryan Helmkamp
1669791956
test: update clone depth snapshots 2026-08-21 17:47:02 -04:00
Bryan Helmkamp
4c467cd6ba
refactor(github): simplify repository access checks 2026-08-21 17:12:27 -04:00
Bryan Helmkamp
438bab29f0
feat: support shallow sandbox clones 2026-08-21 17:11:24 -04:00
Bryan Helmkamp
47954f731e
refactor(github): deduplicate additional-repository access plumbing
Consolidate the copies that review found across the feature:

- One GITHUB_CREDENTIAL_HELPER / GITHUB_CREDENTIAL_HELPER_KEY pair in
  fabro-github, with apply_probe_git_env() for probe commands; the runtime
  git bridge, server preflight probe, and live contract test all consume it
  so the probes exercise exactly what the bridge configures.
- GitHubRepositoryAccess::resolve_verified_token() owns the
  resolve-installations-then-mint choreography shared by server preflight,
  workflow initialization, and the live test.
- A shared lookup_installation() helper backs both the shared-installation
  resolution and the mint's installation lookup.
- The contents = read|write rule lives once as
  RunIntegrationsGithubSettings::contents_permission_allows_repository_access.
- The preflight probe paces retries with fabro-sandbox's exported
  replication_backoff() (3s/9s) instead of a contradicting 1s/2s loop, and
  shares one run_ls_remote() runner with the existing remote-ref check.

Also: collapse the dead Ok(None) arm and repeated error blocks in the
preflight token check, drop the derivable bridge_entry_count(), privatize
resolve_permissions() behind resolve_integration(), make
GitHubRepositorySlug ordering/hashing allocation-free, and use EnvVars
constants for env names.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 16:31:48 -04:00
Jesse Proudman
53efde3930 feat(search): add Venice backend for web_search
Brave stays the default. Shops that already vault VENICE_API_KEY
can drop BRAVE_SEARCH_API_KEY by setting
[server.integrations.search] provider = "venice".

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-21 12:31:03 -07:00
Bryan Helmkamp
d95b6cace1
feat(server): preflight additional github repository access
When a run declares additional repositories, preflight now proves the
whole effective set works instead of treating a minted token as proof:

- It constructs the same validated `GitHubRepositoryAccess` used by
  runtime initialization, so the two paths cannot disagree.
- In App mode it first resolves every repository's installation with
  the App JWT and requires one shared installation ID, naming any
  repository the App cannot see before the mint; then it mints the one
  scoped token, failing with the raw error on rejection.
- Every effective repository gets a non-interactive
  `git ls-remote <url> HEAD` probe through a shared helper that keeps
  the token out of the URL, argv, and errors (a credential helper reads
  GITHUB_TOKEN from the child environment), retries auth-shaped
  failures with the same token to cover replication lag (classified
  via fabro_sandbox::classify_failure), and reports one check per
  repository in deterministic primary-first order under bounded
  concurrency.
- A resolved run environment that defines GH_TOKEN produces a warning
  (gh prefers it over the managed token) without failing preflight.
- With no additional repositories declared, the primary-only mint
  check is byte-for-byte unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 14:59:45 -04:00
Bryan Helmkamp
d8edd410f3
feat(workflow): bridge git and gh to the shared token
Carry the resolved GitHub integration (permissions plus declared
additional repositories) as one value from run materialization into
workflow startup, and make the sandbox environment reach every declared
repository through the single managed GITHUB_TOKEN.

- `StartServices.github_permissions` becomes
  `github_integration: ResolvedGithubIntegration`; CLI and server
  workers build it with `resolve_integration()` after interpolation and
  pass it through `SandboxEnvSpec` as one unit.
- `build_sandbox_env` constructs the validated
  `GitHubRepositoryAccess` and scopes the App token source to the whole
  effective set. Missing credentials or a missing origin are hard
  initialization errors when additional repositories are declared;
  legacy permissions-only configuration keeps its best-effort behavior.
- When additional repositories are declared, initialization eagerly
  resolves each repository's App installation (naming any repository
  the App cannot see) and the token itself, so an inaccessible declared
  repository fails before the first workflow stage.
- A new `git_bridge` module injects secret-free `GIT_CONFIG_*` entries
  into the stage environment: a github.com credential helper that reads
  `$GITHUB_TOKEN` at invocation time, per-repository SSH-to-HTTPS
  `insteadOf` rewrites, and `GIT_TERMINAL_PROMPT=0`. Entries append
  after a valid user-provided Git config overlay and fail clearly on a
  malformed one. Contract tests drive the installed git binary against
  local fixtures for the rewrite, credential, prefix-collision, and
  overlay-preservation behaviors.
- The long-running ACP notice now says all declared repository access
  expires together.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 14:47:31 -04:00
Bryan Helmkamp
f2047ad9a9
feat(config): add validated additional github repositories
Add `additional_repositories` to `[run.integrations.github]`: a list of
full `owner/repository` slugs, beyond the implicit run origin, that the
minted GITHUB_TOKEN must cover.

- `GitHubRepositorySlug` gains FromStr, Display, string serde, and
  case-insensitive Eq/Ord/Hash identity while preserving the submitted
  spelling for display and serialization.
- The config layer keeps raw strings; the higher-precedence list
  replaces the lower one wholesale, with `[]` as an explicit clear,
  resolving independently from the `permissions` map.
- Resolution validates each entry with indexed error paths: slug
  grammar, case-insensitive duplicates, one shared owner, the
  499-repository cap, and a required `contents = "read"|"write"`
  permission (templated values are re-checked at the runtime boundary).
- `RunIntegrationsGithubSettings` resolves permissions and repositories
  together through `resolve_integration()` so consumers cannot pick up
  one without the other; the field is omitted from serialization when
  empty, keeping single-repository settings byte-identical.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 14:15:46 -04:00
Bryan Helmkamp
7de3b409ed
Request Packages read permission in the GitHub App manifest
Fabro can mint a scoped sandbox GITHUB_TOKEN via
[run.integrations.github.permissions], but apps registered through the
manifest flow could not grant packages = "read" because the manifest
never requested it. Add Packages (read-only) so freshly registered apps
can download private GitHub Packages (for example npm registry
dependencies) inside sandboxes, mirroring how GitHub Actions workflows
use their built-in GITHUB_TOKEN for registry reads.

Existing apps still need the permission added manually in the app's
settings, as the docs already describe.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 14:12:32 -04:00
Scott Werner
3872c04430 Derive initial refresh token session state
Replace the public stored-token row with an initial-token input that carries only token-specific facts. Bind the token to the session and initialize it as unused inside AuthSessionStore so callers cannot create mismatched session/token rows.
2026-08-21 14:01:30 -04:00
Scott Werner
7edef76d77 Revoke replayed auth sessions transactionally
Delete the owning auth session inside the refresh-token rotation transaction when a spent token is replayed. Return the replay outcome only after the revocation commits, and propagate database failures without claiming the chain was revoked.
2026-08-21 13:55:41 -04:00
Scott Werner
a08dff1ce8 Merge main into feat/refresh-tokens-sqlite 2026-08-21 13:16:14 -04:00
Scott Werner
9d3aa7a4d4 Consolidate workflow-version lineage test coverage
The lineage field's `skip_serializing_if` behavior was asserted five times
across three crates. Keep the two assertions in fabro-types, which owns the
attribute, and drop the duplicates:

- Delete `run_created_omits_absent_workflow_version_id` from event/convert.rs,
  a copy of the test above it that re-checked another crate's serde attribute.
  convert.rs's own responsibility is covered by the existing field assertion.
- Delete `legacy_create_input_persists_without_workflow_version_id`, which ran
  the full create() pipeline to prove a hardcoded `None` literal is `None`.
  `CreateRunInput` has no such field, so no input could change the result.
- Fold `run_spec_omits_absent_workflow_version_id` into the adjacent legacy-spec
  test, which already holds an all-`None` record.
- Drop the off-topic spec re-serialization from run_state.rs's retried_from test.

Add `test_support::test_workflow_version_id()` alongside `test_run_provenance()`
and use it everywhere, replacing eight copies of the same magic seed across five
crates plus two assertion sites that recomputed the hash inline. This also
subsumes retry.rs's private helper of the same shape.

Revert the `run_spec_json` parameterization in the projection round-trip test:
`RunProjection` is a `with_replacement` alias for the canonical type, so the
`Some` and `None` call sites exercise identical code.

Have the two run.created literals that mirror a `RunSpec` read the spec's
lineage field instead of hardcoding `None`, so the mirrors stay accurate once a
producer populates it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 12:39:02 -04:00
Scott Werner
27fd48c603 Persist workflow version lineage on runs 2026-08-21 12:39:02 -04:00
Scott Werner
75fa8eca8b Merge remote-tracking branch 'origin/main' into codex/exact-target-checkout
# Conflicts:
#	lib/components/fabro-sandbox/src/clone_retry.rs
#	lib/components/fabro-sandbox/src/daytona/mod.rs
#	lib/components/fabro-sandbox/src/docker.rs
#	lib/components/fabro-sandbox/src/provider/docker.rs
2026-08-21 12:15:59 -04:00
Bryan Helmkamp
e89f03b316
Fix flaky run id vs variable timestamp assertion
RunId is a ULID, so its embedded timestamp is truncated to whole
milliseconds, while Variable.updated_at comes from Utc::now() with
sub-millisecond precision. When the variable write and the run creation
landed in the same millisecond, the run id compared as earlier and the
assertion failed. Truncate the variable timestamp to milliseconds so
both sides use the same precision.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 06:21:33 -04:00
Bryan Helmkamp
66dc30424a
Merge pull request #729 from fabro-sh/fix/doctor-timeout-budget
Bound doctor diagnostics within client timeout
2026-08-20 21:58:22 -04:00