codai docs
Resolve

How it works

The Resolve job lifecycle from triage to attestation, what the public proof document actually proves, and what the sandboxed runner will and will not touch.

A Resolve job is a small state machine with one hard rule at the end: the charge cannot exist without the attestation. Everything below is how a job gets there — or doesn't.

Job lifecycle

 POST /v1/resolve/jobs
        │
        ▼
     triage ──── decline ───▶ declined        ($0, final)
        │
   quote t7|t19|t49
        │
        ├── free tier left ──▶ accepted   (auto)
        │
        ▼
     quoted ── POST …/accept ──▶ accepted
                                    │
                                    ▼
                                 running ──▶ resolved   (attestation + charge)
                                    │
                                    ├──────▶ failed     ($0 — no verified fix within budget)
                                    └──────▶ error      ($0 — infrastructure; safe to resubmit)
StatusMeaningBilled?
triageTransient — the issue is being read. You never see this in a response.—
declinedTriage judged a verified fix unlikely (wrong stack, no clear repro, needs credentials…). decline_reason says why.No
quotedA flat price was set (tier, quote_micro_usd). Nothing runs until you accept.Not yet
acceptedYou accepted (or the free tier applied). Queued for a runner.Not yet
runningThe sandbox is cloning, reproducing, fixing, verifying.Not yet
resolvedRepro flipped fail → pass, regression suite green, attestation written. attestation_url is set.Yes — the quote
failedNo candidate passed both gates within the turn and time budget. fail_reason explains.No
errorEnvironment or infrastructure problem (broken clone, sandbox unavailable, worker lost). Resubmit freely.No

Statuses only move forward; resolved, failed, error and declined are terminal. GET /v1/resolve/jobs/{id} is scoped to the key's user — another user's job id is a 404.

Triage

Runs inline in your POST, against the codai model, and answers with exactly one line: QUOTE t7|t19|t49 <reason> or DECLINE <reason>. Rough meaning of the tiers: t7 — small, well-scoped (single file, clear repro); t19 — moderate (multi-file, needs investigation); t49 — hard but plausible (subtle, cross-cutting). If triage itself is unavailable the job is declined with triage unavailable, please retry — Resolve never quotes blind.

The free tier

Each user gets 3 free verified fixes. The claim is atomic: when a quotable job comes in and you still have a free slot, the job is created as tier: "free", quote_micro_usd: 0, status: "accepted" and starts immediately. free_tier_remaining in the response tells you how many are left after this one. A free job that ends failed or error gives the slot back.

Accept

Only quoted jobs accept; anything else is 409 job is <status>, only quoted jobs can be accepted. Accepting is the moment you agree to the quote — but you still pay only if it resolves.

Running

Reproduce first

The runner clones the repo at HEAD (recorded as base_commit). If you supplied test_command, it runs that: it must fail on the base commit, otherwise the job ends with supplied test_command already passes on HEAD — nothing to fix. Without a command, the agent writes a repro test — a single pytest / vitest / jest file — and confirms it fails. That failing output becomes test_output_before.

Fix

The agent gets the issue, the repo and the failing test, and edits code through a bounded loop of at most 4 turns per candidate (SEARCH/REPLACE edits plus file reads). Paid tiers fan out up to 4 candidates (the first at temperature 0, the rest at 0.8) and stop at the first one that passes; the free tier runs one. A candidate passes only if the repro now passes and the repository's own suite still passes.

Verify

Verification never fails open. Two commands run in the sandbox on base_commit + patch: the FAIL_TO_PASS command (test_command) and a PASS_TO_PASS regression command (regression_command) — the repo's full suite with the repro file excluded. Environment breakage, timeouts or a runner that vanished are error, never resolved.

Attest, then charge

The attestation row is written, then — and only then — the resolve_charges row for the quoted amount. If anything between the two fails, you have a proof and no bill, never the reverse.

What the attestation proves

GET /v1/resolve/attestations/{id} needs no authentication: the UUID is the capability. Anyone with the link can audit the fix. It returns:

{
  "id": "a45764d8-…",
  "kind": "codai.resolve.attestation.v0",
  "repo_url": "https://github.com/owner/repo",
  "base_commit": "3f9c…",
  "patch": "diff --git a/src/parse.ts b/src/parse.ts\n…",
  "repro_test": { "path": "tests/test_issue_42.py", "content": "…" },
  "test_command": "python -m pytest -q tests/test_issue_42.py",
  "regression_command": "python -m pytest -q --deselect tests/test_issue_42.py --ignore=tests/test_issue_42.py",
  "test_output_before": "… 1 failed …",
  "test_output_after": "… 1 passed …",
  "regression_output": "… 212 passed …",
  "runner_image_digest": "sha256:…",
  "started_at": "2026-09-23T09:12:04.118Z",
  "verified_at": "2026-09-23T09:19:41.402Z"
}
FieldWhat it lets you check
repo_url + base_commitThe exact tree the fix was verified against. git checkout <base_commit> and reproduce it yourself.
patchThe complete unified diff. Nothing outside it was changed.
repro_testThe test that encodes the bug (null when you supplied test_command). Read it to judge whether it captures the issue.
test_command / test_output_beforeThe FAIL_TO_PASS command and its output on the base commit — the "before".
test_output_afterThe same command after the patch — the "after".
regression_command / regression_outputThe repo's own suite, still green.
runner_image_digestThe container image the tests ran in, pinned by digest.
started_at / verified_atWall-clock window of the attempt.

What it does not prove: that the repro test is the right test for the issue, or that the fix is the one you would have written. That is what the diff and the test content are there for — read them like a PR, with the advantage that the execution evidence is attached.

The attestation is immutable and stays public; the same URL is what the GitHub Action links from the PR.

Signed attestations

Since 2026-09-27 every new attestation also carries signature: a DSSE v1 envelope signed with an ECDSA P-256 / SHA-256 key held in Cloud KMS. You do not have to trust the JSON we serve — check it against the signature.

What is signed is a canonical statement (RFC 8785 JSON, payloadType application/vnd.codai.resolve.attestation+json;v=1) that binds every field above, bodies by their sha256 (lowercase hex of the UTF-8 bytes):

{
   "kind": "codai.resolve.attestation.v1",
   "attestation_id": "a45764d8-…",
   "repo_url": "https://github.com/owner/repo",
   "base_commit": "3f9c…",
   "patch_sha256": "…",
   "repro_test": { "path": "tests/test_issue_42.py", "sha256": "…" },
   "test_command": "python -m pytest -q tests/test_issue_42.py",
   "regression_command": "python -m pytest -q …",
   "test_output_before_sha256": "…",
   "test_output_after_sha256": "…",
   "regression_output_sha256": "…",
   "runner_image_digest": "sha256:…",
   "started_at": "2026-09-23T09:12:04.118Z",
   "verified_at": "2026-09-23T09:19:41.402Z"
}

repro_test is null when you supplied test_command; a missing regression run gives null for both regression fields. The envelope is { payloadType, payload: base64(statement), signatures: [{ keyid, sig }] }, and sig is a DER signature (base64) over the DSSE PAE: "DSSEv1 " + len(payloadType) + " " + payloadType + " " + len(payload) + " " + payload (byte lengths).

Where the key is: GET /v1/resolve/keys (public, CORS-open, cached 1 h) returns { keys: [{ keyid, algorithm: "ECDSA_P256_SHA256", payload_type, encoding, public_key_pem }] }. Pin the PEM if you want verification that does not depend on our servers at all.

A valid attestation passes both checks: the signature verifies with a published key, and the decoded statement equals the statement recomputed from the attestation's own fields. A valid signature over different content is a failure (STATEMENT_MISMATCH).

How to verify:

codaid resolve verify a45764d8-…        # or the full attestation URL; no API key needed
# VERIFIED (key projects/…/cryptoKeyVersions/1)   — exit 0
# NOT VERIFIED: STATEMENT_MISMATCH                — exit 1
const proof = await codai.resolve.attestation(job.attestation_url!);
const r = await codai.resolve.verifyAttestation(proof); // { keys } to pin
// { ok: true, keyid } | { ok: false, reason: 'UNSIGNED' | 'BAD_SIGNATURE' | 'STATEMENT_MISMATCH' | 'BAD_PAYLOAD_TYPE' }
proof = client.resolve.attestation(job["attestation_url"])
r = client.resolve.verify_attestation(proof)  # needs `pip install cryptography` for the signature step

Attestations created before 2026-09-27 are unsigned (signature: null) and verify as UNSIGNED; they are still valid execution evidence, just not cryptographically bound.

Webhooks

Instead of polling GET /v1/resolve/jobs/{id}, pass callback_url on intake — an https URL on a public host (no credentials in the URL, no localhost / private / link-local / metadata addresses, at most 2000 characters). The 201 quote or 200 decline response then carries callback_secret (whsec_…). It is returned once and never shown again — store it with the job id.

{ "repo_url": "https://github.com/owner/repo", "issue_url": "https://github.com/owner/repo/issues/42",
   "callback_url": "https://ci.example.com/hooks/codai-resolve" }

Events

EventSent when
job.quotedA paid quote is ready — call /accept.
job.declinedTriage declined; decline_reason is set.
job.resolvedVerified fix; attestation_id and an absolute attestation_url are set.
job.failedNo verified fix within budget; the wallet hold is refunded.
job.errorInfrastructure failure; the hold is refunded, resubmit freely.

Each delivery is a POST with content-type: application/json and these headers:

HeaderValue
x-codai-eventThe event name, same as event in the body.
x-codai-delivery<jobId>:<event> — identical on every retry; use it to deduplicate.
x-codai-signaturet=<unix seconds>,v1=<hex> where v1 = HMAC-SHA256(callback_secret, "<t>.<raw body>").
{
   "event": "job.resolved",
   "job": {
      "id": "6f1d2c3e-…", "status": "resolved", "tier": "t19", "quote_micro_usd": 19000000,
      "repo_url": "https://github.com/owner/repo", "decline_reason": null, "fail_reason": null,
      "cost_micro_usd": 1830000, "attestation_id": "a45764d8-…",
      "attestation_url": "https://resolve.codai.ro/v1/resolve/attestations/a45764d8-…",
      "completed_at": "2026-09-27T09:19:41.402Z"
   },
   "sent_at": "2026-09-27T09:19:42.010Z"
}

Verify the signature

Verify over the raw request body, before parsing JSON; the SDK helpers compare in constant time and reject a timestamp more than 300 seconds away from now.

import { verifyResolveWebhook, type ResolveWebhookEvent } from 'codai-sdk';

export async function POST(req: Request) {
   const raw = await req.text();
   if (!verifyResolveWebhook(process.env.RESOLVE_WHSEC!, raw, req.headers.get('x-codai-signature'))) {
      return new Response('bad signature', { status: 401 });
   }
   const evt = JSON.parse(raw) as ResolveWebhookEvent;
   if (evt.event === 'job.resolved') console.log(evt.job.attestation_url);
   return new Response(null, { status: 204 });
}
import json, os
from codai import verify_resolve_webhook

@app.post("/hooks/codai-resolve")  # FastAPI
async def resolve_hook(request: Request):
      raw = await request.body()
      if not verify_resolve_webhook(os.environ["RESOLVE_WHSEC"], raw, request.headers.get("x-codai-signature")):
            raise HTTPException(401, "bad signature")
      evt = json.loads(raw)
      return {"ok": True}

Delivery semantics

  • Up to 3 attempts per event (backoff 1 s, then 4 s) on a 5xx, a 429 or a network error; any other non-2xx stops retrying. Each attempt times out after 10 s. Answer 2xx quickly and do the work asynchronously.
  • Redirects are not followed, and every DNS answer for the host is re-checked to be public at delivery time.
  • Delivery never changes job state or billing — a dead endpoint does not fail a job. GET /v1/resolve/jobs/{id} stays the source of truth; poll it if you missed an event.

Safety: what the runner touches

Resolve runs untrusted repository code — your tests, your install scripts — so the execution model is deliberately narrow.

  • Sandboxed execution. Tests, setup and the fix candidates run in a separate, powerless Cloud Run job (codai-swe-exec) with no database access and no gateway credentials; results come back as artifacts. The API process itself only clones the repo, reads files and builds diffs. In production the API refuses to run repository code in-process — a misconfiguration turns into error, never into code running beside secrets.
  • Public repos, read-only. Resolve fetches https://github.com/<owner>/<repo> anonymously. It never pushes; the GitHub Action does that with your token in your workflow.
  • Diff-only changes. The patch in the attestation is the full extent of what changed. The agent edits via SEARCH/REPLACE blocks; it cannot run arbitrary shell in the API process.
  • Bounded budgets. At most 4 agent turns per candidate, at most 4 candidates, a per-job timeout of 30 minutes with a sandbox run limit of 15 minutes each, and an orphan watchdog that marks a job error (and returns a free slot) if its worker disappears.
  • Nothing hidden. The model calls happen on the codai gateway under a session id resolve-<jobId>-…, so the upstream cost per job is auditable in usage_events like any other request.

Private beta means an honest limits list: public github.com repos only; Python (pytest) and JavaScript/TypeScript (vitest, jest, node:test); repos whose tests need secrets, services or non-trivial system setup will usually be declined or end in error.

Last updated:

On this page