From 70b255c4e23d427cc3ce062d843e44ba44356839 Mon Sep 17 00:00:00 2001 From: suzuke Date: Mon, 27 Apr 2026 11:59:34 +0800 Subject: [PATCH 1/2] =?UTF-8?q?docs(migration):=20add=20Sprint=202=20Phase?= =?UTF-8?q?=20A=20=E2=80=94=20CLI=20flag=20mapping=20+=20fleet.yaml=20sche?= =?UTF-8?q?ma=20diff?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Establishes the canonical migration reference for moving an existing @suzuke/agend (TS) install to agend-terminal (Rust). Built incrementally per Sprint 2 plan — Phase A lands Sections 2 + 3 first because CLI flags and fleet.yaml schema are the highest-friction surface for operators. Other sections carry Phase B / Phase C placeholders so follow-up phases stack without conflict. Cross-team source-of-truth confirmed via dev-impl-2 (Rust side hot context, agend-terminal main): - Rust CLI tree: src/main.rs:165-337 (clap defs, single flat enum) - Rust fleet.yaml schema: src/fleet.rs:7-183 (FleetConfig + InstanceConfig + ChannelConfig) - user_allowlist three-state semantics + drop log line: src/channel/mod.rs:240-260 - group_id i64 round-trip tests: src/fleet.rs:725-826 Section 2 (CLI flag mapping) — every TS top-level / fleet / backend / topic / access / schedule subcommand mapped against the Rust flat list. The Rust surface is significantly leaner: many TS commands (logs, history, activity, cleanup, init, web, reload, install, uninstall, restart, export*, attach, topic, access, schedule) have no Rust equivalent and are explicitly marked Removed with the operator migration action (read log files directly, edit fleet.yaml, use MCP tools, etc.). Stable anchor {#cli-flag-mapping} placed for Phase C. Section 3 (fleet.yaml schema diff) — top-level keys + every InstanceConfig field diffed. The Rust schema is deliberately leaner; many TS per-instance knobs (tags, restart_policy, context_guardian, log_level, tool_set, lightweight, systemPrompt, skipPermissions, model_failover, cost_guard, workflow, startup_timeout_ms, agent_mode) are not present in Rust — most have moved to env vars, backend presets, or per-backend instruction files. Each removal documents the Rust alternative. Two TS fields kept via serde alias on Rust: description→role, worktree_source→git_branch. High-friction calls covered with concrete examples + failure-mode debugging: #1 user_allowlist fail-closed default (Rust PR #216) — - top-level channel.user_allowlist (NOT under access); - three-state semantics: absent = legacy open + deprecation warn; []= lockdown; [ids] = allowlist; - exact daemon.log line cited (`outbound notify dropped — channel not authorised (fail-closed; configure user_allowlist to opt in)`); - migration debugging checklist included. #2 group_id strict i64 — REVERSES the TS recommendation. Rust accepts ONLY bare int form (i64 native, covers full negative supergroup range). Quoted-string form is rejected at startup with serde error "expected i64". TS users who quoted per the @suzuke/agend workaround must un-quote when porting. Same caveat noted for topic_id (also strict i32). Stable anchors placed: {#why-migrate} {#cli-flag-mapping} {#fleet-yaml-schema-diff} {#backend-invocation-diff} {#mcp-tool-api-diff} {#migration-steps} {#known-incompatibilities}. zh-TW pair (`docs/migration-to-agend-terminal.zh-TW.md`) translates banner body and section content while retaining English for GitHub URLs, agend-terminal keyword, and TS field names. 486 lines total (244 EN + 242 zh-TW), no impl changes — pure docs PR. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/migration-to-agend-terminal.md | 244 ++++++++++++++++++++++ docs/migration-to-agend-terminal.zh-TW.md | 242 +++++++++++++++++++++ 2 files changed, 486 insertions(+) create mode 100644 docs/migration-to-agend-terminal.md create mode 100644 docs/migration-to-agend-terminal.zh-TW.md diff --git a/docs/migration-to-agend-terminal.md b/docs/migration-to-agend-terminal.md new file mode 100644 index 00000000..2aa15b8a --- /dev/null +++ b/docs/migration-to-agend-terminal.md @@ -0,0 +1,244 @@ +# Migrating from AgEnD (TS) to agend-terminal (Rust) + +> [!IMPORTANT] +> This guide is the canonical migration reference for moving an existing +> `@suzuke/agend` (TypeScript) install to +> [`agend-terminal`](https://github.com/suzuke/agend-terminal) (Rust). It is +> built incrementally — Sprint 2 Phase A landed Sections 2 + 3 first because +> CLI flags and `fleet.yaml` schema are the highest-friction surface for +> existing operators. Other sections fill in over Phases B and C. + +> [!WARNING] +> `agend-terminal` is a Rust rewrite, not a port. The `fleet.yaml` schema is +> deliberately leaner than the TS one — many per-instance knobs that lived in +> the YAML have moved to env vars, backend presets, or per-backend instruction +> files. **Read Section 3 carefully before copying your existing config.** + +## Why migrate? {#why-migrate} + +*(Phase C — covers: motivation, agend-terminal feature delta over @suzuke/agend, when not to migrate, supported migration window.)* + +## CLI flag mapping {#cli-flag-mapping} + +`agend-terminal` exposes a **single flat command list** (see [`src/main.rs:165-337`](https://github.com/suzuke/agend-terminal/blob/main/src/main.rs#L165-L337)) rather than the multi-group structure (`agend fleet …`, `agend backend …`, …) you are used to from `@suzuke/agend`. Most subcommand groups collapse to top-level commands; several are removed entirely (functionality moved to MCP tools, TUI overlays, env vars, or deferred). + +### Top-level commands (TS top-level → Rust) + +| `@suzuke/agend` | `agend-terminal` | Status / notes | +|---|---|---| +| `agend init` | — | **Removed.** Use `agend-terminal quickstart` for the interactive setup, or hand-author `fleet.yaml`. | +| `agend quickstart` | `agend-terminal quickstart` | ✓ Renamed equivalent. | +| `agend start` | `agend-terminal start` | ✓ Starts the daemon. Note: Rust also has `agend-terminal daemon` for explicit daemon-mode launch and `agend-terminal app` for the TUI multi-pane terminal — these are distinct on Rust side, whereas TS folds the multiplex into `start`. | +| `agend stop` | `agend-terminal stop` | ✓ | +| `agend restart` | — | **Removed.** Use `stop` + `start`. | +| `agend ls` | `agend-terminal ls` (alias of `list`) | ✓ | +| `agend health` | `agend-terminal doctor` | **Renamed.** Combines health + backend probe into one diagnostic. | +| `agend attach ` | `agend-terminal attach ` | ✓ Native PTY multiplexing replaces the TS `tmux` wrapper, so attach quit shortcuts may differ. | +| `agend logs ` | — | **Removed.** Read log files directly under `$AGEND_HOME` (`daemon.log` and per-instance log files). | +| `agend update` | `agend-terminal upgrade` (Unix-only) | **Renamed and platform-narrowed.** Hot in-place upgrade; takes `--binary `, `--yes`, `--install-supervisor`, `--stability-secs N`, `--ready-timeout-secs N`. | +| `agend reload` | — | **Removed.** Stop and restart the daemon to pick up `fleet.yaml` changes. | +| `agend install` / `agend uninstall` | — | **Removed.** Service install is delegated to OS-native tools (`systemd` / `launchd`) — the Rust binary does not register itself. | +| `agend web` | — | **Removed.** No web UI in `agend-terminal`; the TUI (`app`) replaces it. | +| `agend export` / `agend import` | — | **Removed.** No archive format yet; if you need to back up state, copy `$AGEND_HOME` directly. | +| `agend export-chat` | — | **Removed.** | +| — | `agend-terminal app` | **New.** Launches the multi-pane TUI. | +| — | `agend-terminal tray` | **New.** System-tray integration (feature-gated). | +| — | `agend-terminal inject ` | **New.** Inject a message into an instance's stdin without a Telegram round trip. | +| — | `agend-terminal kill ` | **New.** Force-kill a hung instance. | +| — | `agend-terminal connect` | **New.** Connect a controller to a running daemon. | +| — | `agend-terminal demo` | **New.** Run a guided demo flow. | +| — | `agend-terminal bugreport` | **New.** Bundle logs/config for a bug report. | +| — | `agend-terminal completions` | **New.** Print shell completions. | +| — | `agend-terminal mcp` | **New.** MCP-related diagnostics. | +| — | `agend-terminal capture` | **New.** Capture a session snapshot for postmortem. | +| — | `agend-terminal test` / `verify` | **New.** Internal verification commands. | + +### `agend fleet` group → flattened + +| TS | Rust | Status | +|---|---|---| +| `agend fleet start` | `agend-terminal fleet start [config]` | ✓ | +| `agend fleet stop` | `agend-terminal fleet stop` | ✓ | +| `agend fleet restart` | — | **Removed.** Use `fleet stop` + `fleet start`. | +| `agend fleet status` | `agend-terminal status` | **Renamed and flattened** to top-level `status`. | +| `agend fleet logs` | — | **Removed.** Read log files directly. | +| `agend fleet history` | — | **Removed.** No equivalent. | +| `agend fleet activity` | — | **Removed.** No equivalent. | +| `agend fleet cleanup` | — | **Removed.** Rust does not auto-clean stale instance dirs; delete manually if needed. | +| — | `agend-terminal admin cleanup-branches [--yes]` | **New.** Admin sub-group; cleans up stale review branches. | + +### `agend backend` group → mostly removed + +| TS | Rust | Status | +|---|---|---| +| `agend backend doctor` | `agend-terminal doctor` | **Renamed and flattened.** Same diagnostic surface; no `backend` sub-group on Rust. | +| `agend backend trust ` | — | **Removed.** Backends manage their own trust files; `agend-terminal` does not pre-approve directories. Run the backend CLI manually once to accept its trust prompt. | + +### `agend topic ...` → removed (configuration-only) + +`topic list` / `topic bind` / `topic unbind` have **no CLI equivalent** in `agend-terminal`. Topic-to-instance routing is configured in `fleet.yaml` (`instances..topic_id`); there is no runtime command to mutate the binding. + +**Migration**: edit `fleet.yaml` directly. If you need to inspect current bindings at runtime, `agend-terminal status` lists each instance's `topic_id`. + +### `agend access ...` → removed (replaced by `channel.user_allowlist`) + +`access lock` / `unlock` / `list` / `remove` / `pair` have **no CLI equivalent** in `agend-terminal`. Access control is now declarative via the top-level `channel.user_allowlist` field — see High-friction #1 in Section 3. + +**Migration**: replace runtime `access ...` mutations with edits to `channel.user_allowlist` in `fleet.yaml`, then restart the daemon. + +### `agend schedule ...` → moved to MCP tools / TUI + +`schedule list` / `add` / `update` / `delete` / `enable` / `disable` / `history` / `trigger` have **no CLI equivalent** in `agend-terminal`. Scheduling lives in two places now: + +- **MCP tools** — `create_schedule`, `list_schedules`, `update_schedule`, `delete_schedule` (callable from inside any agent's tool surface). +- **TUI overlay** — `agend-terminal app` exposes a schedule pane. + +**Migration**: scripts that called `agend schedule add` from cron/CI must invoke the daemon's MCP tool surface instead, or pre-seed schedules through `agend-terminal app`. + +## fleet.yaml schema diff {#fleet-yaml-schema-diff} + +The Rust schema lives at [`src/fleet.rs:7-183`](https://github.com/suzuke/agend-terminal/blob/main/src/fleet.rs#L7-L183) and is the source of truth for everything below. The Rust schema is **deliberately leaner** than the TS one — many per-instance knobs have moved to env vars, backend presets, or per-backend instruction files. Plan to rewrite, not just rename, when porting an existing `fleet.yaml`. + +### High-friction change #1: `user_allowlist` is fail-closed by default + +**Reference:** `agend-terminal` PR #216, schema at [`src/fleet.rs:50-60`](https://github.com/suzuke/agend-terminal/blob/main/src/fleet.rs#L50-L60), drop logic at [`src/channel/mod.rs:240-260`](https://github.com/suzuke/agend-terminal/blob/main/src/channel/mod.rs#L240-L260). + +**Field**: `channel.user_allowlist: Option>` — a top-level field on `channel`, **not nested under `access`** (Rust has no `access` sub-key on `channel`). + +**Three-state semantics:** + +| YAML | Behaviour | +|---|---| +| key absent | **Legacy open mode** — accepts all group members. Daemon logs a deprecation warning at startup; this state is supported for backwards compatibility but flagged for removal. | +| `user_allowlist: []` | **Lockdown.** Reject all senders. Useful as a kill-switch without removing the rest of the channel config. | +| `user_allowlist: [123, 456]` | **Allowlist.** Only those Telegram numeric user IDs accepted. | + +**Outbound failure mode (post-PR #216 fail-closed for outbound notify):** + +If the daemon would have notified the channel but the recipient is not allowlisted, you'll see this line in `daemon.log`: + +``` +WARN outbound notify dropped — channel not authorised (fail-closed; configure user_allowlist to opt in) +``` + +(Source: `src/channel/mod.rs:254`.) + +**Inbound failure mode:** + +Each rejected inbound is dropped with a `WARN` log naming the offending `user_id`. + +**Migration action** + +```yaml +# fleet.yaml on agend-terminal +channel: + type: telegram + bot_token_env: BOT_TOKEN + group_id: -1001234567890 # bare int, see High-friction #2 + user_allowlist: # top-level on channel; copy from access.allowed_users on @suzuke/agend + - 111111111 # Telegram numeric user ID, bare int + - 222222222 +``` + +**Debugging checklist when the bot goes silent on Rust:** + +1. `grep "outbound notify dropped" $AGEND_HOME/daemon.log` — confirms the gate fired. +2. Confirm `channel.user_allowlist` is set in `fleet.yaml` and your numeric user ID is in it (use [@userinfobot](https://t.me/userinfobot) on Telegram if unsure). +3. If you previously had `access.allowed_users` on `@suzuke/agend`, that path is **not read** by Rust — copy the entries to top-level `channel.user_allowlist` and use bare int form for IDs. + +> **Why fail-closed:** an empty/absent allowlist on a Telegram bot is a credential exposure waiting to happen — anyone who guesses or exfiltrates the bot token can DM the bot and trigger arbitrary backend tool use. Failing closed forces the operator to make an explicit access decision, which is both safer and more debuggable than silently exposing the bot. + +### High-friction change #2: `group_id` is strict `i64` — **bare int form only** + +**Reference:** [`src/fleet.rs:46`](https://github.com/suzuke/agend-terminal/blob/main/src/fleet.rs#L46) (field type) and [`src/fleet.rs:725-826`](https://github.com/suzuke/agend-terminal/blob/main/src/fleet.rs#L725-L826) (round-trip tests covering `-100123456`, `-100999`, `-3`, `-1`, `-2`). + +In `@suzuke/agend`, `channel.group_id` is typed as `number | string` and the YAML loader accepts either. Operator guidance has been "always quote large negative IDs as strings" because some code paths mishandled the negative-prefix supergroup IDs as numbers (precision/sign edge cases). + +In `agend-terminal`, `channel.group_id` is typed as **`i64`** with strict serde deserialization. **Only the bare int form is accepted** — a quoted-string form (`group_id: "-1001234567890"`) **fails at startup** with a serde error like `"invalid type: string \"-1001234567890\", expected i64"`. The Rust YAML parser does not auto-coerce string ↔ int. + +The TS string-handling bug does not apply on Rust (i64 covers the full negative supergroup range natively, with round-trip tests committed at fleet.rs:725-826). + +**This reverses the TS migration guidance.** If you have been quoting `group_id` on `@suzuke/agend` (the recommended workaround), you must **un-quote** it before the new daemon will load your `fleet.yaml`. + +```yaml +# Required form on agend-terminal: +channel: + group_id: -1001234567890 # bare int + +# Will fail to load on agend-terminal: +channel: + group_id: "-1001234567890" # quoted — serde rejects with "expected i64" +``` + +**Other int-vs-string parity worth knowing:** `instances..topic_id` is also strictly `Option` on Rust (fleet.rs:160) — bare int only. + +### Top-level keys + +`@suzuke/agend` `FleetConfig` (`src/types.ts:218`) → `agend-terminal` `FleetConfig` (`src/fleet.rs:7-29`): + +| TS key | Rust equivalent | Notes | +|---|---|---| +| `channel` | `channel: ChannelConfig` ✓ | Singular form. **Plus** Rust accepts plural `channels: HashMap` for multi-channel routing — `normalize()` at startup collapses `channels` to `channel` if only one entry. | +| `project_roots` | **removed** | No fleet-level allowlist of project roots; use per-instance `working_directory`. | +| `defaults` | `defaults: InstanceDefaults` ✓ | Field set differs significantly — see instance table below. | +| `instances` | `instances: HashMap` ✓ | Same role; field set leaner. | +| `teams` | `teams: HashMap` ✓ | Same shape (`{ members, description? }`). | +| `templates` | `templates: Option>` | **Parser-only on Rust today** — keys round-trip but template expansion is not implemented yet. Do not depend on TS template semantics. | +| `profiles` | **removed** | Per-instance fields are flat; reusable profiles will return as a separate concern if needed. | +| `health_port` | **removed** | Daemon does not expose a separate health port; use `agend-terminal doctor`. | +| `stt` | **removed** | Voice→cloud transcription not implemented; voice messages are not supported. | + +### `instances.` field diff + +`@suzuke/agend` `InstanceConfig` (`src/types.ts:65-111`) → `agend-terminal` `InstanceConfig` (`src/fleet.rs:140-183`): + +| TS field | Rust field (snake_case) | Migration action | +|---|---|---| +| `working_directory` | `working_directory: Option` | ✓ same. Required in practice. | +| `display_name` | `display_name: Option` | ✓ same. | +| `description` | `role: Option` (with serde alias `description`) | **Rename or use alias.** Both `role: …` and `description: …` are accepted on Rust; canonical is `role`. | +| `tags` | **removed** | Drop the field. No replacement; Rust does not use tag-based routing. | +| `topic_id` | `topic_id: Option` | ✓ same; **bare int only** (see High-friction #2). | +| `general_topic` | **removed** | Drop the field. | +| `restart_policy` | **removed** | Drop the field. Restart logic is daemon-managed via fixed constants in `health.rs` (`CRASH_WINDOW=10min`, `BACKOFF_BASE=5s`, `BACKOFF_MAX=300s`, `DEFAULT_MAX_RETRIES=5`). Per-instance restart tuning is not exposed. | +| `context_guardian` | **removed** | Drop the field. Context-rotation tuning lives in backend presets / runtime defaults. | +| `log_level` | **removed** | Drop the field. Set globally via env var (`RUST_LOG=…`). | +| `backend` | `backend: Option` | ✓ same. Backend identifier strings should match (claude / gemini / codex / opencode / kiro). | +| `tool_set` | **removed** | Drop the field. MCP tool ACL is now controlled by env vars `AGEND_MCP_TOOLS_ALLOW` / `AGEND_MCP_TOOLS_DENY` set when launching the daemon. | +| `lightweight` | **removed** | Drop the field. | +| `systemPrompt` | **removed** | Drop the field. Instructions are injected via per-backend `instructions_path` files (e.g. a workspace `CLAUDE.md`) — see the `agend-terminal` backend preset for paths. The TS `file:`-chain syntax has no Rust equivalent; concatenate manually if needed. | +| `skipPermissions` | **removed** | Drop the field. Permission flags are baked into each backend preset's `spawn_flags()`. | +| `model` | `model: Option` | ✓ same. Passed as `--model` to the backend CLI. | +| `model_failover` | **removed** | Drop the field. No automatic failover on Rust today; restart with a different model manually if the primary is rate-limited. | +| `cost_guard` | **removed** | Drop the field. No equivalent yet on Rust. | +| `worktree_source` | `git_branch: Option` (with serde alias `worktree_source`) | **Rename or use alias.** Rust canonical is `git_branch`; the TS spelling still loads via alias. | +| `workflow` | **removed** | Drop the field. | +| `startup_timeout_ms` | **removed** | Drop the field. Startup timing is governed by each backend preset's `ready_timeout_secs`. | +| `agent_mode` | **removed** | Drop the field. Communication mode (MCP vs HTTP) is implied by the backend preset; not user-tunable per instance. | + +**Rust-only fields you may want to add** when porting: + +| Rust field | Type | Purpose | +|---|---|---| +| `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. | +| `command`, `args`, `ready_pattern` | low-level overrides | Used when the chosen backend is not a built-in preset (legacy / custom CLI). | + +**snake_case caveat:** TS schema mixes `camelCase` (`systemPrompt`, `skipPermissions`) and `snake_case` (everything else). Rust is uniformly `snake_case`. The two TS camelCase fields above are removed on Rust anyway, so this only matters for `worktree_source` / `git_branch` (alias accepts both) and `description` / `role` (alias accepts both). + +## Backend invocation diff {#backend-invocation-diff} + +*(Phase B — covers: `--mcp-config` vs Rust equivalent, `--append-system-prompt-file` flow, per-backend env-var injection, MCP server respawn semantics, fleet-instructions delivery channel — see also `docs/fleet-instructions-injection.md` on the TS side for the post-#55 model.)* + +## MCP tool API diff {#mcp-tool-api-diff} + +*(Phase B — covers: full inventory of MCP tools (~20 in TS full set), name renames, argument shape changes, return-value diffs, broadcast `cost_limited` field carry-over, deferred or removed tools.)* + +## Migration steps {#migration-steps} + +*(Phase C — covers: pre-flight checklist, `agend-terminal migrate` invocation if available, dual-run period, rollback, post-migration validation.)* + +## Known incompatibilities and deferred parity {#known-incompatibilities} + +*(Phase C — covers: features intentionally not ported, parity items deferred to a later Rust release, items still in design.)* diff --git a/docs/migration-to-agend-terminal.zh-TW.md b/docs/migration-to-agend-terminal.zh-TW.md new file mode 100644 index 00000000..08f5f88e --- /dev/null +++ b/docs/migration-to-agend-terminal.zh-TW.md @@ -0,0 +1,242 @@ +# 從 AgEnD (TS) 遷移到 agend-terminal (Rust) + +> [!IMPORTANT] +> 本文件是把現有 `@suzuke/agend` (TypeScript) 安裝遷移到 +> [`agend-terminal`](https://github.com/suzuke/agend-terminal) (Rust) 的標準 +> migration 指引。內容會分階段補完 —— Sprint 2 Phase A 先處理 Sections 2 + 3,因為對 +> 既有 operator 來說,CLI flags 與 `fleet.yaml` schema 是阻力最高的兩個面向。其他章節 +> 會在 Phase B 與 Phase C 補上。 + +> [!WARNING] +> `agend-terminal` 是 Rust 重寫,不是直譯 port。`fleet.yaml` schema 比 TS 版精簡許多 —— +> 許多原本住在 YAML 的 per-instance 旋鈕已搬到 env vars、backend presets 或 per-backend +> 指令檔案。**搬既有 config 之前請仔細讀 Section 3。** + +## 為什麼要遷移? {#why-migrate} + +*(Phase C —— 涵蓋:遷移動機、agend-terminal 相對 @suzuke/agend 的功能差異、何時不要遷移、支援的遷移期間。)* + +## CLI flag mapping {#cli-flag-mapping} + +`agend-terminal` 採 **單一扁平指令清單** (見 [`src/main.rs:165-337`](https://github.com/suzuke/agend-terminal/blob/main/src/main.rs#L165-L337)),不再使用 `@suzuke/agend` 的多群組結構 (`agend fleet …`、`agend backend …`…)。多數子群組摺到 top-level;部分整組移除 (功能搬到 MCP tools、TUI overlay、env vars,或延後實作)。 + +### Top-level commands (TS top-level → Rust) + +| `@suzuke/agend` | `agend-terminal` | 狀態 / 備註 | +|---|---|---| +| `agend init` | — | **移除。** 改用 `agend-terminal quickstart` 互動式 setup,或手動撰寫 `fleet.yaml`。 | +| `agend quickstart` | `agend-terminal quickstart` | ✓ 等價改名。 | +| `agend start` | `agend-terminal start` | ✓ 啟動 daemon。注意:Rust 另有 `agend-terminal daemon` (顯式 daemon-mode 啟動) 與 `agend-terminal app` (TUI 多 pane 終端機介面) —— 兩者在 Rust 上是分開的指令,TS 將 multiplex 包進 `start`。 | +| `agend stop` | `agend-terminal stop` | ✓ | +| `agend restart` | — | **移除。** 用 `stop` + `start`。 | +| `agend ls` | `agend-terminal ls` (alias of `list`) | ✓ | +| `agend health` | `agend-terminal doctor` | **改名。** 把 health + backend probe 合併為一個診斷指令。 | +| `agend attach ` | `agend-terminal attach ` | ✓ 原生 PTY multiplexing 取代 TS 的 `tmux` wrapper,attach 退出快捷鍵可能不同。 | +| `agend logs ` | — | **移除。** 直接讀 `$AGEND_HOME` 下的 log 檔案 (`daemon.log` 與每個 instance 的 log)。 | +| `agend update` | `agend-terminal upgrade` (僅 Unix) | **改名且平台收斂。** Hot in-place upgrade;接收 `--binary `、`--yes`、`--install-supervisor`、`--stability-secs N`、`--ready-timeout-secs N`。 | +| `agend reload` | — | **移除。** 改 `fleet.yaml` 後 stop + start daemon。 | +| `agend install` / `agend uninstall` | — | **移除。** Service 安裝交給作業系統工具 (`systemd` / `launchd`) —— Rust binary 不自動註冊。 | +| `agend web` | — | **移除。** `agend-terminal` 沒有 web UI;TUI (`app`) 取代之。 | +| `agend export` / `agend import` | — | **移除。** 目前無 archive 格式;若要備份,直接複製 `$AGEND_HOME`。 | +| `agend export-chat` | — | **移除。** | +| — | `agend-terminal app` | **新增。** 啟動多 pane TUI。 | +| — | `agend-terminal tray` | **新增。** 系統匣整合 (feature-gated)。 | +| — | `agend-terminal inject ` | **新增。** 不透過 Telegram 直接把訊息打進 instance 的 stdin。 | +| — | `agend-terminal kill ` | **新增。** 強制 kill hung 住的 instance。 | +| — | `agend-terminal connect` | **新增。** 把 controller 接到一個運行中的 daemon。 | +| — | `agend-terminal demo` | **新增。** 跑導覽式 demo 流程。 | +| — | `agend-terminal bugreport` | **新增。** 打包 logs/config 供 bug report 使用。 | +| — | `agend-terminal completions` | **新增。** 印出 shell completions。 | +| — | `agend-terminal mcp` | **新增。** MCP 相關診斷。 | +| — | `agend-terminal capture` | **新增。** Postmortem 用 session snapshot。 | +| — | `agend-terminal test` / `verify` | **新增。** 內部驗證指令。 | + +### `agend fleet` 群組 → 摺平 + +| TS | Rust | 狀態 | +|---|---|---| +| `agend fleet start` | `agend-terminal fleet start [config]` | ✓ | +| `agend fleet stop` | `agend-terminal fleet stop` | ✓ | +| `agend fleet restart` | — | **移除。** 用 `fleet stop` + `fleet start`。 | +| `agend fleet status` | `agend-terminal status` | **改名且摺到 top-level**。 | +| `agend fleet logs` | — | **移除。** 直接讀 log 檔。 | +| `agend fleet history` | — | **移除。** 無等價物。 | +| `agend fleet activity` | — | **移除。** 無等價物。 | +| `agend fleet cleanup` | — | **移除。** Rust 不自動清 stale instance 資料夾;需要時手動刪。 | +| — | `agend-terminal admin cleanup-branches [--yes]` | **新增。** Admin 子群組;清 stale review branches。 | + +### `agend backend` 群組 → 大部分移除 + +| TS | Rust | 狀態 | +|---|---|---| +| `agend backend doctor` | `agend-terminal doctor` | **改名且摺平。** 同樣的診斷面向;Rust 無 `backend` 子群組。 | +| `agend backend trust ` | — | **移除。** 後端各自管 trust 檔案;`agend-terminal` 不預先信任目錄。改成手動跑一次 backend CLI 接受 trust 對話框。 | + +### `agend topic ...` → 移除 (僅靠設定檔) + +`topic list` / `topic bind` / `topic unbind` 在 `agend-terminal` 上 **無 CLI 等價物**。Topic-to-instance 路由改在 `fleet.yaml` (`instances..topic_id`) 設定;沒有運行時指令可改 binding。 + +**遷移**:直接編輯 `fleet.yaml`。需要查運行時 binding 改用 `agend-terminal status`,它會列每個 instance 的 `topic_id`。 + +### `agend access ...` → 移除 (改用 `channel.user_allowlist`) + +`access lock` / `unlock` / `list` / `remove` / `pair` 在 `agend-terminal` **無 CLI 等價物**。存取控制改成宣告式 —— 透過 top-level `channel.user_allowlist` 欄位 —— 見 Section 3 的 High-friction #1。 + +**遷移**:把運行時 `access ...` 操作換成編輯 `fleet.yaml` 的 `channel.user_allowlist`,然後 restart daemon。 + +### `agend schedule ...` → 搬到 MCP tools / TUI + +`schedule list` / `add` / `update` / `delete` / `enable` / `disable` / `history` / `trigger` 在 `agend-terminal` **無 CLI 等價物**。排程改住兩個地方: + +- **MCP tools** —— `create_schedule`、`list_schedules`、`update_schedule`、`delete_schedule` (任何 agent 的工具面向皆可呼叫)。 +- **TUI overlay** —— `agend-terminal app` 提供 schedule pane。 + +**遷移**:之前在 cron/CI 呼叫 `agend schedule add` 的 script 必須改呼叫 daemon 的 MCP tool 介面,或透過 `agend-terminal app` 預先輸入排程。 + +## fleet.yaml schema diff {#fleet-yaml-schema-diff} + +Rust schema 在 [`src/fleet.rs:7-183`](https://github.com/suzuke/agend-terminal/blob/main/src/fleet.rs#L7-L183),是底下所有對照的 source of truth。Rust schema **刻意比 TS 精簡** —— 許多 per-instance 旋鈕已搬到 env vars、backend presets,或 per-backend 指令檔案。Port 既有 `fleet.yaml` 時請當成「重寫」而非「重命名」。 + +### 高摩擦變更 #1:`user_allowlist` 預設 fail-closed + +**參考:** `agend-terminal` PR #216,schema 在 [`src/fleet.rs:50-60`](https://github.com/suzuke/agend-terminal/blob/main/src/fleet.rs#L50-L60),drop 邏輯在 [`src/channel/mod.rs:240-260`](https://github.com/suzuke/agend-terminal/blob/main/src/channel/mod.rs#L240-L260)。 + +**欄位**:`channel.user_allowlist: Option>` —— 是 `channel` 的 top-level 欄位,**不**巢狀於 `access` 之下 (Rust 的 `channel` 沒有 `access` 子鍵)。 + +**三態語意:** + +| YAML | 行為 | +|---|---| +| 鍵不存在 | **Legacy open mode** —— 接受所有群組成員。Daemon 啟動時記 deprecation warning;此狀態為 backwards-compat 而保留,已被標記為將來移除。 | +| `user_allowlist: []` | **Lockdown。** 拒絕所有 sender。可作為不移除其他 channel config 的 kill-switch。 | +| `user_allowlist: [123, 456]` | **Allowlist。** 只接受這些 Telegram 數字 user IDs。 | + +**Outbound 失敗模式 (post-PR #216 outbound notify fail-closed):** + +如果 daemon 想對 channel 通知,但收件者不在 allowlist 中,你會在 `daemon.log` 看到: + +``` +WARN outbound notify dropped — channel not authorised (fail-closed; configure user_allowlist to opt in) +``` + +(來源:`src/channel/mod.rs:254`。) + +**Inbound 失敗模式:** + +每筆被拒絕的 inbound 會 drop,並記一條帶該 `user_id` 的 `WARN` log。 + +**遷移動作** + +```yaml +# fleet.yaml on agend-terminal +channel: + type: telegram + bot_token_env: BOT_TOKEN + group_id: -1001234567890 # 裸 int,見 High-friction #2 + user_allowlist: # channel 的 top-level;從 @suzuke/agend 的 access.allowed_users 複製 + - 111111111 # Telegram 數字 user ID,裸 int + - 222222222 +``` + +**Rust 上 bot 安靜時的 debug 清單:** + +1. `grep "outbound notify dropped" $AGEND_HOME/daemon.log` —— 確認 gate 觸發了。 +2. 確認 `channel.user_allowlist` 已設定於 `fleet.yaml`,且你的數字 user ID 在裡面 (不確定可在 Telegram 用 [@userinfobot](https://t.me/userinfobot))。 +3. 如果你之前在 `@suzuke/agend` 用 `access.allowed_users`,該路徑 Rust **不再讀取** —— 把條目搬到 top-level `channel.user_allowlist`,IDs 用裸 int 形式。 + +> **為什麼 fail-closed:** Telegram bot 上空的 / 不存在的 allowlist 是 credential 暴露的等待事故 —— 任何人猜到或外洩 bot token 後可以 DM bot 觸發任意 backend tool use。Fail-closed 強迫 operator 做出明確的存取決策,比靜默暴露更安全也更易 debug。 + +### 高摩擦變更 #2:`group_id` 嚴格 `i64` —— **僅接受裸 int** + +**參考:** [`src/fleet.rs:46`](https://github.com/suzuke/agend-terminal/blob/main/src/fleet.rs#L46) (欄位型別) 與 [`src/fleet.rs:725-826`](https://github.com/suzuke/agend-terminal/blob/main/src/fleet.rs#L725-L826) (round-trip 測試覆蓋 `-100123456`、`-100999`、`-3`、`-1`、`-2`)。 + +在 `@suzuke/agend`,`channel.group_id` 型別是 `number | string`,YAML loader 兩種都接受。Operator 指引一直是「大型負 ID 永遠 quote 成字串」,因為某些 code path 把 negative-prefix supergroup IDs 當 number 處理會 hit precision/sign 邊界。 + +在 `agend-terminal`,`channel.group_id` 嚴格定為 **`i64`**,serde deserialization 嚴格。**只接受裸 int 形式** —— quoted-string 形式 (`group_id: "-1001234567890"`) **會在啟動時失敗**,serde error 形如 `"invalid type: string \"-1001234567890\", expected i64"`。Rust YAML parser 不會自動 string ↔ int 互轉。 + +TS 的 string-handling bug 在 Rust 上不適用 (i64 原生覆蓋整個負 supergroup 範圍,並有 fleet.rs:725-826 的 round-trip 測試)。 + +**這 反轉了 TS 的遷移建議。** 如果你之前依 `@suzuke/agend` 建議 quote `group_id`,在新 daemon 載入 `fleet.yaml` 之前必須 **取消 quote**。 + +```yaml +# agend-terminal 必要形式: +channel: + group_id: -1001234567890 # 裸 int + +# agend-terminal 載入會失敗: +channel: + group_id: "-1001234567890" # quoted —— serde 拒絕「expected i64」 +``` + +**其他值得注意的 int-vs-string parity:** `instances..topic_id` 在 Rust 也是嚴格 `Option` (fleet.rs:160) —— 僅裸 int。 + +### Top-level keys + +`@suzuke/agend` 的 `FleetConfig` (`src/types.ts:218`) → `agend-terminal` 的 `FleetConfig` (`src/fleet.rs:7-29`): + +| TS key | Rust 等價 | 備註 | +|---|---|---| +| `channel` | `channel: ChannelConfig` ✓ | 單數形式。**此外** Rust 接受複數形式 `channels: HashMap` 用於 multi-channel routing —— 啟動時的 `normalize()` 會在只有一個 entry 時把 `channels` 摺成 `channel`。 | +| `project_roots` | **移除** | 沒有 fleet-level 的 project root allowlist;改用 per-instance `working_directory`。 | +| `defaults` | `defaults: InstanceDefaults` ✓ | 欄位集合差異很大 —— 見下方 instance 表。 | +| `instances` | `instances: HashMap` ✓ | 角色相同;欄位集合精簡。 | +| `teams` | `teams: HashMap` ✓ | 形狀相同 (`{ members, description? }`)。 | +| `templates` | `templates: Option>` | **Rust 目前僅 parser** —— 鍵會 round-trip,但 template 展開尚未實作。**不要** 依賴 TS template 語意。 | +| `profiles` | **移除** | Per-instance 欄位攤平;將來若需要,reusable profiles 會以另一個 concern 重新引入。 | +| `health_port` | **移除** | Daemon 不開獨立 health port;改用 `agend-terminal doctor`。 | +| `stt` | **移除** | Voice→cloud 轉錄未實作;不支援 voice 訊息。 | + +### `instances.` 欄位 diff + +`@suzuke/agend` `InstanceConfig` (`src/types.ts:65-111`) → `agend-terminal` `InstanceConfig` (`src/fleet.rs:140-183`): + +| TS 欄位 | Rust 欄位 (snake_case) | 遷移動作 | +|---|---|---| +| `working_directory` | `working_directory: Option` | ✓ 相同。實務上必填。 | +| `display_name` | `display_name: Option` | ✓ 相同。 | +| `description` | `role: Option` (帶 serde alias `description`) | **改名或用 alias。** Rust 上 `role: …` 與 `description: …` 都接受;canonical 是 `role`。 | +| `tags` | **移除** | 砍掉欄位。無替代;Rust 不採 tag-based routing。 | +| `topic_id` | `topic_id: Option` | ✓ 相同;**僅裸 int** (見 High-friction #2)。 | +| `general_topic` | **移除** | 砍掉欄位。 | +| `restart_policy` | **移除** | 砍掉欄位。Restart 邏輯 daemon-managed,固定常數在 `health.rs` (`CRASH_WINDOW=10min`、`BACKOFF_BASE=5s`、`BACKOFF_MAX=300s`、`DEFAULT_MAX_RETRIES=5`)。Per-instance restart 不開放調整。 | +| `context_guardian` | **移除** | 砍掉欄位。Context-rotation 調整改住 backend presets / runtime defaults。 | +| `log_level` | **移除** | 砍掉欄位。改用 env var (`RUST_LOG=…`) 全域設定。 | +| `backend` | `backend: Option` | ✓ 相同。Backend 識別字串應一致 (claude / gemini / codex / opencode / kiro)。 | +| `tool_set` | **移除** | 砍掉欄位。MCP tool ACL 改成啟動 daemon 時設 env var `AGEND_MCP_TOOLS_ALLOW` / `AGEND_MCP_TOOLS_DENY`。 | +| `lightweight` | **移除** | 砍掉欄位。 | +| `systemPrompt` | **移除** | 砍掉欄位。指令改透過 per-backend `instructions_path` 檔案 (例如 workspace `CLAUDE.md`) 注入 —— 路徑見 `agend-terminal` 對應的 backend preset。TS `file:`-chain 語法 Rust 上無等價;若需要請手動拼接。 | +| `skipPermissions` | **移除** | 砍掉欄位。Permission flags 烘進每個 backend preset 的 `spawn_flags()`。 | +| `model` | `model: Option` | ✓ 相同。傳成 `--model` 給 backend CLI。 | +| `model_failover` | **移除** | 砍掉欄位。Rust 目前無自動 failover;主 model rate-limit 時手動換 model 重啟。 | +| `cost_guard` | **移除** | 砍掉欄位。Rust 上目前無等價物。 | +| `worktree_source` | `git_branch: Option` (帶 serde alias `worktree_source`) | **改名或用 alias。** Rust canonical 是 `git_branch`;TS 拼寫透過 alias 仍會載入。 | +| `workflow` | **移除** | 砍掉欄位。 | +| `startup_timeout_ms` | **移除** | 砍掉欄位。Startup 計時由各 backend preset 的 `ready_timeout_secs` 控制。 | +| `agent_mode` | **移除** | 砍掉欄位。通訊模式 (MCP vs HTTP) 由 backend preset 決定;不開放 per-instance 調整。 | + +**Port 時可能想加的 Rust-only 欄位:** + +| Rust 欄位 | 型別 | 用途 | +|---|---|---| +| `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。 | +| `command`、`args`、`ready_pattern` | low-level overrides | 用於選的 backend 不是內建 preset 的情況 (legacy / 客製化 CLI)。 | + +**snake_case 注意:** TS schema 混用 `camelCase` (`systemPrompt`、`skipPermissions`) 與 `snake_case` (其餘)。Rust 一律 `snake_case`。上述兩個 TS camelCase 欄位 Rust 端都已移除,所以這個差異實際只影響 `worktree_source` / `git_branch` (alias 兩邊收) 與 `description` / `role` (alias 兩邊收)。 + +## Backend invocation diff {#backend-invocation-diff} + +*(Phase B —— 涵蓋:`--mcp-config` 對應到 Rust 等價物、`--append-system-prompt-file` 流程、per-backend env-var 注入、MCP server respawn 語意、fleet-instructions 傳遞通道 —— TS 端 post-#55 模型也可參考 `docs/fleet-instructions-injection.md`。)* + +## MCP tool API diff {#mcp-tool-api-diff} + +*(Phase B —— 涵蓋:MCP tools 完整清單 (TS full set 約 20 個)、name rename、argument 形狀變更、return 值差異、broadcast `cost_limited` 欄位接續、deferred 或移除的 tools。)* + +## Migration steps {#migration-steps} + +*(Phase C —— 涵蓋:pre-flight 檢查清單、`agend-terminal migrate` (若提供) 呼叫、雙跑期間、rollback、post-migration 驗證。)* + +## Known incompatibilities and deferred parity {#known-incompatibilities} + +*(Phase C —— 涵蓋:意圖不 port 的功能、被延後到後續 Rust release 的 parity 項目、仍在設計中的項目。)* From 296c9f3ae01582ae56adafa3c358f11e76326164 Mon Sep 17 00:00:00 2001 From: suzuke Date: Mon, 27 Apr 2026 12:17:34 +0800 Subject: [PATCH 2/2] docs(migration): address dual-review findings (Phase A revision) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Six findings from the parallel ts-reviewer + dev-reviewer-2 audit on PR #63 reviewed_head 70b255c. Grouped into three areas; each addressed in EN and zh-TW. Area 1 — drop log + group_id framing (HIGH) - Sub-fix 1a (dev-reviewer-2 F1): doc claimed `WARN outbound notify dropped …` but src/channel/mod.rs:251-255 emits the line at `tracing::debug!`. With default `RUST_LOG=info` the line is invisible to grep — operator concludes "config OK" while the bot is silently dropping outbound notifies, the opposite of what the doc was trying to teach. Updated to DEBUG with an `[!IMPORTANT]` callout instructing the operator to set `RUST_LOG=debug` (or `RUST_LOG=agend_terminal= debug`) when reproducing. Architectural fix (raise debug→warn for security-gate visibility) is separately filed by ts-lead to dev-team and not part of this PR. - Sub-fix 1b (ts-reviewer F1): the prior framing claimed "@suzuke/agend taught operators to always quote large negative IDs." That overstated the TS canonical position. TS canonical docs use bare int for Telegram supergroup IDs (docs/configuration.md:23, tests/setup-wizard-config.test.ts:127). Quoted-string was reserved for Discord guild IDs whose 18-19-digit positive snowflakes exceed JS Number.MAX_SAFE_INTEGER (docs/features.md:302, docs/plugin-development.md:293, docs/plugin-adapter-architecture.md: 28,298). Re-framed: bare int is canonical on TS; Discord-quoting was the only quoted-form pattern; Rust i64 covers both with bare int — un-quote any quoted values, especially Discord. Area 2 — schema details (LOW) - Sub-fix 2a (dev-reviewer-2 F2): TeamConfig (src/fleet.rs:175-183) has `orchestrator: Option` between `members` and `description`; the prior diff row missed it. Updated row shape to `{ members, orchestrator?, description? }` and added a sentence describing orchestrator's role (routing target for team-addressed delegate_task; team-tab grouping in the TUI) plus the porting note that leaving orchestrator unset preserves TS-equivalent behaviour. - Sub-fix 2b (ts-reviewer F2): the prior text used `access.allowed_users` as shorthand. Per src/types.ts:57 AccessConfig is nested under ChannelConfig, so the canonical TS path is `channel.access.allowed_users`. Fully qualified at both occurrences (config example callout, debugging step 3) in EN and zh-TW. Area 3 — citation/scope tightening (LOW) - Sub-fix 3a (dev-reviewer-2 F3): the prior text implied src/fleet.rs:725-826 round-trip tests covered quoted-string rejection. They cover only bare-int parsing for representative negative IDs; quoted-string rejection is serde's default behaviour for `i64`-typed fields and is not separately regression-tested. Re-phrased per reviewer's suggested wording, noting future serde drift is unlikely but technically not pinned. - Sub-fix 3b (ts-reviewer F3): TS AccessConfig.mode supports both `"pairing"` and `"locked"` (src/types.ts:29). The prior High-friction #1 covered the locked-mode → user_allowlist path but did not address pairing-mode users (issued via `agend access pair`, redeemed by users to land their IDs in `channel.access.allowed_users`). Rust has no pairing flow equivalent. Added an explicit migration step for pairing-mode operators: enumerate every active user ID into `channel.user_allowlist` directly. zh-TW pair updated identically. Stats: 2 files changed, 30 insertions(+), 26 deletions(-). New commit on top of 70b255c per CLAUDE.md (no amend). Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/migration-to-agend-terminal.md | 28 ++++++++++++----------- docs/migration-to-agend-terminal.zh-TW.md | 28 ++++++++++++----------- 2 files changed, 30 insertions(+), 26 deletions(-) diff --git a/docs/migration-to-agend-terminal.md b/docs/migration-to-agend-terminal.md index 2aa15b8a..d630efd2 100644 --- a/docs/migration-to-agend-terminal.md +++ b/docs/migration-to-agend-terminal.md @@ -115,17 +115,18 @@ The Rust schema lives at [`src/fleet.rs:7-183`](https://github.com/suzuke/agend- **Outbound failure mode (post-PR #216 fail-closed for outbound notify):** -If the daemon would have notified the channel but the recipient is not allowlisted, you'll see this line in `daemon.log`: +If the daemon would have notified the channel but the recipient is not allowlisted, this line is emitted to `daemon.log` at **DEBUG** level (`tracing::debug!` at `src/channel/mod.rs:251-255`): ``` -WARN outbound notify dropped — channel not authorised (fail-closed; configure user_allowlist to opt in) +DEBUG outbound notify dropped — channel not authorised (fail-closed; configure user_allowlist to opt in) ``` -(Source: `src/channel/mod.rs:254`.) +> [!IMPORTANT] +> The drop event is logged at `DEBUG`, **not** `WARN`. With the default `RUST_LOG=info` you will not see it — `grep` against `daemon.log` returns nothing and the operator's natural conclusion is "config OK," which is exactly the wrong inference. **When reproducing this failure mode, set `RUST_LOG=debug` (or `RUST_LOG=agend_terminal=debug`) before starting the daemon.** A separate `agend-terminal`-side change to raise this line to `WARN` is being tracked by dev-team; this guide reflects the current behaviour. **Inbound failure mode:** -Each rejected inbound is dropped with a `WARN` log naming the offending `user_id`. +Each rejected inbound is dropped with a log naming the offending `user_id`. Same caveat: confirm log level on `agend-terminal` source before relying on a specific level for grep. **Migration action** @@ -135,7 +136,7 @@ channel: type: telegram bot_token_env: BOT_TOKEN group_id: -1001234567890 # bare int, see High-friction #2 - user_allowlist: # top-level on channel; copy from access.allowed_users on @suzuke/agend + user_allowlist: # top-level on channel; copy from channel.access.allowed_users on @suzuke/agend - 111111111 # Telegram numeric user ID, bare int - 222222222 ``` @@ -144,24 +145,25 @@ channel: 1. `grep "outbound notify dropped" $AGEND_HOME/daemon.log` — confirms the gate fired. 2. Confirm `channel.user_allowlist` is set in `fleet.yaml` and your numeric user ID is in it (use [@userinfobot](https://t.me/userinfobot) on Telegram if unsure). -3. If you previously had `access.allowed_users` on `@suzuke/agend`, that path is **not read** by Rust — copy the entries to top-level `channel.user_allowlist` and use bare int form for IDs. +3. If you previously had `channel.access.allowed_users` on `@suzuke/agend` (the fully qualified path; `access` is nested under `channel` per `src/types.ts:57`), that path is **not read** by Rust — copy the entries to top-level `channel.user_allowlist` and use bare int form for IDs. +4. **TS pairing-mode users** (those who got their user IDs onto `channel.access.allowed_users` via `agend access pair` issuing pairing codes) must also be enumerated into `channel.user_allowlist` directly — `agend-terminal` has no pairing flow equivalent. If your `@suzuke/agend` install used `access.mode: "pairing"`, list every active user ID in the Rust allowlist explicitly. > **Why fail-closed:** an empty/absent allowlist on a Telegram bot is a credential exposure waiting to happen — anyone who guesses or exfiltrates the bot token can DM the bot and trigger arbitrary backend tool use. Failing closed forces the operator to make an explicit access decision, which is both safer and more debuggable than silently exposing the bot. ### High-friction change #2: `group_id` is strict `i64` — **bare int form only** -**Reference:** [`src/fleet.rs:46`](https://github.com/suzuke/agend-terminal/blob/main/src/fleet.rs#L46) (field type) and [`src/fleet.rs:725-826`](https://github.com/suzuke/agend-terminal/blob/main/src/fleet.rs#L725-L826) (round-trip tests covering `-100123456`, `-100999`, `-3`, `-1`, `-2`). +**Reference:** [`src/fleet.rs:46`](https://github.com/suzuke/agend-terminal/blob/main/src/fleet.rs#L46) (field type) and [`src/fleet.rs:725-826`](https://github.com/suzuke/agend-terminal/blob/main/src/fleet.rs#L725-L826) (round-trip tests). -In `@suzuke/agend`, `channel.group_id` is typed as `number | string` and the YAML loader accepts either. Operator guidance has been "always quote large negative IDs as strings" because some code paths mishandled the negative-prefix supergroup IDs as numbers (precision/sign edge cases). +In `@suzuke/agend`, `channel.group_id` was typed as `number | string` and the YAML loader accepted either. **Canonical TS docs used the bare int form for Telegram supergroup IDs** — see `docs/configuration.md:23` (`group_id: -1001234567890`) and `tests/setup-wizard-config.test.ts:127` (`toBe(-1001234567890)`). The quoted-string form was reserved for **Discord guild IDs** (positive 18–19-digit snowflakes that exceed JavaScript's `Number.MAX_SAFE_INTEGER` of 2^53 − 1) — see `docs/features.md:302`, `docs/plugin-development.md:293`, `docs/plugin-adapter-architecture.md:28,298`. Quoting Discord IDs dodged JS `Number` precision loss; quoting Telegram IDs was never the canonical TS recommendation. -In `agend-terminal`, `channel.group_id` is typed as **`i64`** with strict serde deserialization. **Only the bare int form is accepted** — a quoted-string form (`group_id: "-1001234567890"`) **fails at startup** with a serde error like `"invalid type: string \"-1001234567890\", expected i64"`. The Rust YAML parser does not auto-coerce string ↔ int. +In `agend-terminal`, `channel.group_id` is typed as **`i64`** with strict serde deserialization. **Only the bare int form is accepted** — a quoted-string form (`group_id: "-1001234567890"`) **fails at startup** with a serde error like `"invalid type: string \"-1001234567890\", expected i64"`. The Rust YAML parser does not auto-coerce string ↔ int. `i64` covers both Telegram negative supergroup IDs (well within range) and Discord snowflakes (which fit in 2^63 − 1) with the same bare-int form. -The TS string-handling bug does not apply on Rust (i64 covers the full negative supergroup range natively, with round-trip tests committed at fleet.rs:725-826). +Round-trip tests at [`src/fleet.rs:725-826`](https://github.com/suzuke/agend-terminal/blob/main/src/fleet.rs#L725-L826) lock the bare-int parsing contract for representative negative IDs (`-100123456`, `-100999`, `-3`, `-1`, `-2`). Quoted-string rejection is serde's default behaviour for `i64`-typed fields and is not separately regression-tested — a future serde version with a permissive coercion knob could change it, though the typed-field contract makes such drift unlikely. -**This reverses the TS migration guidance.** If you have been quoting `group_id` on `@suzuke/agend` (the recommended workaround), you must **un-quote** it before the new daemon will load your `fleet.yaml`. +**Migration action.** If your `fleet.yaml` quoted any `group_id` value (Discord users especially, where quoting was the standard TS workaround for the precision issue), un-quote it before the new daemon will load the config: ```yaml -# Required form on agend-terminal: +# Required form on agend-terminal (Telegram and Discord both): channel: group_id: -1001234567890 # bare int @@ -182,7 +184,7 @@ channel: | `project_roots` | **removed** | No fleet-level allowlist of project roots; use per-instance `working_directory`. | | `defaults` | `defaults: InstanceDefaults` ✓ | Field set differs significantly — see instance table below. | | `instances` | `instances: HashMap` ✓ | Same role; field set leaner. | -| `teams` | `teams: HashMap` ✓ | Same shape (`{ members, description? }`). | +| `teams` | `teams: HashMap` ✓ | Rust shape (`src/fleet.rs:175-183`): `{ members, orchestrator?, description? }`. The new `orchestrator: Option` field names a member that acts as the routing target for team-addressed `delegate_task` and groups the team in the TUI's team-tab view. TS `TeamConfig` has only `{ members, description? }` — when porting, leave `orchestrator` unset to preserve TS-equivalent behaviour, or set it once you adopt the new routing convention. | | `templates` | `templates: Option>` | **Parser-only on Rust today** — keys round-trip but template expansion is not implemented yet. Do not depend on TS template semantics. | | `profiles` | **removed** | Per-instance fields are flat; reusable profiles will return as a separate concern if needed. | | `health_port` | **removed** | Daemon does not expose a separate health port; use `agend-terminal doctor`. | diff --git a/docs/migration-to-agend-terminal.zh-TW.md b/docs/migration-to-agend-terminal.zh-TW.md index 08f5f88e..6e78a1bf 100644 --- a/docs/migration-to-agend-terminal.zh-TW.md +++ b/docs/migration-to-agend-terminal.zh-TW.md @@ -113,17 +113,18 @@ Rust schema 在 [`src/fleet.rs:7-183`](https://github.com/suzuke/agend-terminal/ **Outbound 失敗模式 (post-PR #216 outbound notify fail-closed):** -如果 daemon 想對 channel 通知,但收件者不在 allowlist 中,你會在 `daemon.log` 看到: +如果 daemon 想對 channel 通知,但收件者不在 allowlist 中,以下這行會以 **DEBUG** level 寫入 `daemon.log` (`tracing::debug!` 在 `src/channel/mod.rs:251-255`): ``` -WARN outbound notify dropped — channel not authorised (fail-closed; configure user_allowlist to opt in) +DEBUG outbound notify dropped — channel not authorised (fail-closed; configure user_allowlist to opt in) ``` -(來源:`src/channel/mod.rs:254`。) +> [!IMPORTANT] +> Drop 事件是 `DEBUG`,**不是** `WARN`。預設 `RUST_LOG=info` 下你看不到 ——`grep daemon.log` 也找不到,operator 自然會結論「config OK」,**這正好是相反的判斷**。**重現此失敗模式時請先設 `RUST_LOG=debug` (或 `RUST_LOG=agend_terminal=debug`) 再啟動 daemon。** 把這行升到 `WARN` 是 `agend-terminal` 端的修正 (dev-team backlog),本指引反映目前行為。 **Inbound 失敗模式:** -每筆被拒絕的 inbound 會 drop,並記一條帶該 `user_id` 的 `WARN` log。 +每筆被拒絕的 inbound 會 drop,並記一條帶該 `user_id` 的 log。同樣注意:依特定 log level 做 grep 前,請先在 `agend-terminal` 原始碼確認 level。 **遷移動作** @@ -133,7 +134,7 @@ channel: type: telegram bot_token_env: BOT_TOKEN group_id: -1001234567890 # 裸 int,見 High-friction #2 - user_allowlist: # channel 的 top-level;從 @suzuke/agend 的 access.allowed_users 複製 + user_allowlist: # channel 的 top-level;從 @suzuke/agend 的 channel.access.allowed_users 複製 - 111111111 # Telegram 數字 user ID,裸 int - 222222222 ``` @@ -142,24 +143,25 @@ channel: 1. `grep "outbound notify dropped" $AGEND_HOME/daemon.log` —— 確認 gate 觸發了。 2. 確認 `channel.user_allowlist` 已設定於 `fleet.yaml`,且你的數字 user ID 在裡面 (不確定可在 Telegram 用 [@userinfobot](https://t.me/userinfobot))。 -3. 如果你之前在 `@suzuke/agend` 用 `access.allowed_users`,該路徑 Rust **不再讀取** —— 把條目搬到 top-level `channel.user_allowlist`,IDs 用裸 int 形式。 +3. 如果你之前在 `@suzuke/agend` 用 `channel.access.allowed_users` (完整路徑;`access` 在 `src/types.ts:57` 嵌在 `channel` 之下),該路徑 Rust **不再讀取** —— 把條目搬到 top-level `channel.user_allowlist`,IDs 用裸 int 形式。 +4. **TS pairing-mode 使用者** (透過 `agend access pair` 發 pairing 碼,使用者兌換後 ID 進到 `channel.access.allowed_users`) 也必須直接列進 `channel.user_allowlist` —— `agend-terminal` 沒有 pairing flow 等價物。如果你的 `@suzuke/agend` 環境用 `access.mode: "pairing"`,請把每個目前 active 的 user ID 明列進 Rust allowlist。 > **為什麼 fail-closed:** Telegram bot 上空的 / 不存在的 allowlist 是 credential 暴露的等待事故 —— 任何人猜到或外洩 bot token 後可以 DM bot 觸發任意 backend tool use。Fail-closed 強迫 operator 做出明確的存取決策,比靜默暴露更安全也更易 debug。 ### 高摩擦變更 #2:`group_id` 嚴格 `i64` —— **僅接受裸 int** -**參考:** [`src/fleet.rs:46`](https://github.com/suzuke/agend-terminal/blob/main/src/fleet.rs#L46) (欄位型別) 與 [`src/fleet.rs:725-826`](https://github.com/suzuke/agend-terminal/blob/main/src/fleet.rs#L725-L826) (round-trip 測試覆蓋 `-100123456`、`-100999`、`-3`、`-1`、`-2`)。 +**參考:** [`src/fleet.rs:46`](https://github.com/suzuke/agend-terminal/blob/main/src/fleet.rs#L46) (欄位型別) 與 [`src/fleet.rs:725-826`](https://github.com/suzuke/agend-terminal/blob/main/src/fleet.rs#L725-L826) (round-trip 測試)。 -在 `@suzuke/agend`,`channel.group_id` 型別是 `number | string`,YAML loader 兩種都接受。Operator 指引一直是「大型負 ID 永遠 quote 成字串」,因為某些 code path 把 negative-prefix supergroup IDs 當 number 處理會 hit precision/sign 邊界。 +在 `@suzuke/agend`,`channel.group_id` 型別是 `number | string`,YAML loader 兩種都接受。**TS canonical 文件對 Telegram supergroup ID 採裸 int** —— 見 `docs/configuration.md:23` (`group_id: -1001234567890`) 與 `tests/setup-wizard-config.test.ts:127` (`toBe(-1001234567890)`)。需要 quote 成字串的是 **Discord guild ID** (正向 18-19 位 snowflake,超過 JavaScript `Number.MAX_SAFE_INTEGER` 即 2^53 − 1) —— 見 `docs/features.md:302`、`docs/plugin-development.md:293`、`docs/plugin-adapter-architecture.md:28,298`。Quote Discord ID 是為了避開 JS `Number` 的精度損失;quote Telegram ID 從來不是 TS 的 canonical 建議。 -在 `agend-terminal`,`channel.group_id` 嚴格定為 **`i64`**,serde deserialization 嚴格。**只接受裸 int 形式** —— quoted-string 形式 (`group_id: "-1001234567890"`) **會在啟動時失敗**,serde error 形如 `"invalid type: string \"-1001234567890\", expected i64"`。Rust YAML parser 不會自動 string ↔ int 互轉。 +在 `agend-terminal`,`channel.group_id` 嚴格定為 **`i64`**,serde deserialization 嚴格。**只接受裸 int 形式** —— quoted-string 形式 (`group_id: "-1001234567890"`) **會在啟動時失敗**,serde error 形如 `"invalid type: string \"-1001234567890\", expected i64"`。Rust YAML parser 不會自動 string ↔ int 互轉。`i64` 同時涵蓋 Telegram 負向 supergroup ID (在範圍內) 與 Discord snowflake (適合 2^63 − 1),兩者都可用同一裸 int 形式。 -TS 的 string-handling bug 在 Rust 上不適用 (i64 原生覆蓋整個負 supergroup 範圍,並有 fleet.rs:725-826 的 round-trip 測試)。 +[`src/fleet.rs:725-826`](https://github.com/suzuke/agend-terminal/blob/main/src/fleet.rs#L725-L826) 的 round-trip 測試鎖定的是裸 int 解析契約,代表性負 ID 包含 (`-100123456`、`-100999`、`-3`、`-1`、`-2`)。Quoted-string 拒絕是 serde 對 `i64`-typed 欄位的預設行為,沒有單獨 regression-pinned —— 將來若 serde 出現允許寬鬆轉型的旋鈕,行為可能變,不過 typed-field 契約讓這種漂移不太可能發生。 -**這 反轉了 TS 的遷移建議。** 如果你之前依 `@suzuke/agend` 建議 quote `group_id`,在新 daemon 載入 `fleet.yaml` 之前必須 **取消 quote**。 +**遷移動作。** 如果你的 `fleet.yaml` 對任何 `group_id` 值有 quote (尤其是 Discord 用戶,quote 是 TS 端的標準避坑作法),在新 daemon 載入 config 之前先取消 quote: ```yaml -# agend-terminal 必要形式: +# agend-terminal 必要形式 (Telegram 與 Discord 皆然): channel: group_id: -1001234567890 # 裸 int @@ -180,7 +182,7 @@ channel: | `project_roots` | **移除** | 沒有 fleet-level 的 project root allowlist;改用 per-instance `working_directory`。 | | `defaults` | `defaults: InstanceDefaults` ✓ | 欄位集合差異很大 —— 見下方 instance 表。 | | `instances` | `instances: HashMap` ✓ | 角色相同;欄位集合精簡。 | -| `teams` | `teams: HashMap` ✓ | 形狀相同 (`{ members, description? }`)。 | +| `teams` | `teams: HashMap` ✓ | Rust 形狀 (`src/fleet.rs:175-183`):`{ members, orchestrator?, description? }`。新增的 `orchestrator: Option` 欄位指定一個成員作為 team-addressed `delegate_task` 的 routing 目標,並用於 TUI team-tab 視圖的分組。TS `TeamConfig` 只有 `{ members, description? }` —— port 時若想保持 TS 等價行為,`orchestrator` 留空即可;採用新 routing 慣例後再設定。 | | `templates` | `templates: Option>` | **Rust 目前僅 parser** —— 鍵會 round-trip,但 template 展開尚未實作。**不要** 依賴 TS template 語意。 | | `profiles` | **移除** | Per-instance 欄位攤平;將來若需要,reusable profiles 會以另一個 concern 重新引入。 | | `health_port` | **移除** | Daemon 不開獨立 health port;改用 `agend-terminal doctor`。 |