# How it works (/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 [#job-lifecycle]

```text
 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)
```

| Status | Meaning | Billed? |
| --- | --- | --- |
| `triage` | Transient — the issue is being read. You never see this in a response. | — |
| `declined` | Triage judged a verified fix unlikely (wrong stack, no clear repro, needs credentials…). `decline_reason` says why. | No |
| `quoted` | A flat price was set (`tier`, `quote_micro_usd`). Nothing runs until you accept. | Not yet |
| `accepted` | You accepted (or the free tier applied). Queued for a runner. | Not yet |
| `running` | The sandbox is cloning, reproducing, fixing, verifying. | Not yet |
| `resolved` | Repro flipped fail → pass, regression suite green, attestation written. `attestation_url` is set. | **Yes** — the quote |
| `failed` | No candidate passed both gates within the turn and time budget. `fail_reason` explains. | No |
| `error` | Environment 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 [#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 [#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 [#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 [#running]

<Steps>
<Step>

### Reproduce first [#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`.

</Step>
<Step>

### Fix [#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.

</Step>
<Step>

### Verify [#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`.

</Step>
<Step>

### Attest, then charge [#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.

</Step>
</Steps>

## What the attestation proves [#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:

```json
{
  "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"
}
```

| Field | What it lets you check |
| --- | --- |
| `repo_url` + `base_commit` | The exact tree the fix was verified against. `git checkout <base_commit>` and reproduce it yourself. |
| `patch` | The complete unified diff. Nothing outside it was changed. |
| `repro_test` | The test that encodes the bug (`null` when you supplied `test_command`). Read it to judge whether it captures the issue. |
| `test_command` / `test_output_before` | The FAIL_TO_PASS command and its output on the base commit — the "before". |
| `test_output_after` | The same command after the patch — the "after". |
| `regression_command` / `regression_output` | The repo's own suite, still green. |
| `runner_image_digest` | The container image the tests ran in, pinned by digest. |
| `started_at` / `verified_at` | Wall-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 [#signed-attestations]

Since 2026-09-27 every new attestation also carries `signature`: a [DSSE v1](https://github.com/secure-systems-lab/dsse) 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):

```json
{
   "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:**

```sh
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
```

```ts
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' }
```

```python
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 [#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.

```json
{ "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 [#events]

| Event | Sent when |
| --- | --- |
| `job.quoted` | A paid quote is ready — call `/accept`. |
| `job.declined` | Triage declined; `decline_reason` is set. |
| `job.resolved` | Verified fix; `attestation_id` and an absolute `attestation_url` are set. |
| `job.failed` | No verified fix within budget; the wallet hold is refunded. |
| `job.error` | Infrastructure failure; the hold is refunded, resubmit freely. |

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

| Header | Value |
| --- | --- |
| `x-codai-event` | The event name, same as `event` in the body. |
| `x-codai-delivery` | `<jobId>:<event>` — identical on every retry; use it to deduplicate. |
| `x-codai-signature` | `t=<unix seconds>,v1=<hex>` where `v1 = HMAC-SHA256(callback_secret, "<t>.<raw body>")`. |

```json
{
   "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-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.

```ts
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 });
}
```

```python
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 [#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 [#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.

<Callout type="warn">
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`.
</Callout>

<Cards>
  <Card title="Pricing and limits" href="./pricing-and-limits.mdx" description="Tiers, free fixes, caps, how the wallet hold is kept or refunded." />
  <Card title="API reference" href="../api/resolve.mdx" description="Field-level reference and playground." />
</Cards>
