diff --git a/docs/migration-to-agend-terminal.md b/docs/migration-to-agend-terminal.md index f7705ec9..81c3e878 100644 --- a/docs/migration-to-agend-terminal.md +++ b/docs/migration-to-agend-terminal.md @@ -215,43 +215,47 @@ channel: **Other int-vs-string parity worth knowing:** `instances..topic_id` is also strictly `Option` on Rust (fleet.rs:160) — bare int only. -### High-friction change #3: `outbound_capabilities` is a Rust-only required field +### High-friction change #3: `outbound_capabilities` is a Rust-only field with a deliberately-different default than `user_allowlist` -**Reference:** Sprint 22 P0 PR [#230](https://github.com/suzuke/agend-terminal/pull/230). Schema doc-comment at [`src/fleet.rs:173-208`](https://github.com/suzuke/agend-terminal/blob/main/src/fleet.rs#L173-L208); enforcement helper at [`src/channel/auth.rs::gate_outbound_for_agent`](https://github.com/suzuke/agend-terminal/blob/main/src/channel/auth.rs); enum at the same file's `ChannelOpKind`. Operator deeper-dive: [`docs/MIGRATION-OUTBOUND-CAPS.md`](https://github.com/suzuke/agend-terminal/blob/main/docs/MIGRATION-OUTBOUND-CAPS.md). +**Reference:** Sprint 22 P0 PR [#230](https://github.com/suzuke/agend-terminal/pull/230) introduced this field with a fail-closed default; Sprint 23 P1 PR [#242](https://github.com/suzuke/agend-terminal/pull/242) reversed the absent-state default to **default-open** per operator philosophy ("absent ≡ all permitted"). Schema doc-comment at [`src/fleet.rs:173-208`](https://github.com/suzuke/agend-terminal/blob/main/src/fleet.rs#L173-L208); enum at [`src/channel/auth.rs::ChannelOpKind`](https://github.com/suzuke/agend-terminal/blob/main/src/channel/auth.rs). Operator deeper-dive (the Sprint 23 P1 section reflects current default-open semantics): [`docs/MIGRATION-OUTBOUND-CAPS.md`](https://github.com/suzuke/agend-terminal/blob/main/docs/MIGRATION-OUTBOUND-CAPS.md). -`@suzuke/agend` has no equivalent — outbound channel ACLs were implicit ("any tool surface is callable by any instance"). `agend-terminal` introduces `instances..outbound_capabilities: Vec` to gate which agent-driven channel ops each instance may invoke. The four current variants (snake_case in YAML → `ChannelOpKind` variant): `reply` → `Reply`, `react` → `React`, `edit` → `Edit`, `inject_provenance` → `InjectProvenance`. +`@suzuke/agend` has no equivalent — outbound channel ACLs were implicit ("any tool surface is callable by any instance"). `agend-terminal` introduces `instances..outbound_capabilities: Vec` so an operator who *wants* to restrict an instance's channel ops can do so. The four current variants (snake_case in YAML → `ChannelOpKind` variant): `reply` → `Reply`, `react` → `React`, `edit` → `Edit`, `inject_provenance` → `InjectProvenance`. -**Tri-state semantics** (mirrors `channel.user_allowlist` from High-friction #1): +**Decision matrix (post-PR #242 default-open):** -| YAML | Sprint 22 P0 (current) | Sprint 23 (next) | -|---|---|---| -| `outbound_capabilities: [reply, react]` | listed ops permitted | same | -| `outbound_capabilities: []` | fail-closed (explicit no agent outbound) | same | -| key absent | FATAL warn-but-permit one daemon cycle | hard parse error | +| YAML | Behaviour | +|---|---| +| key absent | all ops permitted (default-open) | +| `outbound_capabilities: [reply, react]` | only listed ops permitted | +| `outbound_capabilities: []` | all rejected (explicit opt-out, retained from Sprint 22 P0) | -**Symptom when key is absent (Sprint 22 P0 grace window).** The daemon emits via `tracing::error!`: +There is **no FATAL warn-but-permit grace cycle** and **no hard-parse-error promotion**. Sprint 22 P0's gradual-migration scaffolding was removed by Sprint 23 P1 along with the original fail-closed default. Built-in coordinators (`general` and any future auto-created coordinator) inherit the default-open behaviour like every other instance — there is no special auto-inject anymore. -``` -ERROR FATAL (warn-but-permit one daemon cycle): instance '' outbound_capabilities NOT SET. Sprint 22 P0 grants this call under gradual-migration grace. ... -``` +**Why is this default different from `user_allowlist`?** This is a deliberate threat-model distinction, not an inconsistency: -The op proceeds — but the warning is fired once per instance per daemon process (mutex-guarded `HashSet`, rate-limited so no log spam). When Sprint 23 ships, the absent state becomes a hard parse error and the daemon refuses to load the config. Do not depend on the grace window past Sprint 22 P0. +| Gate | Scope | Default | Threat model | +|---|---|---|---| +| `channel.user_allowlist` (High-friction #1) | per-channel, channel ingress | **fail-closed** | a channel-wide allowlist that defaults open is one credential leak away from any internet user being able to drive your fleet — silent operator regression possible. | +| `outbound_capabilities` | per-instance, per-op outbound | **default-open** | a per-instance allowlist that defaults closed forces every operator-defined instance to declare every op, even though the operator is the one running every instance — over-specification without a corresponding cascade-attack risk. | -**Built-in coordinators are auto-injected.** The `general` instance (and any future auto-created coordinator) gets `[reply, react, edit, inject_provenance]` injected automatically at startup by [`bootstrap::fleet_normalize::auto_create_general`](https://github.com/suzuke/agend-terminal/blob/main/src/bootstrap/fleet_normalize.rs#L23-L87) (the linked range covers both the `default_built_in_outbound_capabilities()` helper that defines the four-cap default and the `auto_create_general` fn that consumes it). **User-authored YAML entries do not get auto-inject** — every operator-defined instance must declare `outbound_capabilities` explicitly before Sprint 23. +Both gates are correct for their threat model. When porting from `@suzuke/agend`, you must populate `channel.user_allowlist` (or the bot will silently fail to reply); you may leave `outbound_capabilities` unset on operator-defined instances unless you actually want to restrict an instance's channel ops. -**Migration action.** When porting an existing `@suzuke/agend` `fleet.yaml`, add `outbound_capabilities` to every operator-authored instance: +**Migration action.** When porting an existing `@suzuke/agend` `fleet.yaml`, `outbound_capabilities` is **optional** for operator-authored instances — leave it unset to preserve TS-equivalent permissive behaviour: ```yaml instances: worker-a: working_directory: /path/to/repo - outbound_capabilities: [reply, react] # explicit; matches the channel ops the agent should use + # outbound_capabilities omitted — defaults to all ops permitted (TS-equivalent) worker-b: working_directory: /path/to/other-repo - outbound_capabilities: [] # explicit lockdown — agent cannot call channel ops + outbound_capabilities: [reply, react] # explicitly restrict to these ops only + worker-c: + working_directory: /path/to/audit-repo + outbound_capabilities: [] # explicit lockdown — agent cannot call any channel op ``` -For the full `ChannelOpKind` enum reference, the rationale behind the 2-stage transition timeline, and cross-channel architecture notes (Telegram-vs-Discord shared gate behaviour), see the operator deeper-dive at [`docs/MIGRATION-OUTBOUND-CAPS.md`](https://github.com/suzuke/agend-terminal/blob/main/docs/MIGRATION-OUTBOUND-CAPS.md). +For the full `ChannelOpKind` enum reference, the Sprint 23 P1 reversal rationale, and cross-channel architecture notes (Telegram-vs-Discord shared gate behaviour), see the operator deeper-dive at [`docs/MIGRATION-OUTBOUND-CAPS.md`](https://github.com/suzuke/agend-terminal/blob/main/docs/MIGRATION-OUTBOUND-CAPS.md). ### Top-level keys @@ -301,7 +305,7 @@ For the full `ChannelOpKind` enum reference, the rationale behind the 2-stage tr | Rust field | Type | Purpose | |---|---|---| -| `outbound_capabilities` | `Vec` | **Required for every operator-authored instance from Sprint 23.** Gates which agent-driven channel ops (`reply` / `react` / `edit` / `inject_provenance`) the instance may invoke. Tri-state semantics + 2-stage timeline + built-in auto-inject for `general` — see High-friction #3 above. | +| `outbound_capabilities` | `Vec` | Per-instance gate over agent-driven channel ops (`reply` / `react` / `edit` / `inject_provenance`). **Default-open since Sprint 23 P1 (PR #242)** — leave unset to preserve TS-equivalent permissive behaviour, or list specific ops to restrict, or set `[]` for full lockdown. See High-friction #3 above for the threat-model rationale and decision matrix. | | `receive_fleet_updates` | `Option` | Default opt-in. Set `false` on instances that should not see fleet `` injections. | | `cols`, `rows` | `Option` | Override PTY size for the instance's terminal. | | `env` | `HashMap` | Per-instance env additions. Note: Rust filters credential-like keys per `agent.rs::SENSITIVE_ENV_KEYS` — secrets injected here may be redacted. | diff --git a/docs/migration-to-agend-terminal.zh-TW.md b/docs/migration-to-agend-terminal.zh-TW.md index e678a341..a018840d 100644 --- a/docs/migration-to-agend-terminal.zh-TW.md +++ b/docs/migration-to-agend-terminal.zh-TW.md @@ -213,43 +213,47 @@ channel: **其他值得注意的 int-vs-string parity:** `instances..topic_id` 在 Rust 也是嚴格 `Option` (fleet.rs:160) —— 僅裸 int。 -### 高摩擦變更 #3:`outbound_capabilities` 是 Rust 端必填的新增欄位 +### 高摩擦變更 #3:`outbound_capabilities` 是 Rust 獨有欄位,且預設刻意與 `user_allowlist` 不同 -**參考:** Sprint 22 P0 PR [#230](https://github.com/suzuke/agend-terminal/pull/230)。Schema doc-comment 在 [`src/fleet.rs:173-208`](https://github.com/suzuke/agend-terminal/blob/main/src/fleet.rs#L173-L208);enforcement helper 在 [`src/channel/auth.rs::gate_outbound_for_agent`](https://github.com/suzuke/agend-terminal/blob/main/src/channel/auth.rs);enum 在同檔案的 `ChannelOpKind`。Operator 完整參考:[`docs/MIGRATION-OUTBOUND-CAPS.md`](https://github.com/suzuke/agend-terminal/blob/main/docs/MIGRATION-OUTBOUND-CAPS.md)。 +**參考:** Sprint 22 P0 PR [#230](https://github.com/suzuke/agend-terminal/pull/230) 引入此欄位、原採 fail-closed 預設;Sprint 23 P1 PR [#242](https://github.com/suzuke/agend-terminal/pull/242) 依 operator 哲學「沒顯式寫入即視為全開」把 absent 的預設反轉為 **default-open**。Schema doc-comment 在 [`src/fleet.rs:173-208`](https://github.com/suzuke/agend-terminal/blob/main/src/fleet.rs#L173-L208);enum 在 [`src/channel/auth.rs::ChannelOpKind`](https://github.com/suzuke/agend-terminal/blob/main/src/channel/auth.rs)。Operator 完整參考(Sprint 23 P1 區段反映目前 default-open 語意):[`docs/MIGRATION-OUTBOUND-CAPS.md`](https://github.com/suzuke/agend-terminal/blob/main/docs/MIGRATION-OUTBOUND-CAPS.md)。 -`@suzuke/agend` 沒有對等欄位 —— outbound channel ACL 過去是隱式的(「任何工具任何 instance 都能呼叫」)。`agend-terminal` 引入 `instances..outbound_capabilities: Vec`,用來限制每個 instance 可呼叫哪些 agent-driven channel ops。目前四個變體 (YAML 用 snake_case → 對應 `ChannelOpKind` variant):`reply` → `Reply`、`react` → `React`、`edit` → `Edit`、`inject_provenance` → `InjectProvenance`。 +`@suzuke/agend` 沒有對等欄位 —— outbound channel ACL 過去是隱式的(「任何工具任何 instance 都能呼叫」)。`agend-terminal` 引入 `instances..outbound_capabilities: Vec`,讓 *想要* 限制某 instance channel ops 的 operator 可以這麼做。目前四個變體 (YAML 用 snake_case → 對應 `ChannelOpKind` variant):`reply` → `Reply`、`react` → `React`、`edit` → `Edit`、`inject_provenance` → `InjectProvenance`。 -**三態語意** (與高摩擦變更 #1 的 `channel.user_allowlist` 同模式): +**Decision matrix(post-PR #242 default-open):** -| YAML | Sprint 22 P0(目前) | Sprint 23(下一階段) | -|---|---|---| -| `outbound_capabilities: [reply, react]` | 列出的 ops 允許 | 同 | -| `outbound_capabilities: []` | fail-closed(明確不允許 agent outbound) | 同 | -| 鍵不存在 | FATAL warn-but-permit one daemon cycle | hard parse error | +| YAML | 行為 | +|---|---| +| 鍵不存在 | 全部 ops 允許(default-open) | +| `outbound_capabilities: [reply, react]` | 只有列出的 ops 允許 | +| `outbound_capabilities: []` | 全部拒絕(明確 opt-out,自 Sprint 22 P0 保留) | -**鍵不存在時的症狀(Sprint 22 P0 grace 期間)。** Daemon 透過 `tracing::error!` 發: +**沒有 FATAL warn-but-permit 過渡期**,**也沒有 hard-parse-error 升級**。Sprint 22 P0 為了 gradual migration 加的 scaffolding 在 Sprint 23 P1 連同原本的 fail-closed 預設一起被拿掉了。Built-in coordinator(`general` 與未來自動建立的 coordinator)和其他 instance 一樣繼承 default-open —— 不再有特殊的 auto-inject。 -``` -ERROR FATAL (warn-but-permit one daemon cycle): instance '' outbound_capabilities NOT SET. Sprint 22 P0 grants this call under gradual-migration grace. ... -``` +**為什麼這個預設和 `user_allowlist` 不同?** 這是刻意的 threat-model 區分,**不是** inconsistency: -該 op 仍會繼續執行 —— 但這條 warning 在每個 daemon process 內每個 instance 只會發一次(以 mutex-guarded `HashSet` 限速,不會 spam log)。Sprint 23 ship 後,鍵不存在會變成 hard parse error,daemon 會直接拒絕載入 config。**不要依賴 grace window 撐到 Sprint 22 P0 之後**。 +| Gate | Scope | 預設 | Threat model | +|---|---|---|---| +| `channel.user_allowlist` (高摩擦變更 #1) | per-channel,channel ingress | **fail-closed** | channel-wide allowlist 預設開只要一次 credential 外洩就能讓網路上任何人驅動你的 fleet —— 安靜的 operator regression 是可能的。 | +| `outbound_capabilities` | per-instance,per-op outbound | **default-open** | per-instance allowlist 預設關會強迫每個 operator-defined instance 都列出每個 op,但每個 instance 本來就由 operator 自己起 —— 過度規範卻對應不到實質的 cascade-attack 風險。 | -**Built-in coordinator 自動注入。** `general` instance(以及未來自動建立的 coordinator)會在啟動時由 [`bootstrap::fleet_normalize::auto_create_general`](https://github.com/suzuke/agend-terminal/blob/main/src/bootstrap/fleet_normalize.rs#L23-L87) 自動注入 `[reply, react, edit, inject_provenance]`(連結範圍同時涵蓋定義四項預設值的 `default_built_in_outbound_capabilities()` helper 與消費它的 `auto_create_general` fn)。**User 自己寫的 YAML entry 不會自動注入** —— Sprint 23 之前,operator 定義的每個 instance 都必須明確宣告 `outbound_capabilities`。 +兩個 gate 各自符合自己的 threat model。從 `@suzuke/agend` 遷移過來時,你 **必須** 填 `channel.user_allowlist`(否則 bot 會安靜地不回覆);至於 `outbound_capabilities`,operator 自己定義的 instance 上可以不填 —— 除非你真的想限制某 instance 的 channel ops。 -**遷移動作。** 把既有 `@suzuke/agend` `fleet.yaml` 移到 `agend-terminal` 時,給每一個 operator 自寫的 instance 加上 `outbound_capabilities`: +**遷移動作。** 把既有 `@suzuke/agend` `fleet.yaml` 移到 `agend-terminal` 時,operator 自寫的 instance 上 `outbound_capabilities` 是 **選填** —— 不填即保留 TS 的開放行為: ```yaml instances: worker-a: working_directory: /path/to/repo - outbound_capabilities: [reply, react] # 明確列出 agent 應該用的 channel ops + # outbound_capabilities 不填 —— 預設全部 ops 允許 (TS-equivalent) worker-b: working_directory: /path/to/other-repo - outbound_capabilities: [] # 明確 lockdown —— agent 不可呼叫 channel ops + outbound_capabilities: [reply, react] # 明確只允許這些 ops + worker-c: + working_directory: /path/to/audit-repo + outbound_capabilities: [] # 明確 lockdown —— agent 不可呼叫任何 channel op ``` -完整的 `ChannelOpKind` enum 參考、2-stage transition timeline 的 rationale、以及跨 channel 架構備註(Telegram-vs-Discord 共用 gate 的行為),請見 operator 完整參考 [`docs/MIGRATION-OUTBOUND-CAPS.md`](https://github.com/suzuke/agend-terminal/blob/main/docs/MIGRATION-OUTBOUND-CAPS.md)。 +完整的 `ChannelOpKind` enum 參考、Sprint 23 P1 反轉的 rationale、以及跨 channel 架構備註(Telegram-vs-Discord 共用 gate 的行為),請見 operator 完整參考 [`docs/MIGRATION-OUTBOUND-CAPS.md`](https://github.com/suzuke/agend-terminal/blob/main/docs/MIGRATION-OUTBOUND-CAPS.md)。 ### Top-level keys @@ -299,7 +303,7 @@ instances: | Rust 欄位 | 型別 | 用途 | |---|---|---| -| `outbound_capabilities` | `Vec` | **Sprint 23 起,operator 自寫的每個 instance 都必填。** 限制該 instance 可呼叫哪些 agent-driven channel ops(`reply` / `react` / `edit` / `inject_provenance`)。三態語意 + 2-stage timeline + `general` 自動注入 —— 詳見上方「高摩擦變更 #3」。 | +| `outbound_capabilities` | `Vec` | Per-instance gate,限制 agent-driven channel ops(`reply` / `react` / `edit` / `inject_provenance`)。**Sprint 23 P1 (PR #242) 起為 default-open** —— 不填即保留 TS-equivalent 的開放行為,列出特定 ops 即收斂,設 `[]` 則完全 lockdown。Threat-model rationale 與 decision matrix 詳見上方「高摩擦變更 #3」。 | | `receive_fleet_updates` | `Option` | 預設 opt-in。對不該收到 fleet `` 注入的 instance 設 `false`。 | | `cols`、`rows` | `Option` | 覆寫該 instance 的 PTY 尺寸。 | | `env` | `HashMap` | Per-instance 加 env。注意:Rust 會依 `agent.rs::SENSITIVE_ENV_KEYS` 過濾類 credential 鍵 —— 從這裡注入的 secrets 可能會被 redacted。 |