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ă)| Status | Semnificație | Facturat? |
|---|---|---|
triage | Tranzitoriu — issue-ul este citit. Nu îl vezi niciodată într-un răspuns. | — |
declined | Triajul a considerat un fix verificat improbabil (stack greșit, fără reproducere clară, are nevoie de credențiale…). decline_reason spune de ce. | Nu |
quoted | S-a stabilit un preț fix (tier, quote_micro_usd). Nu rulează nimic până nu accepți. | Încă nu |
accepted | Ai acceptat (sau s-a aplicat nivelul gratuit). În coadă pentru un runner. | Încă nu |
running | Sandbox-ul clonează, reproduce, repară, verifică. | Încă nu |
resolved | Reproducerea a trecut de la eșec la succes, suita de regresie e verde, atestarea scrisă. attestation_url este setat. | Da — cotația |
failed | Niciun candidat nu a trecut ambele porți în bugetul de ture și timp. fail_reason explică. | Nu |
error | Problemă 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âmp | Ce îți permite să verifici |
|---|---|
repo_url + base_commit | Arborele exact pe care a fost verificat fix-ul. git checkout <base_commit> și reproduci singur. |
patch | Diff-ul unificat complet. Nimic în afara lui nu a fost schimbat. |
repro_test | Testul 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_before | Comanda FAIL_TO_PASS și output-ul ei pe commit-ul de bază — „înainte”. |
test_output_after | Aceeași comandă după patch — „după”. |
regression_command / regression_output | Suita proprie a repo-ului, în continuare verde. |
runner_image_digest | Imaginea de container în care au rulat testele, fixată prin digest. |
started_at / verified_at | Fereastra 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 1const 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
| Eveniment | Trimis când |
|---|---|
job.quoted | O cotație plătită este gata — apelează /accept. |
job.declined | Triajul a refuzat; decline_reason este setat. |
job.resolved | Fix verificat; attestation_id și un attestation_url absolut sunt setate. |
job.failed | Niciun fix verificat în buget; suma blocată în portofel este returnată. |
job.error | Eș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:
| Header | Valoare |
|---|---|
x-codai-event | Numele evenimentului, identic cu event din body. |
x-codai-delivery | <jobId>:<event> — identic la fiecare reîncercare; folosește-l pentru deduplicare. |
x-codai-signature | t=<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,429sau eroare de rețea; orice alt cod non-2xx oprește reîncercările. Fiecare încercare expiră după 10 s. Răspunde rapid cu2xxș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ă devineerror, 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
codaisub un id de sesiuneresolve-<jobId>-…, așa că costul upstream per job este auditabil înusage_eventsca 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:
Acțiunea GitHub
Instalezi acțiunea Codai Resolve — etichetezi un issue codai-fix și primești un PR cu un fix verificat și o atestare publică. Intrări, ieșiri, secrete, permisiuni și workflow-uri exemplu.
Prețuri și limite
Cotații fixe de 7 €, 19 € sau 49 € blocate în portofelul în euro și păstrate doar la un fix verificat; trei remedieri gratuite; alimentare la 402; plafoane per utilizator și timeout-uri.