From 38a32ee65ea61e0dee94d638837ca62eee2bbae7 Mon Sep 17 00:00:00 2001 From: suzuke Date: Mon, 27 Apr 2026 12:47:46 +0800 Subject: [PATCH 1/2] fix(migration): add outbound_capabilities to fleet.yaml schema diff (#230 forward-link) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cross-team Sprint 22 P0 PR #230 on agend-terminal introduced a breaking change: `InstanceConfig.outbound_capabilities` flipped from `Option>` to required. Existing `fleet.yaml` files ported from `@suzuke/agend` will silently log a FATAL warn-but-permit line on Sprint 22 P0, then fail to load entirely once Sprint 23 ships. The migration guide must surface this before operators try to port. Adds High-friction change #3 to §3 (between #2 group_id and the top-level keys table) plus a row in the "Rust-only fields you may want to add" table. Mirrors the user_allowlist / High-friction #1 treatment: tri-state semantics table, exact symptom log line, explicit migration action with YAML example, and a forward-link to the operator deeper-dive at agend-terminal/docs/MIGRATION-OUTBOUND-CAPS.md. Content covered: - `instances..outbound_capabilities: Vec` framing as Rust-only (no TS equivalent — TS had implicit channel ACLs). - Four current ChannelOpKind variants + YAML snake_case mapping: reply / react / edit / inject_provenance ↔ Reply / React / Edit / InjectProvenance. - Tri-state semantics: list of ops permitted / `[]` fail-closed / absent FATAL warn-but-permit-one-cycle (Sprint 22 P0) → hard parse error (Sprint 23). - Symptom log line via `tracing::error!`, once-per-instance per daemon process (mutex-guarded HashSet). - Built-in coordinator auto-inject: `general` (and any future auto-created coordinator) gets [reply, react, edit, inject_provenance] via bootstrap::fleet_normalize::auto_create_general (src/bootstrap/fleet_normalize.rs:24-67). User-authored YAML entries are NOT auto-injected. - Migration action with YAML example covering both the explicit-list case and the explicit-empty (lockdown) case. Source pointers all verified against agend-terminal main per dev-impl-2's confirmation (cross-team query, m-20260427044154458574-342): - `src/fleet.rs:173-208` (field doc-comment) - `src/channel/auth.rs::gate_outbound_for_agent` - `src/channel/auth.rs::ChannelOpKind` (enum) - `src/bootstrap/fleet_normalize.rs:24-67` (auto-create-general) - Forward-link to `docs/MIGRATION-OUTBOUND-CAPS.md` for the full enum reference, 2-stage transition rationale, and cross-channel architecture notes. zh-TW pair updated identically. 39 lines added on each side, 78 total. No content rewrite of existing sections; pure addition. Surrogate authority: `d-20260427025246010573-0`. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/migration-to-agend-terminal.md | 39 +++++++++++++++++++++++ docs/migration-to-agend-terminal.zh-TW.md | 39 +++++++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/docs/migration-to-agend-terminal.md b/docs/migration-to-agend-terminal.md index d630efd2..69fc98a5 100644 --- a/docs/migration-to-agend-terminal.md +++ b/docs/migration-to-agend-terminal.md @@ -174,6 +174,44 @@ 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 + +**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). + +`@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`. + +**Tri-state semantics** (mirrors `channel.user_allowlist` from High-friction #1): + +| 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 | + +**Symptom when key is absent (Sprint 22 P0 grace window).** The daemon emits via `tracing::error!`: + +``` +ERROR FATAL (warn-but-permit one daemon cycle): instance '' outbound_capabilities NOT SET. Sprint 22 P0 grants this call under gradual-migration grace. ... +``` + +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. + +**Built-in coordinators are auto-injected.** The `general` instance (and any future auto-created coordinator) gets `[reply, react, edit, inject_provenance]` injected automatically by [`bootstrap::fleet_normalize::auto_create_general`](https://github.com/suzuke/agend-terminal/blob/main/src/bootstrap/fleet_normalize.rs#L24-L67) at startup. **User-authored YAML entries do not get auto-inject** — every operator-defined instance must declare `outbound_capabilities` explicitly before Sprint 23. + +**Migration action.** When porting an existing `@suzuke/agend` `fleet.yaml`, add `outbound_capabilities` to every operator-authored instance: + +```yaml +instances: + worker-a: + working_directory: /path/to/repo + outbound_capabilities: [reply, react] # explicit; matches the channel ops the agent should use + worker-b: + working_directory: /path/to/other-repo + outbound_capabilities: [] # explicit lockdown — agent cannot call channel ops +``` + +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). + ### Top-level keys `@suzuke/agend` `FleetConfig` (`src/types.ts:218`) → `agend-terminal` `FleetConfig` (`src/fleet.rs:7-29`): @@ -222,6 +260,7 @@ channel: | 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. | | `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 6e78a1bf..4089936f 100644 --- a/docs/migration-to-agend-terminal.zh-TW.md +++ b/docs/migration-to-agend-terminal.zh-TW.md @@ -172,6 +172,44 @@ channel: **其他值得注意的 int-vs-string parity:** `instances..topic_id` 在 Rust 也是嚴格 `Option` (fleet.rs:160) —— 僅裸 int。 +### 高摩擦變更 #3:`outbound_capabilities` 是 Rust 端必填的新增欄位 + +**參考:** 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)。 + +`@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`。 + +**三態語意** (與高摩擦變更 #1 的 `channel.user_allowlist` 同模式): + +| 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 | + +**鍵不存在時的症狀(Sprint 22 P0 grace 期間)。** Daemon 透過 `tracing::error!` 發: + +``` +ERROR FATAL (warn-but-permit one daemon cycle): instance '' outbound_capabilities NOT SET. Sprint 22 P0 grants this call under gradual-migration grace. ... +``` + +該 op 仍會繼續執行 —— 但這條 warning 在每個 daemon process 內每個 instance 只會發一次(以 mutex-guarded `HashSet` 限速,不會 spam log)。Sprint 23 ship 後,鍵不存在會變成 hard parse error,daemon 會直接拒絕載入 config。**不要依賴 grace window 撐到 Sprint 22 P0 之後**。 + +**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#L24-L67) 自動注入 `[reply, react, edit, inject_provenance]`。**User 自己寫的 YAML entry 不會自動注入** —— Sprint 23 之前,operator 定義的每個 instance 都必須明確宣告 `outbound_capabilities`。 + +**遷移動作。** 把既有 `@suzuke/agend` `fleet.yaml` 移到 `agend-terminal` 時,給每一個 operator 自寫的 instance 加上 `outbound_capabilities`: + +```yaml +instances: + worker-a: + working_directory: /path/to/repo + outbound_capabilities: [reply, react] # 明確列出 agent 應該用的 channel ops + worker-b: + working_directory: /path/to/other-repo + outbound_capabilities: [] # 明確 lockdown —— agent 不可呼叫 channel ops +``` + +完整的 `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)。 + ### Top-level keys `@suzuke/agend` 的 `FleetConfig` (`src/types.ts:218`) → `agend-terminal` 的 `FleetConfig` (`src/fleet.rs:7-29`): @@ -220,6 +258,7 @@ channel: | Rust 欄位 | 型別 | 用途 | |---|---|---| +| `outbound_capabilities` | `Vec` | **Sprint 23 起,operator 自寫的每個 instance 都必填。** 限制該 instance 可呼叫哪些 agent-driven channel ops(`reply` / `react` / `edit` / `inject_provenance`)。三態語意 + 2-stage timeline + `general` 自動注入 —— 詳見上方「高摩擦變更 #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。 | From 788d90ce292597166a683e452fb204e15fc9ed83 Mon Sep 17 00:00:00 2001 From: suzuke Date: Mon, 27 Apr 2026 12:58:09 +0800 Subject: [PATCH 2/2] docs(migration): fix fleet_normalize.rs line range citation (dev-reviewer-2 F1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dev-reviewer-2 cross-team audit on PR #65 (reviewed_head 38a32ee65) flagged the `auto_create_general` line-range cite as drifted: - Doc cited :L24-L67 in two places (EN + zh-TW). - Actual `fn auto_create_general` spans :L38-L87 in agend-terminal main. - Lines :L24-L29 are the `default_built_in_outbound_capabilities()` helper's `vec!` literal, not the fn body. - :L67 sliced the fn mid-body (between `if !persist { return; }` and the `let entry = …` block). Per ts-lead's option (a) recommendation, both citations now use the inclusive range :L23-L87 — single anchor that covers both the helper (four-cap source) and the fn (call site) without splitting the operator's mental model. Added a parenthetical clarifying the helper + fn coverage so the reader knows what they're seeing on click-through. EN line 199 + zh-TW line 197. New commit on top of 38a32ee65 per CLAUDE.md (no amend). ts-reviewer F1 cross-link 404 NOT addressed in this PR (resolves naturally on agend-terminal #230 merge per dev-lead's sequential merge plan; my PR body already frames the link as forward-link). ts-reviewer O1 table shape NOT changed (justified by phased rollout narrative; non-blocking). Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/migration-to-agend-terminal.md | 2 +- docs/migration-to-agend-terminal.zh-TW.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/migration-to-agend-terminal.md b/docs/migration-to-agend-terminal.md index 69fc98a5..613c0b09 100644 --- a/docs/migration-to-agend-terminal.md +++ b/docs/migration-to-agend-terminal.md @@ -196,7 +196,7 @@ ERROR FATAL (warn-but-permit one daemon cycle): instance '' outbound_capab 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. -**Built-in coordinators are auto-injected.** The `general` instance (and any future auto-created coordinator) gets `[reply, react, edit, inject_provenance]` injected automatically by [`bootstrap::fleet_normalize::auto_create_general`](https://github.com/suzuke/agend-terminal/blob/main/src/bootstrap/fleet_normalize.rs#L24-L67) at startup. **User-authored YAML entries do not get auto-inject** — every operator-defined instance must declare `outbound_capabilities` explicitly before Sprint 23. +**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. **Migration action.** When porting an existing `@suzuke/agend` `fleet.yaml`, add `outbound_capabilities` to every operator-authored instance: diff --git a/docs/migration-to-agend-terminal.zh-TW.md b/docs/migration-to-agend-terminal.zh-TW.md index 4089936f..a4df9f0f 100644 --- a/docs/migration-to-agend-terminal.zh-TW.md +++ b/docs/migration-to-agend-terminal.zh-TW.md @@ -194,7 +194,7 @@ ERROR FATAL (warn-but-permit one daemon cycle): instance '' outbound_capab 該 op 仍會繼續執行 —— 但這條 warning 在每個 daemon process 內每個 instance 只會發一次(以 mutex-guarded `HashSet` 限速,不會 spam log)。Sprint 23 ship 後,鍵不存在會變成 hard parse error,daemon 會直接拒絕載入 config。**不要依賴 grace window 撐到 Sprint 22 P0 之後**。 -**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#L24-L67) 自動注入 `[reply, react, edit, inject_provenance]`。**User 自己寫的 YAML entry 不會自動注入** —— Sprint 23 之前,operator 定義的每個 instance 都必須明確宣告 `outbound_capabilities`。 +**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`。 **遷移動作。** 把既有 `@suzuke/agend` `fleet.yaml` 移到 `agend-terminal` 時,給每一個 operator 自寫的 instance 加上 `outbound_capabilities`: