# API-ul pentru medii (/ro/docs/environments/api)

Endpoint-urile /v1/projects și /v1/environments de pe ai.codai.ro, cu exemple de cereri și răspunsuri.

URL de bază `https://ai.codai.ro`, `Authorization: Bearer codai_…`, JSON la intrare și la ieșire. Starea live a daemonului (online, porturi care ascultă, stare git, lock-uri, clienți atașați) este servită de **relay** la `https://relay.codai.ro/v1/environments/{id}/status`, cu aceeași cheie.

Convenții, toate preluate din gateway:

- Rândurile de proiect și de mediu sunt returnate în **camelCase**, așa cum sunt stocate; corpurile cererilor și listele de membri/secrete sunt în **snake_case**.
- Un proiect sau un mediu pe care nu îl poți vedea răspunde `404` (niciodată `403`), ca id-urile să nu poată fi enumerate. O încălcare de rol pe ceva ce poți vedea răspunde `403`.
- Erorile folosesc anvelopa gateway-ului `{ "error": { "message", "type", "code" } }`; erorile de validare sunt `400 bad_request`.
- Fiecare modificare adaugă un rând în `environment_events`.

## Endpoint-uri [#endpoint-uri]

| Metodă | Cale | Rol minim | Observații |
| --- | --- | --- | --- |
| `GET` | `/v1/projects` | — | Ale tale plus ale organizațiilor tale, cele mai noi primele |
| `POST` | `/v1/projects` | — | `org_id` cere owner/admin de organizație |
| `GET` | `/v1/projects/{id}` | vizibil | Proiectul + toate mediile lui |
| `PATCH` | `/v1/projects/{id}` | proprietar de proiect / admin de organizație | Slug-ul nu se poate schimba |
| `DELETE` | `/v1/projects/{id}` | proprietar de proiect / admin de organizație | `409 project_has_environments` până când toate sunt distruse |
| `GET` | `/v1/environments` | — | `?project_id=`, `?for_user_id=me` |
| `POST` | `/v1/environments` | proiect vizibil | Furnizorii administrați sunt activați per cont (`403`) |
| `GET` | `/v1/environments/{id}` | viewer | Mediu + rol + izolare + membri + porturi + ultimele 20 de evenimente |
| `PATCH` | `/v1/environments/{id}` | maintainer | `name`, `idle_timeout_minutes` |
| `POST` | `/v1/environments/{id}/start` · `stop` · `archive` | maintainer | `409 invalid_transition` |
| `POST` | `/v1/environments/{id}/destroy` | owner | |
| `POST` | `/v1/environments/{id}/enroll-token` | maintainer | Doar BYO (`409 not_byo`) |
| `POST` | `/v1/environments/enroll` | token | Apelat de `codaid`, fără cheie API |
| `GET` | `/v1/environments/{id}/members` | viewer | |
| `POST` | `/v1/environments/{id}/members` | maintainer | `owner` cere owner |
| `PUT` | `/v1/environments/{id}/members/me/ssh-key` | developer | Propria ta cheie |
| `POST` | `/v1/environments/{id}/members/me/ssh-cert` | developer | `{ public_key }` → certificat de utilizator OpenSSH valabil 12 ore pentru utilizatorul tău Linux (`certificate`, `principal`, `valid_before`, `host`); `503 ssh_ca_unconfigured` fără CA |
| `GET` | `/v1/environments/{id}/ssh-ca` | viewer | `{ public_keys }` ale autorității SSH în care au încredere mașinile virtuale; `404` când nu e configurată nicio CA |
| `PATCH` · `DELETE` | `/v1/environments/{id}/members/{userId}` | maintainer | Ca să pleci singur nu ai nevoie de rol |
| `GET` | `/v1/environments/{id}/ports` | viewer | Porturi publicate |
| `PUT` · `DELETE` | `/v1/environments/{id}/ports/{port}` | developer | `public` cere maintainer |
| `GET` | `/v1/environments/{id}/secrets` | developer | Doar nume |
| `PUT` · `DELETE` | `/v1/environments/{id}/secrets/{name}` | maintainer | Nume în stil `ENV_STYLE` |
| `GET` | `/v1/environments/{id}/tasks` | viewer | Ultimele 50 de sarcini ale agentului |
| `POST` | `/v1/environments/{id}/tasks` | developer | `202`; mediul trebuie să fie `running` (`409 environment_not_running`) |
| `GET` | `/v1/environments/{id}/tasks/{taskId}` | viewer | |
| `POST` | `/v1/environments/{id}/exec` | developer | O operație pe relay, blocantă; `409 environment_offline`, `504 host_timeout` |
| `GET` | `/v1/environments/{id}/files` | developer | `?path=` (implicit spațiul tău de lucru) |
| `GET` · `PUT` | `/v1/environments/{id}/files/{path}` | developer | Text UTF-8; citire ≤ 512 KB, scriere ≤ 2 MB (`413`) |
| `POST` | `/v1/environments/{id}/jobs` | developer | Job în fundal, întoarce imediat `job_id` |
| `GET` | `/v1/environments/{id}/jobs/{jobId}` | developer | Stare + output (`?since=`, `?tail=`) |

## Creează un proiect [#creează-un-proiect]

```bash
curl https://ai.codai.ro/v1/projects \
  -H "Authorization: Bearer $CODAI_API_KEY" -H "Content-Type: application/json" \
  -d '{ "name": "brivio", "repo_url": "https://github.com/acme/brivio", "isolation": "shared_vm" }'
```

`201`:

```json
{
  "project": {
    "id": "2b6d1c0e-4f3a-4b9c-8d7e-1a2b3c4d5e6f",
    "ownerUserId": "5f0c…", "orgId": null,
    "name": "brivio", "slug": "brivio",
    "repoUrl": "https://github.com/acme/brivio", "defaultBranch": "main", "devcontainerPath": null,
    "isolation": "shared_vm", "settings": {},
    "createdAt": "2026-09-25T09:00:00.000Z", "updatedAt": "2026-09-25T09:00:00.000Z"
  }
}
```

`slug` are implicit valoarea numelui transformat în slug și trebuie să fie unic între proiectele tale (`409 slug_taken`). `isolation` este `shared_vm` (implicit), `container` sau `vm_per_dev`.

## Creează un mediu [#creează-un-mediu]

| Câmp | Tip | Observații |
| --- | --- | --- |
| `project_id` | uuid | Obligatoriu. |
| `provider` | `byo` · `openstack` · `hetzner` | Obligatoriu. `local` este rezervat. |
| `name` | șir ≤ 120 | Obligatoriu, cu excepția proiectelor `vm_per_dev` (implicit `<proiect> — <tu>`). |
| `slug` | `^[a-z0-9][a-z0-9-]{1,38}[a-z0-9]$` | Implicit `<slug-proiect>-<4 caractere aleatorii>`; unic la nivel global (`409 slug_taken`). |
| `size` | `small` · `medium` · `large` | Doar administrate; implicit `small`. |
| `region` | șir | Doar administrate; implicit `eu-east-1` (OpenStack) sau `nbg1` (Hetzner). |
| `idle_timeout_minutes` | 5–1440 | Implicit 30. |
| `budget_micro_eur` | întreg ≥ 0 sau `null` | Doar administrate; plafon lunar de calcul. |

```bash
curl https://ai.codai.ro/v1/environments \
  -H "Authorization: Bearer $CODAI_API_KEY" -H "Content-Type: application/json" \
  -d '{ "project_id": "2b6d1c0e-4f3a-4b9c-8d7e-1a2b3c4d5e6f", "name": "dev-box", "provider": "byo" }'
```

`201`:

```json
{
  "environment": {
    "id": "9c1e5a7b-2d4f-4e6a-8b0c-3d5e7f9a1b2c",
    "projectId": "2b6d1c0e-4f3a-4b9c-8d7e-1a2b3c4d5e6f",
    "ownerUserId": "5f0c…", "orgId": null, "forUserId": null, "for_user_id": null,
    "isolation": "shared_vm",
    "name": "dev-box", "slug": "brivio-k3xq",
    "provider": "byo", "region": null, "size": null,
    "state": "pending", "stateReason": null,
    "deviceId": null, "providerRef": null, "publicIp": null,
    "idleTimeoutMinutes": 30, "lastActivityAt": null, "startedAt": null, "stoppedAt": null,
    "hourlyRateMicroEur": null, "includedHoursUsedSeconds": 0,
    "metadata": {},
    "createdAt": "2026-09-25T09:01:00.000Z", "updatedAt": "2026-09-25T09:01:00.000Z"
  }
}
```

Un furnizor administrat pornește în `creating`, cu `size`, `region`, `hourlyRateMicroEur` (de ex. `30000` pentru `small`) și `metadata.budget_micro_eur` completate. Într-un proiect `vm_per_dev`, un al doilea mediu activ primește răspunsul:

```json
{
  "error": { "message": "You already have an environment in this vm_per_dev project.", "type": "invalid_request_error", "code": "already_has_env" },
  "environment_id": "9c1e5a7b-2d4f-4e6a-8b0c-3d5e7f9a1b2c"
}
```

## Înrolează o mașină BYO [#înrolează-o-mașină-byo]

```bash
curl -X POST https://ai.codai.ro/v1/environments/$ENV_ID/enroll-token -H "Authorization: Bearer $CODAI_API_KEY"
```

```json
{ "token": "codai_env_4Qm9…32 chars", "expires_at": "2026-09-25T09:31:00.000Z", "environment_id": "9c1e5a7b-…" }
```

Tokenul este returnat o singură dată, se stochează doar ca hash SHA-256, e valabil 30 de minute, iar o nouă generare îl înlocuiește. `codaid` îl prezintă la `POST /v1/environments/enroll`:

```json
{ "token": "codai_env_…", "device_id": "1c2f…-uuid", "daemon_version": "0.3.3", "os": "linux", "arch": "x86_64" }
```

→ `{ "environment_id": "9c1e…", "relay_url": "wss://relay.codai.ro" }` (mașinile administrate primesc în plus un `api_key` afișat o singură dată). Un token necunoscut, folosit sau expirat înseamnă `401 invalid_api_key`; o stare care nu permite înrolarea înseamnă `409 not_enrollable`.

## Acțiuni de ciclu de viață [#acțiuni-de-ciclu-de-viață]

```bash
curl -X POST https://ai.codai.ro/v1/environments/$ENV_ID/stop -H "Authorization: Bearer $CODAI_API_KEY"
```

```json
{ "environment": { "id": "9c1e…", "state": "stopping", "stateReason": "user_request", "…": "…" }, "from": "running", "to": "stopping" }
```

Tranzițiile permise sunt pe [pagina mediilor administrate](./managed.mdx#ciclul-de-viață); mediile BYO acceptă doar `archive` și `destroy`.

## Membri [#membri]

```bash
curl https://ai.codai.ro/v1/environments/$ENV_ID/members \
  -H "Authorization: Bearer $CODAI_API_KEY" -H "Content-Type: application/json" \
  -d '{ "email": "ana@example.com", "role": "developer" }'
```

`201` → `{ "member": { "user_id": "7a1c09e4-…", "role": "developer", "linux_username": "u-7a1c09e4", "port_range_start": 30100 } }`. `GET …/members` întoarce `{ "members": [{ "user_id", "role", "email", "name", "linux_username", "port_range_start", "ssh_public_key": true, "provisioned_at", "created_at" }] }` — `ssh_public_key` este doar un indicator. Erori: `404` (niciun cont cu acel e-mail), `409 already_member`, `409 vm_per_dev_single_member`, `409 last_owner`.

Înregistrează-ți propria cheie:

```bash
curl -X PUT https://ai.codai.ro/v1/environments/$ENV_ID/members/me/ssh-key \
  -H "Authorization: Bearer $CODAI_API_KEY" -H "Content-Type: application/json" \
  -d '{ "public_key": "ssh-ed25519 AAAAC3Nza… ana@laptop" }'
```

→ `{ "user_id": "…", "ssh_public_key": true }`.

### Certificate SSH de scurtă durată [#certificate-ssh-de-scurtă-durată]

În loc de o cheie permanentă în `authorized_keys`, ceri CA-ului SSH al mediului un certificat valabil 12 ore. CA-ul semnează chei publice **`ssh-ed25519`** și **`ecdsa-sha2-nistp256`** — acestea din urmă sunt cheile produse de Secure Enclave pe iPhone, neexportabile, așa că aplicația codai pentru iOS nu ține niciodată o cheie privată care poate fi copiată. Alte tipuri de chei (RSA, alte curbe) primesc `400`.

```bash
curl https://ai.codai.ro/v1/environments/$ENV_ID/members/me/ssh-cert \
  -H "Authorization: Bearer $CODAI_API_KEY" -H "Content-Type: application/json" \
  -d '{ "public_key": "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAI… ana@iphone" }'
```

→ `{ "certificate": "ecdsa-sha2-nistp256-cert-v01@openssh.com AAAA… user:<uuid>/env:<id>", "principal": "u-7a1c09e4", "valid_before": "…", "host": "203.0.113.7" }`. Salvează `certificate` lângă cheie ca `<cheie>-cert.pub` (`codaid env ssh` face asta pentru tine). Certificatul e valabil de acum 5 minute până peste 12 ore, doar pentru utilizatorul tău Linux.

## Porturi [#porturi]

`PUT /v1/environments/{id}/ports/{port}` cu `{ "visibility": "owner" | "org" | "authenticated" | "public", "label"?: șir ≤ 64, "protocol"?: "http" }` întoarce `{ "port": { "id", "environmentId", "port", "protocol", "label", "visibility", "openedByUserId", "createdAt", "updatedAt" } }`. Vezi [porturi și preview-uri](./ports-and-previews.mdx).

## Secrete [#secrete]

```bash
curl -X PUT https://ai.codai.ro/v1/environments/$ENV_ID/secrets/DATABASE_URL \
  -H "Authorization: Bearer $CODAI_API_KEY" -H "Content-Type: application/json" \
  -d '{ "value": "postgres://…" }'
```

`201` → `{ "name": "DATABASE_URL", "updated": false }` (`200` și `true` la înlocuire). Valorile sunt sigilate prin criptare de tip envelope (≤ 64 KB) și nu sunt returnate niciodată; `GET …/secrets` listează `{ "name", "created_by_user_id", "created_at", "updated_at" }`. Injectarea secretelor în shell-urile din mediu vine în curând; azi se folosește doar `CODAI_GIT_TOKEN`, pentru clonare (vezi [echipa](./team.mdx#repository-partajat)).

## Sarcini pentru agent [#sarcini-pentru-agent]

„Trimite și uită”: pui un prompt în coadă, închizi laptopul și primești o notificare când munca e în commit.

```bash
curl https://ai.codai.ro/v1/environments/$ENV_ID/tasks \
  -H "Authorization: Bearer $CODAI_API_KEY" -H "Content-Type: application/json" \
  -d '{ "prompt": "Adaugă o rută /health cu un test", "branch": "feat/health" }'
```

`202` → `{ "task_id": "…", "session_id": "…", "status": "queued" }`. Relay-ul predă sarcina daemonului la următorul heartbeat; agentul rulează fără interfață ca utilizatorul tău Linux, în worktree-ul tău, pe `branch` (implicit `codai/task-<primele 8 caractere din id>`), cu permisiuni autopilot și o limită de 2 ore. La final, modificările necomise intră într-un commit `codai task <id8>` cu identitatea ta git. Primești apoi o notificare în inbox (`GET /v1/notifications`) și un push pe telefon: `env.task.done` cu statisticile diff, SHA-ul scurt și un URL de preview dacă un server de dezvoltare ascultă în intervalul tău de porturi, sau `env.task.failed` cu motivul (`no_agent_runtime` când mașina nu are sidecar-ul agentului instalat).

`session_id` este o sesiune partajată obișnuită: deschide-o în aplicația desktop sau pe telefon ca să urmărești rularea live, s-o ghidezi sau s-o anulezi. `GET …/tasks/{taskId}` întoarce `status` (`queued` → `running` → `done` | `failed`) și `result` (`diff_stat`, `commit_sha`, `branch`, `preview_url`, `error`).

## Exec prin REST [#exec-prin-rest]

Rulează comenzi și lucrează cu fișiere într-un mediu pornit prin HTTPS simplu, fără client WebSocket. Gateway-ul deschide o conexiune scurtă la relay **în numele tău** (cu propria ta cheie API, deci se aplică aceleași verificări de rol), trimite o operație și întoarce răspunsul. Viewerii primesc `403`; comanda rulează ca utilizatorul tău Linux, în spațiul tău de lucru.

```bash
curl https://ai.codai.ro/v1/environments/$ENV_ID/exec \
  -H "Authorization: Bearer $CODAI_API_KEY" -H "Content-Type: application/json" \
  -d '{ "command": "pnpm test", "timeout_ms": 120000 }'
```

`200` → `{ "ok": true, "exit_code": 0, "stdout": "…", "stderr": "", "timed_out": false, "truncated": false, "duration_ms": 8123 }`. `timeout_ms` este implicit 60 000 (maxim 600 000). Celelalte operații ale relay-ului trec prin același endpoint cu `op` + `args` (`{ "op": "fs_roots" }`, `{ "op": "job_kill", "args": { "id": 3 } }`) și întorc `{ ok, result }`; un refuz pe mașină (cale în afara rădăcinilor tale, job necunoscut) este `200 { "ok": false, "error": "…" }`. Erori: `409 environment_offline` (daemonul nu e conectat), `504 host_timeout`, `502 relay_unavailable`.

- **Fișiere**: `GET …/files?path=/home/u-…/work` listează un director; `GET …/files/home/u-…/work/README.md` întoarce `{ path, content, size, truncated }` (64 KB implicit, `?max_bytes=` până la 512 KB); `PUT …/files/<cale>` cu un corp text brut (≤ 2 MB, altfel `413`) îl scrie și creează directoarele părinte.
- **Lucru lung**: `POST …/jobs` `{ "command": "pnpm build", "expected_s": 300 }` → `201 { "job_id": 4 }`; interoghează `GET …/jobs/4?since=<next_offset>` pentru `{ job, output }` în loc să ții un exec deschis.

## Starea din relay [#starea-din-relay]

```bash
curl https://relay.codai.ro/v1/environments/$ENV_ID/status -H "Authorization: Bearer $CODAI_API_KEY"
```

```json
{
  "environment_id": "9c1e…", "online": true, "instance": "relay-7f3a", "last_heartbeat": 1790326860000,
  "activity": "agent",
  "ports": [{ "port": 3000, "pid": 4121, "process": "node" }],
  "git_state": { "branch": "main", "dirty": true, "ahead": 1, "behind": 0, "worktrees": ["/home/u-7a1c09e4/work/brivio"] },
  "daemon": { "daemon_version": "0.3.3", "os": "linux", "arch": "x86_64", "capabilities": ["shell", "pty", "tcp", "http", "lock:build", "identity:per-user"] },
  "streams": 2,
  "attached": [{ "client_id": "…", "user_id": "…", "role": "owner", "kind": "agent", "device_id": "…", "name": "Laptop de lucru", "platform": "desktop", "attached_at": "2026-09-25T09:20:00.000Z" }],
  "locks": [{ "name": "build-0123456789ab", "holder": "u-7a1c09e4 · pnpm build", "since": "2026-09-25T09:20:30Z" }],
  "clients": 1
}
```

## Referință [#referință]

<OpenAPIPage document="./openapi/ro/gateway.yaml" operations={[{path: '/v1/projects',method: 'get'},{path: '/v1/projects',method: 'post'},{path: '/v1/projects/{id}',method: 'get'},{path: '/v1/projects/{id}',method: 'patch'},{path: '/v1/projects/{id}',method: 'delete'},{path: '/v1/environments',method: 'get'},{path: '/v1/environments',method: 'post'},{path: '/v1/environments/enroll',method: 'post'},{path: '/v1/environments/{id}',method: 'get'},{path: '/v1/environments/{id}',method: 'patch'},{path: '/v1/environments/{id}/start',method: 'post'},{path: '/v1/environments/{id}/stop',method: 'post'},{path: '/v1/environments/{id}/archive',method: 'post'},{path: '/v1/environments/{id}/destroy',method: 'post'},{path: '/v1/environments/{id}/enroll-token',method: 'post'},{path: '/v1/environments/{id}/members',method: 'get'},{path: '/v1/environments/{id}/members',method: 'post'},{path: '/v1/environments/{id}/members/me/ssh-key',method: 'put'},{path: '/v1/environments/{id}/members/{userId}',method: 'patch'},{path: '/v1/environments/{id}/members/{userId}',method: 'delete'},{path: '/v1/environments/{id}/ports',method: 'get'},{path: '/v1/environments/{id}/ports/{port}',method: 'put'},{path: '/v1/environments/{id}/ports/{port}',method: 'delete'},{path: '/v1/environments/{id}/secrets',method: 'get'},{path: '/v1/environments/{id}/secrets/{name}',method: 'put'},{path: '/v1/environments/{id}/secrets/{name}',method: 'delete'},{path: '/v1/environments/{id}/tasks',method: 'get'},{path: '/v1/environments/{id}/tasks',method: 'post'},{path: '/v1/environments/{id}/tasks/{taskId}',method: 'get'},{path: '/v1/environments/{id}/exec',method: 'post'},{path: '/v1/environments/{id}/files',method: 'get'},{path: '/v1/environments/{id}/files/{path}',method: 'get'},{path: '/v1/environments/{id}/files/{path}',method: 'put'},{path: '/v1/environments/{id}/jobs',method: 'post'},{path: '/v1/environments/{id}/jobs/{jobId}',method: 'get'}]} />
