# Agent: Session gates and approvals **Page:** api/agent/sessions/approvals [Download Raw Markdown](./api/agent/sessions/approvals.md) --- # Session gates and approvals A gate parks a turn until a human answers it. There are two kinds: a **confirm** gate asks for an approve or deny decision on a tool call or directory access, and a **question** gate asks for a free-form or structured answer. This page covers the four surfaces that surround a gate: listing and answering parked gates, the session's approval policy (mode, lock, per-tool rules, lease, and YOLO), and the tool-call rules that determine which calls actually park. Answering a confirm gate on an `always` policy also requires the approver lease; that surface is documented on the [leases page](/api/agent/sessions/leases/). A gate's `gate_id` is the answerable id you send on the answer. It is scoped to the gateway's session incarnation, so an id from before a re-attach never matches. Pair it with `generation` to disambiguate when the parked gate has changed. ## List pending gates ### `GET /api/v1/agent/gates` List every gate this gateway can answer that is waiting for a human, across the sessions `GET /sessions` shows. Each entry is one live session's confirm or question, or, on a helper-gates session, one helper's gate (a background task's pending gate is its helper's gate, with `task_id`). Order is oldest parked first, then `session_id`, then `generation`. Pagination is 1-based; an omitted or zero `limit`, or one above 100, is served as 100, and `meta.limit` echoes the value used. `meta.omitted` counts gates that changed or resolved during the read. Answer one with `POST /sessions/{session_id}/confirm` or `/answer`, sending its `gate_id`. A gate is only listed while this gateway holds its session live; a session held by another client (the TUI) is not listed until it is attached here. ### Parameters | Name | In | Type | Required | Description | |------|-----|------|----------|-------------| | `include_system` | query | boolean | No | When true, also list the gates of daemon-owned system/resident sessions (as `sessions.list` does). | | `realm` | query | string | No | The realm to list: "global" (gates not tied to a realm), a 24-hex realm id, or "all" for every realm this login serves. Also accepted as the X-Hoody-Realm header, except "all". Omitted, the agent's current realm. | | `page` | query | integer | No | 1-based page number for pagination. | | `limit` | query | integer | No | Maximum items per page, at most 100. A value of 0, an omitted value, or a value above 100 is served as 100, and `meta.limit` echoes the value used. | | `X-Hoody-Cwd` | header | string | No | Per-request working-directory scope: the `.hoody` project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd. | | `X-Hoody-Config-Dir` | header | string | No | Per-request `--config-dir` override selecting which on-disk `.hoody` install a stateless read/write resolves against. | | `X-Hoody-Realm` | header | string | No | Per-request realm selector: "global" (not tied to a realm) or a 24-hex realm id (also accepted as ?realm=). On a session route it names the realm the session is looked up in: a session in another realm, or in a realm this login does not serve, is 404 not_found. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes, and for any other realm on an agent pinned to one realm. | ### Response The pending gates. ```json { "items": [ { "agent": "builder", "gate": { "gate_cause": "rules", "gate_id": 3, "generation": 0, "human_only": false, "lease_required": false, "params": { "command": "git push origin main" }, "risk": "unknown", "rules": { "outcome": "ask", "rule_ids": [ "ask-git" ] }, "status": "parked", "tool_name": "bash", "type": "confirm" }, "gate_id": "gate-9f2c4e1ab07d3355-3", "generation": 3, "kind": "confirm", "parked_at": "2026-09-28T10:15:04.512Z", "realm": "global", "rules": { "outcome": "ask", "rule_ids": [ "ask-git" ] }, "session_id": "9f2c4e1a-5b7d-4c3e-8a21-6d0f3b9e7c55" } ], "meta": { "limit": 100, "omitted": 0, "page": 1, "total": 1 } } ``` | Field | Type | Description | |-------|------|-------------| | `items` | array | The page of items, built by the gateway. | | `items[].session_id` | string | The session the gate is parked on. | | `items[].realm` | string | The session's realm: `"global"` for a session not tied to a realm, else its 24-hex realm id. Pass it as `X-Hoody-Realm` (or `?realm=`) when answering the gate. | | `items[].gate_id` | string | The answerable gate id: send it as `gate_id` on `POST /sessions/{session_id}/confirm` (kind `confirm`) or `/answer` (kind `question`). Scoped to this gateway's session incarnation. | | `items[].generation` | integer | The gateway's gate generation (optional on the answer; a mismatch is refused as `stale_gate`). | | `items[].kind` | string | What the gate asks for: `confirm` (approve or deny a tool call) or `question` (an answer). | | `items[].helper_id` | string | Present on a helper's gate: the `spawn_agent` helper that parked it. | | `items[].parent_tool_call_id` | string | Present on a helper's gate: the lead's `spawn_agent` tool call that started the helper. | | `items[].task_id` | string | Present on a background helper's gate: its task id. | | `items[].agent` | string | The session's agent. Omitted when the session record names none. | | `items[].parked_at` | string | RFC3339 time this gateway parked the gate. A re-attach parks it again with a new `gate_id` and time. | | `items[].rules` | object | Present when tool-call rules asked for the gate: `{rule_ids, outcome}`. | | `items[].gate` | object | The daemon's redacted description of the gate, as `GET /sessions/{id}/state` shows it. | | `meta` | object | Pagination metadata. | | `meta.total` | integer | Every gate parked when the list was read, across all pages. | | `meta.page` | integer | The 1-based page served. Always present. | | `meta.limit` | integer | The page size used, at most 100. Always present. | | `meta.omitted` | integer | Gates in this page window that changed or resolved during the read. | ```json { "code": "bad_request", "message": "invalid request" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `bad_request` | Bad request | The request was malformed or carried invalid parameters. | Correct the request body or query parameters. | | `realm_scope_unsupported` | Realm scope unsupported | A per-request realm header was supplied to an active-only or global-no-realm RPC, which has no realm dimension to scope. | Omit the realm header on this route, or open a session to scope by realm. | | `invalid_realm` | Invalid realm selector | The realm selector is malformed (not ""/"global"/a 24-hex id). | Pass a valid realm selector. | ```json { "code": "forbidden", "message": "request must arrive through the Hoody proxy" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `forbidden` | Forbidden (Source IP Guard) | The request did not come through the program's URL. Hoody Kit programs are reached through their URLs only. | Call the agent through its URL, for example with `hoody agent …`. | ```json { "code": "rate_limited", "message": "request rate limit exceeded" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `rate_limited` | Too many requests | The per-client request rate limit was exceeded; the gateway throttled the request before dispatch. | Honor the `Retry-After` header and retry; reduce the request rate. | ```json { "code": "internal_error", "message": "internal server error" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `internal_error` | Internal error | An unexpected error occurred while handling the request. | Retry; if persistent, inspect the daemon logs. | ```json { "code": "restriction_unknown", "message": "the login's realm restriction could not be read — retry" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `restriction_unknown` | Login restriction unknown | The agent cannot read yet which realms its login may serve, so it refuses realm-scoped requests until it can. Nothing was read or changed. | Retry after the restriction is readable. | | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `not_found` | Not found | The requested resource does not exist. | Verify the path and identifier. | ### SDK usage ```ts import { HoodyClient } from 'hoody-sdk'; const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN }); for await (const item of client.agent.gates.listIterator({ page: 1, limit: 100 })) { console.log(item); } ``` ```bash curl -X GET "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/gates?page=1&limit=100" \ -H "Authorization: Bearer $HOODY_TOKEN" ``` ## Read the approval policy ### `GET /api/v1/agent/sessions/{id}/approval` Returns the session's effective approval policy: the mode (`default` or `always`), the monotonic lock, the revision, the waiver posture (`yolo`, `auto_write`, `auto_dir_access`, `backend_auto_approving`), the session permission rules, and the approver lease state. The response `ETag` is the revision; send it as `If-Match` on a change. Works for a live or persisted session. The `event.permission_rules` frames pushed on the stream are snapshots of the same rule set. Each one states what the rules are at that moment and follows a successful durable write (a write whose commit fails rolls back and broadcasts nothing), so a client converges on the frame's contents rather than on having asked for a change. ### Parameters | Name | In | Type | Required | Description | |------|-----|------|----------|-------------| | `id` | path | string | Yes | The session id. | | `X-Hoody-Cwd` | header | string | No | Per-request working-directory scope. | | `X-Hoody-Config-Dir` | header | string | No | Per-request `--config-dir` override. | | `X-Hoody-Container` | header | string | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. | | `X-Hoody-Realm` | header | string | No | Per-request realm selector: `"global"` or a 24-hex id (also accepted as `?realm=`). Rejected (400 `realm_scope_unsupported`) on active-only or no-realm routes. | | `realm` | query | string | No | Per-request realm selector (the in:query alias of the `X-Hoody-Realm` header, read only when the header is absent). | ### Response The session's approval policy. `ETag = revision`. ```json { "mode": "default", "locked": false, "revision": 3, "yolo": false, "auto_write": false, "auto_dir_access": false, "backend_auto_approving": false, "rules": [ { "tool": "bash", "decision": "allow" } ], "lease": { "held": false, "holder": null, "generation": 0, "expires_at": null, "epoch": "ep-7f3a1c" }, "epoch": "ep-7f3a1c", "capabilities": { "always": true, "locked": true, "lease": true } } ``` | Field | Type | Description | |-------|------|-------------| | `mode` | string | `"default"` (automatic waivers may satisfy gates) or `"always"` (one explicit decision per side-effecting invocation). Sessionless headless runs, sessionless tool runs and management operations are outside any session's policy. | | `locked` | boolean | Whether the policy is frozen for the session's lifetime (monotonic; never unlocked). | | `revision` | integer | Monotonic policy revision, the `ETag` value. | | `yolo` | boolean | Whether YOLO auto-approve is armed. | | `auto_write` | boolean | Whether automatic file-write permission is on (a CLI-default posture an HTTP attach may inherit); always false under `"always"`. | | `auto_dir_access` | boolean | Whether automatic directory access is on. | | `backend_auto_approving` | boolean | Whether a delegated backend auto-approves its own tools. | | `rules` | array | Session permission rules. | | `rules[].tool` | string | Rule key (a tool name, or `tool:action`). | | `rules[].decision` | string | `allow` or `deny`. | | `lease` | object | Approver lease state: `{held, holder, generation, expires_at, epoch}`. The capability itself is never shown here. | | `epoch` | string | The daemon execution epoch (an opaque id). Changes on restart; leases and parked gates do not survive it. | | `changed` | boolean | PUT only: whether the request changed anything. | | `capabilities` | object | What this daemon supports: `{always, locked, lease}`. | ```json { "code": "bad_request", "message": "invalid request" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `bad_request` | Bad request | The request was malformed or carried invalid parameters. | Correct the request body or query parameters. | | `realm_scope_unsupported` | Realm scope unsupported | A per-request realm header was supplied to an active-only or global-no-realm RPC. | Omit the realm header on this route, or open a session to scope by realm. | | `invalid_realm` | Invalid realm selector | The realm selector is malformed (not ""/"global"/a 24-hex id). | Pass a valid realm selector. | ```json { "code": "forbidden", "message": "request must arrive through the Hoody proxy" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `forbidden` | Forbidden (Source IP Guard) | The request did not come through the program's URL. Hoody Kit programs are reached through their URLs only. | Call the agent through its URL, for example with `hoody agent …`. | ```json { "code": "not_found", "message": "resource not found" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `not_found` | Not found | The requested resource does not exist. | Verify the path and identifier. | ```json { "code": "rate_limited", "message": "request rate limit exceeded" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `rate_limited` | Too many requests | The per-client request rate limit was exceeded. | Honor the `Retry-After` header and retry; reduce the request rate. | ```json { "code": "internal_error", "message": "internal server error" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `internal_error` | Internal error | An unexpected error occurred while handling the request. | Retry; if persistent, inspect the daemon logs. | ```json { "code": "restriction_unknown", "message": "the login's realm restriction could not be read — retry" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `restriction_unknown` | Login restriction unknown | The agent cannot read yet which realms its login may serve. | Retry after the restriction is readable. | ### SDK usage ```ts import { HoodyClient } from 'hoody-sdk'; const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN }); const policy = await client.agent.sessions.getApproval('{session_id}'); ``` ```bash curl -X GET "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/sessions/{session_id}/approval" \ -H "Authorization: Bearer $HOODY_TOKEN" ``` ## Inspect which rules apply ### `GET /api/v1/agent/sessions/{id}/rules/applies` Returns the tool-call rules a live session checks for one agent's calls, from the same selection the rules check itself runs. A rule without `tools` skips read-only tools, a rule with `tools` covers only those, and a rule with `agents` covers only those agents' calls. `agent` defaults to the session's own agent; name a helper's or workflow step's agent to see what its calls are checked against. With `tool`, the reply's checked rules (`applies_as: checked`) are exactly the rules sent to Jev for that call; without it, every checked rule that covers the agent's calls to some tool. Guidance and playbook rules are listed too, with `applies_as: prompt`. `fail_closed_reason` says when every covered call fails closed right now (`trust_hold`, `jev_disabled`, or `jev_no_key`). A dormant session answers 404 `not_found`. ### Parameters | Name | In | Type | Required | Description | |------|-----|------|----------|-------------| | `id` | path | string | Yes | The session id. | | `agent` | query | string | No | Agent name to ask about (default: the session's own agent). | | `tool` | query | string | No | Tool name to ask about (default: any tool). | | `X-Hoody-Cwd` | header | string | No | Per-request working-directory scope. | | `X-Hoody-Config-Dir` | header | string | No | Per-request `--config-dir` override. | | `X-Hoody-Container` | header | string | No | Per-request bound remote container. | | `X-Hoody-Realm` | header | string | No | Per-request realm selector. | | `realm` | query | string | No | Per-request realm selector (in:query alias of the `X-Hoody-Realm` header). | ### Response The applying rules. ```json { "agent": "builder", "tool": "bash", "read_only": false, "active": true, "count": 2, "rules": [ { "id": "ask-git", "text": "Ask before running git push.", "kind": "check_in", "applies_as": "checked", "tools": ["bash"], "agents": [], "source": "/etc/hoody/settings.json" } ], "prompt_off": false, "prompt_bytes": 184, "prompt_budget_bytes": 8192, "prompt_over_budget": [] } ``` | Field | Type | Description | |-------|------|-------------| | `agent` | string | The acting agent asked about: the `agent` query value, else the session's own agent. | | `tool` | string | The tool asked about. Omitted when no tool was named. | | `read_only` | boolean | Whether the named tool is read-only. Omitted when no tool was named. | | `active` | boolean | Whether the session has rules that are checked at all (`limit` or `check_in`). | | `count` | integer | `len(rules)`: the rules that apply to the agent, checked and prompt together. | | `rules` | array | The rules that apply to the agent, in settings order. | | `rules[].id` | string | The rule id. | | `rules[].text` | string | The rule in plain words. | | `rules[].kind` | string | `limit`, `check_in`, `guidance`, or `playbook`. | | `rules[].applies_as` | string | Output only, derived from kind. `checked` = Jev checks covered tool calls; `prompt` = its text is in the covered agents' system prompt. | | `rules[].tools` | array | The tools the rule is limited to; empty = every tool that is not read-only. Always empty for guidance and playbook rules. | | `rules[].agents` | array | The agents whose calls the rule covers; empty = every agent. | | `rules[].source` | string | The settings file that declared the rule. | | `prompt_off` | boolean | Whether `prompt_blocks` drops the team-rules block for this agent. | | `prompt_bytes` | integer | The size in bytes of the rule lines the agent's team-rules block carries. 0 when `prompt_off` is true. | | `prompt_budget_bytes` | integer | The most rule-line bytes one agent's team-rules block carries (8192). | | `prompt_over_budget` | array | The guidance and playbook rules that cover the agent but did not fit `prompt_budget_bytes`. | | `fail_closed_reason` | string | Present when rules apply and none can be checked right now: `trust_hold`, `jev_disabled`, or `jev_no_key`. | ```json { "code": "bad_request", "message": "invalid request" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `bad_request` | Bad request | The request was malformed or carried invalid parameters. | Correct the request body or query parameters. | | `realm_scope_unsupported` | Realm scope unsupported | A per-request realm header was supplied to an active-only or global-no-realm RPC. | Omit the realm header on this route, or open a session to scope by realm. | | `invalid_realm` | Invalid realm selector | The realm selector is malformed (not ""/"global"/a 24-hex id). | Pass a valid realm selector. | ```json { "code": "forbidden", "message": "request must arrive through the Hoody proxy" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `forbidden` | Forbidden (Source IP Guard) | The request did not come through the program's URL. Hoody Kit programs are reached through their URLs only. | Call the agent through its URL, for example with `hoody agent …`. | ```json { "code": "not_found", "message": "resource not found" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `not_found` | Not found | The requested resource does not exist. | Verify the path and identifier. | ```json { "code": "rate_limited", "message": "request rate limit exceeded" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `rate_limited` | Too many requests | The per-client request rate limit was exceeded. | Honor the `Retry-After` header and retry; reduce the request rate. | ```json { "code": "internal_error", "message": "internal server error" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `internal_error` | Internal error | An unexpected error occurred while handling the request. | Retry; if persistent, inspect the daemon logs. | ```json { "code": "restriction_unknown", "message": "the login's realm restriction could not be read — retry" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `restriction_unknown` | Login restriction unknown | The agent cannot read yet which realms its login may serve. | Retry after the restriction is readable. | ### SDK usage ```ts import { HoodyClient } from 'hoody-sdk'; const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN }); const rules = await client.agent.sessions.listApplicableRules('{session_id}', { agent: 'builder', tool: 'bash' }); ``` ```bash curl -X GET "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/sessions/{session_id}/rules/applies?agent=builder&tool=bash" \ -H "Authorization: Bearer $HOODY_TOKEN" ``` ## Answer a parked question ### `POST /api/v1/agent/sessions/{id}/answer` Answers a parked ask-the-user question. Provide `answer` or `text` for a free-form reply, or `answers` (a map) for a structured multi-field question. `gate_id` and `generation` are echoes of the parked gate; a mismatch is 409 `stale_gate`. 409 `no_pending_gate` when nothing is parked; 409 `gate_already_answered` when an earlier request already won; 409 `gate_type_mismatch` when the parked gate is a confirm gate (use `/confirm` for those); 409 `gate_cancelled` when the turn was cancelled while the answer was in flight (do not retry this answer). A 503 `decision_unconfirmed` means the daemon did not acknowledge within the wait. The gateway rolls the answered mark back so the gate stays answerable. Check `GET /sessions/{id}/state` to see whether the gate is gone (decision applied) or still parked (retry it). ### Parameters | Name | In | Type | Required | Description | |------|-----|------|----------|-------------| | `id` | path | string | Yes | The session id. | | `X-Hoody-Cwd` | header | string | No | Per-request working-directory scope. | | `X-Hoody-Config-Dir` | header | string | No | Per-request `--config-dir` override. | | `X-Hoody-Container` | header | string | No | Per-request bound remote container. | | `X-Hoody-Realm` | header | string | No | Per-request realm selector. | | `realm` | query | string | No | Per-request realm selector (in:query alias of the `X-Hoody-Realm` header). | ### Request body | Field | Type | Required | Description | |-------|------|----------|-------------| | `gate_id` | string | No | Echo of the parked gate id, as published on the frame that parked it. Valid only for the session in the path. Ids are unique per session incarnation, so an id from before a re-attach never matches. | | `generation` | integer | No | Optional echo of the parked gate generation. Restarts when the session is re-attached, so it is not an identity on its own. | | `answer` | string | No | Free-form answer text. When it is absent or blank, `text` is used in its place. It may be empty when `text` or `answers` carries the answer. | | `text` | string | No | Answer text used when `answer` is absent or blank; ignored otherwise. | | `answers` | object | No | Structured per-field answers for a multi-field question. | ```json { "gate_id": "gate-3f9a1c2b7d4e6f80-2", "generation": 2, "answer": "Yes, push to main." } ``` ### Response Answered. ```json { "status": "ok", "resolved": { "gate_id": "gate-3f9a1c2b7d4e6f80-2", "outcome": "answered" } } ``` | Field | Type | Description | |-------|------|-------------| | `status` | string | `"ok"` on success. | | `resolved` | object | The daemon's resolution of the gate, when it acknowledged within the wait. | | `replayed` | boolean | True when a retried decision naming the same gate got the original acknowledgement back. Nothing was forwarded a second time. | | `session_scope_applied` | boolean | Present when a session-wide grant could not be applied; see `note`. | | `note` | string | Present alongside `session_scope_applied`: why the wider grant did not apply. | ```json { "code": "bad_request", "message": "invalid request" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `bad_request` | Bad request | The request was malformed or carried invalid parameters. | Correct the request body or query parameters. | | `realm_scope_unsupported` | Realm scope unsupported | A per-request realm header was supplied to an active-only or global-no-realm RPC. | Omit the realm header on this route, or open a session to scope by realm. | | `invalid_realm` | Invalid realm selector | The realm selector is malformed (not ""/"global"/a 24-hex id). | Pass a valid realm selector. | ```json { "code": "forbidden", "message": "request must arrive through the Hoody proxy" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `forbidden` | Forbidden (Source IP Guard) | The request did not come through the program's URL. Hoody Kit programs are reached through their URLs only. | Call the agent through its URL, for example with `hoody agent …`. | ```json { "code": "not_found", "message": "resource not found" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `not_found` | Not found | The requested resource does not exist. | Verify the path and identifier. | ```json { "code": "stale_gate", "message": "the gate id/generation does not match the parked gate", "details": { "gate_id": "gate-3f9a1c2b7d4e6f80-2", "generation": 2, "type": "question" } } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `no_pending_gate` | No pending gate | No confirm/question gate is parked for this session, so there is nothing to answer. | Dispatch a turn and answer the gate it parks; check the pending gate via `GET /sessions/{id}`. | | `stale_gate` | Stale gate | The supplied `gate_id`/`generation` does not match the currently parked gate. | Answer the gate in `details` (echo its `gate_id` and `generation`), or re-read the pending gate when details are absent. | | `gate_already_answered` | Gate already answered | The parked gate was already answered by an earlier request (first valid answer wins). | Wait for the next gate; do not re-answer. | | `gate_type_mismatch` | Gate type mismatch | The parked gate is a different type than this endpoint answers. | Use the endpoint matching the parked gate type (`/confirm` for a confirm gate, `/answer` for a question gate). | | `gate_cancelled` | Gate cancelled | The gate was resolved without consuming this decision: the turn was cancelled or the session ended while the decision was in flight. The decision was NOT applied. | Do not retry this decision: the gate it answered no longer exists. Re-read `GET /sessions/{id}/state` and answer the gate parked there, if any. | ```json { "code": "payload_too_large", "message": "request body exceeds the configured size limit" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `payload_too_large` | Payload too large | The request body exceeds the size limit (default 8 MiB). | Reduce the request body below the configured limit (default 8 MiB); split a large payload into smaller requests. | ```json { "code": "rate_limited", "message": "request rate limit exceeded" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `rate_limited` | Too many requests | The per-client request rate limit was exceeded. | Honor the `Retry-After` header and retry; reduce the request rate. | ```json { "code": "internal_error", "message": "internal server error" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `internal_error` | Internal error | An unexpected error occurred while handling the request. | Retry; if persistent, inspect the daemon logs. | ```json { "code": "decision_unconfirmed", "message": "the daemon did not acknowledge the decision within the wait", "details": { "gate_id": "gate-…", "generation": 1 } } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `service_unavailable` | Service unavailable | The daemon could not service the request. | Honor `Retry-After` and retry. | | `decision_unconfirmed` | Decision unconfirmed | The decision was forwarded but the daemon acknowledged neither its consumption nor a refusal within the wait. It may still apply. | `GET /sessions/{id}/state`: if the gate is gone the decision applied; otherwise retry it. | ### SDK usage ```ts import { HoodyClient } from 'hoody-sdk'; const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN }); await client.agent.gates.answer('{session_id}', { gate_id: 'gate-3f9a1c2b7d4e6f80-2', generation: 2, answer: 'Yes, push to main.', }); ``` ```bash curl -X POST "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/sessions/{session_id}/answer" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "gate_id": "gate-3f9a1c2b7d4e6f80-2", "generation": 2, "answer": "Yes, push to main." }' ``` ## Propose answers for a parked question ### `POST /api/v1/agent/sessions/{id}/answer:assist` Runs a one-shot helper-model call that proposes answers for the parked question. The real answer still travels via `/answer`; the daemon never answers itself. The suggestion arrives later as `event.question_suggestion`, with the job id from the 202 ack. 409 `no_pending_gate` when no question is parked; 409 `assist_in_flight` when another assist is already running for this session. ### Parameters | Name | In | Type | Required | Description | |------|-----|------|----------|-------------| | `id` | path | string | Yes | The session id. | | `X-Hoody-Cwd` | header | string | No | Per-request working-directory scope. | | `X-Hoody-Config-Dir` | header | string | No | Per-request `--config-dir` override. | | `X-Hoody-Container` | header | string | No | Per-request bound remote container. | | `X-Hoody-Realm` | header | string | No | Per-request realm selector. | | `realm` | query | string | No | Per-request realm selector (in:query alias of the `X-Hoody-Realm` header). | ### Request body | Field | Type | Required | Description | |-------|------|----------|-------------| | `mode` | string | No | Suggestion mode (default `"suggest"`). | | `model` | string | No | Helper model override; empty uses the configured helper. | | `gen` | integer | No | Generation counter to correlate the suggestion event (echoed as `event.question_suggestion` `gen`). | ```json { "mode": "suggest", "gen": 1 } ``` ### Response Suggestion job dispatched. Fetch its result via `GET /jobs/{id}/result`. ```json { "job_id": "job-9f2c4e1ab07d3355-7", "session_id": "9f2c4e1a-5b7d-4c3e-8a21-6d0f3b9e7c55" } ``` | Field | Type | Description | |-------|------|-------------| | `job_id` | string | Gateway-minted job id for the helper call. `GET /jobs/{id}/result` returns the suggestion under `result` once the job is terminal; it is null while the job is still running. | | `session_id` | string | The session whose parked question is being answered (echo of the path id). | ```json { "code": "bad_request", "message": "invalid request" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `bad_request` | Bad request | The request was malformed or carried invalid parameters. | Correct the request body or query parameters. | | `realm_scope_unsupported` | Realm scope unsupported | A per-request realm header was supplied to an active-only or global-no-realm RPC. | Omit the realm header on this route, or open a session to scope by realm. | | `invalid_realm` | Invalid realm selector | The realm selector is malformed (not ""/"global"/a 24-hex id). | Pass a valid realm selector. | ```json { "code": "realm_not_allowed", "message": "realm 0123456789abcdef01234567 is not allowed for this realm-limited login" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `realm_not_allowed` | Realm not allowed for this login | The agent is logged in with a token limited to some realms, and this session's realm is not one of them. | Continue in a session of a realm the login serves, or log the agent in with a token that covers this session's realm. | | `forbidden` | Forbidden (Source IP Guard) | The request did not come through the program's URL. Hoody Kit programs are reached through their URLs only. | Call the agent through its URL, for example with `hoody agent …`. | ```json { "code": "not_found", "message": "resource not found" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `not_found` | Not found | The requested resource does not exist. | Verify the path and identifier. | ```json { "code": "assist_in_flight", "message": "an answer:assist job is already running for this session" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `no_pending_gate` | No pending gate | No confirm/question gate is parked for this session, so there is nothing to answer. | Dispatch a turn and answer the gate it parks; check the pending gate via `GET /sessions/{id}`. | | `assist_in_flight` | Assist in flight | An `answer:assist` suggestion job is already running for this session; only one in-flight assist per session is allowed. | Wait for the prior suggestion (observe `event.question_suggestion` or poll the job), then retry. | ```json { "code": "payload_too_large", "message": "request body exceeds the configured size limit" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `payload_too_large` | Payload too large | The request body exceeds the size limit (default 8 MiB). | Reduce the request body below the configured limit (default 8 MiB); split a large payload into smaller requests. | ```json { "code": "rate_limited", "message": "request rate limit exceeded" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `rate_limited` | Too many requests | The per-client request rate limit was exceeded. | Honor the `Retry-After` header and retry; reduce the request rate. | ```json { "code": "internal_error", "message": "internal server error" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `internal_error` | Internal error | An unexpected error occurred while handling the request. | Retry; if persistent, inspect the daemon logs. | ```json { "code": "service_unavailable", "message": "service unavailable" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `service_unavailable` | Service unavailable | The daemon could not service the request. | Honor `Retry-After` and retry. | | `restriction_unknown` | Login restriction unknown | The agent cannot read yet which realms its login may serve. | Retry after the restriction is readable. | ### SDK usage ```ts import { HoodyClient } from 'hoody-sdk'; const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN }); const assist = await client.agent.gates.suggest('{session_id}', { mode: 'suggest', gen: 1 }); const jobId = assist.data.job_id; ``` ```bash curl -X POST "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/sessions/{session_id}/answer:assist" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "mode": "suggest", "gen": 1 }' ``` ## Answer a parked confirm gate ### `POST /api/v1/agent/sessions/{id}/confirm` Approves or denies a parked tool or directory confirmation. A 200 means the agent consumed **this** decision; nothing else is a 200. The required `approved` boolean is mandatory: a confirm without a boolean `approved` is rejected 400 `approved_required` and the gate stays parked. On a session that requires approval on every action (`mode: "always"`), `gate_id` and `generation` are required too, and once the session's approver lease was minted, every decision also carries it in `X-Hoody-Approver-Lease`. `session_scope: true` remembers the decision for the rest of the session (with `approved: true` the tool stops asking, with `approved: false` it is refused without asking). Offer allow-for-session only when the gate's `event.confirm_request` carried `offer_session_allow`. Under a locked policy the wider grant is refused, but the one-shot decision still applies and the reply says so (`session_scope_applied: false`, `note`). `trust_container: true` accepts the gate's `exec_trust` offer (also only on the gate's offer). A 409 `gate_decision_pending` means a decision for this gate is in flight and its outcome is not known yet: another caller's, this one when the agent did not answer within the wait, or the connection was lost (`details.reason: "connection_lost"`). Retry the same decision naming the gate to learn the outcome. A 409 `gate_already_resolved` means the gate already ended, so this decision was not applied. `details.outcome` says how it ended: `answered`, `cancelled`, `timeout`, `yolo`, `incarnation_ended`, or `unknown`. `details.request_id` is present when the decision consumed carried that `request_id`. Never resend. ### Parameters | Name | In | Type | Required | Description | |------|-----|------|----------|-------------| | `id` | path | string | Yes | The session id. | | `X-Hoody-Approver-Lease` | header | string | No | The approver-lease capability returned by `POST /sessions/{id}/approver-lease`. Required on every decision on an `"always"` session whose lease was minted; the daemon verifies it at decision consumption. | | `X-Hoody-Cwd` | header | string | No | Per-request working-directory scope. | | `X-Hoody-Config-Dir` | header | string | No | Per-request `--config-dir` override. | | `X-Hoody-Container` | header | string | No | Per-request bound remote container. | | `X-Hoody-Realm` | header | string | No | Per-request realm selector. | | `realm` | query | string | No | Per-request realm selector (in:query alias of the `X-Hoody-Realm` header). | ### Request body | Field | Type | Required | Description | |-------|------|----------|-------------| | `approved` | boolean | Yes | `true` to approve, `false` to deny. There is no default; a missing, null, or non-boolean value is rejected 400. | | `gate_id` | string | No | Echo of the parked gate id. Required on a session that requires approval on every action: a confirm without it is rejected 400. | | `generation` | integer | No | Echo of the parked gate generation. Optional on a default-policy session; required, and non-zero, on a session that requires approval on every action. | | `persist_dirs` | boolean | No | Persist an approved directory grant to `settings.json` (WS↔REST parity). | | `session_scope` | boolean | No | Remember this decision for the rest of the session. With `approved: true` the tool stops asking, with `approved: false` it is refused without asking. Offer allow-for-session only when the gate's `event.confirm_request` carried `offer_session_allow`. | | `trust_container` | boolean | No | With `approved: true`, accept the gate's `exec_trust` offer. Ignored when the gate carried no offer or the approval policy is locked. | | `request_id` | string | No | Optional caller id for this decision, at most 64 characters from `A-Z`, `a-z`, `0-9`, `.`, `_`, and `-`. A later 409 `gate_already_resolved` for the gate echoes it as `details.request_id`. | | `lease_generation` | integer | No | The approver-lease generation the decision was made under. Forwarded as is; omitted or 0 sends none. | ```json { "gate_id": "gate-9f2c4e1ab07d3355-3", "generation": 3, "approved": true, "session_scope": false } ``` ### Response Answered. ```json { "status": "ok", "resolved": { "gate_id": "gate-9f2c4e1ab07d3355-3", "outcome": "answered" } } ``` | Field | Type | Description | |-------|------|-------------| | `status` | string | `"ok"` on success. | | `resolved` | object | The daemon's resolution of the gate (`event.gate_resolved`) that consumed this decision. | | `session_scope_applied` | boolean | Present (false) when the decision applied once but its session-wide grant could not be applied; see `note`. | | `note` | string | Present alongside `session_scope_applied`: why the wider grant did not apply. | ```json { "code": "approved_required", "message": "/confirm must carry approved: true to approve or false to deny" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `bad_request` | Bad request | The request was malformed or carried invalid parameters. | Correct the request body or query parameters. | | `realm_scope_unsupported` | Realm scope unsupported | A per-request realm header was supplied to an active-only or global-no-realm RPC. | Omit the realm header on this route, or open a session to scope by realm. | | `approved_required` | Decision missing | The confirm did not carry a boolean `approved`. An omitted or null decision never approves or denies anything, and the gate stays parked. | Send `approved: true` to approve or `approved: false` to deny. | | `invalid_realm` | Invalid realm selector | The realm selector is malformed (not ""/"global"/a 24-hex id). | Pass a valid realm selector. | ```json { "code": "forbidden", "message": "request must arrive through the Hoody proxy" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `forbidden` | Forbidden (Source IP Guard) | The request did not come through the program's URL. Hoody Kit programs are reached through their URLs only. | Call the agent through its URL, for example with `hoody agent …`. | ```json { "code": "not_found", "message": "resource not found" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `not_found` | Not found | The requested resource does not exist. | Verify the path and identifier. | ```json { "code": "gate_already_resolved", "message": "the gate was already resolved; this decision was not applied", "details": { "decision": { "approved": true, "persist_dirs": false, "session_scope": false, "trust_container": false }, "gate_id": "gate-9f2c4e1ab07d3355-3", "generation": 3, "outcome": "answered", "request_id": "ui-7f3a" } } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `gate_decision_pending` | Gate decision pending | A decision for this gate was sent to the agent and its outcome is not known yet. | Retry the same decision naming the gate. | | `gate_already_resolved` | Gate already resolved | The gate already ended, so this decision was not applied. `details.outcome` says how it ended. | Do not resend the decision. Read `details.decision` for what was decided, then re-read `GET /sessions/{id}/state` and answer the gate parked there, if any. | | `no_pending_gate` | No pending gate | No confirm/question gate is parked for this session, so there is nothing to answer. | Dispatch a turn and answer the gate it parks; check the pending gate via `GET /sessions/{id}`. | | `stale_gate` | Stale gate | The supplied `gate_id`/`generation` does not match the currently parked gate. | Answer the gate in `details` (echo its `gate_id` and `generation`), or re-read the pending gate when details are absent. | | `gate_type_mismatch` | Gate type mismatch | The parked gate is a different type than this endpoint answers. | Use the endpoint matching the parked gate type (`/confirm` for a confirm gate, `/answer` for a question gate). | | `decision_incomplete` | Decision incomplete | The session's approval policy is `"always"`: a decision must carry an explicit `approved`, the exact `gate_id`, and its `generation`. Nothing was decided and the gate stays parked. | Re-send the confirm with `approved`, the parked gate's `gate_id`, and its `generation`. | | `approver_lease_required` | Approver lease required | This session's approver lease was minted, so every decision must present the current capability in `X-Hoody-Approver-Lease`. | Acquire the lease (`POST /sessions/{id}/approver-lease`) and re-issue the confirmation with `X-Hoody-Approver-Lease`. | | `approver_lease_invalid` | Approver lease invalid | The presented capability does not verify for this session (wrong session, a fenced generation, or a daemon restart). | Re-acquire the lease (`POST /sessions/{id}/approver-lease`). | ```json { "code": "approver_lease_expired", "message": "the approver lease expired" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `approver_lease_expired` | Approver lease expired | The presented lease lapsed; an expired lease never authorises a decision. | Re-acquire the lease (`POST /sessions/{id}/approver-lease`). | ```json { "code": "payload_too_large", "message": "request body exceeds the configured size limit" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `payload_too_large` | Payload too large | The request body exceeds the size limit (default 8 MiB). | Reduce the request body below the configured limit (default 8 MiB); split a large payload into smaller requests. | ```json { "code": "rate_limited", "message": "request rate limit exceeded" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `rate_limited` | Too many requests | The per-client request rate limit was exceeded. | Honor the `Retry-After` header and retry; reduce the request rate. | ```json { "code": "internal_error", "message": "internal server error" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `internal_error` | Internal error | An unexpected error occurred while handling the request. | Retry; if persistent, inspect the daemon logs. | ```json { "code": "service_unavailable", "message": "service unavailable" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `service_unavailable` | Service unavailable | The daemon could not service the request. | Honor `Retry-After` and retry. | ### SDK usage The SDK reaches this operation through two methods: - `client.agent.gates.approve(...)`: sets `approved: true`. - `client.agent.gates.deny(...)`: sets `approved: false`. ```ts import { HoodyClient } from 'hoody-sdk'; const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN }); await client.agent.gates.approve('{session_id}', { gate_id: 'gate-9f2c4e1ab07d3355-3', generation: 3, session_scope: false, }); ``` ```bash curl -X POST "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/sessions/{session_id}/confirm" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "gate_id": "gate-9f2c4e1ab07d3355-3", "generation": 3, "approved": true, "session_scope": false }' ``` ## Set approval mode and lock ### `PUT /api/v1/agent/sessions/{id}/approval` Changes the session's approval mode (`"default"` or `"always"`) and/or its lock. Use `If-Match` (the `ETag` from `GET`, or `*` for unconditional) to make the change conditional. A stale `If-Match` is 412 (the response `ETag` carries the current revision). A locked policy is 409 `approval_policy_locked`; an unknown mode, a delegated session, an unlock attempt, or weakening an inherited lock is 409 `approval_policy_unsatisfiable`; a busy session is 409 `session_busy` (the policy only changes at quiescence); a failed durable commit is 503 `policy_commit_failed` (nothing acknowledged). Emits `event.approval_policy_changed` to every attached client. `"always"` makes every side-effecting Hoody-visible invocation require one explicit decision naming its gate. Nested executors that cannot park a decision are refused (subagents, workflow runs and resumes, the orchestrator, foreign CLIs, `run_todo`, `test_hook`); the user's configured hook commands are skipped, each disclosed as `event.hook_run {decision:"skipped"}`; configured MCP servers are not started; stdin to a background bash process is refused. Machine-confirmed execution paths (a composite or fusion model's winner-commit run in writes mode, subagents, workflow tool steps, auto-reply) have their side-effecting invocations refused under `"always"` (error code `approval_policy_unsatisfiable`); reads still pass. `"locked"` freezes the mode and every waiver alias (YOLO, allow rules, session-scoped remembered approvals, auto-reply arming) for the session's lifetime. It is monotonic and never unlocked. A locked policy is never acknowledged unlocked. ### Parameters | Name | In | Type | Required | Description | |------|-----|------|----------|-------------| | `id` | path | string | Yes | The session id. | | `If-Match` | header | string | No | Conditional-request precondition: the `ETag` from `GET /sessions/{id}/approval` (a quoted policy revision, e.g. `"3"`) or `*`. A mismatch is 412 `precondition_failed`. | | `X-Hoody-Cwd` | header | string | No | Per-request working-directory scope. | | `X-Hoody-Config-Dir` | header | string | No | Per-request `--config-dir` override. | | `X-Hoody-Container` | header | string | No | Per-request bound remote container. | | `X-Hoody-Realm` | header | string | No | Per-request realm selector. | | `realm` | query | string | No | Per-request realm selector (in:query alias of the `X-Hoody-Realm` header). | ### Request body | Field | Type | Required | Description | |-------|------|----------|-------------| | `mode` | string | No | `"default"` or `"always"`. Omit to keep the current mode. | | `locked` | boolean | No | Freeze the policy for the session's lifetime (monotonic; never unlocked). Omit to keep the current lock. | ```json { "mode": "always", "locked": true } ``` ### Response The updated policy. `ETag = new revision`. ```json { "mode": "always", "locked": true, "revision": 4, "yolo": false, "auto_write": false, "auto_dir_access": false, "backend_auto_approving": false, "rules": [], "lease": { "held": false, "holder": null, "generation": 0, "expires_at": null, "epoch": "ep-7f3a1c" }, "epoch": "ep-7f3a1c", "changed": true, "capabilities": { "always": true, "locked": true, "lease": true } } ``` | Field | Type | Description | |-------|------|-------------| | `mode` | string | `"default"` or `"always"`. | | `locked` | boolean | Whether the policy is frozen for the session's lifetime (monotonic; never unlocked). | | `revision` | integer | Monotonic policy revision, the `ETag` value. | | `yolo` | boolean | Whether YOLO auto-approve is armed. | | `auto_write` | boolean | Whether automatic file-write permission is on; always false under `"always"`. | | `auto_dir_access` | boolean | Whether automatic directory access is on. | | `backend_auto_approving` | boolean | Whether a delegated backend auto-approves its own tools. | | `rules` | array | Session permission rules. | | `rules[].tool` | string | Rule key (a tool name, or `tool:action`). | | `rules[].decision` | string | `allow` or `deny`. | | `lease` | object | Approver lease state: `{held, holder, generation, expires_at, epoch}`. | | `epoch` | string | The daemon execution epoch (an opaque id). | | `changed` | boolean | PUT only: whether the request changed anything. | | `capabilities` | object | What this daemon supports: `{always, locked, lease}`. | ```json { "code": "bad_request", "message": "invalid request" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `bad_request` | Bad request | The request was malformed or carried invalid parameters. | Correct the request body or query parameters. | | `realm_scope_unsupported` | Realm scope unsupported | A per-request realm header was supplied to an active-only or global-no-realm RPC. | Omit the realm header on this route, or open a session to scope by realm. | | `invalid_realm` | Invalid realm selector | The realm selector is malformed (not ""/"global"/a 24-hex id). | Pass a valid realm selector. | ```json { "code": "forbidden", "message": "request must arrive through the Hoody proxy" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `forbidden` | Forbidden (Source IP Guard) | The request did not come through the program's URL. Hoody Kit programs are reached through their URLs only. | Call the agent through its URL, for example with `hoody agent …`. | ```json { "code": "not_found", "message": "resource not found" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `not_found` | Not found | The requested resource does not exist. | Verify the path and identifier. | ```json { "code": "approval_policy_locked", "message": "the approval policy is locked" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `approval_policy_locked` | Approval policy locked | The session's approval policy is locked for its lifetime: the mode, the lock, YOLO, allow rules, session-scoped remembered approvals and auto-reply arming cannot be changed. | Create a new session with the posture you need; a locked policy is never unlocked. | | `approval_policy_unsatisfiable` | Approval policy unsatisfiable | The approval assertion cannot be honoured: an attach asserted a posture the shared session does not have, the backend cannot park on a decision, or the session's posture is already open. | Attach without the assertion (or with the session's actual posture), or create a fresh session with the policy you need. | | `session_busy` | Session busy | The session is already live in another connection (single-writer-per-session). | Close the other attach, or attach a fork instead. | ```json { "code": "precondition_failed", "message": "the approval policy revision changed; refetch and retry with the current ETag", "details": { "revision": 4 } } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `precondition_failed` | Precondition failed | `If-Match` named a policy revision that is no longer current. The response `ETag` carries the current revision (also under `details.revision`). | Refetch `GET /sessions/{id}/approval` (or use the `ETag` on this response) and retry. | ```json { "code": "payload_too_large", "message": "request body exceeds the configured size limit" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `payload_too_large` | Payload too large | The request body exceeds the size limit (default 8 MiB). | Reduce the request body below the configured limit (default 8 MiB); split a large payload into smaller requests. | ```json { "code": "rate_limited", "message": "request rate limit exceeded" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `rate_limited` | Too many requests | The per-client request rate limit was exceeded. | Honor the `Retry-After` header and retry; reduce the request rate. | ```json { "code": "internal_error", "message": "internal server error" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `internal_error` | Internal error | An unexpected error occurred while handling the request. | Retry; if persistent, inspect the daemon logs. | ```json { "code": "policy_commit_failed", "message": "the approval policy could not be persisted; nothing was acknowledged" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `service_unavailable` | Service unavailable | The daemon could not service the request. | Honor `Retry-After` and retry. | | `policy_commit_failed` | Policy commit failed | The daemon could not durably write the policy, so the change was NOT acknowledged (a locked policy is never acknowledged unlocked). | Retry; if it persists, check the daemon's session store (disk or permissions). | | `restriction_unknown` | Login restriction unknown | The agent cannot read yet which realms its login may serve. | Retry after the restriction is readable. | ### SDK usage ```ts import { HoodyClient } from 'hoody-sdk'; const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN }); await client.agent.sessions.updateApproval('{session_id}', { mode: 'always', locked: true }, { IfMatch: '"3"' }); ``` ```bash curl -X PUT "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/sessions/{session_id}/approval" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -H "If-Match: \"3\"" \ -d '{ "mode": "always", "locked": true }' ``` ## Set a session permission rule ### `PUT /api/v1/agent/sessions/{id}/approval/rules/{tool}` Upserts one session permission rule for `{tool}`: `{"decision":"allow"|"deny"}`. One rule per request (never an array). An allow is refused 409 `approval_policy_locked` on a locked policy and 409 `approval_policy_active` on an `"always"` policy (allow rules are never consulted there); a deny is always accepted. Optional `If-Match` (the `ETag` from `GET /approval`): a stale one is 412 with the current `ETag`; the answer carries the new `ETag`. Emits `event.permission_rules` and `event.approval_policy_changed`. A rule write is durable-or-nothing: if the durable write fails the rule is rolled back, the answer is 503 `policy_commit_failed`, and nothing is broadcast, because nothing was announced to retract. Every `event.permission_rules` frame therefore carries rules that are persisted. The frame is still a snapshot to converge on rather than a receipt for this request: another client's write, or an in-session decision, produces one too. ### Parameters | Name | In | Type | Required | Description | |------|-----|------|----------|-------------| | `id` | path | string | Yes | The session id. | | `tool` | path | string | Yes | The tool. | | `If-Match` | header | string | No | Conditional-request precondition: the `ETag` from `GET /sessions/{id}/approval` (a quoted policy revision, e.g. `"3"`) or `*`. A mismatch is 412 `precondition_failed`. | | `X-Hoody-Cwd` | header | string | No | Per-request working-directory scope. | | `X-Hoody-Config-Dir` | header | string | No | Per-request `--config-dir` override. | | `X-Hoody-Container` | header | string | No | Per-request bound remote container. | | `X-Hoody-Realm` | header | string | No | Per-request realm selector. | | `realm` | query | string | No | Per-request realm selector (in:query alias of the `X-Hoody-Realm` header). | ### Request body | Field | Type | Required | Description | |-------|------|----------|-------------| | `decision` | string | Yes | `allow` or `deny`. | ```json { "decision": "allow" } ``` ### Response The session's permission rules after the change. ```json { "status": "ok", "rules": [ { "tool": "bash", "decision": "allow" } ] } ``` | Field | Type | Description | |-------|------|-------------| | `status` | string | `"ok"`. | | `rules` | array | Session permission rules. | | `rules[].tool` | string | Rule key (a tool name, or `tool:action`). | | `rules[].decision` | string | `allow` or `deny`. | ```json { "code": "bad_request", "message": "invalid request" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `bad_request` | Bad request | The request was malformed or carried invalid parameters. | Correct the request body or query parameters. | | `realm_scope_unsupported` | Realm scope unsupported | A per-request realm header was supplied to an active-only or global-no-realm RPC. | Omit the realm header on this route, or open a session to scope by realm. | | `invalid_realm` | Invalid realm selector | The realm selector is malformed (not ""/"global"/a 24-hex id). | Pass a valid realm selector. | ```json { "code": "forbidden", "message": "request must arrive through the Hoody proxy" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `forbidden` | Forbidden (Source IP Guard) | The request did not come through the program's URL. Hoody Kit programs are reached through their URLs only. | Call the agent through its URL, for example with `hoody agent …`. | ```json { "code": "not_found", "message": "resource not found" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `not_found` | Not found | The requested resource does not exist. | Verify the path and identifier. | ```json { "code": "approval_policy_locked", "message": "the approval policy is locked" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `approval_policy_locked` | Approval policy locked | The session's approval policy is locked for its lifetime. | Create a new session with the posture you need; a locked policy is never unlocked. | | `approval_policy_active` | Approval policy active | This session requires an explicit decision on every action, so an automatic-answer alias (the `auto_approve` gate posture, arming YOLO, an allow rule) is refused. | Answer each gate through `POST /sessions/{id}/confirm`, or change the policy first (`PUT /sessions/{id}/approval`) if it is not locked. | ```json { "code": "precondition_failed", "message": "the approval policy revision changed; refetch and retry with the current ETag", "details": { "revision": 4 } } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `precondition_failed` | Precondition failed | `If-Match` named a policy revision that is no longer current. The response `ETag` carries the current revision (also under `details.revision`). | Refetch `GET /sessions/{id}/approval` (or use the `ETag` on this response) and retry. | ```json { "code": "payload_too_large", "message": "request body exceeds the configured size limit" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `payload_too_large` | Payload too large | The request body exceeds the size limit (default 8 MiB). | Reduce the request body below the configured limit (default 8 MiB); split a large payload into smaller requests. | ```json { "code": "rate_limited", "message": "request rate limit exceeded" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `rate_limited` | Too many requests | The per-client request rate limit was exceeded. | Honor the `Retry-After` header and retry; reduce the request rate. | ```json { "code": "internal_error", "message": "internal server error" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `internal_error` | Internal error | An unexpected error occurred while handling the request. | Retry; if persistent, inspect the daemon logs. | ```json { "code": "policy_commit_failed", "message": "the approval policy could not be persisted; nothing was acknowledged" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `service_unavailable` | Service unavailable | The daemon could not service the request. | Honor `Retry-After` and retry. | | `policy_commit_failed` | Policy commit failed | The daemon could not durably write the policy, so the change was NOT acknowledged (a locked policy is never acknowledged unlocked). | Retry; if it persists, check the daemon's session store (disk or permissions). | | `restriction_unknown` | Login restriction unknown | The agent cannot read yet which realms its login may serve. | Retry after the restriction is readable. | ### SDK usage ```ts import { HoodyClient } from 'hoody-sdk'; const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN }); await client.agent.sessions.setApprovalRule('{session_id}', 'bash', { decision: 'allow' }); ``` ```bash curl -X PUT "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/sessions/{session_id}/approval/rules/bash" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "decision": "allow" }' ``` ## Remove a session permission rule ### `DELETE /api/v1/agent/sessions/{id}/approval/rules/{tool}` Clears the session permission rule for `{tool}` (re-arming the per-call prompt). Refused 409 `approval_policy_locked` on a locked policy (rules cannot be cleared there). Optional `If-Match`, as on the PUT (412 when stale). Emits `event.permission_rules` and `event.approval_policy_changed`. A rule write is durable-or-nothing: if the durable write fails the rule is rolled back, the answer is 503 `policy_commit_failed`, and nothing is broadcast. ### Parameters | Name | In | Type | Required | Description | |------|-----|------|----------|-------------| | `id` | path | string | Yes | The session id. | | `tool` | path | string | Yes | The tool. | | `If-Match` | header | string | No | Conditional-request precondition: the `ETag` from `GET /sessions/{id}/approval` (a quoted policy revision, e.g. `"3"`) or `*`. A mismatch is 412 `precondition_failed`. | | `X-Hoody-Cwd` | header | string | No | Per-request working-directory scope. | | `X-Hoody-Config-Dir` | header | string | No | Per-request `--config-dir` override. | | `X-Hoody-Container` | header | string | No | Per-request bound remote container. | | `X-Hoody-Realm` | header | string | No | Per-request realm selector. | | `realm` | query | string | No | Per-request realm selector (in:query alias of the `X-Hoody-Realm` header). | This endpoint takes no body. ### Response The session's permission rules after the change. ```json { "status": "ok", "rules": [] } ``` | Field | Type | Description | |-------|------|-------------| | `status` | string | `"ok"`. | | `rules` | array | Session permission rules. | | `rules[].tool` | string | Rule key (a tool name, or `tool:action`). | | `rules[].decision` | string | `allow` or `deny`. | ```json { "code": "bad_request", "message": "invalid request" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `bad_request` | Bad request | The request was malformed or carried invalid parameters. | Correct the request body or query parameters. | | `realm_scope_unsupported` | Realm scope unsupported | A per-request realm header was supplied to an active-only or global-no-realm RPC. | Omit the realm header on this route, or open a session to scope by realm. | | `invalid_realm` | Invalid realm selector | The realm selector is malformed (not ""/"global"/a 24-hex id). | Pass a valid realm selector. | ```json { "code": "forbidden", "message": "request must arrive through the Hoody proxy" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `forbidden` | Forbidden (Source IP Guard) | The request did not come through the program's URL. Hoody Kit programs are reached through their URLs only. | Call the agent through its URL, for example with `hoody agent …`. | ```json { "code": "not_found", "message": "resource not found" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `not_found` | Not found | The requested resource does not exist. | Verify the path and identifier. | ```json { "code": "approval_policy_locked", "message": "the approval policy is locked" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `approval_policy_locked` | Approval policy locked | The session's approval policy is locked for its lifetime. | Create a new session with the posture you need; a locked policy is never unlocked. | | `approval_policy_active` | Approval policy active | This session requires an explicit decision on every action, so an automatic-answer alias is refused. | Answer each gate through `POST /sessions/{id}/confirm`, or change the policy first (`PUT /sessions/{id}/approval`) if it is not locked. | ```json { "code": "precondition_failed", "message": "the approval policy revision changed; refetch and retry with the current ETag", "details": { "revision": 4 } } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `precondition_failed` | Precondition failed | `If-Match` named a policy revision that is no longer current. The response `ETag` carries the current revision (also under `details.revision`). | Refetch `GET /sessions/{id}/approval` (or use the `ETag` on this response) and retry. | ```json { "code": "rate_limited", "message": "request rate limit exceeded" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `rate_limited` | Too many requests | The per-client request rate limit was exceeded. | Honor the `Retry-After` header and retry; reduce the request rate. | ```json { "code": "internal_error", "message": "internal server error" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `internal_error` | Internal error | An unexpected error occurred while handling the request. | Retry; if persistent, inspect the daemon logs. | ```json { "code": "policy_commit_failed", "message": "the approval policy could not be persisted; nothing was acknowledged" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `service_unavailable` | Service unavailable | The daemon could not service the request. | Honor `Retry-After` and retry. | | `policy_commit_failed` | Policy commit failed | The daemon could not durably write the policy, so the change was NOT acknowledged (a locked policy is never acknowledged unlocked). | Retry; if it persists, check the daemon's session store (disk or permissions). | | `restriction_unknown` | Login restriction unknown | The agent cannot read yet which realms its login may serve. | Retry after the restriction is readable. | ### SDK usage ```ts import { HoodyClient } from 'hoody-sdk'; const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN }); await client.agent.sessions.deleteApprovalRule('{session_id}', 'bash'); ``` ```bash curl -X DELETE "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/sessions/{session_id}/approval/rules/bash" \ -H "Authorization: Bearer $HOODY_TOKEN" ``` ## Arm or disarm YOLO ### `PATCH /api/v1/agent/sessions/{id}/yolo` Flips the session's YOLO auto-approve on or off, the same power as the TUI's lightning toggle, including clearing actions the agent otherwise reserves for a person (a caller that can reach this API is treated as having that permission). A locked policy refuses it 409 `approval_policy_locked` and an `"always"` policy refuses it 409 `approval_policy_active`, rather than dropping it silently. Returns the applied `{yolo}`. Emits `event.yolo_mode`. ### Parameters | Name | In | Type | Required | Description | |------|-----|------|----------|-------------| | `id` | path | string | Yes | The session id. | | `X-Hoody-Cwd` | header | string | No | Per-request working-directory scope. | | `X-Hoody-Config-Dir` | header | string | No | Per-request `--config-dir` override. | | `X-Hoody-Container` | header | string | No | Per-request bound remote container. | | `X-Hoody-Realm` | header | string | No | Per-request realm selector. | | `realm` | query | string | No | Per-request realm selector (in:query alias of the `X-Hoody-Realm` header). | ### Request body | Field | Type | Required | Description | |-------|------|----------|-------------| | `enabled` | boolean | Yes | `true` to arm auto-approve, `false` to disarm. | ```json { "enabled": true } ``` ### Response Applied YOLO state. ```json { "status": "ok", "yolo": true } ``` | Field | Type | Description | |-------|------|-------------| | `status` | string | `"ok"`. | | `yolo` | boolean | The applied auto-approve state. | | `forwarded` | boolean | `true` when the command was delivered but not acknowledged within the wait; observe `event.yolo_mode`. | ```json { "code": "bad_request", "message": "invalid request" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `bad_request` | Bad request | The request was malformed or carried invalid parameters. | Correct the request body or query parameters. | | `realm_scope_unsupported` | Realm scope unsupported | A per-request realm header was supplied to an active-only or global-no-realm RPC. | Omit the realm header on this route, or open a session to scope by realm. | | `invalid_realm` | Invalid realm selector | The realm selector is malformed (not ""/"global"/a 24-hex id). | Pass a valid realm selector. | ```json { "code": "forbidden", "message": "request must arrive through the Hoody proxy" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `forbidden` | Forbidden (Source IP Guard) | The request did not come through the program's URL. Hoody Kit programs are reached through their URLs only. | Call the agent through its URL, for example with `hoody agent …`. | ```json { "code": "not_found", "message": "resource not found" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `not_found` | Not found | The requested resource does not exist. | Verify the path and identifier. | ```json { "code": "approval_policy_locked", "message": "the approval policy is locked" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `approval_policy_locked` | Approval policy locked | The session's approval policy is locked for its lifetime. | Create a new session with the posture you need; a locked policy is never unlocked. | | `approval_policy_active` | Approval policy active | This session requires an explicit decision on every action, so arming YOLO is refused. | Answer each gate through `POST /sessions/{id}/confirm`, or change the policy first (`PUT /sessions/{id}/approval`) if it is not locked. | ```json { "code": "payload_too_large", "message": "request body exceeds the configured size limit" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `payload_too_large` | Payload too large | The request body exceeds the size limit (default 8 MiB). | Reduce the request body below the configured limit (default 8 MiB); split a large payload into smaller requests. | ```json { "code": "rate_limited", "message": "request rate limit exceeded" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `rate_limited` | Too many requests | The per-client request rate limit was exceeded. | Honor the `Retry-After` header and retry; reduce the request rate. | ```json { "code": "internal_error", "message": "internal server error" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `internal_error` | Internal error | An unexpected error occurred while handling the request. | Retry; if persistent, inspect the daemon logs. | ```json { "code": "service_unavailable", "message": "service unavailable" } ``` | Error Code | Title | Description | Resolution | |------------|-------|-------------|------------| | `service_unavailable` | Service unavailable | The daemon could not service the request. | Honor `Retry-After` and retry. | ### SDK usage ```ts import { HoodyClient } from 'hoody-sdk'; const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN }); await client.agent.sessions.setYolo('{session_id}', { enabled: true }); ``` ```bash curl -X PATCH "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/sessions/{session_id}/yolo" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "enabled": true }' ```