@te-river/opencode-team-mode 1.5.15 → 1.6.1
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 +853 -690
- package/README.zh-CN.md +761 -631
- package/dist/agents.d.ts +5 -3
- package/dist/agents.d.ts.map +1 -1
- package/dist/agents.js +19 -8
- package/dist/agents.js.map +1 -1
- package/dist/capabilities.d.ts +1 -1
- package/dist/capabilities.js +2 -2
- package/dist/capabilities.js.map +1 -1
- package/dist/host/note.d.ts +7 -0
- package/dist/host/note.d.ts.map +1 -0
- package/dist/host/note.js +28 -0
- package/dist/host/note.js.map +1 -0
- package/dist/host/v1.d.ts +14 -0
- package/dist/host/v1.d.ts.map +1 -0
- package/dist/host/v1.js +340 -0
- package/dist/host/v1.js.map +1 -0
- package/dist/host/v2-browser-gate.d.ts +45 -0
- package/dist/host/v2-browser-gate.d.ts.map +1 -0
- package/dist/host/v2-browser-gate.js +319 -0
- package/dist/host/v2-browser-gate.js.map +1 -0
- package/dist/host/v2-capabilities.d.ts +68 -0
- package/dist/host/v2-capabilities.d.ts.map +1 -0
- package/dist/host/v2-capabilities.js +146 -0
- package/dist/host/v2-capabilities.js.map +1 -0
- package/dist/host/v2-client.d.ts +53 -0
- package/dist/host/v2-client.d.ts.map +1 -0
- package/dist/host/v2-client.js +193 -0
- package/dist/host/v2-client.js.map +1 -0
- package/dist/host/v2-events.d.ts +56 -0
- package/dist/host/v2-events.d.ts.map +1 -0
- package/dist/host/v2-events.js +158 -0
- package/dist/host/v2-events.js.map +1 -0
- package/dist/host/v2-guard.d.ts +101 -0
- package/dist/host/v2-guard.d.ts.map +1 -0
- package/dist/host/v2-guard.js +199 -0
- package/dist/host/v2-guard.js.map +1 -0
- package/dist/host/v2-offload.d.ts +148 -0
- package/dist/host/v2-offload.d.ts.map +1 -0
- package/dist/host/v2-offload.js +288 -0
- package/dist/host/v2-offload.js.map +1 -0
- package/dist/host/v2-permissions.d.ts +123 -0
- package/dist/host/v2-permissions.d.ts.map +1 -0
- package/dist/host/v2-permissions.js +190 -0
- package/dist/host/v2-permissions.js.map +1 -0
- package/dist/host/v2-probe.d.ts +85 -0
- package/dist/host/v2-probe.d.ts.map +1 -0
- package/dist/host/v2-probe.js +268 -0
- package/dist/host/v2-probe.js.map +1 -0
- package/dist/host/v2-scope.d.ts +51 -0
- package/dist/host/v2-scope.d.ts.map +1 -0
- package/dist/host/v2-scope.js +58 -0
- package/dist/host/v2-scope.js.map +1 -0
- package/dist/host/v2-session-client.d.ts +74 -0
- package/dist/host/v2-session-client.d.ts.map +1 -0
- package/dist/host/v2-session-client.js +171 -0
- package/dist/host/v2-session-client.js.map +1 -0
- package/dist/host/v2-session.d.ts +85 -0
- package/dist/host/v2-session.d.ts.map +1 -0
- package/dist/host/v2-session.js +171 -0
- package/dist/host/v2-session.js.map +1 -0
- package/dist/host/v2-subagent.d.ts +157 -0
- package/dist/host/v2-subagent.d.ts.map +1 -0
- package/dist/host/v2-subagent.js +286 -0
- package/dist/host/v2-subagent.js.map +1 -0
- package/dist/host/v2-tool.d.ts +67 -0
- package/dist/host/v2-tool.d.ts.map +1 -0
- package/dist/host/v2-tool.js +209 -0
- package/dist/host/v2-tool.js.map +1 -0
- package/dist/host/v2-transcript.d.ts +73 -0
- package/dist/host/v2-transcript.d.ts.map +1 -0
- package/dist/host/v2-transcript.js +151 -0
- package/dist/host/v2-transcript.js.map +1 -0
- package/dist/host/v2-types.d.ts +229 -0
- package/dist/host/v2-types.d.ts.map +1 -0
- package/dist/host/v2-types.js +17 -0
- package/dist/host/v2-types.js.map +1 -0
- package/dist/host/v2.d.ts +42 -0
- package/dist/host/v2.d.ts.map +1 -0
- package/dist/host/v2.js +706 -0
- package/dist/host/v2.js.map +1 -0
- package/dist/host-hooks.d.ts +13 -2
- package/dist/host-hooks.d.ts.map +1 -1
- package/dist/host-hooks.js +18 -5
- package/dist/host-hooks.js.map +1 -1
- package/dist/index.d.ts +23 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +29 -326
- package/dist/index.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 +452 -359
- package/dist/prompts/lead.js.map +1 -1
- package/dist/prompts/shared.d.ts +2 -2
- package/dist/prompts/shared.d.ts.map +1 -1
- package/dist/prompts/shared.js +89 -17
- package/dist/prompts/shared.js.map +1 -1
- package/dist/prompts/specialists.d.ts +2 -2
- package/dist/prompts/specialists.d.ts.map +1 -1
- package/dist/prompts/specialists.js +44 -10
- package/dist/prompts/specialists.js.map +1 -1
- package/dist/task-offload.d.ts +83 -0
- package/dist/task-offload.d.ts.map +1 -0
- package/dist/task-offload.js +174 -0
- package/dist/task-offload.js.map +1 -0
- package/dist/tm/args-schema.d.ts +4 -0
- package/dist/tm/args-schema.d.ts.map +1 -1
- package/dist/tm/args-schema.js +36 -3
- package/dist/tm/args-schema.js.map +1 -1
- package/dist/tm/board.d.ts +113 -0
- package/dist/tm/board.d.ts.map +1 -0
- package/dist/tm/board.js +275 -0
- package/dist/tm/board.js.map +1 -0
- package/dist/tm/browser.d.ts +325 -6
- package/dist/tm/browser.d.ts.map +1 -1
- package/dist/tm/browser.js +1360 -135
- package/dist/tm/browser.js.map +1 -1
- package/dist/tm/config.d.ts +36 -25
- package/dist/tm/config.d.ts.map +1 -1
- package/dist/tm/config.js +29 -7
- package/dist/tm/config.js.map +1 -1
- package/dist/tm/dispatch.d.ts +133 -53
- package/dist/tm/dispatch.d.ts.map +1 -1
- package/dist/tm/dispatch.js +481 -279
- package/dist/tm/dispatch.js.map +1 -1
- package/dist/tm/dupe-guard.d.ts +16 -3
- package/dist/tm/dupe-guard.d.ts.map +1 -1
- package/dist/tm/dupe-guard.js +27 -7
- package/dist/tm/dupe-guard.js.map +1 -1
- package/dist/tm/egress.d.ts +44 -0
- package/dist/tm/egress.d.ts.map +1 -0
- package/dist/tm/egress.js +191 -0
- package/dist/tm/egress.js.map +1 -0
- package/dist/tm/guard.js +1 -1
- package/dist/tm/guard.js.map +1 -1
- package/dist/tm/index.d.ts +67 -2
- package/dist/tm/index.d.ts.map +1 -1
- package/dist/tm/index.js +228 -20
- package/dist/tm/index.js.map +1 -1
- package/dist/tm/ledger.d.ts +114 -0
- package/dist/tm/ledger.d.ts.map +1 -0
- package/dist/tm/ledger.js +308 -0
- package/dist/tm/ledger.js.map +1 -0
- package/dist/tm/perm-ask.d.ts +45 -5
- package/dist/tm/perm-ask.d.ts.map +1 -1
- package/dist/tm/perm-ask.js +107 -7
- package/dist/tm/perm-ask.js.map +1 -1
- package/dist/tm/pipelines.d.ts.map +1 -1
- package/dist/tm/pipelines.js +24 -2
- package/dist/tm/pipelines.js.map +1 -1
- package/dist/tm/preview.d.ts +55 -0
- package/dist/tm/preview.d.ts.map +1 -1
- package/dist/tm/preview.js +225 -0
- package/dist/tm/preview.js.map +1 -1
- package/dist/tm/ptc/tool.d.ts.map +1 -1
- package/dist/tm/ptc/tool.js +2 -4
- package/dist/tm/ptc/tool.js.map +1 -1
- package/dist/tm/pty.d.ts.map +1 -1
- package/dist/tm/pty.js +6 -3
- package/dist/tm/pty.js.map +1 -1
- package/dist/tm/search.d.ts +5 -0
- package/dist/tm/search.d.ts.map +1 -1
- package/dist/tm/search.js +40 -8
- package/dist/tm/search.js.map +1 -1
- package/dist/tm/serp-loop.d.ts +45 -0
- package/dist/tm/serp-loop.d.ts.map +1 -0
- package/dist/tm/serp-loop.js +80 -0
- package/dist/tm/serp-loop.js.map +1 -0
- package/dist/tm/stats.d.ts +26 -0
- package/dist/tm/stats.d.ts.map +1 -1
- package/dist/tm/stats.js +194 -4
- package/dist/tm/stats.js.map +1 -1
- package/dist/tm/webfetch.d.ts +56 -0
- package/dist/tm/webfetch.d.ts.map +1 -1
- package/dist/tm/webfetch.js +187 -19
- package/dist/tm/webfetch.js.map +1 -1
- package/dist/tool-coerce.d.ts +24 -0
- package/dist/tool-coerce.d.ts.map +1 -0
- package/dist/tool-coerce.js +63 -0
- package/dist/tool-coerce.js.map +1 -0
- package/dist/types.d.ts +7 -1
- package/dist/types.d.ts.map +1 -1
- package/docs/installation-v2.md +436 -0
- package/docs/installation.md +81 -2
- package/docs/research/agent-data-exchange.md +117 -0
- package/docs/research/browser-pane.md +89 -0
- package/docs/research/host-http-api.md +132 -0
- package/docs/research/host-subagent-injection.md +187 -0
- package/docs/research/plugin-loader-contract.md +261 -0
- package/docs/research/v2-builtin-tools.md +447 -0
- package/docs/v2-port-notes.md +315 -0
- package/index.js +7 -0
- package/package.json +4 -1
- package/scripts/gen-v2-config.mjs +304 -0
- package/scripts/lib/config-surgery.cjs +239 -0
- package/scripts/lib/fake-ctx.mjs +146 -0
package/dist/host/v2.js
ADDED
|
@@ -0,0 +1,706 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The v2 personality: `Plugin.define({id, setup(ctx)})`.
|
|
3
|
+
*
|
|
4
|
+
* `Plugin.define` in @opencode/plugin@2.0.16 is literally `plugin => plugin`,
|
|
5
|
+
* so this module exports the plain object and the v2 SDK never becomes a
|
|
6
|
+
* runtime dependency — which is what keeps the dual-personality package
|
|
7
|
+
* installable by a v1 user who cannot resolve that package at all.
|
|
8
|
+
*
|
|
9
|
+
* Scope of this file (milestone M2): build the governed runtime on a
|
|
10
|
+
* filesystem-backed client shim, register the tm_* tools through
|
|
11
|
+
* `ctx.tool.transform`, and normalize the permission matrix onto the agents the
|
|
12
|
+
* CONFIG declares. It deliberately does NOT touch: the approval gate (#94), the
|
|
13
|
+
* session hooks / compaction / description appends (#93), or the browser
|
|
14
|
+
* (#95). Those need this seam proven live first.
|
|
15
|
+
*
|
|
16
|
+
* Two v1 behaviors do not carry over, and both absences are stated rather than
|
|
17
|
+
* discovered later:
|
|
18
|
+
* - creating agents: `AgentEditor` has no `add`, so the six roles must come
|
|
19
|
+
* from the user's config (the installer writes `agents/*.md`).
|
|
20
|
+
* - the conservative default-agent promotion: v1 read `cfg.default_agent` and
|
|
21
|
+
* only filled it when empty or "build". v2 exposes `default(id)` with NO
|
|
22
|
+
* getter, so "only if the user did not choose" is not expressible — the
|
|
23
|
+
* choice is between never promoting and promoting every boot. The user's
|
|
24
|
+
* standing instruction (2026-09-25) is that Team is ALWAYS the default, so
|
|
25
|
+
* this promotes unconditionally; `defaultAgent: false` in the plugin options
|
|
26
|
+
* opts out, exactly as on v1.
|
|
27
|
+
*
|
|
28
|
+
* The official confirmation dialog is absent as well, and for a different
|
|
29
|
+
* reason: probed on the live host, a plugin has no way to raise one.
|
|
30
|
+
* `askFnOf` therefore finds no bridge and every governed call fails closed with
|
|
31
|
+
* the v2-worded refusal set below.
|
|
32
|
+
*/
|
|
33
|
+
import { agents } from "../agents.js";
|
|
34
|
+
import { DEFAULT_TTL_DAYS, resolveTtlMs, startBlackboardMaintenance } from "../blackboard.js";
|
|
35
|
+
import { parseExtraDeny, resolveEnvProtectMode } from "../envprotect.js";
|
|
36
|
+
import { createTmTools } from "../tm/index.js";
|
|
37
|
+
import { setAskUnavailableNote } from "../tm/perm-ask.js";
|
|
38
|
+
import { setPrivateSpacePolicy } from "../tm/webfetch.js";
|
|
39
|
+
import { blackboardNote } from "./note.js";
|
|
40
|
+
import { applyV2BackgroundForce, applyV2PermissionGuards, needsCoarseShellAsk } from "./v2-guard.js";
|
|
41
|
+
import { applyV2EventFeed } from "./v2-events.js";
|
|
42
|
+
import { createV2SessionReader } from "./v2-session-client.js";
|
|
43
|
+
import { applyV2Probe, probeSummary } from "./v2-probe.js";
|
|
44
|
+
import { applyV2NativeOffload } from "./v2-offload.js";
|
|
45
|
+
import { applyV2SubagentRegistry, applyV2CompletionWatch } from "./v2-subagent.js";
|
|
46
|
+
import { applyV2BrowserGate, browserGateSummary } from "./v2-browser-gate.js";
|
|
47
|
+
import { seedWebfetchDomains } from "../tm/webfetch.js";
|
|
48
|
+
import { v2CapabilityRows } from "./v2-capabilities.js";
|
|
49
|
+
import { applyV2SessionLayer, removalPlan } from "./v2-session.js";
|
|
50
|
+
import { createStorageLedgerStore } from "../tm/ledger.js";
|
|
51
|
+
import { createTeamScope } from "./v2-scope.js";
|
|
52
|
+
import { bindV2Tool } from "./v2-tool.js";
|
|
53
|
+
import { mergeTriples, triplesFromAgentPermission, V1_ONLY_TOOLS } from "./v2-permissions.js";
|
|
54
|
+
/** The refusal has to name the real reason: on v2 the absence is the new
|
|
55
|
+
* protocol, and "旧版协议" would send the model looking for an upgrade the
|
|
56
|
+
* user already installed. It must ALSO not point at a domain gate this
|
|
57
|
+
* personality no longer runs (the default is `"*"`), so the knob it names is
|
|
58
|
+
* the host's own permission config, and the address red line is excluded from
|
|
59
|
+
* "ask the user" because that one has no consent path at all. */
|
|
60
|
+
export const V2_NO_DIALOG_NOTE = "这个宿主(OpenCode v2)不给插件弹出确认窗的入口,所以我没有问任何人就直接拒绝了——" +
|
|
61
|
+
"我不会在没有对话框的情况下假装用户同意。请换一个不需要这次访问的源," +
|
|
62
|
+
"或让用户在宿主的权限设置里放行(私网/元数据地址是红线,那条没有放行路径),不要重复调用。";
|
|
63
|
+
function directoryOf(ctx) {
|
|
64
|
+
const loc = ctx?.location;
|
|
65
|
+
if (typeof loc?.directory === "string" && loc.directory)
|
|
66
|
+
return loc.directory;
|
|
67
|
+
if (typeof loc?.project?.directory === "string" && loc.project.directory)
|
|
68
|
+
return loc.project.directory;
|
|
69
|
+
return process.cwd();
|
|
70
|
+
}
|
|
71
|
+
export const v2Personality = {
|
|
72
|
+
id: "team-mode",
|
|
73
|
+
async setup(ctx) {
|
|
74
|
+
if (!ctx || typeof ctx.tool?.transform !== "function") {
|
|
75
|
+
// Nothing can be registered without the tool domain. Say it in the
|
|
76
|
+
// server log instead of failing the load silently: a plugin that injects
|
|
77
|
+
// nothing looks identical to one that was never installed.
|
|
78
|
+
console.error("[team-mode] v2 setup aborted: ctx.tool.transform is missing on this host");
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const directory = directoryOf(ctx);
|
|
82
|
+
// ---------- Team-scope isolation (#22, the user's requirement 2026-09-25) ----------
|
|
83
|
+
// Every v2 hook fires for EVERY agent on this host. This one object is what
|
|
84
|
+
// each layer asks before it mutates a request, a result, or a permission —
|
|
85
|
+
// build/plan and any third-party agent must look exactly like a fresh install.
|
|
86
|
+
const scope = createTeamScope(Object.keys(agents));
|
|
87
|
+
setAskUnavailableNote(V2_NO_DIALOG_NOTE);
|
|
88
|
+
const options = (ctx.options ?? {});
|
|
89
|
+
const envProtectMode = options.envProtect ? resolveEnvProtectMode(process.env.TM_ENV_PROTECT) : "off";
|
|
90
|
+
const envProtectExtra = parseExtraDeny(process.env.TM_ENV_PROTECT_EXTRA_DENY);
|
|
91
|
+
// ---------- the v2 network policy: no domain gate, IP red line only ----------
|
|
92
|
+
// The user's standing instruction for v2 (2026-09-25): do not block network
|
|
93
|
+
// access at all EXCEPT sensitive and internal addresses. v1's 22-host seed list
|
|
94
|
+
// existed because v1 could open the host's official per-request dialog; on v2 a
|
|
95
|
+
// plugin cannot raise one, so an allowlist became a set of pages the agent can
|
|
96
|
+
// never see and no one can approve -- a gate with no door. `"*"` therefore
|
|
97
|
+
// replaces the DEFAULT here, and what still holds absolutely is `checkWebUrl`'s
|
|
98
|
+
// address policy underneath it: link-local / metadata / reserved ranges are a hard
|
|
99
|
+
// deny no config can open (and were never consentable), and private space
|
|
100
|
+
// (loopback / RFC1918 / CGNAT / .localhost) stays gated — refused through our
|
|
101
|
+
// tools, which have no dialog to ask with, and opened only by the host's own
|
|
102
|
+
// `effect:"ask"` for the native tools. IPv4-mapped and DNS64 spellings are
|
|
103
|
+
// unwrapped before that check, so the notation is not a way around it.
|
|
104
|
+
// An explicit TM_WEBFETCH_ALLOWED_DOMAINS always wins; this only changes which
|
|
105
|
+
// default applies, and it is resolved from a COPY of the env so the v1
|
|
106
|
+
// personality in the same process is untouched.
|
|
107
|
+
const v2Env = { ...process.env };
|
|
108
|
+
if (!String(v2Env.TM_WEBFETCH_ALLOWED_DOMAINS ?? "").trim())
|
|
109
|
+
v2Env.TM_WEBFETCH_ALLOWED_DOMAINS = "*";
|
|
110
|
+
// The DOMAIN gate is off on this personality (see the note above): 2.x gives a
|
|
111
|
+
// plugin no dialog, so "approve to proceed" is an instruction to wait for a
|
|
112
|
+
// window that never opens. Private space is NOT opened by the same logic,
|
|
113
|
+
// because that is not a whitelist question — a default that let a governed tool
|
|
114
|
+
// GET 192.168.1.1 would put the user's router in the trajectory for a mistake the
|
|
115
|
+
// model made. So the answer here is a refusal that names both exits
|
|
116
|
+
// (TM_PRIVATE_SPACE=allow, or the one host in TM_WEBFETCH_ALLOWED_DOMAINS) instead
|
|
117
|
+
// of promising a dialog, and `deny` is what a quiet v2 host gets.
|
|
118
|
+
const privateSpace = setPrivateSpacePolicy(String(v2Env.TM_PRIVATE_SPACE ?? "").trim() || "deny");
|
|
119
|
+
// ---------- how our tools reach the model at all (measured in the binary) ----------
|
|
120
|
+
// 2.0.16 decides tool visibility with `options.codemode`: a tool whose value is
|
|
121
|
+
// not `false` is offered ONLY inside the Code Mode catalog, where the host keeps
|
|
122
|
+
// the first line of its description (≤120 chars) under a ~2 000-token budget.
|
|
123
|
+
// `bindV2Tool` used to send no `options` at all, which is the real explanation
|
|
124
|
+
// for the live session that showed six tools and zero `tm_*` after a successful
|
|
125
|
+
// registration: the tools were there, and almost none of the governance text we
|
|
126
|
+
// wrote for them ever arrived. `TM_V2_CODEMODE=direct` sends the flag; the
|
|
127
|
+
// default keeps today's behaviour until the token cost is measured, because the
|
|
128
|
+
// other half of that sentence is that a direct tool definition rides EVERY
|
|
129
|
+
// request (our own estimate for the full set is 9 528 tokens).
|
|
130
|
+
// Delivered as REAL tools by default: the user's call (2026-09-25) was "slim the
|
|
131
|
+
// description first, then go direct", and the description is now 729 tokens for
|
|
132
|
+
// tm_browser (was 2 788 for the whole tool), which puts the measured per-role
|
|
133
|
+
// cost at 2 660 build-class / 4 726 tester / 6 391 researcher / 7 793 lead.
|
|
134
|
+
// `TM_V2_CODEMODE=off` restores catalog-only for anyone who wants the tokens back
|
|
135
|
+
// — and the boot note says which world this is, because in catalog mode most of
|
|
136
|
+
// our governance text never reaches the model at all.
|
|
137
|
+
// `TM_V2_CODEMODE` is now OPT-IN for direct delivery. Measured on a live 2.0.16
|
|
138
|
+
// desktop session (export read 2026-09-26): with `options.codemode:false` sent for
|
|
139
|
+
// every tool, the host still delivered all ten `tm_*` inside the Code Mode catalog
|
|
140
|
+
// ("They cannot be called directly…"), and the model's own callable list was the
|
|
141
|
+
// nine native tools. So the flag changes what we send, not what the model gets —
|
|
142
|
+
// and shipping it as the default made the README claim a delivery mode the host
|
|
143
|
+
// never gave. `direct` stays available as an experiment for a build that honours it,
|
|
144
|
+
// and the shutdown line reports both halves: `tools_codemode` (sent) and
|
|
145
|
+
// `tools_in_request` (what the assembled request actually carried).
|
|
146
|
+
const v2CodeModeDirect = /^(1|true|yes|on|direct)$/i.test(String(v2Env.TM_V2_CODEMODE ?? "").trim());
|
|
147
|
+
// No SDK client in the v1 sense. v1's `client` carried `file.read`, `find.text`,
|
|
148
|
+
// `session.*` and `pty.*`; the v2 plugin ctx has no such object, and the fs shim
|
|
149
|
+
// that used to stand in for the first two went away with `tm_read`/`tm_grep` (the
|
|
150
|
+
// native tools, governed at `execute.after`, are the file ladder now).
|
|
151
|
+
// What IS bridged is the one thing the collect path cannot be honest without:
|
|
152
|
+
// `tm_join` must be able to check whether a named session id is really the
|
|
153
|
+
// caller's child, and a live 2.0.16 run proved that without it the tool reports
|
|
154
|
+
// "no pending dispatch matched" about a child that already finished. v2 has
|
|
155
|
+
// `ctx.session.get`, so `v2-session-client.ts` wraps it in the shape the existing
|
|
156
|
+
// code reads. `session.children` has no counterpart, so adoption from the host's
|
|
157
|
+
// tree keeps reporting `no_seam` rather than implying we looked.
|
|
158
|
+
const sessionReader = createV2SessionReader(ctx);
|
|
159
|
+
const tmRuntime = await createTmTools({ directory, project: "", client: sessionReader.client, $: undefined }, {
|
|
160
|
+
mode: envProtectMode,
|
|
161
|
+
extra: envProtectExtra,
|
|
162
|
+
env: v2Env,
|
|
163
|
+
// The matrix is built from observations that do not exist yet at this line
|
|
164
|
+
// (the probe, the guards, the offloader all register below), so it is a
|
|
165
|
+
// LATE-BOUND closure rather than a snapshot taken too early — a row read
|
|
166
|
+
// before the observation would be exactly the "declared" lie this table
|
|
167
|
+
// exists to avoid.
|
|
168
|
+
capabilities: () => v2Matrix(),
|
|
169
|
+
// The LEDGER's home. v2 has no `todowrite` and no `session.todo` to
|
|
170
|
+
// read, so the lead's list lives in the host's own storage domain and
|
|
171
|
+
// `tm_join`'s goal tripwire checks THAT — see src/tm/ledger.ts. When the
|
|
172
|
+
// domain is missing the tool refuses with the reason rather than keeping
|
|
173
|
+
// the list in process memory and calling it recorded.
|
|
174
|
+
ledgerStore: createStorageLedgerStore(ctx.storage),
|
|
175
|
+
sessionReaderReport: () => sessionReader.report,
|
|
176
|
+
});
|
|
177
|
+
let v2Matrix = () => [];
|
|
178
|
+
const registrations = [];
|
|
179
|
+
const notes = [];
|
|
180
|
+
// ---------- register the governed tools ----------
|
|
181
|
+
const entries = Object.entries(tmRuntime.tools);
|
|
182
|
+
// v1 keeps tm_ptc_run; v2 does not register it. The host's own `execute`
|
|
183
|
+
// (Code Mode) already runs "one program, N governed calls, zero round-trips,
|
|
184
|
+
// only the aggregate entering the context", and a live session showed our
|
|
185
|
+
// governed results still come back offloaded through it — so shipping a
|
|
186
|
+
// second batch runner would hand the model two tools for one job. The
|
|
187
|
+
// module stays in the tree because the frozen v1 personality needs it, and
|
|
188
|
+
// the prompts the v2 config files carry are forked by gen-v2-config.mjs.
|
|
189
|
+
const V2_UNREGISTERED = V1_ONLY_TOOLS;
|
|
190
|
+
const bindings = [];
|
|
191
|
+
const derived = [];
|
|
192
|
+
for (const [name, def] of entries) {
|
|
193
|
+
if (V2_UNREGISTERED.has(name))
|
|
194
|
+
continue;
|
|
195
|
+
const bound = await bindV2Tool(name, def, directory, (agent, sessionID) => scope.learn(agent, sessionID), {
|
|
196
|
+
codemodeDirect: v2CodeModeDirect,
|
|
197
|
+
});
|
|
198
|
+
if (!bound) {
|
|
199
|
+
notes.push(`${name}: 没有 execute,未注册`);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (!bound.inputExact)
|
|
203
|
+
notes.push(`${name}: 参数表不精确(${bound.note ?? "未知原因"})`);
|
|
204
|
+
else if (bound.inputSource === "descriptor")
|
|
205
|
+
derived.push(name);
|
|
206
|
+
bindings.push(bound);
|
|
207
|
+
}
|
|
208
|
+
// A descriptor-derived table is `exact` in the sense that the model sees the
|
|
209
|
+
// parameter names, but the TYPES were read off a `name: type` prefix by
|
|
210
|
+
// regex, not by zod. Saying nothing would let the log imply a zod-grade
|
|
211
|
+
// shape, so name the tools and say how they were obtained.
|
|
212
|
+
const retired = entries.map(([name]) => name).filter((n) => V2_UNREGISTERED.has(n));
|
|
213
|
+
if (retired.length) {
|
|
214
|
+
// Deliberate absences are said, not silently missing: an agent that reads a
|
|
215
|
+
// v1 document and finds no `tm_read` needs to learn from the boot log that
|
|
216
|
+
// this is the design, not a failed install.
|
|
217
|
+
notes.push(`v2 不注册 ${retired.join("/")} —— 同一件工作交给宿主自己的 read/grep/shell/execute(结果照样被 execute.after 治理),v1 保留这些别名`);
|
|
218
|
+
}
|
|
219
|
+
if (derived.length) {
|
|
220
|
+
notes.push(`${derived.join("/")} 没有 zod 形状,参数表是从描述符文本推出来的(类型靠 name: type 前缀判定,不是 zod 保证)`);
|
|
221
|
+
}
|
|
222
|
+
// How our tools reach the model at all — said every boot, because the two
|
|
223
|
+
// answers differ by thousands of tokens and by whether our governance text
|
|
224
|
+
// arrives at all. Both branches describe only what WE send: on 2.0.16 a live
|
|
225
|
+
// session showed `options.codemode:false` does not actually move the tools out of
|
|
226
|
+
// the Code Mode catalog, so the outcome is verified from the shutdown line's
|
|
227
|
+
// `tools_in_request` (or tm_stats), never from this flag.
|
|
228
|
+
notes.push(v2CodeModeDirect
|
|
229
|
+
? "TM_V2_CODEMODE=direct:我们发了 options.codemode=false。宿主是否因此改成直接交付,要看 tm_stats 的 tools_in_request(2.0.16 实测:仍然只在 Code Mode 目录里,模型可直接调用的是那九个原生工具)"
|
|
230
|
+
: "tm_* 未带 options.codemode=false:宿主把它们放进 Code Mode 目录,模型看到的只有描述首行(≤120 字)——我们写在下面的治理文案大部分没送到。2.0.16 实测:TM_V2_CODEMODE=direct 也不改变这一点,所以默认留在此状态,真实交付情况以 tools_in_request 为准");
|
|
231
|
+
registrations.push(await ctx.tool.transform((editor) => {
|
|
232
|
+
for (const b of bindings)
|
|
233
|
+
editor.add(b.tool);
|
|
234
|
+
}));
|
|
235
|
+
// Did they actually land? Ask the host, do not assume — the whole reason
|
|
236
|
+
// this plugin has a capability probe is that "registered" and "live" are
|
|
237
|
+
// different facts on this platform.
|
|
238
|
+
let registeredNames = [];
|
|
239
|
+
try {
|
|
240
|
+
const live = (await ctx.tool.list());
|
|
241
|
+
const ids = new Set(live.map((t) => String(t.id ?? t.name ?? "")));
|
|
242
|
+
registeredNames = bindings.map((b) => b.tool.name).filter((n) => ids.has(n));
|
|
243
|
+
}
|
|
244
|
+
catch (err) {
|
|
245
|
+
notes.push(`tool.list 失败:${err?.message ?? String(err)}`);
|
|
246
|
+
}
|
|
247
|
+
const missing = bindings.map((b) => b.tool.name).filter((n) => !registeredNames.includes(n));
|
|
248
|
+
// ---------- normalize the agents the config declares ----------
|
|
249
|
+
// ---------- the permission guard (egress red line + R6 per command) ----------
|
|
250
|
+
// Installed BEFORE the agent transform because whether the config still needs
|
|
251
|
+
// the coarse `shell -> ask` escalation depends on this hook being there.
|
|
252
|
+
const guards = await applyV2PermissionGuards(ctx, { envProtectMode, scope });
|
|
253
|
+
// Said out loud because it is a promise with a boundary: the user's rule is
|
|
254
|
+
// that nothing outside Team may be touched, and the honest consequence is that
|
|
255
|
+
// the red lines we inject are therefore NOT protecting a build session either.
|
|
256
|
+
notes.push("已按 Team 作用域收口:工具面裁剪、温度、黑板提示、原生结果卸载、R6 与地址红线、subagent 强制后台,都只对我们六个角色的会话生效;build / plan 等其余模式保持宿主出厂行为(代价:那些会话也不由我们补红线,需要全局红线请让宿主自己的 permission 规则承担)");
|
|
257
|
+
registrations.push(...guards.registrations);
|
|
258
|
+
const bgForce = await applyV2BackgroundForce(ctx, { scope });
|
|
259
|
+
registrations.push(...bgForce.registrations);
|
|
260
|
+
// JIT governance over the HOST's tools, so the offload promise does not depend
|
|
261
|
+
// on which tool the model happened to pick (see v2-offload.ts for why a
|
|
262
|
+
// per-tool promise is no promise). Registered BEFORE the probe so the probe's
|
|
263
|
+
// throttled snapshot can carry its counters — a counter that only lands at
|
|
264
|
+
// teardown is a counter that does not exist.
|
|
265
|
+
const offload = await applyV2NativeOffload(ctx, { pipelines: tmRuntime.pipelines, scope });
|
|
266
|
+
registrations.push(...(await Promise.all(offload.registrations)));
|
|
267
|
+
// The host's own sub-agents, entered into tm_join's registry (decision 4,
|
|
268
|
+
// 2026-09-26). On v2 every dispatch IS the host's `subagent` tool, so without this
|
|
269
|
+
// the collect side had nothing to collect: tm_join answered 没有待收集的派发 about
|
|
270
|
+
// children the user could see on screen, and the settle events the feed forwards
|
|
271
|
+
// were dropped because the dispatcher ignores ids it has never heard of.
|
|
272
|
+
const hostChildren = applyV2SubagentRegistry(ctx, {
|
|
273
|
+
register: tmRuntime.registerHostChild,
|
|
274
|
+
scope,
|
|
275
|
+
});
|
|
276
|
+
registrations.push(...(await Promise.all(hostChildren.registrations)));
|
|
277
|
+
if (!hostChildren.active) {
|
|
278
|
+
notes.push("宿主 subagent 的认领没装上(ctx.tool.hook 不可用):后台子代理会回到 tm_join 看不见的状态");
|
|
279
|
+
}
|
|
280
|
+
// The settle path. Measured on 2.0.18: a child's own `session.idle` never reaches a
|
|
281
|
+
// plugin subscriber, and the parent-idle presumption cannot fire mid-turn, so a child
|
|
282
|
+
// whose report had already been injected stayed 运行中. The host's completion envelope
|
|
283
|
+
// IS the observation, so watch the two session seams that can carry it and count both.
|
|
284
|
+
const completion = applyV2CompletionWatch(ctx, {
|
|
285
|
+
settle: tmRuntime.settleHostChild,
|
|
286
|
+
hasOpen: tmRuntime.hasOpenHostChild,
|
|
287
|
+
scope,
|
|
288
|
+
});
|
|
289
|
+
registrations.push(...(await Promise.all(completion.registrations)));
|
|
290
|
+
if (!completion.active) {
|
|
291
|
+
notes.push("子代理完成注入的监听没装上(ctx.session.hook 不可用):tm_join 会把已完成的后台子代理报成运行中");
|
|
292
|
+
}
|
|
293
|
+
// BEFORE the session layer registers its own context hook, so the probe sees
|
|
294
|
+
// the host's full surface rather than the set we trimmed — that difference is
|
|
295
|
+
// exactly what it is there to record.
|
|
296
|
+
// The host's own browser catalog, behind OUR gate (#9): `permission.evaluate` was
|
|
297
|
+
// observed NOT firing for `browser_*`, so until now the domain list, the address
|
|
298
|
+
// red line and the R6 env-file rule had a bypass the length of a tool name — in
|
|
299
|
+
// the one surface that is also the only one with a native side panel.
|
|
300
|
+
const browserGate = applyV2BrowserGate(ctx, {
|
|
301
|
+
allowlist: seedWebfetchDomains(tmRuntime.config.webfetchAllowedDomains),
|
|
302
|
+
scope,
|
|
303
|
+
env: v2Env,
|
|
304
|
+
});
|
|
305
|
+
if (!browserGate.registrations.length) {
|
|
306
|
+
notes.push("原生 browser_* 的门禁没装上(ctx.tool.hook 不可用):宿主的 45 个浏览器工具在 Team 会话里也不受我们的域名 / 地址规则约束");
|
|
307
|
+
}
|
|
308
|
+
registrations.push(...browserGate.registrations);
|
|
309
|
+
const probe = await applyV2Probe(ctx, {
|
|
310
|
+
onSummary: (summary) => {
|
|
311
|
+
try {
|
|
312
|
+
tmRuntime.pipelines.store.appendTrajectory({
|
|
313
|
+
tool: "host",
|
|
314
|
+
step_id: "v2-surface",
|
|
315
|
+
event: "personality",
|
|
316
|
+
api: 2,
|
|
317
|
+
native_seen: offload.report.seen,
|
|
318
|
+
// The coverage denominator (#23): every call we resolved to a Team
|
|
319
|
+
// session, and the part of it whose tool we do not touch. Printed by
|
|
320
|
+
// tm_stats, so "most tool calls are JIT-governed" is checkable.
|
|
321
|
+
native_ours: offload.report.ours,
|
|
322
|
+
native_unmatched: offload.report.unmatched,
|
|
323
|
+
native_offloaded: offload.report.offloaded,
|
|
324
|
+
native_envelopes: offload.report.envelopes,
|
|
325
|
+
native_tokens_saved: offload.report.tokensSaved,
|
|
326
|
+
native_offload_active: offload.active && offload.registrations.length > 0,
|
|
327
|
+
host_subagent_seen: hostChildren.report.seen,
|
|
328
|
+
host_children_registered: hostChildren.report.registered,
|
|
329
|
+
host_children_unpaired: hostChildren.report.unpaired,
|
|
330
|
+
// The session seam rides the throttled snapshot too, not only teardown: a run
|
|
331
|
+
// that is interrupted never reaches dispose, and "did ctx.session.context
|
|
332
|
+
// answer" is exactly the fact an interrupted round is there to establish.
|
|
333
|
+
session_get_attempts: sessionReader.report.attempted,
|
|
334
|
+
session_get_shape: sessionReader.report.usedShape ?? "(none)",
|
|
335
|
+
session_get_resolved: sessionReader.report.resolved,
|
|
336
|
+
session_context_ok: sessionReader.report.contextOk,
|
|
337
|
+
session_context_keys: sessionReader.report.contextKeys.join(","),
|
|
338
|
+
completion_context_fired: completion.report.contextFired,
|
|
339
|
+
completion_skipped: completion.report.skipped,
|
|
340
|
+
completion_settled: completion.report.settled,
|
|
341
|
+
...summary,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
/* the probe is an extra */
|
|
346
|
+
}
|
|
347
|
+
},
|
|
348
|
+
});
|
|
349
|
+
registrations.push(...probe.registrations);
|
|
350
|
+
if (probe.report.hooksMissing.length) {
|
|
351
|
+
notes.push(`探针没挂上的钩子:${probe.report.hooksMissing.join(", ")}`);
|
|
352
|
+
}
|
|
353
|
+
// Three states, three sentences: "no seam", "you turned it off" and "it is
|
|
354
|
+
// running" are different facts for the user, and collapsing them is how a
|
|
355
|
+
// fallback gets read as a setting.
|
|
356
|
+
if (offload.registrations.length === 0) {
|
|
357
|
+
notes.push('这个宿主没有 tool.hook("execute.after"):原生 read/shell 的大输出不会被卸载,JIT 承诺只在 tm_* 上成立');
|
|
358
|
+
}
|
|
359
|
+
else if (!offload.active) {
|
|
360
|
+
notes.push("TM_NATIVE_OFFLOAD=off:原生工具的大输出按宿主原样进上下文(承诺退回 tm_* 范围)");
|
|
361
|
+
}
|
|
362
|
+
if (!guards.installed) {
|
|
363
|
+
notes.push("ctx.permission.hook 不存在:原生 webfetch 的元数据/私网红线和 R6 的按命令行判定都没地方落");
|
|
364
|
+
}
|
|
365
|
+
const missingAgents = [];
|
|
366
|
+
const unmappedActions = new Set();
|
|
367
|
+
const wantedIds = Object.keys(agents);
|
|
368
|
+
// R6 on v2: the plugin cannot raise a dialog (probed), but an `ask` EFFECT
|
|
369
|
+
// the host evaluates DOES open one. A live `--standalone` run recorded
|
|
370
|
+
// `{action:"shell", resourceCount:1}` reaching `permission.evaluate` for a real
|
|
371
|
+
// command, so the per-command classifier is now in charge and the config keeps
|
|
372
|
+
// the COARSE escalation (every command asks) only as the fallback:
|
|
373
|
+
// `TM_R6_FINE_ASK=off`, or no hook installed to hand the decision to.
|
|
374
|
+
const escalateShellAsk = envProtectMode !== "off" && needsCoarseShellAsk(process.env, guards.installed);
|
|
375
|
+
// Team is ALWAYS the default (the user's standing instruction — see the
|
|
376
|
+
// header for why this cannot be the conservative v1 form). `default()` on
|
|
377
|
+
// an agent that does not exist would just make the host fall back to build
|
|
378
|
+
// silently, so it is gated on the role actually being present, and the
|
|
379
|
+
// refusal to promote is reported rather than hidden.
|
|
380
|
+
const promoteTeamDefault = options.defaultAgent !== false;
|
|
381
|
+
let defaultPromoted = null;
|
|
382
|
+
let defaultPostCheck = "not-attempted";
|
|
383
|
+
if (typeof ctx.agent?.transform === "function") {
|
|
384
|
+
registrations.push(await ctx.agent.transform((editor) => {
|
|
385
|
+
for (const id of wantedIds) {
|
|
386
|
+
const existing = editor.get(id);
|
|
387
|
+
if (!existing) {
|
|
388
|
+
missingAgents.push(id);
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
const { triples, unmapped } = triplesFromAgentPermission(agents[id]?.permission, { escalateShellAsk, agentName: id });
|
|
392
|
+
for (const u of unmapped)
|
|
393
|
+
unmappedActions.add(u);
|
|
394
|
+
// `V1_ONLY_TOOLS` is passed as the reclaim set: a triple naming an
|
|
395
|
+
// action this personality never registers can only have come from an
|
|
396
|
+
// earlier boot, and a permission rule for a tool the host has never
|
|
397
|
+
// heard of reads as a capability the user granted.
|
|
398
|
+
const merged = mergeTriples(existing.permissions, triples, V1_ONLY_TOOLS);
|
|
399
|
+
if (merged.changed)
|
|
400
|
+
editor.update(id, (a) => { a.permissions = merged.triples; });
|
|
401
|
+
}
|
|
402
|
+
if (promoteTeamDefault) {
|
|
403
|
+
// `editor.get("team")` returning nothing is NOT evidence the host does
|
|
404
|
+
// not know the role — the live run is literally executing as `team` while
|
|
405
|
+
// this snapshot lists only the 7 built-ins, so the transform receives the
|
|
406
|
+
// agent set from BEFORE the config directory merged. Gating the
|
|
407
|
+
// promotion on that stale look-up is how "Team is always the default"
|
|
408
|
+
// silently stopped being true, so the call is attempted regardless and
|
|
409
|
+
// the AFTER view is what gets recorded.
|
|
410
|
+
let ok = true;
|
|
411
|
+
try {
|
|
412
|
+
editor.default("team");
|
|
413
|
+
}
|
|
414
|
+
catch {
|
|
415
|
+
ok = false;
|
|
416
|
+
}
|
|
417
|
+
defaultPromoted = ok;
|
|
418
|
+
defaultPostCheck = editor.get?.("team") ? "present-after" : "absent-after";
|
|
419
|
+
}
|
|
420
|
+
// Recorded HERE, at the moment of observation. The host defers this
|
|
421
|
+
// callback (a live boot reached the trajectory line with
|
|
422
|
+
// defaultPromoted still null, which printed as a clean-looking "n/a"), so
|
|
423
|
+
// the boot record cannot claim anything about what has not happened yet —
|
|
424
|
+
// least of all `agents_missing: ""`, which reads as "all six roles found"
|
|
425
|
+
// when the truth at that instant was "nobody has looked".
|
|
426
|
+
try {
|
|
427
|
+
tmRuntime.pipelines.store.appendTrajectory({
|
|
428
|
+
tool: "host",
|
|
429
|
+
step_id: "v2-agents",
|
|
430
|
+
event: "personality",
|
|
431
|
+
api: 2,
|
|
432
|
+
agents_missing: missingAgents.join(" "),
|
|
433
|
+
agents_default: defaultPromoted === null ? "opt-out" : defaultPostCheck === "present-after" ? "team" : "called-unverified",
|
|
434
|
+
agents_unmapped: [...unmappedActions].join(" "),
|
|
435
|
+
agents_normalized: true,
|
|
436
|
+
agents_default_post: defaultPostCheck,
|
|
437
|
+
// Is "all six missing" the host having no roles, or the callback
|
|
438
|
+
// firing before it loaded them? Those need different fixes and the
|
|
439
|
+
// difference is not guessable from the outside — the editor's own view
|
|
440
|
+
// is the only witness.
|
|
441
|
+
agents_in_editor: (() => {
|
|
442
|
+
try {
|
|
443
|
+
return (editor.list?.() ?? []).length;
|
|
444
|
+
}
|
|
445
|
+
catch {
|
|
446
|
+
return -1;
|
|
447
|
+
}
|
|
448
|
+
})(),
|
|
449
|
+
agents_editor_ids: (() => {
|
|
450
|
+
try {
|
|
451
|
+
return (editor.list?.() ?? [])
|
|
452
|
+
.map((a) => String(a?.id ?? "?"))
|
|
453
|
+
.filter((n) => wantedIds.includes(n) || n === "build" || n === "plan")
|
|
454
|
+
.join(" ");
|
|
455
|
+
}
|
|
456
|
+
catch {
|
|
457
|
+
return "list-threw";
|
|
458
|
+
}
|
|
459
|
+
})(),
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
catch {
|
|
463
|
+
/* the record is an extra, never a reason to fail the transform */
|
|
464
|
+
}
|
|
465
|
+
}));
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
notes.push("ctx.agent.transform 不存在,角色权限矩阵未规范化");
|
|
469
|
+
}
|
|
470
|
+
if (defaultPromoted === false) {
|
|
471
|
+
notes.push("Team 没有成为默认:editor.default(\"team\") 抛错了");
|
|
472
|
+
}
|
|
473
|
+
else if (defaultPromoted && defaultPostCheck !== "present-after") {
|
|
474
|
+
// Measured on a live 2.0.16 standalone boot: the call is accepted, `team` is
|
|
475
|
+
// absent from the editor before AND after, and `default_agent` in
|
|
476
|
+
// opencode.jsonc keeps the user's value. The plugin therefore cannot promote
|
|
477
|
+
// the default on v2 — the transform receives the agent set from before the
|
|
478
|
+
// config directory merges. Saying so is the whole point: this file used to
|
|
479
|
+
// claim the promotion was live-verified, and the only thing that actually
|
|
480
|
+
// makes Team the default is the installer writing the key.
|
|
481
|
+
notes.push("Team 默认:editor.default(\"team\") 已被宿主接受但**无法核验生效**(transform 拿到的是配置合并前的角色集,实测调用后 editor 里仍无 team)。真正让这个要求成立的是安装器写 `default_agent: \"team\"`——没装过安装器就别假定 Team 是默认。");
|
|
482
|
+
}
|
|
483
|
+
if (unmappedActions.size) {
|
|
484
|
+
notes.push(`白名单里这些键在 v2 没有对应动作,已如实丢弃:${[...unmappedActions].join(",")}`);
|
|
485
|
+
}
|
|
486
|
+
if (escalateShellAsk) {
|
|
487
|
+
notes.push(guards.installed
|
|
488
|
+
? "R6 已开,但 TM_R6_FINE_ASK=off 显式要回粗粒度:shell 在配置里升为 ask(每条命令都问),按命令行判定的 evaluate 钩子不再决定这件事。"
|
|
489
|
+
: "R6 已开,但这个宿主没给 permission.hook:按命令行的红线无处可挂,只能把 shell 整体升为 ask(每条命令都问)——这是退路,不是设计。");
|
|
490
|
+
}
|
|
491
|
+
// ---------- the request layer ----------
|
|
492
|
+
// The whitelist was only ever a DENY, which stops a call but leaves the
|
|
493
|
+
// tool's description in every request. Here it becomes a request-level
|
|
494
|
+
// removal, and the two things v1 baked into the team prompt (the resolved
|
|
495
|
+
// board root) or into the agent config (temperature) ride the request
|
|
496
|
+
// instead — a v2 agent is a config FILE and cannot carry a per-workspace
|
|
497
|
+
// path, and `temperature` is a legacy agent field the runner no longer
|
|
498
|
+
// sends. This also starts the blackboard TTL sweeper, which v1 owns and v2
|
|
499
|
+
// otherwise silently lacked: the note promises a sweep, so shipping it
|
|
500
|
+
// without the sweeper would be the exact overstated claim this product is
|
|
501
|
+
// built to refuse.
|
|
502
|
+
const ttlMs = resolveTtlMs(options);
|
|
503
|
+
const ttlDays = Math.round(ttlMs / (24 * 60 * 60 * 1000)) || DEFAULT_TTL_DAYS;
|
|
504
|
+
const note = blackboardNote(startBlackboardMaintenance(directory, ttlMs), ttlDays);
|
|
505
|
+
const temperature = options.temperature === false
|
|
506
|
+
? false
|
|
507
|
+
: typeof options.temperature === "number"
|
|
508
|
+
? options.temperature
|
|
509
|
+
: 0.2;
|
|
510
|
+
const plan = removalPlan(agents);
|
|
511
|
+
// ---------- does ctx.storage actually round-trip? ----------
|
|
512
|
+
// The LEDGER needs somewhere to live and `ctx.storage` is the candidate; the
|
|
513
|
+
// difference between "the domain exists" and "a value we wrote comes back"
|
|
514
|
+
// is not something a type file can settle, so boot asks it one question with a
|
|
515
|
+
// key that carries no user data and records the answer.
|
|
516
|
+
const storageProbe = { state: "absent" };
|
|
517
|
+
{
|
|
518
|
+
const st = ctx?.storage;
|
|
519
|
+
if (st && typeof st.set === "function" && typeof st.get === "function") {
|
|
520
|
+
const key = "team-mode/selfcheck";
|
|
521
|
+
const marker = `boot-${process.pid}`;
|
|
522
|
+
try {
|
|
523
|
+
await st.set(key, { marker, at: Date.now() });
|
|
524
|
+
const back = await st.get(key);
|
|
525
|
+
const got = back?.value?.marker ?? back?.marker;
|
|
526
|
+
storageProbe.state = got === marker ? "round-trip" : "read-back-mismatch";
|
|
527
|
+
if (got !== marker)
|
|
528
|
+
storageProbe.detail = `写回的值不是刚写进去的那个(拿到 ${JSON.stringify(got)?.slice(0, 40)})`;
|
|
529
|
+
}
|
|
530
|
+
catch (err) {
|
|
531
|
+
storageProbe.state = "threw";
|
|
532
|
+
storageProbe.detail = String(err?.message ?? err).slice(0, 80);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
const session = await applyV2SessionLayer(ctx, { temperature, note, noteAgents: ["team"], plan, scope });
|
|
537
|
+
// Everything the matrix reports now exists, so the late-bound closure can be
|
|
538
|
+
// pointed at the real observations.
|
|
539
|
+
v2Matrix = () => v2CapabilityRows({
|
|
540
|
+
ctx,
|
|
541
|
+
probe,
|
|
542
|
+
guardsInstalled: guards.installed,
|
|
543
|
+
backgroundForced: true,
|
|
544
|
+
offload,
|
|
545
|
+
sessionHooks: 2,
|
|
546
|
+
temperature,
|
|
547
|
+
// Both derived, not asserted: "we think v2 has no X" has to come from the
|
|
548
|
+
// object we were handed, so a host that grows the seam shows up here
|
|
549
|
+
// automatically instead of leaving a stale "missing" row forever.
|
|
550
|
+
// tm_join reads the todo list through the SDK client, and this personality
|
|
551
|
+
// passes none — so the row is false until #8 bridges `ctx.session` into the
|
|
552
|
+
// path the tool actually consumes. Deriving it from ctx rather than
|
|
553
|
+
// hard-coding `false` is what keeps that honest either way.
|
|
554
|
+
eventFeed: feed.report,
|
|
555
|
+
hasTodoSeam: typeof ctx.session?.todo === "function",
|
|
556
|
+
hasAsk: typeof ctx.tool === "function",
|
|
557
|
+
storageState: storageProbe.state,
|
|
558
|
+
scope,
|
|
559
|
+
});
|
|
560
|
+
registrations.push(...session.registrations);
|
|
561
|
+
if (!session.registrations.length) {
|
|
562
|
+
notes.push("ctx.session.hook 不存在:工具面裁剪、温度、黑板根目录三项请求层治理都没装上");
|
|
563
|
+
}
|
|
564
|
+
// v1 handed the plugin a host `event` hook and pumped every session.idle /
|
|
565
|
+
// session.error / session.status into the child registry. This personality had
|
|
566
|
+
// no subscription at all (#8), so a child that settled two seconds after the
|
|
567
|
+
// dispatch stayed "running" for the whole wait budget and `tm_join` reported a
|
|
568
|
+
// state it had never observed — goal #6's failure shape, and a bug rather than
|
|
569
|
+
// a limitation now that `ctx.event.subscribe()` is measured to work.
|
|
570
|
+
const feed = await applyV2EventFeed(ctx, { onEvent: (ev) => tmRuntime.observeDispatchEvent(ev) });
|
|
571
|
+
if (!feed.report.active) {
|
|
572
|
+
notes.push(`事件流没接通(${feed.report.stopped ?? "原因未知"}):tm_join 的结算检测只剩等待预算内的轮询,子代理结算了也要等到超时才报告`);
|
|
573
|
+
}
|
|
574
|
+
const removedPlanSizes = [...plan.entries()].map(([id, set]) => `${id}=${set.size}`).join(" ");
|
|
575
|
+
try {
|
|
576
|
+
tmRuntime.pipelines.store.appendTrajectory({
|
|
577
|
+
tool: "host",
|
|
578
|
+
step_id: "v2-boot",
|
|
579
|
+
event: "personality",
|
|
580
|
+
api: 2,
|
|
581
|
+
directory,
|
|
582
|
+
tools_registered: registeredNames.length,
|
|
583
|
+
tools_v1_only: [...V2_UNREGISTERED].join(","),
|
|
584
|
+
tools_total: bindings.length,
|
|
585
|
+
tools_missing: missing.join(","),
|
|
586
|
+
agents_missing: missingAgents.length ? missingAgents.join(",") : defaultPromoted === null ? "待观察" : "",
|
|
587
|
+
agents_default: defaultPromoted === null ? "待观察(宿主异步调用 transform,见 v2-agents 行)" : defaultPromoted ? "team" : "not-promoted",
|
|
588
|
+
request_hooks: session.registrations.length,
|
|
589
|
+
guard_hooks: guards.registrations.length,
|
|
590
|
+
storage_probe: storageProbe.state,
|
|
591
|
+
storage_probe_detail: storageProbe.detail ?? "",
|
|
592
|
+
// Read at teardown, so these are counts of what actually happened in this
|
|
593
|
+
// process — the number that says whether the isolation was exercised at all.
|
|
594
|
+
scope_ours: scope.report.ours,
|
|
595
|
+
scope_foreign: scope.report.foreign,
|
|
596
|
+
scope_unknown: scope.report.unknown,
|
|
597
|
+
browser_gate: browserGate.registrations.length ? "armed" : "absent",
|
|
598
|
+
private_space: privateSpace,
|
|
599
|
+
tools_codemode: v2CodeModeDirect ? "direct" : "catalog",
|
|
600
|
+
guard_foreign_skipped: guards.report.foreignSkipped,
|
|
601
|
+
subagent_background: bgForce.registrations.length ? "forced-true" : "no-hook",
|
|
602
|
+
event_feed: feed.report.active ? "subscribed" : "absent",
|
|
603
|
+
event_feed_detail: feed.report.stopped ?? "",
|
|
604
|
+
guard_shell_coarse: escalateShellAsk,
|
|
605
|
+
request_temperature: temperature === false ? "off" : temperature,
|
|
606
|
+
request_removed_plan: removedPlanSizes,
|
|
607
|
+
board_ttl_days: ttlDays,
|
|
608
|
+
env_protect: envProtectMode,
|
|
609
|
+
note: notes.join(" · "),
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
catch {
|
|
613
|
+
/* the trajectory is an extra here, never a reason to fail the boot */
|
|
614
|
+
}
|
|
615
|
+
// A v1 user reading their own logs would otherwise conclude the plugin was
|
|
616
|
+
// silently broken: these two facts are the visible difference between the
|
|
617
|
+
// personalities.
|
|
618
|
+
if (missingAgents.length || missing.length || notes.length) {
|
|
619
|
+
const detail = [
|
|
620
|
+
missingAgents.length ? `配置里缺角色:${missingAgents.join(",")}(v2 插件不能新建角色,交给安装器)` : "",
|
|
621
|
+
missing.length ? `工具未出现在宿主表面:${missing.join(",")}` : "",
|
|
622
|
+
notes.length ? notes.join(" · ") : "",
|
|
623
|
+
].filter(Boolean).join(";");
|
|
624
|
+
console.warn(`[team-mode] v2 人格已启动,但有缺口:${detail}`);
|
|
625
|
+
}
|
|
626
|
+
return async () => {
|
|
627
|
+
// The counters only exist in this process, so they have to be written down
|
|
628
|
+
// before teardown — otherwise "did permission.evaluate ever fire for
|
|
629
|
+
// shell?" (#12, the thing gating the fine-grained R6 ask) is unanswerable
|
|
630
|
+
// after the fact, and tm_stats would keep showing a boot line that says
|
|
631
|
+
// what was installed but never what it saw.
|
|
632
|
+
try {
|
|
633
|
+
tmRuntime.pipelines.store.appendTrajectory({
|
|
634
|
+
tool: "host",
|
|
635
|
+
step_id: "v2-shutdown",
|
|
636
|
+
event: "personality",
|
|
637
|
+
api: 2,
|
|
638
|
+
guard_seen: guards.report.seen,
|
|
639
|
+
guard_actions: Object.entries(guards.report.byAction).map(([k, v]) => `${k}=${v}`).join(" "),
|
|
640
|
+
guard_shell_matched: guards.report.shellMatched,
|
|
641
|
+
guard_strictened: guards.report.strictened,
|
|
642
|
+
guard_denied: guards.report.denied,
|
|
643
|
+
subagent_seen: bgForce.report.seen,
|
|
644
|
+
subagent_forced: bgForce.report.forced,
|
|
645
|
+
tools_removed: Object.entries(session.report.removed).map(([k, v]) => `${k}=${v}`).join(" "),
|
|
646
|
+
note_pushed: session.report.notePushed,
|
|
647
|
+
compaction_lines: session.report.compactionLines,
|
|
648
|
+
// Accumulates for the whole process, so it can only be written at
|
|
649
|
+
// teardown: this is the number that answers "did the feed stay alive, and
|
|
650
|
+
// did the host rename the event types under us after the last upgrade?"
|
|
651
|
+
event_received: feed.report.received,
|
|
652
|
+
event_forwarded: feed.report.forwarded,
|
|
653
|
+
event_unknown_types: Object.entries(feed.report.unknown).map(([k, v]) => `${k}=${v}`).join(" "),
|
|
654
|
+
event_stopped: feed.report.stopped ?? "",
|
|
655
|
+
browser_gate_seen: browserGate.report.seen,
|
|
656
|
+
browser_gate_refused: browserGate.report.refused,
|
|
657
|
+
browser_gate_leaked: browserGate.report.leaked,
|
|
658
|
+
browser_gate_held: browserGate.report.held,
|
|
659
|
+
browser_gate_note: browserGateSummary(browserGate.report),
|
|
660
|
+
// What the model could actually call, read off the assembled requests —
|
|
661
|
+
// the half of the delivery story the sent flag cannot tell.
|
|
662
|
+
tools_in_request: session.report.tmInRequestSurface ? "tm-in-request" : "catalog-only",
|
|
663
|
+
native_capped: offload.report.capped,
|
|
664
|
+
native_report_capped: offload.report.reportCapped,
|
|
665
|
+
// Did the collect side see the host's children at all? A zero here with a
|
|
666
|
+
// non-zero `seen` is the tm_join gap still open, and that is the difference
|
|
667
|
+
// between "nothing was dispatched" and "we were deaf to it".
|
|
668
|
+
host_subagent_seen: hostChildren.report.seen,
|
|
669
|
+
host_children_registered: hostChildren.report.registered,
|
|
670
|
+
host_children_unpaired: hostChildren.report.unpaired,
|
|
671
|
+
host_children_no_id: hostChildren.report.noChildId,
|
|
672
|
+
host_children_settled_at_once: hostChildren.report.settledAtOnce,
|
|
673
|
+
// The session-read seam, recorded rather than assumed: this bridge failed for a
|
|
674
|
+
// whole release cycle because the argument shape was guessed, and a guess that
|
|
675
|
+
// fails looks identical to a host that has no such method.
|
|
676
|
+
session_get_attempts: sessionReader.report.attempted,
|
|
677
|
+
session_get_shape: sessionReader.report.usedShape ?? "(none)",
|
|
678
|
+
session_get_resolved: sessionReader.report.resolved,
|
|
679
|
+
session_context_ok: sessionReader.report.contextOk,
|
|
680
|
+
session_context_keys: sessionReader.report.contextKeys.join(","),
|
|
681
|
+
session_get_error: sessionReader.report.lastError ?? sessionReader.report.contextError ?? "",
|
|
682
|
+
completion_context_fired: completion.report.contextFired,
|
|
683
|
+
completion_prompt_fired: completion.report.promptFired,
|
|
684
|
+
completion_skipped: completion.report.skipped,
|
|
685
|
+
completion_envelopes: completion.report.seen,
|
|
686
|
+
completion_settled: completion.report.settled,
|
|
687
|
+
...probeSummary(probe.report),
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
catch {
|
|
691
|
+
/* the trajectory is an extra, never a reason to fail teardown */
|
|
692
|
+
}
|
|
693
|
+
for (const r of registrations) {
|
|
694
|
+
try {
|
|
695
|
+
await r.dispose();
|
|
696
|
+
}
|
|
697
|
+
catch {
|
|
698
|
+
/* a host that already tore down its own registry has nothing left to free */
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
// tm_browser owns a child process — teardown waits on v1 and must wait here too.
|
|
702
|
+
await tmRuntime.dispose();
|
|
703
|
+
};
|
|
704
|
+
},
|
|
705
|
+
};
|
|
706
|
+
//# sourceMappingURL=v2.js.map
|