progmune-runtime 3.7.23 → 3.7.25

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 CHANGED
@@ -1,10 +1,32 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.7.25] — 2026-09-10
4
+
5
+ ### 失败语料上报链路修复(中央 hub 恢复数据流)
6
+
7
+ - **诊断**:中央 hub 自 2026-05-18 起零新增(totalCount=12),npm 周下载 1000-2000 但数据全丢——两个断点:① `immune-reporter` 读废弃路径 `failure_corpus/`(语料已统一到 `.progmune_corpus/`)② 端点默认 `localhost:3000`,安装态用户到不了中央 hub
8
+ - **修复**:数据源对齐 `.progmune_corpus/{date}/fail_*.json`(`PROGMUNE_PROJECT_DIR`/`PROGMUNE_CORPUS_DIR` 感知,损坏记录跳过);端点默认 `https://progmune-runtime.fly.dev/report`;`PROGMUNE_HUB=off` 可关闭上报
9
+ - **默认脱敏**:函数名 SHA-256 截 12 位(同名同哈希,模式聚合仍有效);`PROGMUNE_FINGERPRINT_DETAIL=1` 才发送原文函数名;源码与变量值永不上传
10
+ - **隐私口径如实披露**:README/落地页/privacy 页——「代码不上传」保持为真,「无遥测」表述改为如实披露脱敏指纹默认上报与关闭方式
11
+ - **中央 hub 防护层上线**:字段白名单 + 类型校验(SVL-1~4、时间戳 ±窗口、序列长度上限);请求体 512KB / 每请求 ≤500 条;实例+时间戳+模式指纹去重;每实例每 10 分钟 ≤5 次;每日 ≤10000 条;可选 `PROGMUNE_HUB_TOKEN` Bearer 认证(客户端 `PROGMUNE_HUB_TOKEN` 环境变量同步支持)
12
+ - 回归 12 green;hub 冒烟六场景(正常/畸形/去重/超大/限流/认证)全过
13
+
14
+ ## [3.7.24] — 2026-09-06
15
+
16
+ ### 审计复检三项修复
17
+
18
+ - **空规则集 fail-closed**:`evaluatePolicy` 对空规则数组直接 BLOCK(合成 `policy_config` 违规)——空数组是 truthy,原实现零规则→零违规→ALLOW;写盘门 / policy CLI / MCP 三路径同步收口,测试锁定
19
+ - **loadEnterprisePolicyConfig 解析失败显式携带 configError**(与 loadPolicyConfig 同口径,六条 fail-open 中第 4 条全修)
20
+ - **写盘门口径修正**(README 双语 / TIERED_POLICY / AUDIT_RESPONSE):「写盘策略门」→「**写后回滚门——BLOCK 时文件不残留**」,明确为补偿控制而非写前拦截,写盘与回滚之间进程中断的残留为已知边界
21
+ - **落地页 66 金标口径自解释**:「66 金标 检出 64(97%)/ 100% / 0 FP——2 条为注解依赖前置的已知漏检(基线文档单列)」,与发布门 64 一致
22
+ - npm gitHead 溯源核对:npm gitHead = tag v3.7.23 commit,三点对齐(发布产物 ↔ tag ↔ 发布 commit)
23
+ - 回归 29 green;check 免疫正常
24
+
3
25
  ## [3.7.23] — 2026-09-06
4
26
 
5
27
  ### 第三方审计修复批次 + Oracle 隔离政策 + 分资产分级策略
6
28
 
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
29
+ - **治理层 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
30
  - **跨命名空间死规则归位 + 活性守卫**(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
31
  - **P1 批**:SUPPRESS 等全部决策持久化审计事件日志(含 fpRate/envFactor/importance 全输入);截断序列覆盖率显式降级上报(truncatedSequences + summary 标注);README 双语边界声明(刻意规避不在防护目标内 / BLOCK 强制力三执行点);FAQ 澄清 LLM 密钥为可选增强;落地页免疫叙事收敛
10
32
  - **Oracle 隔离政策**(Hunyuan 定律 5 落地,`docs/ORACLE_ISOLATION_POLICY.md`):四类评估体系变更(规则/匹配策略/阈值/别名)必须人工确认,AI 只提案;protocols.json 全量 `status: "confirmed"` + 加载端跳过 proposed + `scripts/rule-propose.js` 提案脚手架;evaluateTrust 输出 **oracleIndependence**(confirmedRules / sameSourceAnnotations / confirmedAliases)
package/README.md CHANGED
@@ -6,6 +6,10 @@
6
6
  [![MCP](https://img.shields.io/badge/MCP-Compatible-blue)](https://modelcontextprotocol.io)
7
7
  [![TS Benchmark](https://img.shields.io/badge/TS%20R98.5%25%20P100%25-22c55e)]()
8
8
  [![Python Benchmark](https://img.shields.io/badge/Python%20R100%25%20P100%25-22c55e)]()
9
+ [![npm version](https://img.shields.io/npm/v/progmune-runtime?color=cb3837)](https://www.npmjs.com/package/progmune-runtime)
10
+ [![npm weekly downloads](https://img.shields.io/npm/dw/progmune-runtime?color=cb3837)](https://www.npmjs.com/package/progmune-runtime)
11
+ [![GitHub stars](https://img.shields.io/github/stars/shenlian19831109/progmune-runtime?color=gold)](https://github.com/shenlian19831109/progmune-runtime)
12
+ [![GitHub release](https://img.shields.io/github/v/release/shenlian19831109/progmune-runtime?color=gold)](https://github.com/shenlian19831109/progmune-runtime/releases)
9
13
 
10
14
  > [中文版](https://github.com/shenlian19831109/progmune-runtime/blob/main/README.zh-CN.md) · English Version
11
15
 
@@ -119,7 +123,7 @@ Progmune is honest about what it can and cannot verify.
119
123
  - **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
124
  - **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
125
  - **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).
126
+ - **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-then-rollback gate** (with `.progmune-policy.json`, `execute` writes then verifies immediately; on BLOCK the file does not persist. This is a compensating control, not pre-write interception: a process killed between write and rollback may leave the content behind — a known boundary).
123
127
 
124
128
  **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
129
 
package/README.zh-CN.md CHANGED
@@ -119,7 +119,7 @@ Progmune 对能验证什么、不能验证什么保持诚实。
119
119
  - **框架内部件**——知名框架的分发/缓存机制(如 django-unicorn 内部件)可能产生少量边界误报;各语料基准 gold 文件中已逐条记录。
120
120
  - **已知失败边界一律记录**而非隐藏:如果 Progmune 无法验证某语言(如 Go),置信度会降低而不是假装 100%。
121
121
  - **刻意规避不在防护目标内**——注解驱动模型拦截的是 AI 的「意外」协议错误(漏写前置步骤、顺序颠倒);对故意改名/混淆的对抗性规避不设防(任何静态工具同理)。
122
- - **BLOCK 的强制力由集成方承担**——Progmune 输出判定与证据,不运行时拦截。OS 级强制执行点:`trust --ci` 退出码(CI 门禁)、`policy CLI` 退出码、以及 opt-in 的写盘策略门(项目配置 `.progmune-policy.json` 后,execute 写盘即时验证,BLOCK 自动回滚)。
122
+ - **BLOCK 的强制力由集成方承担**——Progmune 输出判定与证据,不运行时拦截。OS 级强制执行点:`trust --ci` 退出码(CI 门禁)、`policy CLI` 退出码、以及 opt-in 的**写后回滚门**(项目配置 `.progmune-policy.json` 后,execute 写盘随即验证,BLOCK 时文件不残留。注意这是补偿控制而非写前拦截:写盘与回滚之间若进程被中断,违规内容可能短暂/持续残留——已知边界)。
123
123
 
124
124
  **分资产分级接入**([TIERED_POLICY](https://github.com/shenlian19831109/progmune-runtime/blob/main/docs/TIERED_POLICY.md)):不要给所有代码同一套门禁——按「出错代价 × AI 参与度」选档,一条命令落地:
125
125
 
@@ -33,6 +33,9 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.buildHeaders = buildHeaders;
37
+ exports.resolveEndpoint = resolveEndpoint;
38
+ exports.maskFunctionName = maskFunctionName;
36
39
  exports.extractFingerprints = extractFingerprints;
37
40
  exports.reportFingerprints = reportFingerprints;
38
41
  exports.previewFingerprints = previewFingerprints;
@@ -41,8 +44,34 @@ const path = __importStar(require("path"));
41
44
  const https = __importStar(require("https"));
42
45
  const http = __importStar(require("http"));
43
46
  const crypto = __importStar(require("crypto"));
44
- const REPORT_ENDPOINT = process.env.PROGMUNE_HUB || "http://localhost:3000/report";
45
- const CURSOR_FILE = path.resolve(__dirname, "../.progmune_memory/report_cursor.json");
47
+ // ── 数据源(与 failure-corpus.ts 统一路径,项目级、可写) ──
48
+ const PROJECT_DIR = process.env.PROGMUNE_PROJECT_DIR || process.cwd();
49
+ const CORPUS_DIR = process.env.PROGMUNE_CORPUS_DIR || path.resolve(PROJECT_DIR, ".progmune_corpus");
50
+ const CURSOR_FILE = path.join(CORPUS_DIR, ".report_cursor.json");
51
+ // ── 端点与开关 ──
52
+ const HUB_ENV = process.env.PROGMUNE_HUB;
53
+ const HUB_TOKEN = process.env.PROGMUNE_HUB_TOKEN;
54
+ const DETAIL_ENABLED = process.env.PROGMUNE_FINGERPRINT_DETAIL === "1";
55
+ const OFF_PATTERN = /^(off|0|false|no|disabled)$/i;
56
+ /** 上报请求头:hub 设了 PROGMUNE_HUB_TOKEN 时需带 Bearer 认证。 */
57
+ function buildHeaders(token = HUB_TOKEN) {
58
+ const headers = { "Content-Type": "application/json" };
59
+ if (token)
60
+ headers["Authorization"] = `Bearer ${token}`;
61
+ return headers;
62
+ }
63
+ /** 解析上报端点:显式关闭 → null;未设 → 中央 hub;其他 → 自定义 URL。 */
64
+ function resolveEndpoint(env = HUB_ENV) {
65
+ if (env && OFF_PATTERN.test(env))
66
+ return null;
67
+ return env || "https://progmune-runtime.fly.dev/report";
68
+ }
69
+ /** 脱敏:默认哈希函数名;DETAIL 开关打开时保留原文。 */
70
+ function maskFunctionName(name, detail = DETAIL_ENABLED) {
71
+ if (detail)
72
+ return name;
73
+ return "fn:" + crypto.createHash("sha256").update(name).digest("hex").substring(0, 12);
74
+ }
46
75
  function getInstanceId() {
47
76
  const host = require("os").hostname();
48
77
  const cwd = process.cwd();
@@ -62,8 +91,8 @@ function saveReportCursor(timestamp, count) {
62
91
  fs.mkdirSync(dir, { recursive: true });
63
92
  fs.writeFileSync(CURSOR_FILE, JSON.stringify({ lastTimestamp: timestamp, reportedCount: count, updatedAt: new Date().toISOString() }, null, 2));
64
93
  }
65
- function extractFingerprints(cursor) {
66
- const corpusDir = path.resolve(__dirname, "../failure_corpus");
94
+ /** 收集语料目录中的新失败指纹(含脱敏处理)。corpusDir 参数便于测试注入。 */
95
+ function extractFingerprints(cursor, corpusDir = CORPUS_DIR) {
67
96
  if (!fs.existsSync(corpusDir))
68
97
  return [];
69
98
  const fingerprints = [];
@@ -73,16 +102,22 @@ function extractFingerprints(cursor) {
73
102
  if (!fs.statSync(datePath).isDirectory())
74
103
  continue;
75
104
  for (const file of fs.readdirSync(datePath).sort()) {
76
- if (!file.endsWith(".json"))
105
+ if (!file.endsWith(".json") || !file.startsWith("fail_"))
77
106
  continue;
78
107
  const filePath = path.join(datePath, file);
79
- const record = JSON.parse(fs.readFileSync(filePath, "utf-8"));
80
- // 如果游标存在且该记录时间戳 <= 游标,跳过
81
- if (cursor && cursor.lastTimestamp && record.timestamp <= cursor.lastTimestamp)
108
+ let record;
109
+ try {
110
+ record = JSON.parse(fs.readFileSync(filePath, "utf-8"));
111
+ }
112
+ catch {
113
+ continue; /* 损坏记录跳过,best-effort */
114
+ }
115
+ // 游标存在且该记录时间戳 <= 游标,跳过
116
+ if (cursor && cursor.lastTimestamp && (record.timestamp || "") <= cursor.lastTimestamp)
82
117
  continue;
83
118
  const funcSeq = (record.actionSequence || [])
84
119
  .filter((a) => a.kind === "call")
85
- .map((a) => a.function);
120
+ .map((a) => maskFunctionName(String(a.function)));
86
121
  fingerprints.push({
87
122
  instance_id: instanceId,
88
123
  timestamp: record.timestamp,
@@ -99,6 +134,10 @@ function extractFingerprints(cursor) {
99
134
  }
100
135
  /** @requires CORPUS @produces FINGERPRINT_REPORT */
101
136
  async function reportFingerprints() {
137
+ const endpoint = resolveEndpoint();
138
+ if (endpoint === null) {
139
+ return { success: true, message: "上报已禁用(PROGMUNE_HUB=off),跳过" };
140
+ }
102
141
  const cursor = getReportCursor();
103
142
  const fingerprints = extractFingerprints(cursor);
104
143
  if (fingerprints.length === 0) {
@@ -109,11 +148,11 @@ async function reportFingerprints() {
109
148
  const maxTimestamp = fingerprints[fingerprints.length - 1].timestamp;
110
149
  const payload = JSON.stringify({ fingerprints });
111
150
  return new Promise((resolve) => {
112
- const url = new URL(REPORT_ENDPOINT);
151
+ const url = new URL(endpoint);
113
152
  const transport = url.protocol === "https:" ? https : http;
114
- const req = transport.request(REPORT_ENDPOINT, {
153
+ const req = transport.request(endpoint, {
115
154
  method: "POST",
116
- headers: { "Content-Type": "application/json" },
155
+ headers: buildHeaders(),
117
156
  }, (res) => {
118
157
  let data = "";
119
158
  res.on("data", (chunk) => data += chunk);
@@ -128,6 +167,7 @@ async function reportFingerprints() {
128
167
  }
129
168
  });
130
169
  });
170
+ req.setTimeout(8000, () => req.destroy(new Error("上报超时")));
131
171
  req.on("error", (e) => resolve({ success: false, message: `网络错误: ${e.message}` }));
132
172
  req.write(payload);
133
173
  req.end();
@@ -0,0 +1,149 @@
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
+ const vitest_1 = require("vitest");
37
+ const fs = __importStar(require("fs"));
38
+ const os = __importStar(require("os"));
39
+ const path = __importStar(require("path"));
40
+ const immune_reporter_1 = require("./immune-reporter");
41
+ /**
42
+ * Immune Reporter 测试——2026-09 修复轮:
43
+ * 默认脱敏、PROGMUNE_HUB=off 开关、语料路径对齐 .progmune_corpus。
44
+ * 涉及文件系统的用例使用临时目录注入(corpusDir 参数),不触碰真实语料。
45
+ */
46
+ let tmpDir;
47
+ (0, vitest_1.beforeEach)(() => {
48
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "progmune-reporter-"));
49
+ });
50
+ (0, vitest_1.afterEach)(() => {
51
+ fs.rmSync(tmpDir, { recursive: true, force: true });
52
+ });
53
+ (0, vitest_1.describe)("resolveEndpoint", () => {
54
+ (0, vitest_1.it)("未设置 PROGMUNE_HUB 时默认指向中央 hub", () => {
55
+ (0, vitest_1.expect)((0, immune_reporter_1.resolveEndpoint)(undefined)).toBe("https://progmune-runtime.fly.dev/report");
56
+ });
57
+ (0, vitest_1.it)("PROGMUNE_HUB=off 及其变体(0/false/no/disabled)关闭上报", () => {
58
+ for (const v of ["off", "0", "false", "no", "disabled", "OFF"]) {
59
+ (0, vitest_1.expect)((0, immune_reporter_1.resolveEndpoint)(v)).toBeNull();
60
+ }
61
+ });
62
+ (0, vitest_1.it)("自定义 URL 原样生效", () => {
63
+ (0, vitest_1.expect)((0, immune_reporter_1.resolveEndpoint)("http://localhost:9999/report")).toBe("http://localhost:9999/report");
64
+ });
65
+ });
66
+ (0, vitest_1.describe)("maskFunctionName", () => {
67
+ (0, vitest_1.it)("默认脱敏:同名函数产生相同哈希(模式聚合保持有效)", () => {
68
+ const a = (0, immune_reporter_1.maskFunctionName)("generate_jwt", false);
69
+ const b = (0, immune_reporter_1.maskFunctionName)("generate_jwt", false);
70
+ (0, vitest_1.expect)(a).toBe(b);
71
+ (0, vitest_1.expect)(a).toMatch(/^fn:[0-9a-f]{12}$/);
72
+ (0, vitest_1.expect)(a).not.toContain("generate_jwt");
73
+ });
74
+ (0, vitest_1.it)("不同函数名产生不同哈希", () => {
75
+ (0, vitest_1.expect)((0, immune_reporter_1.maskFunctionName)("generate_jwt", false)).not.toBe((0, immune_reporter_1.maskFunctionName)("create_session", false));
76
+ });
77
+ (0, vitest_1.it)("PROGMUNE_FINGERPRINT_DETAIL=1 时保留原文", () => {
78
+ (0, vitest_1.expect)((0, immune_reporter_1.maskFunctionName)("generate_jwt", true)).toBe("generate_jwt");
79
+ });
80
+ });
81
+ (0, vitest_1.describe)("buildHeaders", () => {
82
+ (0, vitest_1.it)("无 token 时只带 Content-Type", () => {
83
+ (0, vitest_1.expect)((0, immune_reporter_1.buildHeaders)(undefined)).toEqual({ "Content-Type": "application/json" });
84
+ });
85
+ (0, vitest_1.it)("设置 PROGMUNE_HUB_TOKEN 时带 Bearer 认证头", () => {
86
+ const h = (0, immune_reporter_1.buildHeaders)("secret-token");
87
+ (0, vitest_1.expect)(h["Authorization"]).toBe("Bearer secret-token");
88
+ (0, vitest_1.expect)(h["Content-Type"]).toBe("application/json");
89
+ });
90
+ });
91
+ (0, vitest_1.describe)("extractFingerprints", () => {
92
+ function writeRecord(date, id, record) {
93
+ const dir = path.join(tmpDir, date);
94
+ fs.mkdirSync(dir, { recursive: true });
95
+ fs.writeFileSync(path.join(dir, `fail_${id}.json`), JSON.stringify(record));
96
+ }
97
+ (0, vitest_1.it)("从 .progmune_corpus 路径读取并默认脱敏函数名", () => {
98
+ writeRecord("2026-09-09", "1", {
99
+ timestamp: "2026-09-09T10:00:00.000Z",
100
+ violatedSVL: "SVL-4",
101
+ constraintType: "protocol",
102
+ actionSequence: [{ kind: "call", function: "generate_jwt" }],
103
+ ssgState: ["UNAUTHENTICATED"],
104
+ });
105
+ const fps = (0, immune_reporter_1.extractFingerprints)({ lastTimestamp: null }, tmpDir);
106
+ (0, vitest_1.expect)(fps.length).toBe(1);
107
+ (0, vitest_1.expect)(fps[0].functionSequence[0]).toMatch(/^fn:[0-9a-f]{12}$/);
108
+ (0, vitest_1.expect)(fps[0].preState).toEqual([["UNAUTHENTICATED"]]);
109
+ });
110
+ (0, vitest_1.it)("游标之后的新记录才会上报,旧记录跳过", () => {
111
+ writeRecord("2026-09-01", "old", {
112
+ timestamp: "2026-09-01T10:00:00.000Z",
113
+ violatedSVL: "SVL-4",
114
+ constraintType: "protocol",
115
+ actionSequence: [],
116
+ });
117
+ writeRecord("2026-09-09", "new", {
118
+ timestamp: "2026-09-09T10:00:00.000Z",
119
+ violatedSVL: "SVL-4",
120
+ constraintType: "protocol",
121
+ actionSequence: [],
122
+ });
123
+ const fps = (0, immune_reporter_1.extractFingerprints)({ lastTimestamp: "2026-09-05T00:00:00.000Z" }, tmpDir);
124
+ (0, vitest_1.expect)(fps.length).toBe(1);
125
+ (0, vitest_1.expect)(fps[0].timestamp).toBe("2026-09-09T10:00:00.000Z");
126
+ });
127
+ (0, vitest_1.it)("非 fail_ 前缀文件与损坏 JSON 跳过,不中断", () => {
128
+ writeRecord("2026-09-09", "ok", {
129
+ timestamp: "2026-09-09T10:00:00.000Z",
130
+ violatedSVL: "SVL-4",
131
+ constraintType: "protocol",
132
+ actionSequence: [],
133
+ });
134
+ fs.writeFileSync(path.join(tmpDir, "2026-09-09", "trajectory_x.json"), "{}");
135
+ fs.writeFileSync(path.join(tmpDir, "2026-09-09", "fail_broken.json"), "{corrupted");
136
+ const fps = (0, immune_reporter_1.extractFingerprints)({ lastTimestamp: null }, tmpDir);
137
+ (0, vitest_1.expect)(fps.length).toBe(1);
138
+ });
139
+ (0, vitest_1.it)("无调用序列时 functionSequence 为空数组而非报错", () => {
140
+ writeRecord("2026-09-09", "empty", {
141
+ timestamp: "2026-09-09T10:00:00.000Z",
142
+ violatedSVL: "SVL-2",
143
+ constraintType: "types",
144
+ });
145
+ const fps = (0, immune_reporter_1.extractFingerprints)({ lastTimestamp: null }, tmpDir);
146
+ (0, vitest_1.expect)(fps.length).toBe(1);
147
+ (0, vitest_1.expect)(fps[0].functionSequence).toEqual([]);
148
+ });
149
+ });
@@ -135,6 +135,28 @@ function extractCallsBestEffort(file) {
135
135
  function evaluatePolicy(ctx, rules) {
136
136
  const activeRules = rules || types_1.DEFAULT_POLICY;
137
137
  const violations = [];
138
+ // fail-closed(审计复检 2026-09-06):空数组是 truthy,原实现会
139
+ // 零规则 → 零违规 → ALLOW——空规则集必须拒绝而非放行
140
+ if (activeRules.length === 0) {
141
+ return {
142
+ passed: false,
143
+ verdict: "BLOCK",
144
+ rules: 0,
145
+ passed_rules: 0,
146
+ failed_rules: 1,
147
+ violations: [{
148
+ rule: {
149
+ type: "policy_config",
150
+ severity: "block",
151
+ description: "Empty policy ruleset is rejected (fail-closed)",
152
+ },
153
+ actual: "0 active rules",
154
+ expected: ">= 1 active rule",
155
+ detail: "empty ruleset passes nothing — refusing to ALLOW (fail-closed)",
156
+ }],
157
+ summary: "BLOCK: empty policy ruleset",
158
+ };
159
+ }
138
160
  for (const rule of activeRules) {
139
161
  switch (rule.type) {
140
162
  // ── Confidence ──
@@ -391,7 +413,13 @@ function loadEnterprisePolicyConfig(projectPath, configPath) {
391
413
  };
392
414
  }
393
415
  catch (e) {
394
- console.error(`⚠️ Failed to load enterprise policy config: ${e.message}. Using defaults.`);
395
- return emptyResult;
416
+ console.error(`⚠️ Failed to load enterprise policy config: ${e.message}.`);
417
+ // fail-closed 信号(审计复检 2026-09-06):与 loadPolicyConfig 同口径——
418
+ // 显式携带 configError,调用方拒绝静默评估
419
+ return {
420
+ ...emptyResult,
421
+ source: "built-in defaults (enterprise config error)",
422
+ configError: `Failed to parse ${cfgFile}: ${e.message}`,
423
+ };
396
424
  }
397
425
  }
@@ -97,6 +97,14 @@ function baseCtx(file) {
97
97
  (0, vitest_1.expect)(res.configError).toBeDefined();
98
98
  (0, vitest_1.expect)(res.configError).toContain("Failed to parse");
99
99
  });
100
+ (0, vitest_1.it)("空规则集 fail-closed:[] 是 truthy 但必须拒绝(BLOCK,非 ALLOW)", () => {
101
+ const f = path.join(dir, "empty.ts");
102
+ fs.writeFileSync(f, "");
103
+ const result = (0, engine_1.evaluatePolicy)(baseCtx(f), []);
104
+ (0, vitest_1.expect)(result.passed).toBe(false);
105
+ (0, vitest_1.expect)(result.verdict).toBe("BLOCK");
106
+ (0, vitest_1.expect)(result.violations.some((v) => v.rule.type === "policy_config")).toBe(true);
107
+ });
100
108
  });
101
109
  (0, vitest_1.describe)("execute 写盘策略门(opt-in)", () => {
102
110
  const MARKED = `// @progmune-generated session=s1 timestamp=2026-09-06T00:00:00.000Z
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";
23
+ exports.RUNTIME_VERSION = "3.7.25";
24
24
  function verify(filePath) {
25
25
  const cert = (0, certify_1.certify)(filePath);
26
26
  const kb = (0, protocol_knowledge_1.buildKnowledgeBase)();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "progmune-runtime",
3
- "version": "3.7.23",
3
+ "version": "3.7.25",
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/",