codai docs
Resolve

Cum funcționează

Ciclul de viață al unui job Resolve de la triaj la atestare, ce dovedește de fapt documentul public de dovadă și ce atinge și ce nu atinge runner-ul izolat.

Un job Resolve este o mică mașină de stări cu o regulă dură la final: taxa nu poate exista fără atestare. Tot ce urmează este cum ajunge un job acolo — sau nu.

Ciclul de viață al job-ului

 POST /v1/resolve/jobs
        │
        ▼
     triage ──── decline ───▶ declined        (0 $, final)
        │
   quote t7|t19|t49
        │
        ├── nivel gratuit rămas ──▶ accepted   (auto)
        │
        ▼
     quoted ── POST …/accept ──▶ accepted
                                    │
                                    ▼
                                 running ──▶ resolved   (atestare + taxă)
                                    │
                                    ├──────▶ failed     (0 $ — niciun fix verificat în buget)
                                    └──────▶ error      (0 $ — infrastructură; retrimiți fără grijă)
StatusSemnificațieFacturat?
triageTranzitoriu — issue-ul este citit. Nu îl vezi niciodată într-un răspuns.—
declinedTriajul a considerat un fix verificat improbabil (stack greșit, fără reproducere clară, are nevoie de credențiale…). decline_reason spune de ce.Nu
quotedS-a stabilit un preț fix (tier, quote_micro_usd). Nu rulează nimic până nu accepți.Încă nu
acceptedAi acceptat (sau s-a aplicat nivelul gratuit). În coadă pentru un runner.Încă nu
runningSandbox-ul clonează, reproduce, repară, verifică.Încă nu
resolvedReproducerea a trecut de la eșec la succes, suita de regresie e verde, atestarea scrisă. attestation_url este setat.Da — cotația
failedNiciun candidat nu a trecut ambele porți în bugetul de ture și timp. fail_reason explică.Nu
errorProblemă de mediu sau infrastructură (clonă defectă, sandbox indisponibil, worker pierdut). Retrimiți liber.Nu

Statusurile merg doar înainte; resolved, failed, error și declined sunt terminale. GET /v1/resolve/jobs/{id} este limitat la utilizatorul cheii — id-ul de job al altui utilizator este 404.

Triaj

Rulează inline în POST-ul tău, pe modelul codai, și răspunde cu exact o linie: QUOTE t7|t19|t49 <motiv> sau DECLINE <motiv>. Sensul aproximativ al pragurilor: t7 — mic, bine delimitat (un fișier, reproducere clară); t19 — moderat (mai multe fișiere, are nevoie de investigație); t49 — greu dar plauzibil (subtil, transversal). Dacă triajul în sine este indisponibil, job-ul este refuzat cu triage unavailable, please retry — Resolve nu cotează niciodată orb.

Nivelul gratuit

Fiecare utilizator primește 3 remedieri verificate gratuite. Revendicarea este atomică: când sosește un job cotabil și mai ai un loc gratuit, job-ul este creat cu tier: "free", quote_micro_usd: 0, status: "accepted" și pornește imediat. free_tier_remaining din răspuns îți spune câte mai rămân după acesta. Un job gratuit care se termină failed sau error dă locul înapoi.

Acceptare

Doar job-urile quoted acceptă; orice altceva este 409 job is <status>, only quoted jobs can be accepted. Acceptarea este momentul în care ești de acord cu cotația — dar plătești în continuare doar dacă se rezolvă.

Rularea

Reproduce întâi

Runner-ul clonează repo-ul la HEAD (înregistrat ca base_commit). Dacă ai furnizat test_command, îl rulează: trebuie să eșueze pe commit-ul de bază, altfel job-ul se termină cu supplied test_command already passes on HEAD — nothing to fix. Fără comandă, agentul scrie un test de reproducere — un singur fișier pytest / vitest / jest — și confirmă că eșuează. Acel output de eșec devine test_output_before.

Repară

Agentul primește issue-ul, repo-ul și testul care eșuează, și editează cod într-o buclă limitată la cel mult 4 ture per candidat (editări SEARCH/REPLACE plus citiri de fișiere). Pragurile plătite se ramifică în până la 4 candidați (primul la temperatură 0, restul la 0,8) și se opresc la primul care trece; nivelul gratuit rulează unul singur. Un candidat trece doar dacă reproducerea trece acum și suita proprie a repo-ului trece în continuare.

Verifică

Verificarea nu eșuează niciodată permisiv. Două comenzi rulează în sandbox pe base_commit + patch: comanda FAIL_TO_PASS (test_command) și o comandă de regresie PASS_TO_PASS (regression_command) — suita completă a repo-ului cu fișierul de reproducere exclus. Un mediu defect, timeout-urile sau un runner dispărut sunt error, niciodată resolved.

Atestă, apoi taxează

Se scrie rândul de atestare, apoi — și doar atunci — rândul resolve_charges pentru suma cotată. Dacă ceva eșuează între cele două, ai o dovadă și nicio plată, niciodată invers.

Ce dovedește atestarea

GET /v1/resolve/attestations/{id} nu are nevoie de nicio autentificare: UUID-ul este capabilitatea. Oricine are linkul poate audita fix-ul. Returnează:

{
  "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"
}
CâmpCe îți permite să verifici
repo_url + base_commitArborele exact pe care a fost verificat fix-ul. git checkout <base_commit> și reproduci singur.
patchDiff-ul unificat complet. Nimic în afara lui nu a fost schimbat.
repro_testTestul care codifică bug-ul (null când ai furnizat test_command). Îl citești ca să judeci dacă surprinde issue-ul.
test_command / test_output_beforeComanda FAIL_TO_PASS și output-ul ei pe commit-ul de bază — „înainte”.
test_output_afterAceeași comandă după patch — „după”.
regression_command / regression_outputSuita proprie a repo-ului, în continuare verde.
runner_image_digestImaginea de container în care au rulat testele, fixată prin digest.
started_at / verified_atFereastra de timp a încercării.

Ce nu dovedește: că testul de reproducere este testul potrivit pentru issue, sau că fix-ul este cel pe care l-ai fi scris tu. Pentru asta sunt acolo diff-ul și conținutul testului — le citești ca pe un PR, cu avantajul că dovada de execuție este atașată.

Atestarea este imutabilă și rămâne publică; același URL este cel pe care acțiunea GitHub îl leagă din PR.

Atestări semnate

Din 2026-09-27, fiecare atestare nouă conține și signature: un plic DSSE v1 semnat cu o cheie ECDSA P-256 / SHA-256 ținută în Cloud KMS. Nu trebuie să ai încredere în JSON-ul pe care îl servim — îl verifici cu semnătura.

Ce se semnează este o declarație canonică (JSON RFC 8785, payloadType application/vnd.codai.resolve.attestation+json;v=1) care leagă fiecare câmp de mai sus, iar conținuturile lungi prin sha256 (hex cu litere mici al octeților UTF-8):

{
   "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 este null când ai furnizat test_command; dacă nu s-a rulat suita de regresie, ambele câmpuri de regresie sunt null. Plicul este { payloadType, payload: base64(declarație), signatures: [{ keyid, sig }] }, iar sig este o semnătură DER (base64) peste PAE-ul DSSE: "DSSEv1 " + len(payloadType) + " " + payloadType + " " + len(payload) + " " + payload (lungimi în octeți).

Unde e cheia: GET /v1/resolve/keys (public, CORS deschis, cache 1 h) întoarce { keys: [{ keyid, algorithm: "ECDSA_P256_SHA256", payload_type, encoding, public_key_pem }] }. Fixează PEM-ul local dacă vrei o verificare care nu depinde deloc de serverele noastre.

O atestare validă trece ambele verificări: semnătura se verifică cu o cheie publicată și declarația decodată este egală cu declarația recalculată din câmpurile atestării. O semnătură validă peste alt conținut este un eșec (STATEMENT_MISMATCH).

Cum verifici:

codaid resolve verify a45764d8-…        # sau URL-ul complet al atestării; nu e nevoie de cheie API
# VERIFIED (key projects/…/cryptoKeyVersions/1)   — cod de ieșire 0
# NOT VERIFIED: STATEMENT_MISMATCH                — cod de ieșire 1
const proof = await codai.resolve.attestation(job.attestation_url!);
const r = await codai.resolve.verifyAttestation(proof); // { keys } ca să fixezi cheile
// { 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)  # pasul de semnătură cere `pip install cryptography`

Atestările create înainte de 2026-09-27 nu sunt semnate (signature: null) și ies UNSIGNED la verificare; rămân dovezi de execuție valide, doar că nu sunt legate criptografic.

Webhook-uri

În loc să interoghezi GET /v1/resolve/jobs/{id}, trimite callback_url la intake — un URL https pe un host public (fără credențiale în URL, fără localhost / adrese private / link-local / de metadate, maximum 2000 de caractere). Răspunsul 201 cu cotația sau 200 cu refuzul conține atunci callback_secret (whsec_…). Este întors o singură dată și nu mai este afișat niciodată — salvează-l împreună cu id-ul job-ului.

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

Evenimente

EvenimentTrimis când
job.quotedO cotație plătită este gata — apelează /accept.
job.declinedTriajul a refuzat; decline_reason este setat.
job.resolvedFix verificat; attestation_id și un attestation_url absolut sunt setate.
job.failedNiciun fix verificat în buget; suma blocată în portofel este returnată.
job.errorEșec de infrastructură; suma blocată este returnată, poți retrimite liniștit.

Fiecare livrare este un POST cu content-type: application/json și aceste header-e:

HeaderValoare
x-codai-eventNumele evenimentului, identic cu event din body.
x-codai-delivery<jobId>:<event> — identic la fiecare reîncercare; folosește-l pentru deduplicare.
x-codai-signaturet=<secunde unix>,v1=<hex>, unde v1 = HMAC-SHA256(callback_secret, "<t>.<body brut>").
{
   "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"
}

Verificarea semnăturii

Verifică pe body-ul brut al request-ului, înainte de parsarea JSON; helper-ele din SDK compară în timp constant și resping un timestamp aflat la mai mult de 300 de secunde de momentul curent.

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}

Semantica livrării

  • Până la 3 încercări per eveniment (backoff 1 s, apoi 4 s) la 5xx, 429 sau eroare de rețea; orice alt cod non-2xx oprește reîncercările. Fiecare încercare expiră după 10 s. Răspunde rapid cu 2xx și fă procesarea asincron.
  • Redirect-urile nu sunt urmate, iar fiecare răspuns DNS pentru host este reverificat ca public la momentul livrării.
  • Livrarea nu schimbă niciodată starea job-ului sau facturarea — un endpoint mort nu face job-ul să eșueze. GET /v1/resolve/jobs/{id} rămâne sursa de adevăr; interoghează-l dacă ai ratat un eveniment.

Siguranță: ce atinge runner-ul

Resolve rulează cod de repository în care nu are încredere — testele tale, scripturile tale de instalare — așa că modelul de execuție este intenționat îngust.

  • Execuție izolată. Testele, setup-ul și candidații de fix rulează într-un job Cloud Run separat și fără privilegii (codai-swe-exec), fără acces la baza de date și fără credențiale de gateway; rezultatele revin ca artefacte. Procesul API în sine doar clonează repo-ul, citește fișiere și construiește diff-uri. În producție, API-ul refuză să ruleze cod de repository în proces — o configurare greșită devine error, niciodată cod rulând lângă secrete.
  • Repo-uri publice, doar citire. Resolve preia https://github.com/<owner>/<repo> anonim. Nu face niciodată push; acțiunea GitHub face asta cu token-ul tău, în workflow-ul tău.
  • Schimbări doar prin diff. Patch-ul din atestare este întreaga întindere a ceea ce s-a schimbat. Agentul editează prin blocuri SEARCH/REPLACE; nu poate rula shell arbitrar în procesul API.
  • Bugete limitate. Cel mult 4 ture de agent per candidat, cel mult 4 candidați, un timeout per job de 30 de minute cu o limită de 15 minute per rulare în sandbox, și un watchdog de orfani care marchează un job error (și returnează un loc gratuit) dacă worker-ul lui dispare.
  • Nimic ascuns. Apelurile către model se fac pe gateway-ul codai sub un id de sesiune resolve-<jobId>-…, așa că costul upstream per job este auditabil în usage_events ca orice altă cerere.

Beta privat înseamnă o listă onestă de limite: doar repo-uri publice github.com; Python (pytest) și JavaScript/TypeScript (vitest, jest, node:test); repo-urile ale căror teste au nevoie de secrete, servicii sau setup de sistem netrivial vor fi de regulă refuzate sau se termină în error.

Ultima actualizare:

Pe această pagină