@te-river/opencode-team-mode 1.6.0 → 1.6.2

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.
Files changed (150) hide show
  1. package/README.md +80 -15
  2. package/README.zh-CN.md +67 -18
  3. package/dist/host/note.d.ts +7 -0
  4. package/dist/host/note.d.ts.map +1 -0
  5. package/dist/host/note.js +31 -0
  6. package/dist/host/note.js.map +1 -0
  7. package/dist/host/v1.d.ts +14 -0
  8. package/dist/host/v1.d.ts.map +1 -0
  9. package/dist/host/v1.js +340 -0
  10. package/dist/host/v1.js.map +1 -0
  11. package/dist/host/v2-browser-gate.d.ts +45 -0
  12. package/dist/host/v2-browser-gate.d.ts.map +1 -0
  13. package/dist/host/v2-browser-gate.js +319 -0
  14. package/dist/host/v2-browser-gate.js.map +1 -0
  15. package/dist/host/v2-capabilities.d.ts +78 -0
  16. package/dist/host/v2-capabilities.d.ts.map +1 -0
  17. package/dist/host/v2-capabilities.js +173 -0
  18. package/dist/host/v2-capabilities.js.map +1 -0
  19. package/dist/host/v2-client.d.ts +53 -0
  20. package/dist/host/v2-client.d.ts.map +1 -0
  21. package/dist/host/v2-client.js +193 -0
  22. package/dist/host/v2-client.js.map +1 -0
  23. package/dist/host/v2-events.d.ts +56 -0
  24. package/dist/host/v2-events.d.ts.map +1 -0
  25. package/dist/host/v2-events.js +158 -0
  26. package/dist/host/v2-events.js.map +1 -0
  27. package/dist/host/v2-guard.d.ts +101 -0
  28. package/dist/host/v2-guard.d.ts.map +1 -0
  29. package/dist/host/v2-guard.js +199 -0
  30. package/dist/host/v2-guard.js.map +1 -0
  31. package/dist/host/v2-offload.d.ts +148 -0
  32. package/dist/host/v2-offload.d.ts.map +1 -0
  33. package/dist/host/v2-offload.js +288 -0
  34. package/dist/host/v2-offload.js.map +1 -0
  35. package/dist/host/v2-permissions.d.ts +123 -0
  36. package/dist/host/v2-permissions.d.ts.map +1 -0
  37. package/dist/host/v2-permissions.js +190 -0
  38. package/dist/host/v2-permissions.js.map +1 -0
  39. package/dist/host/v2-probe.d.ts +85 -0
  40. package/dist/host/v2-probe.d.ts.map +1 -0
  41. package/dist/host/v2-probe.js +268 -0
  42. package/dist/host/v2-probe.js.map +1 -0
  43. package/dist/host/v2-scope.d.ts +51 -0
  44. package/dist/host/v2-scope.d.ts.map +1 -0
  45. package/dist/host/v2-scope.js +58 -0
  46. package/dist/host/v2-scope.js.map +1 -0
  47. package/dist/host/v2-session-client.d.ts +105 -0
  48. package/dist/host/v2-session-client.d.ts.map +1 -0
  49. package/dist/host/v2-session-client.js +390 -0
  50. package/dist/host/v2-session-client.js.map +1 -0
  51. package/dist/host/v2-session.d.ts +85 -0
  52. package/dist/host/v2-session.d.ts.map +1 -0
  53. package/dist/host/v2-session.js +171 -0
  54. package/dist/host/v2-session.js.map +1 -0
  55. package/dist/host/v2-subagent.d.ts +157 -0
  56. package/dist/host/v2-subagent.d.ts.map +1 -0
  57. package/dist/host/v2-subagent.js +286 -0
  58. package/dist/host/v2-subagent.js.map +1 -0
  59. package/dist/host/v2-tool.d.ts +67 -0
  60. package/dist/host/v2-tool.d.ts.map +1 -0
  61. package/dist/host/v2-tool.js +209 -0
  62. package/dist/host/v2-tool.js.map +1 -0
  63. package/dist/host/v2-transcript.d.ts +73 -0
  64. package/dist/host/v2-transcript.d.ts.map +1 -0
  65. package/dist/host/v2-transcript.js +151 -0
  66. package/dist/host/v2-transcript.js.map +1 -0
  67. package/dist/host/v2-types.d.ts +229 -0
  68. package/dist/host/v2-types.d.ts.map +1 -0
  69. package/dist/host/v2-types.js +17 -0
  70. package/dist/host/v2-types.js.map +1 -0
  71. package/dist/host/v2.d.ts +42 -0
  72. package/dist/host/v2.d.ts.map +1 -0
  73. package/dist/host/v2.js +853 -0
  74. package/dist/host/v2.js.map +1 -0
  75. package/dist/index.d.ts +23 -17
  76. package/dist/index.d.ts.map +1 -1
  77. package/dist/index.js +29 -366
  78. package/dist/index.js.map +1 -1
  79. package/dist/prompts/lead.d.ts +1 -1
  80. package/dist/prompts/lead.d.ts.map +1 -1
  81. package/dist/prompts/lead.js +456 -426
  82. package/dist/prompts/lead.js.map +1 -1
  83. package/dist/prompts/shared.d.ts +2 -2
  84. package/dist/prompts/shared.d.ts.map +1 -1
  85. package/dist/prompts/shared.js +38 -11
  86. package/dist/prompts/shared.js.map +1 -1
  87. package/dist/prompts/specialists.d.ts +2 -2
  88. package/dist/prompts/specialists.d.ts.map +1 -1
  89. package/dist/prompts/specialists.js +7 -6
  90. package/dist/prompts/specialists.js.map +1 -1
  91. package/dist/task-offload.d.ts +16 -0
  92. package/dist/task-offload.d.ts.map +1 -1
  93. package/dist/task-offload.js +51 -0
  94. package/dist/task-offload.js.map +1 -1
  95. package/dist/tm/browser.d.ts.map +1 -1
  96. package/dist/tm/browser.js +41 -10
  97. package/dist/tm/browser.js.map +1 -1
  98. package/dist/tm/dispatch.d.ts +86 -1
  99. package/dist/tm/dispatch.d.ts.map +1 -1
  100. package/dist/tm/dispatch.js +360 -39
  101. package/dist/tm/dispatch.js.map +1 -1
  102. package/dist/tm/dupe-guard.d.ts +16 -3
  103. package/dist/tm/dupe-guard.d.ts.map +1 -1
  104. package/dist/tm/dupe-guard.js +27 -7
  105. package/dist/tm/dupe-guard.js.map +1 -1
  106. package/dist/tm/index.d.ts +40 -1
  107. package/dist/tm/index.d.ts.map +1 -1
  108. package/dist/tm/index.js +40 -11
  109. package/dist/tm/index.js.map +1 -1
  110. package/dist/tm/ledger.d.ts +114 -0
  111. package/dist/tm/ledger.d.ts.map +1 -0
  112. package/dist/tm/ledger.js +308 -0
  113. package/dist/tm/ledger.js.map +1 -0
  114. package/dist/tm/perm-ask.d.ts +1 -3
  115. package/dist/tm/perm-ask.d.ts.map +1 -1
  116. package/dist/tm/perm-ask.js +10 -1
  117. package/dist/tm/perm-ask.js.map +1 -1
  118. package/dist/tm/preview.d.ts +55 -0
  119. package/dist/tm/preview.d.ts.map +1 -1
  120. package/dist/tm/preview.js +225 -0
  121. package/dist/tm/preview.js.map +1 -1
  122. package/dist/tm/search.d.ts.map +1 -1
  123. package/dist/tm/search.js +40 -10
  124. package/dist/tm/search.js.map +1 -1
  125. package/dist/tm/serp-loop.d.ts +45 -0
  126. package/dist/tm/serp-loop.d.ts.map +1 -0
  127. package/dist/tm/serp-loop.js +80 -0
  128. package/dist/tm/serp-loop.js.map +1 -0
  129. package/dist/tm/stats.d.ts +29 -0
  130. package/dist/tm/stats.d.ts.map +1 -1
  131. package/dist/tm/stats.js +278 -4
  132. package/dist/tm/stats.js.map +1 -1
  133. package/dist/tm/webfetch.d.ts +4 -0
  134. package/dist/tm/webfetch.d.ts.map +1 -1
  135. package/dist/tm/webfetch.js +75 -8
  136. package/dist/tm/webfetch.js.map +1 -1
  137. package/docs/installation-v2.md +445 -0
  138. package/docs/installation.md +12 -0
  139. package/docs/research/agent-data-exchange.md +117 -0
  140. package/docs/research/browser-pane.md +89 -0
  141. package/docs/research/host-http-api.md +132 -0
  142. package/docs/research/host-subagent-injection.md +187 -0
  143. package/docs/research/plugin-loader-contract.md +261 -0
  144. package/docs/research/v2-builtin-tools.md +447 -0
  145. package/docs/v2-port-notes.md +413 -0
  146. package/index.js +7 -0
  147. package/package.json +64 -61
  148. package/scripts/gen-v2-config.mjs +304 -0
  149. package/scripts/lib/config-surgery.cjs +239 -0
  150. package/scripts/lib/fake-ctx.mjs +169 -0
@@ -0,0 +1,199 @@
1
+ /**
2
+ * The v2 permission-guard layer (#7).
3
+ *
4
+ * v1 enforced two things in code that v2 can enforce one level lower — at the
5
+ * host's own `permission.hook("evaluate")`, whose `effect` is mutable and whose
6
+ * `resources` carry the concrete command line or URL (live-probed: `action`
7
+ * arrives as shell / read / edit / external_directory with the real payload).
8
+ *
9
+ * 1. THE EGRESS RED LINE ON THE HOST'S OWN WEB TOOL. v1 never had to think
10
+ * about this because native `webfetch` was denied in the whitelist; on v2 a
11
+ * Build-class agent has it, and it has no notion of `169.254.169.254` — the
12
+ * cloud metadata endpoint whose response is temporary credentials. The
13
+ * domain allowlist answers "is this host on the list"; only `checkWebUrl`
14
+ * asks the address question, and that question has to be asked of the native
15
+ * path too or the port becomes a way around it.
16
+ * 2. R6's per-command precision. v2's closest thing to v1's pattern-object
17
+ * escalation is currently "ask on every shell command", which is honest but
18
+ * coarse. The classifier can restore the per-command shape — but only if
19
+ * the host actually calls `evaluate` for shell, which has not been proven on
20
+ * a live 2.0.16 yet. So the strictening hook is installed either way and
21
+ * COUNTS what it sees, while the config-level coarse ask stays in force
22
+ * until `TM_R6_FINE_ASK=on` says otherwise. A guard that fails open because
23
+ * we trusted an unproven hook is the opposite of this product.
24
+ *
25
+ * The hook only ever makes a decision STRICTER (allow -> ask, anything -> deny on
26
+ * a red line). It never loosens what the host or the user's own rules chose.
27
+ */
28
+ import { R2_DANGER_BASH_ASK_PATTERNS, R6_ENV_BASH_ASK_PATTERNS, commandMatchesAnyAskPattern, isEnvFilePath, } from "../envprotect.js";
29
+ import { classifyHost } from "../tm/egress.js";
30
+ const rank = { allow: 0, ask: 1, deny: 2 };
31
+ /** A resource may be a full URL or a bare host; both have to be answered, since
32
+ * the host's own tool is free to hand us either shape. */
33
+ function hostOf(text) {
34
+ const s = text.trim();
35
+ if (!s)
36
+ return null;
37
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(s)) {
38
+ try {
39
+ return new URL(s).hostname;
40
+ }
41
+ catch {
42
+ return null;
43
+ }
44
+ }
45
+ // A path-shaped resource is not a host.
46
+ if (s.startsWith("/") || s.includes(" "))
47
+ return null;
48
+ return s;
49
+ }
50
+ /** The address question, asked of the host's native web path. */
51
+ export function webGuard(resources) {
52
+ for (const raw of resources) {
53
+ const text = String(raw ?? "");
54
+ if (isEnvFilePath(text)) {
55
+ return { effect: "deny", why: "env-file URL", message: "R6 红线:这个 URL 指向环境文件(.env / shell rc 家族),不授权、不弹窗。" };
56
+ }
57
+ const host = hostOf(text);
58
+ if (!host)
59
+ continue;
60
+ const verdict = classifyHost(host);
61
+ if (verdict.level === "forbidden") {
62
+ return {
63
+ effect: "deny",
64
+ why: `egress:${verdict.via ?? "forbidden"}`,
65
+ message: `${host} 是元数据/链路本地/保留地址(${verdict.via ?? "特殊用途"}),拿它的响应等于把临时凭据读进上下文和轨迹。这条不可授权,配置里的 "*" 也不算授权。`,
66
+ };
67
+ }
68
+ if (verdict.level === "private") {
69
+ return {
70
+ effect: "ask",
71
+ why: `egress-private:${verdict.via ?? "private"}`,
72
+ message: `${host} 是私网/回环地址(${verdict.via ?? "私有"})。v1 在这里要求用户逐次批准;v2 的插件弹不出对话框,所以交给宿主自己的确认——不要重复调用绕过它。`,
73
+ };
74
+ }
75
+ }
76
+ return null;
77
+ }
78
+ /** R6's env face and R2's danger face, per command line. */
79
+ export function shellGuard(command, mode) {
80
+ const text = String(command ?? "").trim();
81
+ if (!text || mode === "off")
82
+ return null;
83
+ if (commandMatchesAnyAskPattern(text, R2_DANGER_BASH_ASK_PATTERNS)) {
84
+ return { effect: "ask", why: "r2-danger", message: "这条命令落在 R2 危险面上(删除/推送/发布/杀进程一类),要人批准一次。" };
85
+ }
86
+ if (commandMatchesAnyAskPattern(text, R6_ENV_BASH_ASK_PATTERNS)) {
87
+ return { effect: "ask", why: "r6-env", message: "这条命令读环境变量(R6),要人批准一次。" };
88
+ }
89
+ return null;
90
+ }
91
+ /**
92
+ * Every sub-agent dispatch runs in the background.
93
+ *
94
+ * The user's standing instruction (2026-09-25). It is not a preference tweak: a
95
+ * foreground `subagent` call blocks the lead for the whole child run, which is
96
+ * the one thing the throughput mandate cannot survive — and on v2 background is
97
+ * a first-class host capability (`subagent {background:true}` returns
98
+ * immediately and the parent is notified when the child finishes), so nothing
99
+ * has to be opted into at the environment level the way v1 needed
100
+ * `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS`.
101
+ *
102
+ * `execute.before` takes a mutable `input` (docs say "inspect or REPLACE"; a
103
+ * live host confirmed the write lands), so this is the honest place to enforce
104
+ * it — a prompt rule thousands of tokens earlier is not the same thing. An
105
+ * input that is not an object is left alone rather than invented.
106
+ */
107
+ export async function applyV2BackgroundForce(ctx, opts = {}) {
108
+ const report = { seen: 0, forced: 0 };
109
+ const tool = ctx.tool;
110
+ if (!tool || typeof tool.hook !== "function")
111
+ return { registrations: [], report };
112
+ const registrations = [
113
+ await tool.hook("execute.before", (event) => {
114
+ if (String(event?.tool ?? "") !== "subagent")
115
+ return;
116
+ // #22: rewriting the input of a call that is not ours is the clearest kind
117
+ // of side effect — a user who chose a synchronous dispatch in build mode
118
+ // would have it taken away by a plugin installed for Team.
119
+ if (opts.scope && opts.scope.count(opts.scope.decide(event)) !== "ours")
120
+ return;
121
+ const input = event.input;
122
+ if (!input || typeof input !== "object" || Array.isArray(input))
123
+ return;
124
+ report.seen++;
125
+ if (input.background === true)
126
+ return;
127
+ input.background = true;
128
+ report.forced++;
129
+ }),
130
+ ];
131
+ return { registrations, report };
132
+ }
133
+ export async function applyV2PermissionGuards(ctx, opts) {
134
+ const report = { seen: 0, byAction: {}, strictened: 0, denied: 0, shellMatched: 0, foreignSkipped: 0 };
135
+ const permission = ctx.permission;
136
+ if (!permission || typeof permission.hook !== "function") {
137
+ return { registrations: [], report, installed: false };
138
+ }
139
+ const registrations = [
140
+ await permission.hook("evaluate", (event) => {
141
+ const action = String(event?.action ?? "");
142
+ const resources = (Array.isArray(event?.resources) ? event.resources : []).map(String);
143
+ report.seen++;
144
+ report.byAction[action] = (report.byAction[action] ?? 0) + 1;
145
+ // #22: the host evaluates permissions for every agent. Tightening a rule a
146
+ // build/plan session is running under is a change to somebody else's mode,
147
+ // so inside Team only — and the count says how much of that floor is
148
+ // therefore NOT ours to enforce (see the note in the boot line).
149
+ if (opts.scope && opts.scope.count(opts.scope.decide(event)) !== "ours") {
150
+ report.foreignSkipped++;
151
+ return;
152
+ }
153
+ const decision = action === "webfetch" || action === "websearch"
154
+ ? webGuard(resources)
155
+ : action === "shell"
156
+ ? shellGuard(resources[0], opts.envProtectMode)
157
+ : null;
158
+ if (!decision)
159
+ return;
160
+ if (action === "shell")
161
+ report.shellMatched++;
162
+ // Only ever stricter. A host that already decided to ask or deny keeps
163
+ // that decision — our classifier is a floor, not an override of the user.
164
+ if (rank[decision.effect] <= rank[event.effect ?? "allow"])
165
+ return;
166
+ event.effect = decision.effect;
167
+ if (decision.message)
168
+ event.message = decision.message;
169
+ if (decision.effect === "deny")
170
+ report.denied++;
171
+ else
172
+ report.strictened++;
173
+ }),
174
+ ];
175
+ return { registrations, report, installed: true };
176
+ }
177
+ /** Does the config still need the coarse `shell -> ask` escalation? Only while
178
+ * the fine path is switched off; with the classifier in charge, escalating every
179
+ * command would mask it.
180
+ * Was the coarse default until a live host proved the seam exists: a
181
+ * `--standalone` run of the v2 personality recorded
182
+ * `{action:"shell", resourceCount:1, hasUrl:false}` reaching `evaluate` for a real
183
+ * `git status --short` (2026-09-25), which is the observation this function was
184
+ * waiting for. So the classifier is in charge by default and the escalation is
185
+ * the fallback again — `TM_R6_FINE_ASK=off` restores "every command asks", which is
186
+ * what a user should reach for if their host build turns out not to fire the hook.
187
+ * An absent hook still forces coarse: no seam, no per-command judgement.
188
+ */
189
+ export function needsCoarseShellAsk(env, guardsInstalled) {
190
+ if (!guardsInstalled)
191
+ return true;
192
+ const v = String(env.TM_R6_FINE_ASK ?? "").trim();
193
+ // Only an EXPLICIT off falls back; an empty or unparseable value keeps the
194
+ // classifier, because "unset" is now the supported configuration.
195
+ if (/^(0|false|no|off)$/i.test(v))
196
+ return true;
197
+ return false;
198
+ }
199
+ //# sourceMappingURL=v2-guard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"v2-guard.js","sourceRoot":"","sources":["../../src/host/v2-guard.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,148 @@
1
+ import type { V2Registration } from "./v2-types.js";
2
+ import type { TeamScope } from "./v2-scope.js";
3
+ import { detectContentType } from "../tm/preview.js";
4
+ /**
5
+ * JIT context governance over the HOST's own tools.
6
+ *
7
+ * The whole product rests on one promise: an oversized tool result never enters
8
+ * the context window — it lands in the run store and the model gets an
9
+ * ≤80-token preview plus a handle. That promise was true only inside our own
10
+ * `tm_*` tools, which is what made `tm_read`/`tm_grep`/`tm_bash` load-bearing
11
+ * rather than merely nicer. The moment a role is allowed the native `read` or
12
+ * `shell` (which is the retirement plan for those three), an un-governed native
13
+ * result would put the promise back to zero for exactly the tools a user would
14
+ * most plausibly enable — and the promise would then depend on which tool the
15
+ * model happened to pick, which is not a guarantee at all.
16
+ *
17
+ * So the governance moves to the seam that does not care what the model chose.
18
+ * `tool.execute.after` hands us the finished result for EVERY tool (measured
19
+ * live: `{action:"shell"}` reaches `permission.evaluate`, and the same call
20
+ * reaches `execute.after` with `resultKeys: content / metadata / output`), and
21
+ * the host documents `result` as mutable. One hook, both worlds.
22
+ *
23
+ * Design constraints this file is written under:
24
+ * - **It only ever shrinks what reaches the context, never what the tool did.**
25
+ * The bytes on disk are the host's; a preview plus a handle replaces the text
26
+ * part. `metadata` and any non-text part (an image attachment) pass through
27
+ * untouched — dropping a screenshot to save tokens would be a bad trade.
28
+ * - **It reuses the same threshold, preview builder, store and HMAC handles as
29
+ * `tm_*`**, because a second implementation would drift, and the drift would
30
+ * show up as "the same 40 KB was free through one tool and offloaded through
31
+ * another".
32
+ * - **A native result is rendered as native-shaped text.** `tm_*` returns our own
33
+ * structured object; overwriting a host tool's text part with `{offloaded:true,
34
+ * ref:…}` would hand the model a shape it has no reason to understand. The
35
+ * sentence below says the same facts in prose the tm_* tools already train it
36
+ * to act on: how much was saved, the handle, how to page it in.
37
+ * - Nothing here may throw into somebody's tool call: a governance failure
38
+ * degrades to the host's verbatim result, and says so in the trajectory.
39
+ */
40
+ /** Tools whose NATIVE output we govern. Deliberately a closed list: the built-in
41
+ * agent/patch/question results have shapes nobody has observed here, and rewriting
42
+ * a shape you guessed at is content destruction, not governance.
43
+ *
44
+ * `execute` is on the list for one specific reason, and it is the reason the
45
+ * native browser can be governed at all: the Team's direct tool surface on v2 is
46
+ * six tools (edit / execute / question / shell / subagent / write), so
47
+ * `tools.browser.*` -- every snapshot, tab list and evaluate result -- reaches the
48
+ * context window ONLY as the aggregate return of one Code Mode program. There is
49
+ * no other door, so governing `execute` IS "put JIT on the native browser". */
50
+ export declare const NATIVE_GOVERNED_TOOLS: ReadonlySet<string>;
51
+ /** Is this tool's output inside the governed surface? The host's browser catalog is
52
+ * 45 tools sharing one namespace (`browser_*`, each registered with
53
+ * `permission:"browser"`), and a snapshot from any of them is exactly the oversized
54
+ * payload the offload promise is about — enumerating 45 names would only guarantee
55
+ * that a host adding the 46th escapes us. Unknown RESULT shapes are still left
56
+ * untouched; that test is per payload, not per name. */
57
+ export declare function isGovernedNative(tool: string): boolean;
58
+ export interface V2OffloadReport {
59
+ seen: number;
60
+ /** every execute.after we resolved to a Team session, governed tool or not — the
61
+ * denominator the coverage sentence is read against (#23: "most tool calls are
62
+ * JIT-governed" has to be a number, not a feeling) */
63
+ ours: number;
64
+ /** ours, for a tool whose output we do not touch (unknown tool, or a name the
65
+ * governed set has not been widened to) — counted rather than invisible */
66
+ unmatched: number;
67
+ considered: number;
68
+ offloaded: number;
69
+ /** addressing results (native browser snapshots) that stayed IN context, capped
70
+ * rather than replaced by a handle — the tokens they cost are counted, and so is
71
+ * the fact that we chose a usable `ref` over the smaller number */
72
+ capped: number;
73
+ /** report-shaped payloads (tables kept, prose dropped) — a third outcome, because
74
+ * "offloaded" and "capped" answer different questions about what entered context */
75
+ reportCapped: number;
76
+ /** envelopes RECOGNISED, whether or not they needed rewriting — a counter that
77
+ * moves only on a rewrite cannot tell "the channel is alive, nothing was big"
78
+ * apart from "this host's format is not the one we match" (which is exactly how
79
+ * the v1 `<task id=` matcher read as zero on v2). */
80
+ envelopes: number;
81
+ degraded: number;
82
+ tokensSaved: number;
83
+ byTool: Record<string, {
84
+ seen: number;
85
+ offloaded: number;
86
+ }>;
87
+ }
88
+ interface OffloadDeps {
89
+ /** the shared tm pipeline instance — one store, one step counter, one threshold table */
90
+ pipelines: {
91
+ nextStepId: () => string;
92
+ govern: (stepId: string, tool: string, content: string, opts: {
93
+ contentType: ReturnType<typeof detectContentType>;
94
+ clue?: string;
95
+ }) => unknown;
96
+ };
97
+ env?: Record<string, string | undefined>;
98
+ /** Team-scope isolation (#22): a non-Team session's result is nobody's to rewrite
99
+ * but the host's, and an unknown owner is treated as not-ours and COUNTED — a
100
+ * governance layer that silently stopped applying is the same overstated claim
101
+ * this product exists to refuse. */
102
+ scope?: TeamScope;
103
+ }
104
+ /** Rendered in the user's tool-output language (the tm_* strings are Chinese and
105
+ * flow into every reply an agent writes about them). */
106
+ /**
107
+ * Tools whose result IS an addressing table: native `browser_click` / `browser_fill`
108
+ * take a `ref` out of the snapshot, so a preview that drops the ref tokens does not
109
+ * save the model a round-trip — it makes it click the wrong element, or spend a call
110
+ * re-snapshotting. For these two the text stays in context, capped, with the handle
111
+ * kept for the tail. 1 200 tokens is the budget tm_browser's own snapshot already
112
+ * runs at, so the governed door and the native one cost the same window space.
113
+ */
114
+ export declare const ADDRESSING_NATIVE_TOOLS: ReadonlySet<string>;
115
+ export declare const NATIVE_SNAPSHOT_MAX_TOKENS = 1200;
116
+ export declare function snapshotTokenBudget(env: Record<string, string | undefined> | undefined): number;
117
+ export declare function renderAddressingCap(tool: string, capped: string, governed: {
118
+ ref: string;
119
+ access_token: string;
120
+ expire_at: number;
121
+ tokens: number;
122
+ }): string;
123
+ /** The budget a report (a `tm_stats`-shaped answer, a `tm_join` summary) keeps in
124
+ * context: tables win the space, prose is what gets dropped. Pinned separately from
125
+ * the snapshot budget because a report has more prose per table. */
126
+ export declare const NATIVE_REPORT_MAX_TOKENS = 1600;
127
+ export declare function reportTokenBudget(env: Record<string, string | undefined> | undefined): number;
128
+ export declare function renderReportCap(tool: string, capped: string, governed: {
129
+ ref: string;
130
+ access_token: string;
131
+ expire_at: number;
132
+ tokens: number;
133
+ }, tablesDropped: number): string;
134
+ export declare function renderNativeOffload(tool: string, governed: {
135
+ offloaded: true;
136
+ ref: string;
137
+ access_token: string;
138
+ expire_at: number;
139
+ tokens: number;
140
+ preview: string;
141
+ }): string;
142
+ export declare function applyV2NativeOffload(ctx: unknown, deps: OffloadDeps): {
143
+ registrations: Promise<V2Registration>[];
144
+ report: V2OffloadReport;
145
+ active: boolean;
146
+ };
147
+ export {};
148
+ //# sourceMappingURL=v2-offload.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"v2-offload.d.ts","sourceRoot":"","sources":["../../src/host/v2-offload.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,288 @@
1
+ import { capKeepingAddressing, capKeepingTables, detectContentType, hasMarkdownTable } from "../tm/preview.js";
2
+ import { parseHostEnvelope, renderOffloadedSubagent } from "../task-offload.js";
3
+ import { estimateTokens } from "../tm/config.js";
4
+ /**
5
+ * JIT context governance over the HOST's own tools.
6
+ *
7
+ * The whole product rests on one promise: an oversized tool result never enters
8
+ * the context window — it lands in the run store and the model gets an
9
+ * ≤80-token preview plus a handle. That promise was true only inside our own
10
+ * `tm_*` tools, which is what made `tm_read`/`tm_grep`/`tm_bash` load-bearing
11
+ * rather than merely nicer. The moment a role is allowed the native `read` or
12
+ * `shell` (which is the retirement plan for those three), an un-governed native
13
+ * result would put the promise back to zero for exactly the tools a user would
14
+ * most plausibly enable — and the promise would then depend on which tool the
15
+ * model happened to pick, which is not a guarantee at all.
16
+ *
17
+ * So the governance moves to the seam that does not care what the model chose.
18
+ * `tool.execute.after` hands us the finished result for EVERY tool (measured
19
+ * live: `{action:"shell"}` reaches `permission.evaluate`, and the same call
20
+ * reaches `execute.after` with `resultKeys: content / metadata / output`), and
21
+ * the host documents `result` as mutable. One hook, both worlds.
22
+ *
23
+ * Design constraints this file is written under:
24
+ * - **It only ever shrinks what reaches the context, never what the tool did.**
25
+ * The bytes on disk are the host's; a preview plus a handle replaces the text
26
+ * part. `metadata` and any non-text part (an image attachment) pass through
27
+ * untouched — dropping a screenshot to save tokens would be a bad trade.
28
+ * - **It reuses the same threshold, preview builder, store and HMAC handles as
29
+ * `tm_*`**, because a second implementation would drift, and the drift would
30
+ * show up as "the same 40 KB was free through one tool and offloaded through
31
+ * another".
32
+ * - **A native result is rendered as native-shaped text.** `tm_*` returns our own
33
+ * structured object; overwriting a host tool's text part with `{offloaded:true,
34
+ * ref:…}` would hand the model a shape it has no reason to understand. The
35
+ * sentence below says the same facts in prose the tm_* tools already train it
36
+ * to act on: how much was saved, the handle, how to page it in.
37
+ * - Nothing here may throw into somebody's tool call: a governance failure
38
+ * degrades to the host's verbatim result, and says so in the trajectory.
39
+ */
40
+ /** Tools whose NATIVE output we govern. Deliberately a closed list: the built-in
41
+ * agent/patch/question results have shapes nobody has observed here, and rewriting
42
+ * a shape you guessed at is content destruction, not governance.
43
+ *
44
+ * `execute` is on the list for one specific reason, and it is the reason the
45
+ * native browser can be governed at all: the Team's direct tool surface on v2 is
46
+ * six tools (edit / execute / question / shell / subagent / write), so
47
+ * `tools.browser.*` -- every snapshot, tab list and evaluate result -- reaches the
48
+ * context window ONLY as the aggregate return of one Code Mode program. There is
49
+ * no other door, so governing `execute` IS "put JIT on the native browser". */
50
+ export const NATIVE_GOVERNED_TOOLS = new Set([
51
+ "read",
52
+ "grep",
53
+ "glob",
54
+ "shell",
55
+ "bash",
56
+ "webfetch",
57
+ "websearch",
58
+ "execute",
59
+ "edit",
60
+ "write",
61
+ "patch",
62
+ "question",
63
+ /*
64
+ * `subagent` is v2's version of Plan B, and it is here because of what was
65
+ * MEASURED rather than what v1 did. A live run recorded
66
+ * `execute.before subagent {agent,background,description,prompt}` ->
67
+ * `permission.evaluate action=subagent` -> `execute.after subagent
68
+ * {content,metadata,output}`: a synchronous child's whole reply arrives as THIS
69
+ * tool's result, so one hook governs it. The same run showed
70
+ * `session.hook("context")` carrying a single message and no
71
+ * `<task id=… state="completed">` envelope at all -- so v1's chat.message offload
72
+ * has no anchor on v2, and rewriting the message list against a guessed shape is
73
+ * how evidence gets deleted silently (the same reason host-hooks.ts declines
74
+ * `experimental.chat.messages.transform`). Consequence stated plainly: a
75
+ * BACKGROUND child's reply, which the host injects later, is NOT governed here --
76
+ * closing that needs the ctx.event/ctx.session rebuild, which is an open decision.
77
+ */
78
+ "subagent",
79
+ ]);
80
+ /** Is this tool's output inside the governed surface? The host's browser catalog is
81
+ * 45 tools sharing one namespace (`browser_*`, each registered with
82
+ * `permission:"browser"`), and a snapshot from any of them is exactly the oversized
83
+ * payload the offload promise is about — enumerating 45 names would only guarantee
84
+ * that a host adding the 46th escapes us. Unknown RESULT shapes are still left
85
+ * untouched; that test is per payload, not per name. */
86
+ export function isGovernedNative(tool) {
87
+ return NATIVE_GOVERNED_TOOLS.has(tool) || tool.startsWith("browser_");
88
+ }
89
+ const textPart = (p) => !!p && typeof p === "object" && p.type === "text" && typeof p.text === "string";
90
+ /** The host's result shape, as measured: `{content, metadata, output}` with
91
+ * `content` an array of parts. Every other shape is left alone rather than
92
+ * interpreted — an unknown shape we rewrite is content destruction. */
93
+ function locatableText(result) {
94
+ const r = result;
95
+ if (!r || !Array.isArray(r.content))
96
+ return null;
97
+ let joined = "";
98
+ let index = -1;
99
+ for (let i = 0; i < r.content.length; i++) {
100
+ const p = r.content[i];
101
+ if (!textPart(p))
102
+ continue;
103
+ if (index < 0)
104
+ index = i;
105
+ joined += (index === i && joined === "" ? "" : joined ? "\n" : "") + p.text;
106
+ }
107
+ if (index < 0 || !joined)
108
+ return null;
109
+ return { parts: r.content, index, text: joined };
110
+ }
111
+ /** Rendered in the user's tool-output language (the tm_* strings are Chinese and
112
+ * flow into every reply an agent writes about them). */
113
+ /**
114
+ * Tools whose result IS an addressing table: native `browser_click` / `browser_fill`
115
+ * take a `ref` out of the snapshot, so a preview that drops the ref tokens does not
116
+ * save the model a round-trip — it makes it click the wrong element, or spend a call
117
+ * re-snapshotting. For these two the text stays in context, capped, with the handle
118
+ * kept for the tail. 1 200 tokens is the budget tm_browser's own snapshot already
119
+ * runs at, so the governed door and the native one cost the same window space.
120
+ */
121
+ export const ADDRESSING_NATIVE_TOOLS = new Set(["browser_snapshot", "browser_find"]);
122
+ export const NATIVE_SNAPSHOT_MAX_TOKENS = 1200;
123
+ export function snapshotTokenBudget(env) {
124
+ const raw = Number(String(env?.TM_NATIVE_SNAPSHOT_MAX_TOKENS ?? "").trim());
125
+ return Number.isFinite(raw) && raw >= 100 ? Math.min(20_000, Math.floor(raw)) : NATIVE_SNAPSHOT_MAX_TOKENS;
126
+ }
127
+ export function renderAddressingCap(tool, capped, governed) {
128
+ const when = new Date(governed.expire_at).toISOString().replace("T", " ").slice(0, 16);
129
+ return (capped +
130
+ `
131
+
132
+ (原生 ${tool} 的快照按 JIT 治理截到预算内,没有整体卸载:截断部分里的 ref 不在你眼前,` +
133
+ `需要时用 tm_fetch { ref:"${governed.ref}", access_token:"${governed.access_token}", mode:"lines" } 分段取回再点,` +
134
+ `不要为了看一眼把全文读回来。原文 ${governed.tokens} token · 过期 ${governed.expire_at} · ${when})`);
135
+ }
136
+ /** The budget a report (a `tm_stats`-shaped answer, a `tm_join` summary) keeps in
137
+ * context: tables win the space, prose is what gets dropped. Pinned separately from
138
+ * the snapshot budget because a report has more prose per table. */
139
+ export const NATIVE_REPORT_MAX_TOKENS = 1600;
140
+ export function reportTokenBudget(env) {
141
+ const raw = Number(String(env?.TM_NATIVE_REPORT_MAX_TOKENS ?? "").trim());
142
+ return Number.isFinite(raw) && raw >= 200 ? Math.min(20_000, Math.floor(raw)) : NATIVE_REPORT_MAX_TOKENS;
143
+ }
144
+ export function renderReportCap(tool, capped, governed, tablesDropped) {
145
+ const when = new Date(governed.expire_at).toISOString().replace("T", " ").slice(0, 16);
146
+ return (capped +
147
+ `
148
+
149
+ (原生 ${tool} 的返回是报告形的:表格已整份留在上面,被删掉的只有表格之间的说明文字——` +
150
+ `全文 ${governed.tokens} token,别为了看一句散文把整份读回来。` +
151
+ (tablesDropped > 0 ? `注意:预算之外还有 ${tablesDropped} 行表格内容没进来,需要时用 ` : "需要更多行时用 ") +
152
+ `tm_fetch { ref:"${governed.ref}", access_token:"${governed.access_token}", mode:"lines" } 分段取。过期 ${governed.expire_at} · ${when})`);
153
+ }
154
+ export function renderNativeOffload(tool, governed) {
155
+ const when = new Date(governed.expire_at).toISOString().replace("T", " ").slice(0, 16);
156
+ return [
157
+ `(原生 ${tool} 的输出过大,已按 JIT 治理卸载:${governed.tokens} token 没有进入上下文——全文在句柄里,不在下面这段里。)`,
158
+ "",
159
+ governed.preview,
160
+ "",
161
+ `取全文:tm_fetch { ref:"${governed.ref}", access_token:"${governed.access_token}", mode:"structure" | "lines" }(过期 ${governed.expire_at} · ${when})`,
162
+ "只要摘要就到此为止;需要具体行请用 mode:\"lines\" 分段取,不要为了看一眼把全文读回来。",
163
+ ].join("\n");
164
+ }
165
+ export function applyV2NativeOffload(ctx, deps) {
166
+ const env = deps.env ?? process.env;
167
+ const off = /^(0|false|no|off)$/i.test(String(env.TM_NATIVE_OFFLOAD ?? "").trim());
168
+ const report = { seen: 0, ours: 0, unmatched: 0, considered: 0, offloaded: 0, capped: 0, reportCapped: 0, envelopes: 0, degraded: 0, tokensSaved: 0, byTool: {} };
169
+ const hook = ctx?.tool?.hook;
170
+ if (typeof hook !== "function") {
171
+ // No seam at all: report it, do not silently pretend the promise holds.
172
+ return { registrations: [], report, active: false };
173
+ }
174
+ const reg = hook("execute.after", (raw) => {
175
+ const event = raw;
176
+ const tool = String(event?.tool ?? "");
177
+ // The owner question comes FIRST: a foreign or unattributable session is left
178
+ // alone no matter which tool it called, and the counters then describe every
179
+ // call we looked at rather than only the ones we recognised a tool for.
180
+ if (deps.scope) {
181
+ if (deps.scope.count(deps.scope.decide(event)) !== "ours")
182
+ return;
183
+ // A resolved call is also a fact about the session — remember it so a later
184
+ // event that arrives without `agent` still resolves to ours.
185
+ deps.scope.learn(event.agent, event.sessionID);
186
+ }
187
+ report.ours++;
188
+ if (!isGovernedNative(tool)) {
189
+ report.unmatched++;
190
+ report.byTool[tool] ??= { seen: 0, offloaded: 0 };
191
+ report.byTool[tool].seen++;
192
+ return;
193
+ }
194
+ report.seen++;
195
+ report.byTool[tool] ??= { seen: 0, offloaded: 0 };
196
+ report.byTool[tool].seen++;
197
+ if (off)
198
+ return;
199
+ const found = locatableText(event?.result);
200
+ if (!found)
201
+ return;
202
+ report.considered++;
203
+ // A sub-agent reply arrives wrapped in the host's own envelope. Offloading it
204
+ // means replacing the BODY and reproducing the wrapper: the host and the UI key
205
+ // on `<subagent sessionID=…>`, so flattening it into a generic governed result
206
+ // would delete the very pointer the lead needs to fetch the whole thing back.
207
+ const envelope = tool === "subagent" ? parseHostEnvelope(found.text) : null;
208
+ if (envelope)
209
+ report.envelopes++;
210
+ const stepId = deps.pipelines.nextStepId();
211
+ // Content class is inferred from the BODY, not from a path we do not have:
212
+ // that is the same basis tm_bash uses, so a 30 KB JSON stdout gets the data
213
+ // tier here and there too.
214
+ const contentType = detectContentType(found.text);
215
+ let governed;
216
+ try {
217
+ governed = deps.pipelines.govern(stepId, `native:${tool}`, found.text, {
218
+ contentType,
219
+ clue: `原生 ${tool} 的结果`,
220
+ });
221
+ }
222
+ catch {
223
+ report.degraded++;
224
+ return;
225
+ }
226
+ if (!governed || typeof governed !== "object" || governed.offloaded !== true)
227
+ return;
228
+ const g = governed;
229
+ const parts = found.parts;
230
+ if (ADDRESSING_NATIVE_TOOLS.has(tool)) {
231
+ // The snapshot is an addressing table, not a document: replacing it with a
232
+ // handle would strand the next click. Cap it, keep the handle for the tail,
233
+ // and count the tokens that DID enter.
234
+ const budget = snapshotTokenBudget(deps.env);
235
+ const cut = capKeepingAddressing(found.text, budget);
236
+ const capped = `${cut.text}${cut.fellBack
237
+ ? `\n\n(这个结果是一整行没有换行的 JSON,按行保留寻址行的做法装不下任何内容,所以退成了从头截断 —— 内容不完整,别把它当成页面是空的;要精确字段就用 tm_fetch mode:"lines" 或改拍一次范围更小的快照。)`
238
+ : cut.addressesDropped > 0
239
+ ? `
240
+
241
+ (预算 ${budget} token 之内只装下了部分寻址行:另有 ${cut.addressesDropped} 行带 ref 的内容被截掉了,要点对它们先用 tm_fetch mode:"lines" 取回,或缩小范围重新拍一次快照——不要凭猜去点。)`
242
+ : ""}`;
243
+ const rendered = renderAddressingCap(tool, capped, g);
244
+ parts[found.index] = { type: "text", text: rendered };
245
+ for (let i = found.index + 1; i < parts.length; i++) {
246
+ if (textPart(parts[i]))
247
+ parts[i] = { type: "text", text: "" };
248
+ }
249
+ report.capped++;
250
+ report.tokensSaved += Math.max(0, estimateTokens(found.text) - estimateTokens(rendered));
251
+ report.byTool[tool].offloaded++;
252
+ return;
253
+ }
254
+ if (!envelope && hasMarkdownTable(found.text)) {
255
+ // A report is not a log. Offloading `tm_stats` down to an 80-token preview
256
+ // (measured live, on a report that came back through Code Mode's `execute` at
257
+ // 2 917 tokens) leaves the lead describing a table it cannot see and the user
258
+ // reading prose about numbers instead of the numbers. So the tables stay, the
259
+ // prose goes, and the handle still carries the whole thing.
260
+ const budget = reportTokenBudget(deps.env);
261
+ const cut = capKeepingTables(found.text, budget);
262
+ const rendered = renderReportCap(tool, cut.text, g, cut.tablesDropped);
263
+ parts[found.index] = { type: "text", text: rendered };
264
+ for (let i = found.index + 1; i < parts.length; i++) {
265
+ if (textPart(parts[i]))
266
+ parts[i] = { type: "text", text: "" };
267
+ }
268
+ report.reportCapped++;
269
+ report.tokensSaved += Math.max(0, estimateTokens(found.text) - estimateTokens(rendered));
270
+ report.byTool[tool].offloaded++;
271
+ return;
272
+ }
273
+ const keepTokens = estimateTokens(g.preview);
274
+ parts[found.index] = {
275
+ type: "text",
276
+ text: envelope ? renderOffloadedSubagent(envelope, g.preview, g.tokens) : renderNativeOffload(tool, g),
277
+ };
278
+ for (let i = found.index + 1; i < parts.length; i++) {
279
+ if (textPart(parts[i]))
280
+ parts[i] = { type: "text", text: "" };
281
+ }
282
+ report.offloaded++;
283
+ report.tokensSaved += Math.max(0, estimateTokens(found.text) - keepTokens);
284
+ report.byTool[tool].offloaded++;
285
+ });
286
+ return { registrations: [reg], report, active: !off };
287
+ }
288
+ //# sourceMappingURL=v2-offload.js.map