@jjh32456/dsh-longlive 1.6.7 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +70 -1508
- package/LICENSE +1 -1
- package/QUICKSTART.md +34 -13
- package/README.en.md +18 -7
- package/README.md +34 -9
- package/SPEC.md +5 -5
- package/cordis.patch.yml +23 -9
- package/docs/ANSWER-GUARD.md +92 -0
- package/docs/ARCHITECTURE.md +4 -3
- package/docs/AUTO-RECALL-WIRING.md +1 -1
- package/docs/AUTO-RECALL.md +15 -9
- package/docs/CONFIG.md +168 -35
- package/docs/ERRORS.md +17 -5
- package/docs/FAQ.md +7 -7
- package/docs/FEATURES.md +96 -28
- package/docs/MIGRATION-1.6.0.md +2 -2
- package/docs/OPERATIONS.md +6 -6
- package/docs/PANEL-INTEGRATION.md +6 -0
- package/docs/PRIVACY.md +88 -0
- package/docs/QUICKSTART.md +5 -5
- package/docs/README.en.md +4 -2
- package/docs/RESIDENT-WATCHER.md +1 -1
- package/docs/SESSION-WATCH.md +6 -6
- package/docs/TROUBLESHOOTING.md +4 -4
- package/package.json +47 -117
- package/runbook.md +2 -2
- package/scripts/backfill-auto-cards.mjs +98 -0
- package/scripts/install-surgery-task.ps1 +5 -2
- package/scripts/lib/session-watch-fixture.mjs +1 -1
- package/scripts/longlive-doctor.mjs +6 -0
- package/scripts/migrate-180.mjs +163 -0
- package/scripts/recall-index-build.mjs +128 -0
- package/scripts/session-watch-drill.mjs +1 -1
- package/scripts/surgery-run.cmd +1 -1
- package/scripts/unattended-surgery.ps1 +5 -2
- package/scripts/verify-card-anchors.mjs +138 -0
- package/src/actions/audit.js +125 -0
- package/src/actions/evidence.js +122 -0
- package/src/actions/fs.js +162 -0
- package/src/activity/index.js +760 -0
- package/src/activity/model.js +351 -0
- package/src/activity/store.js +644 -0
- package/src/activity/thinking.js +176 -0
- package/src/adapters/constants.js +11 -0
- package/src/adapters/engram/data/aliases.json +51 -0
- package/src/adapters/engram/data/policy.json +8 -0
- package/src/adapters/engram/data/routing-rules.json +546 -0
- package/src/adapters/engram/index.js +235 -0
- package/src/adapters/registry.js +315 -0
- package/src/answer/audit.js +193 -0
- package/src/answer/evidence.js +30 -0
- package/src/answer/guard.js +1370 -0
- package/src/answer/index.js +1067 -0
- package/src/answer/query.js +111 -0
- package/src/answer/second-hop.mjs +101 -0
- package/src/auto-truncate.js +1 -1
- package/src/config.js +171 -47
- package/src/deps.js +6 -4
- package/src/features.js +182 -6
- package/src/heartbeat.js +156 -23
- package/src/identity/handover.js +52 -1
- package/src/identity/index.js +13 -5
- package/src/identity/inject-position.js +2 -2
- package/src/index.js +97 -7
- package/src/lifecycle/gap-clocks.js +328 -0
- package/src/lifecycle/index.js +269 -0
- package/src/live.js +22 -2
- package/src/memory/activity-capture.js +229 -0
- package/src/memory/anchors.js +194 -0
- package/src/memory/audit-records.js +222 -0
- package/src/memory/auto-cards.js +401 -0
- package/src/memory/capture.js +442 -0
- package/src/memory/card-anchors.js +203 -0
- package/src/memory/conflicts.js +106 -0
- package/src/memory/decay.js +311 -0
- package/src/memory/distill.js +471 -0
- package/src/memory/event-index.js +917 -0
- package/src/memory/evidence-chain.js +295 -0
- package/src/memory/evidence.js +145 -0
- package/src/memory/filter-runner.js +1 -1
- package/src/memory/filter.js +5 -1
- package/src/memory/forget-v2.js +410 -0
- package/src/memory/forget.js +71 -0
- package/src/memory/frames-activation.js +247 -0
- package/src/memory/fts-query.js +174 -0
- package/src/memory/governance.js +604 -0
- package/src/memory/index.js +1 -0
- package/src/memory/l2-gate.js +169 -0
- package/src/memory/lock.js +121 -0
- package/src/memory/migrate.js +139 -0
- package/src/memory/recall-v2.js +223 -0
- package/src/memory/recall.js +65 -2
- package/src/memory/records.js +425 -0
- package/src/memory/retention.js +128 -0
- package/src/memory/review.js +124 -0
- package/src/memory/trust.js +88 -0
- package/src/memory/version-chain.js +264 -0
- package/src/memory/wal.js +137 -0
- package/src/memory/working-frame.js +516 -0
- package/src/metrics/classify.mjs +5 -3
- package/src/metrics/index.mjs +21 -1
- package/src/metrics/prestep-latency.mjs +300 -0
- package/src/metrics/store.mjs +25 -0
- package/src/policy-profile.js +18 -3
- package/src/recall/anaphora.mjs +87 -0
- package/src/recall/cards.mjs +7 -0
- package/src/recall/embedding.mjs +8 -8
- package/src/recall/entities.mjs +112 -0
- package/src/recall/index.js +235 -19
- package/src/recall/inject-order.mjs +10 -8
- package/src/recall/privacy.mjs +105 -16
- package/src/recall/recall-core.mjs +116 -10
- package/src/recall/shared-visibility.js +302 -0
- package/src/recall/time-normalizer.mjs +196 -0
- package/src/role/activation.js +191 -0
- package/src/role/attention.js +318 -0
- package/src/role/base.js +36 -0
- package/src/role/channels.js +423 -0
- package/src/role/index.js +425 -0
- package/src/role/interfaces.js +115 -0
- package/src/role/personality.js +370 -0
- package/src/role/readers.js +151 -0
- package/src/role/relationships.js +477 -0
- package/src/role/store.js +202 -0
- package/src/role/writeprotect.js +373 -0
- package/src/router/aliases.json +176 -25
- package/src/router/background/agent-runner.mjs +1 -1
- package/src/router/background/anchor.mjs +1 -1
- package/src/router/background/audit.mjs +2 -2
- package/src/router/background/budget-ledger.mjs +2 -2
- package/src/router/background/host-sim.mjs +1 -1
- package/src/router/background/memory.mjs +4 -4
- package/src/router/background/queue.mjs +1 -1
- package/src/router/background/retry.mjs +1 -1
- package/src/router/background/router.mjs +1 -1
- package/src/router/background/stream.mjs +1 -1
- package/src/router/background/tool-policy.mjs +1 -1
- package/src/router/design-headings.json +5 -5
- package/src/router/face/config.mjs +1 -1
- package/src/router/face/identity.mjs +3 -3
- package/src/router/judge.mjs +8 -7
- package/src/router/policies/generic.json +21 -4
- package/src/router/route.js +109 -22
- package/src/router/router.mjs +10 -6
- package/src/router/routing-rules.json +260 -445
- package/src/router/run-sim.mjs +8 -8
- package/src/router/runtime.js +23 -12
- package/src/router/service/board.mjs +95 -15
- package/src/router/service/config.mjs +5 -5
- package/src/router/service/context.mjs +1 -1
- package/src/router/service/hint.mjs +14 -0
- package/src/router/service/queue.mjs +1 -1
- package/src/router/service/service.mjs +27 -11
- package/src/router/shadow.mjs +4 -3
- package/src/surgery-recovery.js +2 -2
- package/src/tool-output.js +2 -2
- package/src/tools.js +39 -7
- package/src/truncate-visible.js +2 -2
- package/src/ui/actions.js +1312 -0
- package/src/ui/panel-api.js +367 -0
- package/src/ui/panel-auth.js +251 -0
- package/src/ui/panel-integrity.js +243 -0
- package/src/ui/routes-readonly.js +695 -0
- package/src/ui/routes.js +3 -0
- package/docs/DEV.md +0 -279
- package/docs/PRESENCE-BUS-WIRING.md +0 -491
- package/docs/PRESENCE-BUS.md +0 -694
- package/docs/SURGERY.md +0 -186
- package/scripts/mark-core-cards.mjs +0 -289
- package/scripts/pack.mjs +0 -392
- package/scripts/router-proposals.mjs +0 -386
- package/src/router/fixtures/M1.json +0 -16
- package/src/router/fixtures/M2.json +0 -16
- package/src/router/fixtures/M3.json +0 -20
- package/src/router/fixtures/M4.json +0 -20
- package/src/router/fixtures/M5.json +0 -45
- package/src/router/fixtures/M6.json +0 -53
- package/src/router/fixtures/M7.json +0 -46
- package/src/router/fixtures/S1.json +0 -20
- package/src/router/fixtures/S2.json +0 -23
- package/src/router/fixtures/S3.json +0 -20
- package/src/router/fixtures/S4.json +0 -34
- package/src/router/fixtures/S5.json +0 -43
- package/src/router/fixtures/S6.json +0 -47
- package/src/router/fixtures/T1.json +0 -23
- package/src/router/fixtures/T2.json +0 -16
- package/src/router/fixtures/T3.json +0 -19
- package/src/router/fixtures/T4.json +0 -16
- package/src/router/fixtures/X1.json +0 -17
- package/src/router/fixtures/X2.json +0 -20
- package/src/router/gen-design-headings.mjs +0 -107
- package/src/router/policies/owner-rulings-2026-11-03.json +0 -62
- package/test/auto-recall.test.mjs +0 -474
- package/test/auto-truncate.test.mjs +0 -931
- package/test/autobind.test.mjs +0 -385
- package/test/bus.test.mjs +0 -1572
- package/test/cli-guard.test.mjs +0 -198
- package/test/conclusion.test.mjs +0 -408
- package/test/config-schema.test.mjs +0 -204
- package/test/d6-output.test.mjs +0 -194
- package/test/d6.test.mjs +0 -395
- package/test/deps.test.mjs +0 -342
- package/test/doctor-setup.test.mjs +0 -297
- package/test/embedding-recall.test.mjs +0 -347
- package/test/feature-flags.test.mjs +0 -568
- package/test/filter-shadow.test.mjs +0 -170
- package/test/fixtures/baseline-0.8.0-registration.json +0 -556
- package/test/fixtures/d6-header-tools.json +0 -1
- package/test/fixtures/lock-child.mjs +0 -67
- package/test/fixtures/pre-step-container.mjs +0 -100
- package/test/fixtures/privacy-memory/topics/pii-unmarked.md +0 -9
- package/test/fixtures/privacy-memory/topics/secret-local-only.md +0 -11
- package/test/fixtures/profile-session-longlive-config.json +0 -20
- package/test/fixtures/proposal-cas-child.mjs +0 -66
- package/test/host-engine-status.test.mjs +0 -307
- package/test/hot-truncate.test.mjs +0 -1349
- package/test/identity-card-marker.test.mjs +0 -307
- package/test/identity-router/run.mjs +0 -721
- package/test/integration-pre-step.test.mjs +0 -553
- package/test/live.test.mjs +0 -1020
- package/test/maintenance-schedule.test.mjs +0 -538
- package/test/memory.test.mjs +0 -2828
- package/test/metrics.test.mjs +0 -478
- package/test/orchestrator.test.mjs +0 -266
- package/test/p1_probe.mjs +0 -97
- package/test/packaging.test.mjs +0 -182
- package/test/portability.test.mjs +0 -343
- package/test/rc4-regressions.test.mjs +0 -562
- package/test/rc5-regressions.test.mjs +0 -197
- package/test/rc6-chinese.test.mjs +0 -394
- package/test/router-1100/apply-off.test.mjs +0 -175
- package/test/router-1100/budget-reconcile.test.mjs +0 -215
- package/test/router-1100/config-context.test.mjs +0 -211
- package/test/router-1100/helpers.mjs +0 -129
- package/test/router-1100/hint-envelope.test.mjs +0 -150
- package/test/router-1100/lock-crossproc.test.mjs +0 -206
- package/test/router-1100/proposal-cas-crossproc.test.mjs +0 -184
- package/test/router-1100/router-apply.test.mjs +0 -372
- package/test/router-1100/router-fixb.test.mjs +0 -602
- package/test/router-1100/router-proposals.test.mjs +0 -405
- package/test/router-1100/router-queue-lease-wal.test.mjs +0 -355
- package/test/router-1100/router-security-cr.test.mjs +0 -333
- package/test/router-1100/service-lifecycle.test.mjs +0 -220
- package/test/router-1100/tools-contract.test.mjs +0 -164
- package/test/router-1100/turn-reflect-tick.test.mjs +0 -89
- package/test/router-1100/v6-contract.test.mjs +0 -158
- package/test/router-1100/v6b-contract.test.mjs +0 -293
- package/test/router-1100/v6c-contract.test.mjs +0 -249
- package/test/router-1100/wal-rotation.test.mjs +0 -375
- package/test/router-1100/wp1-config.test.mjs +0 -170
- package/test/router-1100/wp1-default-off.test.mjs +0 -77
- package/test/router-1100/wp1-model-status.test.mjs +0 -90
- package/test/router-1100/wp1-status.test.mjs +0 -220
- package/test/router-1100/wp1-tools.test.mjs +0 -146
- package/test/router-1100/wp5-cli.test.mjs +0 -240
- package/test/router-1100/wp5-model-auto.test.mjs +0 -418
- package/test/router-1100/wp6-merge.test.mjs +0 -386
- package/test/router-1100/wp6-wake.test.mjs +0 -502
- package/test/router-background/audit.test.mjs +0 -37
- package/test/router-background/budget-ledger.test.mjs +0 -330
- package/test/router-background/cross-process.test.mjs +0 -222
- package/test/router-background/fixtures/cross-child.mjs +0 -85
- package/test/router-background/fixtures/real-shapes.mjs +0 -239
- package/test/router-background/router.test.mjs +0 -1044
- package/test/router-background/stream-retry.test.mjs +0 -358
- package/test/router-background/tool-agent.test.mjs +0 -384
- package/test/router-face/bridge.test.mjs +0 -169
- package/test/router-face/face.test.mjs +0 -316
- package/test/router-failclosed.test.mjs +0 -466
- package/test/router-identity.test.mjs +0 -384
- package/test/router-l1-l2.contract.test.mjs +0 -1075
- package/test/router-memory-tools.test.mjs +0 -262
- package/test/router-policy.test.mjs +0 -225
- package/test/router-runtime.test.mjs +0 -438
- package/test/router-shadow.test.mjs +0 -287
- package/test/router.test.mjs +0 -903
- package/test/run-rc.mjs +0 -299
- package/test/run.mjs +0 -2469
- package/test/safety-defaults.test.mjs +0 -179
- package/test/seeded-load.test.mjs +0 -436
- package/test/session-original.test.mjs +0 -338
- package/test/surgery-recovery.test.mjs +0 -936
- package/test/t7_drive.mjs +0 -151
- package/test/tool-output.test.mjs +0 -393
- package/test/tool-schema.test.mjs +0 -177
- package/test/tools_contract.test.mjs +0 -716
- package/test/truncate-visible.test.mjs +0 -1150
- package/test/ui-flags.test.mjs +0 -477
- package/test/v4-hardening.test.mjs +0 -427
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
// src/memory/governance.js — 1.8.0 / v2.8 §15.1-A1:Claim / Evidence / Bank + 授权矩阵
|
|
2
|
+
//
|
|
3
|
+
// 三个实体(写死,后续 A2/A3/A4 在其上扩展):
|
|
4
|
+
// Claim {id, bankId, subject:{entity,attribute}, content, kind, salience, sourceTier,
|
|
5
|
+
// trustScore, evidence:{version, items[]}, conflict, conflict_with[], state}
|
|
6
|
+
// Evidence {id, claimId, anchorId, sha256, excerptRef, verifier, verifiedAt, version,
|
|
7
|
+
// supersedes, sourceDoc}
|
|
8
|
+
// Bank {bankId, scope:user|project|org, owner, readers[], writers[], auditors[]}
|
|
9
|
+
//
|
|
10
|
+
// 授权矩阵(§15.1,写死):
|
|
11
|
+
// user read {user,project,org} write {user,project} audit {user,project}
|
|
12
|
+
// host read all write {project,org} audit all
|
|
13
|
+
// agent read {user} write {user} 需人判 (requiresHumanReview)
|
|
14
|
+
// plugin read {user} write {user} 需人判
|
|
15
|
+
// 越权 → E_BANK_FORBIDDEN + `.longlive/audit/bank.jsonl` 一行 denied(不静默)。
|
|
16
|
+
//
|
|
17
|
+
// 文件:
|
|
18
|
+
// <memoryRoot>/.longlive/claims.jsonl Claim 只追加(版本由 evidenceVersion 单调)
|
|
19
|
+
// <memoryRoot>/.longlive/banks.json 自定义 bank 注册表(可缺省,内置三个默认 bank)
|
|
20
|
+
// <memoryRoot>/.longlive/audit/bank.jsonl 越权/授权审计
|
|
21
|
+
// 旧 records.jsonl 保持只读兼容(listRecords 仍可读);迁移脚本 migrate-180.mjs 写入 claims.jsonl。
|
|
22
|
+
|
|
23
|
+
import crypto from 'node:crypto';
|
|
24
|
+
|
|
25
|
+
import { appendAuditRecord } from './audit-records.js';
|
|
26
|
+
import { trustScoreAt } from './trust.js';
|
|
27
|
+
import fs from 'node:fs';
|
|
28
|
+
import path from 'node:path';
|
|
29
|
+
|
|
30
|
+
export const CLAIMS_SCHEMA = 'claims/v1';
|
|
31
|
+
export const BANKS_SCHEMA = 'banks/v1';
|
|
32
|
+
export const BANK_SCOPES = Object.freeze(['user', 'project', 'org']);
|
|
33
|
+
export const BANK_ACTIONS = Object.freeze(['read', 'write', 'audit']);
|
|
34
|
+
export const BANK_ROLES = Object.freeze(['user', 'host', 'agent', 'plugin']);
|
|
35
|
+
export const CLAIM_STATES = Object.freeze(['active', 'verified', 'disputed', 'superseded', 'archived', 'unverified']);
|
|
36
|
+
|
|
37
|
+
export const DEFAULT_BANKS = Object.freeze([
|
|
38
|
+
{ bankId: 'bank:user', scope: 'user', owner: 'user', readers: ['user', 'host'], writers: ['user'], auditors: ['user', 'host'] },
|
|
39
|
+
{ bankId: 'bank:project', scope: 'project', owner: 'project', readers: ['user', 'host'], writers: ['user', 'host'], auditors: ['user', 'host'] },
|
|
40
|
+
{ bankId: 'bank:org', scope: 'org', owner: 'org', readers: ['user', 'host'], writers: ['host'], auditors: ['host'] },
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
/** Authorization matrix (role × action × scope) — written down, not policy-engine. */
|
|
44
|
+
export const BANK_MATRIX = Object.freeze({
|
|
45
|
+
user: Object.freeze({
|
|
46
|
+
read: Object.freeze(['user', 'project', 'org']),
|
|
47
|
+
write: Object.freeze(['user', 'project']),
|
|
48
|
+
audit: Object.freeze(['user', 'project']),
|
|
49
|
+
}),
|
|
50
|
+
host: Object.freeze({
|
|
51
|
+
read: Object.freeze(['user', 'project', 'org']),
|
|
52
|
+
write: Object.freeze(['project', 'org']),
|
|
53
|
+
audit: Object.freeze(['user', 'project', 'org']),
|
|
54
|
+
}),
|
|
55
|
+
agent: Object.freeze({
|
|
56
|
+
read: Object.freeze(['user']),
|
|
57
|
+
write: Object.freeze(['user']),
|
|
58
|
+
audit: Object.freeze([]),
|
|
59
|
+
}),
|
|
60
|
+
plugin: Object.freeze({
|
|
61
|
+
read: Object.freeze(['user']),
|
|
62
|
+
write: Object.freeze(['user']),
|
|
63
|
+
audit: Object.freeze([]),
|
|
64
|
+
}),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
export class GovernanceError extends Error {
|
|
68
|
+
constructor(code, message, detail = undefined, status = 403) {
|
|
69
|
+
super(message);
|
|
70
|
+
this.name = 'GovernanceError';
|
|
71
|
+
this.code = code;
|
|
72
|
+
this.status = status;
|
|
73
|
+
if (detail !== undefined) this.detail = detail;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function claimsPathOf(memoryRoot) {
|
|
78
|
+
return path.join(path.resolve(String(memoryRoot)), '.longlive', 'claims.jsonl');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function banksPathOf(memoryRoot) {
|
|
82
|
+
return path.join(path.resolve(String(memoryRoot)), '.longlive', 'banks.json');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function bankAuditPathOf(memoryRoot) {
|
|
86
|
+
return path.join(path.resolve(String(memoryRoot)), '.longlive', 'audit', 'bank.jsonl');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function sha256Hex(text) {
|
|
90
|
+
return crypto.createHash('sha256').update(text, 'utf8').digest('hex');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function sha1Short(text, length = 12) {
|
|
94
|
+
return crypto.createHash('sha1').update(text, 'utf8').digest('hex').slice(0, length);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function nowIso(now) {
|
|
98
|
+
const value = typeof now === 'function' ? now() : new Date();
|
|
99
|
+
return (value instanceof Date ? value : new Date(value)).toISOString();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Pure authorization decision.
|
|
104
|
+
* @returns {{allowed:boolean, requiresHumanReview:boolean, role:string, action:string, scope:string, code:string}}
|
|
105
|
+
*/
|
|
106
|
+
export function canBankAccess({ role, action, scope }) {
|
|
107
|
+
const roleKey = BANK_ROLES.includes(role) ? role : null;
|
|
108
|
+
const actionKey = BANK_ACTIONS.includes(action) ? action : null;
|
|
109
|
+
const scopeKey = BANK_SCOPES.includes(scope) ? scope : null;
|
|
110
|
+
if (roleKey === null) return { allowed: false, requiresHumanReview: false, role, action, scope, code: 'E_BANK_ROLE' };
|
|
111
|
+
if (actionKey === null) return { allowed: false, requiresHumanReview: false, role, action, scope, code: 'E_BANK_ACTION' };
|
|
112
|
+
if (scopeKey === null) return { allowed: false, requiresHumanReview: false, role, action, scope, code: 'E_BANK_SCOPE' };
|
|
113
|
+
const allowed = BANK_MATRIX[roleKey][actionKey].includes(scopeKey);
|
|
114
|
+
const requiresHumanReview = allowed && roleKey !== 'user' && roleKey !== 'host' && actionKey === 'write';
|
|
115
|
+
return { allowed, requiresHumanReview, role: roleKey, action: actionKey, scope: scopeKey, code: allowed ? 'OK' : 'E_BANK_FORBIDDEN' };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function appendBankAudit(memoryRoot, line) {
|
|
119
|
+
try {
|
|
120
|
+
fs.mkdirSync(path.dirname(bankAuditPathOf(memoryRoot)), { recursive: true });
|
|
121
|
+
fs.appendFileSync(bankAuditPathOf(memoryRoot), `${JSON.stringify(line)}\n`, 'utf8');
|
|
122
|
+
} catch {
|
|
123
|
+
/* audit is best effort; the decision itself already failed closed */
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Assert access; denied access is audited with denied:true then throws E_BANK_FORBIDDEN. */
|
|
128
|
+
export function assertBankAccess({ memoryRoot, role, action, scope, actor = null, reason = '', now = () => new Date() } = {}) {
|
|
129
|
+
const decision = canBankAccess({ role, action, scope });
|
|
130
|
+
if (!decision.allowed) {
|
|
131
|
+
appendBankAudit(memoryRoot, {
|
|
132
|
+
schema: 'bank-audit/v1',
|
|
133
|
+
at: nowIso(now),
|
|
134
|
+
role: decision.role ?? role,
|
|
135
|
+
action: decision.action ?? action,
|
|
136
|
+
scope: decision.scope ?? scope,
|
|
137
|
+
actor,
|
|
138
|
+
allowed: false,
|
|
139
|
+
denied: true,
|
|
140
|
+
code: decision.code,
|
|
141
|
+
reason,
|
|
142
|
+
});
|
|
143
|
+
throw new GovernanceError('E_BANK_FORBIDDEN', `bank 越权:role=${role} action=${action} scope=${scope}`, decision);
|
|
144
|
+
}
|
|
145
|
+
return decision;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Register/read custom banks; built-in three always available. */
|
|
149
|
+
export function listBanks({ memoryRoot }) {
|
|
150
|
+
const builtIn = DEFAULT_BANKS.map((bank) => ({ ...bank, builtin: true }));
|
|
151
|
+
let custom = [];
|
|
152
|
+
try {
|
|
153
|
+
const parsed = JSON.parse(fs.readFileSync(banksPathOf(memoryRoot), 'utf8'));
|
|
154
|
+
if (Array.isArray(parsed?.banks)) custom = parsed.banks;
|
|
155
|
+
} catch {
|
|
156
|
+
custom = [];
|
|
157
|
+
}
|
|
158
|
+
return [...builtIn, ...custom];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function registerBank({ memoryRoot, bank, now = () => new Date() }) {
|
|
162
|
+
const value = bank && typeof bank === 'object' ? bank : null;
|
|
163
|
+
if (!value || typeof value.bankId !== 'string' || value.bankId === '') {
|
|
164
|
+
throw new GovernanceError('E_BANK_ID', 'bank.bankId required', undefined, 400);
|
|
165
|
+
}
|
|
166
|
+
if (!BANK_SCOPES.includes(value.scope)) {
|
|
167
|
+
throw new GovernanceError('E_BANK_SCOPE', `bank.scope 非法:${String(value.scope)}`, undefined, 400);
|
|
168
|
+
}
|
|
169
|
+
const banks = listBanks({ memoryRoot }).filter((entry) => entry.builtin !== true);
|
|
170
|
+
const next = banks.filter((entry) => entry.bankId !== value.bankId);
|
|
171
|
+
next.push({
|
|
172
|
+
bankId: value.bankId,
|
|
173
|
+
scope: value.scope,
|
|
174
|
+
owner: value.owner ?? null,
|
|
175
|
+
readers: Array.isArray(value.readers) ? value.readers : ['host'],
|
|
176
|
+
writers: Array.isArray(value.writers) ? value.writers : ['host'],
|
|
177
|
+
auditors: Array.isArray(value.auditors) ? value.auditors : ['host'],
|
|
178
|
+
createdAt: value.createdAt ?? nowIso(now),
|
|
179
|
+
});
|
|
180
|
+
const file = banksPathOf(memoryRoot);
|
|
181
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
182
|
+
const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
|
|
183
|
+
fs.writeFileSync(tmp, `${JSON.stringify({ schema: BANKS_SCHEMA, banks: next }, null, 2)}\n`, 'utf8');
|
|
184
|
+
fs.renameSync(tmp, file);
|
|
185
|
+
return { ok: true, banks: next, file };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Claim/Evidence entity normalization (backward compatible with v2.6.4 records). */
|
|
189
|
+
export function normalizeEvidence(entry, claimId = null) {
|
|
190
|
+
const value = entry && typeof entry === 'object' ? entry : {};
|
|
191
|
+
return {
|
|
192
|
+
id: typeof value.id === 'string' ? value.id : `ev_${sha1Short(`${claimId ?? ''}|${value.anchorId ?? ''}|${value.sha256 ?? ''}`)}`,
|
|
193
|
+
claimId: typeof value.claimId === 'string' ? value.claimId : claimId,
|
|
194
|
+
anchorId: typeof value.anchorId === 'string' ? value.anchorId : null,
|
|
195
|
+
sha256: typeof value.sha256 === 'string' ? value.sha256 : null,
|
|
196
|
+
excerptRef: typeof value.excerptRef === 'string' ? value.excerptRef : null,
|
|
197
|
+
verifier: typeof value.verifier === 'string' ? value.verifier : null,
|
|
198
|
+
verifiedAt: value.verifiedAt ?? null,
|
|
199
|
+
version: Number.isInteger(value.version) ? value.version : 1,
|
|
200
|
+
supersedes: typeof value.supersedes === 'string' ? value.supersedes : null,
|
|
201
|
+
sourceDoc: typeof value.sourceDoc === 'string' ? value.sourceDoc : null,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function normalizeSubject(subject, fallbackId = null) {
|
|
206
|
+
if (subject && typeof subject === 'object' && typeof subject.entity === 'string') {
|
|
207
|
+
return {
|
|
208
|
+
entity: subject.entity.trim().toLowerCase(),
|
|
209
|
+
attribute: typeof subject.attribute === 'string' && subject.attribute.trim() !== '' ? subject.attribute.trim().toLowerCase() : null,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
const raw = typeof subject === 'string' && subject.trim() !== '' ? subject.trim().toLowerCase() : fallbackId;
|
|
213
|
+
return { entity: raw ?? 'unknown', attribute: null };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function normalizeClaim(row) {
|
|
217
|
+
const value = row && typeof row === 'object' ? row : {};
|
|
218
|
+
const claimId = typeof value.id === 'string' && value.id !== '' ? value.id : `clm_${sha1Short(JSON.stringify(value))}`;
|
|
219
|
+
const evidenceItems = Array.isArray(value.evidence)
|
|
220
|
+
? value.evidence.map((entry) => normalizeEvidence(entry, claimId))
|
|
221
|
+
: (Array.isArray(value.evidence?.items) ? value.evidence.items.map((entry) => normalizeEvidence(entry, claimId)) : []);
|
|
222
|
+
const evidenceVersion = Number.isInteger(value.evidenceVersion)
|
|
223
|
+
? value.evidenceVersion
|
|
224
|
+
: (Number.isInteger(value.evidence?.version) ? value.evidence.version : evidenceItems.length);
|
|
225
|
+
return {
|
|
226
|
+
schema: CLAIMS_SCHEMA,
|
|
227
|
+
id: claimId,
|
|
228
|
+
bankId: typeof value.bankId === 'string' && value.bankId !== '' ? value.bankId : 'bank:project',
|
|
229
|
+
subject: normalizeSubject(value.subject, claimId),
|
|
230
|
+
content: typeof value.content === 'string' ? value.content : '',
|
|
231
|
+
kind: typeof value.kind === 'string' ? value.kind : 'fact',
|
|
232
|
+
salience: Number.isInteger(value.salience) ? value.salience : 3,
|
|
233
|
+
sourceTier: ['user', 'host', 'agent', 'plugin'].includes(value.sourceTier) ? value.sourceTier : 'host',
|
|
234
|
+
trustScore: typeof value.trustScore === 'number' ? value.trustScore : null,
|
|
235
|
+
verifyCount: Number.isInteger(value.verifyCount) ? value.verifyCount : 0,
|
|
236
|
+
claimVersion: Number.isInteger(value.claimVersion) ? value.claimVersion : 1,
|
|
237
|
+
supersedes: Array.isArray(value.supersedes) ? value.supersedes.map(Number).filter(Number.isInteger) : [],
|
|
238
|
+
supersededBy: Number.isInteger(value.supersededBy) ? value.supersededBy : null,
|
|
239
|
+
evidence: { version: evidenceVersion, items: evidenceItems },
|
|
240
|
+
conflict: value.conflict === true,
|
|
241
|
+
conflict_with: Array.isArray(value.conflict_with) ? value.conflict_with : [],
|
|
242
|
+
state: CLAIM_STATES.includes(value.state) ? value.state : (['active', 'superseded', 'forgotten', 'archived', 'restored'].includes(value.status) ? value.status : 'active'),
|
|
243
|
+
project: value.project ?? null,
|
|
244
|
+
anchors: Array.isArray(value.anchors) ? value.anchors : [],
|
|
245
|
+
review: value.review && typeof value.review === 'object' ? value.review : null,
|
|
246
|
+
createdAt: value.createdAt ?? null,
|
|
247
|
+
updatedAt: value.updatedAt ?? null,
|
|
248
|
+
audit: value.audit && typeof value.audit === 'object' ? value.audit : {},
|
|
249
|
+
legacy: value.legacy === true,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function readClaimsForHost({ memoryRoot }) {
|
|
254
|
+
const file = claimsPathOf(memoryRoot);
|
|
255
|
+
const items = [];
|
|
256
|
+
const bad = [];
|
|
257
|
+
let present = false;
|
|
258
|
+
let text = '';
|
|
259
|
+
try {
|
|
260
|
+
text = fs.readFileSync(file, 'utf8');
|
|
261
|
+
present = true;
|
|
262
|
+
} catch {
|
|
263
|
+
text = '';
|
|
264
|
+
}
|
|
265
|
+
for (const line of text.split('\n')) {
|
|
266
|
+
if (line.trim() === '') continue;
|
|
267
|
+
try {
|
|
268
|
+
items.push(normalizeClaim(JSON.parse(line)));
|
|
269
|
+
} catch {
|
|
270
|
+
bad.push(line.slice(0, 120));
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
// GOV-05: append-only rows keep every version; the read view marks older versions.
|
|
274
|
+
const maxById = new Map();
|
|
275
|
+
for (const item of items) {
|
|
276
|
+
const version = Number.isInteger(item.claimVersion) ? item.claimVersion : 1;
|
|
277
|
+
if (!maxById.has(item.id) || version > maxById.get(item.id)) maxById.set(item.id, version);
|
|
278
|
+
}
|
|
279
|
+
const marked = items.map((item) => {
|
|
280
|
+
const max = maxById.get(item.id) ?? 1;
|
|
281
|
+
const version = Number.isInteger(item.claimVersion) ? item.claimVersion : 1;
|
|
282
|
+
if (version >= max) return item;
|
|
283
|
+
return { ...item, state: ['forgotten', 'archived'].includes(item.state) ? item.state : 'superseded', supersededBy: max };
|
|
284
|
+
});
|
|
285
|
+
return { schema: CLAIMS_SCHEMA, present, items: marked, badLines: bad, file };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* GOV-02 read axis (A5): Claim reads require a host-injected actor AND are
|
|
290
|
+
* filtered by the bank authorization matrix for that actor's role. An actor
|
|
291
|
+
* that can read none of the present claims is refused with E_BANK_FORBIDDEN.
|
|
292
|
+
*/
|
|
293
|
+
export function readClaims({ memoryRoot, actor = null, now = () => new Date() }) {
|
|
294
|
+
if (!actor || typeof actor.kind !== 'string' || actor.kind === '') {
|
|
295
|
+
throw new GovernanceError('E_ACTOR_REQUIRED', '读 Claim 需要宿主 actor(禁止匿名读)。', undefined, 401);
|
|
296
|
+
}
|
|
297
|
+
const role = actor.kind;
|
|
298
|
+
if (!BANK_ROLES.includes(role)) {
|
|
299
|
+
throw new GovernanceError('E_BANK_FORBIDDEN', `未知 actor 角色:${role}`, undefined, 403);
|
|
300
|
+
}
|
|
301
|
+
const result = readClaimsForHost({ memoryRoot });
|
|
302
|
+
const scopeOf = new Map(listBanks({ memoryRoot }).map((bank) => [bank.bankId, bank.scope]));
|
|
303
|
+
const readable = [];
|
|
304
|
+
const denied = [];
|
|
305
|
+
for (const claim of result.items) {
|
|
306
|
+
const scope = scopeOf.get(claim.bankId) ?? null;
|
|
307
|
+
const decision = canBankAccess({ role, action: 'read', scope });
|
|
308
|
+
if (decision.allowed) readable.push(claim);
|
|
309
|
+
else denied.push({ id: claim.id, bankId: claim.bankId ?? null });
|
|
310
|
+
}
|
|
311
|
+
if (result.items.length > 0 && readable.length === 0) {
|
|
312
|
+
appendBankAudit(memoryRoot, { schema: 'bank-audit/v1', at: nowIso(now), role, action: 'read', scope: null, allowed: false, denied: true, code: 'E_BANK_FORBIDDEN', deniedCount: denied.length, reason: 'read-axis' });
|
|
313
|
+
auditRow(memoryRoot, { action: 'denied', recordId: denied[0]?.id ?? null, readerTier: role, now, extra: { code: 'E_BANK_FORBIDDEN', readAxis: true } });
|
|
314
|
+
throw new GovernanceError('E_BANK_FORBIDDEN', `读轴越权:role=${role} 可读 0/${result.items.length} 条(bank/scope 授权矩阵拒绝)。`, { role, deniedCount: denied.length }, 403);
|
|
315
|
+
}
|
|
316
|
+
// A5: read auditing for answer/recall 生产路径 is written there (not on every
|
|
317
|
+
// internal readClaims call — that would turn panel reads into audit noise).
|
|
318
|
+
return { ...result, items: readable, total: result.items.length, deniedCount: denied.length };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** Append a claim (idempotent by id: same id+content returns existing). */
|
|
322
|
+
export function appendClaim({ memoryRoot, claim, now = () => new Date() }) {
|
|
323
|
+
const normalized = normalizeClaim({ ...claim, createdAt: claim?.createdAt ?? nowIso(now), updatedAt: nowIso(now) });
|
|
324
|
+
const existing = readClaimsForHost({ memoryRoot }).items.find((item) => item.id === normalized.id);
|
|
325
|
+
let next = normalized;
|
|
326
|
+
if (existing) {
|
|
327
|
+
const sameContent = existing.content === normalized.content
|
|
328
|
+
&& JSON.stringify(existing.evidence?.items ?? []) === JSON.stringify(normalized.evidence?.items ?? []);
|
|
329
|
+
if (sameContent) {
|
|
330
|
+
return { ok: true, appended: false, claim: existing, file: claimsPathOf(memoryRoot) };
|
|
331
|
+
}
|
|
332
|
+
// GOV-05: append-only version chain — v1 stays on disk, new row is claimVersion+1.
|
|
333
|
+
const prevVersion = Number.isInteger(existing.claimVersion) ? existing.claimVersion : 1;
|
|
334
|
+
next = normalizeClaim({
|
|
335
|
+
...normalized,
|
|
336
|
+
claimVersion: prevVersion + 1,
|
|
337
|
+
supersedes: [...new Set([...(existing.supersedes ?? []), prevVersion])],
|
|
338
|
+
createdAt: existing.createdAt ?? normalized.createdAt,
|
|
339
|
+
updatedAt: nowIso(now),
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
const file = claimsPathOf(memoryRoot);
|
|
343
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
344
|
+
fs.appendFileSync(file, `${JSON.stringify(next)}\n`, 'utf8');
|
|
345
|
+
return { ok: true, appended: true, claim: next, file };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* GOV-05 claim.verify — host-internal writer (NOT a panel action).
|
|
350
|
+
* CAS on expectedVersion (== current max claimVersion); increments verifyCount,
|
|
351
|
+
* recomputes trustScore server-side, appends a new version (append-only).
|
|
352
|
+
*/
|
|
353
|
+
export function claimVerify({ memoryRoot, claimId, expectedVersion, verifier = 'host', actor = null, now = () => new Date() } = {}) {
|
|
354
|
+
const role = actor?.kind ?? null;
|
|
355
|
+
if (role === null || role === '') throw new GovernanceError('E_ACTOR_REQUIRED', 'claim.verify 需要宿主 actor。', undefined, 401);
|
|
356
|
+
if (role !== 'host') throw new GovernanceError('E_BANK_FORBIDDEN', 'claim.verify 只允许 host 内部写入器(不经面板动作面)。', { role }, 403);
|
|
357
|
+
if (!Number.isInteger(expectedVersion) || expectedVersion < 1) {
|
|
358
|
+
throw new GovernanceError('E_EXPECTED_VERSION_REQUIRED', 'claim.verify 需要 expectedVersion(正整数 CAS)。', undefined, 400);
|
|
359
|
+
}
|
|
360
|
+
const id = String(claimId ?? '');
|
|
361
|
+
if (id === '') throw new GovernanceError('E_CLAIM_ID', 'claim.verify 需要 claimId。', undefined, 400);
|
|
362
|
+
const all = readClaimsForHost({ memoryRoot }).items.filter((item) => item.id === id);
|
|
363
|
+
if (all.length === 0) throw new GovernanceError('E_CLAIM_NOT_FOUND', `claim 不存在:${id}`, undefined, 404);
|
|
364
|
+
const latest = all.reduce((best, item) => ((item.claimVersion ?? 1) > (best.claimVersion ?? 1) ? item : best), all[0]);
|
|
365
|
+
const currentVersion = Number.isInteger(latest.claimVersion) ? latest.claimVersion : 1;
|
|
366
|
+
if (currentVersion !== expectedVersion) {
|
|
367
|
+
throw new GovernanceError('E_CAS_CONFLICT', `claim 版本冲突:expected=${expectedVersion} current=${currentVersion}`, { expectedVersion, currentVersion }, 409);
|
|
368
|
+
}
|
|
369
|
+
const verifyCount = (Number.isInteger(latest.verifyCount) ? latest.verifyCount : 0) + 1;
|
|
370
|
+
const score = trustScoreAt({ sourceTier: latest.sourceTier, verifyCount, createdAt: latest.createdAt, now });
|
|
371
|
+
const next = normalizeClaim({
|
|
372
|
+
...latest,
|
|
373
|
+
claimVersion: currentVersion + 1,
|
|
374
|
+
verifyCount,
|
|
375
|
+
trustScore: score.trustScore,
|
|
376
|
+
supersedes: [...new Set([...(latest.supersedes ?? []), currentVersion])],
|
|
377
|
+
updatedAt: nowIso(now),
|
|
378
|
+
audit: { ...(latest.audit ?? {}), lastVerifiedBy: String(verifier), lastVerifiedAt: nowIso(now) },
|
|
379
|
+
});
|
|
380
|
+
const file = claimsPathOf(memoryRoot);
|
|
381
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
382
|
+
fs.appendFileSync(file, `${JSON.stringify(next)}\n`, 'utf8');
|
|
383
|
+
auditRow(memoryRoot, { action: 'verify', recordId: id, readerTier: role, now, extra: { claimVersion: next.claimVersion, verifyCount, verifier: String(verifier) } });
|
|
384
|
+
return { ok: true, claim: next, claimVersion: next.claimVersion, verifyCount, trustScore: next.trustScore, file };
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** Queue a claim awaiting human review (EA-09: never land it as active first). */
|
|
388
|
+
export function enqueueReview({ memoryRoot, item, now = () => new Date() }) {
|
|
389
|
+
const file = path.join(path.resolve(String(memoryRoot)), '.longlive', 'review', 'queue.jsonl');
|
|
390
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
391
|
+
const line = JSON.stringify({ schema: 'review-queue/v1', state: 'pending', ...item, createdAt: nowIso(now) });
|
|
392
|
+
fs.appendFileSync(file, `${line}\n`, 'utf8');
|
|
393
|
+
return { file, item: JSON.parse(line) };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** Reject client-supplied trust/state inputs (GOV-03 P0). */
|
|
397
|
+
export function assertNoTrustInput(claim) {
|
|
398
|
+
const value = claim && typeof claim === 'object' ? claim : {};
|
|
399
|
+
const forbidden = ['sourceTier', 'trustScore', 'verifyCount', 'claimVersion'].filter((key) => Object.prototype.hasOwnProperty.call(value, key));
|
|
400
|
+
if (forwardState(value.state)) forbidden.push('state');
|
|
401
|
+
if (forbidden.length > 0) {
|
|
402
|
+
throw new GovernanceError('E_TRUST_INPUT_FORBIDDEN', `trust/state 字段只能由服务端计算,禁止 payload 直传:${forbidden.join(',')}`, { forbidden }, 400);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function forwardState(state) {
|
|
407
|
+
return state !== undefined && state !== null && !['active', 'disputed'].includes(state);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function bankScopeOf(memoryRoot, bankId) {
|
|
411
|
+
const bank = listBanks({ memoryRoot }).find((entry) => entry.bankId === bankId);
|
|
412
|
+
return bank ? bank.scope : null;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function auditRow(memoryRoot, { action, recordId, readerTier, extra, now }) {
|
|
416
|
+
try {
|
|
417
|
+
appendAuditRecord({
|
|
418
|
+
memoryRoot,
|
|
419
|
+
action,
|
|
420
|
+
recordId: recordId ?? null,
|
|
421
|
+
readerTier: readerTier ?? null,
|
|
422
|
+
questionId: 'unknown',
|
|
423
|
+
expectedTurns: null,
|
|
424
|
+
answerText: null,
|
|
425
|
+
now,
|
|
426
|
+
extra,
|
|
427
|
+
});
|
|
428
|
+
} catch {
|
|
429
|
+
/* audit must not be silently lost: callers still fail closed on the primary path */
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Write path (GOV-01/03): role + scope are SERVER-derived from the host actor
|
|
435
|
+
* (token-bound); bank scope must agree with the requested scope; trust fields
|
|
436
|
+
* are computed by the server and client-supplied values are rejected.
|
|
437
|
+
*/
|
|
438
|
+
export function writeClaim({ memoryRoot, claim, role: claimedRole = null, scope: claimedScope = null, actor = null, now = () => new Date() }) {
|
|
439
|
+
// 1) actor is host-injected; missing actor never falls back to a role.
|
|
440
|
+
if (!actor || typeof actor.kind !== 'string' || actor.kind === '') {
|
|
441
|
+
auditRow(memoryRoot, { action: 'denied', recordId: claim?.id, readerTier: 'unknown', now, extra: { code: 'E_ACTOR_REQUIRED' } });
|
|
442
|
+
throw new GovernanceError('E_ACTOR_REQUIRED', 'actor 必须由宿主注入(令牌绑定),缺省拒绝。', undefined, 401);
|
|
443
|
+
}
|
|
444
|
+
const role = actor.kind;
|
|
445
|
+
// Caller-declared role/scope are NEVER trusted: any mismatch with the token actor
|
|
446
|
+
// or the bank's scope is a forgery attempt (GOV-01).
|
|
447
|
+
if (claimedRole !== null && claimedRole !== role) {
|
|
448
|
+
auditRow(memoryRoot, { action: 'denied', recordId: claim?.id, readerTier: role, now, extra: { code: 'E_BANK_FORBIDDEN', claimedRole } });
|
|
449
|
+
throw new GovernanceError('E_BANK_FORBIDDEN', `role 伪造:payload=${claimedRole} actor=${role}`, { claimedRole, role }, 403);
|
|
450
|
+
}
|
|
451
|
+
if (!BANK_ROLES.includes(role)) {
|
|
452
|
+
auditRow(memoryRoot, { action: 'denied', recordId: claim?.id, readerTier: null, now, extra: { code: 'E_BANK_ROLE' } });
|
|
453
|
+
throw new GovernanceError('E_BANK_ROLE', `未知 actor 角色:${role}`, undefined, 403);
|
|
454
|
+
}
|
|
455
|
+
const claimId = claim?.id ?? null;
|
|
456
|
+
// 2) trust/state inputs must never come from the payload (denied attempt is audited).
|
|
457
|
+
try {
|
|
458
|
+
assertNoTrustInput(claim);
|
|
459
|
+
} catch (error) {
|
|
460
|
+
auditRow(memoryRoot, { action: 'denied', recordId: claim?.id ?? null, readerTier: role, now, extra: { code: error.code ?? 'E_TRUST_INPUT_FORBIDDEN' } });
|
|
461
|
+
throw error;
|
|
462
|
+
}
|
|
463
|
+
// 3) bank/scope are server-derived: claim.bankId decides scope, caller cannot override.
|
|
464
|
+
const bankId = typeof claim?.bankId === 'string' ? claim.bankId : 'bank:project';
|
|
465
|
+
const scope = bankScopeOf(memoryRoot, bankId);
|
|
466
|
+
if (scope === null) {
|
|
467
|
+
auditRow(memoryRoot, { action: 'denied', recordId: claimId, readerTier: role, now, extra: { code: 'E_BANK_FORBIDDEN', bankId } });
|
|
468
|
+
throw new GovernanceError('E_BANK_FORBIDDEN', `未知 bank:${bankId}`, { bankId }, 403);
|
|
469
|
+
}
|
|
470
|
+
if (claimedScope !== null && claimedScope !== scope) {
|
|
471
|
+
auditRow(memoryRoot, { action: 'denied', recordId: claimId, readerTier: role, now, extra: { code: 'E_BANK_FORBIDDEN', claimedScope, bankId } });
|
|
472
|
+
throw new GovernanceError('E_BANK_FORBIDDEN', `scope 伪造:payload=${claimedScope} bank=${bankId} scope=${scope}`, { claimedScope, bankId, scope }, 403);
|
|
473
|
+
}
|
|
474
|
+
const decision = canBankAccess({ role, action: 'write', scope });
|
|
475
|
+
if (!decision.allowed) {
|
|
476
|
+
appendBankAuditFor(memoryRoot, { role, scope, claimId, now });
|
|
477
|
+
auditRow(memoryRoot, { action: 'denied', recordId: claimId, readerTier: role, now, extra: { code: 'E_BANK_FORBIDDEN', bankId, scope } });
|
|
478
|
+
throw new GovernanceError('E_BANK_FORBIDDEN', `bank 越权:role=${role} bank=${bankId} scope=${scope}`, decision, 403);
|
|
479
|
+
}
|
|
480
|
+
// 4) server computes trust/state/sourceTier (client values already rejected).
|
|
481
|
+
const score = trustScoreAt({ sourceTier: role, verifyCount: 0, createdAt: null, now });
|
|
482
|
+
const normalized = normalizeClaim({
|
|
483
|
+
...claim,
|
|
484
|
+
bankId,
|
|
485
|
+
sourceTier: role,
|
|
486
|
+
trustScore: score.trustScore,
|
|
487
|
+
verifyCount: 0,
|
|
488
|
+
claimVersion: 1,
|
|
489
|
+
state: 'active',
|
|
490
|
+
});
|
|
491
|
+
if (decision.requiresHumanReview) {
|
|
492
|
+
const queued = enqueueReview({ memoryRoot, now, item: { kind: 'claim', target: { kind: 'claim', id: normalized.id }, claim: normalized, reason: 'E_HUMAN_REVIEW_REQUIRED', requestedBy: role } });
|
|
493
|
+
auditRow(memoryRoot, { action: 'review', recordId: normalized.id, readerTier: role, now, extra: { code: 'E_HUMAN_REVIEW_REQUIRED' } });
|
|
494
|
+
return { ok: true, appended: false, queued: true, requiresHumanReview: true, code: 'E_HUMAN_REVIEW_REQUIRED', review: queued.item };
|
|
495
|
+
}
|
|
496
|
+
const result = appendClaim({ memoryRoot, claim: normalized, now });
|
|
497
|
+
auditRow(memoryRoot, { action: 'write', recordId: normalized.id, readerTier: role, now, extra: { bankId, scope, trustScore: normalized.trustScore } });
|
|
498
|
+
return { ...result, requiresHumanReview: false };
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function appendBankAuditFor(memoryRoot, { role, scope, claimId, now }) {
|
|
502
|
+
try {
|
|
503
|
+
const dir = path.join(path.resolve(String(memoryRoot)), '.longlive', 'audit');
|
|
504
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
505
|
+
fs.appendFileSync(path.join(dir, 'bank.jsonl'), `${JSON.stringify({ schema: 'bank-audit/v1', at: nowIso(now), role, action: 'write', scope, allowed: false, denied: true, code: 'E_BANK_FORBIDDEN', recordId: claimId })}\n`, 'utf8');
|
|
506
|
+
} catch {
|
|
507
|
+
/* best effort */
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/** Map a runtime record (records.jsonl writer) to a Claim for the runtime consumer path. */
|
|
512
|
+
/** A5: server-derived producer tier. Incoming record.sourceTier is NEVER trusted. */
|
|
513
|
+
export function serverTierOfRecord(record) {
|
|
514
|
+
const value = record && typeof record === 'object' ? record : {};
|
|
515
|
+
const createdBy = String(value?.audit?.createdBy ?? '');
|
|
516
|
+
if (createdBy === 'memory-note') return 'user';
|
|
517
|
+
if (createdBy === 'activity-capture') return 'agent';
|
|
518
|
+
if (createdBy.startsWith('capture')) return 'host';
|
|
519
|
+
// Unknown producer => fail-safe agent (never silently escalate to host/user).
|
|
520
|
+
return 'agent';
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
export function claimFromRecord(record, { now = () => new Date() } = {}) {
|
|
524
|
+
const value = record && typeof record === 'object' ? record : {};
|
|
525
|
+
const id = typeof value.id === 'string' && value.id.startsWith('clm_') ? value.id : `clm_${sha1Short(String(value.id ?? JSON.stringify(value)))}`;
|
|
526
|
+
// A5: sourceTier + trustScore are SERVER-computed. Caller-supplied record.sourceTier/
|
|
527
|
+
// trustScore/verifyCount/state are ignored (only evidence/anchors/content are data).
|
|
528
|
+
const sourceTier = serverTierOfRecord(value);
|
|
529
|
+
const createdAt = value.createdAt ?? nowIso(now);
|
|
530
|
+
const score = trustScoreAt({ sourceTier, verifyCount: 0, createdAt, now });
|
|
531
|
+
const bankId = sourceTier === 'user' || sourceTier === 'agent' || sourceTier === 'plugin' ? 'bank:user' : 'bank:project';
|
|
532
|
+
return normalizeClaim({
|
|
533
|
+
id,
|
|
534
|
+
bankId,
|
|
535
|
+
subject: value.subject ?? { entity: String(value.id ?? id) },
|
|
536
|
+
content: value.content ?? '',
|
|
537
|
+
kind: value.kind ?? 'fact',
|
|
538
|
+
salience: value.salience,
|
|
539
|
+
sourceTier,
|
|
540
|
+
trustScore: score.trustScore,
|
|
541
|
+
verifyCount: 0,
|
|
542
|
+
claimVersion: 1,
|
|
543
|
+
evidence: { version: Array.isArray(value.evidence) ? value.evidence.length : 0, items: Array.isArray(value.evidence) ? value.evidence : [] },
|
|
544
|
+
state: 'active',
|
|
545
|
+
project: value.project ?? null,
|
|
546
|
+
anchors: Array.isArray(value.anchors) ? value.anchors : [],
|
|
547
|
+
createdAt,
|
|
548
|
+
updatedAt: nowIso(now),
|
|
549
|
+
audit: { ...(value.audit ?? {}), bridgedFromRecords: true, serverDerivedTier: true },
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/** Runtime dual-write: every records write also lands a Claim + audit write row. */
|
|
554
|
+
export function appendClaimFromRecord({ memoryRoot, record, now = () => new Date() }) {
|
|
555
|
+
const claim = claimFromRecord(record, { now });
|
|
556
|
+
const result = appendClaim({ memoryRoot, claim, now });
|
|
557
|
+
if (result.appended) {
|
|
558
|
+
auditRow(memoryRoot, { action: 'write', recordId: claim.id, readerTier: claim.sourceTier, now, extra: { kind: 'claim-from-record', bridgeRecordId: record?.id ?? null } });
|
|
559
|
+
}
|
|
560
|
+
return { claim: result.claim ?? claim, appended: result.appended === true };
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/** Migration helper: map one legacy records.jsonl row to a Claim (pure). */
|
|
564
|
+
export function claimFromLegacyRecord(row) {
|
|
565
|
+
const value = row && typeof row === 'object' ? row : {};
|
|
566
|
+
const legacyId = typeof value.id === 'string' ? value.id : `rec:${sha1Short(JSON.stringify(value))}`;
|
|
567
|
+
const claimId = legacyId.startsWith('clm_') ? legacyId : `clm_${sha1Short(legacyId)}`;
|
|
568
|
+
const evidenceItems = Array.isArray(value.evidence) ? value.evidence : [];
|
|
569
|
+
return normalizeClaim({
|
|
570
|
+
id: claimId,
|
|
571
|
+
bankId: typeof value.bankId === 'string' ? value.bankId : 'bank:project',
|
|
572
|
+
subject: value.subject ?? { entity: legacyId, attribute: null },
|
|
573
|
+
content: value.content ?? '',
|
|
574
|
+
kind: value.kind ?? 'fact',
|
|
575
|
+
salience: value.salience,
|
|
576
|
+
// 迁移不因缺字段提权:未知来源按 agent 处理(绝不升成 host)。
|
|
577
|
+
sourceTier: ['user', 'host', 'agent', 'plugin'].includes(value.sourceTier) ? value.sourceTier : 'agent',
|
|
578
|
+
trustScore: value.trustScore,
|
|
579
|
+
evidence: {
|
|
580
|
+
version: evidenceItems.length,
|
|
581
|
+
items: evidenceItems.map((entry) => ({
|
|
582
|
+
...entry,
|
|
583
|
+
verifier: entry.verifier ?? null,
|
|
584
|
+
sourceDoc: entry.sourceDoc ?? 'records.jsonl',
|
|
585
|
+
})),
|
|
586
|
+
},
|
|
587
|
+
conflict: value.conflict === true,
|
|
588
|
+
conflict_with: Array.isArray(value.conflict_with) ? value.conflict_with : [],
|
|
589
|
+
state: value.state ?? value.status ?? 'active',
|
|
590
|
+
project: value.project ?? null,
|
|
591
|
+
anchors: Array.isArray(value.anchors) ? value.anchors : [],
|
|
592
|
+
review: value.review && typeof value.review === 'object' ? value.review : null,
|
|
593
|
+
createdAt: value.createdAt ?? null,
|
|
594
|
+
legacy: true,
|
|
595
|
+
audit: {
|
|
596
|
+
...(value.audit ?? {}),
|
|
597
|
+
migratedFrom: 'records.jsonl',
|
|
598
|
+
migratedAt: nowIso(),
|
|
599
|
+
sourceTierDefaulted: value.sourceTier === undefined,
|
|
600
|
+
},
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
export { sha256Hex };
|