progmune-runtime 3.7.22 → 3.7.23
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 +12 -0
- package/README.md +10 -0
- package/README.zh-CN.md +10 -0
- package/dist/certify.js +20 -15
- package/dist/check.js +18 -0
- package/dist/decision-engine.js +26 -3
- package/dist/execute.js +84 -1
- package/dist/mcp-server.mjs +1 -0
- package/dist/policy/cli.js +8 -1
- package/dist/policy/engine.js +82 -18
- package/dist/policy/engine.test.js +135 -0
- package/dist/sdk.js +1 -1
- package/dist/ssg-validator.js +3 -0
- package/dist/trust/engine.js +24 -1
- package/dist/trust/protocol-rules-liveness.test.js +114 -0
- package/package.json +6 -3
- package/protocols.json +300 -152
- package/scripts/init-policy.js +49 -0
- package/templates/.progmune-policy.tier1.json +5 -0
- package/templates/.progmune-policy.tier2.json +9 -0
- package/templates/.progmune-policy.tier3.json +11 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [3.7.23] — 2026-09-06
|
|
4
|
+
|
|
5
|
+
### 第三方审计修复批次 + Oracle 隔离政策 + 分资产分级策略
|
|
6
|
+
|
|
7
|
+
- **治理层 fail-open 修复**(Kimi 行号级审计 P0):risk 规则 fail-closed(不再伪造 SSL 输入,真实调用提取,无数据/模块不可用 → 显式违规);kb_coverage 显式化;策略配置解析失败拒绝静默评估(policy CLI exit 2);**execute 写盘策略门**(项目 opt-in `.progmune-policy.json` 后写盘即时验证,BLOCK 自动回滚——策略引擎首次获得写盘强制力);`PROGMUNE_STRICT=false` 逃生门写入审计事件日志;决策层注释与实现对齐(乘积→加权平均);证书时间戳贯通 PolicyContext;**潜伏 bug**:certify 的 plsbRecall 恒为 0(metadata 从未有 recall 字段,HIGH 置信度门槛永远不可达)→ 修为 verified/total
|
|
8
|
+
- **跨命名空间死规则归位 + 活性守卫**(Kimi 发现 3 系统化):全量扫描 14 条死规则——4 条归位(logout_without_invalidate/admin_action_without_check/double_commit/check_resource_ownership 挪进 pre 状态所在命名空间)、10 条 printlab 业务链为文档化例外(待跨命名空间状态引用特性);`protocol-rules-liveness.test.ts` 使死规则永久回归可防
|
|
9
|
+
- **P1 批**:SUPPRESS 等全部决策持久化审计事件日志(含 fpRate/envFactor/importance 全输入);截断序列覆盖率显式降级上报(truncatedSequences + summary 标注);README 双语边界声明(刻意规避不在防护目标内 / BLOCK 强制力三执行点);FAQ 澄清 LLM 密钥为可选增强;落地页免疫叙事收敛
|
|
10
|
+
- **Oracle 隔离政策**(Hunyuan 定律 5 落地,`docs/ORACLE_ISOLATION_POLICY.md`):四类评估体系变更(规则/匹配策略/阈值/别名)必须人工确认,AI 只提案;protocols.json 全量 `status: "confirmed"` + 加载端跳过 proposed + `scripts/rule-propose.js` 提案脚手架;evaluateTrust 输出 **oracleIndependence**(confirmedRules / sameSourceAnnotations / confirmedAliases)
|
|
11
|
+
- **分资产分级策略**(`docs/TIERED_POLICY.md` + `templates/` 三档 + `progmune-init-policy` bin):Tier-1 强制(致命资产,写盘门激活)/ Tier-2 标准(violations 阻断,其余 WARN)/ Tier-3 观察(只报告)——「出错代价 × AI 参与度」二维分级,一条命令落地
|
|
12
|
+
- **审计回应文档** `docs/AUDIT_RESPONSE_2026-09.md`:逐条回应智谱 + Kimi 两份审查(已修复 12 项 / 事实澄清 5 项 / 设计边界 3 项 / 计划 4 项)
|
|
13
|
+
- 发布门:套件全绿(含 policy 新测试 6 项);Python 盲测 64 / TS 795 / C 应用金标零漂移;check 免疫正常
|
|
14
|
+
|
|
3
15
|
## [3.7.22] — 2026-09-06
|
|
4
16
|
|
|
5
17
|
### Java 方法学三连——恢复率裁决 + 真实闭环 + 名碰撞消除
|
package/README.md
CHANGED
|
@@ -118,6 +118,16 @@ Progmune is honest about what it can and cannot verify.
|
|
|
118
118
|
- **Runtime behavior** — Progmune is static analysis only; no DAST/sandbox execution.
|
|
119
119
|
- **Framework internals** — well-known framework dispatch/cache machinery (e.g. django-unicorn internals) can produce a small number of boundary false positives; they are documented per-corpus in the benchmark gold files.
|
|
120
120
|
- **Known failure boundaries are documented** rather than hidden: if Progmune cannot verify a language (e.g. Go), Confidence is lowered instead of pretending 100%.
|
|
121
|
+
- **Deliberate evasion is out of scope** — the annotation-driven model catches *accidental* AI protocol mistakes (missing prerequisites, wrong ordering); it does not defend against adversarial renaming/obfuscation (true of any static tool).
|
|
122
|
+
- **BLOCK enforcement is the integrator's responsibility** — Progmune outputs decisions and evidence; it does not intercept at runtime. OS-level enforcement points: `trust --ci` exit code (CI gates), `policy CLI` exit code, and the opt-in write-gate (with `.progmune-policy.json`, `execute` verifies immediately after writing and rolls back on BLOCK).
|
|
123
|
+
|
|
124
|
+
**Tiered adoption by asset class** ([TIERED_POLICY](https://github.com/shenlian19831109/progmune-runtime/blob/main/docs/TIERED_POLICY.md)): don't gate all code alike — pick a tier by "cost of failure × AI involvement", one command to install:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
npx progmune-init-policy --tier 1 # Enforce: auth/payment/resource lifecycle — write-gate active
|
|
128
|
+
npx progmune-init-policy --tier 2 # Standard: business logic — violations block, rest WARN
|
|
129
|
+
npx progmune-init-policy --tier 3 # Observe: tools/demos — report only
|
|
130
|
+
```
|
|
121
131
|
|
|
122
132
|
→ [Full Coverage Matrix](https://github.com/shenlian19831109/progmune-runtime/blob/main/docs/coverage-matrix-en.md)
|
|
123
133
|
|
package/README.zh-CN.md
CHANGED
|
@@ -118,6 +118,16 @@ Progmune 对能验证什么、不能验证什么保持诚实。
|
|
|
118
118
|
- **运行时行为**——Progmune 仅做静态分析;无 DAST/沙箱执行。
|
|
119
119
|
- **框架内部件**——知名框架的分发/缓存机制(如 django-unicorn 内部件)可能产生少量边界误报;各语料基准 gold 文件中已逐条记录。
|
|
120
120
|
- **已知失败边界一律记录**而非隐藏:如果 Progmune 无法验证某语言(如 Go),置信度会降低而不是假装 100%。
|
|
121
|
+
- **刻意规避不在防护目标内**——注解驱动模型拦截的是 AI 的「意外」协议错误(漏写前置步骤、顺序颠倒);对故意改名/混淆的对抗性规避不设防(任何静态工具同理)。
|
|
122
|
+
- **BLOCK 的强制力由集成方承担**——Progmune 输出判定与证据,不运行时拦截。OS 级强制执行点:`trust --ci` 退出码(CI 门禁)、`policy CLI` 退出码、以及 opt-in 的写盘策略门(项目配置 `.progmune-policy.json` 后,execute 写盘即时验证,BLOCK 自动回滚)。
|
|
123
|
+
|
|
124
|
+
**分资产分级接入**([TIERED_POLICY](https://github.com/shenlian19831109/progmune-runtime/blob/main/docs/TIERED_POLICY.md)):不要给所有代码同一套门禁——按「出错代价 × AI 参与度」选档,一条命令落地:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
npx progmune-init-policy --tier 1 # 强制:鉴权/支付/资源生命周期——写盘策略门激活
|
|
128
|
+
npx progmune-init-policy --tier 2 # 标准:业务逻辑——violations 阻断,其余 WARN
|
|
129
|
+
npx progmune-init-policy --tier 3 # 观察:工具/demo——只报告不拦截
|
|
130
|
+
```
|
|
121
131
|
|
|
122
132
|
→ [完整覆盖矩阵](https://github.com/shenlian19831109/progmune-runtime/blob/main/docs/coverage-matrix.md)
|
|
123
133
|
|
package/dist/certify.js
CHANGED
|
@@ -49,6 +49,14 @@ exports.certify = certify;
|
|
|
49
49
|
exports.formatCertificate = formatCertificate;
|
|
50
50
|
const fs = __importStar(require("fs"));
|
|
51
51
|
const path = __importStar(require("path"));
|
|
52
|
+
// 静态导入(vitest 下 lazy require CJS 互操作不可靠——engine.ts 同款
|
|
53
|
+
// 陷阱,2026-08-27 DSH 修复先例;以下模块均不反向依赖 certify,无环)
|
|
54
|
+
const failure_corpus_1 = require("./failure-corpus");
|
|
55
|
+
const ssg_validator_1 = require("./ssg-validator");
|
|
56
|
+
const protocol_registry_1 = require("./protocol-registry");
|
|
57
|
+
const ledger_registry_1 = require("./ledger-registry");
|
|
58
|
+
const plsb_benchmark_1 = require("./plsb-benchmark");
|
|
59
|
+
const protocol_knowledge_1 = require("./protocol-knowledge");
|
|
52
60
|
// ── Core ──
|
|
53
61
|
function certify(filePath) {
|
|
54
62
|
const absPath = path.resolve(filePath);
|
|
@@ -66,8 +74,7 @@ function certify(filePath) {
|
|
|
66
74
|
const sessionId = markerMatch[1];
|
|
67
75
|
const markerTimestamp = markerMatch[2];
|
|
68
76
|
// 2. Load session from corpus
|
|
69
|
-
const
|
|
70
|
-
const sessions = getAllSessions();
|
|
77
|
+
const sessions = (0, failure_corpus_1.getAllSessions)();
|
|
71
78
|
const session = sessions.find((s) => s.sessionId === sessionId || s.sessionId?.startsWith(sessionId));
|
|
72
79
|
// 3. Collect transitions
|
|
73
80
|
let allTransitions = [];
|
|
@@ -85,9 +92,7 @@ function certify(filePath) {
|
|
|
85
92
|
let violations = 0;
|
|
86
93
|
try {
|
|
87
94
|
if (session && allTransitions.length > 0) {
|
|
88
|
-
const
|
|
89
|
-
const { getNsInit } = require("./protocol-registry");
|
|
90
|
-
const result = checkLedgerConsistency(allTransitions, getNsInit());
|
|
95
|
+
const result = (0, ssg_validator_1.checkLedgerConsistency)(allTransitions, (0, protocol_registry_1.getNsInit)());
|
|
91
96
|
validated = result.consistent;
|
|
92
97
|
violations = (result.violations || []).length;
|
|
93
98
|
}
|
|
@@ -102,12 +107,10 @@ function certify(filePath) {
|
|
|
102
107
|
let fingerprint = "";
|
|
103
108
|
let provenanceIntact = true;
|
|
104
109
|
try {
|
|
105
|
-
const
|
|
106
|
-
const fp = verifyFingerprint(sessionId, allTransitions);
|
|
110
|
+
const fp = (0, ledger_registry_1.verifyFingerprint)(sessionId, allTransitions);
|
|
107
111
|
fingerprint = fp.stored?.ledgerHash || fp.stored?.ledgerHash || "";
|
|
108
112
|
if (!fingerprint) {
|
|
109
|
-
const
|
|
110
|
-
const fp2 = getFingerprint(sessionId);
|
|
113
|
+
const fp2 = (0, ledger_registry_1.getFingerprint)(sessionId);
|
|
111
114
|
fingerprint = fp2?.ledgerHash || "";
|
|
112
115
|
}
|
|
113
116
|
provenanceIntact = !fp.tampered;
|
|
@@ -120,11 +123,14 @@ function certify(filePath) {
|
|
|
120
123
|
let plsbCoverage = "unknown";
|
|
121
124
|
let plsbRecall = 0;
|
|
122
125
|
try {
|
|
123
|
-
const
|
|
124
|
-
const benchmark = buildPLSB();
|
|
126
|
+
const benchmark = (0, plsb_benchmark_1.buildPLSB)();
|
|
125
127
|
plsbVersion = benchmark.version || "1.0";
|
|
126
|
-
|
|
127
|
-
|
|
128
|
+
// recall = verified/total(类型修复暴露的潜伏 bug:metadata 从未有
|
|
129
|
+
// recall 字段,原写法恒为 0 → HIGH 置信度门槛永远不可达)
|
|
130
|
+
plsbRecall = benchmark.metadata
|
|
131
|
+
? benchmark.metadata.verified / Math.max(1, benchmark.metadata.total)
|
|
132
|
+
: 0;
|
|
133
|
+
const taxonomy = plsb_benchmark_1.PROTOCOL_WEAKNESS_TAXONOMY;
|
|
128
134
|
const byPLS = benchmark.metadata?.byPLS || {};
|
|
129
135
|
const covered = taxonomy.filter((t) => (byPLS[t.id] || 0) > 0).length;
|
|
130
136
|
plsbCoverage = `${covered}/${taxonomy.length}`;
|
|
@@ -161,8 +167,7 @@ function certify(filePath) {
|
|
|
161
167
|
let kbVersion;
|
|
162
168
|
let kbAssets;
|
|
163
169
|
try {
|
|
164
|
-
const
|
|
165
|
-
const kb = buildKnowledgeBase();
|
|
170
|
+
const kb = (0, protocol_knowledge_1.buildKnowledgeBase)();
|
|
166
171
|
kbVersion = kb.version;
|
|
167
172
|
const stableAssets = kb.units.filter((u) => u.maturity === "stable");
|
|
168
173
|
kbAssets = stableAssets.map((u) => ({
|
package/dist/check.js
CHANGED
|
@@ -473,6 +473,24 @@ step("4/6 Ledger 不变量");
|
|
|
473
473
|
fail("PROGMUNE_STRICT=true — 严格不变量断言发现违规");
|
|
474
474
|
}
|
|
475
475
|
}
|
|
476
|
+
else if (process.env.PROGMUNE_STRICT === "false") {
|
|
477
|
+
// 逃生门记账(审计修复 2026-09-06):降级必须留痕——
|
|
478
|
+
// 不变量违规降级为提示时,向审计事件日志追加一条记录
|
|
479
|
+
warn("PROGMUNE_STRICT=false — 不变量违规降级为提示(已记入审计事件日志)");
|
|
480
|
+
try {
|
|
481
|
+
const fs = require("fs");
|
|
482
|
+
const path = require("path");
|
|
483
|
+
const auditDir = path.join(process.cwd(), ".progmune_memory");
|
|
484
|
+
if (!fs.existsSync(auditDir))
|
|
485
|
+
fs.mkdirSync(auditDir, { recursive: true });
|
|
486
|
+
fs.appendFileSync(path.join(auditDir, "audit-events.jsonl"), JSON.stringify({
|
|
487
|
+
event: "PROGMUNE_STRICT_DISABLED",
|
|
488
|
+
timestamp: new Date().toISOString(),
|
|
489
|
+
cwd: process.cwd(),
|
|
490
|
+
}) + "\n", "utf-8");
|
|
491
|
+
}
|
|
492
|
+
catch { /* audit log write — best-effort */ }
|
|
493
|
+
}
|
|
476
494
|
// P0: Fingerprint registration — ensure every session has an execution certificate
|
|
477
495
|
{
|
|
478
496
|
const registered = (0, ledger_registry_1.registerAllMissingFingerprints)();
|
package/dist/decision-engine.js
CHANGED
|
@@ -17,7 +17,9 @@
|
|
|
17
17
|
* Output: BLOCK/WARN/ALLOW | promote/demote/hold | fix/retry/rollback | accept/reject
|
|
18
18
|
*
|
|
19
19
|
* Importance is COMPUTED, not configured:
|
|
20
|
-
* Importance =
|
|
20
|
+
* Importance = 0.4×Impact + 0.25×Universality + 0.35×Criticality
|
|
21
|
+
* (加权平均,权重之和为 1;Impact 权重最高——安全影响优先于
|
|
22
|
+
* 出现广度。审计修复 2026-09-06:原注释写乘积公式,与实现矛盾)
|
|
21
23
|
*/
|
|
22
24
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
25
|
exports.DecisionEngine = void 0;
|
|
@@ -105,7 +107,7 @@ function computeImportance(asset) {
|
|
|
105
107
|
const impact = computeImpact(asset);
|
|
106
108
|
const universality = computeUniversality(asset);
|
|
107
109
|
const criticality = computeCriticality(asset);
|
|
108
|
-
// Weighted
|
|
110
|
+
// Weighted average (weights sum to 1): Impact is most important, Universality secondary
|
|
109
111
|
return Math.round((impact * 0.4 + universality * 0.25 + criticality * 0.35) * 100);
|
|
110
112
|
}
|
|
111
113
|
/**
|
|
@@ -181,7 +183,9 @@ class DecisionEngine {
|
|
|
181
183
|
reason: action === "BLOCK"
|
|
182
184
|
? `High-confidence violation (${(confidence * 100).toFixed(0)}%, importance ${importance})`
|
|
183
185
|
: action === "SUPPRESS"
|
|
184
|
-
|
|
186
|
+
// 审计修复 2026-09-06:抑制决策的输入全部入审计记录
|
|
187
|
+
// (FP 率/环境因子/资产),可追溯为何被压、被谁压
|
|
188
|
+
? `Suppressed (silent) at ${(confidence * 100).toFixed(0)}% confidence — fpRate=${(fpRate * 100).toFixed(0)}%, envFactor=${envFactor}, importance=${importance}, ${context.recentFPs || 0} recent FPs`
|
|
185
189
|
: `Moderate confidence (${(confidence * 100).toFixed(0)}%) — ${action}`,
|
|
186
190
|
provenance: {
|
|
187
191
|
assets: [asset.id],
|
|
@@ -373,6 +377,25 @@ class DecisionEngine {
|
|
|
373
377
|
const id = `DEC-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
|
|
374
378
|
const decision = { id, ...partial };
|
|
375
379
|
this.history.push(decision);
|
|
380
|
+
// 审计修复 2026-09-06:决策(尤其 SUPPRESS 静默抑制)持久化到审计
|
|
381
|
+
// 事件日志——此前仅内存记录,抑制决策对外不可见、不可追溯
|
|
382
|
+
try {
|
|
383
|
+
const fs = require("fs");
|
|
384
|
+
const path = require("path");
|
|
385
|
+
const auditDir = path.join(process.cwd(), ".progmune_memory");
|
|
386
|
+
if (!fs.existsSync(auditDir))
|
|
387
|
+
fs.mkdirSync(auditDir, { recursive: true });
|
|
388
|
+
fs.appendFileSync(path.join(auditDir, "audit-events.jsonl"), JSON.stringify({
|
|
389
|
+
event: "DECISION_RECORDED",
|
|
390
|
+
decisionId: id,
|
|
391
|
+
kind: decision.kind,
|
|
392
|
+
action: decision.action,
|
|
393
|
+
confidence: decision.confidence,
|
|
394
|
+
reason: decision.reason,
|
|
395
|
+
timestamp: decision.provenance.timestamp,
|
|
396
|
+
}) + "\n", "utf-8");
|
|
397
|
+
}
|
|
398
|
+
catch { /* audit log write — best-effort */ }
|
|
376
399
|
return decision;
|
|
377
400
|
}
|
|
378
401
|
}
|
package/dist/execute.js
CHANGED
|
@@ -43,6 +43,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
43
43
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
44
44
|
exports.recordGeneration = recordGeneration;
|
|
45
45
|
exports.getExecutionMetrics = getExecutionMetrics;
|
|
46
|
+
exports.applyPolicyGateAfterWrite = applyPolicyGateAfterWrite;
|
|
46
47
|
exports.execute = execute;
|
|
47
48
|
exports.verifyCompiles = verifyCompiles;
|
|
48
49
|
exports.verifyFileMarker = verifyFileMarker;
|
|
@@ -52,6 +53,9 @@ const planner_1 = require("./planner");
|
|
|
52
53
|
const extract_project_ir_1 = require("./extract-project-ir");
|
|
53
54
|
const failure_collector_1 = require("./failure-collector");
|
|
54
55
|
const emitter_1 = require("./emitter");
|
|
56
|
+
// 静态导入(vitest 下 lazy require CJS 互操作不可靠;certify/policy 无环)
|
|
57
|
+
const certify_1 = require("./certify");
|
|
58
|
+
const engine_1 = require("./policy/engine");
|
|
55
59
|
const METRICS_FILE = ".progmune_corpus/metrics.json";
|
|
56
60
|
function loadMetrics() {
|
|
57
61
|
try {
|
|
@@ -95,6 +99,68 @@ function getExecutionMetrics() {
|
|
|
95
99
|
* @param projectPath - Absolute path to project root
|
|
96
100
|
* @param filePath - Optional: write generated code to this file
|
|
97
101
|
*/
|
|
102
|
+
/**
|
|
103
|
+
* 策略门(审计修复 2026-09-06):项目配置 .progmune-policy.json 时,
|
|
104
|
+
* 写盘后即时 certify + evaluatePolicy;BLOCK 或验证自身失败 → 回滚写盘
|
|
105
|
+
* (恢复 prevContent 或删除新文件)——策略引擎 BLOCK 至此获得写盘强制力。
|
|
106
|
+
* 未配置策略文件时无操作(旧行为保持)。
|
|
107
|
+
*/
|
|
108
|
+
function applyPolicyGateAfterWrite(projectPath, resolvedPath, prevContent) {
|
|
109
|
+
const policyOptIn = fs.existsSync(path.join(projectPath, ".progmune-policy.json"));
|
|
110
|
+
if (!policyOptIn)
|
|
111
|
+
return { blocked: false };
|
|
112
|
+
const rollback = () => {
|
|
113
|
+
if (prevContent !== undefined) {
|
|
114
|
+
fs.writeFileSync(resolvedPath, prevContent, "utf-8");
|
|
115
|
+
}
|
|
116
|
+
else if (fs.existsSync(resolvedPath)) {
|
|
117
|
+
fs.unlinkSync(resolvedPath);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
try {
|
|
121
|
+
const cert = (0, certify_1.certify)(resolvedPath);
|
|
122
|
+
const { rules, configError } = (0, engine_1.loadPolicyConfig)(projectPath);
|
|
123
|
+
if (configError) {
|
|
124
|
+
throw new Error(configError);
|
|
125
|
+
}
|
|
126
|
+
const policyResult = (0, engine_1.evaluatePolicy)({
|
|
127
|
+
certificate: {
|
|
128
|
+
validated: cert.validated,
|
|
129
|
+
confidence: cert.confidence,
|
|
130
|
+
provenanceIntact: cert.provenanceIntact,
|
|
131
|
+
fingerprint: cert.fingerprint,
|
|
132
|
+
violations: cert.violations,
|
|
133
|
+
plsbCoverage: cert.plsbCoverage,
|
|
134
|
+
plsbRecall: cert.plsbRecall,
|
|
135
|
+
degraded: cert.degraded,
|
|
136
|
+
sessionId: cert.sessionId,
|
|
137
|
+
file: cert.file,
|
|
138
|
+
timestamp: cert.timestamp,
|
|
139
|
+
},
|
|
140
|
+
}, rules);
|
|
141
|
+
if (!policyResult.passed) {
|
|
142
|
+
rollback();
|
|
143
|
+
return {
|
|
144
|
+
blocked: true,
|
|
145
|
+
decision: policyResult.verdict,
|
|
146
|
+
violations: policyResult.violations.map((v) => ({
|
|
147
|
+
ruleType: v.rule.type,
|
|
148
|
+
detail: v.detail || v.actual,
|
|
149
|
+
})),
|
|
150
|
+
error: `Policy gate BLOCKED the write: ${policyResult.violations.map((v) => v.rule.type).join(", ")} — file rolled back`,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
return { blocked: false };
|
|
154
|
+
}
|
|
155
|
+
catch (e) {
|
|
156
|
+
// fail-closed:策略验证自身失败 → 回滚写盘
|
|
157
|
+
rollback();
|
|
158
|
+
return {
|
|
159
|
+
blocked: true,
|
|
160
|
+
error: `Policy gate failed (fail-closed): ${e.message} — file rolled back`,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
}
|
|
98
164
|
/** @requires INTENT @produces CODE */
|
|
99
165
|
/** @requires INTENT @produces CODE */
|
|
100
166
|
async function execute(intent, projectPath, filePath) {
|
|
@@ -149,14 +215,31 @@ async function execute(intent, projectPath, filePath) {
|
|
|
149
215
|
irFunctionCount: ir.length,
|
|
150
216
|
protocolRuleCount,
|
|
151
217
|
});
|
|
152
|
-
// 5. Write to file if requested
|
|
218
|
+
// 5. Write to file if requested(策略门:项目配置了 .progmune-policy.json
|
|
219
|
+
// 时,写盘后即时验证;BLOCK → 回滚写盘并返回策略违规——审计修复
|
|
220
|
+
// 2026-09-06:此前策略引擎与代码生成管道完全脱节,BLOCK 无强制力)
|
|
153
221
|
if (filePath) {
|
|
154
222
|
const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(projectPath, filePath);
|
|
155
223
|
const dir = path.dirname(resolvedPath);
|
|
156
224
|
if (!fs.existsSync(dir)) {
|
|
157
225
|
fs.mkdirSync(dir, { recursive: true });
|
|
158
226
|
}
|
|
227
|
+
const prevContent = fs.existsSync(resolvedPath)
|
|
228
|
+
? fs.readFileSync(resolvedPath, "utf-8") : undefined;
|
|
159
229
|
fs.writeFileSync(resolvedPath, code, "utf-8");
|
|
230
|
+
const gate = applyPolicyGateAfterWrite(projectPath, resolvedPath, prevContent);
|
|
231
|
+
if (gate.blocked) {
|
|
232
|
+
return {
|
|
233
|
+
success: false, degraded: planResult.degraded ?? false, code,
|
|
234
|
+
sessionId: planResult.sessionId, hash: "", ruleHash: planResult.ruleHash || "",
|
|
235
|
+
irFunctionCount: ir.length, protocolRuleCount, violations: 0,
|
|
236
|
+
repairApplied: false, repairCount: 0, repairBranchIds: [],
|
|
237
|
+
branchWinner: undefined,
|
|
238
|
+
policyDecision: gate.decision,
|
|
239
|
+
policyViolations: gate.violations,
|
|
240
|
+
error: gate.error,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
160
243
|
}
|
|
161
244
|
// 6. Compute hash (from code content)
|
|
162
245
|
const crypto = require("crypto");
|
package/dist/mcp-server.mjs
CHANGED
|
@@ -1090,6 +1090,7 @@ ${result.code.split("\n").slice(0, 80).join("\n")}` }] };
|
|
|
1090
1090
|
degraded: cert.degraded,
|
|
1091
1091
|
sessionId: cert.sessionId,
|
|
1092
1092
|
file: cert.file,
|
|
1093
|
+
timestamp: cert.timestamp,
|
|
1093
1094
|
},
|
|
1094
1095
|
accountability: acct ? {
|
|
1095
1096
|
humanEvents: acct.humanEvents,
|
package/dist/policy/cli.js
CHANGED
|
@@ -69,7 +69,13 @@ catch {
|
|
|
69
69
|
}
|
|
70
70
|
// 3. Load policy (from project config or defaults)
|
|
71
71
|
const projectDir = process.env.PROGMUNE_PROJECT_DIR || process.cwd();
|
|
72
|
-
const { rules, source } = (0, engine_1.loadPolicyConfig)(projectDir, policyPath);
|
|
72
|
+
const { rules, source, configError } = (0, engine_1.loadPolicyConfig)(projectDir, policyPath);
|
|
73
|
+
// 配置解析失败:拒绝静默评估(fail-closed 信号,审计修复 2026-09-06)
|
|
74
|
+
if (configError) {
|
|
75
|
+
console.error(`❌ ${configError}`);
|
|
76
|
+
console.error(` 拒绝在损坏的策略配置下评估——请修复配置后重试。`);
|
|
77
|
+
process.exit(2);
|
|
78
|
+
}
|
|
73
79
|
// 4. Evaluate policy
|
|
74
80
|
const ctx = {
|
|
75
81
|
certificate: {
|
|
@@ -83,6 +89,7 @@ const ctx = {
|
|
|
83
89
|
degraded: cert.degraded,
|
|
84
90
|
sessionId: cert.sessionId,
|
|
85
91
|
file: cert.file,
|
|
92
|
+
timestamp: cert.timestamp,
|
|
86
93
|
},
|
|
87
94
|
accountability: acct ? {
|
|
88
95
|
humanEvents: acct.humanEvents,
|
package/dist/policy/engine.js
CHANGED
|
@@ -46,6 +46,10 @@ exports.loadEnterprisePolicyConfig = loadEnterprisePolicyConfig;
|
|
|
46
46
|
const fs = __importStar(require("fs"));
|
|
47
47
|
const path = __importStar(require("path"));
|
|
48
48
|
const types_1 = require("./types");
|
|
49
|
+
// 静态导入(vitest 环境下 lazy require 的 CJS 互操作不可靠——engine.ts
|
|
50
|
+
// 同款陷阱,2026-08-27 DSH 修复先例;两模块无环,静态导入安全)
|
|
51
|
+
const risk_model_1 = require("../risk-model");
|
|
52
|
+
const protocol_knowledge_1 = require("../protocol-knowledge");
|
|
49
53
|
function confidenceToNumber(c) {
|
|
50
54
|
if (c === "high")
|
|
51
55
|
return 2;
|
|
@@ -93,8 +97,39 @@ function loadPolicyConfig(projectPath, configPath) {
|
|
|
93
97
|
return { rules: merged, source: cfgFile };
|
|
94
98
|
}
|
|
95
99
|
catch (e) {
|
|
96
|
-
|
|
97
|
-
|
|
100
|
+
// 审计修复(fail-closed 信号):解析失败不再静默回退——显式携带
|
|
101
|
+
// configError,由调用方(policy CLI / MCP)决定拒绝评估并报错
|
|
102
|
+
console.error(`⚠️ Failed to load policy config: ${e.message}.`);
|
|
103
|
+
return {
|
|
104
|
+
rules: [...types_1.DEFAULT_POLICY],
|
|
105
|
+
source: "built-in defaults (config error)",
|
|
106
|
+
configError: `Failed to parse ${cfgFile}: ${e.message}`,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/** 真实调用提取(certificate.file 的 best-effort 词法扫描,供 risk 规则
|
|
111
|
+
* 使用——不伪造输入;读不到文件时返回空数组,由调用方按 fail-closed
|
|
112
|
+
* 计为违规) */
|
|
113
|
+
function extractCallsBestEffort(file) {
|
|
114
|
+
if (!file)
|
|
115
|
+
return [];
|
|
116
|
+
try {
|
|
117
|
+
const fs = require("fs");
|
|
118
|
+
const code = fs.readFileSync(file, "utf-8");
|
|
119
|
+
const calls = [];
|
|
120
|
+
const re = /(?<=^|[^\w$])([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)\s*\(/g;
|
|
121
|
+
let m;
|
|
122
|
+
while ((m = re.exec(code)) !== null) {
|
|
123
|
+
const seg = m[1].split(".").pop() || "";
|
|
124
|
+
if (/^(if|for|while|switch|catch|return|new|case|do|super|this)$/.test(seg))
|
|
125
|
+
continue;
|
|
126
|
+
if (calls.length < 200 && !calls.includes(seg))
|
|
127
|
+
calls.push(seg);
|
|
128
|
+
}
|
|
129
|
+
return calls;
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return [];
|
|
98
133
|
}
|
|
99
134
|
}
|
|
100
135
|
function evaluatePolicy(ctx, rules) {
|
|
@@ -177,24 +212,44 @@ function evaluatePolicy(ctx, rules) {
|
|
|
177
212
|
const minSeverity = rule.threshold ?? 2;
|
|
178
213
|
const minConfidence = rule.require ?? 70;
|
|
179
214
|
try {
|
|
180
|
-
|
|
181
|
-
//
|
|
182
|
-
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
const sevOrder = ["Low", "Medium", "High", "Critical"];
|
|
186
|
-
return sevOrder.indexOf(p.severity) >= minSeverity && p.confidence >= minConfidence;
|
|
187
|
-
});
|
|
188
|
-
if (criticalOrHigh.length > 0) {
|
|
215
|
+
// 真实调用提取(certificate.file 的 best-effort 词法扫描)——
|
|
216
|
+
// 审计修复(Kimi 2026-09-06):不再伪造 ["SSL_CTX_new","SSL_connect"]
|
|
217
|
+
// 假输入喂评估;输入不可得时按 fail-closed 计为违规
|
|
218
|
+
const calls = extractCallsBestEffort(ctx.certificate.file);
|
|
219
|
+
if (calls.length === 0) {
|
|
189
220
|
violations.push({
|
|
190
221
|
rule,
|
|
191
|
-
actual:
|
|
192
|
-
expected:
|
|
193
|
-
detail:
|
|
222
|
+
actual: "no call data extractable from certificate file",
|
|
223
|
+
expected: "risk assessment over real call sequence",
|
|
224
|
+
detail: "risk rule requires a readable file with calls; unavailable input is a violation (fail-closed)",
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
const risk = (0, risk_model_1.assessRisk)(calls);
|
|
229
|
+
const criticalOrHigh = risk.patterns.filter((p) => {
|
|
230
|
+
const sevOrder = ["Low", "Medium", "High", "Critical"];
|
|
231
|
+
return sevOrder.indexOf(p.severity) >= minSeverity && p.confidence >= minConfidence;
|
|
194
232
|
});
|
|
233
|
+
if (criticalOrHigh.length > 0) {
|
|
234
|
+
violations.push({
|
|
235
|
+
rule,
|
|
236
|
+
actual: `${criticalOrHigh.length} risk pattern(s) ≥ severity threshold`,
|
|
237
|
+
expected: `0 patterns at this severity+confidence level`,
|
|
238
|
+
detail: criticalOrHigh.map((p) => `${p.patternName} (${p.severity}, ${p.confidence}%): ${p.detail}`).join("; "),
|
|
239
|
+
});
|
|
240
|
+
}
|
|
195
241
|
}
|
|
196
242
|
}
|
|
197
|
-
catch {
|
|
243
|
+
catch {
|
|
244
|
+
// fail-closed:risk model 不可用 → 显式违规,而非静默跳过
|
|
245
|
+
// (审计修复:原空 catch 使整条规则无声失效)
|
|
246
|
+
violations.push({
|
|
247
|
+
rule,
|
|
248
|
+
actual: "risk model unavailable",
|
|
249
|
+
expected: "risk assessment completed",
|
|
250
|
+
detail: "risk-model module failed to load — failing closed (explicit violation)",
|
|
251
|
+
});
|
|
252
|
+
}
|
|
198
253
|
break;
|
|
199
254
|
}
|
|
200
255
|
// ── Knowledge Base Coverage ──
|
|
@@ -202,11 +257,20 @@ function evaluatePolicy(ctx, rules) {
|
|
|
202
257
|
const minStable = rule.threshold ?? 3;
|
|
203
258
|
let stableCount = 0;
|
|
204
259
|
try {
|
|
205
|
-
const
|
|
206
|
-
const kb = buildKnowledgeBase();
|
|
260
|
+
const kb = (0, protocol_knowledge_1.buildKnowledgeBase)();
|
|
207
261
|
stableCount = kb.units.filter((u) => u.maturity === "stable").length;
|
|
208
262
|
}
|
|
209
|
-
catch {
|
|
263
|
+
catch {
|
|
264
|
+
// fail-closed 显式化(审计修复):原空 catch 后 stableCount=0 恰好
|
|
265
|
+
// 触发违规,但消息误导为「0 stable assets」——改为明确的不可用违规
|
|
266
|
+
violations.push({
|
|
267
|
+
rule,
|
|
268
|
+
actual: "knowledge base unavailable",
|
|
269
|
+
expected: `>= ${minStable} stable assets`,
|
|
270
|
+
detail: "protocol-knowledge module failed to load — failing closed (explicit violation)",
|
|
271
|
+
});
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
210
274
|
if (stableCount < minStable) {
|
|
211
275
|
violations.push({
|
|
212
276
|
rule,
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
/**
|
|
37
|
+
* policy/engine.test.ts — 策略引擎 fail-closed 回归(审计修复 2026-09-06)
|
|
38
|
+
*
|
|
39
|
+
* 锁定 Kimi 审计的三条修复:
|
|
40
|
+
* 1. risk 规则不再伪造 ["SSL_CTX_new","SSL_connect"] 输入——无真实调用
|
|
41
|
+
* 数据时按 fail-closed 计违规
|
|
42
|
+
* 2. 配置解析失败显式携带 configError(不再静默回退默认)
|
|
43
|
+
* 3. execute 写盘策略门:项目 opt-in(.progmune-policy.json)时 BLOCK
|
|
44
|
+
* 回滚写盘;未配置时无操作
|
|
45
|
+
*/
|
|
46
|
+
const vitest_1 = require("vitest");
|
|
47
|
+
const fs = __importStar(require("fs"));
|
|
48
|
+
const os = __importStar(require("os"));
|
|
49
|
+
const path = __importStar(require("path"));
|
|
50
|
+
const engine_1 = require("./engine");
|
|
51
|
+
const execute_1 = require("../execute");
|
|
52
|
+
let dir;
|
|
53
|
+
(0, vitest_1.beforeEach)(() => { dir = fs.mkdtempSync(path.join(os.tmpdir(), "pm-policy-")); });
|
|
54
|
+
(0, vitest_1.afterEach)(() => { fs.rmSync(dir, { recursive: true, force: true }); });
|
|
55
|
+
function baseCtx(file) {
|
|
56
|
+
return {
|
|
57
|
+
certificate: {
|
|
58
|
+
validated: true,
|
|
59
|
+
confidence: "high",
|
|
60
|
+
provenanceIntact: true,
|
|
61
|
+
fingerprint: "test-fp",
|
|
62
|
+
violations: 0,
|
|
63
|
+
plsbCoverage: "10/13",
|
|
64
|
+
plsbRecall: 1,
|
|
65
|
+
degraded: false,
|
|
66
|
+
sessionId: "test-session",
|
|
67
|
+
file,
|
|
68
|
+
timestamp: new Date().toISOString(),
|
|
69
|
+
},
|
|
70
|
+
accountability: {
|
|
71
|
+
humanEvents: 1,
|
|
72
|
+
aiEvents: 1,
|
|
73
|
+
automatedEvents: 0,
|
|
74
|
+
custodyGap: false,
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
(0, vitest_1.describe)("policy engine(fail-closed 回归)", () => {
|
|
79
|
+
(0, vitest_1.it)("risk 规则:无可提取调用数据 → 显式违规(fail-closed,不再伪造输入)", () => {
|
|
80
|
+
const f = path.join(dir, "empty.ts");
|
|
81
|
+
fs.writeFileSync(f, "");
|
|
82
|
+
const result = (0, engine_1.evaluatePolicy)(baseCtx(f));
|
|
83
|
+
const riskViolations = result.violations.filter((v) => v.rule.type === "risk");
|
|
84
|
+
(0, vitest_1.expect)(riskViolations.length).toBeGreaterThanOrEqual(1);
|
|
85
|
+
(0, vitest_1.expect)(riskViolations[0].detail).toContain("fail-closed");
|
|
86
|
+
});
|
|
87
|
+
(0, vitest_1.it)("risk 规则:真实调用提取后良性代码不产生风险违规", () => {
|
|
88
|
+
const f = path.join(dir, "benign.ts");
|
|
89
|
+
fs.writeFileSync(f, "function hello() { console.log('x'); return computeSum(a, b); }");
|
|
90
|
+
const result = (0, engine_1.evaluatePolicy)(baseCtx(f));
|
|
91
|
+
const riskViolations = result.violations.filter((v) => v.rule.type === "risk");
|
|
92
|
+
(0, vitest_1.expect)(riskViolations).toHaveLength(0);
|
|
93
|
+
});
|
|
94
|
+
(0, vitest_1.it)("loadPolicyConfig:JSON 解析失败显式携带 configError(不再静默回退)", () => {
|
|
95
|
+
fs.writeFileSync(path.join(dir, ".progmune-policy.json"), "{ broken json !!!");
|
|
96
|
+
const res = (0, engine_1.loadPolicyConfig)(dir);
|
|
97
|
+
(0, vitest_1.expect)(res.configError).toBeDefined();
|
|
98
|
+
(0, vitest_1.expect)(res.configError).toContain("Failed to parse");
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
(0, vitest_1.describe)("execute 写盘策略门(opt-in)", () => {
|
|
102
|
+
const MARKED = `// @progmune-generated session=s1 timestamp=2026-09-06T00:00:00.000Z
|
|
103
|
+
function doThing() { return 1; }
|
|
104
|
+
`;
|
|
105
|
+
(0, vitest_1.it)("未配置 .progmune-policy.json → 无操作(旧行为)", () => {
|
|
106
|
+
const f = path.join(dir, "out.ts");
|
|
107
|
+
fs.writeFileSync(f, MARKED);
|
|
108
|
+
const gate = (0, execute_1.applyPolicyGateAfterWrite)(dir, f);
|
|
109
|
+
(0, vitest_1.expect)(gate.blocked).toBe(false);
|
|
110
|
+
(0, vitest_1.expect)(fs.existsSync(f)).toBe(true);
|
|
111
|
+
});
|
|
112
|
+
(0, vitest_1.it)("配置阻断规则 → BLOCK 回滚(新文件删除)", () => {
|
|
113
|
+
fs.writeFileSync(path.join(dir, ".progmune-policy.json"), JSON.stringify({
|
|
114
|
+
inherit: false,
|
|
115
|
+
rules: [{ type: "confidence", severity: "block", threshold: 2 }],
|
|
116
|
+
}));
|
|
117
|
+
const f = path.join(dir, "out.ts");
|
|
118
|
+
fs.writeFileSync(f, MARKED);
|
|
119
|
+
const gate = (0, execute_1.applyPolicyGateAfterWrite)(dir, f);
|
|
120
|
+
(0, vitest_1.expect)(gate.blocked).toBe(true);
|
|
121
|
+
(0, vitest_1.expect)(gate.decision).toBe("BLOCK");
|
|
122
|
+
(0, vitest_1.expect)(fs.existsSync(f)).toBe(false); // 回滚 = 删除新文件
|
|
123
|
+
});
|
|
124
|
+
(0, vitest_1.it)("配置阻断规则 → BLOCK 回滚(已有文件恢复原内容)", () => {
|
|
125
|
+
fs.writeFileSync(path.join(dir, ".progmune-policy.json"), JSON.stringify({
|
|
126
|
+
inherit: false,
|
|
127
|
+
rules: [{ type: "confidence", severity: "block", threshold: 2 }],
|
|
128
|
+
}));
|
|
129
|
+
const f = path.join(dir, "out.ts");
|
|
130
|
+
const prev = "// original content\n";
|
|
131
|
+
const gate = (0, execute_1.applyPolicyGateAfterWrite)(dir, f, prev);
|
|
132
|
+
(0, vitest_1.expect)(gate.blocked).toBe(true);
|
|
133
|
+
(0, vitest_1.expect)(fs.readFileSync(f, "utf-8")).toBe(prev); // 恢复原内容
|
|
134
|
+
});
|
|
135
|
+
});
|
package/dist/sdk.js
CHANGED
|
@@ -20,7 +20,7 @@ const risk_model_1 = require("./risk-model");
|
|
|
20
20
|
const protocol_knowledge_1 = require("./protocol-knowledge");
|
|
21
21
|
const evidence_repository_1 = require("./evidence-repository");
|
|
22
22
|
/** Runtime version — stable public identifier. Internal layers evolve underneath. */
|
|
23
|
-
exports.RUNTIME_VERSION = "3.7.
|
|
23
|
+
exports.RUNTIME_VERSION = "3.7.23";
|
|
24
24
|
function verify(filePath) {
|
|
25
25
|
const cert = (0, certify_1.certify)(filePath);
|
|
26
26
|
const kb = (0, protocol_knowledge_1.buildKnowledgeBase)();
|