progmune-runtime 3.4.0 → 3.5.0
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 +34 -0
- package/README.md +6 -3
- package/README.zh-CN.md +4 -1
- package/dist/agent-cli.js +8 -2
- package/dist/agent-loop.js +20 -11
- package/dist/agent-loop.test.js +1 -0
- package/dist/agent-perception.js +2 -2
- package/dist/agent-perception.test.js +5 -5
- package/dist/agent-permissions.js +159 -0
- package/dist/agent-permissions.test.js +76 -0
- package/dist/audit.js +50 -3
- package/dist/bootstrap-validation.js +15 -0
- package/dist/check.js +62 -9
- package/dist/execute.js +2 -2
- package/dist/extract-ir-python.js +22 -3
- package/dist/extract-project-ir.js +136 -0
- package/dist/extract-project-ir.test.js +116 -0
- package/dist/function-synonyms.test.js +6 -2
- package/dist/mcp-server.mjs +5 -7
- package/dist/patrol-cli.js +14 -2
- package/dist/protocol-registry.js +20 -5
- package/dist/ssg-validator.js +18 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [3.5.0] — 2026-08-22
|
|
4
|
+
|
|
5
|
+
### 新增:多语言合并 IR(注册表式提取)
|
|
6
|
+
|
|
7
|
+
- `src/extract-project-ir.ts`:`LanguageExtractor` 注册表(detect + extract),`extractProjectIR` 合并所有检测到语言的 FunctionInfo——混合项目中 TS 与 Python 函数共存于同一 IR
|
|
8
|
+
- agent loop 感知路径(`extractIRWithDelta`)、`execute()` 的 ir.json 写盘、MCP server 统一走合并入口:Python 项目的函数协议链进入 agent 编排范围(此前 agent 侧 IR 仅 TS)
|
|
9
|
+
- `extractIRPython` 默认写临时文件(可选 `outPath`),不再覆盖项目根 ir.json;单语言提取失败不中断其余语言,全部失败才抛错(保留 execute 硬失败语义)
|
|
10
|
+
- 新增语言(Go/Java/Rust):实现 detect + extract → `LANGUAGE_EXTRACTORS` 注册一条 → 调用方零改动
|
|
11
|
+
|
|
12
|
+
### 修复:function-synonyms 本地超时(遗留)
|
|
13
|
+
|
|
14
|
+
- `runBootstrapValidation` 无参调用结果缓存(同进程复用,语料重度测试 5 次重计算降为 1 次)
|
|
15
|
+
- vitest 改用 forks 池 + 4GB 堆上限(本地语料丰富时 threads 池触 V8 自适应堆上限 OOM);本地 7/7 通过(~24s)
|
|
16
|
+
|
|
17
|
+
### 验证
|
|
18
|
+
|
|
19
|
+
- 相关套件 26/26;`npm run check` 0 失败;合并冒烟:progmune-runtime 自身 4043 函数(TS 1949 + Python 2094)
|
|
20
|
+
|
|
21
|
+
## [3.4.1] — 2026-08-22
|
|
22
|
+
|
|
23
|
+
### 修复:`npm run check` 四项失败根因
|
|
24
|
+
|
|
25
|
+
- protocol-registry:protocols.json 解析加包目录回退——在无协议文件的项目目录下运行时,命名空间初始状态不再退化为仅 `_global`(session 记录与 check 重建的世界一致)
|
|
26
|
+
- checkLedgerConsistency:只比较 ledger 中记录过的非空快照命名空间(早期 session 的空数组/部分命名空间不参与比较)
|
|
27
|
+
- check:历史约定兼容——早期 session 的 `INIT` 初始状态按当前约定(`UNAUTHENTICATED`)规范化比较(只比较、不改盘)
|
|
28
|
+
- audit:`.progmune_allowlist` 祖父条款——存量手写代码一次入册,新文件仍受覆盖率约束
|
|
29
|
+
- 结果:check 从 4 失败 → 0 失败(免疫状态正常),1313/1313 Ledger 全过
|
|
30
|
+
|
|
31
|
+
### 新增:P5 操作级安全层 v1
|
|
32
|
+
|
|
33
|
+
- 权限决策引擎(auto / sandbox / approve / deny 四级)+ patrol / agent 预设
|
|
34
|
+
- FsSandbox 白名单(巡逻报告等产品文件);shell 执行审批门(`--yes` 或交互确认)
|
|
35
|
+
- **commit 恒拒绝且不可被 `--yes` 绕过**(修复信任悖论:自动修复/自动合并永不)
|
|
36
|
+
|
|
3
37
|
## [3.4.0] — 2026-08-21
|
|
4
38
|
|
|
5
39
|
### 新增:Agent 化 P1–P4.5
|
package/README.md
CHANGED
|
@@ -106,6 +106,8 @@ Progmune is honest about what it can and cannot verify.
|
|
|
106
106
|
| **C** | ⚠️ Research-only | Gold benchmark F1=16.5%. L3 cross-function experiment terminated; L4 not planned. See [C Language Status](https://github.com/shenlian19831109/progmune-runtime/blob/main/docs/c-language-status.md). |
|
|
107
107
|
| **Go, Java** | ❌ None | Planned |
|
|
108
108
|
|
|
109
|
+
**Multi-language IR (registry-based).** TypeScript (ts-morph) and Python (AST) extractors are registered entries in one registry (`src/extract-project-ir.ts`); `extractProjectIR` merges every detected language into a single function IR shared by the agent loop, `execute()`, and the MCP server — the agent composes function-protocol chains across both languages. Adding a language (Go, Java, …) = register one extractor entry; callers don't change.
|
|
110
|
+
|
|
109
111
|
**Framework adapters: 2/13.** Express ✅ and tRPC ✅ have dedicated detectors; Next.js has version-aware governance; NestJS is partial. Django, FastAPI and 8 more remain — framework adaptation is the #1 product gap.
|
|
110
112
|
|
|
111
113
|
### What Progmune does NOT cover (honest boundaries)
|
|
@@ -172,9 +174,10 @@ SDK (src/sdk.ts) verify() → APPROVED / NEEDS_REVIEW / BLOCKED
|
|
|
172
174
|
├─ Policy Engine Enterprise policy enforcement (ALLOW/WARN/BLOCK)
|
|
173
175
|
├─ SSG Validator Protocol state machine verification
|
|
174
176
|
├─ Protocol Detector Regex-based protocol step detection (22 detectors)
|
|
175
|
-
├─ IR
|
|
176
|
-
│
|
|
177
|
-
│
|
|
177
|
+
├─ IR Extraction Registry-based: TS (ts-morph) + Python (ast module)
|
|
178
|
+
│ merged into one function IR per project;
|
|
179
|
+
│ source-level markers: taint tracking, import resolution,
|
|
180
|
+
│ qualified call chains, cross-file template analysis
|
|
178
181
|
├─ Repair Executor detect → plan → fix → validate → commit/rollback
|
|
179
182
|
└─ Knowledge Base 31 domains, 140 rules, evidence chains
|
|
180
183
|
```
|
package/README.zh-CN.md
CHANGED
|
@@ -106,6 +106,8 @@ Progmune 对能验证什么、不能验证什么保持诚实。
|
|
|
106
106
|
| **C** | ⚠️ 仅研究 | 黄金基准 F1=16.5%。L3 跨函数实验已终止;L4 无计划。见 [C 语言状态](https://github.com/shenlian19831109/progmune-runtime/blob/main/docs/c-language-status.md)。 |
|
|
107
107
|
| **Go, Java** | ❌ 无 | 规划中 |
|
|
108
108
|
|
|
109
|
+
**多语言 IR(注册表式)。** TypeScript(ts-morph)与 Python(AST)提取器是同一注册表(`src/extract-project-ir.ts`)中的注册项;`extractProjectIR` 把检测到的所有语言合并为一份函数 IR,由 agent loop、`execute()` 与 MCP server 共享——agent 可在两种语言上编排函数协议链。新增语言(Go、Java……)= 注册一条提取器,调用方零改动。
|
|
110
|
+
|
|
109
111
|
**框架适配器:2/13。** Express ✅ 与 tRPC ✅ 有专用检测器;Next.js 有版本感知治理;NestJS 部分支持。Django、FastAPI 及另外 8 个待适配——框架适配是 #1 产品缺口。
|
|
110
112
|
|
|
111
113
|
### Progmune 不覆盖什么(诚实边界)
|
|
@@ -172,7 +174,8 @@ SDK (src/sdk.ts) verify() → APPROVED / NEEDS_REVIEW / BLOCKED
|
|
|
172
174
|
├─ 策略引擎 企业策略执行(ALLOW/WARN/BLOCK)
|
|
173
175
|
├─ SSG 校验器 协议状态机验证
|
|
174
176
|
├─ 协议检测器 基于正则的协议步骤检测(22 个检测器)
|
|
175
|
-
├─ IR
|
|
177
|
+
├─ IR 提取 注册表式:TS(ts-morph)+ Python(ast 模块)
|
|
178
|
+
│ 合并为每项目一份函数 IR;
|
|
176
179
|
│ 源码级标记:污点追踪、import 解析、限定调用链、跨文件模板分析
|
|
177
180
|
├─ 修复执行器 detect → plan → fix → validate → commit/rollback
|
|
178
181
|
└─ 知识库 31 个域、140 条规则、证据链
|
package/dist/agent-cli.js
CHANGED
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
* --timeout <ms> 单次执行超时毫秒(默认 120000)
|
|
19
19
|
* --context 注入 git 仓库上下文(默认开启)
|
|
20
20
|
* --no-context 关闭 git 上下文注入
|
|
21
|
-
* --test
|
|
21
|
+
* --test 编译/指纹通过后追加项目测试门(shell 执行需审批)
|
|
22
|
+
* --yes 预批准审批门(配合 --test;无此参数时交互确认)
|
|
22
23
|
* --json JSON 输出
|
|
23
24
|
* --help, -h 显示帮助
|
|
24
25
|
*/
|
|
@@ -95,6 +96,7 @@ function parseArgs(argv) {
|
|
|
95
96
|
let timeout = 120000;
|
|
96
97
|
let context = true;
|
|
97
98
|
let test = false;
|
|
99
|
+
let yes = false;
|
|
98
100
|
let json = false;
|
|
99
101
|
for (let i = 0; i < argv.length; i++) {
|
|
100
102
|
const a = argv[i];
|
|
@@ -122,6 +124,9 @@ function parseArgs(argv) {
|
|
|
122
124
|
else if (a === "--test") {
|
|
123
125
|
test = true;
|
|
124
126
|
}
|
|
127
|
+
else if (a === "--yes") {
|
|
128
|
+
yes = true;
|
|
129
|
+
}
|
|
125
130
|
else if (a === "--json") {
|
|
126
131
|
json = true;
|
|
127
132
|
}
|
|
@@ -134,7 +139,7 @@ function parseArgs(argv) {
|
|
|
134
139
|
console.error("❌ 缺少意图参数。用法: progmune agent \"实现 XX\" [--file path] [--project dir]");
|
|
135
140
|
process.exit(2);
|
|
136
141
|
}
|
|
137
|
-
return { intent, file, project, iterations, retries, timeout, context, test, json };
|
|
142
|
+
return { intent, file, project, iterations, retries, timeout, context, test, yes, json };
|
|
138
143
|
}
|
|
139
144
|
// ── Formatting ──
|
|
140
145
|
function printAttempt(i, r) {
|
|
@@ -192,6 +197,7 @@ async function main() {
|
|
|
192
197
|
timeoutMs: opts.timeout,
|
|
193
198
|
includeContext: opts.context,
|
|
194
199
|
runTests: opts.test,
|
|
200
|
+
approveExec: opts.yes,
|
|
195
201
|
});
|
|
196
202
|
if (opts.json) {
|
|
197
203
|
console.log(JSON.stringify(result, null, 2));
|
package/dist/agent-loop.js
CHANGED
|
@@ -62,6 +62,7 @@ const execute_1 = require("./execute");
|
|
|
62
62
|
const goal_planner_1 = require("./goal-planner");
|
|
63
63
|
const agent_perception_1 = require("./agent-perception");
|
|
64
64
|
const agent_supervision_1 = require("./agent-supervision");
|
|
65
|
+
const agent_permissions_1 = require("./agent-permissions");
|
|
65
66
|
// ── Helpers ──
|
|
66
67
|
/** 单次执行超时包装。超时后底层 promise 继续运行(P1 已知限制,文档化即可)。 */
|
|
67
68
|
function withTimeout(p, ms, label) {
|
|
@@ -216,18 +217,26 @@ async function runAgentLoop(opts) {
|
|
|
216
217
|
let testPass = true;
|
|
217
218
|
let testFailureSummary = "";
|
|
218
219
|
if (result.success && compilePass && markerPass && runTestsGate && filePath) {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
audit("verify:test", t.pass ? `测试通过 (${t.command})` : `测试失败: ${t.failures.slice(0, 3).join(" | ")}`);
|
|
225
|
-
if (!t.pass)
|
|
226
|
-
testFailureSummary = `项目测试失败: ${t.failures.slice(0, 3).join(";")}`;
|
|
227
|
-
}
|
|
220
|
+
// ── P5 安全层:跑测试 = shell 执行 → 审批门 ──
|
|
221
|
+
const execDecision = (0, agent_permissions_1.decidePermission)("agent", { level: "exec", target: "项目测试(npm test / pytest)", projectPath, preApproved: opts.approveExec }, agent_permissions_1.interactiveConfirm);
|
|
222
|
+
audit(execDecision.audit.event, execDecision.audit.detail);
|
|
223
|
+
if (!execDecision.allowed) {
|
|
224
|
+
audit("verify:test", "测试门被审批门拒绝,跳过(--yes 可预批准)");
|
|
228
225
|
}
|
|
229
|
-
|
|
230
|
-
|
|
226
|
+
else {
|
|
227
|
+
try {
|
|
228
|
+
const t = (0, agent_supervision_1.runProjectTests)(projectPath);
|
|
229
|
+
testRan = t.ran;
|
|
230
|
+
testPass = t.pass;
|
|
231
|
+
if (t.ran) {
|
|
232
|
+
audit("verify:test", t.pass ? `测试通过 (${t.command})` : `测试失败: ${t.failures.slice(0, 3).join(" | ")}`);
|
|
233
|
+
if (!t.pass)
|
|
234
|
+
testFailureSummary = `项目测试失败: ${t.failures.slice(0, 3).join(";")}`;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
catch (e) {
|
|
238
|
+
audit("verify:test", `测试门异常(忽略): ${e.message}`);
|
|
239
|
+
}
|
|
231
240
|
}
|
|
232
241
|
}
|
|
233
242
|
const attempt = {
|
package/dist/agent-loop.test.js
CHANGED
|
@@ -207,6 +207,7 @@ function failResult(error) {
|
|
|
207
207
|
intent: "实现登录流程",
|
|
208
208
|
filePath: "login.ts",
|
|
209
209
|
runTests: true,
|
|
210
|
+
approveExec: true, // P5:测试门 shell 执行需审批(--yes)
|
|
210
211
|
});
|
|
211
212
|
(0, vitest_1.expect)(r.success).toBe(true);
|
|
212
213
|
(0, vitest_1.expect)(r.attempts).toHaveLength(2);
|
package/dist/agent-perception.js
CHANGED
|
@@ -49,7 +49,7 @@ exports.extractIRWithDelta = extractIRWithDelta;
|
|
|
49
49
|
const fs = __importStar(require("fs"));
|
|
50
50
|
const path = __importStar(require("path"));
|
|
51
51
|
const child_process_1 = require("child_process");
|
|
52
|
-
const
|
|
52
|
+
const extract_project_ir_1 = require("./extract-project-ir");
|
|
53
53
|
// ── Git Context ──
|
|
54
54
|
function git(projectPath, args) {
|
|
55
55
|
return (0, child_process_1.execSync)(`git -C "${projectPath}" ${args}`, {
|
|
@@ -125,7 +125,7 @@ function listSourceFiles(projectPath) {
|
|
|
125
125
|
* prevNames 缺省时只返回全量 IR(delta 为空)。
|
|
126
126
|
*/
|
|
127
127
|
function extractIRWithDelta(projectPath, prevNames) {
|
|
128
|
-
const ir = (0,
|
|
128
|
+
const ir = (0, extract_project_ir_1.extractProjectIR)(projectPath);
|
|
129
129
|
const names = ir.map((f) => String(f.name || "")).filter(Boolean);
|
|
130
130
|
if (!prevNames) {
|
|
131
131
|
return { ir, delta: { added: [], removed: [], functionCount: names.length } };
|
|
@@ -41,16 +41,16 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
41
41
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
42
|
const vitest_1 = require("vitest");
|
|
43
43
|
const child_process_1 = require("child_process");
|
|
44
|
-
const
|
|
44
|
+
const extract_project_ir_1 = require("./extract-project-ir");
|
|
45
45
|
const agent_perception_1 = require("./agent-perception");
|
|
46
46
|
vitest_1.vi.mock("child_process", () => ({
|
|
47
47
|
execSync: vitest_1.vi.fn(),
|
|
48
48
|
}));
|
|
49
|
-
vitest_1.vi.mock("./extract-ir", () => ({
|
|
50
|
-
|
|
49
|
+
vitest_1.vi.mock("./extract-project-ir", () => ({
|
|
50
|
+
extractProjectIR: vitest_1.vi.fn(),
|
|
51
51
|
}));
|
|
52
52
|
const mockExecSync = vitest_1.vi.mocked(child_process_1.execSync);
|
|
53
|
-
const
|
|
53
|
+
const mockExtractProjectIR = vitest_1.vi.mocked(extract_project_ir_1.extractProjectIR);
|
|
54
54
|
(0, vitest_1.beforeEach)(() => {
|
|
55
55
|
vitest_1.vi.clearAllMocks();
|
|
56
56
|
});
|
|
@@ -82,7 +82,7 @@ const mockExtractIR = vitest_1.vi.mocked(extract_ir_1.extractIR);
|
|
|
82
82
|
(0, vitest_1.expect)(ctx.error).toContain("not a git repository");
|
|
83
83
|
});
|
|
84
84
|
(0, vitest_1.it)("extractIRWithDelta 计算新增/消失函数差集", () => {
|
|
85
|
-
|
|
85
|
+
mockExtractProjectIR.mockReturnValue([
|
|
86
86
|
{ name: "verify_password" },
|
|
87
87
|
{ name: "main" },
|
|
88
88
|
]);
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Phase 12: Agent 操作级安全层 (P5 v1) —— 沙箱 + 审批门
|
|
4
|
+
*
|
|
5
|
+
* 设计文档 P5 决策结果:DSH(deepseek-harness)不可验证/不存在,
|
|
6
|
+
* 按「独立立项」自建最小可用集:
|
|
7
|
+
*
|
|
8
|
+
* 读(read) → 自动
|
|
9
|
+
* 写(write) → 必须经验证门(execute 的 8 门 + 编译/指纹;巡逻报告走 FsSandbox 白名单)
|
|
10
|
+
* 跑 shell(exec) → 审批门(交互确认或 --yes)
|
|
11
|
+
* 提交(commit) → 审批门(本版默认拒绝,需显式 --approve-commit)
|
|
12
|
+
*
|
|
13
|
+
* 修复信任悖论:自动修复/自动合并 = 永不(autoApplied 恒 false,由 agent-patrol 保证)。
|
|
14
|
+
* 每次权限决策产出审计事件,供 loop/patrol 审计轨迹复用。
|
|
15
|
+
*/
|
|
16
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
17
|
+
if (k2 === undefined) k2 = k;
|
|
18
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
19
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
20
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
21
|
+
}
|
|
22
|
+
Object.defineProperty(o, k2, desc);
|
|
23
|
+
}) : (function(o, m, k, k2) {
|
|
24
|
+
if (k2 === undefined) k2 = k;
|
|
25
|
+
o[k2] = m[k];
|
|
26
|
+
}));
|
|
27
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
28
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
29
|
+
}) : function(o, v) {
|
|
30
|
+
o["default"] = v;
|
|
31
|
+
});
|
|
32
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
33
|
+
var ownKeys = function(o) {
|
|
34
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
35
|
+
var ar = [];
|
|
36
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
37
|
+
return ar;
|
|
38
|
+
};
|
|
39
|
+
return ownKeys(o);
|
|
40
|
+
};
|
|
41
|
+
return function (mod) {
|
|
42
|
+
if (mod && mod.__esModule) return mod;
|
|
43
|
+
var result = {};
|
|
44
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
45
|
+
__setModuleDefault(result, mod);
|
|
46
|
+
return result;
|
|
47
|
+
};
|
|
48
|
+
})();
|
|
49
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
50
|
+
exports.PRESET_AGENT = exports.PRESET_PATROL = void 0;
|
|
51
|
+
exports.checkSandboxWrite = checkSandboxWrite;
|
|
52
|
+
exports.decidePermission = decidePermission;
|
|
53
|
+
exports.interactiveConfirm = interactiveConfirm;
|
|
54
|
+
const fs = __importStar(require("fs"));
|
|
55
|
+
const path = __importStar(require("path"));
|
|
56
|
+
// ── Presets ──
|
|
57
|
+
exports.PRESET_PATROL = {
|
|
58
|
+
read: "auto",
|
|
59
|
+
write: "sandbox", // 只允许 .progmune_* 报告文件
|
|
60
|
+
exec: "deny", // 巡逻不执行 shell
|
|
61
|
+
commit: "deny",
|
|
62
|
+
};
|
|
63
|
+
exports.PRESET_AGENT = {
|
|
64
|
+
read: "auto",
|
|
65
|
+
// 写文件的安全由 execute 内置验证门保证(8 门 + SSG + 编译/指纹,免疫门在环内)——
|
|
66
|
+
// 权限层不重复设门,避免双重审批拖垮自主循环。
|
|
67
|
+
write: "auto",
|
|
68
|
+
exec: "approve", // 跑测试等 shell 操作需审批(--yes 或交互确认)
|
|
69
|
+
commit: "deny", // P5 v1 不自动 commit
|
|
70
|
+
};
|
|
71
|
+
function presetOf(name) {
|
|
72
|
+
return name === "patrol" ? exports.PRESET_PATROL : exports.PRESET_AGENT;
|
|
73
|
+
}
|
|
74
|
+
// ── FsSandbox ──
|
|
75
|
+
/** 巡逻报告等产品文件白名单(相对项目根) */
|
|
76
|
+
const SANDBOX_ALLOWED_FILES = new Set([".progmune_patrol_report.md", ".progmune_patrol_report.json"]);
|
|
77
|
+
/** 判断目标路径是否在项目目录内。 */
|
|
78
|
+
function isInsideProject(projectPath, target) {
|
|
79
|
+
const absProject = path.resolve(projectPath);
|
|
80
|
+
const absTarget = path.resolve(target);
|
|
81
|
+
return absTarget === absProject || absTarget.startsWith(absProject + path.sep);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* 沙箱写判定:仅允许项目内 + 白名单文件。
|
|
85
|
+
*/
|
|
86
|
+
function checkSandboxWrite(ctx) {
|
|
87
|
+
const absProject = path.resolve(ctx.projectPath);
|
|
88
|
+
const rel = path.relative(absProject, path.resolve(ctx.target));
|
|
89
|
+
const allowed = isInsideProject(absProject, ctx.target) && SANDBOX_ALLOWED_FILES.has(rel.replace(/\\/g, "/"));
|
|
90
|
+
return {
|
|
91
|
+
allowed,
|
|
92
|
+
level: "write",
|
|
93
|
+
detail: allowed
|
|
94
|
+
? `sandbox: 项目内白名单文件 ${rel}`
|
|
95
|
+
: `sandbox: 拒绝写入 ${rel}(不在白名单或超出项目边界)`,
|
|
96
|
+
audit: { event: "permission:sandbox", detail: allowed ? `allow write ${rel}` : `deny write ${rel}` },
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
// ── Decision Engine ──
|
|
100
|
+
/**
|
|
101
|
+
* 按预设判定一次操作。规则:
|
|
102
|
+
* auto → 允许
|
|
103
|
+
* sandbox → FsSandbox 白名单判定
|
|
104
|
+
* approve → 审批门(preApproved 或交互确认)
|
|
105
|
+
* deny → 拒绝
|
|
106
|
+
*/
|
|
107
|
+
function decidePermission(preset, ctx, confirmFn) {
|
|
108
|
+
const mode = presetOf(preset)[ctx.level];
|
|
109
|
+
if (mode === "auto") {
|
|
110
|
+
return {
|
|
111
|
+
allowed: true,
|
|
112
|
+
level: ctx.level,
|
|
113
|
+
detail: `auto: ${ctx.level} ${ctx.target}`,
|
|
114
|
+
audit: { event: "permission:auto", detail: `${ctx.level} ${ctx.target}` },
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
if (mode === "sandbox") {
|
|
118
|
+
return checkSandboxWrite(ctx);
|
|
119
|
+
}
|
|
120
|
+
if (mode === "approve") {
|
|
121
|
+
const prompt = `审批请求: ${ctx.level} → ${ctx.target}`;
|
|
122
|
+
const approved = ctx.preApproved === true || (confirmFn ? confirmFn(prompt) : false);
|
|
123
|
+
return {
|
|
124
|
+
allowed: approved,
|
|
125
|
+
level: ctx.level,
|
|
126
|
+
detail: approved ? `approve: ${ctx.level} ${ctx.target}` : `审批未通过: ${ctx.level} ${ctx.target}`,
|
|
127
|
+
audit: { event: "permission:approve", detail: approved ? `approved ${ctx.level} ${ctx.target}` : `denied ${ctx.level} ${ctx.target}` },
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
allowed: false,
|
|
132
|
+
level: ctx.level,
|
|
133
|
+
detail: `deny: ${ctx.level} ${ctx.target}(P5 v1 默认拒绝,需显式升级预设)`,
|
|
134
|
+
audit: { event: "permission:deny", detail: `${ctx.level} ${ctx.target}` },
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
// ── Interactive confirm (TTY) ──
|
|
138
|
+
/** 交互确认:读 stdin 一行,y/yes 为同意。无 TTY 时返回 false。 */
|
|
139
|
+
function interactiveConfirm(prompt) {
|
|
140
|
+
if (!process.stdin.isTTY)
|
|
141
|
+
return false;
|
|
142
|
+
const fd = fs.openSync("/dev/tty", "r"); // 直读 tty,绕过管道 stdin
|
|
143
|
+
try {
|
|
144
|
+
process.stdout.write(`${prompt} [y/N] `);
|
|
145
|
+
const buf = Buffer.alloc(16);
|
|
146
|
+
const n = fs.readSync(fd, buf, 0, 16, null);
|
|
147
|
+
const answer = buf.toString("utf-8", 0, n).trim().toLowerCase();
|
|
148
|
+
return answer === "y" || answer === "yes";
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
finally {
|
|
154
|
+
try {
|
|
155
|
+
fs.closeSync(fd);
|
|
156
|
+
}
|
|
157
|
+
catch { /* ignore */ }
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Phase 12: 操作级安全层测试 (P5 v1)
|
|
4
|
+
*
|
|
5
|
+
* 预设判定 / FsSandbox 白名单 / 审批门 / deny 默认——全部纯函数,不触真实 FS。
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
const vitest_1 = require("vitest");
|
|
9
|
+
const agent_permissions_1 = require("./agent-permissions");
|
|
10
|
+
(0, vitest_1.describe)("agent-permissions", () => {
|
|
11
|
+
(0, vitest_1.it)("auto 操作直接允许(读、agent 写)", () => {
|
|
12
|
+
const read = (0, agent_permissions_1.decidePermission)("patrol", { level: "read", target: "src/auth.ts", projectPath: "/p" });
|
|
13
|
+
(0, vitest_1.expect)(read.allowed).toBe(true);
|
|
14
|
+
(0, vitest_1.expect)(read.audit.event).toBe("permission:auto");
|
|
15
|
+
// agent 预设:写 = auto(安全由 execute 验证门保证)
|
|
16
|
+
const write = (0, agent_permissions_1.decidePermission)("agent", { level: "write", target: "out.ts", projectPath: "/p" });
|
|
17
|
+
(0, vitest_1.expect)(write.allowed).toBe(true);
|
|
18
|
+
});
|
|
19
|
+
(0, vitest_1.it)("sandbox 写:白名单内项目文件允许,越界或非白名单拒绝", () => {
|
|
20
|
+
const ok = (0, agent_permissions_1.checkSandboxWrite)({
|
|
21
|
+
level: "write",
|
|
22
|
+
target: "/p/.progmune_patrol_report.md",
|
|
23
|
+
projectPath: "/p",
|
|
24
|
+
});
|
|
25
|
+
(0, vitest_1.expect)(ok.allowed).toBe(true);
|
|
26
|
+
const outside = (0, agent_permissions_1.checkSandboxWrite)({
|
|
27
|
+
level: "write",
|
|
28
|
+
target: "/etc/passwd",
|
|
29
|
+
projectPath: "/p",
|
|
30
|
+
});
|
|
31
|
+
(0, vitest_1.expect)(outside.allowed).toBe(false);
|
|
32
|
+
(0, vitest_1.expect)(outside.detail).toContain("拒绝");
|
|
33
|
+
const notWhitelisted = (0, agent_permissions_1.checkSandboxWrite)({
|
|
34
|
+
level: "write",
|
|
35
|
+
target: "/p/src/auth.ts",
|
|
36
|
+
projectPath: "/p",
|
|
37
|
+
});
|
|
38
|
+
(0, vitest_1.expect)(notWhitelisted.allowed).toBe(false);
|
|
39
|
+
});
|
|
40
|
+
(0, vitest_1.it)("审批门:preApproved 或 confirmFn 同意才放行", () => {
|
|
41
|
+
const denied = (0, agent_permissions_1.decidePermission)("agent", { level: "exec", target: "npm test", projectPath: "/p" });
|
|
42
|
+
(0, vitest_1.expect)(denied.allowed).toBe(false);
|
|
43
|
+
(0, vitest_1.expect)(denied.audit.event).toBe("permission:approve");
|
|
44
|
+
const approved = (0, agent_permissions_1.decidePermission)("agent", {
|
|
45
|
+
level: "exec", target: "npm test", projectPath: "/p", preApproved: true,
|
|
46
|
+
});
|
|
47
|
+
(0, vitest_1.expect)(approved.allowed).toBe(true);
|
|
48
|
+
const confirmed = (0, agent_permissions_1.decidePermission)("agent", { level: "exec", target: "npm test", projectPath: "/p" }, () => true);
|
|
49
|
+
(0, vitest_1.expect)(confirmed.allowed).toBe(true);
|
|
50
|
+
const rejected = (0, agent_permissions_1.decidePermission)("agent", { level: "exec", target: "npm test", projectPath: "/p" }, () => false);
|
|
51
|
+
(0, vitest_1.expect)(rejected.allowed).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
(0, vitest_1.it)("deny 默认:巡逻 exec/commit 与 agent commit 一律拒绝(修复信任悖论)", () => {
|
|
54
|
+
(0, vitest_1.expect)((0, agent_permissions_1.decidePermission)("patrol", { level: "exec", target: "tsc", projectPath: "/p" }).allowed).toBe(false);
|
|
55
|
+
(0, vitest_1.expect)((0, agent_permissions_1.decidePermission)("patrol", { level: "commit", target: "git commit", projectPath: "/p" }).allowed).toBe(false);
|
|
56
|
+
(0, vitest_1.expect)((0, agent_permissions_1.decidePermission)("agent", { level: "commit", target: "git commit", projectPath: "/p" }).allowed).toBe(false);
|
|
57
|
+
// 即使 --yes 也不能绕过 deny
|
|
58
|
+
(0, vitest_1.expect)((0, agent_permissions_1.decidePermission)("agent", {
|
|
59
|
+
level: "commit", target: "git commit", projectPath: "/p", preApproved: true,
|
|
60
|
+
}).allowed).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
(0, vitest_1.it)("预设表结构完整(四种级别齐全)", () => {
|
|
63
|
+
const levels = ["read", "write", "exec", "commit"];
|
|
64
|
+
for (const l of levels) {
|
|
65
|
+
(0, vitest_1.expect)(agent_permissions_1.PRESET_PATROL[l]).toBeDefined();
|
|
66
|
+
(0, vitest_1.expect)(agent_permissions_1.PRESET_AGENT[l]).toBeDefined();
|
|
67
|
+
}
|
|
68
|
+
// 修复信任悖论:两个预设的 commit 都不可自动放行
|
|
69
|
+
(0, vitest_1.expect)(agent_permissions_1.PRESET_PATROL.commit).toBe("deny");
|
|
70
|
+
(0, vitest_1.expect)(agent_permissions_1.PRESET_AGENT.commit).toBe("deny");
|
|
71
|
+
});
|
|
72
|
+
(0, vitest_1.it)("审批未通过时审计事件带 denied 标记", () => {
|
|
73
|
+
const d = (0, agent_permissions_1.decidePermission)("agent", { level: "exec", target: "pytest", projectPath: "/p" });
|
|
74
|
+
(0, vitest_1.expect)(d.audit.detail).toContain("denied");
|
|
75
|
+
});
|
|
76
|
+
});
|
package/dist/audit.js
CHANGED
|
@@ -66,7 +66,11 @@ function auditDirectory(dir, threshold = DEFAULT_THRESHOLD) {
|
|
|
66
66
|
if (!stat.isDirectory()) {
|
|
67
67
|
return result;
|
|
68
68
|
}
|
|
69
|
-
|
|
69
|
+
// Allowlist(.progmune_allowlist):豁免名单中的文件不参与覆盖率分母——
|
|
70
|
+
// 祖父条款:存量手写代码一次入册,新文件仍受覆盖要求约束。
|
|
71
|
+
// 匹配相对 allowlist 所在目录(通常为项目根),而非被扫描目录。
|
|
72
|
+
const allowlist = loadAllowlist(dir);
|
|
73
|
+
scanDir(dir, dir, result, allowlist);
|
|
70
74
|
result.coverage = result.totalFiles > 0
|
|
71
75
|
? result.progmuneFiles / result.totalFiles
|
|
72
76
|
: 0;
|
|
@@ -84,7 +88,46 @@ function auditDirectory(dir, threshold = DEFAULT_THRESHOLD) {
|
|
|
84
88
|
}
|
|
85
89
|
const MARKER_REGEX = /@progmune-generated\s+session=(\S+)(?:\s+timestamp=(\S+))?(?:\s+ruleHash=(\S+))?/;
|
|
86
90
|
const EXCLUDED_DIRS = new Set(["node_modules", ".git", "dist", ".progmune_corpus", ".progmune_memory"]);
|
|
87
|
-
|
|
91
|
+
/** 读取 .progmune_allowlist:从被扫描目录向上查找,返回匹配函数(相对 allowlist 所在目录)。 */
|
|
92
|
+
function loadAllowlist(dir) {
|
|
93
|
+
let root = dir;
|
|
94
|
+
let allowlistPath = "";
|
|
95
|
+
for (let i = 0; i < 4; i++) {
|
|
96
|
+
const p = path.join(root, ".progmune_allowlist");
|
|
97
|
+
if (fs.existsSync(p)) {
|
|
98
|
+
allowlistPath = p;
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
const parent = path.dirname(root);
|
|
102
|
+
if (parent === root)
|
|
103
|
+
break;
|
|
104
|
+
root = parent;
|
|
105
|
+
}
|
|
106
|
+
if (!allowlistPath)
|
|
107
|
+
return { root: dir, match: () => false };
|
|
108
|
+
try {
|
|
109
|
+
const patterns = fs.readFileSync(allowlistPath, "utf-8")
|
|
110
|
+
.split("\n")
|
|
111
|
+
.map((l) => l.trim())
|
|
112
|
+
.filter((l) => l.length > 0 && !l.startsWith("#"));
|
|
113
|
+
return {
|
|
114
|
+
root: path.dirname(allowlistPath),
|
|
115
|
+
match: (relPath) => {
|
|
116
|
+
const normalized = relPath.replace(/\\/g, "/");
|
|
117
|
+
return patterns.some((p) => {
|
|
118
|
+
const pat = p.replace(/^\.\//, ""); // 兼容 "./foo.ts" 写法
|
|
119
|
+
if (pat.endsWith("*"))
|
|
120
|
+
return normalized.startsWith(pat.slice(0, -1));
|
|
121
|
+
return normalized === pat || normalized.startsWith(pat.replace(/\/$/, "") + "/");
|
|
122
|
+
});
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return { root: dir, match: () => false };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function scanDir(rootDir, currentDir, result, allowlist) {
|
|
88
131
|
let entries;
|
|
89
132
|
try {
|
|
90
133
|
entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
@@ -96,7 +139,7 @@ function scanDir(rootDir, currentDir, result) {
|
|
|
96
139
|
const fullPath = path.join(currentDir, entry.name);
|
|
97
140
|
if (entry.isDirectory()) {
|
|
98
141
|
if (!EXCLUDED_DIRS.has(entry.name) && !entry.name.startsWith(".")) {
|
|
99
|
-
scanDir(rootDir, fullPath, result);
|
|
142
|
+
scanDir(rootDir, fullPath, result, allowlist);
|
|
100
143
|
}
|
|
101
144
|
continue;
|
|
102
145
|
}
|
|
@@ -104,6 +147,10 @@ function scanDir(rootDir, currentDir, result) {
|
|
|
104
147
|
if (!entry.name.endsWith(".ts") && !entry.name.endsWith(".tsx") && !entry.name.endsWith(".mjs")) {
|
|
105
148
|
continue;
|
|
106
149
|
}
|
|
150
|
+
// 豁免名单(祖父条款):不在覆盖率分母内——相对 allowlist 根匹配
|
|
151
|
+
const rel = path.relative(allowlist.root, fullPath);
|
|
152
|
+
if (allowlist.match(rel))
|
|
153
|
+
continue;
|
|
107
154
|
result.totalFiles++;
|
|
108
155
|
try {
|
|
109
156
|
const content = fs.readFileSync(fullPath, "utf-8");
|
|
@@ -69,8 +69,23 @@ function buildPath(startFn, rules, visited) {
|
|
|
69
69
|
}
|
|
70
70
|
/**
|
|
71
71
|
* Run the bootstrap validation experiment.
|
|
72
|
+
*
|
|
73
|
+
* 无参调用结果缓存(纯计算、确定性):同进程内多次调用复用同一结果。
|
|
74
|
+
* 运行时与本地语料规模线性相关(语料丰富时可 >30s / 2GB 堆)——
|
|
75
|
+
* function-synonyms 等测试会 5 次重计算,缓存降为 1 次。
|
|
72
76
|
*/
|
|
77
|
+
let _defaultResultCache = null;
|
|
73
78
|
async function runBootstrapValidation(existingRules, extraSequences) {
|
|
79
|
+
if (!existingRules && !extraSequences) {
|
|
80
|
+
if (!_defaultResultCache) {
|
|
81
|
+
_defaultResultCache = runBootstrapValidationUncached();
|
|
82
|
+
}
|
|
83
|
+
return _defaultResultCache;
|
|
84
|
+
}
|
|
85
|
+
return runBootstrapValidationUncached(existingRules, extraSequences);
|
|
86
|
+
}
|
|
87
|
+
/** 原实现(无缓存)。 */
|
|
88
|
+
async function runBootstrapValidationUncached(existingRules, extraSequences) {
|
|
74
89
|
// 1. Ground truth
|
|
75
90
|
const defs = (0, protocol_coverage_1.loadDefaultProtocolDefinitions)();
|
|
76
91
|
const originalRules = existingRules || new Map();
|
package/dist/check.js
CHANGED
|
@@ -117,10 +117,16 @@ if (cliArg === "--ledger") {
|
|
|
117
117
|
const rebuilt = (0, ssg_validator_1.rebuildState)(allTransitions, nsInit);
|
|
118
118
|
const lastTransition = allTransitions[allTransitions.length - 1];
|
|
119
119
|
const recorded = lastTransition.statesAfter;
|
|
120
|
-
|
|
120
|
+
// 与 checkLedgerConsistency 同规则:只比较 recorded 中有过非空快照的 ns——
|
|
121
|
+
// 早期 session 对 file/db 等记录空数组(无信息),不参与比较。
|
|
122
|
+
const informativeNs = new Set();
|
|
123
|
+
for (const [ns, s] of Object.entries(recorded)) {
|
|
124
|
+
if ((s || []).length > 0)
|
|
125
|
+
informativeNs.add(ns);
|
|
126
|
+
}
|
|
121
127
|
const norm = (snap) => {
|
|
122
128
|
const out = {};
|
|
123
|
-
for (const ns of [...
|
|
129
|
+
for (const ns of [...informativeNs].sort())
|
|
124
130
|
out[ns] = [...(snap[ns] || [])].sort();
|
|
125
131
|
return out;
|
|
126
132
|
};
|
|
@@ -307,6 +313,35 @@ else {
|
|
|
307
313
|
}
|
|
308
314
|
}
|
|
309
315
|
// ── 4. Ledger 不变量检查 (Phase 3) ──
|
|
316
|
+
/**
|
|
317
|
+
* 历史约定兼容:早期 session 以 "INIT" 作为 _global 初始状态
|
|
318
|
+
* (当前约定 UNAUTHENTICATED;当前 148 条协议规则已不含 INIT 状态)。
|
|
319
|
+
* 返回规范化视图(仅比较用,不改盘)与受影响转移数。
|
|
320
|
+
*/
|
|
321
|
+
function normalizeLegacyInit(transitions, nsInit) {
|
|
322
|
+
let migrated = 0;
|
|
323
|
+
const view = transitions.map((t) => {
|
|
324
|
+
const normSnap = (snap) => {
|
|
325
|
+
const out = {};
|
|
326
|
+
let changed = false;
|
|
327
|
+
for (const [ns, states] of Object.entries(snap)) {
|
|
328
|
+
const currentInit = nsInit.get(ns);
|
|
329
|
+
if (currentInit && currentInit !== "INIT" && states.length === 1 && states[0] === "INIT") {
|
|
330
|
+
out[ns] = [currentInit];
|
|
331
|
+
changed = true;
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
out[ns] = [...states];
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (changed)
|
|
338
|
+
migrated++;
|
|
339
|
+
return out;
|
|
340
|
+
};
|
|
341
|
+
return { ...t, statesBefore: normSnap(t.statesBefore), statesAfter: normSnap(t.statesAfter) };
|
|
342
|
+
});
|
|
343
|
+
return { view, migrated };
|
|
344
|
+
}
|
|
310
345
|
step("4/6 Ledger 不变量");
|
|
311
346
|
{
|
|
312
347
|
// Load namespace initial states from protocols.json for correct replay
|
|
@@ -314,6 +349,7 @@ step("4/6 Ledger 不变量");
|
|
|
314
349
|
let checked = 0;
|
|
315
350
|
let consistent = 0;
|
|
316
351
|
let stateMatch = 0;
|
|
352
|
+
let legacyInitMigrations = 0;
|
|
317
353
|
const allLedgers = [];
|
|
318
354
|
const violationsDetail = [];
|
|
319
355
|
const replayMismatchDetail = [];
|
|
@@ -335,9 +371,15 @@ step("4/6 Ledger 不变量");
|
|
|
335
371
|
const transitions = attempt.transitions || [];
|
|
336
372
|
if (transitions.length === 0)
|
|
337
373
|
continue;
|
|
338
|
-
|
|
374
|
+
// 历史约定兼容:早期 session 以 "INIT" 作为 _global 初始状态
|
|
375
|
+
// (当前约定 UNAUTHENTICATED;当前协议规则已不含 INIT)。
|
|
376
|
+
// 规范化视图只用于比较,不改盘。
|
|
377
|
+
const { view, migrated } = normalizeLegacyInit(transitions, nsInit);
|
|
378
|
+
if (migrated > 0)
|
|
379
|
+
legacyInitMigrations++;
|
|
380
|
+
allLedgers.push(...view);
|
|
339
381
|
// Invariant check
|
|
340
|
-
const result = (0, ssg_validator_1.checkLedgerConsistency)(
|
|
382
|
+
const result = (0, ssg_validator_1.checkLedgerConsistency)(view, nsInit);
|
|
341
383
|
checked++;
|
|
342
384
|
if (result.consistent) {
|
|
343
385
|
consistent++;
|
|
@@ -348,10 +390,16 @@ step("4/6 Ledger 不变量");
|
|
|
348
390
|
}
|
|
349
391
|
}
|
|
350
392
|
// Replay check: rebuildState === recorded statesAfter
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
const
|
|
354
|
-
|
|
393
|
+
// 只比较 recorded 中有过非空快照的 ns(早期 session 对部分 ns
|
|
394
|
+
// 记录空数组,不携带可比较的状态信息)
|
|
395
|
+
const rebuilt = (0, ssg_validator_1.rebuildState)(view, nsInit);
|
|
396
|
+
const recorded = view[view.length - 1].statesAfter;
|
|
397
|
+
const informativeNs = new Set();
|
|
398
|
+
for (const [ns, s] of Object.entries(recorded)) {
|
|
399
|
+
if ((s || []).length > 0)
|
|
400
|
+
informativeNs.add(ns);
|
|
401
|
+
}
|
|
402
|
+
if (JSON.stringify(norm(rebuilt, informativeNs)) === JSON.stringify(norm(recorded, informativeNs))) {
|
|
355
403
|
stateMatch++;
|
|
356
404
|
}
|
|
357
405
|
else {
|
|
@@ -368,6 +416,9 @@ step("4/6 Ledger 不变量");
|
|
|
368
416
|
else if (consistent === checked && stateMatch === checked) {
|
|
369
417
|
const combinedHash = (0, ssg_validator_1.hashLedger)(allLedgers);
|
|
370
418
|
pass(`全部 ${checked} 个 Ledger 通过 (Invariant-0 + Invariant-1 + Replay) | 完整性指纹: ${combinedHash}`);
|
|
419
|
+
if (legacyInitMigrations > 0) {
|
|
420
|
+
warn(`历史约定兼容:${legacyInitMigrations} 条转移的初始状态 INIT 已按当前约定(${nsInit.get("_global")})规范化比较(不改盘)`);
|
|
421
|
+
}
|
|
371
422
|
}
|
|
372
423
|
else {
|
|
373
424
|
if (consistent < checked) {
|
|
@@ -404,7 +455,9 @@ step("4/6 Ledger 不变量");
|
|
|
404
455
|
if (transitions.length === 0)
|
|
405
456
|
continue;
|
|
406
457
|
try {
|
|
407
|
-
|
|
458
|
+
// 历史约定兼容:与主检查一致,先规范化旧 INIT 初值
|
|
459
|
+
const { view } = normalizeLegacyInit(transitions, nsInit);
|
|
460
|
+
(0, runtime_invariants_1.assertLedgerInvariants)(view, nsInit);
|
|
408
461
|
}
|
|
409
462
|
catch (e) {
|
|
410
463
|
if (e instanceof runtime_invariants_1.InvariantViolationError) {
|
package/dist/execute.js
CHANGED
|
@@ -49,7 +49,7 @@ exports.verifyFileMarker = verifyFileMarker;
|
|
|
49
49
|
const fs = __importStar(require("fs"));
|
|
50
50
|
const path = __importStar(require("path"));
|
|
51
51
|
const planner_1 = require("./planner");
|
|
52
|
-
const
|
|
52
|
+
const extract_project_ir_1 = require("./extract-project-ir");
|
|
53
53
|
const failure_collector_1 = require("./failure-collector");
|
|
54
54
|
const emitter_1 = require("./emitter");
|
|
55
55
|
const METRICS_FILE = ".progmune_corpus/metrics.json";
|
|
@@ -101,7 +101,7 @@ async function execute(intent, projectPath, filePath) {
|
|
|
101
101
|
// 1. IR extraction
|
|
102
102
|
let ir;
|
|
103
103
|
try {
|
|
104
|
-
ir = (0,
|
|
104
|
+
ir = (0, extract_project_ir_1.extractProjectIR)(projectPath);
|
|
105
105
|
}
|
|
106
106
|
catch (e) {
|
|
107
107
|
return { success: false, degraded: false, code: "", sessionId: "", hash: "", ruleHash: "", irFunctionCount: 0, protocolRuleCount: 0, violations: 0, repairApplied: false, repairCount: 0, repairBranchIds: [], branchWinner: undefined, error: `IR extraction failed: ${e.message}` };
|
|
@@ -50,10 +50,18 @@ exports.extractIRPython = extractIRPython;
|
|
|
50
50
|
exports.isPythonProject = isPythonProject;
|
|
51
51
|
const child_process_1 = require("child_process");
|
|
52
52
|
const fs = __importStar(require("fs"));
|
|
53
|
+
const os = __importStar(require("os"));
|
|
53
54
|
const path = __importStar(require("path"));
|
|
54
|
-
|
|
55
|
+
/**
|
|
56
|
+
* @param projectRoot - Project root to scan
|
|
57
|
+
* @param outPath - Output path for the Python script. Defaults to a temp
|
|
58
|
+
* file (cleaned up afterwards) so extraction never clobbers the project's
|
|
59
|
+
* own ir.json — the merged extractProjectIR runs this on TS projects too.
|
|
60
|
+
* Pass an explicit path when the extracted IR must be persisted.
|
|
61
|
+
*/
|
|
62
|
+
function extractIRPython(projectRoot, outPath) {
|
|
55
63
|
const scriptPath = path.resolve(__dirname, "..", "tools", "extract_ir.py");
|
|
56
|
-
const irPath = path.
|
|
64
|
+
const irPath = outPath ?? path.join(os.tmpdir(), `progmune-py-ir-${process.pid}-${Date.now()}.json`);
|
|
57
65
|
const cmd = `python3 "${scriptPath}" "${projectRoot}" "${irPath}"`;
|
|
58
66
|
try {
|
|
59
67
|
(0, child_process_1.execSync)(cmd, { encoding: "utf-8", stdio: "pipe" });
|
|
@@ -64,7 +72,18 @@ function extractIRPython(projectRoot) {
|
|
|
64
72
|
}
|
|
65
73
|
if (!fs.existsSync(irPath))
|
|
66
74
|
return [];
|
|
67
|
-
|
|
75
|
+
let raw;
|
|
76
|
+
try {
|
|
77
|
+
raw = JSON.parse(fs.readFileSync(irPath, "utf-8"));
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
if (!outPath) {
|
|
81
|
+
try {
|
|
82
|
+
fs.unlinkSync(irPath);
|
|
83
|
+
}
|
|
84
|
+
catch { /* 临时文件清理失败无害 */ }
|
|
85
|
+
}
|
|
86
|
+
}
|
|
68
87
|
if (!Array.isArray(raw))
|
|
69
88
|
return [];
|
|
70
89
|
// Map Python IR records to FunctionInfo
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Project IR extraction — merged multi-language entry point.
|
|
4
|
+
*
|
|
5
|
+
* Registry pattern: each supported language contributes a detector +
|
|
6
|
+
* extractor entry. extractProjectIR runs every extractor whose language
|
|
7
|
+
* is detected in the project and merges the results into one FunctionInfo
|
|
8
|
+
* list, so TS + Python coexist in a mixed project.
|
|
9
|
+
*
|
|
10
|
+
* Adding a language later (Go, Java, ...) = register one entry in
|
|
11
|
+
* LANGUAGE_EXTRACTORS. The agent loop (extractIRWithDelta), execute()'s
|
|
12
|
+
* ir.json write and the MCP server all pick it up automatically — no
|
|
13
|
+
* dispatch rewiring anywhere else.
|
|
14
|
+
*/
|
|
15
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
18
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
19
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
20
|
+
}
|
|
21
|
+
Object.defineProperty(o, k2, desc);
|
|
22
|
+
}) : (function(o, m, k, k2) {
|
|
23
|
+
if (k2 === undefined) k2 = k;
|
|
24
|
+
o[k2] = m[k];
|
|
25
|
+
}));
|
|
26
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
27
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
28
|
+
}) : function(o, v) {
|
|
29
|
+
o["default"] = v;
|
|
30
|
+
});
|
|
31
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
32
|
+
var ownKeys = function(o) {
|
|
33
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
34
|
+
var ar = [];
|
|
35
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
36
|
+
return ar;
|
|
37
|
+
};
|
|
38
|
+
return ownKeys(o);
|
|
39
|
+
};
|
|
40
|
+
return function (mod) {
|
|
41
|
+
if (mod && mod.__esModule) return mod;
|
|
42
|
+
var result = {};
|
|
43
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
44
|
+
__setModuleDefault(result, mod);
|
|
45
|
+
return result;
|
|
46
|
+
};
|
|
47
|
+
})();
|
|
48
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
49
|
+
exports.LANGUAGE_EXTRACTORS = void 0;
|
|
50
|
+
exports.detectLanguages = detectLanguages;
|
|
51
|
+
exports.extractProjectIR = extractProjectIR;
|
|
52
|
+
const fs = __importStar(require("fs"));
|
|
53
|
+
const path = __importStar(require("path"));
|
|
54
|
+
const extract_ir_1 = require("./extract-ir");
|
|
55
|
+
const extract_ir_python_1 = require("./extract-ir-python");
|
|
56
|
+
const SKIP_DIRS = new Set([
|
|
57
|
+
"node_modules", "dist", "build", ".git", ".progmune_corpus",
|
|
58
|
+
"__pycache__", "venv", ".venv",
|
|
59
|
+
]);
|
|
60
|
+
/** 有界递归扫描:项目是否含指定扩展名源文件(首个命中即返回)。 */
|
|
61
|
+
function hasSourceFiles(projectRoot, exts) {
|
|
62
|
+
const stack = [projectRoot];
|
|
63
|
+
const seen = new Set();
|
|
64
|
+
while (stack.length > 0) {
|
|
65
|
+
const dir = stack.pop();
|
|
66
|
+
if (seen.has(dir))
|
|
67
|
+
continue;
|
|
68
|
+
seen.add(dir);
|
|
69
|
+
let entries;
|
|
70
|
+
try {
|
|
71
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
for (const e of entries) {
|
|
77
|
+
const full = path.join(dir, e.name);
|
|
78
|
+
if (e.isDirectory()) {
|
|
79
|
+
if (!SKIP_DIRS.has(e.name) && !e.name.startsWith("."))
|
|
80
|
+
stack.push(full);
|
|
81
|
+
}
|
|
82
|
+
else if (exts.has(path.extname(e.name))) {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
/** 已注册语言提取器(新增语言在此追加一条)。 */
|
|
90
|
+
exports.LANGUAGE_EXTRACTORS = [
|
|
91
|
+
{
|
|
92
|
+
language: "typescript",
|
|
93
|
+
detect: (p) => hasSourceFiles(p, new Set([".ts", ".tsx"])),
|
|
94
|
+
extract: (p) => (0, extract_ir_1.extractIR)(p),
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
language: "python",
|
|
98
|
+
detect: (p) => hasSourceFiles(p, new Set([".py"])),
|
|
99
|
+
extract: (p) => (0, extract_ir_python_1.extractIRPython)(p),
|
|
100
|
+
},
|
|
101
|
+
];
|
|
102
|
+
/** 项目检测到的语言列表(审计/标签用)。 */
|
|
103
|
+
function detectLanguages(projectRoot) {
|
|
104
|
+
return exports.LANGUAGE_EXTRACTORS.filter((e) => e.detect(projectRoot)).map((e) => e.language);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Extract the merged project IR across all detected languages.
|
|
108
|
+
*
|
|
109
|
+
* 单语言提取失败不拖垮其余语言(与感知层 best-effort 原则一致);
|
|
110
|
+
* 仅当所有检测到的语言全部失败时才抛错,保留 execute 的硬失败语义。
|
|
111
|
+
*
|
|
112
|
+
* @param projectRoot - Absolute path to project root
|
|
113
|
+
* @param extractors - Registry override (tests inject fake entries here)
|
|
114
|
+
* @returns Merged FunctionInfo list
|
|
115
|
+
*/
|
|
116
|
+
function extractProjectIR(projectRoot, extractors = exports.LANGUAGE_EXTRACTORS) {
|
|
117
|
+
const merged = [];
|
|
118
|
+
let anyDetected = false;
|
|
119
|
+
let anySucceeded = false;
|
|
120
|
+
for (const e of extractors) {
|
|
121
|
+
if (!e.detect(projectRoot))
|
|
122
|
+
continue;
|
|
123
|
+
anyDetected = true;
|
|
124
|
+
try {
|
|
125
|
+
merged.push(...e.extract(projectRoot));
|
|
126
|
+
anySucceeded = true;
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
console.error(`[extractProjectIR] ${e.language} 提取失败: ${err?.message || err}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (anyDetected && !anySucceeded) {
|
|
133
|
+
throw new Error("所有已检测语言的 IR 提取均失败");
|
|
134
|
+
}
|
|
135
|
+
return merged;
|
|
136
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Project IR extraction (merged multi-language registry) — unit tests.
|
|
4
|
+
*
|
|
5
|
+
* Registry injection: tests pass fake extractor entries to extractProjectIR
|
|
6
|
+
* so no real extractor runs; the real detectors are covered by two
|
|
7
|
+
* temp-dir tests (per module convention, FS use is kept minimal).
|
|
8
|
+
*/
|
|
9
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
12
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
13
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
14
|
+
}
|
|
15
|
+
Object.defineProperty(o, k2, desc);
|
|
16
|
+
}) : (function(o, m, k, k2) {
|
|
17
|
+
if (k2 === undefined) k2 = k;
|
|
18
|
+
o[k2] = m[k];
|
|
19
|
+
}));
|
|
20
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
21
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
22
|
+
}) : function(o, v) {
|
|
23
|
+
o["default"] = v;
|
|
24
|
+
});
|
|
25
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
26
|
+
var ownKeys = function(o) {
|
|
27
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
28
|
+
var ar = [];
|
|
29
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
30
|
+
return ar;
|
|
31
|
+
};
|
|
32
|
+
return ownKeys(o);
|
|
33
|
+
};
|
|
34
|
+
return function (mod) {
|
|
35
|
+
if (mod && mod.__esModule) return mod;
|
|
36
|
+
var result = {};
|
|
37
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
38
|
+
__setModuleDefault(result, mod);
|
|
39
|
+
return result;
|
|
40
|
+
};
|
|
41
|
+
})();
|
|
42
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
+
const vitest_1 = require("vitest");
|
|
44
|
+
const fs = __importStar(require("fs"));
|
|
45
|
+
const os = __importStar(require("os"));
|
|
46
|
+
const path = __importStar(require("path"));
|
|
47
|
+
const extract_project_ir_1 = require("./extract-project-ir");
|
|
48
|
+
(0, vitest_1.describe)("extractProjectIR(注册表合并)", () => {
|
|
49
|
+
(0, vitest_1.it)("合并所有检测到语言的函数", () => {
|
|
50
|
+
const tsFns = [{ name: "getSession", file: "src/auth.ts" }];
|
|
51
|
+
const pyFns = [{ name: "verify_password", file: "auth_service.py" }];
|
|
52
|
+
const extractors = [
|
|
53
|
+
{ language: "typescript", detect: () => true, extract: () => tsFns },
|
|
54
|
+
{ language: "python", detect: () => true, extract: () => pyFns },
|
|
55
|
+
];
|
|
56
|
+
const ir = (0, extract_project_ir_1.extractProjectIR)("/tmp/fake", extractors);
|
|
57
|
+
(0, vitest_1.expect)(ir).toEqual([...tsFns, ...pyFns]);
|
|
58
|
+
});
|
|
59
|
+
(0, vitest_1.it)("未检测到的语言不运行提取器", () => {
|
|
60
|
+
const ran = [];
|
|
61
|
+
const extractors = [
|
|
62
|
+
{ language: "typescript", detect: () => true, extract: () => { ran.push("ts"); return []; } },
|
|
63
|
+
{ language: "python", detect: () => false, extract: () => { ran.push("py"); return []; } },
|
|
64
|
+
];
|
|
65
|
+
(0, extract_project_ir_1.extractProjectIR)("/tmp/fake", extractors);
|
|
66
|
+
(0, vitest_1.expect)(ran).toEqual(["ts"]);
|
|
67
|
+
});
|
|
68
|
+
(0, vitest_1.it)("单语言提取失败不拖垮其余语言", () => {
|
|
69
|
+
vitest_1.vi.spyOn(console, "error").mockImplementation(() => { });
|
|
70
|
+
const extractors = [
|
|
71
|
+
{ language: "typescript", detect: () => true, extract: () => { throw new Error("broken tsconfig"); } },
|
|
72
|
+
{ language: "python", detect: () => true, extract: () => [{ name: "f" }] },
|
|
73
|
+
];
|
|
74
|
+
const ir = (0, extract_project_ir_1.extractProjectIR)("/tmp/fake", extractors);
|
|
75
|
+
(0, vitest_1.expect)(ir).toEqual([{ name: "f" }]);
|
|
76
|
+
});
|
|
77
|
+
(0, vitest_1.it)("所有检测到的语言都失败时抛错(保留 execute 硬失败语义)", () => {
|
|
78
|
+
vitest_1.vi.spyOn(console, "error").mockImplementation(() => { });
|
|
79
|
+
const extractors = [
|
|
80
|
+
{ language: "typescript", detect: () => true, extract: () => { throw new Error("broken"); } },
|
|
81
|
+
];
|
|
82
|
+
(0, vitest_1.expect)(() => (0, extract_project_ir_1.extractProjectIR)("/tmp/fake", extractors)).toThrow();
|
|
83
|
+
});
|
|
84
|
+
(0, vitest_1.it)("无任何检测到的语言时返回空数组,不抛错", () => {
|
|
85
|
+
const extractors = [
|
|
86
|
+
{ language: "typescript", detect: () => false, extract: () => [] },
|
|
87
|
+
{ language: "python", detect: () => false, extract: () => [] },
|
|
88
|
+
];
|
|
89
|
+
(0, vitest_1.expect)((0, extract_project_ir_1.extractProjectIR)("/tmp/fake", extractors)).toEqual([]);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
(0, vitest_1.describe)("detectLanguages(真实探测器)", () => {
|
|
93
|
+
(0, vitest_1.it)("TS/Python 混合项目两种语言都被检测", () => {
|
|
94
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pm-lang-"));
|
|
95
|
+
try {
|
|
96
|
+
fs.writeFileSync(path.join(dir, "main.ts"), "export function f() {}");
|
|
97
|
+
fs.mkdirSync(path.join(dir, "src"));
|
|
98
|
+
fs.writeFileSync(path.join(dir, "src", "app.py"), "def g():\n pass\n");
|
|
99
|
+
(0, vitest_1.expect)((0, extract_project_ir_1.detectLanguages)(dir).sort()).toEqual(["python", "typescript"]);
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
(0, vitest_1.it)("依赖目录里的源文件不计入检测(node_modules/.py)", () => {
|
|
106
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pm-lang2-"));
|
|
107
|
+
try {
|
|
108
|
+
fs.mkdirSync(path.join(dir, "node_modules", "pkg"), { recursive: true });
|
|
109
|
+
fs.writeFileSync(path.join(dir, "node_modules", "pkg", "x.py"), "x = 1\n");
|
|
110
|
+
(0, vitest_1.expect)((0, extract_project_ir_1.detectLanguages)(dir)).toEqual([]);
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -49,12 +49,16 @@ const bootstrap_validation_1 = require("./bootstrap-validation");
|
|
|
49
49
|
});
|
|
50
50
|
});
|
|
51
51
|
(0, vitest_1.describe)("Synonym Normalization Impact", () => {
|
|
52
|
+
// 注:这两个测试的重度与本地语料规模线性相关(.progmune_corpus 864+ 条时
|
|
53
|
+
// 实测 40-130s)。CI 干净检出无语料 → 秒级通过;本地语料丰富 → 需放宽超时。
|
|
54
|
+
// 显式超时 180s,不依赖 vitest 全局 30s。
|
|
55
|
+
const CORPUS_HEAVY_TIMEOUT = 180000;
|
|
52
56
|
(0, vitest_1.it)("reduces unique function count via normalization", async () => {
|
|
53
57
|
const report = await (0, function_synonyms_1.runSynonymNormalization)();
|
|
54
58
|
(0, vitest_1.expect)(report.uniqueAfter).toBeLessThanOrEqual(report.uniqueBefore);
|
|
55
59
|
(0, vitest_1.expect)(report.uniqueAfter).toBeLessThanOrEqual(report.uniqueBefore);
|
|
56
60
|
(0, function_synonyms_1.printSynonymReport)(report);
|
|
57
|
-
});
|
|
61
|
+
}, CORPUS_HEAVY_TIMEOUT);
|
|
58
62
|
(0, vitest_1.it)("bootstrap function overlap improves with normalization", async () => {
|
|
59
63
|
// Baseline without normalization
|
|
60
64
|
const baseline = await (0, bootstrap_validation_1.runBootstrapValidation)();
|
|
@@ -64,5 +68,5 @@ const bootstrap_validation_1 = require("./bootstrap-validation");
|
|
|
64
68
|
console.log(`Function overlap: ${(after.functionOverlap * 100).toFixed(0)}%`);
|
|
65
69
|
console.log(`State overlap: ${(after.stateOverlap * 100).toFixed(0)}%`);
|
|
66
70
|
console.log(`Behavioral: ${after.behavioralMatch}/${after.behavioralTotal}`);
|
|
67
|
-
},
|
|
71
|
+
}, CORPUS_HEAVY_TIMEOUT);
|
|
68
72
|
});
|
package/dist/mcp-server.mjs
CHANGED
|
@@ -39,8 +39,7 @@ for (const envPath of [
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
import { plan } from "./planner.js";
|
|
42
|
-
import {
|
|
43
|
-
import { extractIRPython, isPythonProject } from "./extract-ir-python.js";
|
|
42
|
+
import { extractProjectIR, detectLanguages } from "./extract-project-ir.js";
|
|
44
43
|
import { emitCode } from "./emitter.js";
|
|
45
44
|
import { recordRun } from "./feedback.js";
|
|
46
45
|
import { reportFingerprints } from "./immune-reporter.js";
|
|
@@ -419,10 +418,9 @@ Then restart Claude Code.`,
|
|
|
419
418
|
log.warn(`No .ts files found in project root, IR may be empty`);
|
|
420
419
|
}
|
|
421
420
|
// IR extraction — auto-detect language
|
|
422
|
-
const isPython = isPythonProject(projectPath);
|
|
423
421
|
let ir;
|
|
424
422
|
try {
|
|
425
|
-
ir =
|
|
423
|
+
ir = extractProjectIR(projectPath);
|
|
426
424
|
}
|
|
427
425
|
catch (e) {
|
|
428
426
|
return {
|
|
@@ -573,9 +571,9 @@ Then restart Claude Code.`,
|
|
|
573
571
|
const warn = (msg) => results.push(`! ${msg}`);
|
|
574
572
|
// 1. IR extraction — auto-detect language
|
|
575
573
|
try {
|
|
576
|
-
const
|
|
577
|
-
const ir =
|
|
578
|
-
pass(`IR (${
|
|
574
|
+
const langs = detectLanguages(projectPath);
|
|
575
|
+
const ir = extractProjectIR(projectPath);
|
|
576
|
+
pass(`IR (${langs.length > 0 ? langs.join("+") : "无已支持语言"}): ${ir.length} functions extracted`);
|
|
579
577
|
}
|
|
580
578
|
catch (e) {
|
|
581
579
|
fail(`IR extraction: ${e.message}`);
|
package/dist/patrol-cli.js
CHANGED
|
@@ -52,6 +52,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
52
52
|
const path = __importStar(require("path"));
|
|
53
53
|
const agent_patrol_1 = require("./agent-patrol");
|
|
54
54
|
const agent_perception_1 = require("./agent-perception");
|
|
55
|
+
const agent_permissions_1 = require("./agent-permissions");
|
|
55
56
|
// 在 chdir 到项目目录之前按启动 CWD 加载 .env——
|
|
56
57
|
// trust 引擎内部的 lazy require(语义映射 LLM 回退)发生在 chdir 之后,
|
|
57
58
|
// 若不预载,LLM_API_KEY 不可用,映射降级会导致漏报。
|
|
@@ -90,13 +91,24 @@ const json = args.includes("--json");
|
|
|
90
91
|
async function scanOnce(label) {
|
|
91
92
|
try {
|
|
92
93
|
const report = await (0, agent_patrol_1.runPatrol)(projectPath);
|
|
93
|
-
|
|
94
|
+
// ── P5 安全层:报告写入经 FsSandbox(巡逻预设:写=沙箱白名单) ──
|
|
95
|
+
const writeDecision = (0, agent_permissions_1.decidePermission)("patrol", {
|
|
96
|
+
level: "write",
|
|
97
|
+
target: path.join(projectPath, ".progmune_patrol_report.md"),
|
|
98
|
+
projectPath,
|
|
99
|
+
});
|
|
100
|
+
let reportFile = "";
|
|
101
|
+
if (writeDecision.allowed) {
|
|
102
|
+
reportFile = (0, agent_patrol_1.writePatrolReport)(report, projectPath);
|
|
103
|
+
}
|
|
94
104
|
if (json && !watch) {
|
|
95
105
|
console.log(JSON.stringify(report, null, 2));
|
|
96
106
|
}
|
|
97
107
|
else {
|
|
98
108
|
console.log(`[${label}] ` + (0, agent_patrol_1.formatPatrolTerminal)(report).replace(/\n/g, "\n "));
|
|
99
|
-
console.log(
|
|
109
|
+
console.log(writeDecision.allowed
|
|
110
|
+
? ` 报告: ${reportFile}`
|
|
111
|
+
: ` ⚠️ 报告写入被沙箱拒绝: ${writeDecision.detail}`);
|
|
100
112
|
}
|
|
101
113
|
}
|
|
102
114
|
catch (e) {
|
|
@@ -68,9 +68,22 @@ function getProtocolConfig() {
|
|
|
68
68
|
let rules = [];
|
|
69
69
|
let version = "1.0";
|
|
70
70
|
const protoPath = path.resolve(process.env.PROGMUNE_PROJECT_DIR || process.cwd(), "protocols.json");
|
|
71
|
-
|
|
71
|
+
// 解析顺序与 loadIR 一致:显式目录 → CWD → 包目录回退。
|
|
72
|
+
// 修复:在无 protocols.json 的项目目录里运行时(如 agent CLI chdir 到
|
|
73
|
+
// demo-project),此前 nsInit 退化为仅 _global —— session 只记录 1 个
|
|
74
|
+
// 命名空间,而 check 在仓库根跑用全量 27 个 ns 重建 → before-consistency
|
|
75
|
+
// 全量误报(1308/1308)。包目录回退保证任何 cwd 下世界一致。
|
|
76
|
+
const candidates = [protoPath];
|
|
77
|
+
try {
|
|
78
|
+
candidates.push(path.resolve(__dirname, "../protocols.json"));
|
|
79
|
+
}
|
|
80
|
+
catch { /* __dirname 不可用时跳过 */ }
|
|
81
|
+
let loaded = false;
|
|
82
|
+
for (const p of candidates) {
|
|
83
|
+
if (!fs.existsSync(p))
|
|
84
|
+
continue;
|
|
72
85
|
try {
|
|
73
|
-
const proto = JSON.parse(fs.readFileSync(
|
|
86
|
+
const proto = JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
74
87
|
version = proto.$schema || proto.version || "1.0";
|
|
75
88
|
// Load namespace initial states
|
|
76
89
|
nsInit.set("_global", "UNAUTHENTICATED");
|
|
@@ -81,11 +94,13 @@ function getProtocolConfig() {
|
|
|
81
94
|
}
|
|
82
95
|
// Parse rules
|
|
83
96
|
rules = (0, ssg_validator_1.parseProtocolsFromJSON)(proto);
|
|
97
|
+
loaded = true;
|
|
98
|
+
break;
|
|
84
99
|
}
|
|
85
|
-
catch { /*
|
|
100
|
+
catch { /* 下一个候选 */ }
|
|
86
101
|
}
|
|
87
|
-
|
|
88
|
-
// Fallback: minimal defaults (no protocols.json
|
|
102
|
+
if (!loaded) {
|
|
103
|
+
// Fallback: minimal defaults (no protocols.json anywhere)
|
|
89
104
|
nsInit.set("_global", "UNAUTHENTICATED");
|
|
90
105
|
}
|
|
91
106
|
// Compute rule hash
|
package/dist/ssg-validator.js
CHANGED
|
@@ -308,10 +308,26 @@ function checkLedgerConsistency(ledger, namespaceInitialStates = new Map([["_glo
|
|
|
308
308
|
running.set(ns, new Set());
|
|
309
309
|
}
|
|
310
310
|
}
|
|
311
|
-
// Normalize a snapshot:
|
|
311
|
+
// Normalize a snapshot: 只比较 ledger 中实际出现过的命名空间。
|
|
312
|
+
// 历史 session 只记录其触及的命名空间(protocol-registry 包目录回退修复前,
|
|
313
|
+
// 无 protocols.json 的 cwd 下 nsInit 退化为仅 _global)——用全量 nsInit
|
|
314
|
+
// 重建时未记录的 ns 不应参与比较,否则旧数据 before-consistency 全量误报。
|
|
315
|
+
// 更严格一步:只记录"有过非空快照"的 ns——早期 session 对 file/db 等
|
|
316
|
+
// 记录空数组(键存在但无信息),空数组不携带可比较的状态信息。
|
|
317
|
+
const recordedNamespaces = new Set();
|
|
318
|
+
for (const t of ledger) {
|
|
319
|
+
for (const [ns, states] of Object.entries(t.statesBefore)) {
|
|
320
|
+
if ((states || []).length > 0)
|
|
321
|
+
recordedNamespaces.add(ns);
|
|
322
|
+
}
|
|
323
|
+
for (const [ns, states] of Object.entries(t.statesAfter)) {
|
|
324
|
+
if ((states || []).length > 0)
|
|
325
|
+
recordedNamespaces.add(ns);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
312
328
|
function normalizeSnap(snap) {
|
|
313
329
|
const out = {};
|
|
314
|
-
for (const ns of
|
|
330
|
+
for (const ns of recordedNamespaces) {
|
|
315
331
|
out[ns] = [...(snap[ns] || [])].sort();
|
|
316
332
|
}
|
|
317
333
|
return out;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "progmune-runtime",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.5.0",
|
|
4
4
|
"description": "Progmune — AI Trust Decision Engine. Verify AI-generated code before it reaches production. Outputs APPROVED / NEEDS_REVIEW / BLOCKED with evidence.",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist/",
|