@yangdcm/dsh-expert-team 1.3.15 → 1.3.17
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/CHANGELOG.md +113 -0
- package/README.en.md +32 -27
- package/README.md +32 -27
- package/client.js +133 -22
- package/lib/artifact-redirect-watch.js +118 -0
- package/lib/command.js +316 -37
- package/lib/effort-preflight.js +135 -0
- package/package.json +2 -2
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* R1 绕过检测:`bash` / `pwsh` 里"重定向写进 run 工件"的**只报不拦**观测器(2026-09-15)。
|
|
3
|
+
*
|
|
4
|
+
* ── 为什么需要它(而不是把它也做成门禁)────────────────────────────────────
|
|
5
|
+
* R1 硬门禁(`lib/artifact-ownership.js`)只覆盖 **`write` / `edit`** 通道。preset 里
|
|
6
|
+
* `backend` / `frontend` / `researcher` / `qa` / `dba` / `devops` **持 `bash`**,理论上可以
|
|
7
|
+
* `cat > SPEC.md` 绕过门禁。**静默绕过**违背本仓纪律("失败要出声"),但**首版刻意不做阻断**:
|
|
8
|
+
* · `bash` 的写目标可以是重定向、`tee`、变量、子命令替换 —— 静态判断**不可靠**;
|
|
9
|
+
* · 一旦误判,代价是"把正常命令判成越权"(比漏报更伤,本仓有前车之鉴:门禁变成故障源)。
|
|
10
|
+
* ⇒ 折中:**只留痕**,用真实运行里的命中频率来决定以后要不要收紧。
|
|
11
|
+
*
|
|
12
|
+
* ── 口径(保守,宁可漏报)────────────────────────────────────────────────
|
|
13
|
+
* 只有同时满足下面三条才算命中:
|
|
14
|
+
* ① 工具是 `bash` / `pwsh`;
|
|
15
|
+
* ② 命令里出现**明显的写目标**:`>` / `>>` / `tee [-a]` 之后紧跟一个路径 token;
|
|
16
|
+
* ③ 该路径解析后**恰好是** `<team 根>/<runId>/<文件名>`(沿用 `runScopedTarget` 的两段口径,
|
|
17
|
+
* 更深的子目录 / 工作区代码 / `/tmp` / `/dev/null` 一律不命中),且文件名属于**已知工件**。
|
|
18
|
+
*
|
|
19
|
+
* 本监听器**绝不**改动结果、**绝不**抛错(沿用 `interception.js` 的纪律:
|
|
20
|
+
* 监听器一旦抛错会被宿主收敛为工具失败,2026-09-12 出过全工具瘫痪事故)。
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { resolve, basename } from 'node:path';
|
|
24
|
+
import { runScopedTarget } from './interception.js';
|
|
25
|
+
|
|
26
|
+
/** shell 类工具(R1 门禁覆盖不到的那些)。 */
|
|
27
|
+
export const SHELL_WRITE_TOOLS = Object.freeze(new Set(['bash', 'pwsh']));
|
|
28
|
+
|
|
29
|
+
/** 去掉包裹的引号(`"a b"` / `'a b'`),保留原样其余内容。 */
|
|
30
|
+
function unquote(tok) {
|
|
31
|
+
const s = String(tok || '').trim();
|
|
32
|
+
if (s.length >= 2 && ((s[0] === '"' && s[s.length - 1] === '"') || (s[0] === "'" && s[s.length - 1] === "'"))) {
|
|
33
|
+
return s.slice(1, -1);
|
|
34
|
+
}
|
|
35
|
+
return s;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 从命令串里提取**候选写目标**(纯函数、保守)。
|
|
40
|
+
* 只认两种形态:`>` / `>>`(可带 fd 前缀如 `2>`)与 `tee [-a] <path>`。
|
|
41
|
+
* 变量 / 命令替换 / 通配符**不解析**(拿不准就漏报 —— 这是本文件的既定口径)。
|
|
42
|
+
*/
|
|
43
|
+
export function extractWriteTargets(command) {
|
|
44
|
+
const cmd = String(command || '');
|
|
45
|
+
const out = [];
|
|
46
|
+
const push = (raw) => {
|
|
47
|
+
const t = unquote(raw);
|
|
48
|
+
if (!t) return;
|
|
49
|
+
if (t === '/dev/null' || t === '/dev/stdout' || t === '/dev/stderr') return;
|
|
50
|
+
if (/[$`*?]/.test(t)) return; // 含变量/替换/通配 ⇒ 不解析(宁可漏报)
|
|
51
|
+
if (t.startsWith('&')) return; // `&>` 的 fd 形式交给下一条规则
|
|
52
|
+
out.push(t);
|
|
53
|
+
};
|
|
54
|
+
// `>` / `>>`(含 `1>`, `2>>`, `&>`),后跟一个 token
|
|
55
|
+
const redir = /(?:\d?&?>>?|&>>?)\s*("[^"]+"|'[^']+'|[^\s;|&<>()]+)/g;
|
|
56
|
+
let m;
|
|
57
|
+
while ((m = redir.exec(cmd)) !== null) push(m[1]);
|
|
58
|
+
// `tee [-a] <path>`:只取**第一个**路径参数(`tee a b` 少见,且多写不算漏报)
|
|
59
|
+
const tee = /(?:^|[\s;|&(])tee(?:\s+-[a-zA-Z]+)*\s+("[^"]+"|'[^']+'|[^\s;|&<>()]+)/g;
|
|
60
|
+
while ((m = tee.exec(cmd)) !== null) push(m[1]);
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 判定命令里是否**明显**写向 `<teamRoot>/<runId>/<已知工件>`。
|
|
66
|
+
* @returns {{abs:string, runId:string, base:string}|null}
|
|
67
|
+
*/
|
|
68
|
+
export function detectArtifactRedirect(command, { cwd, teamRoot, knownArtifacts } = {}) {
|
|
69
|
+
if (!cwd || !teamRoot) return null;
|
|
70
|
+
const known = knownArtifacts instanceof Set ? knownArtifacts : new Set(knownArtifacts || []);
|
|
71
|
+
if (!known.size) return null;
|
|
72
|
+
for (const raw of extractWriteTargets(command)) {
|
|
73
|
+
let abs;
|
|
74
|
+
try { abs = resolve(cwd, raw); } catch { continue; }
|
|
75
|
+
const scoped = runScopedTarget(abs, teamRoot); // 只认 `<teamRoot>/<runId>/<文件名>` 两段
|
|
76
|
+
if (!scoped) continue;
|
|
77
|
+
const base = basename(abs);
|
|
78
|
+
if (!known.has(base)) continue;
|
|
79
|
+
return { abs, runId: scoped.runId, base };
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 创建 `tools/post-execute` 监听器(**纯观测**:不改结果、不阻断、不抛错)。
|
|
86
|
+
* deps:`knownArtifacts`(已知工件文件名集合,由调用方从**单一真源**传入)、
|
|
87
|
+
* `cwdFor(exec)`、`teamRootFor(cwd)`、`onEvent(type, payload)`、可选 `warn(line)`。
|
|
88
|
+
*/
|
|
89
|
+
export function createArtifactRedirectWatcher({ knownArtifacts, cwdFor, teamRootFor, onEvent, warn } = {}) {
|
|
90
|
+
const known = knownArtifacts instanceof Set ? knownArtifacts : new Set(knownArtifacts || []);
|
|
91
|
+
const emit = typeof onEvent === 'function' ? onEvent : () => {};
|
|
92
|
+
const say = typeof warn === 'function' ? warn : (line) => console.warn(line);
|
|
93
|
+
return async function artifactRedirectWatcher(exec, result, next) {
|
|
94
|
+
// 与 `interception.js` 同契约:签名是 (exec, result, next);next 不是函数就降级为不干涉。
|
|
95
|
+
if (typeof next !== 'function') return { kind: 'accept' };
|
|
96
|
+
const downstream = await next();
|
|
97
|
+
try {
|
|
98
|
+
const name = String((exec && exec.name) || '');
|
|
99
|
+
if (!SHELL_WRITE_TOOLS.has(name)) return downstream;
|
|
100
|
+
const args = (exec && exec.arguments) || {};
|
|
101
|
+
const command = String(args.command || args.cmd || args.script || '');
|
|
102
|
+
if (!command) return downstream;
|
|
103
|
+
const cwd = typeof cwdFor === 'function' ? cwdFor(exec) : '';
|
|
104
|
+
if (!cwd) return downstream;
|
|
105
|
+
const teamRoot = typeof teamRootFor === 'function' ? teamRootFor(cwd) : '';
|
|
106
|
+
if (!teamRoot) return downstream;
|
|
107
|
+
const hit = detectArtifactRedirect(command, { cwd, teamRoot, knownArtifacts: known });
|
|
108
|
+
if (!hit) return downstream;
|
|
109
|
+
try {
|
|
110
|
+
say(`[expert-team] R1 绕过检测:${name} 写入 run 工件 ${hit.abs}(write/edit 门禁覆盖不到;仅留痕,不阻断)`);
|
|
111
|
+
} catch { /* 打印失败也不能影响工具调用 */ }
|
|
112
|
+
emit('artifact-redirect-bypass', { tool: name, runId: hit.runId, base: hit.base });
|
|
113
|
+
} catch (e) {
|
|
114
|
+
try { emit('artifact-redirect-watch-error', { error: String((e && e.message) || e) }); } catch { /* ignore */ }
|
|
115
|
+
}
|
|
116
|
+
return downstream;
|
|
117
|
+
};
|
|
118
|
+
}
|