@te-river/opencode-team-mode 1.5.0 → 1.5.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.
@@ -0,0 +1,97 @@
1
+ /**
2
+ * JIT layer-2 tools (T1.2 + T1.3) — configuration surface.
3
+ *
4
+ * Every knob resolves from the process environment with a typed default;
5
+ * invalid values never crash plugin startup, they fall back to the default
6
+ * (same fail-soft philosophy as blackboard.resolveTtlMs and
7
+ * envprotect.resolveEnvProtectMode).
8
+ *
9
+ * Token 口径 (estimate basis): tokens ≈ chars/4, ceil. This is a rough
10
+ * ASCII/code calibration; CJK text is under-counted by it (one CJK char is
11
+ * closer to a full token than to 0.25). Two design choices keep that honest:
12
+ * the conservative boundary (estimate == threshold still offloads) and the
13
+ * hard preview cap — a mis-estimate can only make governance MORE aggressive,
14
+ * never less. This口径 is pinned by test-tm-tools.mjs.
15
+ *
16
+ * Naming note: TM_BLACKBOARD_DIR (this module, run-payload store under the
17
+ * project root, default `.blackboard/`) is a DIFFERENT artifact from the
18
+ * team blackboard in blackboard.ts (`.git/opencode-team/`, plugin options).
19
+ * They share a philosophy (TTL sweeper is the sole cleanup path), not code.
20
+ */
21
+ /** Default tm_bash read-only allowlist (P3, command-level). */
22
+ export const DEFAULT_BASH_READONLY_ALLOWED = [
23
+ "ls", "cat", "head", "tail", "grep", "rg", "find", "awk", "sort", "uniq",
24
+ "wc", "cut", "dir", "Get-Content", "Get-ChildItem", "Select-String",
25
+ "Measure-Object",
26
+ ];
27
+ export const TM_CONFIG_DEFAULTS = {
28
+ offloadThreshold: 2000,
29
+ previewLines: 20,
30
+ previewMaxTokens: 80,
31
+ fetchMaxLines: 2000,
32
+ blackboardDir: ".blackboard/",
33
+ trajectoryDir: ".trajectory/",
34
+ blackboardTtlDays: 7,
35
+ };
36
+ function envInt(env, key, def, min, max) {
37
+ const raw = env[key];
38
+ if (typeof raw !== "string" || raw.trim() === "")
39
+ return def;
40
+ const n = Number(raw.trim());
41
+ if (!Number.isFinite(n))
42
+ return def;
43
+ const i = Math.trunc(n);
44
+ if (i < min || i > max)
45
+ return def;
46
+ return i;
47
+ }
48
+ function envStr(env, key, def) {
49
+ const raw = env[key];
50
+ if (typeof raw !== "string")
51
+ return def;
52
+ const v = raw.trim();
53
+ return v === "" ? def : v;
54
+ }
55
+ /**
56
+ * Parse `TM_BASH_READONLY_ALLOWED`. Unset/absent -> null (caller applies the
57
+ * default list). An explicitly EMPTY string parses to [] (deny-all bash) —
58
+ * an explicit user choice is respected; separator is comma or semicolon.
59
+ */
60
+ export function parseAllowlistEnv(raw) {
61
+ if (typeof raw !== "string")
62
+ return null;
63
+ return raw.split(/[,;]/).map((s) => s.trim()).filter(Boolean);
64
+ }
65
+ /** Resolve the full tm-tools config from an env-like record (default: process.env). */
66
+ export function resolveTmConfig(env = process.env) {
67
+ const allowlist = parseAllowlistEnv(env.TM_BASH_READONLY_ALLOWED);
68
+ return {
69
+ offloadThreshold: envInt(env, "TM_OFFLOAD_THRESHOLD", TM_CONFIG_DEFAULTS.offloadThreshold, 0, 10_000_000),
70
+ previewLines: envInt(env, "TM_PREVIEW_LINES", TM_CONFIG_DEFAULTS.previewLines, 1, 1000),
71
+ previewMaxTokens: envInt(env, "TM_PREVIEW_MAX_TOKENS", TM_CONFIG_DEFAULTS.previewMaxTokens, 10, 100_000),
72
+ fetchMaxLines: envInt(env, "TM_FETCH_MAX_LINES", TM_CONFIG_DEFAULTS.fetchMaxLines, 1, 1_000_000),
73
+ blackboardDir: envStr(env, "TM_BLACKBOARD_DIR", TM_CONFIG_DEFAULTS.blackboardDir),
74
+ trajectoryDir: envStr(env, "TM_TRAJECTORY_DIR", TM_CONFIG_DEFAULTS.trajectoryDir),
75
+ blackboardTtlDays: envInt(env, "TM_BLACKBOARD_TTL", TM_CONFIG_DEFAULTS.blackboardTtlDays, 1, 365),
76
+ bashReadonlyAllowed: allowlist ?? [...DEFAULT_BASH_READONLY_ALLOWED],
77
+ };
78
+ }
79
+ /**
80
+ * Token estimate — chars/4, ceil (口径 documented above). Empty input = 0.
81
+ */
82
+ export function estimateTokens(text) {
83
+ return Math.ceil((text?.length ?? 0) / 4);
84
+ }
85
+ /**
86
+ * Offload decision. estimate == threshold ALSO offloads — the conservative
87
+ * boundary (HUMAN-pinned spec: "等于阈值也卸载").
88
+ */
89
+ export function shouldOffload(tokens, threshold) {
90
+ return tokens >= threshold;
91
+ }
92
+ /** Collapse whitespace and hard-cap a string for messages / clue lines. */
93
+ export function shorten(text, max) {
94
+ const s = String(text ?? "").replace(/\s+/g, " ").trim();
95
+ return s.length <= max ? s : s.slice(0, Math.max(0, max - 1)) + "…";
96
+ }
97
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/tm/config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * JIT layer-2 tools — permission guards (R1 pipeline).
3
+ *
4
+ * P2 (path level, tm_read / tm_grep scope): read scope = project root +
5
+ * blackboard dir + trajectory dir. Everything else — including `..` escapes
6
+ * and absolute paths outside the scopes — is rejected. Both the target and
7
+ * each scope are fs.realpathSync'd first (fail-closed): a junction/symlink
8
+ * pointing outside the scope defeats purely lexical resolve/relative checks.
9
+ *
10
+ * P3 (command level, tm_bash): a read-only allowlist gates what R6 does not
11
+ * already forbid. The two layers are deliberately non-conflicting:
12
+ * R6 governs what is FORBIDDEN (env dumps, env files — same source);
13
+ * the P3 allowlist governs what is PERMITTED (read-only heads only).
14
+ * Hardening beyond the head token: output redirection, command substitution
15
+ * ($()/backticks/<>()), VAR=value assignment prefixes (BASH_ENV/LD_PRELOAD/
16
+ * PATH borrow an allowlisted head to run arbitrary scripts — fail closed),
17
+ * find -delete/-exec, tail -f and Get-Content/Get-ChildItem -Wait (hang),
18
+ * awk system() and rg --pre are rejected even under allowlisted heads.
19
+ * Quoted metacharacters (`|`, `;`, `>`, single-quoted `$()`) are literal
20
+ * text in both dialects — one shared quote-blanking view keeps segmentation
21
+ * and the redirect/substitution checks from misreading them (see the
22
+ * blankQuoted helpers; one implementation, never two drifting copies).
23
+ */
24
+ export interface AllowVerdict {
25
+ ok: boolean;
26
+ reason?: string;
27
+ suggestion?: string;
28
+ }
29
+ /**
30
+ * Classify a tm_bash command against the read-only allowlist.
31
+ * Every segment head (across && ; | & and newlines) must be allowlisted.
32
+ * Matching is case-insensitive (PowerShell is; sh spellings are unaffected).
33
+ */
34
+ export declare function classifyReadonlyCommand(command: string, allowlist: readonly string[]): AllowVerdict;
35
+ /** True when `target` resolves inside `rootDir` (win32 compares case-insensitively). */
36
+ export declare function isInsideDir(rootDir: string, target: string): boolean;
37
+ export type PathScopeVerdict = {
38
+ ok: true;
39
+ abs: string;
40
+ } | {
41
+ ok: false;
42
+ message: string;
43
+ };
44
+ /**
45
+ * Resolve a requested path against the tool's directory and enforce the P2
46
+ * read scope (project root + extra dirs, i.e. blackboard + trajectory).
47
+ * Junction/symlink hardening (#6): BOTH the target and each scope dir are
48
+ * fs.realpathSync'd before the containment check — a purely lexical
49
+ * resolve/relative is defeated by a junction pointing outside the scope.
50
+ * Fail-closed: an unresolvable target is rejected; a scope dir that cannot
51
+ * be resolved (not created yet) simply cannot contain anything real and is
52
+ * skipped. The error message never echoes env-file style paths beyond the
53
+ * scope verdict itself (R6 rejections upstream carry no path text at all).
54
+ */
55
+ export declare function assertReadablePath(baseDir: string, requested: unknown, extraDirs?: readonly string[]): PathScopeVerdict;
56
+ //# sourceMappingURL=guard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"guard.d.ts","sourceRoot":"","sources":["../../src/tm/guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAMH,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,OAAO,CAAA;IACX,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAuED;;;;GAIG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,SAAS,MAAM,EAAE,GAC3B,YAAY,CAwHd;AAID,wFAAwF;AACxF,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAWpE;AAED,MAAM,MAAM,gBAAgB,GACxB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GACzB;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAA;AAWlC;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,OAAO,EAClB,SAAS,GAAE,SAAS,MAAM,EAAO,GAChC,gBAAgB,CAyBlB"}
@@ -0,0 +1,249 @@
1
+ /**
2
+ * JIT layer-2 tools — permission guards (R1 pipeline).
3
+ *
4
+ * P2 (path level, tm_read / tm_grep scope): read scope = project root +
5
+ * blackboard dir + trajectory dir. Everything else — including `..` escapes
6
+ * and absolute paths outside the scopes — is rejected. Both the target and
7
+ * each scope are fs.realpathSync'd first (fail-closed): a junction/symlink
8
+ * pointing outside the scope defeats purely lexical resolve/relative checks.
9
+ *
10
+ * P3 (command level, tm_bash): a read-only allowlist gates what R6 does not
11
+ * already forbid. The two layers are deliberately non-conflicting:
12
+ * R6 governs what is FORBIDDEN (env dumps, env files — same source);
13
+ * the P3 allowlist governs what is PERMITTED (read-only heads only).
14
+ * Hardening beyond the head token: output redirection, command substitution
15
+ * ($()/backticks/<>()), VAR=value assignment prefixes (BASH_ENV/LD_PRELOAD/
16
+ * PATH borrow an allowlisted head to run arbitrary scripts — fail closed),
17
+ * find -delete/-exec, tail -f and Get-Content/Get-ChildItem -Wait (hang),
18
+ * awk system() and rg --pre are rejected even under allowlisted heads.
19
+ * Quoted metacharacters (`|`, `;`, `>`, single-quoted `$()`) are literal
20
+ * text in both dialects — one shared quote-blanking view keeps segmentation
21
+ * and the redirect/substitution checks from misreading them (see the
22
+ * blankQuoted helpers; one implementation, never two drifting copies).
23
+ */
24
+ import * as fs from "node:fs";
25
+ import * as path from "node:path";
26
+ import { shorten } from "./config.js";
27
+ /**
28
+ * Split a command into shell segments so compound statements and pipelines
29
+ * are checked at every boundary (mirrors envprotect's segmentation).
30
+ */
31
+ function segments(command) {
32
+ return command.split(/&&|\|\||[;\n|&]/);
33
+ }
34
+ /**
35
+ * Quoted-span blanking — ONE shared implementation for every quote-aware
36
+ * view (two prior review rounds both traced back to drifted copies). A
37
+ * quoted span is literal text in bash AND PowerShell, so its metacharacters
38
+ * are neither segment boundaries nor operators.
39
+ * blankQuoted — blanks single- AND double-quoted spans: used for the
40
+ * redirect check and for segmentation (rg "err|warn"
41
+ * src, grep -E "a;b" f must not split on quoted `|;/`).
42
+ * blankSingleQuoted — blanks single-quoted spans ONLY: used by the
43
+ * substitution check, because double quotes still
44
+ * expand $()/backticks in bash (and $( ) is a PS
45
+ * subexpression inside "..."), so blanking them would
46
+ * be a real allowance — single quotes are literal in
47
+ * both dialects (awk '{print $(NF)}' is a field ref).
48
+ */
49
+ const QUOTED_SPAN = /"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/g;
50
+ const SINGLE_QUOTED_SPAN = /'(?:[^'\\]|\\.)*'/g;
51
+ function blankQuoted(text) {
52
+ return text.replace(QUOTED_SPAN, " ");
53
+ }
54
+ function blankSingleQuoted(text) {
55
+ return text.replace(SINGLE_QUOTED_SPAN, " ");
56
+ }
57
+ /**
58
+ * Statement head of one segment: peel subshell/group openers, escaped
59
+ * commands and the `time` keyword until the real command word surfaces (same
60
+ * peeling idea as envprotect). `assignment` reports that a leading
61
+ * `VAR=value` prefix was peeled on the way — the caller must FAIL CLOSED on
62
+ * it: a read-only aggregate never needs an assignment, and BASH_ENV/
63
+ * LD_PRELOAD/PATH prefixes are exactly how arbitrary scripts borrow an
64
+ * allowlisted head (non-interactive bash sources BASH_ENV before running
65
+ * `ls`).
66
+ */
67
+ function headInfo(segment) {
68
+ let s = segment.trim();
69
+ for (;;) {
70
+ const next = s
71
+ .replace(/^[({\\]+/, "")
72
+ .replace(/[)}]+$/, "")
73
+ .replace(/^time\s+/i, "")
74
+ .trim();
75
+ if (next === s)
76
+ break;
77
+ s = next;
78
+ }
79
+ const sp = s.search(/\s/);
80
+ const token = sp === -1 ? s : s.slice(0, sp);
81
+ if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(token)) {
82
+ const rest = sp === -1 ? "" : s.slice(sp).trim();
83
+ const inner = rest ? headInfo(rest) : { head: "", assignment: false };
84
+ return { head: inner.head, assignment: true };
85
+ }
86
+ return { head: token, assignment: false };
87
+ }
88
+ function allowVerdict(reason, suggestion) {
89
+ return { ok: false, reason, suggestion };
90
+ }
91
+ /**
92
+ * Classify a tm_bash command against the read-only allowlist.
93
+ * Every segment head (across && ; | & and newlines) must be allowlisted.
94
+ * Matching is case-insensitive (PowerShell is; sh spellings are unaffected).
95
+ */
96
+ export function classifyReadonlyCommand(command, allowlist) {
97
+ const text = String(command ?? "").trim();
98
+ if (!text) {
99
+ return allowVerdict("空命令", "提供要执行的只读命令");
100
+ }
101
+ // Output redirection is a write path — reject (stream dups like `2>&1` and
102
+ // `2>>&1` are blanked first so legitimate stream merging still passes —
103
+ // blanking BEFORE segmentation also stops the `&` inside `2>&1` from
104
+ // splitting the command into bogus segments).
105
+ const noDup = text.replace(/\d+>&\d+/g, " ");
106
+ // Quoted `>`/`|`/`;` are literal text (grep "a>b", rg "err|warn" src) —
107
+ // blank quoted spans for the redirect check AND for segmentation; every
108
+ // real unquoted metacharacter still matches (no new allowance surface).
109
+ const noQuote = blankQuoted(noDup);
110
+ if (/(>>|>|&>|<>)/.test(noQuote)) {
111
+ return allowVerdict("只读模式禁止输出重定向(>/>>)", "去掉重定向只看输出;需要写文件请向 HUMAN 申请使用内置 bash");
112
+ }
113
+ // Command substitution executes arbitrary commands INSIDE an allowlisted
114
+ // head — the head token never sees it (review probes A1/A2/A3: `ls $(rm
115
+ // -rf x)`, backticks, `cat <(rm …)` all passed the allowlist). A read-only
116
+ // aggregate never needs substitution: fail closed on all of it. Only
117
+ // single-quoted spans are blanked here — single quotes are literal in both
118
+ // dialects (awk '{print $(NF)}' is a field reference, not a call), while
119
+ // double-quoted $( ) still expands (grep "$(x)" f stays rejected).
120
+ if (/\$\(|`|<\(|>\(/.test(blankSingleQuoted(noDup))) {
121
+ return allowVerdict("只读模式禁止命令替换($() / 反引号 / <() / >())", "命令替换会执行任意命令;改用管道组合只读命令,或向 HUMAN 申请");
122
+ }
123
+ const allow = (head) => allowlist.some((a) => a.toLowerCase() === head);
124
+ for (const seg of segments(noQuote)) {
125
+ const { head, assignment } = headInfo(seg);
126
+ // Fail closed on assignment prefixes: peeling used to expose the real
127
+ // head (BASH_ENV=./setup.sh ls -> ls) and let env injection borrow an
128
+ // allowlisted head. No read-only aggregation needs VAR=value.
129
+ if (assignment) {
130
+ return allowVerdict("只读模式禁止环境变量赋值前缀(VAR=value cmd)", "BASH_ENV/LD_PRELOAD/PATH 等赋值前缀可借白名单头执行任意脚本;去掉赋值前缀,或向 HUMAN 申请");
131
+ }
132
+ if (!head)
133
+ continue;
134
+ const base = head.replace(/^.*[\\/]/, "").toLowerCase();
135
+ if (!allow(base)) {
136
+ return allowVerdict(`命令 "${shorten(base, 40)}" 不在 tm_bash 只读白名单内`, "改用白名单只读命令(ls/cat/head/tail/grep/rg/find/awk/sort/uniq/wc/cut/dir/Get-Content/Get-ChildItem/Select-String/Measure-Object),或向 HUMAN 申请批准");
137
+ }
138
+ }
139
+ // Write / execute / hang escapes that ride on allowlisted heads.
140
+ // deq view (review round-4 major): the shell dequotes argv, so `find .
141
+ // "-delete"` really arrives as `-delete` — but the raw-text checks below
142
+ // want whitespace before the flag (a `"`/`\` prefix slipped past) and the
143
+ // blanked view erases in-quote flags entirely. One quote-char-stripped
144
+ // view closes all three; over-rejection is safe (`find . -name "-delete"`
145
+ // also dies — acceptable for a read-only aggregate).
146
+ const deq = text.replace(/["'\\]/g, "");
147
+ if (/\bfind\b/i.test(deq) && /\s-{1,2}(?:delete|exec(?:dir)?|ok(?:dir)?|fls|fprint\w*)\b/i.test(deq)) {
148
+ return allowVerdict("find 的写入/执行参数(-delete/-exec/…)不允许", "find 只用于查找;删除或执行请向 HUMAN 申请");
149
+ }
150
+ if (/\bawk\b/i.test(text) && /\bsystem\s*\(/.test(text)) {
151
+ return allowVerdict("awk 的 system() 调用不允许", "awk 仅做文本聚合;执行外部命令请向 HUMAN 申请");
152
+ }
153
+ // `--pre` only when NOT `--pre-glob` (lookahead requires [\s=] or EOL —
154
+ // a `\b` between "pre" and "-" also matched, wrongly banning --pre-glob).
155
+ if (/--pre(?=[\s=]|$)/i.test(deq)) {
156
+ return allowVerdict("rg --pre(外部预处理命令)不允许", "去掉 --pre;需要预处理请向 HUMAN 申请");
157
+ }
158
+ // Hang escapes: tail -f/--follow, and PowerShell -Wait on Get-Content /
159
+ // Get-ChildItem / its `dir` alias (streams/waits — never returns).
160
+ // Checked on BOTH views: the quote-blanked view keeps `dir "a|b" -Wait`
161
+ // rejected (deq segmentation would split the quoted pipe and strand the
162
+ // -Wait on a non-dir head); the deq view catches in-quote flags the
163
+ // blanked view erases (`tail "-f" x`, `dir "-Wait" x`).
164
+ for (const view of [noQuote, deq]) {
165
+ for (const seg of segments(view)) {
166
+ const tokens = seg.trim().split(/\s+/);
167
+ const head = tokens[0]?.replace(/^.*[\\/]/, "").toLowerCase();
168
+ if (head === "tail") {
169
+ for (const t of tokens.slice(1)) {
170
+ if (t === "-f" || t.startsWith("--follow")) {
171
+ return allowVerdict("tail -f/--follow 会挂起不返回", "改用 tail -n <N> 取末尾若干行");
172
+ }
173
+ }
174
+ }
175
+ else if (head === "get-content" || head === "get-childitem" || head === "dir") {
176
+ // `-wai` still covers PowerShell's unambiguous -Wait prefixes
177
+ // (-wai/-wait) without also matching -WarningAction/-WarningVariable.
178
+ if (tokens.slice(1).some((t) => /^-wai/i.test(t))) {
179
+ return allowVerdict("Get-Content/Get-ChildItem -Wait 会挂起不返回", "去掉 -Wait 取当前内容即可");
180
+ }
181
+ }
182
+ }
183
+ }
184
+ return { ok: true };
185
+ }
186
+ // ---------- P2 path scope ----------
187
+ /** True when `target` resolves inside `rootDir` (win32 compares case-insensitively). */
188
+ export function isInsideDir(rootDir, target) {
189
+ const rootAbs = path.resolve(rootDir);
190
+ const targetAbs = path.resolve(target);
191
+ const rel = path.relative(rootAbs, targetAbs);
192
+ if (rel === "")
193
+ return true;
194
+ if (!rel.startsWith("..") && !path.isAbsolute(rel))
195
+ return true;
196
+ if (process.platform === "win32") {
197
+ const relLc = path.relative(rootAbs.toLowerCase(), targetAbs.toLowerCase());
198
+ return relLc !== "" && !relLc.startsWith("..") && !path.isAbsolute(relLc);
199
+ }
200
+ return false;
201
+ }
202
+ /** realpathSync wrapper — null when the path does not exist / cannot resolve. */
203
+ function realPathSafe(p) {
204
+ try {
205
+ return fs.realpathSync(p);
206
+ }
207
+ catch {
208
+ return null;
209
+ }
210
+ }
211
+ /**
212
+ * Resolve a requested path against the tool's directory and enforce the P2
213
+ * read scope (project root + extra dirs, i.e. blackboard + trajectory).
214
+ * Junction/symlink hardening (#6): BOTH the target and each scope dir are
215
+ * fs.realpathSync'd before the containment check — a purely lexical
216
+ * resolve/relative is defeated by a junction pointing outside the scope.
217
+ * Fail-closed: an unresolvable target is rejected; a scope dir that cannot
218
+ * be resolved (not created yet) simply cannot contain anything real and is
219
+ * skipped. The error message never echoes env-file style paths beyond the
220
+ * scope verdict itself (R6 rejections upstream carry no path text at all).
221
+ */
222
+ export function assertReadablePath(baseDir, requested, extraDirs = []) {
223
+ const raw = String(requested ?? "").trim();
224
+ if (!raw) {
225
+ return { ok: false, message: "缺少 path 参数" };
226
+ }
227
+ const abs = path.isAbsolute(raw) ? path.resolve(raw) : path.resolve(baseDir, raw);
228
+ const absReal = realPathSafe(abs);
229
+ if (!absReal) {
230
+ return {
231
+ ok: false,
232
+ message: `路径不存在或无法解析(P2 fail-closed): ${shorten(abs, 160)}`,
233
+ };
234
+ }
235
+ const scopes = [baseDir, ...extraDirs];
236
+ for (const scope of scopes) {
237
+ const scopeReal = realPathSafe(scope);
238
+ if (!scopeReal)
239
+ continue;
240
+ if (isInsideDir(scopeReal, absReal)) {
241
+ return { ok: true, abs };
242
+ }
243
+ }
244
+ return {
245
+ ok: false,
246
+ message: `路径越界(P2 只读范围 = 项目根 + 黑板 + Trajectory): ${shorten(abs, 160)}`,
247
+ };
248
+ }
249
+ //# sourceMappingURL=guard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"guard.js","sourceRoot":"","sources":["../../src/tm/guard.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * JIT layer-2 tools — runtime assembly.
3
+ *
4
+ * `createTmTools(input)` is called ONCE per server() invocation and returns
5
+ * the runtime the plugin merges into its hooks:
6
+ *
7
+ * const tm = await createTmTools(input)
8
+ * return { config, "tool.execute.before", tool: tm.tools }
9
+ *
10
+ * It generates the run identity (run id + process-random HMAC key), resolves
11
+ * the env config, sweeps expired run payloads (startup-only TTL reclamation)
12
+ * and assembles the four statically-registered tools (T0.4-verified shape:
13
+ * `{ tool: { tm_read: { description, args, execute(args, ctx) } } }`).
14
+ */
15
+ import type { PluginInput, ToolDefinition } from "../types.js";
16
+ import { type EnvProtectMode } from "../envprotect.js";
17
+ import { type TmConfig } from "./config.js";
18
+ import { RunStore } from "./store.js";
19
+ export interface TmRuntime {
20
+ runId: string;
21
+ config: TmConfig;
22
+ store: RunStore;
23
+ tools: Record<string, ToolDefinition>;
24
+ }
25
+ export interface CreateTmToolsOptions {
26
+ /** R6 mode — pass the plugin-level resolved value to keep ONE source. */
27
+ mode?: EnvProtectMode;
28
+ /** R6 extra deny rules — same single-source rule. */
29
+ extra?: RegExp[];
30
+ }
31
+ export declare function createTmTools(input: PluginInput, opts?: CreateTmToolsOptions): Promise<TmRuntime>;
32
+ export { DEFAULT_BASH_READONLY_ALLOWED, estimateTokens, parseAllowlistEnv, resolveTmConfig, shouldOffload, shorten, TM_CONFIG_DEFAULTS, } from "./config.js";
33
+ export { buildRef, hmacToken, isExpired, newRunId, parseRef, REF_PATTERN, verifyToken, } from "./refs.js";
34
+ export { RunStore, USAGE_JSONL_RELPATH } from "./store.js";
35
+ export { buildPreview, buildStructureSummary, capTokens, contentTypeForPath, detectContentType, sniffContentType, } from "./preview.js";
36
+ export { assertReadablePath, classifyReadonlyCommand, isInsideDir, } from "./guard.js";
37
+ export { buildTmTools, HANDLE_INVALID_MESSAGE, tmError } from "./tools.js";
38
+ export type { TmDeps, TmPhase } from "./tools.js";
39
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tm/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAC9D,OAAO,EAGL,KAAK,cAAc,EACpB,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAmB,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAA;AAE5D,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAGrC,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,QAAQ,CAAA;IAChB,KAAK,EAAE,QAAQ,CAAA;IACf,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAA;CACtC;AAED,MAAM,WAAW,oBAAoB;IACnC,yEAAyE;IACzE,IAAI,CAAC,EAAE,cAAc,CAAA;IACrB,qDAAqD;IACrD,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;CACjB;AAED,wBAAsB,aAAa,CACjC,KAAK,EAAE,WAAW,EAClB,IAAI,GAAE,oBAAyB,GAC9B,OAAO,CAAC,SAAS,CAAC,CAmCpB;AAID,OAAO,EACL,6BAA6B,EAC7B,cAAc,EACd,iBAAiB,EACjB,eAAe,EACf,aAAa,EACb,OAAO,EACP,kBAAkB,GACnB,MAAM,aAAa,CAAA;AACpB,OAAO,EACL,QAAQ,EACR,SAAS,EACT,SAAS,EACT,QAAQ,EACR,QAAQ,EACR,WAAW,EACX,WAAW,GACZ,MAAM,WAAW,CAAA;AAClB,OAAO,EAAE,QAAQ,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAA;AAC1D,OAAO,EACL,YAAY,EACZ,qBAAqB,EACrB,SAAS,EACT,kBAAkB,EAClB,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,cAAc,CAAA;AACrB,OAAO,EACL,kBAAkB,EAClB,uBAAuB,EACvB,WAAW,GACZ,MAAM,YAAY,CAAA;AACnB,OAAO,EAAE,YAAY,EAAE,sBAAsB,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AAC1E,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA"}
@@ -0,0 +1,63 @@
1
+ /**
2
+ * JIT layer-2 tools — runtime assembly.
3
+ *
4
+ * `createTmTools(input)` is called ONCE per server() invocation and returns
5
+ * the runtime the plugin merges into its hooks:
6
+ *
7
+ * const tm = await createTmTools(input)
8
+ * return { config, "tool.execute.before", tool: tm.tools }
9
+ *
10
+ * It generates the run identity (run id + process-random HMAC key), resolves
11
+ * the env config, sweeps expired run payloads (startup-only TTL reclamation)
12
+ * and assembles the four statically-registered tools (T0.4-verified shape:
13
+ * `{ tool: { tm_read: { description, args, execute(args, ctx) } } }`).
14
+ */
15
+ import * as crypto from "node:crypto";
16
+ import { parseExtraDeny, resolveEnvProtectMode, } from "../envprotect.js";
17
+ import { resolveTmConfig } from "./config.js";
18
+ import { hmacToken, newRunId } from "./refs.js";
19
+ import { RunStore } from "./store.js";
20
+ import { buildTmTools } from "./tools.js";
21
+ export async function createTmTools(input, opts = {}) {
22
+ const cfg = resolveTmConfig(process.env);
23
+ const directory = typeof input?.directory === "string" && input.directory.length > 0
24
+ ? input.directory
25
+ : process.cwd();
26
+ const runId = newRunId();
27
+ const hmacKey = crypto.randomBytes(32);
28
+ const accessToken = hmacToken(hmacKey, runId);
29
+ const store = new RunStore({
30
+ projectRoot: directory,
31
+ blackboardDir: cfg.blackboardDir,
32
+ trajectoryDir: cfg.trajectoryDir,
33
+ runId,
34
+ ttlDays: cfg.blackboardTtlDays,
35
+ });
36
+ // Startup-only TTL reclamation for expired run payloads — the sole cleanup
37
+ // path for the tm store (mirrors blackboard.ts's sweeper philosophy).
38
+ store.sweepExpired();
39
+ const mode = opts.mode ?? resolveEnvProtectMode(process.env.TM_ENV_PROTECT);
40
+ const extra = opts.extra ?? parseExtraDeny(process.env.TM_ENV_PROTECT_EXTRA_DENY);
41
+ const expireAt = Date.now() + cfg.blackboardTtlDays * 24 * 60 * 60 * 1000;
42
+ const tools = await buildTmTools({
43
+ client: input?.client,
44
+ $: input?.$,
45
+ cfg,
46
+ store,
47
+ runId,
48
+ hmacKey,
49
+ accessToken,
50
+ expireAt,
51
+ mode,
52
+ extra,
53
+ });
54
+ return { runId, config: cfg, store, tools };
55
+ }
56
+ // ---------- re-exports (stable import surface for tests + plugin entry) ----------
57
+ export { DEFAULT_BASH_READONLY_ALLOWED, estimateTokens, parseAllowlistEnv, resolveTmConfig, shouldOffload, shorten, TM_CONFIG_DEFAULTS, } from "./config.js";
58
+ export { buildRef, hmacToken, isExpired, newRunId, parseRef, REF_PATTERN, verifyToken, } from "./refs.js";
59
+ export { RunStore, USAGE_JSONL_RELPATH } from "./store.js";
60
+ export { buildPreview, buildStructureSummary, capTokens, contentTypeForPath, detectContentType, sniffContentType, } from "./preview.js";
61
+ export { assertReadablePath, classifyReadonlyCommand, isInsideDir, } from "./guard.js";
62
+ export { buildTmTools, HANDLE_INVALID_MESSAGE, tmError } from "./tools.js";
63
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/tm/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,46 @@
1
+ /**
2
+ * JIT layer-2 tools — content-aware previews (the soul of the offload handle)
3
+ * plus the L1 "structure" summaries for tm_fetch mode:"structure".
4
+ *
5
+ * Five branches, chosen by content_type (path hint) with sniffing fallback:
6
+ * json -> first 3 keys: path + type + magnitude
7
+ * csv -> header + 2 sample rows + total rows x cols
8
+ * log -> first 3 lines + pattern stats (ERROR x N, ...) + path:line refs
9
+ * code -> function/class signature list (with line numbers)
10
+ * binary -> type + size + "不可预览"
11
+ *
12
+ * RED LINE: previews are hard-capped at TM_PREVIEW_MAX_TOKENS (default 80
13
+ * estimated tokens). A bloated preview is an R4 regression — the cap is
14
+ * enforced by composition (body budget = cap - meta) so clue + hint lines
15
+ * always survive truncation.
16
+ */
17
+ export type ContentType = "json" | "csv" | "log" | "text" | "code" | "binary";
18
+ /** Content type derived from the file extension (deterministic hint). */
19
+ export declare function contentTypeForPath(p: string): ContentType;
20
+ /**
21
+ * Sniff content type when no path hint exists (or the hint was plain "text").
22
+ * Order: binary -> json -> csv -> code -> text. Logs and plain text share
23
+ * the same preview branch, so no separate sniff rule is needed for them.
24
+ */
25
+ export declare function sniffContentType(content: string): ContentType;
26
+ /**
27
+ * Resolve the effective branch: a path-derived hint wins (deterministic);
28
+ * plain-text hints and missing hints fall through to sniffing (which may
29
+ * still upgrade the content to json / csv / code / binary).
30
+ */
31
+ export declare function detectContentType(content: string, hint?: ContentType): ContentType;
32
+ /** Hard cap by estimated tokens (chars/4): never exceed maxTokens. */
33
+ export declare function capTokens(text: string, maxTokens: number): string;
34
+ export interface PreviewOptions {
35
+ /** Raw-line scan budget for the branch builders (TM_PREVIEW_LINES). */
36
+ lines?: number;
37
+ /** Hard cap in estimated tokens (TM_PREVIEW_MAX_TOKENS). */
38
+ maxTokens?: number;
39
+ /** Retrieval clue embedded into the preview (path / pattern / cmd). */
40
+ clue?: string;
41
+ }
42
+ /** Build the governed preview for offloaded content. Never throws. */
43
+ export declare function buildPreview(content: string, contentType: ContentType, opts?: PreviewOptions): string;
44
+ /** ~100-token-level L1 map of an offloaded payload (TOC / key tree / error lines). */
45
+ export declare function buildStructureSummary(content: string, contentType: string): string;
46
+ //# sourceMappingURL=preview.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preview.d.ts","sourceRoot":"","sources":["../../src/tm/preview.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAKH,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,KAAK,GAAG,KAAK,GAAG,MAAM,GAAG,MAAM,GAAG,QAAQ,CAAA;AAiB7E,yEAAyE;AACzE,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,MAAM,GAAG,WAAW,CAQzD;AAsBD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,WAAW,CAc7D;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,GAAG,WAAW,CAGlF;AAED,sEAAsE;AACtE,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CASjE;AAiJD,MAAM,WAAW,cAAc;IAC7B,uEAAuE;IACvE,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,4DAA4D;IAC5D,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,uEAAuE;IACvE,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED,sEAAsE;AACtE,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,WAAW,EAAE,IAAI,GAAE,cAAmB,GAAG,MAAM,CAwBzG;AA+CD,sFAAsF;AACtF,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAqBlF"}