progmune-runtime 3.3.8 → 3.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +50 -0
- package/dist/agent-cli.js +215 -0
- package/dist/agent-loop.js +333 -0
- package/dist/agent-loop.test.js +257 -0
- package/dist/agent-patrol.js +200 -0
- package/dist/agent-patrol.test.js +141 -0
- package/dist/agent-perception.js +197 -0
- package/dist/agent-perception.test.js +125 -0
- package/dist/agent-permissions.js +159 -0
- package/dist/agent-permissions.test.js +76 -0
- package/dist/agent-supervision.js +145 -0
- package/dist/agent-supervision.test.js +60 -0
- package/dist/audit.js +50 -3
- package/dist/check.js +62 -9
- package/dist/execute.js +6 -2
- package/dist/extract-ir.js +3 -1
- package/dist/patrol-cli.js +142 -0
- package/dist/planner-prompts.js +3 -0
- package/dist/planner.js +2 -0
- package/dist/protocol-registry.js +20 -5
- package/dist/ssg-validator.js +18 -2
- package/dist/trust/engine.js +81 -0
- package/package.json +4 -1
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Phase 12: Agent 感知层 (P2)
|
|
4
|
+
*
|
|
5
|
+
* 为 agent loop 提供"看清世界"的能力(设计文档 P2):
|
|
6
|
+
* - GitContext — 仓库扫描 + git 上下文(分支/最近提交/变更文件/源文件清单)
|
|
7
|
+
* - extractIRWithDelta — IR 提取 + 前后函数名差集(IR 增量重提的观测面)
|
|
8
|
+
* - RepoWatcher — 文件变更监听(fs.watch + 防抖)→ 触发 IR 重提回调
|
|
9
|
+
*
|
|
10
|
+
* 原则:感知失败不阻塞主循环(best-effort,明确记录 unavailable)。
|
|
11
|
+
*/
|
|
12
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
15
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
16
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
17
|
+
}
|
|
18
|
+
Object.defineProperty(o, k2, desc);
|
|
19
|
+
}) : (function(o, m, k, k2) {
|
|
20
|
+
if (k2 === undefined) k2 = k;
|
|
21
|
+
o[k2] = m[k];
|
|
22
|
+
}));
|
|
23
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
24
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
25
|
+
}) : function(o, v) {
|
|
26
|
+
o["default"] = v;
|
|
27
|
+
});
|
|
28
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
29
|
+
var ownKeys = function(o) {
|
|
30
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
31
|
+
var ar = [];
|
|
32
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
33
|
+
return ar;
|
|
34
|
+
};
|
|
35
|
+
return ownKeys(o);
|
|
36
|
+
};
|
|
37
|
+
return function (mod) {
|
|
38
|
+
if (mod && mod.__esModule) return mod;
|
|
39
|
+
var result = {};
|
|
40
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
41
|
+
__setModuleDefault(result, mod);
|
|
42
|
+
return result;
|
|
43
|
+
};
|
|
44
|
+
})();
|
|
45
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
46
|
+
exports.RepoWatcher = void 0;
|
|
47
|
+
exports.collectGitContext = collectGitContext;
|
|
48
|
+
exports.extractIRWithDelta = extractIRWithDelta;
|
|
49
|
+
const fs = __importStar(require("fs"));
|
|
50
|
+
const path = __importStar(require("path"));
|
|
51
|
+
const child_process_1 = require("child_process");
|
|
52
|
+
const extract_ir_1 = require("./extract-ir");
|
|
53
|
+
// ── Git Context ──
|
|
54
|
+
function git(projectPath, args) {
|
|
55
|
+
return (0, child_process_1.execSync)(`git -C "${projectPath}" ${args}`, {
|
|
56
|
+
encoding: "utf-8",
|
|
57
|
+
timeout: 10000,
|
|
58
|
+
stdio: "pipe",
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
/** 收集仓库上下文(分支、最近提交、变更文件)与源文件清单。best-effort。 */
|
|
62
|
+
function collectGitContext(projectPath) {
|
|
63
|
+
try {
|
|
64
|
+
const branch = git(projectPath, "rev-parse --abbrev-ref HEAD").trim();
|
|
65
|
+
const recentCommits = git(projectPath, "log --oneline -5")
|
|
66
|
+
.split("\n").filter(Boolean);
|
|
67
|
+
// porcelain 输出首字符可能是空格(未暂存),必须整串不 trim 才能保住 3 字符状态列
|
|
68
|
+
const changedFiles = git(projectPath, "status --porcelain")
|
|
69
|
+
.split("\n").filter(Boolean).map((l) => l.slice(3).trim());
|
|
70
|
+
const sourceFiles = listSourceFiles(projectPath);
|
|
71
|
+
return { available: true, branch, recentCommits, changedFiles, sourceFiles };
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
// 非 git 仓库等 → 只给源文件清单
|
|
75
|
+
try {
|
|
76
|
+
return {
|
|
77
|
+
available: false,
|
|
78
|
+
branch: "",
|
|
79
|
+
recentCommits: [],
|
|
80
|
+
changedFiles: [],
|
|
81
|
+
sourceFiles: listSourceFiles(projectPath),
|
|
82
|
+
error: e?.message || String(e),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return { available: false, branch: "", recentCommits: [], changedFiles: [], sourceFiles: [], error: e?.message || String(e) };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/** 浅层扫描项目源文件(不递归进依赖目录)。 */
|
|
91
|
+
function listSourceFiles(projectPath) {
|
|
92
|
+
const SKIP_DIRS = new Set(["node_modules", "dist", "build", ".git", ".progmune_corpus", "__pycache__", "venv", ".venv"]);
|
|
93
|
+
const EXTS = new Set([".ts", ".js", ".py", ".tsx", ".jsx"]);
|
|
94
|
+
const out = [];
|
|
95
|
+
const stack = [projectPath];
|
|
96
|
+
const seen = new Set();
|
|
97
|
+
while (stack.length > 0 && out.length < 500) {
|
|
98
|
+
const dir = stack.pop();
|
|
99
|
+
if (seen.has(dir))
|
|
100
|
+
continue;
|
|
101
|
+
seen.add(dir);
|
|
102
|
+
let entries;
|
|
103
|
+
try {
|
|
104
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
for (const e of entries) {
|
|
110
|
+
const full = path.join(dir, e.name);
|
|
111
|
+
if (e.isDirectory()) {
|
|
112
|
+
if (!SKIP_DIRS.has(e.name) && !e.name.startsWith("."))
|
|
113
|
+
stack.push(full);
|
|
114
|
+
}
|
|
115
|
+
else if (EXTS.has(path.extname(e.name))) {
|
|
116
|
+
out.push(path.relative(projectPath, full));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return out.sort();
|
|
121
|
+
}
|
|
122
|
+
// ── IR 增量 ──
|
|
123
|
+
/**
|
|
124
|
+
* 提取 IR 并计算与上次函数名集合的差集。
|
|
125
|
+
* prevNames 缺省时只返回全量 IR(delta 为空)。
|
|
126
|
+
*/
|
|
127
|
+
function extractIRWithDelta(projectPath, prevNames) {
|
|
128
|
+
const ir = (0, extract_ir_1.extractIR)(projectPath);
|
|
129
|
+
const names = ir.map((f) => String(f.name || "")).filter(Boolean);
|
|
130
|
+
if (!prevNames) {
|
|
131
|
+
return { ir, delta: { added: [], removed: [], functionCount: names.length } };
|
|
132
|
+
}
|
|
133
|
+
const cur = new Set(names);
|
|
134
|
+
const added = names.filter((n) => !prevNames.has(n));
|
|
135
|
+
const removed = [...prevNames].filter((n) => !cur.has(n));
|
|
136
|
+
return { ir, delta: { added, removed, functionCount: names.length } };
|
|
137
|
+
}
|
|
138
|
+
// ── Repo Watcher ──
|
|
139
|
+
const WATCH_EXTS = new Set([".ts", ".js", ".py", ".tsx", ".jsx"]);
|
|
140
|
+
/**
|
|
141
|
+
* 文件变更监听器:fs.watch 递归监听项目目录,按文件防抖后回调。
|
|
142
|
+
* 用途:agent 或用户修改文件后触发 IR 增量重提(IR_STALE 消费方)。
|
|
143
|
+
*/
|
|
144
|
+
class RepoWatcher {
|
|
145
|
+
constructor(projectPath, onChange, debounceMs = 500) {
|
|
146
|
+
this.projectPath = projectPath;
|
|
147
|
+
this.onChange = onChange;
|
|
148
|
+
this.debounceMs = debounceMs;
|
|
149
|
+
this.watcher = null;
|
|
150
|
+
this.timers = new Map();
|
|
151
|
+
}
|
|
152
|
+
start() {
|
|
153
|
+
if (this.watcher)
|
|
154
|
+
return;
|
|
155
|
+
try {
|
|
156
|
+
this.watcher = fs.watch(this.projectPath, { recursive: true }, (event, filename) => {
|
|
157
|
+
if (!filename)
|
|
158
|
+
return;
|
|
159
|
+
const ext = path.extname(String(filename));
|
|
160
|
+
if (!WATCH_EXTS.has(ext))
|
|
161
|
+
return;
|
|
162
|
+
const rel = path.relative(this.projectPath, path.join(this.projectPath, String(filename)));
|
|
163
|
+
// 防抖:同一文件的连续事件合并
|
|
164
|
+
const prev = this.timers.get(rel);
|
|
165
|
+
if (prev)
|
|
166
|
+
clearTimeout(prev);
|
|
167
|
+
this.timers.set(rel, setTimeout(() => {
|
|
168
|
+
this.timers.delete(rel);
|
|
169
|
+
try {
|
|
170
|
+
this.onChange(rel);
|
|
171
|
+
}
|
|
172
|
+
catch { /* 回调异常不杀死 watcher */ }
|
|
173
|
+
}, this.debounceMs));
|
|
174
|
+
});
|
|
175
|
+
this.watcher.on("error", () => { });
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
this.watcher = null; // recursive 不支持时降级为不可用
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
stop() {
|
|
182
|
+
for (const t of this.timers.values())
|
|
183
|
+
clearTimeout(t);
|
|
184
|
+
this.timers.clear();
|
|
185
|
+
if (this.watcher) {
|
|
186
|
+
try {
|
|
187
|
+
this.watcher.close();
|
|
188
|
+
}
|
|
189
|
+
catch { /* ignore */ }
|
|
190
|
+
this.watcher = null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
get active() {
|
|
194
|
+
return this.watcher !== null;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
exports.RepoWatcher = RepoWatcher;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Phase 12: 感知层测试 (P2)
|
|
4
|
+
*
|
|
5
|
+
* collectGitContext / extractIRWithDelta:mock git 与 IR,不触真实仓库。
|
|
6
|
+
* RepoWatcher:真实临时目录(fs.watch 需要真实文件系统)。
|
|
7
|
+
*/
|
|
8
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
9
|
+
if (k2 === undefined) k2 = k;
|
|
10
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
11
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
12
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
13
|
+
}
|
|
14
|
+
Object.defineProperty(o, k2, desc);
|
|
15
|
+
}) : (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
o[k2] = m[k];
|
|
18
|
+
}));
|
|
19
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
20
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
21
|
+
}) : function(o, v) {
|
|
22
|
+
o["default"] = v;
|
|
23
|
+
});
|
|
24
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
+
var ownKeys = function(o) {
|
|
26
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
+
var ar = [];
|
|
28
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
+
return ar;
|
|
30
|
+
};
|
|
31
|
+
return ownKeys(o);
|
|
32
|
+
};
|
|
33
|
+
return function (mod) {
|
|
34
|
+
if (mod && mod.__esModule) return mod;
|
|
35
|
+
var result = {};
|
|
36
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
+
__setModuleDefault(result, mod);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
})();
|
|
41
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
const vitest_1 = require("vitest");
|
|
43
|
+
const child_process_1 = require("child_process");
|
|
44
|
+
const extract_ir_1 = require("./extract-ir");
|
|
45
|
+
const agent_perception_1 = require("./agent-perception");
|
|
46
|
+
vitest_1.vi.mock("child_process", () => ({
|
|
47
|
+
execSync: vitest_1.vi.fn(),
|
|
48
|
+
}));
|
|
49
|
+
vitest_1.vi.mock("./extract-ir", () => ({
|
|
50
|
+
extractIR: vitest_1.vi.fn(),
|
|
51
|
+
}));
|
|
52
|
+
const mockExecSync = vitest_1.vi.mocked(child_process_1.execSync);
|
|
53
|
+
const mockExtractIR = vitest_1.vi.mocked(extract_ir_1.extractIR);
|
|
54
|
+
(0, vitest_1.beforeEach)(() => {
|
|
55
|
+
vitest_1.vi.clearAllMocks();
|
|
56
|
+
});
|
|
57
|
+
(0, vitest_1.describe)("agent-perception", () => {
|
|
58
|
+
(0, vitest_1.it)("collectGitContext 解析分支/提交/变更文件/源文件清单", () => {
|
|
59
|
+
mockExecSync.mockImplementation((cmd) => {
|
|
60
|
+
const c = String(cmd);
|
|
61
|
+
if (c.includes("rev-parse"))
|
|
62
|
+
return "main";
|
|
63
|
+
if (c.includes("log --oneline"))
|
|
64
|
+
return "abc123 feat: login\nbcd456 fix: session";
|
|
65
|
+
if (c.includes("status --porcelain"))
|
|
66
|
+
return " M src/auth.ts\n?? src/new.ts";
|
|
67
|
+
throw new Error("unexpected cmd: " + c);
|
|
68
|
+
});
|
|
69
|
+
const ctx = (0, agent_perception_1.collectGitContext)("/tmp/fake-project");
|
|
70
|
+
(0, vitest_1.expect)(ctx.available).toBe(true);
|
|
71
|
+
(0, vitest_1.expect)(ctx.branch).toBe("main");
|
|
72
|
+
(0, vitest_1.expect)(ctx.recentCommits).toHaveLength(2);
|
|
73
|
+
(0, vitest_1.expect)(ctx.changedFiles).toEqual(["src/auth.ts", "src/new.ts"]);
|
|
74
|
+
(0, vitest_1.expect)(ctx.sourceFiles.length).toBeGreaterThanOrEqual(0);
|
|
75
|
+
});
|
|
76
|
+
(0, vitest_1.it)("collectGitContext 非 git 仓库时降级为 available=false 且不抛", () => {
|
|
77
|
+
mockExecSync.mockImplementation(() => {
|
|
78
|
+
throw new Error("fatal: not a git repository");
|
|
79
|
+
});
|
|
80
|
+
const ctx = (0, agent_perception_1.collectGitContext)("/tmp/fake-project");
|
|
81
|
+
(0, vitest_1.expect)(ctx.available).toBe(false);
|
|
82
|
+
(0, vitest_1.expect)(ctx.error).toContain("not a git repository");
|
|
83
|
+
});
|
|
84
|
+
(0, vitest_1.it)("extractIRWithDelta 计算新增/消失函数差集", () => {
|
|
85
|
+
mockExtractIR.mockReturnValue([
|
|
86
|
+
{ name: "verify_password" },
|
|
87
|
+
{ name: "main" },
|
|
88
|
+
]);
|
|
89
|
+
const prev = new Set(["verify_password", "logout"]);
|
|
90
|
+
const { delta } = (0, agent_perception_1.extractIRWithDelta)("/tmp/fake-project", prev);
|
|
91
|
+
(0, vitest_1.expect)(delta.added).toEqual(["main"]);
|
|
92
|
+
(0, vitest_1.expect)(delta.removed).toEqual(["logout"]);
|
|
93
|
+
(0, vitest_1.expect)(delta.functionCount).toBe(2);
|
|
94
|
+
});
|
|
95
|
+
(0, vitest_1.it)("RepoWatcher 文件变更防抖回调", async () => {
|
|
96
|
+
const fs = await Promise.resolve().then(() => __importStar(require("fs")));
|
|
97
|
+
const os = await Promise.resolve().then(() => __importStar(require("os")));
|
|
98
|
+
const path = await Promise.resolve().then(() => __importStar(require("path")));
|
|
99
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pm-watch-"));
|
|
100
|
+
const changed = [];
|
|
101
|
+
const w = new agent_perception_1.RepoWatcher(dir, (f) => changed.push(f), 50);
|
|
102
|
+
w.start();
|
|
103
|
+
// 等待 watcher 就绪后写文件
|
|
104
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
105
|
+
fs.writeFileSync(path.join(dir, "new.ts"), "export function f() {}");
|
|
106
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
107
|
+
w.stop();
|
|
108
|
+
(0, vitest_1.expect)(changed).toContain("new.ts");
|
|
109
|
+
(0, vitest_1.expect)(w.active).toBe(false);
|
|
110
|
+
});
|
|
111
|
+
(0, vitest_1.it)("RepoWatcher 忽略非源文件扩展名", async () => {
|
|
112
|
+
const fs = await Promise.resolve().then(() => __importStar(require("fs")));
|
|
113
|
+
const os = await Promise.resolve().then(() => __importStar(require("os")));
|
|
114
|
+
const path = await Promise.resolve().then(() => __importStar(require("path")));
|
|
115
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pm-watch2-"));
|
|
116
|
+
const changed = [];
|
|
117
|
+
const w = new agent_perception_1.RepoWatcher(dir, (f) => changed.push(f), 50);
|
|
118
|
+
w.start();
|
|
119
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
120
|
+
fs.writeFileSync(path.join(dir, "notes.txt"), "hello");
|
|
121
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
122
|
+
w.stop();
|
|
123
|
+
(0, vitest_1.expect)(changed).toHaveLength(0);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -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
|
+
});
|