Depot CI: repo-bound failure inspection and failed-only retry
Coding children need the logs behind Depot check links without a person relaying them. The organization credential grants much more than CI reads: it must remain at the credential-holding Worker, outside both the bot host and model-controlled containers.
- Code:
src/core/depotCi.ts,src/execution/depotCi.ts,src/execution/depotCiAuthorization.ts,src/index.ts,src/tools/depotCi.ts,src/tools/toolsets.ts,src/tools/runnableTool.ts,src/core/dispatch/runLoop.ts,deploy/cloudflare/depotCi.ts,deploy/cloudflare/worker.ts,deploy/secrets.manifest.json. - Tests:
src/execution/depotCi.test.ts,src/execution/depotCiAuthorization.test.ts,src/tools/depotCi.test.ts,deploy/cloudflare/depotCi.test.ts,src/core/secretsManifest.test.ts,src/core/dispatch/runLoop.test.ts.
Behavior
- One narrow bridge, not a CLI or token tool. The bot Worker's
POST /internal/depot-ciauthenticatesDEPOT_CI_BRIDGE_TOKENand accepts only a one-use ticket forinspect,logs, orretry_failed. The bearer alone is insufficient authority. Only the Worker holdsDEPOT_API_TOKEN(an organization token); it is explicitly excluded from container forwarding. The bridge bearer reaches the trusted bot, never the coding environment. No Depot CLI installation, arbitrary RPC name, URL, header, organization, workflow content, dispatch, cancel, secret access or full rerun is exposed. Unconfigured credentials fail closed by name. The existing harness relay authenticates the child and supplies its tool context; no new child-facing HTTP ingress is added. - The repository is not a model argument. The dispatcher binds the authenticated run ID, resolved repo and requester's
canUseRepopredicate. All three tools are coding-only (full). Every call checks the live run and predicate before network access, then registers a random 256-bit one-use permit for that exact operation (including workflow, job/attempt and retry head). The bridge request carries only the ticket, never caller-supplied run/repo/operation authority. Before using the Depot credential, the Worker consumes the permit over its fixed singleton-container binding atPOST /internal/depot-ci/authorization, validates the returned run/repo/operation and derives its upstream request solely from that grant. The callback rechecks live-run/repo authorization and consumes even a refused permit; wrong, revoked, aborted, expired, completed and replayed permits fail closed. The public Worker refuses the callback path; it exposes no mint/list endpoint. Permits expire with the one-minute operation, are removed on completion/abort, and deliberately vanish on bot restart: they are transient in-flight authorizations, not durable run state or permission to replay an unknown mutation. The Worker readsGetWorkflowand verifies its repository against the independently resolved binding before returning metadata, reading logs or writing. A foreign workflow is a generic refusal, not a metadata disclosure.logsalso proves job and attempt membership from that workflow. Unknown or malformed identities refuse rather than guessing. A Depot check URL supplies only its workflow ID; the URL is never fetched. - Finite, sanitized evidence.
inspectreturns workflow/run identity, exact SHAs, status, errors, jobs and attempts.logsreads an explicitly selected attempt or the highest numbered attempt of the selected job, usingGetJobAttemptLogs. Pagination stays pinned to that attempt; at most 20 pages of at most 2 MiB each are read under one one-minute operation deadline. Raw capture additionally stops at 4,194,304 characters including record separators, bounding Worker memory. Redaction processes the entire capture before any tail clipping, not each page separately. It compares newline-preserving and newline-free redaction so credentials, assignments, ANSI sequences and private-key blocks split across records/pages cannot leak a fragment. An ambiguous split withholds the whole capture withwithheld: "credential spans log records"; hitting a page/aggregate ceiling or pagination loop withholds it withwithheld: "incomplete capture", since unread text could finish a secret. Withheld responses contain emptytextandtruncated: true; they are not evidence that a job produced no logs. Otherwise output keeps a bounded tail (default 200, at most 2000 lines and 50,000 characters), with explicit completeness/truncation markers. ANSI, recognized secrets, exact configured credentials and the request ticket are removed before output is clipped; arbitrary upstream error bodies and transport exceptions are never returned. The upstream host and RPC paths are fixed, redirects are refused, and response bodies are byte-bounded. Logs are untrusted data, never instructions. - Failed-only has Depot's precise meaning.
retry_failedrequires the full expected head SHA and a failed/cancelled workflow belonging to the bound repo. The head isheadShawhen present, otherwisesha. It calls onlyRetryFailedJobs({workflowId}), once, and reports returned job IDs/count. Depot retries failed/cancelled jobs plus their skipped dependants; successful jobs remain untouched. It does not callRerunWorkflow, GitHub check rerequest or Actions retry. No automatic retry follows any upstream failure; a lost/invalid mutation response reports an unknown outcome and requires inspection before another request. Even HTTP 200 from the bridge requires a complete normalized acknowledgement: the requested workflow ID, an array of valid job IDs, and a nonnegative integer count equal to its length, with no extra fields. A missing, malformed, inconsistent or foreign-workflow acknowledgement is an unknown outcome, never success. This is not an exactly-once mutation guarantee across separate tool calls. - Auth and API contract. Depot's authentication matrix allows user and organization tokens for CI, not project/pull tokens; project-scoped OIDC is not a CI credential. The API authentication uses an organization bearer. The CI API is Connect JSON at
https://api.depot.dev/depot.ci.v1.CIService/, withContent-Type: application/jsonandConnect-Protocol-Version: 1. The CLI equivalent isdepot ci retry <run-id> --failed --workflow <workflow-id>;--failedand--jobare mutually exclusive. These are documentation-backed contracts, not evidence of successful live credential provisioning.
Provisioning and live acceptance
Provision DEPOT_API_TOKEN and a separately minted DEPOT_CI_BRIDGE_TOKEN with the existing deploy secrets bot --only path. The former stays in the Worker; only the latter is forwarded to the bot. PUBLIC_BASE_URL identifies that Worker's HTTPS origin. Do not put the organization token in a bot .env, agent environment bootstrap, resident secret, container image or shell. Deploy the Worker and bot from the same reviewed release, then restart through the normal human-controlled release path. A mixed-version bridge fails closed: the old repo-bearing body is no longer accepted. Without this configuration the tools name the missing capability rather than suggesting a shell/token workaround.
After deployment, start a coding child on a repo with a real failed Depot check. Record its exact Slack permalink, routed preset/card, Ship unit/ref, deployed commit, check link and head in the operator's tracker. In the child's own timeline, call depot_ci_inspect on the check URL, then depot_ci_logs on the failed job. The child must identify the actual failed test/step from returned log text, without human-pasted logs. Request depot_ci_retry_failed with the observed full head; observe new attempts for the returned jobs and unchanged successful attempts. Confirm a foreign-repo workflow, foreign job/attempt and mismatched SHA are refused. Record the receipt outside this repository; fixture tests cannot substitute for this deployment-gated proof.
Live proof status: unproven, human-gated. No real-child diagnosis/retry receipt is supplied by the fixture tests. The gate stays open until the reviewed release is deployed with edge-only credentials and the procedure above produces the child's actual check/head, log diagnosis and observed failed-only retry result. Do not provision the organization credential in a bot or coding shell to bypass this gate.
Validation criteria
| Criterion | Proof |
|---|---|
| Edge requires the bridge bearer plus an independently consumed run/repo/operation permit, refuses authority overrides/replay, binds workflow/job/attempt to repo, and never returns foreign metadata | [unit] deploy/cloudflare/depotCi.test.ts::Depot CI edge::* |
| Upstream uses fixed Connect paths, no redirects, bounded/redacted logs, exact-head failed-only retry, and honest unknown mutation outcomes | [unit] deploy/cloudflare/depotCi.test.ts::Depot CI edge::* |
| An HTTP 200 RetryFailedJobs acknowledgement missing jobIds or jobCount is an unknown outcome, with no replay | [unit] deploy/cloudflare/depotCi.test.ts::Depot CI edge::treats incomplete upstream retry acknowledgements as unknown outcomes |
| One-use permits preserve immutable bindings, recheck authority, disappear on abort/completion/restart, and cannot be minted over HTTP | [unit] src/execution/depotCiAuthorization.test.ts::Depot CI one-use authorization::* |
| Bot bridge binds repo outside model input, rechecks authorization, strips error detail, and never needs the Depot token | [unit] src/execution/depotCi.test.ts::Depot CI bridge::* |
| Malformed HTTP 200 retry acknowledgements report unknown outcome without retrying or retaining a permit | [unit] src/execution/depotCi.test.ts::Depot CI bridge::treats malformed HTTP 200 retry acknowledgements as unknown without replay… |
| A Ship coding child receives failure text through the authenticated harness relay with its resolved repository, no credential in the evidence | [unit] src/core/dispatch/runLoop.test.ts::runLoop — the model turn and everything that rides on it::a coding child reads Depot failure evidence through its repo-bound Worker bridge |
| Coding-only tools accept check links, refuse broad inputs, expose finite inspection/logs and failed-only retry | [unit] src/tools/depotCi.test.ts::Depot CI tools::* |
| Organization token is Worker-only; bridge bearer alone reaches the bot | [unit] src/core/secretsManifest.test.ts::deploy/secrets.manifest.json::Depot credentials stay at the edge and only the bridge bearer reaches the bot |
| A real coding child diagnoses a failed Depot check and retries only failed/cancelled jobs and skipped dependants | [gap] Follow the live acceptance procedure above; report deployment or credential blockers explicitly. Unproven until a real receipt is recorded. |