@skillfm/local 2.0.0 → 2.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/agent-hints.d.ts +25 -0
  2. package/dist/agent-hints.d.ts.map +1 -0
  3. package/dist/agent-hints.js +87 -0
  4. package/dist/agent-hints.js.map +1 -0
  5. package/dist/doctor.d.ts +30 -0
  6. package/dist/doctor.d.ts.map +1 -0
  7. package/dist/doctor.js +272 -0
  8. package/dist/doctor.js.map +1 -0
  9. package/dist/guard/bin.d.ts +11 -0
  10. package/dist/guard/bin.d.ts.map +1 -0
  11. package/dist/guard/bin.js +16 -0
  12. package/dist/guard/bin.js.map +1 -0
  13. package/dist/guard/cli.d.ts +23 -0
  14. package/dist/guard/cli.d.ts.map +1 -0
  15. package/dist/guard/cli.js +249 -0
  16. package/dist/guard/cli.js.map +1 -0
  17. package/dist/guard/sidecar-client.d.ts +46 -0
  18. package/dist/guard/sidecar-client.d.ts.map +1 -0
  19. package/dist/guard/sidecar-client.js +92 -0
  20. package/dist/guard/sidecar-client.js.map +1 -0
  21. package/dist/guard/state.d.ts +80 -0
  22. package/dist/guard/state.d.ts.map +1 -0
  23. package/dist/guard/state.js +119 -0
  24. package/dist/guard/state.js.map +1 -0
  25. package/dist/harness/detector.d.ts +47 -0
  26. package/dist/harness/detector.d.ts.map +1 -0
  27. package/dist/harness/detector.js +177 -0
  28. package/dist/harness/detector.js.map +1 -0
  29. package/dist/harness/priming.d.ts +42 -0
  30. package/dist/harness/priming.d.ts.map +1 -0
  31. package/dist/harness/priming.js +89 -0
  32. package/dist/harness/priming.js.map +1 -0
  33. package/dist/harness/templates.d.ts +108 -0
  34. package/dist/harness/templates.d.ts.map +1 -0
  35. package/dist/harness/templates.js +171 -0
  36. package/dist/harness/templates.js.map +1 -0
  37. package/dist/harness/writers.d.ts +82 -0
  38. package/dist/harness/writers.d.ts.map +1 -0
  39. package/dist/harness/writers.js +266 -0
  40. package/dist/harness/writers.js.map +1 -0
  41. package/dist/index.js +562 -4
  42. package/dist/index.js.map +1 -1
  43. package/dist/lang.d.ts +21 -0
  44. package/dist/lang.d.ts.map +1 -0
  45. package/dist/lang.js +62 -0
  46. package/dist/lang.js.map +1 -0
  47. package/dist/soul-security.d.ts +76 -0
  48. package/dist/soul-security.d.ts.map +1 -0
  49. package/dist/soul-security.js +197 -0
  50. package/dist/soul-security.js.map +1 -0
  51. package/dist/soul.d.ts +135 -0
  52. package/dist/soul.d.ts.map +1 -0
  53. package/dist/soul.js +439 -0
  54. package/dist/soul.js.map +1 -0
  55. package/package.json +7 -3
@@ -0,0 +1,249 @@
1
+ /**
2
+ * BSO M9-4 — skillfm-guard CLI 入口
3
+ *
4
+ * Refs: docs/prd/PRD-BSO-M9-HARNESS-HOOKS.md §3.4 + §3.5
5
+ *
6
+ * 子命令(由 harness 的 hook 机制触发):
7
+ * - session-start → SessionStart(warn-only;启 sidecar 记录 session)
8
+ * - pre-tool-use → PreToolUse(阻断性;未 brain_run → exit 2)
9
+ * - post-tool-use → PostToolUse(warn-only;audit 用途)
10
+ * - check-script-loaded → doctor / 旁路用,查询当前 sidecar 状态
11
+ *
12
+ * Claude Code hook payload 约定(Anthropic spec):
13
+ * - SessionStart: stdin JSON { session_id, source?, cwd? }
14
+ * - PreToolUse: stdin JSON { session_id, tool_name, tool_input? }
15
+ * - PostToolUse: stdin JSON { session_id, tool_name, tool_response? }
16
+ *
17
+ * Fail-open 原则:sidecar 不可达 / 超时 / 解析失败一律 exit 0 + stderr warn,
18
+ * 只有 sidecar 明确返回 412 才 exit 2。这样保证 hook 故障不会锁死用户 UX。
19
+ */
20
+ import { callGuardCheck, callGuardPostToolUse, callGuardSessionStart, readSidecarEndpoint, } from './sidecar-client.js';
21
+ // ============================================================================
22
+ // stdin 读取(非阻塞,超时 300ms)
23
+ // ============================================================================
24
+ async function readStdinJson(timeoutMs = 300) {
25
+ if (process.stdin.isTTY)
26
+ return {};
27
+ return new Promise((resolve) => {
28
+ const chunks = [];
29
+ let done = false;
30
+ const timer = setTimeout(() => {
31
+ if (done)
32
+ return;
33
+ done = true;
34
+ resolve({});
35
+ }, timeoutMs);
36
+ process.stdin.on('data', (c) => chunks.push(c));
37
+ process.stdin.on('end', () => {
38
+ if (done)
39
+ return;
40
+ done = true;
41
+ clearTimeout(timer);
42
+ const raw = Buffer.concat(chunks).toString('utf-8').trim();
43
+ if (!raw)
44
+ return resolve({});
45
+ try {
46
+ const parsed = JSON.parse(raw);
47
+ if (parsed && typeof parsed === 'object')
48
+ resolve(parsed);
49
+ else
50
+ resolve({});
51
+ }
52
+ catch {
53
+ resolve({});
54
+ }
55
+ });
56
+ process.stdin.on('error', () => {
57
+ if (done)
58
+ return;
59
+ done = true;
60
+ clearTimeout(timer);
61
+ resolve({});
62
+ });
63
+ });
64
+ }
65
+ function parseArgs(argv) {
66
+ const [sub = '', ...rest] = argv;
67
+ const flags = {};
68
+ for (let i = 0; i < rest.length; i += 1) {
69
+ const token = rest[i];
70
+ if (token.startsWith('--')) {
71
+ const eq = token.indexOf('=');
72
+ if (eq > 0) {
73
+ flags[token.slice(2, eq)] = token.slice(eq + 1);
74
+ }
75
+ else {
76
+ const next = rest[i + 1];
77
+ if (next !== undefined && !next.startsWith('--')) {
78
+ flags[token.slice(2)] = next;
79
+ i += 1;
80
+ }
81
+ else {
82
+ flags[token.slice(2)] = true;
83
+ }
84
+ }
85
+ }
86
+ }
87
+ return { subcommand: sub, flags };
88
+ }
89
+ // ============================================================================
90
+ // 辅助:提取 session_id / tool / harness
91
+ // ============================================================================
92
+ function extractContext(stdinJson, flags) {
93
+ const session_id = (typeof flags.session === 'string' && flags.session) ||
94
+ (typeof stdinJson.session_id === 'string' && stdinJson.session_id) ||
95
+ process.env.SKILLFM_HOOK_SESSION_ID ||
96
+ 'unknown-session';
97
+ const tool = (typeof flags.tool === 'string' && flags.tool) ||
98
+ (typeof stdinJson.tool_name === 'string' && stdinJson.tool_name) ||
99
+ undefined;
100
+ const harness = (typeof flags.harness === 'string' && flags.harness) ||
101
+ process.env.SKILLFM_HARNESS ||
102
+ undefined;
103
+ return { session_id, tool, harness };
104
+ }
105
+ function warnToStderr(msg) {
106
+ process.stderr.write(`⚠️ [skillfm-guard] ${msg}\n`);
107
+ }
108
+ function infoToStderr(msg) {
109
+ // 刻意用 stderr:Claude Code / Aider 等 hook 只把 stderr 展示给用户
110
+ process.stderr.write(`[skillfm-guard] ${msg}\n`);
111
+ }
112
+ // ============================================================================
113
+ // 子命令:session-start
114
+ // ============================================================================
115
+ async function cmdSessionStart(stdin, flags) {
116
+ const endpoint = readSidecarEndpoint();
117
+ const { session_id, harness } = extractContext(stdin, flags);
118
+ if (!endpoint) {
119
+ warnToStderr('sidecar 未运行,无法注册 session(hook 层不阻断)');
120
+ return 0;
121
+ }
122
+ try {
123
+ await callGuardSessionStart(endpoint, {
124
+ session_id,
125
+ harness,
126
+ cwd: process.cwd(),
127
+ });
128
+ }
129
+ catch (err) {
130
+ warnToStderr(`session-start 调 sidecar 失败: ${err.message}`);
131
+ }
132
+ return 0;
133
+ }
134
+ // ============================================================================
135
+ // 子命令:pre-tool-use(阻断性)
136
+ // ============================================================================
137
+ async function cmdPreToolUse(stdin, flags) {
138
+ const endpoint = readSidecarEndpoint();
139
+ const { session_id, tool, harness } = extractContext(stdin, flags);
140
+ if (!endpoint) {
141
+ warnToStderr('sidecar 未运行(fail-open,hook 放行)');
142
+ return 0;
143
+ }
144
+ try {
145
+ const r = await callGuardCheck(endpoint, { session_id, tool, harness });
146
+ if (r.status === 200)
147
+ return 0;
148
+ if (r.status === 412) {
149
+ const body = r.body;
150
+ const hint = body?.hint ||
151
+ '请先调用 SkillFM 的 brain_run(或 list_skills 浏览),让 SkillFM 记录本次编排意图再执行破坏性操作。';
152
+ process.stderr.write(`⛔ [skillfm-guard] BLOCKED — ${tool || 'destructive tool'}\n` +
153
+ ` reason: ${body?.reason || 'script not loaded'}\n` +
154
+ ` ${hint}\n`);
155
+ return 2;
156
+ }
157
+ // 其他状态码(5xx / 404 等)fail-open
158
+ warnToStderr(`sidecar 返回异常状态 ${r.status},fail-open 放行`);
159
+ return 0;
160
+ }
161
+ catch (err) {
162
+ warnToStderr(`pre-tool-use 调 sidecar 失败: ${err.message}(fail-open 放行)`);
163
+ return 0;
164
+ }
165
+ }
166
+ // ============================================================================
167
+ // 子命令:post-tool-use(warn-only)
168
+ // ============================================================================
169
+ async function cmdPostToolUse(stdin, flags) {
170
+ const endpoint = readSidecarEndpoint();
171
+ const { session_id, tool, harness } = extractContext(stdin, flags);
172
+ if (!endpoint)
173
+ return 0;
174
+ try {
175
+ await callGuardPostToolUse(endpoint, { session_id, tool, harness, outcome: 'ok' });
176
+ }
177
+ catch {
178
+ // post-tool-use 失败 silent
179
+ }
180
+ return 0;
181
+ }
182
+ // ============================================================================
183
+ // 子命令:check-script-loaded
184
+ // ============================================================================
185
+ async function cmdCheckScriptLoaded(stdin, flags) {
186
+ const endpoint = readSidecarEndpoint();
187
+ const { session_id } = extractContext(stdin, flags);
188
+ if (!endpoint) {
189
+ process.stdout.write(JSON.stringify({ ok: false, reason: 'sidecar not running' }) + '\n');
190
+ return 0;
191
+ }
192
+ try {
193
+ const r = await callGuardCheck(endpoint, { session_id });
194
+ process.stdout.write(JSON.stringify({ status: r.status, body: r.body }) + '\n');
195
+ return 0;
196
+ }
197
+ catch (err) {
198
+ process.stdout.write(JSON.stringify({ ok: false, reason: err.message }) + '\n');
199
+ return 0;
200
+ }
201
+ }
202
+ // ============================================================================
203
+ // 主入口
204
+ // ============================================================================
205
+ export async function runGuardCli(argv) {
206
+ const { subcommand, flags } = parseArgs(argv);
207
+ if (subcommand === '--help' || subcommand === '-h' || subcommand === 'help' || subcommand === '') {
208
+ process.stdout.write([
209
+ 'Usage: skillfm-guard <subcommand> [flags]',
210
+ '',
211
+ 'Subcommands:',
212
+ ' session-start Hook: harness session start (warn-only)',
213
+ ' pre-tool-use Hook: pre-tool-use (blocking — exit 2 if brain_run not called)',
214
+ ' post-tool-use Hook: post-tool-use (warn-only, audit)',
215
+ ' check-script-loaded Diagnostic: print current guard state as JSON',
216
+ '',
217
+ 'Flags:',
218
+ ' --session=<id> Session identifier (else read stdin JSON or $SKILLFM_HOOK_SESSION_ID)',
219
+ ' --tool=<name> Tool name (else read stdin tool_name)',
220
+ ' --harness=<name> Harness hint (else $SKILLFM_HARNESS)',
221
+ '',
222
+ 'Fail-open: sidecar unreachable / timeout → exit 0 + stderr warn. Only sidecar 412 blocks.',
223
+ ].join('\n') + '\n');
224
+ return 0;
225
+ }
226
+ const stdin = await readStdinJson();
227
+ switch (subcommand) {
228
+ case 'session-start':
229
+ return cmdSessionStart(stdin, flags);
230
+ case 'pre-tool-use':
231
+ return cmdPreToolUse(stdin, flags);
232
+ case 'post-tool-use':
233
+ return cmdPostToolUse(stdin, flags);
234
+ case 'check-script-loaded':
235
+ return cmdCheckScriptLoaded(stdin, flags);
236
+ default:
237
+ warnToStderr(`unknown subcommand "${subcommand}" (fail-open)`);
238
+ infoToStderr('run `skillfm-guard --help` for usage');
239
+ return 0;
240
+ }
241
+ }
242
+ // 允许直接 node ./dist/guard/cli.js 运行
243
+ if (import.meta.url === `file://${process.argv[1]}`) {
244
+ runGuardCli(process.argv.slice(2)).then((code) => process.exit(code), (err) => {
245
+ process.stderr.write(`[skillfm-guard] fatal: ${err?.message || err}\n`);
246
+ process.exit(0); // fail-open
247
+ });
248
+ }
249
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../../src/guard/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,46 @@
1
+ /**
2
+ * BSO M9-4 — skillfm-guard → sidecar HTTP client
3
+ *
4
+ * Refs: docs/prd/PRD-BSO-M9-HARNESS-HOOKS.md §3.5
5
+ *
6
+ * hook 进程生命短(几十毫秒),对 sidecar 有严格的"秒级响应 + fail-open"要求:
7
+ * - 超时 (default 500ms) → 降级放行(exit 0)+ stderr warn
8
+ * - 网络错误 / sidecar 不在 → 降级放行
9
+ * - 仅在 sidecar 明确返回 412 时才阻断(exit 2)
10
+ */
11
+ export interface SidecarEndpoint {
12
+ url: string;
13
+ port: number;
14
+ }
15
+ export declare function readSidecarEndpoint(): SidecarEndpoint | null;
16
+ export interface GuardCheckInput {
17
+ session_id: string;
18
+ tool?: string;
19
+ harness?: string;
20
+ }
21
+ export interface GuardCheckResponse {
22
+ status: number;
23
+ body: unknown;
24
+ }
25
+ /**
26
+ * GET /internal/guard/check — fail-open on any error
27
+ */
28
+ export declare function callGuardCheck(endpoint: SidecarEndpoint, input: GuardCheckInput, timeoutMs?: number): Promise<GuardCheckResponse>;
29
+ /**
30
+ * POST /internal/guard/session-start
31
+ */
32
+ export declare function callGuardSessionStart(endpoint: SidecarEndpoint, payload: {
33
+ session_id: string;
34
+ harness?: string;
35
+ cwd?: string;
36
+ }, timeoutMs?: number): Promise<GuardCheckResponse>;
37
+ /**
38
+ * POST /internal/guard/post-tool-use
39
+ */
40
+ export declare function callGuardPostToolUse(endpoint: SidecarEndpoint, payload: {
41
+ session_id: string;
42
+ tool?: string;
43
+ harness?: string;
44
+ outcome?: 'ok' | 'error';
45
+ }, timeoutMs?: number): Promise<GuardCheckResponse>;
46
+ //# sourceMappingURL=sidecar-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sidecar-client.d.ts","sourceRoot":"","sources":["../../src/guard/sidecar-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AASH,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;CACd;AAED,wBAAgB,mBAAmB,IAAI,eAAe,GAAG,IAAI,CAY5D;AAED,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,OAAO,CAAC;CACf;AAED;;GAEG;AACH,wBAAsB,cAAc,CAClC,QAAQ,EAAE,eAAe,EACzB,KAAK,EAAE,eAAe,EACtB,SAAS,SAAqB,GAC7B,OAAO,CAAC,kBAAkB,CAAC,CAgB7B;AAED;;GAEG;AACH,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,eAAe,EACzB,OAAO,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,EAC/D,SAAS,SAAqB,GAC7B,OAAO,CAAC,kBAAkB,CAAC,CAe7B;AAED;;GAEG;AACH,wBAAsB,oBAAoB,CACxC,QAAQ,EAAE,eAAe,EACzB,OAAO,EAAE;IACP,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,IAAI,GAAG,OAAO,CAAC;CAC1B,EACD,SAAS,SAAqB,GAC7B,OAAO,CAAC,kBAAkB,CAAC,CAe7B"}
@@ -0,0 +1,92 @@
1
+ /**
2
+ * BSO M9-4 — skillfm-guard → sidecar HTTP client
3
+ *
4
+ * Refs: docs/prd/PRD-BSO-M9-HARNESS-HOOKS.md §3.5
5
+ *
6
+ * hook 进程生命短(几十毫秒),对 sidecar 有严格的"秒级响应 + fail-open"要求:
7
+ * - 超时 (default 500ms) → 降级放行(exit 0)+ stderr warn
8
+ * - 网络错误 / sidecar 不在 → 降级放行
9
+ * - 仅在 sidecar 明确返回 412 时才阻断(exit 2)
10
+ */
11
+ import { existsSync, readFileSync } from 'node:fs';
12
+ import { homedir } from 'node:os';
13
+ import { join } from 'node:path';
14
+ const LOCAL_SETTINGS_FILE = join(homedir(), '.skillfm', 'local.json');
15
+ const DEFAULT_TIMEOUT_MS = 500;
16
+ export function readSidecarEndpoint() {
17
+ try {
18
+ if (!existsSync(LOCAL_SETTINGS_FILE))
19
+ return null;
20
+ const raw = JSON.parse(readFileSync(LOCAL_SETTINGS_FILE, 'utf-8'));
21
+ if (!raw.url)
22
+ return null;
23
+ const parsed = new URL(raw.url);
24
+ return { url: raw.url.replace(/\/$/, ''), port: Number(parsed.port) };
25
+ }
26
+ catch {
27
+ return null;
28
+ }
29
+ }
30
+ /**
31
+ * GET /internal/guard/check — fail-open on any error
32
+ */
33
+ export async function callGuardCheck(endpoint, input, timeoutMs = DEFAULT_TIMEOUT_MS) {
34
+ const params = new URLSearchParams();
35
+ params.set('session_id', input.session_id);
36
+ if (input.tool)
37
+ params.set('tool', input.tool);
38
+ if (input.harness)
39
+ params.set('harness', input.harness);
40
+ const url = `${endpoint.url}/internal/guard/check?${params.toString()}`;
41
+ const controller = new AbortController();
42
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
43
+ try {
44
+ const res = await fetch(url, { method: 'GET', signal: controller.signal });
45
+ const body = await res.json().catch(() => ({}));
46
+ return { status: res.status, body };
47
+ }
48
+ finally {
49
+ clearTimeout(timer);
50
+ }
51
+ }
52
+ /**
53
+ * POST /internal/guard/session-start
54
+ */
55
+ export async function callGuardSessionStart(endpoint, payload, timeoutMs = DEFAULT_TIMEOUT_MS) {
56
+ const controller = new AbortController();
57
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
58
+ try {
59
+ const res = await fetch(`${endpoint.url}/internal/guard/session-start`, {
60
+ method: 'POST',
61
+ headers: { 'content-type': 'application/json' },
62
+ body: JSON.stringify(payload),
63
+ signal: controller.signal,
64
+ });
65
+ const body = await res.json().catch(() => ({}));
66
+ return { status: res.status, body };
67
+ }
68
+ finally {
69
+ clearTimeout(timer);
70
+ }
71
+ }
72
+ /**
73
+ * POST /internal/guard/post-tool-use
74
+ */
75
+ export async function callGuardPostToolUse(endpoint, payload, timeoutMs = DEFAULT_TIMEOUT_MS) {
76
+ const controller = new AbortController();
77
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
78
+ try {
79
+ const res = await fetch(`${endpoint.url}/internal/guard/post-tool-use`, {
80
+ method: 'POST',
81
+ headers: { 'content-type': 'application/json' },
82
+ body: JSON.stringify(payload),
83
+ signal: controller.signal,
84
+ });
85
+ const body = await res.json().catch(() => ({}));
86
+ return { status: res.status, body };
87
+ }
88
+ finally {
89
+ clearTimeout(timer);
90
+ }
91
+ }
92
+ //# sourceMappingURL=sidecar-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sidecar-client.js","sourceRoot":"","sources":["../../src/guard/sidecar-client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,80 @@
1
+ /**
2
+ * BSO M9-4 — guard session 状态(in-memory,sidecar 内驻)
3
+ *
4
+ * Refs: docs/prd/PRD-BSO-M9-HARNESS-HOOKS.md §3.4
5
+ *
6
+ * 跟踪每个 harness session 的 brain_run 调用状态:
7
+ * - harness SessionStart hook 调 POST /internal/guard/session-start → 登记 session
8
+ * - agent 走 /brain/run → 触发 markSessionBrainRun(session_id? 或 fallback global last-at)
9
+ * - harness PreToolUse hook 调 GET /internal/guard/check → 判定是否 "recent brain_run"
10
+ *
11
+ * MVP 限制(协议层冻结:不能给 /brain/run 加 session_id 参数):
12
+ * - /brain/run 并不知道当前 harness session_id(agent 不传)
13
+ * - 所以 markSessionBrainRun 只打 global `lastBrainRunAt`
14
+ * - check 逻辑:如果 session 存在且 (last_brain_run_at within windowMs) → 放行
15
+ *
16
+ * 后续迭代(M9.5+)可以加 per-session 精确追踪(当 MCP 协议允许带 session_id 时)。
17
+ */
18
+ export interface GuardSession {
19
+ session_id: string;
20
+ harness?: string;
21
+ cwd?: string;
22
+ started_at: number;
23
+ /** 本 session 期间是否有过 brain_run(false 时 check 看 global fallback) */
24
+ brain_run_called_locally: boolean;
25
+ last_tool_check_at?: number;
26
+ last_pre_tool_blocked_at?: number;
27
+ pre_tool_blocked_count: number;
28
+ }
29
+ export interface GuardStateSnapshot {
30
+ sessions: GuardSession[];
31
+ last_brain_run_at: number | null;
32
+ last_brain_run_skill_id: string | null;
33
+ brain_run_window_ms: number;
34
+ }
35
+ export declare class GuardState {
36
+ private readonly now;
37
+ private readonly windowMs;
38
+ private sessions;
39
+ private lastBrainRunAt;
40
+ private lastBrainRunSkillId;
41
+ constructor(now?: () => number, windowMs?: number);
42
+ startSession(opts: {
43
+ session_id: string;
44
+ harness?: string;
45
+ cwd?: string;
46
+ }): GuardSession;
47
+ /** /brain/run 调用后调。因协议层没带 session_id,这里只更新 global;若 caller 知道 sid 可传入 */
48
+ markBrainRun(skill_id: string | null, session_id?: string): void;
49
+ /**
50
+ * PreToolUse 的核心决策。
51
+ *
52
+ * 放行条件(任一命中即可):
53
+ * A) session 存在,且该 session 本地记录过 brain_run
54
+ * B) 没 session 级记录,但 global lastBrainRunAt 在 windowMs 内
55
+ * C) session 存在且 harness 在 fail-open 白名单(目前无)
56
+ *
57
+ * 拦截:session 不存在 → 放行(hook 层可能在 SessionStart 之前触发)
58
+ * session 存在但两个条件都不满足 → 拦截
59
+ */
60
+ checkPreToolUse(opts: {
61
+ session_id: string;
62
+ tool?: string;
63
+ }): {
64
+ allow: boolean;
65
+ reason: string;
66
+ hint?: string;
67
+ };
68
+ /** PostToolUse:纯 audit,不阻断 */
69
+ recordPostToolUse(opts: {
70
+ session_id: string;
71
+ tool?: string;
72
+ outcome?: 'ok' | 'error';
73
+ }): void;
74
+ snapshot(): GuardStateSnapshot;
75
+ /** 测试/诊断用:清空状态 */
76
+ reset(): void;
77
+ }
78
+ /** 单例(sidecar 进程内共享) */
79
+ export declare const guardState: GuardState;
80
+ //# sourceMappingURL=state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../src/guard/state.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAIH,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,kEAAkE;IAClE,wBAAwB,EAAE,OAAO,CAAC;IAClC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,sBAAsB,EAAE,MAAM,CAAC;CAChC;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,uBAAuB,EAAE,MAAM,GAAG,IAAI,CAAC;IACvC,mBAAmB,EAAE,MAAM,CAAC;CAC7B;AAED,qBAAa,UAAU;IAMnB,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAN3B,OAAO,CAAC,QAAQ,CAAmC;IACnD,OAAO,CAAC,cAAc,CAAuB;IAC7C,OAAO,CAAC,mBAAmB,CAAuB;gBAG/B,GAAG,GAAE,MAAM,MAAyB,EACpC,QAAQ,GAAE,MAAoC;IAGjE,YAAY,CAAC,IAAI,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,YAAY;IAaxF,yEAAyE;IACzE,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI;IAShE;;;;;;;;;;OAUG;IACH,eAAe,CAAC,IAAI,EAAE;QACpB,UAAU,EAAE,MAAM,CAAC;QACnB,IAAI,CAAC,EAAE,MAAM,CAAC;KACf,GAAG;QAAE,KAAK,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE;IAuCrD,8BAA8B;IAC9B,iBAAiB,CAAC,IAAI,EAAE;QACtB,UAAU,EAAE,MAAM,CAAC;QACnB,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,OAAO,CAAC,EAAE,IAAI,GAAG,OAAO,CAAC;KAC1B,GAAG,IAAI;IAMR,QAAQ,IAAI,kBAAkB;IAS9B,kBAAkB;IAClB,KAAK,IAAI,IAAI;CAKd;AAED,wBAAwB;AACxB,eAAO,MAAM,UAAU,YAAmB,CAAC"}
@@ -0,0 +1,119 @@
1
+ /**
2
+ * BSO M9-4 — guard session 状态(in-memory,sidecar 内驻)
3
+ *
4
+ * Refs: docs/prd/PRD-BSO-M9-HARNESS-HOOKS.md §3.4
5
+ *
6
+ * 跟踪每个 harness session 的 brain_run 调用状态:
7
+ * - harness SessionStart hook 调 POST /internal/guard/session-start → 登记 session
8
+ * - agent 走 /brain/run → 触发 markSessionBrainRun(session_id? 或 fallback global last-at)
9
+ * - harness PreToolUse hook 调 GET /internal/guard/check → 判定是否 "recent brain_run"
10
+ *
11
+ * MVP 限制(协议层冻结:不能给 /brain/run 加 session_id 参数):
12
+ * - /brain/run 并不知道当前 harness session_id(agent 不传)
13
+ * - 所以 markSessionBrainRun 只打 global `lastBrainRunAt`
14
+ * - check 逻辑:如果 session 存在且 (last_brain_run_at within windowMs) → 放行
15
+ *
16
+ * 后续迭代(M9.5+)可以加 per-session 精确追踪(当 MCP 协议允许带 session_id 时)。
17
+ */
18
+ const DEFAULT_BRAIN_RUN_WINDOW_MS = 5 * 60 * 1000; // 5 分钟
19
+ export class GuardState {
20
+ now;
21
+ windowMs;
22
+ sessions = new Map();
23
+ lastBrainRunAt = null;
24
+ lastBrainRunSkillId = null;
25
+ constructor(now = () => Date.now(), windowMs = DEFAULT_BRAIN_RUN_WINDOW_MS) {
26
+ this.now = now;
27
+ this.windowMs = windowMs;
28
+ }
29
+ startSession(opts) {
30
+ const sess = {
31
+ session_id: opts.session_id,
32
+ harness: opts.harness,
33
+ cwd: opts.cwd,
34
+ started_at: this.now(),
35
+ brain_run_called_locally: false,
36
+ pre_tool_blocked_count: 0,
37
+ };
38
+ this.sessions.set(opts.session_id, sess);
39
+ return sess;
40
+ }
41
+ /** /brain/run 调用后调。因协议层没带 session_id,这里只更新 global;若 caller 知道 sid 可传入 */
42
+ markBrainRun(skill_id, session_id) {
43
+ this.lastBrainRunAt = this.now();
44
+ this.lastBrainRunSkillId = skill_id;
45
+ if (session_id) {
46
+ const s = this.sessions.get(session_id);
47
+ if (s)
48
+ s.brain_run_called_locally = true;
49
+ }
50
+ }
51
+ /**
52
+ * PreToolUse 的核心决策。
53
+ *
54
+ * 放行条件(任一命中即可):
55
+ * A) session 存在,且该 session 本地记录过 brain_run
56
+ * B) 没 session 级记录,但 global lastBrainRunAt 在 windowMs 内
57
+ * C) session 存在且 harness 在 fail-open 白名单(目前无)
58
+ *
59
+ * 拦截:session 不存在 → 放行(hook 层可能在 SessionStart 之前触发)
60
+ * session 存在但两个条件都不满足 → 拦截
61
+ */
62
+ checkPreToolUse(opts) {
63
+ const sess = this.sessions.get(opts.session_id);
64
+ if (sess)
65
+ sess.last_tool_check_at = this.now();
66
+ if (sess?.brain_run_called_locally) {
67
+ return { allow: true, reason: 'session-local brain_run recorded' };
68
+ }
69
+ if (this.lastBrainRunAt !== null) {
70
+ const age = this.now() - this.lastBrainRunAt;
71
+ if (age <= this.windowMs) {
72
+ return {
73
+ allow: true,
74
+ reason: `global brain_run within ${Math.round(age / 1000)}s (window ${Math.round(this.windowMs / 1000)}s)`,
75
+ };
76
+ }
77
+ }
78
+ // 未登记 session 且没 recent global brain_run → 保守拦截
79
+ if (!sess) {
80
+ // Session 未登记(SessionStart hook 可能被跳过):fail-open 放行 + 登记
81
+ this.startSession({ session_id: opts.session_id });
82
+ return {
83
+ allow: true,
84
+ reason: 'session not previously registered; allowed + registered',
85
+ };
86
+ }
87
+ sess.pre_tool_blocked_count += 1;
88
+ sess.last_pre_tool_blocked_at = this.now();
89
+ return {
90
+ allow: false,
91
+ reason: 'no brain_run recorded for this session (locally or globally within 5min window)',
92
+ hint: '请先调用 SkillFM 的 brain_run(或至少一次 list_skills)让 SkillFM 记录本次编排意图,然后再执行破坏性操作。sidecar 的 9 安全红线仍在兜底。',
93
+ };
94
+ }
95
+ /** PostToolUse:纯 audit,不阻断 */
96
+ recordPostToolUse(opts) {
97
+ const s = this.sessions.get(opts.session_id);
98
+ if (s)
99
+ s.last_tool_check_at = this.now();
100
+ // audit log 目前落 stderr 即可,未来可接 M8 审计 endpoint
101
+ }
102
+ snapshot() {
103
+ return {
104
+ sessions: Array.from(this.sessions.values()),
105
+ last_brain_run_at: this.lastBrainRunAt,
106
+ last_brain_run_skill_id: this.lastBrainRunSkillId,
107
+ brain_run_window_ms: this.windowMs,
108
+ };
109
+ }
110
+ /** 测试/诊断用:清空状态 */
111
+ reset() {
112
+ this.sessions.clear();
113
+ this.lastBrainRunAt = null;
114
+ this.lastBrainRunSkillId = null;
115
+ }
116
+ }
117
+ /** 单例(sidecar 进程内共享) */
118
+ export const guardState = new GuardState();
119
+ //# sourceMappingURL=state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.js","sourceRoot":"","sources":["../../src/guard/state.ts"],"names":[],"mappings":"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"}