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>
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>
The release, docker, and Homebrew jobs ran on ubuntu-latest, which
migrates across Ubuntu major versions on GitHub's schedule. Pin to
ubuntu-24.04, the image ubuntu-latest resolved to in the last green
release run, matching the explicit runner labels used elsewhere.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
setup-bun installed the latest Bun at run time, so every job floated to
new Bun releases the day they shipped. Bun bundles the SPA embedded in
release binaries, so an unvetted Bun release could break or silently
change shipped artifacts. Pin to 1.3.14, the version the last green
nightly used, and hold off on the day-old 1.4.0 until it has soaked.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rust 1.98.0 (released 2026-08-20) passes --fix-cortex-a53-843419 to the
linker for aarch64-unknown-linux-musl, which the zig cc wrapper used by
cargo-zigbuild rejects, breaking the release build for that target. Pin
all workflows that installed unpinned stable to 1.97.1 until the zig
toolchain handles the new flag. The nightly-2026-04-14 fmt/clippy
toolchains are unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
Sandbox exec logs previously required command_len fingerprinting to tell a
push from a credential refresh or a checkpoint commit. The shared git
helpers now instrument their futures with a git_op span, so Daytona's and
Docker's `exec_command: entered` lines inherit the operation label and the
log renders as `git_op{op=push}: exec_command: entered timeout_ms=...`.
Ops: push (git_push_via_exec), refresh-credentials (both providers'
refresh_push_credentials), checkpoint-commit (checked_git_checkpoint),
fetch (fetch_source_run_ref), and metadata-push (the run-metadata snapshot
write). Spans are attached with #[tracing::instrument] — attached to the
future, never an entered() guard held across an await — so they follow the
task across worker threads. No trait or signature changes.
Plan: .ai/plans/git-push-token-resilience.md (PR 3: item 10).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A Daytona "Sandbox state change in progress" rejection surfacing
through the pipeline lifecycle path ("Pipeline lifecycle operation
failed") matched no transient-infra hint, so the run failure was
categorized deterministic. The condition is a provider lifecycle
transition that finishes on its own — the definition of transient
infrastructure — and the deterministic label misinforms retry
machinery and anyone reading the failure.
Add two transient-infra hints: the provider rejection ("state change
in progress") and the bounded-wait timeout an activation reports when
a stop transition outlives its budget ("sandbox stop still in
progress").
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Daytona rejects start/stop with HTTP 400 "State change in progress"
while a lifecycle transition is in flight, and activate() only handled
the Started and Starting states: any other state fell through to
start(), which surfaced the rejection as a hard failure. A run died
exactly this way when an inactivity auto-stop began seconds before the
stage finished — activate() saw the sandbox mid-stop and failed the
whole run 35ms later. The cleanup stop() then failed on the same
rejection.
Transitions finish on their own within seconds, so treat them as
wait-and-retry conditions:
- activate() now waits out a Stopping sandbox and dispatches on
whatever state the transition lands on.
- start() and stop() retry the rejected call within a bounded budget,
re-inspecting state between attempts: a transition that lands on
Started needs no further start, and one that lands on Stopped or
Destroyed needs no further stop.
All call sites go through these three provider methods, so no
lifecycle-layer changes are needed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>