@lqc123qwe/car-runtime 1.9.0 → 1.11.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/README.md +17 -1
- package/dist/SHA256SUMS +1 -0
- package/dist/authz/authz.d.ts +46 -0
- package/dist/authz/authz.js +76 -0
- package/dist/car-runtime-1.11.0.tgz +0 -0
- package/dist/car-runtime-1.11.0.tgz.sig.bundle +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +825 -0
- package/dist/dx/doctor.d.ts +62 -0
- package/dist/dx/doctor.js +127 -0
- package/dist/governance/dump.d.ts +47 -0
- package/dist/governance/dump.js +17 -0
- package/dist/host/facade.d.ts +13 -0
- package/dist/host/facade.js +154 -0
- package/dist/host/hostGateway.d.ts +117 -0
- package/dist/host/hostGateway.js +65 -0
- package/dist/host/mappings.d.ts +27 -0
- package/dist/host/mappings.js +52 -0
- package/dist/host/stdio.d.ts +14 -0
- package/dist/host/stdio.js +83 -0
- package/dist/kernel/context.d.ts +76 -0
- package/dist/kernel/context.js +203 -0
- package/dist/kernel/events.d.ts +37 -0
- package/dist/kernel/events.js +82 -0
- package/dist/load/config.d.ts +79 -0
- package/dist/load/config.js +285 -0
- package/dist/load/loader.d.ts +72 -0
- package/dist/load/loader.js +162 -0
- package/dist/load/registry.d.ts +44 -0
- package/dist/load/registry.js +58 -0
- package/dist/load/report.d.ts +98 -0
- package/dist/load/report.js +301 -0
- package/dist/load/sigGate.d.ts +22 -0
- package/dist/load/sigGate.js +68 -0
- package/dist/load/sign.d.ts +25 -0
- package/dist/load/sign.js +47 -0
- package/dist/load/verifier.d.ts +35 -0
- package/dist/load/verifier.js +56 -0
- package/dist/loop/goal.d.ts +31 -0
- package/dist/loop/goal.js +34 -0
- package/dist/loop/recover.d.ts +17 -0
- package/dist/loop/recover.js +39 -0
- package/dist/loop/stop.d.ts +82 -0
- package/dist/loop/stop.js +184 -0
- package/dist/mcp/gateway.d.ts +96 -0
- package/dist/mcp/gateway.js +69 -0
- package/dist/mcp/httpClient.d.ts +27 -0
- package/dist/mcp/httpClient.js +196 -0
- package/dist/mcp/stdioClient.d.ts +8 -0
- package/dist/mcp/stdioClient.js +73 -0
- package/dist/ptc/budget.d.ts +21 -0
- package/dist/ptc/budget.js +36 -0
- package/dist/ptc/erasable.d.ts +14 -0
- package/dist/ptc/erasable.js +27 -0
- package/dist/ptc/runCode.d.ts +65 -0
- package/dist/ptc/runCode.js +140 -0
- package/dist/ptc/schemaRender.d.ts +15 -0
- package/dist/ptc/schemaRender.js +90 -0
- package/dist/ptc/sdk.d.ts +27 -0
- package/dist/ptc/sdk.js +23 -0
- package/dist/ptc/worker-entry.d.ts +1 -0
- package/dist/ptc/worker-entry.js +71 -0
- package/dist/release-audit.jsonl +4 -0
- package/dist/runtime-core/chatStep.d.ts +43 -0
- package/dist/runtime-core/chatStep.js +121 -0
- package/dist/runtime-core/credentials.d.ts +49 -0
- package/dist/runtime-core/credentials.js +118 -0
- package/dist/runtime-core/errors.d.ts +30 -0
- package/dist/runtime-core/errors.js +36 -0
- package/dist/runtime-core/llm.d.ts +76 -0
- package/dist/runtime-core/llm.js +638 -0
- package/dist/runtime-core/redaction.d.ts +39 -0
- package/dist/runtime-core/redaction.js +79 -0
- package/dist/runtime-core/telemetry.d.ts +88 -0
- package/dist/runtime-core/telemetry.js +332 -0
- package/dist/runtime-core/tools.d.ts +15 -0
- package/dist/runtime-core/tools.js +12 -0
- package/dist/runtime-core/trace.d.ts +55 -0
- package/dist/runtime-core/trace.js +137 -0
- package/dist/runtime-core/types.d.ts +104 -0
- package/dist/runtime-core/types.js +13 -0
- package/dist/sandbox/sandbox.d.ts +68 -0
- package/dist/sandbox/sandbox.js +153 -0
- package/dist/sbom.json +263 -0
- package/dist/seam.d.ts +24 -0
- package/dist/seam.js +23 -0
- package/dist/security/secrets-baseline.d.ts +43 -0
- package/dist/security/secrets-baseline.js +176 -0
- package/dist/security/secrets.d.ts +45 -0
- package/dist/security/secrets.js +138 -0
- package/dist/session/export.d.ts +55 -0
- package/dist/session/export.js +81 -0
- package/dist/session/fork.d.ts +51 -0
- package/dist/session/fork.js +63 -0
- package/dist/session/format.d.ts +21 -0
- package/dist/session/format.js +49 -0
- package/dist/session/indexStore.d.ts +44 -0
- package/dist/session/indexStore.js +265 -0
- package/dist/session/log.d.ts +79 -0
- package/dist/session/log.js +157 -0
- package/dist/session/store.d.ts +9 -0
- package/dist/session/store.js +37 -0
- package/dist/telemetry/metrics.d.ts +25 -0
- package/dist/telemetry/metrics.js +26 -0
- package/package.json +17 -1
- package/src/authz/authz.ts +12 -0
- package/src/loop/stop.ts +43 -10
- package/src/runtime-core/chatStep.ts +22 -2
- package/src/runtime-core/llm.ts +96 -13
- package/src/runtime-core/types.ts +22 -1
- package/src/seam.ts +24 -0
- package/src/session/log.ts +1 -1
- package/.gitattributes +0 -11
- package/.github/workflows/ci.yml +0 -293
- package/.github/workflows/release.yml +0 -54
- package/adr/ADR-001.md +0 -4
- package/adr/ADR-002.md +0 -5
- package/adr/ADR-003.md +0 -35
- package/adr/host-onboarding.md +0 -68
- package/adr/mlps-audit-checklist.md +0 -41
- package/adr/win32-spike.md +0 -45
- package/docs/plugin-authoring.md +0 -82
- package/native/landlock-run/Makefile +0 -19
- package/native/landlock-run/landlock-run.c +0 -162
- package/scripts/contract-check.ts +0 -56
- package/scripts/invariant-n1.ts +0 -96
- package/scripts/package-check.ts +0 -29
- package/scripts/ptc-baseline/run-baseline.ts +0 -118
- package/scripts/release-artifacts.ts +0 -111
- package/scripts/release-pipeline.ts +0 -245
- package/scripts/secrets-baseline.ts +0 -33
- package/scripts/secrets-scan.ts +0 -56
- package/scripts/win32-ffi-debug.ts +0 -143
- package/scripts/win32-koffi-spike.ts +0 -537
- package/test/contract/eventmap.golden.json +0 -7
- package/test/fixtures/hello-plugin.ts +0 -5
- package/test/kernel.spec.ts +0 -136
- package/test/registry-e2e/run-e2e.ts +0 -213
- package/test/s11.spec.ts +0 -129
- package/test/s12.spec.ts +0 -135
- package/test/s13.spec.ts +0 -117
- package/test/s14.spec.ts +0 -117
- package/test/s15.spec.ts +0 -80
- package/test/s16.spec.ts +0 -133
- package/test/s17.spec.ts +0 -102
- package/test/s18.spec.ts +0 -81
- package/test/s19.spec.ts +0 -138
- package/test/s2.spec.ts +0 -245
- package/test/s20.spec.ts +0 -241
- package/test/s21.spec.ts +0 -231
- package/test/s22-m7-store-format.spec.ts +0 -135
- package/test/s23-m7-export-index.spec.ts +0 -189
- package/test/s24-signature-gate.spec.ts +0 -185
- package/test/s25-m8-runtime-core.spec.ts +0 -600
- package/test/s26-signature-closeout.spec.ts +0 -349
- package/test/s27-host-load-wiring.spec.ts +0 -134
- package/test/s28-session-index-followup.spec.ts +0 -236
- package/test/s29-otel-producer.spec.ts +0 -252
- package/test/s3.spec.ts +0 -179
- package/test/s30-release-artifacts.spec.ts +0 -127
- package/test/s31-real-model-wiring.spec.ts +0 -329
- package/test/s32-telemetry-mcp-ptc.spec.ts +0 -218
- package/test/s33-anthropic-adapter.spec.ts +0 -354
- package/test/s34-mcp-remote-transport.spec.ts +0 -250
- package/test/s35-config-ts-zod.spec.ts +0 -145
- package/test/s36-host-tool-surface.spec.ts +0 -250
- package/test/s37-projection-dedupe.spec.ts +0 -161
- package/test/s38-config-jiti.spec.ts +0 -116
- package/test/s39-otlp-span-semantics.spec.ts +0 -100
- package/test/s4.spec.ts +0 -169
- package/test/s40-metrics-cumulative.spec.ts +0 -67
- package/test/s41-mcp-serve-turn-span.spec.ts +0 -148
- package/test/s42-traceparent-propagation.spec.ts +0 -415
- package/test/s43-remote-http-traceparent.spec.ts +0 -262
- package/test/s5.spec.ts +0 -95
- package/test/s6.spec.ts +0 -169
- package/test/s7.spec.ts +0 -136
- package/test/s7b-baseline.spec.ts +0 -20
- package/test/s9.spec.ts +0 -89
- package/test/sandbox-escape/j17-orchestrate.sh +0 -30
- package/test/sandbox-escape/run-matrix.sh +0 -98
- package/tsconfig.json +0 -18
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* C-02 会话索引(§3.2.M7.2 索引行 + §3.2.M7.5 Step 3「异步可容忍,丢失可 rebuild」)
|
|
3
|
+
*
|
|
4
|
+
* 口径:
|
|
5
|
+
* - node:sqlite 能力探测(22.5+ 需 --experimental-sqlite / 23.4+ 原生);不可用 = CAR-E-SQLITE 显式拒绝,非静默;
|
|
6
|
+
* - rebuild 语义:全量从 JSONL 重建,单事务幂等覆盖(DELETE + INSERT),任何时刻盘上索引自洽;
|
|
7
|
+
* - 损坏面不吞错:断链 / CAR-E-FORMAT / zstd 能力缺失的文件不入索引,逐条进 errors 报告——
|
|
8
|
+
* 索引只收录链完整会话(审计面口径:可索引 = 可验证);
|
|
9
|
+
* - 物理编码 layout-blind:plain / zstd 统一经 format.ts 装载,索引记录 encoding 供检索。
|
|
10
|
+
*/
|
|
11
|
+
import { readdirSync, existsSync, realpathSync } from 'node:fs';
|
|
12
|
+
import { join, resolve } from 'node:path';
|
|
13
|
+
import { loadSessionLog } from './log.js';
|
|
14
|
+
const CAR_E_SQLITE = 'CAR-E-SQLITE: 当前 Node 无 node:sqlite 能力(22.5+ 需 --experimental-sqlite,23.4+ 原生)——显式拒绝,不静默降级';
|
|
15
|
+
/** node:sqlite 能力探测(模块存在性;静态 import 在无能力 Node 上会崩模块加载,故动态) */
|
|
16
|
+
export async function sqliteAvailable() {
|
|
17
|
+
try {
|
|
18
|
+
await import('node:sqlite');
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** 打开索引库(db 文件必须已存在——lookup 不产生建文件副作用;open 失败 = 索引文件无效显式报错) */
|
|
26
|
+
async function openExistingDb(dbPath) {
|
|
27
|
+
if (!existsSync(dbPath)) {
|
|
28
|
+
throw new Error(`CAR-E-INDEX: 会话索引不存在:${dbPath}——先执行 car session rebuild-index 建立索引`);
|
|
29
|
+
}
|
|
30
|
+
const mod = await import('node:sqlite').catch(() => null);
|
|
31
|
+
if (!mod)
|
|
32
|
+
throw new Error(CAR_E_SQLITE);
|
|
33
|
+
const DatabaseSync = mod.DatabaseSync;
|
|
34
|
+
try {
|
|
35
|
+
return new DatabaseSync(dbPath);
|
|
36
|
+
}
|
|
37
|
+
catch (e) {
|
|
38
|
+
throw new Error(`CAR-E-INDEX: 索引文件无效(打开失败:${e.message}):${dbPath}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** 单会话检索(1.2-S1 W3-1):--session <id> 经索引解析 sessionId → 文件;未收录 = null(「可索引 = 可验证」口径——断链会话不入索引) */
|
|
42
|
+
export async function lookupSession(dbPath, sessionId) {
|
|
43
|
+
const db = await openExistingDb(dbPath);
|
|
44
|
+
try {
|
|
45
|
+
const table = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='sessions'").get();
|
|
46
|
+
if (!table)
|
|
47
|
+
throw new Error(`CAR-E-INDEX: 索引文件无效(无 sessions 表):${dbPath}`);
|
|
48
|
+
const row = db.prepare('SELECT * FROM sessions WHERE sessionId = ?').get(sessionId);
|
|
49
|
+
if (!row)
|
|
50
|
+
return null;
|
|
51
|
+
return {
|
|
52
|
+
sessionId: String(row.sessionId),
|
|
53
|
+
file: String(row.file),
|
|
54
|
+
eventCount: Number(row.eventCount),
|
|
55
|
+
chainHead: row.chainHead == null ? null : String(row.chainHead),
|
|
56
|
+
chainTail: row.chainTail == null ? null : String(row.chainTail),
|
|
57
|
+
firstTs: row.firstTs == null ? null : Number(row.firstTs),
|
|
58
|
+
lastTs: row.lastTs == null ? null : Number(row.lastTs),
|
|
59
|
+
tornTail: Number(row.tornTail) === 1,
|
|
60
|
+
encoding: row.encoding === 'zstd' ? 'zstd' : 'plain',
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
const msg = e.message;
|
|
65
|
+
if (msg.startsWith('CAR-E-'))
|
|
66
|
+
throw e; // 本层显式错误原样透传
|
|
67
|
+
// SQLite 惰性校验:非索引文件 open 不报错、query 才报(file is not a database)——统一收口为显式口径
|
|
68
|
+
throw new Error(`CAR-E-INDEX: 索引文件无效(${msg}):${dbPath}`);
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
db.close();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const CREATE_SESSIONS_SQL = `CREATE TABLE IF NOT EXISTS sessions (
|
|
75
|
+
sessionId TEXT PRIMARY KEY,
|
|
76
|
+
file TEXT NOT NULL,
|
|
77
|
+
eventCount INTEGER NOT NULL,
|
|
78
|
+
chainHead TEXT,
|
|
79
|
+
chainTail TEXT,
|
|
80
|
+
firstTs INTEGER,
|
|
81
|
+
lastTs INTEGER,
|
|
82
|
+
tornTail INTEGER NOT NULL DEFAULT 0,
|
|
83
|
+
encoding TEXT NOT NULL DEFAULT 'plain',
|
|
84
|
+
rebuiltAt TEXT NOT NULL
|
|
85
|
+
)`;
|
|
86
|
+
/** 单行增量 upsert(1.2-S2 W3-2「更新半边」):库/表不存在则自举创建(活会话无需先 rebuild 即可入索引) */
|
|
87
|
+
export async function upsertSessionRow(dbPath, row) {
|
|
88
|
+
const mod = await import('node:sqlite').catch(() => null);
|
|
89
|
+
if (!mod)
|
|
90
|
+
throw new Error(CAR_E_SQLITE);
|
|
91
|
+
const DatabaseSync = mod.DatabaseSync;
|
|
92
|
+
try {
|
|
93
|
+
const db = new DatabaseSync(dbPath);
|
|
94
|
+
try {
|
|
95
|
+
db.exec(CREATE_SESSIONS_SQL);
|
|
96
|
+
db.prepare('INSERT OR REPLACE INTO sessions (sessionId, file, eventCount, chainHead, chainTail, firstTs, lastTs, tornTail, encoding, rebuiltAt) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)').run(row.sessionId, row.file, row.eventCount, row.chainHead, row.chainTail, row.firstTs, row.lastTs, row.tornTail ? 1 : 0, row.encoding, new Date().toISOString());
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
db.close();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch (e) {
|
|
103
|
+
const msg = e.message;
|
|
104
|
+
if (msg.startsWith('CAR-E-'))
|
|
105
|
+
throw e;
|
|
106
|
+
throw new Error(`CAR-E-INDEX: 索引更新失败(${msg}):${dbPath}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
export class IndexUpdater {
|
|
110
|
+
#dbPath;
|
|
111
|
+
#debounceMs;
|
|
112
|
+
#onError;
|
|
113
|
+
#state = null;
|
|
114
|
+
#timer = null;
|
|
115
|
+
#capabilityMissing = false;
|
|
116
|
+
#updates = 0;
|
|
117
|
+
#failures = 0;
|
|
118
|
+
constructor(opts) {
|
|
119
|
+
this.#dbPath = opts.dbPath;
|
|
120
|
+
this.#debounceMs = opts.debounceMs ?? 2_000;
|
|
121
|
+
this.#onError = opts.onError ?? (e => { try {
|
|
122
|
+
console.error(`[index-updater] ${e.message}`);
|
|
123
|
+
}
|
|
124
|
+
catch { /* 收口期 stderr 不可用 */ } });
|
|
125
|
+
}
|
|
126
|
+
get stats() {
|
|
127
|
+
return { updates: this.#updates, failures: this.#failures };
|
|
128
|
+
}
|
|
129
|
+
/** append sink 链上调用(行 JSON):seq 权威增量;非预期行防御性忽略 */
|
|
130
|
+
record(sessionId, line, file) {
|
|
131
|
+
if (this.#capabilityMissing)
|
|
132
|
+
return;
|
|
133
|
+
let e;
|
|
134
|
+
try {
|
|
135
|
+
e = JSON.parse(line);
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const seq = Number(e.seq);
|
|
141
|
+
const ts = Number(e.ts);
|
|
142
|
+
const hash = typeof e.hash === 'string' ? e.hash : '';
|
|
143
|
+
if (!Number.isFinite(seq) || !Number.isFinite(ts) || !hash)
|
|
144
|
+
return;
|
|
145
|
+
// realpath 规范化(1.2-BUG-4):macOS tmpdir 为 symlink(/var → /private/var),子进程 cwd 由
|
|
146
|
+
// OS 返回物理路径——resolve() 不解析符号链接,父子进程会产出同一文件的两个别名形态;
|
|
147
|
+
// 入库统一为 realpath(文件刚被 append 过必存在;异常回退 resolve)
|
|
148
|
+
const canonical = (() => {
|
|
149
|
+
try {
|
|
150
|
+
return realpathSync(file);
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return resolve(file);
|
|
154
|
+
}
|
|
155
|
+
})();
|
|
156
|
+
const s = this.#state;
|
|
157
|
+
if (!s || s.sessionId !== sessionId || s.file !== canonical) {
|
|
158
|
+
this.#state = { sessionId, file: canonical, eventCount: seq + 1, chainHead: hash, chainTail: hash, firstTs: ts, lastTs: ts };
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
s.eventCount = Math.max(s.eventCount, seq + 1);
|
|
162
|
+
s.chainTail = hash;
|
|
163
|
+
s.lastTs = ts;
|
|
164
|
+
}
|
|
165
|
+
this.#timer ??= setTimeout(() => { void this.flush(); }, this.#debounceMs);
|
|
166
|
+
}
|
|
167
|
+
/** 立即落 pending(close/exit 时点调用;幂等——无 pending 即空转) */
|
|
168
|
+
async flush() {
|
|
169
|
+
if (this.#timer) {
|
|
170
|
+
clearTimeout(this.#timer);
|
|
171
|
+
this.#timer = null;
|
|
172
|
+
}
|
|
173
|
+
if (!this.#state)
|
|
174
|
+
return;
|
|
175
|
+
const s = this.#state;
|
|
176
|
+
const row = { ...s, tornTail: false, encoding: 'plain' };
|
|
177
|
+
try {
|
|
178
|
+
await upsertSessionRow(this.#dbPath, row);
|
|
179
|
+
this.#updates++;
|
|
180
|
+
}
|
|
181
|
+
catch (e) {
|
|
182
|
+
this.#failures++;
|
|
183
|
+
const msg = e.message;
|
|
184
|
+
if (msg.startsWith('CAR-E-SQLITE')) {
|
|
185
|
+
// 能力缺席:登记一次后停用(后续 record 不再调度——禁每窗口重复刷屏)
|
|
186
|
+
this.#capabilityMissing = true;
|
|
187
|
+
}
|
|
188
|
+
this.#onError(e);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/** 收口:取消未决 timer + 落 pending(car run 关句柄前调用) */
|
|
192
|
+
async close() {
|
|
193
|
+
await this.flush();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function scanLogFiles(root) {
|
|
197
|
+
const rels = readdirSync(root, { recursive: true });
|
|
198
|
+
return rels
|
|
199
|
+
.filter(f => f.endsWith('.jsonl') || f.endsWith('.jsonl.zstd'))
|
|
200
|
+
.sort()
|
|
201
|
+
.map(f => join(root, f));
|
|
202
|
+
}
|
|
203
|
+
/** 装载 + 校验 + 抽取索引行(格式/断链/zstd 能力问题以 throw 上抛 → errors 通道) */
|
|
204
|
+
function buildRow(file) {
|
|
205
|
+
const { log, brokenAt, encoding, tornTail } = loadSessionLog(file);
|
|
206
|
+
if (brokenAt !== null)
|
|
207
|
+
throw new Error(`哈希链断裂 @ seq=${brokenAt}`);
|
|
208
|
+
const events = log.events;
|
|
209
|
+
const first = events[0];
|
|
210
|
+
const last = events[events.length - 1];
|
|
211
|
+
return {
|
|
212
|
+
sessionId: log.sessionId,
|
|
213
|
+
file,
|
|
214
|
+
eventCount: events.length,
|
|
215
|
+
chainHead: first?.hash ?? null,
|
|
216
|
+
chainTail: last?.hash ?? null,
|
|
217
|
+
firstTs: first?.ts ?? null,
|
|
218
|
+
lastTs: last?.ts ?? null,
|
|
219
|
+
tornTail,
|
|
220
|
+
encoding,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
export async function rebuildIndex(root, dbPath) {
|
|
224
|
+
const mod = await import('node:sqlite').catch(() => null);
|
|
225
|
+
if (!mod)
|
|
226
|
+
throw new Error('CAR-E-SQLITE: 当前 Node 无 node:sqlite 能力(22.5+ 需 --experimental-sqlite,23.4+ 原生)——显式拒绝,不静默降级');
|
|
227
|
+
const DatabaseSync = mod.DatabaseSync;
|
|
228
|
+
const rows = [];
|
|
229
|
+
const errors = [];
|
|
230
|
+
for (const file of scanLogFiles(root)) {
|
|
231
|
+
try {
|
|
232
|
+
rows.push(buildRow(file));
|
|
233
|
+
}
|
|
234
|
+
catch (e) {
|
|
235
|
+
errors.push({ file, reason: e.message });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
const db = new DatabaseSync(dbPath);
|
|
239
|
+
try {
|
|
240
|
+
db.exec(`CREATE TABLE IF NOT EXISTS sessions (
|
|
241
|
+
sessionId TEXT PRIMARY KEY,
|
|
242
|
+
file TEXT NOT NULL,
|
|
243
|
+
eventCount INTEGER NOT NULL,
|
|
244
|
+
chainHead TEXT,
|
|
245
|
+
chainTail TEXT,
|
|
246
|
+
firstTs INTEGER,
|
|
247
|
+
lastTs INTEGER,
|
|
248
|
+
tornTail INTEGER NOT NULL DEFAULT 0,
|
|
249
|
+
encoding TEXT NOT NULL DEFAULT 'plain',
|
|
250
|
+
rebuiltAt TEXT NOT NULL
|
|
251
|
+
)`);
|
|
252
|
+
const rebuiltAt = new Date().toISOString();
|
|
253
|
+
db.exec('BEGIN');
|
|
254
|
+
db.exec('DELETE FROM sessions');
|
|
255
|
+
const insert = db.prepare('INSERT INTO sessions (sessionId, file, eventCount, chainHead, chainTail, firstTs, lastTs, tornTail, encoding, rebuiltAt) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)');
|
|
256
|
+
for (const r of rows) {
|
|
257
|
+
insert.run(r.sessionId, r.file, r.eventCount, r.chainHead, r.chainTail, r.firstTs, r.lastTs, r.tornTail ? 1 : 0, r.encoding, rebuiltAt);
|
|
258
|
+
}
|
|
259
|
+
db.exec('COMMIT');
|
|
260
|
+
}
|
|
261
|
+
finally {
|
|
262
|
+
db.close();
|
|
263
|
+
}
|
|
264
|
+
return { dbPath, rows, errors };
|
|
265
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
export type EventKind = 'user' | 'assistant' | 'toolCall' | 'toolResult' | 'turnEnd' | 'goalUpdate' | 'hostRaw' | 'fork' | 'system' | 'usage';
|
|
2
|
+
export type Actor = 'user' | 'model' | 'plugin' | 'runtime';
|
|
3
|
+
export interface SessionEvent {
|
|
4
|
+
seq: number;
|
|
5
|
+
ts: number;
|
|
6
|
+
actor: Actor;
|
|
7
|
+
kind: EventKind;
|
|
8
|
+
turnId: string;
|
|
9
|
+
payload: unknown;
|
|
10
|
+
meta?: Record<string, unknown>;
|
|
11
|
+
prevHash: string;
|
|
12
|
+
hash: string;
|
|
13
|
+
}
|
|
14
|
+
declare const sha: (s: string) => string;
|
|
15
|
+
export interface ModelSnapshot {
|
|
16
|
+
/** 快照对应日志前缀边界(事件数),断言时逐字节比对前缀投影 */
|
|
17
|
+
atSeq: number;
|
|
18
|
+
request: unknown[];
|
|
19
|
+
}
|
|
20
|
+
export declare class SessionLog {
|
|
21
|
+
#private;
|
|
22
|
+
readonly sessionId: string;
|
|
23
|
+
events: readonly SessionEvent[];
|
|
24
|
+
constructor(sessionId?: string);
|
|
25
|
+
/**
|
|
26
|
+
* 挂载落盘 sink(§3.2.M7.5 Step 1-2「先落日志后放行」):append 时同步写盘,
|
|
27
|
+
* sink 成功后才入内存;sink throw = 写失败 fail-fast(事件不入内存、不推进链尾,
|
|
28
|
+
* 调用方当前 turn 以 error 收口——内存与盘面永不失配)。
|
|
29
|
+
*/
|
|
30
|
+
attachSink(sink: (line: string) => void): void;
|
|
31
|
+
/** 唯一写入路径(append-only;无 update/delete API) */
|
|
32
|
+
append(actor: Actor, kind: EventKind, turnId: string, payload: unknown, meta?: Record<string, unknown>): SessionEvent;
|
|
33
|
+
/** 模型请求发起时调用(agent-loop 集成点):登记「请求时快照」 */
|
|
34
|
+
snapshotModelRequest(): ModelSnapshot;
|
|
35
|
+
/** 逐链校验;返回 null=完整,否则断链事件序号(审计导出前置,断链即中止) */
|
|
36
|
+
verifyChain(): number | null;
|
|
37
|
+
/**
|
|
38
|
+
* 投影:从日志前缀重建模型可见消息流(不依赖模型运行时状态)。
|
|
39
|
+
* 1.6-S1(D-23)同 id toolResult 收敛 last-wins:写类工具授权决策(runtime/toolResult)
|
|
40
|
+
* 与执行结果(plugin/toolResult)成对落链(M4 既有语义冻结——审计全量留痕不动),
|
|
41
|
+
* 模型可见流只保留最终结果(真实 provider 对重复 tool_call_id/tool_use_id 拒绝或未定义,
|
|
42
|
+
* 1.4 观察项经真实适配器落地升级为缺陷面)。id 缺席不收敛(hostRaw/无 id 载荷形态不变,
|
|
43
|
+
* golden ④ 投影语义兼容);授权拒绝单条即唯一结果原样保留。适配器侧零改动(消费已收敛上游)。
|
|
44
|
+
*/
|
|
45
|
+
deriveMessages(upTo?: number): Array<{
|
|
46
|
+
role: string;
|
|
47
|
+
[k: string]: unknown;
|
|
48
|
+
}>;
|
|
49
|
+
/** N1 不变量断言(CI 门禁雏形):全部快照必须与当前日志前缀投影逐字节一致 */
|
|
50
|
+
assertModelVisibleLogged(): {
|
|
51
|
+
ok: boolean;
|
|
52
|
+
failedAt?: number;
|
|
53
|
+
};
|
|
54
|
+
/** 只读统计(供监控/测试) */
|
|
55
|
+
stats(): {
|
|
56
|
+
events: number;
|
|
57
|
+
snapshots: number;
|
|
58
|
+
tailHash: string;
|
|
59
|
+
};
|
|
60
|
+
/** 从已校验事件数组重建(恢复 #tail,续跑 append 不断链——跨宿主 fork/resume 装载入口) */
|
|
61
|
+
static fromEvents(sessionId: string, events: readonly SessionEvent[]): SessionLog;
|
|
62
|
+
/** 导出取证数据(MVP 基础导出:JSONL 全量 + 审计元数据;F15 完整包为 M2) */
|
|
63
|
+
exportJSONL(): string;
|
|
64
|
+
}
|
|
65
|
+
/** 文件路径 → sessionId 推导:`session-<id>.jsonl[.zstd]` 去前缀后缀;`events.jsonl` 取父目录名(审计定位口径) */
|
|
66
|
+
export declare function deriveSessionId(file: string): string;
|
|
67
|
+
/**
|
|
68
|
+
* 从 JSONL 文件装载(审计回放/跨机迁移入口)。格式层语义见 format.ts:
|
|
69
|
+
* 撕裂尾显式报告(丢弃崩溃半行,不静默);完整行解析失败 CAR-E-FORMAT 拒绝装载;
|
|
70
|
+
* 断链 = brokenAt 返回(由调用方决定中止口径)。zstd 文件按 magic 嗅探直读。
|
|
71
|
+
*/
|
|
72
|
+
export declare function loadSessionLog(file: string): {
|
|
73
|
+
log: SessionLog;
|
|
74
|
+
brokenAt: number | null;
|
|
75
|
+
encoding: 'plain' | 'zstd';
|
|
76
|
+
tornTail: boolean;
|
|
77
|
+
tornTailBytes: number;
|
|
78
|
+
};
|
|
79
|
+
export { sha as sha256Hex };
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* append-only 会话日志层(F2 / N1 —— POC-3 断言框架的产品化)
|
|
3
|
+
*
|
|
4
|
+
* 设计口径:
|
|
5
|
+
* - 哈希链(SHA-256)为 CAR 自研增强(dsh 无此物,源码级核实)——合规卖点落地
|
|
6
|
+
* - 「Model-visible means logged」:断言口径 = 请求时快照 === 日志前缀 [0, atSeq) 投影
|
|
7
|
+
* - 1.6-S1(D-23):投影对同 id toolResult 收敛 last-wins——事件面/审计面不动,模型可见流去冗余
|
|
8
|
+
* - append-only:本层仅暴露 append/verifyChain/deriveMessages/assertModelVisibleLogged
|
|
9
|
+
*/
|
|
10
|
+
import { createHash } from 'node:crypto';
|
|
11
|
+
import { readFileSync } from 'node:fs';
|
|
12
|
+
import { decodeLogBuffer, splitJsonlLines } from './format.js';
|
|
13
|
+
const sha = (s) => createHash('sha256').update(s).digest('hex');
|
|
14
|
+
const canonical = (e) => sha(JSON.stringify({ seq: e.seq, ts: e.ts, actor: e.actor, kind: e.kind, turnId: e.turnId, payload: e.payload, meta: e.meta ?? null, prevHash: e.prevHash }));
|
|
15
|
+
export class SessionLog {
|
|
16
|
+
sessionId;
|
|
17
|
+
events = [];
|
|
18
|
+
#tail = 'GENESIS';
|
|
19
|
+
#snapshots = [];
|
|
20
|
+
#sink = null;
|
|
21
|
+
constructor(sessionId = 'S-' + Date.now().toString(36)) {
|
|
22
|
+
this.sessionId = sessionId;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* 挂载落盘 sink(§3.2.M7.5 Step 1-2「先落日志后放行」):append 时同步写盘,
|
|
26
|
+
* sink 成功后才入内存;sink throw = 写失败 fail-fast(事件不入内存、不推进链尾,
|
|
27
|
+
* 调用方当前 turn 以 error 收口——内存与盘面永不失配)。
|
|
28
|
+
*/
|
|
29
|
+
attachSink(sink) {
|
|
30
|
+
this.#sink = sink;
|
|
31
|
+
}
|
|
32
|
+
/** 唯一写入路径(append-only;无 update/delete API) */
|
|
33
|
+
append(actor, kind, turnId, payload, meta) {
|
|
34
|
+
const e = {
|
|
35
|
+
seq: this.events.length, ts: Date.now(), actor, kind, turnId, payload, meta, prevHash: this.#tail,
|
|
36
|
+
};
|
|
37
|
+
const full = { ...e, hash: canonical(e) };
|
|
38
|
+
if (this.#sink)
|
|
39
|
+
this.#sink(JSON.stringify(full));
|
|
40
|
+
this.events = [...this.events, full];
|
|
41
|
+
this.#tail = full.hash;
|
|
42
|
+
return full;
|
|
43
|
+
}
|
|
44
|
+
/** 模型请求发起时调用(agent-loop 集成点):登记「请求时快照」 */
|
|
45
|
+
snapshotModelRequest() {
|
|
46
|
+
const snap = { atSeq: this.events.length, request: structuredClone(this.deriveMessages(this.events.length)) };
|
|
47
|
+
this.#snapshots.push(snap);
|
|
48
|
+
return snap;
|
|
49
|
+
}
|
|
50
|
+
/** 逐链校验;返回 null=完整,否则断链事件序号(审计导出前置,断链即中止) */
|
|
51
|
+
verifyChain() {
|
|
52
|
+
let prev = 'GENESIS';
|
|
53
|
+
for (const e of this.events) {
|
|
54
|
+
if (e.prevHash !== prev || e.hash !== canonical(e))
|
|
55
|
+
return e.seq;
|
|
56
|
+
prev = e.hash;
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* 投影:从日志前缀重建模型可见消息流(不依赖模型运行时状态)。
|
|
62
|
+
* 1.6-S1(D-23)同 id toolResult 收敛 last-wins:写类工具授权决策(runtime/toolResult)
|
|
63
|
+
* 与执行结果(plugin/toolResult)成对落链(M4 既有语义冻结——审计全量留痕不动),
|
|
64
|
+
* 模型可见流只保留最终结果(真实 provider 对重复 tool_call_id/tool_use_id 拒绝或未定义,
|
|
65
|
+
* 1.4 观察项经真实适配器落地升级为缺陷面)。id 缺席不收敛(hostRaw/无 id 载荷形态不变,
|
|
66
|
+
* golden ④ 投影语义兼容);授权拒绝单条即唯一结果原样保留。适配器侧零改动(消费已收敛上游)。
|
|
67
|
+
*/
|
|
68
|
+
deriveMessages(upTo) {
|
|
69
|
+
const scoped = upTo !== undefined ? this.events.slice(0, upTo) : this.events;
|
|
70
|
+
const lastSeqById = new Map();
|
|
71
|
+
for (const e of scoped) {
|
|
72
|
+
if (e.kind !== 'toolResult')
|
|
73
|
+
continue;
|
|
74
|
+
const id = e.payload?.id;
|
|
75
|
+
if (typeof id === 'string')
|
|
76
|
+
lastSeqById.set(id, e.seq);
|
|
77
|
+
}
|
|
78
|
+
const out = [];
|
|
79
|
+
for (const e of scoped) {
|
|
80
|
+
if (e.kind === 'system')
|
|
81
|
+
out.push({ role: 'system', content: e.payload });
|
|
82
|
+
else if (e.kind === 'user')
|
|
83
|
+
out.push({ role: 'user', content: e.payload });
|
|
84
|
+
else if (e.kind === 'assistant')
|
|
85
|
+
out.push({ role: 'assistant', content: e.payload });
|
|
86
|
+
else if (e.kind === 'toolCall')
|
|
87
|
+
out.push({ role: 'assistant', toolCall: e.payload });
|
|
88
|
+
else if (e.kind === 'toolResult') {
|
|
89
|
+
const id = e.payload?.id;
|
|
90
|
+
if (typeof id === 'string' && lastSeqById.get(id) !== e.seq)
|
|
91
|
+
continue; // D-23:后续有同 id 最终结果,本条为授权中间记录
|
|
92
|
+
out.push({ role: 'toolResult', content: e.payload });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
/** N1 不变量断言(CI 门禁雏形):全部快照必须与当前日志前缀投影逐字节一致 */
|
|
98
|
+
assertModelVisibleLogged() {
|
|
99
|
+
for (const s of this.#snapshots) {
|
|
100
|
+
if (JSON.stringify(s.request) !== JSON.stringify(this.deriveMessages(s.atSeq))) {
|
|
101
|
+
return { ok: false, failedAt: s.atSeq };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return { ok: true };
|
|
105
|
+
}
|
|
106
|
+
/** 只读统计(供监控/测试) */
|
|
107
|
+
stats() {
|
|
108
|
+
return { events: this.events.length, snapshots: this.#snapshots.length, tailHash: this.#tail };
|
|
109
|
+
}
|
|
110
|
+
/** 从已校验事件数组重建(恢复 #tail,续跑 append 不断链——跨宿主 fork/resume 装载入口) */
|
|
111
|
+
static fromEvents(sessionId, events) {
|
|
112
|
+
const log = new SessionLog(sessionId);
|
|
113
|
+
log.events = [...events];
|
|
114
|
+
const last = events[events.length - 1];
|
|
115
|
+
if (last)
|
|
116
|
+
log.#tail = last.hash;
|
|
117
|
+
return log;
|
|
118
|
+
}
|
|
119
|
+
/** 导出取证数据(MVP 基础导出:JSONL 全量 + 审计元数据;F15 完整包为 M2) */
|
|
120
|
+
exportJSONL() {
|
|
121
|
+
return this.events.map(e => JSON.stringify(e)).join('\n') + '\n';
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/** 文件路径 → sessionId 推导:`session-<id>.jsonl[.zstd]` 去前缀后缀;`events.jsonl` 取父目录名(审计定位口径) */
|
|
125
|
+
export function deriveSessionId(file) {
|
|
126
|
+
const base = file.replace(/.*[/\\]/, '');
|
|
127
|
+
let id = base.replace(/\.jsonl\.zstd$/, '').replace(/\.jsonl$/, '');
|
|
128
|
+
if (id === 'events') {
|
|
129
|
+
const dir = file.replace(/[\\/][^/\\]*$/, '');
|
|
130
|
+
const name = dir.replace(/.*[/\\]/, '');
|
|
131
|
+
if (name && name !== dir)
|
|
132
|
+
id = name; // 有父目录才取父目录名,否则保留 'events'
|
|
133
|
+
}
|
|
134
|
+
else if (id.startsWith('session-'))
|
|
135
|
+
id = id.slice('session-'.length);
|
|
136
|
+
return id;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* 从 JSONL 文件装载(审计回放/跨机迁移入口)。格式层语义见 format.ts:
|
|
140
|
+
* 撕裂尾显式报告(丢弃崩溃半行,不静默);完整行解析失败 CAR-E-FORMAT 拒绝装载;
|
|
141
|
+
* 断链 = brokenAt 返回(由调用方决定中止口径)。zstd 文件按 magic 嗅探直读。
|
|
142
|
+
*/
|
|
143
|
+
export function loadSessionLog(file) {
|
|
144
|
+
const { text, encoding } = decodeLogBuffer(readFileSync(file));
|
|
145
|
+
const { lines, tornTail, tornTailBytes } = splitJsonlLines(text);
|
|
146
|
+
const events = lines.map((l, i) => {
|
|
147
|
+
try {
|
|
148
|
+
return JSON.parse(l);
|
|
149
|
+
}
|
|
150
|
+
catch (e) {
|
|
151
|
+
throw new Error(`CAR-E-FORMAT: ${file} 第 ${i + 1} 行非法 JSON——拒绝装载带病日志(${e.message})`);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
const log = SessionLog.fromEvents(deriveSessionId(file), events);
|
|
155
|
+
return { log, brokenAt: log.verifyChain(), encoding, tornTail, tornTailBytes };
|
|
156
|
+
}
|
|
157
|
+
export { sha as sha256Hex };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* C-01 落盘存储层(§3.2.M7.5 Step 1-2 兑现——「先落日志后放行」的物理承载)
|
|
3
|
+
*
|
|
4
|
+
* 口径:
|
|
5
|
+
* - 单行原子:O_APPEND 句柄上单 write 系统调用写完整一行(事件 JSON + '\n'),杜绝交错与半行交错面;
|
|
6
|
+
* - fsync 同步刷盘:append 返回 = 该事件已过掉电窗口,kill -9 不丢已返回事件;
|
|
7
|
+
* - fail-fast:任何写失败向上 throw → 调用方(agent loop)当前 turn 以 error 收口,
|
|
8
|
+
* 禁止继续产生模型可见内容(防不变量破坏,L0 不降级——SessionLog.attachSink 的 throw 传播即此语义);
|
|
9
|
+
* - 物理编码恒明文 JSONL(zstd 属归档/导出压缩,见 format.ts 文件头口径)。
|
|
10
|
+
*/
|
|
11
|
+
import { closeSync, fsyncSync, openSync, writeSync } from 'node:fs';
|
|
12
|
+
export class SessionFileStore {
|
|
13
|
+
#fd = -1;
|
|
14
|
+
path;
|
|
15
|
+
constructor(path) {
|
|
16
|
+
this.path = path;
|
|
17
|
+
this.#fd = openSync(path, 'a');
|
|
18
|
+
}
|
|
19
|
+
/** 唯一写入路径:单 write 原子 + fsync;失败即 throw(fail-fast,事件不入内存) */
|
|
20
|
+
append(line) {
|
|
21
|
+
if (this.#fd < 0)
|
|
22
|
+
throw new Error(`CAR-E-STORE: store 已关闭(${this.path})——fail-fast 拒绝续写`);
|
|
23
|
+
const buf = Buffer.from(line.endsWith('\n') ? line : line + '\n', 'utf-8');
|
|
24
|
+
let off = 0;
|
|
25
|
+
while (off < buf.length)
|
|
26
|
+
off += writeSync(this.#fd, buf, off);
|
|
27
|
+
fsyncSync(this.#fd);
|
|
28
|
+
}
|
|
29
|
+
/** 收口关闭(幂等);关闭后续写 = CAR-E-STORE 拒绝 */
|
|
30
|
+
close() {
|
|
31
|
+
if (this.#fd >= 0) {
|
|
32
|
+
const fd = this.#fd;
|
|
33
|
+
this.#fd = -1;
|
|
34
|
+
closeSync(fd);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S17 · 采集面(D-2 enforce 数据窗口 S18–S20 前置):零内容计数器
|
|
3
|
+
*
|
|
4
|
+
* 口径(M4系统设计增补 S17 采集面 / Q-08 遥测三原则 / D-3):
|
|
5
|
+
* - 3 个 Counter:car_load_total{result} / car_unsigned_confirmed{confirmed} / car_registry_decision{source};
|
|
6
|
+
* labels 仅枚举值(来源/结果/模式)——**零内容字段**(无代码/提示词/路径/会话 id 明文),Q-08 兼容可机器断言;
|
|
7
|
+
* - 双通道:遥测显式开(OTel 端点归用户)+ 登记表人工填报兜底(snapshot() 导出计数快照);
|
|
8
|
+
* 未开启遥测时仅进程内累计,不落地不缓存出站(BD-05 语义);
|
|
9
|
+
* - 本层是可插拔计数钩子(onCount 注入面),非完整 OTel Metrics——完整面显式排除在 S17 外。
|
|
10
|
+
*/
|
|
11
|
+
export type CounterName = 'car_load_total' | 'car_unsigned_confirmed' | 'car_registry_decision';
|
|
12
|
+
/** labels 仅枚举值——零内容红线由 AllowedLabels 类型约束 */
|
|
13
|
+
export type AllowedLabels = {
|
|
14
|
+
result?: 'ok' | 'failed';
|
|
15
|
+
confirmed?: 'yes' | 'no';
|
|
16
|
+
source?: 'registry' | 'npm-fallback' | 'rejected' | 'git' | 'path';
|
|
17
|
+
};
|
|
18
|
+
export interface Counters {
|
|
19
|
+
onCount(name: CounterName, labels?: AllowedLabels): void;
|
|
20
|
+
/** 计数快照(登记表人工填报兜底通道的数据源;不含任何内容字段) */
|
|
21
|
+
snapshot(): Record<string, number>;
|
|
22
|
+
}
|
|
23
|
+
export declare function createCounters(): Counters;
|
|
24
|
+
/** 零内容红线自检:snapshot 键不得包含内容型模式(sk-/路径分隔/空格长串) */
|
|
25
|
+
export declare function assertZeroContent(snapshot: Record<string, number>): boolean;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S17 · 采集面(D-2 enforce 数据窗口 S18–S20 前置):零内容计数器
|
|
3
|
+
*
|
|
4
|
+
* 口径(M4系统设计增补 S17 采集面 / Q-08 遥测三原则 / D-3):
|
|
5
|
+
* - 3 个 Counter:car_load_total{result} / car_unsigned_confirmed{confirmed} / car_registry_decision{source};
|
|
6
|
+
* labels 仅枚举值(来源/结果/模式)——**零内容字段**(无代码/提示词/路径/会话 id 明文),Q-08 兼容可机器断言;
|
|
7
|
+
* - 双通道:遥测显式开(OTel 端点归用户)+ 登记表人工填报兜底(snapshot() 导出计数快照);
|
|
8
|
+
* 未开启遥测时仅进程内累计,不落地不缓存出站(BD-05 语义);
|
|
9
|
+
* - 本层是可插拔计数钩子(onCount 注入面),非完整 OTel Metrics——完整面显式排除在 S17 外。
|
|
10
|
+
*/
|
|
11
|
+
export function createCounters() {
|
|
12
|
+
const store = new Map();
|
|
13
|
+
return {
|
|
14
|
+
onCount(name, labels = {}) {
|
|
15
|
+
const key = name + Object.entries(labels).sort().map(([k, v]) => `|${k}=${v}`).join('');
|
|
16
|
+
store.set(key, (store.get(key) ?? 0) + 1);
|
|
17
|
+
},
|
|
18
|
+
snapshot() {
|
|
19
|
+
return Object.fromEntries([...store.entries()].sort());
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/** 零内容红线自检:snapshot 键不得包含内容型模式(sk-/路径分隔/空格长串) */
|
|
24
|
+
export function assertZeroContent(snapshot) {
|
|
25
|
+
return Object.keys(snapshot).every(k => !/sk-|ghp_|\/|\\|.{60,}/.test(k));
|
|
26
|
+
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lqc123qwe/car-runtime",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.0",
|
|
4
4
|
"description": "Composable Agent Runtime (CAR) - microkernel plugin system with sandboxed PTC, append-only hashed session log, and MCP host bridge",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"engines": {
|
|
8
8
|
"node": ">=22.19.0"
|
|
9
9
|
},
|
|
10
|
+
"main": "./dist/seam.js",
|
|
11
|
+
"types": "./dist/seam.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/seam.d.ts",
|
|
15
|
+
"import": "./dist/seam.js"
|
|
16
|
+
},
|
|
17
|
+
"./src/*": "./src/*",
|
|
18
|
+
"./package.json": "./package.json"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"src",
|
|
23
|
+
"README.md"
|
|
24
|
+
],
|
|
10
25
|
"repository": {
|
|
11
26
|
"type": "git",
|
|
12
27
|
"url": "git+https://github.com/lqc624/car-runtime.git"
|
|
@@ -16,6 +31,7 @@
|
|
|
16
31
|
"registry": "https://registry.npmjs.org/"
|
|
17
32
|
},
|
|
18
33
|
"scripts": {
|
|
34
|
+
"build": "tsc -p tsconfig.build.json",
|
|
19
35
|
"test": "node --experimental-transform-types --test test/",
|
|
20
36
|
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
21
37
|
},
|
package/src/authz/authz.ts
CHANGED
|
@@ -61,6 +61,18 @@ export class AuthzService {
|
|
|
61
61
|
return this.#commit(decision)
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/** 会话策略直判(「会话内始终允许」粒度):decidedBy=policy 幂等落链,不入人审通道(审批回调 scope=session 写入 grants 后走此路) */
|
|
65
|
+
decideBySessionPolicy(req: AuthzRequest): AuthzDecision {
|
|
66
|
+
return this.#commit({
|
|
67
|
+
authorizationId: req.authorizationId,
|
|
68
|
+
decision: 'APPROVED',
|
|
69
|
+
decidedBy: 'policy',
|
|
70
|
+
reason: `session policy grant (always-allow): ${req.resource}`,
|
|
71
|
+
degraded: this.#degraded,
|
|
72
|
+
ts: Date.now(),
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
64
76
|
/** 人审(confirm 模式):120s 超时默认拒绝(timeout-default-deny) */
|
|
65
77
|
async decideByUser(req: AuthzRequest, approve: (req: AuthzRequest) => Promise<boolean>): Promise<AuthzDecision> {
|
|
66
78
|
const cached = this.#decisions.get(req.authorizationId)
|