# Releul de host-uri (/ro/docs/sessions/hosts)

Rulează o comandă shell sau citește un fișier pe desktopul tău de pe telefon, dintr-un browser sau dintr-un script — fără SSH, fără port de intrare, doar în același cont.

**Releul de host-uri** permite oricărui client al tău să execute un vocabular mic și fix de operații pe unul dintre desktopurile tale care are aplicația codai desktop deschisă. Desktopul ține o conexiune SSE de ieșire către gateway și se anunță ca **host**; un solicitant face `POST` cu un `exec` și gateway-ul îl retransmite, apoi retransmite răspunsul înapoi. Nimic nu este persistat, nimic nu ascultă pe desktop și în v1 **host-ul și solicitantul trebuie să aparțină aceluiași utilizator**.

```text
   telefon / browser / script               gateway                       desktop (host)
   ──────────────────────────             ─────────                      ───────────────
   POST /v1/hosts/:dev/exec  ───────▶  rendezvous după device id ─────▶  event: exec  (pe SSE-ul lui)
        { op, args }                                                       rulează op-ul local
   200 { ok, result }        ◀───────  rendezvous după req_id    ◀─────  POST /v1/hosts/exec/:reqId/result
```

Această suprafață este servită de `https://ai.codai.ro` și **nu** face parte din specificația publicată Shared Sessions v1; tratează-o ca *remote sessions v1*.

## Ce desktopuri sunt online [#ce-desktopuri-sunt-online]

```bash
curl https://ai.codai.ro/v1/hosts -H "Authorization: Bearer $CODAI_API_KEY"
```

```json
{
  "hosts": [
    {
      "device_id": "1c2f…-uuid",
      "name": "Work laptop",
      "platform": "desktop",
      "os": "windows",
      "hostname": "DRAGOS-PC",
      "roots": ["E:\\gh\\codai", "C:\\Users\\dragos\\Documents"],
      "caps": ["pty"],
      "environment_id": "7c1e2d3f-4a5b-4c6d-8e9f-0a1b2c3d4e5f",
      "since": 1790160000000,
      "last_seen": 1790160045000
    }
  ]
}
```

Apar doar dispozitivele tale, cele văzute cel mai recent întâi. `since` și `last_seen` sunt milisecunde epoch; un host dispare din listă 45 de secunde după ultimul heartbeat. `roots` sunt directoarele pe care desktopul le va lăsa operațiile pe fișiere să le atingă. `caps` enumeră ce oferă host-ul peste operațiile de bază (`pty` = un terminal real, `iphone` + `video` = [bridge-ul pentru iPhone](../apps/iphone-remote.mdx)), iar `environment_id` este mediul care îi servește terminalul sau ecranul live prin codai-relay (`null` dacă nu există).

## Rulează ceva pe un host [#rulează-ceva-pe-un-host]

`POST /v1/hosts/:deviceId/exec` — necesită cheia ta **și** un header `x-codai-device` care identifică solicitantul.

<TypeTable
  type={{
    op: { type: "'shell' | 'fs_read' | 'fs_write' | 'fs_list' | 'fs_roots' | 'info' | 'iphone'", description: 'Operația. Vocabular fix; vezi mai jos.', required: true },
    args: { type: 'object', description: 'Argumente specifice fiecărei operații.', default: '{}' },
    timeout_ms: { type: 'integer', description: 'Cât aștepți host-ul, 1 000 – 600 000. Gateway-ul așteaptă atât plus 2 s înainte să renunțe.', default: '60000' },
    label: { type: 'string', description: 'Etichetă liberă afișată în UI-ul desktopului, ≤ 200 caractere — de ex. "session grp8 · terminal".' },
  }}
/>

<Tabs items={['curl', 'TypeScript', 'Python']}>
<Tab value="curl">

```bash
curl https://ai.codai.ro/v1/hosts/1c2f…/exec \
  -H "Authorization: Bearer $CODAI_API_KEY" \
  -H "x-codai-device: $(uuidgen)" -H "x-codai-device-platform: cli" \
  -H "Content-Type: application/json" \
  -d '{ "op": "shell", "args": { "cmd": "git status --short", "cwd": "E:\\gh\\codai" }, "timeout_ms": 30000, "label": "docs check" }'
```

</Tab>
<Tab value="TypeScript">

```ts
const res = await fetch(`https://ai.codai.ro/v1/hosts/${hostId}/exec`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.CODAI_API_KEY}`,
    'x-codai-device': myDeviceId,
    'x-codai-device-platform': 'web',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ op: 'shell', args: { cmd: 'git status --short', cwd: 'E:\\gh\\codai' }, timeout_ms: 30000 }),
});
const { ok, result, error } = (await res.json()) as {
  ok: boolean;
  result: { code: number; stdout: string; stderr: string } | null;
  error: string | null;
};
```

</Tab>
<Tab value="Python">

```python
r = requests.post(
    f"https://ai.codai.ro/v1/hosts/{host_id}/exec",
    headers={"Authorization": f"Bearer {os.environ['CODAI_API_KEY']}", "x-codai-device": my_device_id},
    json={"op": "shell", "args": {"cmd": "git status --short", "cwd": "E:\\gh\\codai"}, "timeout_ms": 30000},
    timeout=40,
)
body = r.json()
print(body["ok"], body["result"]["stdout"] if body["ok"] else body["error"])
```

</Tab>
</Tabs>

Răspunsul — răspunsul host-ului, exact așa cum a venit:

```json
{
  "req_id": "c0de…-uuid",
  "ok": true,
  "result": { "code": 0, "stdout": " M apps/docs/content/docs/en/sessions/hosts.mdx\n", "stderr": "", "truncated": false, "timed_out": false, "duration_ms": 412 },
  "error": null
}
```

Un eșec pe partea host-ului — o cale în afara `roots`, un argument greșit, o comandă care nu a putut porni — este tot HTTP `200` cu `ok: false` și `error` setat. Doar problemele releului sunt erori HTTP:

| Status | `code` | Când |
| --- | --- | --- |
| `400` | `bad_request` | `x-codai-device` lipsă, `op` necunoscut, `timeout_ms` în afara intervalului, argumente `iphone` care nu sunt `{ tool, args? }`. |
| `404` | `not_found` | `deviceId` nu este unul dintre dispozitivele tale (gateway-ul nu dezvăluie niciodată id-urile de dispozitiv ale altor utilizatori). |
| `409` | `host_offline` | Este dispozitivul tău, dar nu este conectat ca host acum — *deschide codai desktop cu host la distanță activ sau pornește bridge-ul pentru iPhone*. |
| `504` | `host_timeout` | Host-ul nu a răspuns în `timeout_ms` — *host did not answer within 30000 ms*. |

## Operații [#operații]

Desktopul implementează fiecare op cu propriile comenzi verificate de permisiuni; căile de fișiere trebuie să fie sub `roots`-urile host-ului.

| `op` | `args` | `result` |
| --- | --- | --- |
| `shell` | `{ cmd: string, cwd?: string, timeout_ms?: number }` — PowerShell pe Windows, shell-ul implicit al utilizatorului în rest; `timeout_ms` este limitat la cel al request-ului. | `{ code, stdout, stderr, truncated, timed_out, duration_ms }` |
| `fs_read` | `{ path: string, max_bytes?: integer }` — implicit 64 KB, max 512 KB. | `{ path, content, size, truncated }` |
| `fs_write` | `{ path: string, content: string }` | `{ path, bytes, created }` |
| `fs_list` | `{ path: string }` | `{ path, entries: [{ name, kind, size }], truncated }` |
| `fs_roots` | `{}` | `{ roots: string[] }` |
| `info` | `{}` | `{ os: 'windows' \| 'linux' \| 'macos', hostname, roots, platform: 'desktop' }` |
| `iphone` | `{ tool: string, args?: object }` — doar pe host-uri cu capabilitatea `iphone`; `tool` respectă `^[a-z_]{1,64}$` (verificat de gateway). Vezi [Controlează un iPhone de la distanță](../apps/iphone-remote.mdx). | `{ data, image? }` — `image` este o captură opțională `data:image/jpeg;base64,…` (≤ 1,5 MB, tot rezultatul ≤ 2 MB). |

Host-urile cu capabilitatea `pty` (codai desktop cu *Servește ca host la distanță*) deschid și un **terminal real**: desktopul își înrolează o dată propriul mediu BYO și servește stream-uri `pty` prin codai-relay; le deschizi cu id-ul din `environment_id`, exact ca terminalul unui mediu.

<Callout type="warn">
`shell` rulează cu privilegiile utilizatorului de pe desktop. Releul nu adaugă niciun sandbox propriu — setarea de autonomie și regulile aplicației desktop decid ce va executa, și fiecare request apare în UI-ul ei cu `label`-ul tău. Ține cheile API care pot ajunge la `/v1/hosts` la fel de strict delimitate ca orice cheie SSH.
</Callout>

## Construiește un host [#construiește-un-host]

Ai nevoie de această secțiune doar dacă îți scrii propriul host (aplicația codai desktop este deja unul).

<Steps>
<Step>

### Anunță-te [#anunță-te]

Deschide `GET /v1/hosts/stream` cu cheia ta și cu `x-codai-device`-ul propriu al host-ului. Descrie mașina în query string (un GET nu are body): `?os=windows&hostname=DRAGOS-PC&roots=E:\gh\codai,C:\Users\dragos\Documents` — `os` ≤ 40 caractere, `hostname` ≤ 120, până la 32 de roots. Adaugă `caps=` (listă separată prin virgulă, ≤ 16, fiecare `^[a-z0-9_-]{1,32}$`) pentru ce oferi și `env=<uuid mediu>` când un mediu îți servește stream-urile `video` / `pty`; un `caps` sau `env` greșit dă `400`.

Primul frame este `hello`, apoi un `ping` la fiecare 15 s; fiecare ping îți reîmprospătează prezența (TTL 45 s). Serverul închide stream-ul după 6 ore — reconectează-te cu backoff (desktopul folosește 1 s → 30 s).

```text
event: hello
data: {"device_id":"1c2f…","heartbeat_ms":15000}

event: ping
data: {"t":1790160045000}

event: exec
data: {"req_id":"c0de…","op":"shell","args":{"cmd":"git status --short","cwd":"E:\\gh\\codai"},"timeout_ms":30000,"label":"docs check","from_device_id":"9a0b…","ts":1790160050000}
```

</Step>
<Step>

### Execută și răspunde [#execută-și-răspunde]

Pentru fiecare frame `exec`, rulează op-ul și fă `POST /v1/hosts/exec/:reqId/result` cu `{ "ok": boolean, "result"?: any, "error"?: string ≤ 4000 }`. Body-ul trebuie să rămână sub 2 MB, iar `reqId` trebuie să fie UUID-ul de 36 de caractere pe care l-ai primit. Un răspuns lipsă sau întârziat lasă pur și simplu `timeout_ms`-ul solicitantului să expire.

```bash
curl -X POST https://ai.codai.ro/v1/hosts/exec/c0de…/result \
  -H "Authorization: Bearer $CODAI_API_KEY" -H "x-codai-device: 1c2f…" \
  -H "Content-Type: application/json" \
  -d '{ "ok": true, "result": { "code": 0, "stdout": "…", "stderr": "", "truncated": false, "timed_out": false, "duration_ms": 412 } }'
```

`req_id` este o capability imposibil de ghicit, dată doar ție; gateway-ul potrivește după ea răspunsul cu solicitantul care așteaptă și răspunde `{ "ok": true }`.

</Step>
<Step>

### Impune-ți propriile roots [#impune-ți-propriile-roots]

Gateway-ul retransmite; **tu** decizi ce rulează. Refuză căile din afara `roots`-urilor anunțate, limitează timeout-urile `shell` la `timeout_ms`-ul request-ului, plafonează dimensiunile output-ului (setează `truncated: true`) și afișează fiecare request în UI-ul tău cu `label`-ul și `from_device_id`-ul lui.

</Step>
</Steps>
