@agfpd/iapeer 0.4.90 → 0.4.91

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  # 17 — Human approval
2
2
 
3
- A peer can run in one of two **approval modes**. In `yolo` (the default — every peer today) the agent acts autonomously: the runtime is launched with its bypass flag and nothing asks a human before a tool runs. In `gated` the peer is launched **without** bypass, and every blocking approval the runtime would raise is routed to a **human** through the daemon's approval broker — a Telegram button, a tray click, or `iapeer approve` on the CLI. The human's `allow`/`deny` (with a reason) flows back into the runtime and the tool proceeds or is refused.
3
+ A peer can run in one of two **approval modes**. In `yolo` (the default — every peer today) the agent acts autonomously: the runtime is launched with its bypass flag and nothing asks a human before a tool runs. In `gated` the peer is launched **without** bypass, and every blocking approval the runtime would raise is routed to a **human** through the daemon's approval broker — a Telegram button, a tray click, `iapeer approve` on the CLI, or the web console's approvals panel. The human's `allow`/`deny` (with a reason) flows back into the runtime and the tool proceeds or is refused.
4
4
 
5
5
  This is a **normative contract**. The approval broker is the daemon's, the fleet endpoints and `ev` kinds below are the surface that operator faces (CLI, tray, the Telegram runtime) are written against — not the daemon source. The interception facts (which runtime event fires, in which shape) are pinned to **Claude Code 2.1.201** and **codex-cli 0.142.5**, taken from the live binaries + current runtime docs.
6
6
 
@@ -103,7 +103,7 @@ An unknown tool falls back to a pretty-printed `tool_input`, so nothing is opaqu
103
103
 
104
104
  ## The broker (single source of truth)
105
105
 
106
- The approval queue lives **in the daemon** (the always-on process). Every channel that asks (the runtime hook, the supervisor breaker) and every channel that answers (CLI, tray, the Telegram runtime) is an interface to this one queue: a decision from any face resolves the request everywhere. In-memory + ephemeral — pending requests do **not** survive a daemon restart (the blocking hook connection breaks → the hook fails safe to deny); the durable `approvals.log` is the audit trace, not a recovery store.
106
+ The approval queue lives **in the daemon** (the always-on process). Every channel that asks (the runtime hook, the supervisor breaker) and every channel that answers (CLI, tray, the Telegram runtime, the web console) is an interface to this one queue: a decision from any face resolves the request everywhere. In-memory + ephemeral — pending requests do **not** survive a daemon restart (the blocking hook connection breaks → the hook fails safe to deny); the durable `approvals.log` is the audit trace, not a recovery store.
107
107
 
108
108
  Flow: the gated peer's hook (or the supervisor breaker) blocks on `POST /fleet/v1/approvals` → the broker enqueues, emits `approval-request` (to `approvals.log` → SSE), and holds the connection → a face answers via `POST /fleet/v1/approvals/<id>/(approve|deny)` → the promise resolves, the daemon writes the runtime hook JSON back, the tool proceeds or is blocked with the reason.
109
109
 
@@ -118,7 +118,7 @@ The approval surface is served on both daemon listeners under `/fleet/v1`, same
118
118
  **Events** (compact logfmt line → SSE JSON; the full content is *not* in the event — read the queue for it):
119
119
 
120
120
  - `approval-request` — `id, personality, runtime, kind, tool, summary, created, expires, approvers`.
121
- - `approval-resolved` — `id, personality, runtime, decision (allow|deny), reason, by (approver), via (cli|tray|telegram|timeout|disconnect), latencyMs`.
121
+ - `approval-resolved` — `id, personality, runtime, decision (allow|deny), reason, by (approver), via (cli|tray|telegram|web|timeout|disconnect), latencyMs`. `via` is the surface the decision came from, **self-declared by the answering face** in its `POST` body (free-form-tolerant, like `kind`; `timeout`/`disconnect` are broker-internal). A face that did not self-identify gets the field **omitted** — the audit line never guesses a surface.
122
122
 
123
123
  **Endpoints:**
124
124
 
@@ -126,12 +126,14 @@ The approval surface is served on both daemon listeners under `/fleet/v1`, same
126
126
  |---|---|
127
127
  | `GET /fleet/v1/approvals` | list pending requests (full items) |
128
128
  | `GET /fleet/v1/approvals/<id>` | one pending request, full `content` (the multi-line diff / plan / command) |
129
- | `POST /fleet/v1/approvals/<id>/approve` | `{approver?}` → allow |
130
- | `POST /fleet/v1/approvals/<id>/deny` | `{reason?, approver?}` → deny (reason to the model) |
129
+ | `POST /fleet/v1/approvals/<id>/approve` | `{approver?, via?}` → allow |
130
+ | `POST /fleet/v1/approvals/<id>/deny` | `{reason?, approver?, via?}` → deny (reason to the model) |
131
131
  | `POST /fleet/v1/approvals` | the **blocking long-poll** the asking hook/breaker holds: `{personality, runtime, kind, tool, content, summary?}` → `{id, decision, reason?}` |
132
132
 
133
133
  `kind` is the taxonomy tag (`tool` | `plan` | `question` | `circuit-breaker`; free-form-tolerant). So the SSE/badge stays light while the full content is available in every channel through the two `GET`s.
134
134
 
135
+ `via` on approve/deny is the answering face's **self-identification of its surface** for the audit trail: the CLI sends `cli`, the tray `tray`, the Telegram runtime `telegram`, the web console `web`. A face MUST send it (the log omits `via` otherwise); the broker passes it through verbatim into `approvals.log`.
136
+
135
137
  ## CLI
136
138
 
137
139
  - `iapeer approvals [--json]` — the pending queue: personality · runtime · kind · **the verbatim command/content** · age · id.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agfpd/iapeer",
3
- "version": "0.4.90",
3
+ "version": "0.4.91",
4
4
  "description": "Run Claude Code and Codex CLI as local background agents that message each other, wake on demand, and share one memory.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "agfpd",
@@ -1,6 +1,6 @@
1
1
  // Approval broker — the in-daemon queue that is the SINGLE SOURCE OF TRUTH for
2
2
  // human-approval requests (docs/17). Every channel (the runtime hook that ASKS, and
3
- // the CLI / tray / telegram faces that ANSWER) is an interface to THIS one queue: a
3
+ // the CLI / tray / telegram / web-console faces that ANSWER) is an interface to THIS one queue: a
4
4
  // decision from any face resolves the request everywhere (the owner's invariant).
5
5
  //
6
6
  // Flow: a gated peer's PreToolUse hook blocks and POSTs /fleet/v1/approvals → the fleet
@@ -56,7 +56,9 @@ export interface ApprovalDecision {
56
56
  export interface ResolveMeta {
57
57
  /** The approver who answered (a nature-peer personality). */
58
58
  by?: string
59
- /** The channel that answered: cli | tray | telegram | timeout | disconnect. */
59
+ /** The surface that answered: cli | tray | telegram | web | timeout | disconnect.
60
+ * Self-declared by the answering face (free-form-tolerant); absent when the face
61
+ * did not self-identify — the audit line then omits `via` rather than guessing. */
60
62
  via?: string
61
63
  }
62
64
 
@@ -636,7 +636,10 @@ export function buildFleetHandler(opts: FleetHandlerOptions = {}): FleetHandler
636
636
  /* empty/invalid body is fine — approve/deny carry only optional fields */
637
637
  }
638
638
  const by = typeof body.approver === 'string' ? body.approver : undefined
639
- const via = typeof body.via === 'string' ? body.via : 'cli'
639
+ // The answering face self-identifies its surface in `via` (cli | tray | telegram | web | …;
640
+ // free-form-tolerant like `kind`). NO default — an audit line must never claim a surface the
641
+ // caller did not state; a face that sent nothing is logged with the field omitted.
642
+ const via = typeof body.via === 'string' ? body.via : undefined
640
643
  const ok =
641
644
  action === 'approve'
642
645
  ? broker.resolve(id, { decision: 'allow', reason: typeof body.reason === 'string' ? body.reason : undefined }, { by, via })