@te-river/opencode-team-mode 1.7.1 → 1.7.3
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.
- package/README.md +1009 -849
- package/README.zh-CN.md +906 -756
- package/dist/host/v2-browser-gate.d.ts +4 -1
- package/dist/host/v2-browser-gate.d.ts.map +1 -1
- package/dist/host/v2-browser-gate.js +1 -1
- package/dist/host/v2-browser-gate.js.map +1 -1
- package/dist/host/v2-capabilities.js +1 -1
- package/dist/host/v2-capabilities.js.map +1 -1
- package/dist/host/v2-guard.d.ts +3 -1
- package/dist/host/v2-guard.d.ts.map +1 -1
- package/dist/host/v2-guard.js +2 -2
- package/dist/host/v2-guard.js.map +1 -1
- package/dist/host/v2-offload.d.ts +12 -2
- package/dist/host/v2-offload.d.ts.map +1 -1
- package/dist/host/v2-offload.js +9 -9
- package/dist/host/v2-offload.js.map +1 -1
- package/dist/host/v2-probe-chain.d.ts +5 -1
- package/dist/host/v2-probe-chain.d.ts.map +1 -1
- package/dist/host/v2-probe-chain.js +4 -6
- package/dist/host/v2-probe-chain.js.map +1 -1
- package/dist/host/v2-probe.d.ts.map +1 -1
- package/dist/host/v2-probe.js +2 -1
- package/dist/host/v2-probe.js.map +1 -1
- package/dist/host/v2-prune.d.ts +5 -1
- package/dist/host/v2-prune.d.ts.map +1 -1
- package/dist/host/v2-prune.js +8 -14
- package/dist/host/v2-prune.js.map +1 -1
- package/dist/host/v2-retry.d.ts +9 -2
- package/dist/host/v2-retry.d.ts.map +1 -1
- package/dist/host/v2-retry.js +13 -15
- package/dist/host/v2-retry.js.map +1 -1
- package/dist/host/v2-session.d.ts +6 -2
- package/dist/host/v2-session.d.ts.map +1 -1
- package/dist/host/v2-session.js +7 -13
- package/dist/host/v2-session.js.map +1 -1
- package/dist/host/v2.d.ts +7 -2
- package/dist/host/v2.d.ts.map +1 -1
- package/dist/host/v2.js +38 -35
- package/dist/host/v2.js.map +1 -1
- package/dist/prompts/lead.d.ts +1 -1
- package/dist/prompts/lead.d.ts.map +1 -1
- package/dist/prompts/lead.js +23 -7
- package/dist/prompts/lead.js.map +1 -1
- package/dist/prompts/specialists.d.ts +1 -1
- package/dist/prompts/specialists.d.ts.map +1 -1
- package/dist/prompts/specialists.js +1 -1
- package/dist/tm/args-schema.d.ts +1 -1
- package/dist/tm/args-schema.js +6 -6
- package/dist/tm/args-schema.js.map +1 -1
- package/dist/tm/board.js +2 -2
- package/dist/tm/board.js.map +1 -1
- package/dist/tm/config-files.d.ts +41 -17
- package/dist/tm/config-files.d.ts.map +1 -1
- package/dist/tm/config-files.js +68 -73
- package/dist/tm/config-files.js.map +1 -1
- package/dist/tm/config-layers.d.ts +65 -33
- package/dist/tm/config-layers.d.ts.map +1 -1
- package/dist/tm/config-layers.js +165 -81
- package/dist/tm/config-layers.js.map +1 -1
- package/dist/tm/config-template.d.ts +32 -0
- package/dist/tm/config-template.d.ts.map +1 -0
- package/dist/tm/config-template.js +74 -0
- package/dist/tm/config-template.js.map +1 -0
- package/dist/tm/config.d.ts +115 -140
- package/dist/tm/config.d.ts.map +1 -1
- package/dist/tm/config.js +100 -200
- package/dist/tm/config.js.map +1 -1
- package/dist/tm/index.d.ts +11 -3
- package/dist/tm/index.d.ts.map +1 -1
- package/dist/tm/index.js +46 -16
- package/dist/tm/index.js.map +1 -1
- package/dist/tm/ledger.d.ts +7 -3
- package/dist/tm/ledger.d.ts.map +1 -1
- package/dist/tm/ledger.js +5 -4
- package/dist/tm/ledger.js.map +1 -1
- package/dist/tm/memory.js +3 -3
- package/dist/tm/pipelines.js +1 -1
- package/dist/tm/search.js +6 -6
- package/dist/tm/search.js.map +1 -1
- package/dist/tm/stats.js +2 -2
- package/dist/tm/stats.js.map +1 -1
- package/dist/tm/tools.js +2 -2
- package/dist/tm/webfetch.d.ts +5 -2
- package/dist/tm/webfetch.d.ts.map +1 -1
- package/dist/tm/webfetch.js +24 -18
- package/dist/tm/webfetch.js.map +1 -1
- package/docs/installation-v2.md +61 -38
- package/docs/installation.md +4 -4
- package/docs/v2-port-notes.md +31 -0
- package/package.json +1 -1
- package/scripts/gen-v2-config.mjs +40 -4
- package/dist/approval-gate.d.ts +0 -205
- package/dist/approval-gate.d.ts.map +0 -1
- package/dist/approval-gate.js +0 -627
- package/dist/approval-gate.js.map +0 -1
- package/dist/host/v1.d.ts +0 -14
- package/dist/host/v1.d.ts.map +0 -1
- package/dist/host/v1.js +0 -340
- package/dist/host/v1.js.map +0 -1
- package/dist/host/v2-client.d.ts +0 -53
- package/dist/host/v2-client.d.ts.map +0 -1
- package/dist/host/v2-client.js +0 -193
- package/dist/host/v2-client.js.map +0 -1
- package/dist/host/v2-transcript.d.ts +0 -73
- package/dist/host/v2-transcript.d.ts.map +0 -1
- package/dist/host/v2-transcript.js +0 -151
- package/dist/host/v2-transcript.js.map +0 -1
- package/dist/host-hooks.d.ts +0 -126
- package/dist/host-hooks.d.ts.map +0 -1
- package/dist/host-hooks.js +0 -284
- package/dist/host-hooks.js.map +0 -1
- package/dist/tm/bash-timeout.d.ts +0 -72
- package/dist/tm/bash-timeout.d.ts.map +0 -1
- package/dist/tm/bash-timeout.js +0 -100
- package/dist/tm/bash-timeout.js.map +0 -1
- package/dist/tm/browser.d.ts +0 -642
- package/dist/tm/browser.d.ts.map +0 -1
- package/dist/tm/browser.js +0 -2836
- package/dist/tm/browser.js.map +0 -1
- package/dist/tm/ptc/budgets.d.ts +0 -59
- package/dist/tm/ptc/budgets.d.ts.map +0 -1
- package/dist/tm/ptc/budgets.js +0 -138
- package/dist/tm/ptc/budgets.js.map +0 -1
- package/dist/tm/ptc/contract.d.ts +0 -171
- package/dist/tm/ptc/contract.d.ts.map +0 -1
- package/dist/tm/ptc/contract.js +0 -85
- package/dist/tm/ptc/contract.js.map +0 -1
- package/dist/tm/ptc/driver.d.ts +0 -40
- package/dist/tm/ptc/driver.d.ts.map +0 -1
- package/dist/tm/ptc/driver.js +0 -213
- package/dist/tm/ptc/driver.js.map +0 -1
- package/dist/tm/ptc/engines.d.ts +0 -43
- package/dist/tm/ptc/engines.d.ts.map +0 -1
- package/dist/tm/ptc/engines.js +0 -454
- package/dist/tm/ptc/engines.js.map +0 -1
- package/dist/tm/ptc/gate.d.ts +0 -28
- package/dist/tm/ptc/gate.d.ts.map +0 -1
- package/dist/tm/ptc/gate.js +0 -130
- package/dist/tm/ptc/gate.js.map +0 -1
- package/dist/tm/ptc/index.d.ts +0 -19
- package/dist/tm/ptc/index.d.ts.map +0 -1
- package/dist/tm/ptc/index.js +0 -15
- package/dist/tm/ptc/index.js.map +0 -1
- package/dist/tm/ptc/pscan.d.ts +0 -30
- package/dist/tm/ptc/pscan.d.ts.map +0 -1
- package/dist/tm/ptc/pscan.js +0 -341
- package/dist/tm/ptc/pscan.js.map +0 -1
- package/dist/tm/ptc/summary.d.ts +0 -17
- package/dist/tm/ptc/summary.d.ts.map +0 -1
- package/dist/tm/ptc/summary.js +0 -134
- package/dist/tm/ptc/summary.js.map +0 -1
- package/dist/tm/ptc/tool.d.ts +0 -66
- package/dist/tm/ptc/tool.d.ts.map +0 -1
- package/dist/tm/ptc/tool.js +0 -231
- package/dist/tm/ptc/tool.js.map +0 -1
- package/dist/tm/ptc.d.ts +0 -298
- package/dist/tm/ptc.d.ts.map +0 -1
- package/dist/tm/ptc.js +0 -865
- package/dist/tm/ptc.js.map +0 -1
- package/dist/tm/pty.d.ts +0 -70
- package/dist/tm/pty.d.ts.map +0 -1
- package/dist/tm/pty.js +0 -245
- package/dist/tm/pty.js.map +0 -1
- package/dist/tool-coerce.d.ts +0 -24
- package/dist/tool-coerce.d.ts.map +0 -1
- package/dist/tool-coerce.js +0 -63
- package/dist/tool-coerce.js.map +0 -1
package/docs/installation-v2.md
CHANGED
|
@@ -56,7 +56,7 @@ implied:
|
|
|
56
56
|
| Sub-agent settlement | the host `event` hook | `ctx.event.subscribe()` — zero-dependency async iterable, filtered by event-type name (`src/host/v2-events.ts`). Without it `tm_join` cannot tell a settled child from a running one; `tm_stats` reports what the feed forwarded and which type names went unrecognised (names only, never payloads) |
|
|
57
57
|
| The task ledger | the host's `todowrite` | **`tm_ledger`**, stored in the host's `ctx.storage` (v2 has no `todowrite`) |
|
|
58
58
|
| Asking the user | the host's official per-request dialog | **a plugin cannot open a dialog on v2.** Governed calls that would have asked instead **fail closed** with a refusal that says why. The dialogs you do see are raised by the host itself (permission rules, out-of-project access) |
|
|
59
|
-
| Web access | a 22-host allowlist + the confirm dialog | **the domain gate is off** (`
|
|
59
|
+
| Web access | a 22-host allowlist + the confirm dialog | **the domain gate is off** (`webfetchAllowedDomains` defaults to `["*"]` on v2): a 2.x plugin cannot raise a dialog, so "approve to proceed" was an instruction to wait for a window that never opens. Address classes are still policed — cloud-metadata / link-local / reserved ranges are refused under every setting, and private space (RFC1918, CGNAT, ULA, IPv6 link-local, `.localhost`) is refused too, but the refusal now names both exits instead of promising a click: `"privateSpace": "allow"` in the global `team-mode.jsonc` (opens the class) or that one hostname in `webfetchAllowedDomains` (opens exactly it). **Loopback (`127/8`, `::1`, `localhost`) is served by default** — it reaches only a service the user started on their own machine, and a gate whose only exit is a dialog the plugin cannot raise is a wall, not a gate |
|
|
60
60
|
| Sub-agents | needs `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true` | native; the plugin forces `background: true` on every `subagent` call, so **do not set that env var for 2.x** (it is a v1 workaround and changes nothing here) |
|
|
61
61
|
| Effect on your other modes | the plugin's hooks are global | **none.** Every hook checks the session's owner first, so `build`, `plan` and any agent you installed yourself stay exactly as a fresh OpenCode leaves them — no tool is deleted from their requests, no temperature is set, nothing is offloaded, no permission is tightened, no dispatch is converted to background |
|
|
62
62
|
|
|
@@ -246,29 +246,40 @@ env-file line). It is zero-token by default; `--turns` adds the checks that need
|
|
|
246
246
|
|
|
247
247
|
### Layered configuration (`team-mode.jsonc`)
|
|
248
248
|
|
|
249
|
-
|
|
249
|
+
`team-mode.jsonc` is the **only** configuration source — every `TM_*` configuration environment
|
|
250
|
+
variable is gone. Two layers, mirroring the host's own config:
|
|
250
251
|
|
|
251
252
|
| layer | file |
|
|
252
253
|
|---|---|
|
|
253
|
-
| global | `~/.config/opencode/team-mode.jsonc` |
|
|
254
|
+
| global | `~/.config/opencode/team-mode.jsonc` (honours `OPENCODE_CONFIG_DIR`) |
|
|
254
255
|
| project | `<project>/team-mode.jsonc`, or `<project>/.opencode/team-mode.jsonc` (the `.opencode` one wins) |
|
|
255
256
|
|
|
256
|
-
Precedence is `
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
257
|
+
Precedence is `global < project`. **The plugin writes the global file for you at boot** when it is
|
|
258
|
+
absent: a fully-commented, INERT template — every key appears as a comment (`// "key": <default>,`)
|
|
259
|
+
under its one-line doc, so the file sets nothing until you uncomment a line. It is idempotent and
|
|
260
|
+
never overwrites an existing file. Turn it off with the plugin option `autoCreate: false` (or the
|
|
261
|
+
internal `TM_CONFIG_AUTOCREATE=off`); the boot row records `config_autocreate` as
|
|
262
|
+
`created` / `present` / `off` / `failed`.
|
|
263
|
+
|
|
264
|
+
Keys are camelCase (`offloadThreshold`, `searchDefaultEngine`, …) — the registry in
|
|
265
|
+
`src/tm/config-layers.ts` is the single source of truth (59 keys, each with its type, default and
|
|
266
|
+
doc). Five keys are **red lines a PROJECT file may not change** — `envProtect`, `r6FineAsk`,
|
|
267
|
+
`privateSpace`, `webfetchAllowedDomains`, `bashReadonlyAllowed`; they are settable only in the
|
|
268
|
+
global file, and a project value is ignored and reported. A file that fails to parse is skipped
|
|
269
|
+
**whole** (half a config is worse than none), while a single bad key loses only itself. Unknown keys
|
|
270
|
+
warn at boot and are never applied. `tm_stats` shows which layer each key came from, the red-line
|
|
271
|
+
keys a file tried to set and lost, the unknown keys, the skipped layers, and the auto-create state.
|
|
272
|
+
|
|
273
|
+
**R6 (env protection) is armed by default** (`envProtect` defaults to `"strict"`; the canonical
|
|
274
|
+
values are `strict` / `standard` / `off`). A Team role's native `read` of a `.env` is denied
|
|
268
275
|
outright, and a shell command that reads the environment goes to the host's own permission prompt.
|
|
269
|
-
Turn it off with the plugin option `envProtect: false` or with `
|
|
270
|
-
shell face is an `ask` (the host's dialog — a
|
|
271
|
-
a hard `deny` with no consent path.
|
|
276
|
+
Turn it off with the plugin option `envProtect: false` or with `"envProtect": "off"` in the
|
|
277
|
+
**global** file (it is a red-line key); on 2.x the shell face is an `ask` (the host's dialog — a
|
|
278
|
+
plugin cannot raise one) while the env-FILE face is a hard `deny` with no consent path.
|
|
279
|
+
|
|
280
|
+
The only `TM_*` environment variables left are internal/test switches, not configuration:
|
|
281
|
+
`TM_STORE_RECLAIM` (test override of the `storeReclaim` key), `TM_V2_PROBE` (name-level JSONL
|
|
282
|
+
dump) and `TM_CONFIG_AUTOCREATE` (the auto-create off switch).
|
|
272
283
|
|
|
273
284
|
### Uninstalling
|
|
274
285
|
|
|
@@ -287,11 +298,12 @@ a hard `deny` with no consent path.
|
|
|
287
298
|
| Roles are not in the picker | Step 2 never ran, or ran against a different `--dir`. `ls ~/.config/opencode/agents` |
|
|
288
299
|
| The default silently became `build` | `default_agent` names an agent that does not exist — re-run Step 2, then Step 3 |
|
|
289
300
|
| A hand-written `agents/Team.md` was not updated | By design: the generator refuses files it did not create. Move yours aside, or pass `--force` |
|
|
301
|
+
| The picker shows the lead as `team`, or Team is not the default | The host takes an agent's ID from the FILE NAME. A machine that ever carried the pre-1.7 `team.md` registers the lead as `team`, while the installer writes `default_agent: "Team"` — and the documented behaviour for a default naming an agent it cannot find is a silent fall back to `build`. **Fixed in 1.7.2**: the generator renames a case-only entry it has proven is the same file to the canonical spelling. On 1.7.1 or older a re-run does NOT fix it (a write through `Team.md` keeps the old spelling on Windows) — rename by hand in two steps: `team.md` → `Team.md` |
|
|
290
302
|
| `/team-plan` etc. run but not as the specialist | 2.x documents `mode: subagent` as "runs only in a child session"; if a command selecting a specialist does not behave, the fix is `mode: "all"` in the role definition (report it — this is the one item in this flow still unverified on a live host) |
|
|
291
303
|
| `tm_webfetch` on an odd site says it refused **without asking anyone** | Expected on 2.x: a plugin cannot raise a dialog. Use a source that works, or let the host's own permission rule allow it; the address red line (metadata / private) has no consent path at all, on either generation |
|
|
292
304
|
| `task`/`subagent` seems to block the lead | On 2.x the plugin forces `background: true`; if you also set the old `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS`, unset it — it is a v1 flag and only confuses the picture |
|
|
293
|
-
| A tool's output arrives un-offloaded | `
|
|
294
|
-
| Sessions used to compact at 75% on their own; they do not any more | That trigger was **removed in 1.7.1** (it shipped in 1.7.0). The host owns when a conversation is summarized again, exactly as if the plugin were not installed. The three knobs that drove it (`TM_COMPACT_TRIGGER`, `TM_COMPACT_AT_PERCENT`, `TM_COMPACT_MIN_MS`) are gone, and a `team-mode.jsonc` that still names them reports them as unknown keys at boot instead of applying them. What stays is additive and only runs when the HOST compacts:
|
|
305
|
+
| A tool's output arrives un-offloaded | `"nativeOffload": "off"` in the config file (or a missing `tool.execute.after` seam on an older 2.x build) — `tm_stats` names which |
|
|
306
|
+
| Sessions used to compact at 75% on their own; they do not any more | That trigger was **removed in 1.7.1** (it shipped in 1.7.0). The host owns when a conversation is summarized again, exactly as if the plugin were not installed. The three knobs that drove it (`TM_COMPACT_TRIGGER`, `TM_COMPACT_AT_PERCENT`, `TM_COMPACT_MIN_MS`) are gone, and a `team-mode.jsonc` that still names them reports them as unknown keys at boot instead of applying them. What stays is additive and only runs when the HOST compacts: the survival list is pushed on `session.hook("compaction")` (the `compactionContext` key is a v1-only parse-compat entry) |
|
|
295
307
|
| The plugin behaves oddly — two dialogs, doubled hooks, tools registered twice | It is loaded twice. `opencode.json` and `opencode.jsonc` are merged, and the host's dedupe matches only an identical string, so the same package under two spellings (or in both files) is two personalities. Count the Team entries across BOTH files; `msg="loading plugin"` appearing twice for one id is the proof |
|
|
296
308
|
| You wrote the entry and the host says nothing at all about it | Two silent causes, both by design: the key is singular `plugin` (2.x reads `plugins`), or a directory entry without a root `index.js`. The loader resolves a directory as `<dir>/index` only and drops it with no message when that fails — absence of an error is not a success signal here |
|
|
297
309
|
| A background sub-agent's reply is huge | The child is registered, collectable AND readable: `tm_join` lists it (marked 宿主 subagent 派发), reads its report back through `ctx.session.context`, and credits the seam it came from (`正文来源=ctx.session.context`) — so the reply arrives through the same JIT pipeline as everything else (offload + ≤80-token preview + `tm_fetch` handle when it is oversized). What v2 still never gives a plugin is the injected message BEFORE the host persists it, so we can read a report but cannot rewrite the injection — which is why a >~50-line DELIVERABLE still belongs on the blackboard file with its path in the reply |
|
|
@@ -323,7 +335,7 @@ a hard `deny` with no consent path.
|
|
|
323
335
|
| 子代理结算检测 | 宿主的 `event` 钩子 | `ctx.event.subscribe()`——零依赖的 async iterable,按事件类型名过滤(`src/host/v2-events.ts`)。没有它 `tm_join` 分不清「已结算」和「仍在跑」;`tm_stats` 会给出转发了多少、哪些类型名没认出来(只记名字,绝不记负载) |
|
|
324
336
|
| 任务清单 | 宿主 `todowrite` | **`tm_ledger`**,存在宿主的 `ctx.storage` 里(v2 不给插件 `todowrite`) |
|
|
325
337
|
| 征求用户同意 | 宿主官方逐次弹窗 | **插件在 v2 弹不出对话框。** 原本该问的受治理调用一律**直接拒绝**,并说明是"没人可问"而不是"问了被拒"。你看到的弹窗都来自宿主自己(权限规则、越出项目目录) |
|
|
326
|
-
| 联网 | 22 个域名白名单 + 确认弹窗 | **域名门禁关掉**(`
|
|
338
|
+
| 联网 | 22 个域名白名单 + 确认弹窗 | **域名门禁关掉**(`webfetchAllowedDomains` 在 v2 上默认 `["*"]`):2.x 插件弹不出确认框,"批准后放行"等于让 agent 去等一个永远不会出现的窗口。地址类别照管——元数据 / 链路本地 / 保留网段在任何设置下都拒;私网(RFC1918、CGNAT、ULA、IPv6 链路本地、`.localhost`)也拒,但拒绝语现在给出两条出口而不是承诺点击:在全局 `team-mode.jsonc` 里写 `"privateSpace": "allow"`(放开整段),或把那一台主机名写进 `webfetchAllowedDomains`(只放开它自己)。**回环(`127/8`、`::1`、`localhost`)默认放行**——它只打到用户自己机器上的服务,而一道唯一出口是"插件弹不出来的确认框"的门禁,是墙不是门 |
|
|
327
339
|
| 非阻塞命令 | `tm_pty`——**已随 v1 支持一起移除** | **完全不注册**——v2 插件上下文没有 pty 域,这个工具只能报自己缺缝。慢步骤请一条一个 `shell` 调用(各自带 `timeout`),并把输出 tee 到日志好回读 |
|
|
328
340
|
| 子代理 | 需要 `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true` | 原生能力;插件对每次 `subagent` 强制 `background: true`,所以 **2.x 不要去设那个环境变量**(那是 v1 的补丁,在这里什么也不改变,只会让人误判) |
|
|
329
341
|
| 对你其它模式的影响 | 插件钩子是全局的 | **没有影响。** 每个钩子都先看会话归属,所以 `build`、`plan` 和你自己装的 agent 都保持刚装好 OpenCode 时的样子——不会从它们的请求里删工具、不会设温度、不会卸载结果、不会收紧权限、也不会把它们的派发改成后台 |
|
|
@@ -486,24 +498,30 @@ opencode reload
|
|
|
486
498
|
|
|
487
499
|
### 分层配置(`team-mode.jsonc`)
|
|
488
500
|
|
|
489
|
-
|
|
501
|
+
`team-mode.jsonc` 是**唯一**的配置源——所有 `TM_*` 配置环境变量都已移除。两层,与宿主自己的配置同构:
|
|
490
502
|
|
|
491
503
|
| 层 | 文件 |
|
|
492
504
|
|---|---|
|
|
493
|
-
| 全局 | `~/.config/opencode/team-mode.jsonc` |
|
|
505
|
+
| 全局 | `~/.config/opencode/team-mode.jsonc`(认 `OPENCODE_CONFIG_DIR`) |
|
|
494
506
|
| 项目 | `<project>/team-mode.jsonc`,或 `<project>/.opencode/team-mode.jsonc`(后者胜) |
|
|
495
507
|
|
|
496
|
-
优先级
|
|
497
|
-
|
|
508
|
+
优先级 `全局 < 项目`。**全局文件由插件在启动时自动生成**(文件不存在时):一份全注释的**惰性**模板——
|
|
509
|
+
每个键都以注释形式出现(`// "key": <默认值>,`),注释行就是它的一行说明,所以在你取消注释之前,这个文件什么都不设置。
|
|
510
|
+
幂等、原子写入,绝不覆盖已存在的文件。关闭方式:插件选项 `autoCreate: false`(或内部开关 `TM_CONFIG_AUTOCREATE=off`);
|
|
511
|
+
启动行会记录 `config_autocreate`(`created` / `present` / `off` / `failed`)。
|
|
498
512
|
|
|
499
|
-
|
|
500
|
-
|
|
513
|
+
键名是 camelCase(`offloadThreshold`、`searchDefaultEngine` …)——`src/tm/config-layers.ts` 里的注册表是唯一事实来源
|
|
514
|
+
(59 个键,每个都带类型、默认值与说明)。有五个键是**项目文件不许改写的红线**——`envProtect`、`r6FineAsk`、
|
|
515
|
+
`privateSpace`、`webfetchAllowedDomains`、`bashReadonlyAllowed`;它们只能在全局文件里设置,项目文件里写了会被忽略并报告。
|
|
501
516
|
解析失败的那一层**整层跳过**(半份配置比没有配置更危险),单个坏键只丢它自己。未知键在启动时告警、**不应用**。
|
|
502
|
-
`tm_stats`
|
|
517
|
+
`tm_stats` 会显示每个键最终来自哪一层、被忽略的红线键、未知键、被跳过的层,以及自动生成的状态。
|
|
503
518
|
|
|
504
|
-
**R6
|
|
505
|
-
|
|
506
|
-
env-FILE 面是硬 `deny`、没有同意路径。
|
|
519
|
+
**R6(环境变量保护)默认开启**(`envProtect` 默认 `"strict"`;规范取值 `strict` / `standard` / `off`)。Team 角色用原生 `read` 读 `.env` 会被直接拒;
|
|
520
|
+
读环境变量的 shell 命令会走宿主自己的权限提示。关闭方式:插件选项 `envProtect: false`,或在**全局**文件里写 `"envProtect": "off"`(它是红线键)。
|
|
521
|
+
2.x 上 shell 面是 `ask`(宿主的对话框——插件弹不出来),env-FILE 面是硬 `deny`、没有同意路径。
|
|
522
|
+
|
|
523
|
+
仅存的 `TM_*` 环境变量是内部/测试开关,不是配置:`TM_STORE_RECLAIM`(测试覆盖 `storeReclaim` 键)、
|
|
524
|
+
`TM_V2_PROBE`(只记名字的 JSONL 转储)、`TM_CONFIG_AUTOCREATE`(自动生成的关闭开关)。
|
|
507
525
|
|
|
508
526
|
### 卸载
|
|
509
527
|
|
|
@@ -522,11 +540,12 @@ env-FILE 面是硬 `deny`、没有同意路径。
|
|
|
522
540
|
| 选择器里没有那六个角色 | 第 2 步没跑,或者 `--dir` 指到了别处。`ls ~/.config/opencode/agents` |
|
|
523
541
|
| 默认 agent 悄悄变成 `build` | `default_agent` 指向了一个不存在的角色——重跑第 2 步,再做第 3 步 |
|
|
524
542
|
| 手写的 `agents/Team.md` 没被更新 | 这是设计:生成器拒绝覆盖不是它生成的文件。把你的文件挪开,或显式加 `--force` |
|
|
543
|
+
| 选择器里领队显示成 `team`,或者 Team 不是默认 | 宿主按**文件名**取 agent id。机器上只要曾经有过 1.7 之前的 `team.md`,领队就以 `team` 注册,而安装器写的是 `default_agent: "Team"`——按文档,默认值指向一个找不到的 agent 时会**静默回退 build**。**1.7.2 已修**:生成器会把"已确认是同一个文件、仅大小写不同"的条目标成规范拼写。1.7.1 及更早版本重跑**修不好**(Windows 上从 `Team.md` 写入会保留旧拼写),需要手工两步改名:`team.md` → `Team.md` |
|
|
525
544
|
| `/team-plan` 能跑但不是以那个专家身份跑 | 2.x 把 `mode: subagent` 文档化为"只在子会话里运行"。如果选定专家的命令行为不对,改法是角色定义里用 `mode: "all"`(请回报——这是本流程里唯一还没在活体宿主上验证过的一项) |
|
|
526
545
|
| `tm_webfetch` 说它"没问任何人就直接拒绝" | v2 的预期行为:插件弹不出对话框。换一个不需要这次访问的源,或让宿主自己的权限规则放行;地址红线(元数据 / 私网)在两代宿主上都没有授权路径 |
|
|
527
546
|
| `subagent` 好像把领队挡住了 | v2 上插件会强制 `background: true`;如果你顺手设了老的 `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS`,请取消它——那是 v1 的开关,在这里只会误导判断 |
|
|
528
|
-
| 某个工具的输出没被卸载 | `
|
|
529
|
-
| 会话不再自己提前压缩了(1.7.0 曾是模型窗口的 75%) | 那个触发器在 **1.7.1 已整体移除**(它随 1.7.0 发布)。压缩时机重新完全归宿主,跟没装插件一样。驱动它的三个开关(`TM_COMPACT_TRIGGER`、`TM_COMPACT_AT_PERCENT`、`TM_COMPACT_MIN_MS`)已删除;`team-mode.jsonc` 里若还留着它们,启动时会报为未知键而不是照旧生效。保留下来的是"只在宿主自己压缩时"
|
|
547
|
+
| 某个工具的输出没被卸载 | 配置文件里写 `"nativeOffload": "off"`(或者那个 2.x 构建没有 `tool.execute.after` 缝)——`tm_stats` 会告诉你是哪一种 |
|
|
548
|
+
| 会话不再自己提前压缩了(1.7.0 曾是模型窗口的 75%) | 那个触发器在 **1.7.1 已整体移除**(它随 1.7.0 发布)。压缩时机重新完全归宿主,跟没装插件一样。驱动它的三个开关(`TM_COMPACT_TRIGGER`、`TM_COMPACT_AT_PERCENT`、`TM_COMPACT_MIN_MS`)已删除;`team-mode.jsonc` 里若还留着它们,启动时会报为未知键而不是照旧生效。保留下来的是"只在宿主自己压缩时"才追加的一项:必须存活清单在 `session.hook("compaction")` 上推送(`compactionContext` 键是 v1-only 的解析兼容项) |
|
|
530
549
|
| 插件行为怪:两次弹窗、钩子重复、工具像注册了两遍 | 它被加载了两次。`opencode.json` 与 `opencode.jsonc` 是合并读取的,而宿主去重只认完全相同的字符串,所以同一个包换两种写法(或者同时躺在两个文件里)就是两套人格。把两个文件里指向 Team 的条目一起数一遍;`msg="loading plugin"` 对同一个 id 出现两条就是证据 |
|
|
531
550
|
| 写了条目,但宿主日志里关于它一个字都没有 | 两个"设计上静默"的原因:键名写成了单数 `plugin`(2.x 读 `plugins`),或者目录条目缺根 `index.js`。宿主解析目录只试 `<dir>/index`,试不出来就直接丢掉、不留任何消息 —— 在这里"没报错"不是成功信号 |
|
|
532
551
|
| 后台子代理的回复太长 | 子会话现在会被登记、可被收集(`tm_join` 会列出它,行上标着「宿主 subagent 派发」),但它的**正文**仍然是宿主的注入消息,v2 不会在落盘前把这条消息交给插件;我们也不改写发出的消息(猜错那一层的形状等于静默删证据)。所以超限交付走黑板文件:正文进文件、回复里带路径,领队读那个文件 |
|
|
@@ -536,10 +555,14 @@ env-FILE 面是硬 `deny`、没有同意路径。
|
|
|
536
555
|
|
|
537
556
|
### v2 专属的开关
|
|
538
557
|
|
|
539
|
-
|
|
558
|
+
配置键写在 `team-mode.jsonc` 里(红线键只能在全局文件里设);环境变量只剩三个内部/测试开关。
|
|
559
|
+
|
|
560
|
+
| 键 / 变量 | 默认 | 作用 |
|
|
540
561
|
|---|---|---|
|
|
541
|
-
| `
|
|
542
|
-
| `
|
|
543
|
-
| `
|
|
544
|
-
| `TM_V2_PROBE
|
|
562
|
+
| `webfetchAllowedDomains`(配置) | `["*"]`(**仅 v2**) | 域名门禁默认关掉,只留地址红线;显式给了值就以它为准(v1 仍是 24 个种子域名) |
|
|
563
|
+
| `nativeOffload`(配置) | on | 用 `tool.execute.after` 治理宿主原生工具的结果;off 就退回宿主原样 |
|
|
564
|
+
| `r6FineAsk`(配置) | `""`(自动) | 由按命令分类器决定哪条 shell 要问;`off` 回到"每条 shell 都问",宿主没有 `permission.hook` 时也自动回到这一档 |
|
|
565
|
+
| `TM_V2_PROBE`(环境,内部) | 未设 | 把宿主真实工具面/动作名记成 JSONL(只有名字和计数) |
|
|
566
|
+
| `TM_STORE_RECLAIM`(环境,内部) | on | 测试覆盖 `storeReclaim` 键:off 时启动不回收存储分片 |
|
|
567
|
+
| `TM_CONFIG_AUTOCREATE`(环境,内部) | on | off 时不在启动时自动生成全局 `team-mode.jsonc` |
|
|
545
568
|
| `{ "package": "@te-river/opencode-team-mode@latest", "options": { "defaultAgent": false } }` | 不设置 = Team 永远默认 | 退出默认位抢占(2.x 必须用对象形状;1.x 的元组会被宿主拒:`path=$.plugins.1 kind=invalid`) |
|
package/docs/installation.md
CHANGED
|
@@ -97,7 +97,7 @@ Report which checks passed. If verification fails, see Troubleshooting.
|
|
|
97
97
|
OpenCode's own `task { background: true }` is the only sub-agent its interface
|
|
98
98
|
can **show**: the card links to the live child session, it does not block the
|
|
99
99
|
lead, and the host wakes the parent with the result. TeamMode then keeps that
|
|
100
|
-
result inside the token budget (`
|
|
100
|
+
result inside the token budget (`taskOffload`).
|
|
101
101
|
|
|
102
102
|
It rides an OpenCode **experimental flag** read from the host process
|
|
103
103
|
environment at startup, so a plugin cannot set it — the installer writes it for
|
|
@@ -273,7 +273,7 @@ never re-resolved on its own).
|
|
|
273
273
|
| Version is old despite `@latest` | Plugin cache never invalidates | See Updating — delete the cache dir (recursively; both layouts) |
|
|
274
274
|
| Still the old version after bumping the version or re-running the installer | The scoped cache `packages/@te-river/opencode-team-mode@latest` is nested below the top-level `packages/*opencode-team-mode*` glob, so the stale copy was never purged | Recursively delete that cache dir (`Get-ChildItem -Recurse -Filter '*opencode-team-mode*'` / `find -type d -name '*opencode-team-mode*' -prune`), then restart — the current installers already recurse |
|
|
275
275
|
| Node version error on startup | npm needs Node ≥ 18 | Upgrade Node |
|
|
276
|
-
| Env-var commands suddenly prompt for confirmation | That's the plugin's R6 protection working as designed | Approve once per operation, or set `
|
|
276
|
+
| Env-var commands suddenly prompt for confirmation | That's the plugin's R6 protection working as designed | Approve once per operation, or set `envProtect: "off"` in `team-mode.jsonc` (not recommended) |
|
|
277
277
|
| A user-defined agent named `team`/`architect`/… got shadowed | Plugin injects defaults; your own definitions always win | The plugin never clobbers user agents — check for typos in your agent names |
|
|
278
278
|
|
|
279
279
|
---
|
|
@@ -342,7 +342,7 @@ never re-resolved on its own).
|
|
|
342
342
|
|
|
343
343
|
OpenCode 自带的 `task { background: true }` 是唯一能在它界面上**看见**的子代理:
|
|
344
344
|
卡片直接链到那个子会话、不阻塞 lead、完成时宿主把父会话唤醒。TeamMode 再把
|
|
345
|
-
回来的结果压在 token 预算内(`
|
|
345
|
+
回来的结果压在 token 预算内(`taskOffload`)。
|
|
346
346
|
|
|
347
347
|
它依赖一个 OpenCode 的**实验性开关**,由宿主进程启动时读环境变量决定,插件无法
|
|
348
348
|
自己打开——所以安装脚本替你写:
|
|
@@ -501,5 +501,5 @@ https://raw.githubusercontent.com/Te-River/Opencode-TeamMode/main/docs/installat
|
|
|
501
501
|
| 用着 `@latest` 版本还是旧的 | 插件缓存永不失效 | 见"更新"——递归删除缓存目录(两种布局) |
|
|
502
502
|
| 改了版本/重跑安装器后仍是旧版 | 作用域缓存 `packages/@te-river/opencode-team-mode@latest` 嵌在顶层 `packages/*opencode-team-mode*` 通配符扫不到的下一层,旧副本根本没被清掉 | 递归删除该缓存目录(`Get-ChildItem -Recurse -Filter '*opencode-team-mode*'` / `find -type d -name '*opencode-team-mode*' -prune`)后重启——当前安装器已改为递归清理 |
|
|
503
503
|
| 启动报 Node 版本错误 | npm 需要 Node ≥ 18 | 升级 Node |
|
|
504
|
-
| 环境变量命令突然弹确认框 | 这是插件的 R6 保护在正常工作 | 每个操作选"一次",或设 `
|
|
504
|
+
| 环境变量命令突然弹确认框 | 这是插件的 R6 保护在正常工作 | 每个操作选"一次",或设 `envProtect: "off"`(不推荐) |
|
|
505
505
|
| 用户自定义的 `team`/`architect` 等 agent 被遮蔽 | 插件注入默认值;你的定义永远优先 | 插件从不覆盖用户 agent——检查你的 agent 名字有没有拼错 |
|
package/docs/v2-port-notes.md
CHANGED
|
@@ -544,3 +544,34 @@ CALLED" note — a future layer would otherwise have to re-probe a seam the 2.0.
|
|
|
544
544
|
settled. Context Pruning (`TM_PRUNE`, default 70%) is a different mechanism: it rewrites settled
|
|
545
545
|
message BODIES into pointers inside an outgoing request and never asks the host to summarize
|
|
546
546
|
anything. `[R]`
|
|
547
|
+
|
|
548
|
+
### 2026-10-07 — the agent ID is the FILE NAME, and a case-only difference is not cosmetic `[L][R]`
|
|
549
|
+
|
|
550
|
+
**Measured on 2.0.24 / 2026-10-07 (this machine).** The global config directory held
|
|
551
|
+
`agents/team.md` — lowercase, left over from the pre-1.7 naming — while the generator had been
|
|
552
|
+
"writing" `agents/Team.md` on every install since. The host was asked directly:
|
|
553
|
+
|
|
554
|
+
- `opencode-cli run --standalone --agent Team --model nope/nope "x"` → `Agent not found: "Team"`
|
|
555
|
+
- `opencode-cli run --standalone --agent team --model nope/nope "x"` → resolves (fails only on
|
|
556
|
+
the fake model: `Model unavailable: nope/nope`)
|
|
557
|
+
- `opencode-cli api get /api/agent?directory=…` → `{"id":"team","name":"team",…}` for the lead
|
|
558
|
+
|
|
559
|
+
So the on-disk spelling IS the registered id, and the installer's own
|
|
560
|
+
`default_agent: "Team"` named an agent that did not exist. Per the agents doc the fallback for
|
|
561
|
+
an unresolvable default is `build` — silent, which is exactly the failure the installer's
|
|
562
|
+
"write `default_agent` LAST, and read it back" rule exists to prevent; the read-back cannot see
|
|
563
|
+
it, because the key IS on disk and the ID is what is wrong. `[L]`
|
|
564
|
+
|
|
565
|
+
**Why every install failed to fix it.** `writeFileSync(<dir>/Team.md)` on a case-insensitive,
|
|
566
|
+
case-PRESERVING filesystem opens the existing `team.md` and leaves that name alone — and the
|
|
567
|
+
reclaim pass deliberately refused to delete the case-only entry (deleting it would delete the
|
|
568
|
+
file just written: the #47/#48 guard). Nothing in the pipeline could ever change the spelling.
|
|
569
|
+
The fix is an explicit two-step rename (`team.md` → temp → `Team.md`; a case-only `rename` on
|
|
570
|
+
win32 is itself a no-op), performed ONLY on an entry the identity verdict has already PROVEN is
|
|
571
|
+
the same file as a target, with a restore path if the second step throws. `[R]`
|
|
572
|
+
|
|
573
|
+
**The lesson, stated in the general form.** "Is the tool loaded" and "is the file on disk" were
|
|
574
|
+
both yes here — the two checks the installer prints. What no check covered was *which id the
|
|
575
|
+
host derives from it*, and that is the difference between Team being the default and a silent
|
|
576
|
+
`build`. A verifiable claim has to name the artefact the host actually keys on (the FILE NAME),
|
|
577
|
+
not the one we wrote (the path we computed). `[R]`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@te-river/opencode-team-mode",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.3",
|
|
4
4
|
"description": "Team collaboration mode plugin for OpenCode Desktop — injects specialized agents (architect, reviewer, implementer, tester, researcher) and team workflow commands into your OpenCode workspace.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
* Refuses to touch a file it did not generate unless --force is given.
|
|
30
30
|
*/
|
|
31
31
|
|
|
32
|
-
import { mkdirSync, readFileSync, writeFileSync, existsSync, readdirSync, realpathSync, statSync } from "node:fs"
|
|
32
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync, readdirSync, realpathSync, renameSync, statSync } from "node:fs"
|
|
33
33
|
import { join } from "node:path"
|
|
34
34
|
import { homedir } from "node:os"
|
|
35
35
|
import { pathToFileURL } from "node:url"
|
|
@@ -366,9 +366,45 @@ function reclaimStale(dir, keepNames, label) {
|
|
|
366
366
|
}
|
|
367
367
|
}
|
|
368
368
|
if (sameAs) {
|
|
369
|
-
// Same file as a target, stored under a
|
|
370
|
-
// stale
|
|
371
|
-
|
|
369
|
+
// Same file as a target, stored under a DIFFERENT case. Nothing here is
|
|
370
|
+
// stale and deleting it would delete the file we just wrote — but leaving
|
|
371
|
+
// the old spelling is a real defect, not a cosmetic one: the HOST TAKES AN
|
|
372
|
+
// AGENT'S ID FROM THE FILE NAME. A machine that still carries the
|
|
373
|
+
// pre-1.7 `team.md` registers the lead as `team`, so the installer's own
|
|
374
|
+
// `default_agent: "Team"` resolves to nothing and the host falls back to
|
|
375
|
+
// `build` — silently, because that fallback is documented as the normal
|
|
376
|
+
// move for a default that names an agent it cannot find. Measured on
|
|
377
|
+
// 2.0.24 / 2026-10-07 on this very machine: `--agent Team` answered
|
|
378
|
+
// `Agent not found: "Team"`, `--agent team` resolved, and
|
|
379
|
+
// `GET /api/agent` listed `{"id":"team","name":"team"}`.
|
|
380
|
+
//
|
|
381
|
+
// Windows and APFS keep the EXISTING spelling when a write opens that file
|
|
382
|
+
// through the other one, which is how the stale name survives every
|
|
383
|
+
// install, so the rename has to be explicit — and on win32 a case-only
|
|
384
|
+
// rename is itself a no-op, hence the temporary name in the middle.
|
|
385
|
+
if (printOnly) {
|
|
386
|
+
caseNotes.push(`保留 ${file} — 与目标 ${sameAs} 是同一个文件(--print,未修正大小写)`)
|
|
387
|
+
continue
|
|
388
|
+
}
|
|
389
|
+
const target = join(dir, sameAs)
|
|
390
|
+
const tmp = join(dir, `.${entry}.casefix-${process.pid}`)
|
|
391
|
+
try {
|
|
392
|
+
renameSync(file, tmp)
|
|
393
|
+
renameSync(tmp, target)
|
|
394
|
+
caseNotes.push(`已修正大小写 ${entry} → ${sameAs} — 与目标是同一个文件;宿主按文件名取 agent id,不改就一直挂在旧 id 上`)
|
|
395
|
+
} catch (err) {
|
|
396
|
+
// Never leave a half-rename behind: if the second step failed, put the
|
|
397
|
+
// file back under the name we found it (the stale spelling is survivable;
|
|
398
|
+
// a file stranded under the temporary name is not).
|
|
399
|
+
try {
|
|
400
|
+
if (existsSync(tmp) && !existsSync(target)) renameSync(tmp, target)
|
|
401
|
+
} catch {
|
|
402
|
+
/* it stays under the temporary name, and the note below says so */
|
|
403
|
+
}
|
|
404
|
+
caseNotes.push(
|
|
405
|
+
`大小写未修正 ${entry}(目标 ${sameAs}):${String((err && err.message) || err).replace(/\s+/g, " ").slice(0, 80)}`,
|
|
406
|
+
)
|
|
407
|
+
}
|
|
372
408
|
continue
|
|
373
409
|
}
|
|
374
410
|
if (undecidable) {
|
package/dist/approval-gate.d.ts
DELETED
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unified approval gate — Layer 2 (timeout auto-reject) + Layer 3 (SDK
|
|
3
|
-
* fallback) of the R6 + R2 approval flow.
|
|
4
|
-
*
|
|
5
|
-
* The host only shows its official confirmation dialog for command patterns
|
|
6
|
-
* declared `ask` in an agent's permission config (Layer 1, envprotect.ts).
|
|
7
|
-
* When a dialog opens it SUSPENDS the tool call and waits for a human; on the
|
|
8
|
-
* OpenCode DESKTOP that human is present, but a request can also be left
|
|
9
|
-
* unanswered. This module owns ONE timer that watches the host permission
|
|
10
|
-
* events and closes abandoned requests on the safe side.
|
|
11
|
-
*
|
|
12
|
-
* REAL host contract (1.18.29, probed live via SSE + the plugin event hook —
|
|
13
|
-
* see gate-test report-p5):
|
|
14
|
-
* permission.asked → properties = { id, sessionID, permission: "bash",
|
|
15
|
-
* patterns: [<concrete command segment>, …], metadata:
|
|
16
|
-
* { command }, always: [<host's "always" generalization
|
|
17
|
-
* proposal>], tool: { messageID, callID } } — the props
|
|
18
|
-
* carry NO `type` field, and the tool name sits in
|
|
19
|
-
* `permission`. A props matching one of OUR injected
|
|
20
|
-
* bash ask sets means the dialog is ours: audit "ask",
|
|
21
|
-
* arm the timeout, and — for the R6 env face — mark
|
|
22
|
-
* the session deferrable (canDefer, see below).
|
|
23
|
-
* permission.replied → properties = { sessionID, requestID, reply } on the
|
|
24
|
-
* wire, but the plugin-side observer has also seen it
|
|
25
|
-
* with ONLY { sessionID }. The gate therefore cancels
|
|
26
|
-
* EVERY pending timer of that session on a reply: an
|
|
27
|
-
* already-approved command left on a live timer would
|
|
28
|
-
* get a second reject after the timeout, the SDK call
|
|
29
|
-
* would hit a dead id (4xx) and flip the gate
|
|
30
|
-
* permanently degraded.
|
|
31
|
-
*
|
|
32
|
-
* Older/typed spellings (`permission.updated`, `pattern`, `permissionID`,
|
|
33
|
-
* `response`) keep being accepted — the shipped SDK d.ts still names the
|
|
34
|
-
* events that way, and some builds omit the reply id entirely (a short
|
|
35
|
-
* ghost-suppression window per session then absorbs late/duplicate `asked`
|
|
36
|
-
* replays instead of re-arming a ghost timer).
|
|
37
|
-
*
|
|
38
|
-
* Hard rules (HUMAN-approved):
|
|
39
|
-
* - the plugin NEVER self-allows: `reply` is fixed to "reject". The only
|
|
40
|
-
* way a gated command runs is a human approving the dialog;
|
|
41
|
-
* - on `TM_ENV_PROTECT=off` the gate is not armed (the caller decides);
|
|
42
|
-
* - deferral is SESSION-SCOPED (canDefer): only sessions registered as
|
|
43
|
-
* carrying our injected ask set may have env reads passed through —
|
|
44
|
-
* registration comes from an exec-role user prompt (the live-proven
|
|
45
|
-
* `message.updated` UserMessage{role,agent,sessionID} signal, plus the
|
|
46
|
-
* documented `chat.message` hook) or from an R6-env-classified asked
|
|
47
|
-
* event. Stock build/plan sessions never register, so the global R6
|
|
48
|
-
* hook keeps hard-throwing there — closing the R6 bypass the global
|
|
49
|
-
* `isArmed()` deferral allowed;
|
|
50
|
-
* - if the SDK reply FAILS (network/host — including the SDK's
|
|
51
|
-
* `throwOnError:false` envelope resolving with `{ error }`), we cannot
|
|
52
|
-
* close the dialog, so the gate flips itself permanently "degraded" —
|
|
53
|
-
* the R6 hook stops deferring env reads and hard-throws them again
|
|
54
|
-
* (fail-closed), and the event is audited "degraded".
|
|
55
|
-
*
|
|
56
|
-
* SDK surface (probed live): the host client may carry NO `permission`
|
|
57
|
-
* namespace and NO list/create endpoint; the reply path is the top-level
|
|
58
|
-
* `postSessionIdPermissionsPermissionId({ path: { id: sessionID,
|
|
59
|
-
* permissionID }, body: { response } })`. `client.permission.reply` /
|
|
60
|
-
* `client.permission.list` are still tried first so a richer host build is
|
|
61
|
-
* supported, but when they are absent the poll fallback is a no-op (events
|
|
62
|
-
* are the sole pending signal).
|
|
63
|
-
*/
|
|
64
|
-
import type { PermissionEvent } from "./types.js";
|
|
65
|
-
/** Default timeout, in minutes, before an unanswered dialog is auto-rejected.
|
|
66
|
-
* 1 min (user directive) — an unanswered dialog no longer hangs for 10.
|
|
67
|
-
* A short timer is safe because a reject that lands on an already-closed
|
|
68
|
-
* request is benign; see `MIN_ASK_TIMEOUT_MIN` for the full rationale. */
|
|
69
|
-
export declare const DEFAULT_ASK_TIMEOUT_MIN = 1;
|
|
70
|
-
/**
|
|
71
|
-
* Hard floor (minutes) for the auto-reject timeout. The HISTORIC 3-min
|
|
72
|
-
* floor existed to dodge the measured ~120s `permission.replied` bus lag:
|
|
73
|
-
* a shorter timer would fire before the cancel arrives and auto-reject a
|
|
74
|
-
* just-approved request. That lag, though, is the HOST DELIVERING the
|
|
75
|
-
* replied event to the plugin over the bus — NOT the host resolving the
|
|
76
|
-
* user's click: an approval takes effect instantly host-side, so the
|
|
77
|
-
* plugin's late reject simply targets an already-closed request id → 404.
|
|
78
|
-
* T2's `classifyReplyFailure` maps that outcome to the benign
|
|
79
|
-
* `already-closed` verdict (it never flips the gate to `degraded`, and the
|
|
80
|
-
* approved command runs either way); `onReplied` records a reply landing
|
|
81
|
-
* after the auto-reject as `late-<verdict>` for observability. With that
|
|
82
|
-
* classification in place a short timeout no longer disables the gate, so
|
|
83
|
-
* the floor drops to 1 minute (user directive). Trade-off: an UNANSWERED
|
|
84
|
-
* dialog auto-rejects after 1 min — a user who stepped away re-triggers
|
|
85
|
-
* the command. In-range values below the floor still clamp UP to it.
|
|
86
|
-
*
|
|
87
|
-
* This is the FALLBACK default of the floor. The live floor comes from the
|
|
88
|
-
* P0 config knob `TM_ASK_TIMEOUT_FLOOR_MIN`
|
|
89
|
-
* (`resolveTmConfig().askTimeoutFloorMin`, itself defaulted to 1).
|
|
90
|
-
*/
|
|
91
|
-
export declare const MIN_ASK_TIMEOUT_MIN = 1;
|
|
92
|
-
/** The verdict vocabulary recorded in the audit trail (privacy: no command
|
|
93
|
-
* text, path, variable name or value is ever logged). `already-closed` /
|
|
94
|
-
* `rejected-shape-bug` split the old blanket `degraded` on a failed reply by
|
|
95
|
-
* `classifyReplyFailure`; a late reply past the auto-reject is recorded as a
|
|
96
|
-
* dynamic `late-<verdict>` string (observability only — never re-runs or
|
|
97
|
-
* revives the command). */
|
|
98
|
-
export type ApprovalVerdict = "ask" | "allowed-once" | "allowed-always" | "rejected" | "timeout-rejected" | "already-closed" | "rejected-shape-bug" | "degraded";
|
|
99
|
-
/**
|
|
100
|
-
* Resolve `TM_ASK_TIMEOUT_MIN`. Unset / blank / non-numeric / <1 / >1440
|
|
101
|
-
* (24h) fall back to the 1-minute default — a mistyped value can never
|
|
102
|
-
* disable the timeout or make it absurdly long. Valid values below the
|
|
103
|
-
* floor clamp UP to it — the floor is the P0 config knob
|
|
104
|
-
* `TM_ASK_TIMEOUT_FLOOR_MIN` (`resolveTmConfig().askTimeoutFloorMin`,
|
|
105
|
-
* default 1). The historic ~120s bus-lag reason for a 3-min floor is
|
|
106
|
-
* obsolete: a late reject on an already-closed id is benign
|
|
107
|
-
* `already-closed`, not a degraded flip (see `MIN_ASK_TIMEOUT_MIN`).
|
|
108
|
-
*/
|
|
109
|
-
export declare function resolveAskTimeoutMs(env?: Record<string, string | undefined>): number;
|
|
110
|
-
type TimerHandle = unknown;
|
|
111
|
-
export interface GateTimers {
|
|
112
|
-
setTimeoutFn: (cb: () => void, ms: number) => TimerHandle;
|
|
113
|
-
clearTimeoutFn: (handle: TimerHandle) => void;
|
|
114
|
-
}
|
|
115
|
-
export interface ApprovalGateDeps {
|
|
116
|
-
client: unknown;
|
|
117
|
-
timeoutMs: number;
|
|
118
|
-
/** Injectable timers (default: real global timers) for deterministic tests. */
|
|
119
|
-
timers?: GateTimers;
|
|
120
|
-
/** Injectable clock (default: Date.now) for deterministic window tests. */
|
|
121
|
-
now?: () => number;
|
|
122
|
-
/** Best-effort attention hook — fired on EVERY permission.asked (all
|
|
123
|
-
* dialogs: bash R2/R6 asks AND the tm_* ctx.ask web dialogs) so a user
|
|
124
|
-
* not staring at the screen learns a confirmation is waiting. The caller
|
|
125
|
-
* wires this to the host's `tui.showToast`. Never throws into the gate. */
|
|
126
|
-
notify?: (message: string) => void;
|
|
127
|
-
}
|
|
128
|
-
export interface ApprovalGate {
|
|
129
|
-
/** Handle one host event (permission.asked/updated / permission.replied). */
|
|
130
|
-
handleEvent(event: PermissionEvent): void;
|
|
131
|
-
/** Start the poll fallback (no-op when the host has no list endpoint). */
|
|
132
|
-
start(): void;
|
|
133
|
-
/** Stop the poll + every pending timer. */
|
|
134
|
-
dispose(): void;
|
|
135
|
-
/** Global gate health: armed, not degraded, and the client can reply. */
|
|
136
|
-
isArmed(): boolean;
|
|
137
|
-
/**
|
|
138
|
-
* The R6 hook's deferral query: TRUE only when this exact session is
|
|
139
|
-
* registered as carrying our injected ask set AND the gate is healthy.
|
|
140
|
-
* Replaces the old global `isArmed()` deferral (which let stock build/plan
|
|
141
|
-
* sessions pass env reads silently with no dialog in front of them).
|
|
142
|
-
*/
|
|
143
|
-
canDefer(sessionID?: string): boolean;
|
|
144
|
-
/** True once a session registered (exec-role prompt seen on
|
|
145
|
-
* message.updated / chat.message by index.ts, or an R6-env-classified
|
|
146
|
-
* asked event). Introspection for tests/diagnostics. */
|
|
147
|
-
hasLiveAsk(sessionID?: string): boolean;
|
|
148
|
-
/** Mark a session as carrying the injected ask set (index.ts calls this
|
|
149
|
-
* when a user prompt routes to an agent we injected the escalated bash
|
|
150
|
-
* ask object into — proven live: `message.updated` UserMessage carries
|
|
151
|
-
* { sessionID, role:"user", agent } and fires BEFORE the session's first
|
|
152
|
-
* tool call, while permission.asked fires AFTER tool.execute.before). */
|
|
153
|
-
registerExecSession(sessionID?: string): void;
|
|
154
|
-
/**
|
|
155
|
-
* Drop a session's exec-role registration (index.ts calls this when a user
|
|
156
|
-
* prompt routes to an agent that does NOT carry our injected ask set).
|
|
157
|
-
* The verified host passes {tool, sessionID, callID} with NO agent to
|
|
158
|
-
* tool.execute.before (desktop binary: `plugin.trigger("tool.execute.before",
|
|
159
|
-
* { tool, sessionID, callID }, { args })`), so the per-turn agent signal
|
|
160
|
-
* can only come from message.updated/chat.message. Without revocation a
|
|
161
|
-
* session that once ran a team prompt stayed deferrable forever — even
|
|
162
|
-
* after the user switched the picker to a stock agent whose dialogs would
|
|
163
|
-
* never fire, silently passing env reads (canDefer true + no dialog).
|
|
164
|
-
* A later env-classified permission.asked re-registers on real dialog
|
|
165
|
-
* evidence, and the next exec-role prompt re-registers again — both paths
|
|
166
|
-
* re-arm from fresh evidence, so revocation only ever closes a stale
|
|
167
|
-
* window (fail-closed direction).
|
|
168
|
-
*/
|
|
169
|
-
revokeExecSession(sessionID?: string): void;
|
|
170
|
-
/** True after the user picked "always" on an env-related ask in this session
|
|
171
|
-
* — subsequent env reads pass silently (the approval event itself is audited). */
|
|
172
|
-
isEnvApproved(sessionID?: string): boolean;
|
|
173
|
-
/** Number of pending requests currently being timed (tests / introspection). */
|
|
174
|
-
pendingSize(): number;
|
|
175
|
-
}
|
|
176
|
-
/** How a FAILED permission-reply call should be treated by the gate. */
|
|
177
|
-
export type ReplyFailureClass = "benign-closed" | "param-shape" | "transport";
|
|
178
|
-
/**
|
|
179
|
-
* Classify a reply-call failure so the gate no longer flips permanently
|
|
180
|
-
* `degraded` on a harmless close. `unwrapSdkEnvelope` has already hoisted the
|
|
181
|
-
* HTTP status onto the thrown envelope error; a genuine `Promise.reject`
|
|
182
|
-
* arrives as a plain Error with no status.
|
|
183
|
-
* - `benign-closed`: 404, or a NotFound/PermissionNotFound name/_tag — the
|
|
184
|
-
* dialog was ALREADY closed on the host (the D4 late-reply race): our
|
|
185
|
-
* reject was a no-op, so the popup path stays trustworthy.
|
|
186
|
-
* - `param-shape`: 400, or a BadRequest/InvalidRequest name/_tag — our
|
|
187
|
-
* reply body/params are wrong for this host: a real integration bug.
|
|
188
|
-
* - `transport`: everything else — a genuine Error, a 5xx, or an empty /
|
|
189
|
-
* status-less body — we could not reach or close the dialog.
|
|
190
|
-
* Reads only the error CLASS + status number (privacy: never message text).
|
|
191
|
-
*/
|
|
192
|
-
export declare function classifyReplyFailure(err: unknown): ReplyFailureClass;
|
|
193
|
-
/** True when the client exposes a permission-reply path at all. index.ts
|
|
194
|
-
* arms the gate ONLY for such a client — fail-closed otherwise (no armed
|
|
195
|
-
* auto-reject ⇒ the R6 hook keeps hard-throwing every env read). */
|
|
196
|
-
export declare function hasPermissionReplyCapability(client: unknown): boolean;
|
|
197
|
-
/**
|
|
198
|
-
* Create the approval gate. It is intentionally pure about timing +
|
|
199
|
-
* replying + session registration; the caller (index.ts) decides whether to
|
|
200
|
-
* arm it (mode != off AND the client is reply-capable), feeds events in, and
|
|
201
|
-
* wires `canDefer(sessionID)` into the R6 hook.
|
|
202
|
-
*/
|
|
203
|
-
export declare function createApprovalGate(deps: ApprovalGateDeps): ApprovalGate;
|
|
204
|
-
export {};
|
|
205
|
-
//# sourceMappingURL=approval-gate.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"approval-gate.d.ts","sourceRoot":"","sources":["../src/approval-gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8DG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAIjD;;;2EAG2E;AAC3E,eAAO,MAAM,uBAAuB,IAAI,CAAA;AAExC;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,mBAAmB,IAAI,CAAA;AAkBpC;;;;;4BAK4B;AAC5B,MAAM,MAAM,eAAe,GACvB,KAAK,GACL,cAAc,GACd,gBAAgB,GAChB,UAAU,GACV,kBAAkB,GAClB,gBAAgB,GAChB,oBAAoB,GACpB,UAAU,CAAA;AAEd;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAe,GAAG,MAAM,CAWjG;AAED,KAAK,WAAW,GAAG,OAAO,CAAA;AAE1B,MAAM,WAAW,UAAU;IACzB,YAAY,EAAE,CAAC,EAAE,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,KAAK,WAAW,CAAA;IACzD,cAAc,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,CAAA;CAC9C;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,OAAO,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;IACjB,+EAA+E;IAC/E,MAAM,CAAC,EAAE,UAAU,CAAA;IACnB,2EAA2E;IAC3E,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;IAClB;;;iFAG6E;IAC7E,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CACnC;AAED,MAAM,WAAW,YAAY;IAC3B,6EAA6E;IAC7E,WAAW,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,CAAA;IACzC,0EAA0E;IAC1E,KAAK,IAAI,IAAI,CAAA;IACb,2CAA2C;IAC3C,OAAO,IAAI,IAAI,CAAA;IACf,yEAAyE;IACzE,OAAO,IAAI,OAAO,CAAA;IAClB;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;IACrC;;8DAE0D;IAC1D,UAAU,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;IACvC;;;;8EAI0E;IAC1E,mBAAmB,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7C;;;;;;;;;;;;;;OAcG;IACH,iBAAiB,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3C;uFACmF;IACnF,aAAa,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;IAC1C,gFAAgF;IAChF,WAAW,IAAI,MAAM,CAAA;CACtB;AAuED,wEAAwE;AACxE,MAAM,MAAM,iBAAiB,GAAG,eAAe,GAAG,aAAa,GAAG,WAAW,CAAA;AAE7E;;;;;;;;;;;;;GAaG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,OAAO,GAAG,iBAAiB,CAgBpE;AA8DD;;qEAEqE;AACrE,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO,CAErE;AAyBD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,gBAAgB,GAAG,YAAY,CAqUvE"}
|