@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,373 @@
|
|
|
1
|
+
// src/role/writeprotect.js — 1.8.0 / v2.8.1 §16.8 A/E:宿主私有写保护 + HMAC 侧车 + 四元边界
|
|
2
|
+
//
|
|
3
|
+
// 设计(§16.8):
|
|
4
|
+
// A. `.longlive/role/**`、`.longlive/working/**` 等数据「模型与子脑不可写」:
|
|
5
|
+
// ① 宿主进程独占写(tmp + rename + fsync);② 每文件 HMAC 侧车
|
|
6
|
+
// `<file>.hmac` = HMAC-SHA256(auditKey, canonical bytes),读时校验,失败
|
|
7
|
+
// → `E_INTEGRITY_TAMPER` 且拒绝加载;③ 模型工具/子脑直写 → `E_WRITE_DENY`。
|
|
8
|
+
// E. 四元边界 sessionId/userId/projectId/readers[]:私密帧拒绝 `E_FRAME_PRIVATE`;
|
|
9
|
+
// role 文件带 subject/readers,跨用户读取被拒(宿主例外)。
|
|
10
|
+
//
|
|
11
|
+
// 密钥:宿主内存 auditKey 优先(与 panel-auth 同源);未注入时从
|
|
12
|
+
// `<memoryRoot>/.longlive/.hostkey` 取(首启生成,0600 权限,宿主私有目录约定)。
|
|
13
|
+
// 该 key 与内容分开存放且不在任何响应/日志里出现;残余风险见 docs/ROLE.md。
|
|
14
|
+
//
|
|
15
|
+
// 纯本地、失败静默:写路径抛结构化错误;读路径由调用方决定降级。
|
|
16
|
+
|
|
17
|
+
import crypto from 'node:crypto';
|
|
18
|
+
import fs from 'node:fs';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
|
|
21
|
+
import { isInsideRoot, memoryError } from '../memory/paths.js';
|
|
22
|
+
import { RoleError, ROLE_CODES, iso } from './base.js';
|
|
23
|
+
|
|
24
|
+
export const WRITEPROTECT_SCHEMA = 'hostwrite/v1';
|
|
25
|
+
export const SIDECAR_SUFFIX = '.hmac';
|
|
26
|
+
export const HOST_KEY_FILE = '.hostkey';
|
|
27
|
+
/** Host-injected audit key wins over the persisted fallback key file. */
|
|
28
|
+
export const AUDIT_KEY_ENV = 'DSH_LONGLIVE_AUDIT_KEY';
|
|
29
|
+
export const WRITE_CODES = Object.freeze({
|
|
30
|
+
WRITE_DENY: 'E_WRITE_DENY',
|
|
31
|
+
NO_CAP: 'E_NO_CAP',
|
|
32
|
+
TAMPER: 'E_INTEGRITY_TAMPER',
|
|
33
|
+
SIDECAR: 'E_SIDECAR_INVALID',
|
|
34
|
+
FRAME_PRIVATE: 'E_FRAME_PRIVATE',
|
|
35
|
+
ACCESS_DENIED: 'E_ACCESS_DENIED',
|
|
36
|
+
SUBJECT: 'E_SUBJECT_INVALID',
|
|
37
|
+
});
|
|
38
|
+
export const ROLE_SUBJECT_FIELDS = Object.freeze(['sessionId', 'userId', 'projectId', 'readers']);
|
|
39
|
+
/** §16.8 A:模型/子脑永远不是 host writer。 */
|
|
40
|
+
export const HOST_ACTORS = Object.freeze(['host', 'panel']);
|
|
41
|
+
|
|
42
|
+
function writeError(code, message, status = 403, details = undefined) {
|
|
43
|
+
const error = new RoleError(code, message, details);
|
|
44
|
+
error.status = status;
|
|
45
|
+
return error;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Host-only actor gate: anything else is a model/subagent direct write. */
|
|
49
|
+
export function assertHostWrite(actorKind) {
|
|
50
|
+
const actor = String(actorKind ?? 'host');
|
|
51
|
+
if (!HOST_ACTORS.includes(actor)) {
|
|
52
|
+
throw writeError(WRITE_CODES.WRITE_DENY, `模型/子脑不可直写宿主私有数据(actor=${actor});请走宿主写路径。`);
|
|
53
|
+
}
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function hostKeyPath(memoryRoot) {
|
|
58
|
+
return path.join(path.resolve(String(memoryRoot ?? '')), '.longlive', HOST_KEY_FILE);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Read the persisted host key file, or null when absent. */
|
|
62
|
+
export function readHostKeyFile(memoryRoot) {
|
|
63
|
+
try {
|
|
64
|
+
const text = fs.readFileSync(hostKeyPath(memoryRoot), 'utf8').trim();
|
|
65
|
+
return text === '' ? null : text;
|
|
66
|
+
} catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Host-injected key from the environment (never written to disk by this module). */
|
|
72
|
+
export function readHostKeyEnv(env = process.env) {
|
|
73
|
+
const value = env?.[AUDIT_KEY_ENV];
|
|
74
|
+
return typeof value === 'string' && value.trim() !== '' ? value.trim() : null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Resolve a key for read verification without creating anything:
|
|
79
|
+
* host-injected env/auditKey first, then the persisted fallback file.
|
|
80
|
+
* Missing everything => null (callers fail closed).
|
|
81
|
+
*/
|
|
82
|
+
export function readHostKey(memoryRoot, { auditKey = null, env = process.env } = {}) {
|
|
83
|
+
const envKey = readHostKeyEnv(env);
|
|
84
|
+
if (envKey !== null) return envKey;
|
|
85
|
+
if (Buffer.isBuffer(auditKey) || typeof auditKey === 'string') {
|
|
86
|
+
const key = Buffer.isBuffer(auditKey) ? auditKey.toString('hex') : String(auditKey);
|
|
87
|
+
if (key.trim() !== '') return key.trim();
|
|
88
|
+
}
|
|
89
|
+
return readHostKeyFile(memoryRoot);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Keys a read path may verify against: host env key, injected auditKey, then
|
|
94
|
+
* the persisted fallback key file. Writers seal with the first available key.
|
|
95
|
+
*/
|
|
96
|
+
export function hostKeyCandidates(memoryRoot, { auditKey = null, env = process.env } = {}) {
|
|
97
|
+
const candidates = [];
|
|
98
|
+
const envKey = readHostKeyEnv(env);
|
|
99
|
+
if (envKey !== null) candidates.push(envKey);
|
|
100
|
+
if (Buffer.isBuffer(auditKey) || typeof auditKey === 'string') {
|
|
101
|
+
const key = Buffer.isBuffer(auditKey) ? auditKey.toString('hex') : String(auditKey);
|
|
102
|
+
if (key.trim() !== '' && !candidates.includes(key.trim())) candidates.push(key.trim());
|
|
103
|
+
}
|
|
104
|
+
const fileKey = readHostKeyFile(memoryRoot);
|
|
105
|
+
if (fileKey !== null && !candidates.includes(fileKey)) candidates.push(fileKey);
|
|
106
|
+
return candidates;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Resolve the host write key: env DSH_LONGLIVE_AUDIT_KEY > injected auditKey >
|
|
111
|
+
* persisted fallback `<memoryRoot>/.longlive/.hostkey` (created on first use).
|
|
112
|
+
*/
|
|
113
|
+
export function resolveHostKey(memoryRoot, { auditKey = null, env = process.env } = {}) {
|
|
114
|
+
const envKey = readHostKeyEnv(env);
|
|
115
|
+
if (envKey !== null) return envKey;
|
|
116
|
+
if (Buffer.isBuffer(auditKey) || typeof auditKey === 'string') {
|
|
117
|
+
const key = Buffer.isBuffer(auditKey) ? auditKey.toString('hex') : String(auditKey);
|
|
118
|
+
if (key.trim() !== '') return key;
|
|
119
|
+
}
|
|
120
|
+
const file = hostKeyPath(memoryRoot);
|
|
121
|
+
try {
|
|
122
|
+
const text = fs.readFileSync(file, 'utf8').trim();
|
|
123
|
+
if (text !== '') return text;
|
|
124
|
+
} catch {
|
|
125
|
+
/* create below */
|
|
126
|
+
}
|
|
127
|
+
const key = crypto.randomBytes(32).toString('hex');
|
|
128
|
+
const dir = path.dirname(file);
|
|
129
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
130
|
+
const tmp = `${file}.tmp-${process.pid}-${crypto.randomBytes(3).toString('hex')}`;
|
|
131
|
+
fs.writeFileSync(tmp, `${key}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
132
|
+
fs.renameSync(tmp, file);
|
|
133
|
+
try { fs.chmodSync(file, 0o600); } catch { /* windows best effort */ }
|
|
134
|
+
return key;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function hmacFor(key, text) {
|
|
138
|
+
return crypto.createHmac('sha256', key).update(String(text ?? ''), 'utf8').digest('hex');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function sidecarPath(file) {
|
|
142
|
+
return `${file}${SIDECAR_SUFFIX}`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function writeAtomicRaw(file, text) {
|
|
146
|
+
const dir = path.dirname(file);
|
|
147
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
148
|
+
const tmp = path.join(dir, `.${path.basename(file)}.${process.pid}.${crypto.randomBytes(4).toString('hex')}.tmp`);
|
|
149
|
+
const fd = fs.openSync(tmp, 'w');
|
|
150
|
+
try {
|
|
151
|
+
fs.writeFileSync(fd, text, 'utf8');
|
|
152
|
+
fs.fsyncSync(fd);
|
|
153
|
+
} finally {
|
|
154
|
+
fs.closeSync(fd);
|
|
155
|
+
}
|
|
156
|
+
fs.renameSync(tmp, file);
|
|
157
|
+
const readBack = fs.readFileSync(file, 'utf8');
|
|
158
|
+
if (readBack !== text) throw writeError(WRITE_CODES.TAMPER, `写后读回不一致:${file}`, 500);
|
|
159
|
+
return { ok: true, bytes: Buffer.byteLength(text, 'utf8'), sha256: crypto.createHash('sha256').update(text).digest('hex') };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Host-only protected write: atomic file + HMAC sidecar over the exact bytes.
|
|
164
|
+
* Refuses model/subagent actors and paths outside the memory root.
|
|
165
|
+
*/
|
|
166
|
+
export function writeProtectedFile({ memoryRoot, file, text, auditKey = null, actorKind = 'host' }) {
|
|
167
|
+
assertHostWrite(actorKind);
|
|
168
|
+
const root = path.resolve(String(memoryRoot ?? ''));
|
|
169
|
+
if (!isInsideRoot(root, path.resolve(file))) {
|
|
170
|
+
throw memoryError(ROLE_CODES.PATH_OUTSIDE_ROOT, `写保护路径越界:${file}`);
|
|
171
|
+
}
|
|
172
|
+
const key = resolveHostKey(root, { auditKey });
|
|
173
|
+
const written = writeAtomicRaw(file, text);
|
|
174
|
+
const hmac = hmacFor(key, text);
|
|
175
|
+
writeAtomicRaw(sidecarPath(file), `${JSON.stringify({
|
|
176
|
+
schema: WRITEPROTECT_SCHEMA,
|
|
177
|
+
alg: 'HMAC-SHA256',
|
|
178
|
+
file: path.basename(file),
|
|
179
|
+
bytes: written.bytes,
|
|
180
|
+
sha256: written.sha256,
|
|
181
|
+
hmac,
|
|
182
|
+
sealedAt: iso(Date.now()),
|
|
183
|
+
}, null, 2)}\n`);
|
|
184
|
+
return { ok: true, file, bytes: written.bytes, sha256: written.sha256, hmac };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Host-only protected JSON write. */
|
|
188
|
+
export function writeProtectedJson({ memoryRoot, file, value, auditKey = null, actorKind = 'host' }) {
|
|
189
|
+
return writeProtectedFile({
|
|
190
|
+
memoryRoot,
|
|
191
|
+
file,
|
|
192
|
+
text: `${JSON.stringify(value, null, 2)}\n`,
|
|
193
|
+
auditKey,
|
|
194
|
+
actorKind,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Host-only protected JSONL append: append + re-seal the whole file sidecar. */
|
|
199
|
+
export function appendProtectedJsonl({ memoryRoot, file, row, auditKey = null, actorKind = 'host' }) {
|
|
200
|
+
assertHostWrite(actorKind);
|
|
201
|
+
const root = path.resolve(String(memoryRoot ?? ''));
|
|
202
|
+
if (!isInsideRoot(root, path.resolve(file))) {
|
|
203
|
+
throw memoryError(ROLE_CODES.PATH_OUTSIDE_ROOT, `写保护路径越界:${file}`);
|
|
204
|
+
}
|
|
205
|
+
const key = resolveHostKey(root, { auditKey });
|
|
206
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
207
|
+
const line = `${JSON.stringify(row)}\n`;
|
|
208
|
+
const fd = fs.openSync(file, 'a');
|
|
209
|
+
try {
|
|
210
|
+
fs.writeFileSync(fd, line, 'utf8');
|
|
211
|
+
try { fs.fsyncSync(fd); } catch { /* append fsync best effort */ }
|
|
212
|
+
} finally {
|
|
213
|
+
fs.closeSync(fd);
|
|
214
|
+
}
|
|
215
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
216
|
+
const hmac = hmacFor(key, text);
|
|
217
|
+
writeAtomicRaw(sidecarPath(file), `${JSON.stringify({
|
|
218
|
+
schema: WRITEPROTECT_SCHEMA,
|
|
219
|
+
alg: 'HMAC-SHA256',
|
|
220
|
+
file: path.basename(file),
|
|
221
|
+
bytes: Buffer.byteLength(text, 'utf8'),
|
|
222
|
+
hmac,
|
|
223
|
+
sealedAt: iso(Date.now()),
|
|
224
|
+
}, null, 2)}\n`);
|
|
225
|
+
return { ok: true, file, bytes: Buffer.byteLength(line, 'utf8'), hmac };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Verify one file against its sidecar. Returns {ok, code, tampered}.
|
|
230
|
+
* Missing sidecar on a missing file = clean absence; missing sidecar on an
|
|
231
|
+
* existing file = `E_SIDECAR_INVALID` (unprotected content must not load).
|
|
232
|
+
*/
|
|
233
|
+
export function verifyProtectedFile({ memoryRoot, file, auditKey = null }) {
|
|
234
|
+
const root = path.resolve(String(memoryRoot ?? ''));
|
|
235
|
+
let text;
|
|
236
|
+
try {
|
|
237
|
+
text = fs.readFileSync(file, 'utf8');
|
|
238
|
+
} catch (error) {
|
|
239
|
+
if (error?.code === 'ENOENT') return { ok: true, absent: true, tampered: false };
|
|
240
|
+
return { ok: false, absent: false, tampered: true, code: WRITE_CODES.TAMPER, error: error?.code ?? 'EREAD' };
|
|
241
|
+
}
|
|
242
|
+
let sidecar;
|
|
243
|
+
try {
|
|
244
|
+
sidecar = JSON.parse(fs.readFileSync(sidecarPath(file), 'utf8'));
|
|
245
|
+
} catch {
|
|
246
|
+
return {
|
|
247
|
+
ok: false, absent: false, tampered: true, code: WRITE_CODES.SIDECAR,
|
|
248
|
+
message: `缺少/不可读 HMAC 侧车:${sidecarPath(file)}(拒绝加载未受保护的宿主私有内容)`,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
const candidates = hostKeyCandidates(root, { auditKey });
|
|
252
|
+
// 读路径不创建 key 文件:没有可用 key 时 fail-safe 按 tampered 处理。
|
|
253
|
+
const matched = typeof sidecar?.hmac === 'string'
|
|
254
|
+
&& candidates.some((key) => hmacFor(key, text) === sidecar.hmac);
|
|
255
|
+
if (!matched) {
|
|
256
|
+
return { ok: false, absent: false, tampered: true, code: WRITE_CODES.TAMPER, message: 'HMAC 校验失败(文件被篡改或侧车不匹配)' };
|
|
257
|
+
}
|
|
258
|
+
return { ok: true, absent: false, tampered: false, text, sha256: sidecar.sha256 ?? null };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Read + verify protected JSON, parsing on success. */
|
|
262
|
+
export function readProtectedJsonValue({ memoryRoot, file, auditKey = null }) {
|
|
263
|
+
const verify = verifyProtectedFile({ memoryRoot, file, auditKey });
|
|
264
|
+
if (!verify.ok) return { ok: false, absent: verify.absent === true, code: verify.code, tampered: true, message: verify.message };
|
|
265
|
+
if (verify.absent) return { ok: true, absent: true, value: null };
|
|
266
|
+
try {
|
|
267
|
+
return { ok: true, absent: false, value: JSON.parse(verify.text) };
|
|
268
|
+
} catch {
|
|
269
|
+
return { ok: false, absent: false, code: WRITE_CODES.TAMPER, tampered: true, message: '受保护文件 JSON 解析失败' };
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** Read + verify protected JSONL. */
|
|
274
|
+
export function readProtectedJsonl({ memoryRoot, file, auditKey = null, limit = 10_000 }) {
|
|
275
|
+
const verify = verifyProtectedFile({ memoryRoot, file, auditKey });
|
|
276
|
+
if (!verify.ok) return { ok: false, absent: verify.absent === true, code: verify.code, tampered: true, rows: [], bad: 0, message: verify.message };
|
|
277
|
+
if (verify.absent) return { ok: true, absent: true, rows: [], bad: 0, total: 0 };
|
|
278
|
+
const rows = [];
|
|
279
|
+
let bad = 0;
|
|
280
|
+
for (const line of verify.text.split(/\r?\n/)) {
|
|
281
|
+
if (line.trim() === '') continue;
|
|
282
|
+
try {
|
|
283
|
+
const parsed = JSON.parse(line);
|
|
284
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) rows.push(parsed);
|
|
285
|
+
else bad += 1;
|
|
286
|
+
} catch {
|
|
287
|
+
bad += 1;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return { ok: true, absent: false, rows: rows.slice(-limit), total: rows.length, bad };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** Serialize a value to the canonical text used by the sidecar. */
|
|
294
|
+
export function protectedJsonText(value) {
|
|
295
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ------------------------------------------------------------------ 四元边界
|
|
299
|
+
//
|
|
300
|
+
// §16.8 E:sessionId/userId/projectId/readers[]。private 帧或 readers 不含调用者
|
|
301
|
+
// 时,跨用户读取被拒(host/panel 例外——它们持能力令牌)。
|
|
302
|
+
|
|
303
|
+
/** Normalize one subject (missing fields are null, readers is a string array). */
|
|
304
|
+
export function normalizeRoleSubject(raw = {}) {
|
|
305
|
+
const value = raw && typeof raw === 'object' ? raw : {};
|
|
306
|
+
const readers = Array.isArray(value.readers)
|
|
307
|
+
? [...new Set(value.readers.filter((item) => typeof item === 'string' && item.trim() !== '').map((item) => item.trim()))]
|
|
308
|
+
: [];
|
|
309
|
+
const field = (name) => (typeof value[name] === 'string' && value[name].trim() !== '' ? value[name].trim() : null);
|
|
310
|
+
const subject = {
|
|
311
|
+
sessionId: field('sessionId'),
|
|
312
|
+
userId: field('userId'),
|
|
313
|
+
projectId: field('projectId'),
|
|
314
|
+
readers,
|
|
315
|
+
};
|
|
316
|
+
if (value.private === true) subject.private = true;
|
|
317
|
+
return subject;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** True when the caller may read this subject's data. Host/panel always may. */
|
|
321
|
+
export function isSubjectReadable(subject, caller = {}) {
|
|
322
|
+
const actorKind = String(caller?.actorKind ?? 'host');
|
|
323
|
+
if (HOST_ACTORS.includes(actorKind)) return { ok: true, reason: 'host' };
|
|
324
|
+
const norm = normalizeRoleSubject(subject ?? {});
|
|
325
|
+
const userId = typeof caller?.userId === 'string' && caller.userId.trim() !== '' ? caller.userId.trim() : null;
|
|
326
|
+
if (norm.private === true && userId === null) return { ok: false, code: WRITE_CODES.FRAME_PRIVATE, reason: 'private' };
|
|
327
|
+
if (norm.readers.length > 0) {
|
|
328
|
+
if (userId !== null && norm.readers.includes(userId)) return { ok: true, reason: 'reader' };
|
|
329
|
+
return { ok: false, code: WRITE_CODES.ACCESS_DENIED, reason: 'readers' };
|
|
330
|
+
}
|
|
331
|
+
// No explicit reader list: the subject owner is the only non-host reader.
|
|
332
|
+
if (norm.userId !== null && userId !== norm.userId) {
|
|
333
|
+
return { ok: false, code: WRITE_CODES.ACCESS_DENIED, reason: 'owner-only' };
|
|
334
|
+
}
|
|
335
|
+
if (norm.userId !== null && userId === null) {
|
|
336
|
+
return { ok: false, code: WRITE_CODES.ACCESS_DENIED, reason: 'owner-only' };
|
|
337
|
+
}
|
|
338
|
+
return { ok: true, reason: 'open' };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** Throwing variant used by read paths that must fail closed. */
|
|
342
|
+
export function assertSubjectReadable(subject, caller = {}) {
|
|
343
|
+
const verdict = isSubjectReadable(subject, caller);
|
|
344
|
+
if (!verdict.ok) {
|
|
345
|
+
throw writeError(verdict.code, `跨用户读取被拒(${verdict.reason}):subject=${JSON.stringify(normalizeRoleSubject(subject ?? {}))}`, 403);
|
|
346
|
+
}
|
|
347
|
+
return true;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** Frame-specific: `private` frames without a matching reader → E_FRAME_PRIVATE. */
|
|
351
|
+
export function assertFrameReadable(frame, caller = {}) {
|
|
352
|
+
const subject = normalizeRoleSubject(frame?.subject ?? { sessionId: frame?.sessionId ?? null });
|
|
353
|
+
const verdict = isSubjectReadable(subject, caller);
|
|
354
|
+
if (!verdict.ok && (verdict.code === WRITE_CODES.FRAME_PRIVATE || subject.private === true)) {
|
|
355
|
+
throw writeError(WRITE_CODES.FRAME_PRIVATE, `私密帧拒绝读取(sessionId=${subject.sessionId ?? '?'},caller=${caller?.userId ?? '?'})`, 403);
|
|
356
|
+
}
|
|
357
|
+
if (!verdict.ok) {
|
|
358
|
+
throw writeError(WRITE_CODES.ACCESS_DENIED, `跨用户帧读取被拒(${verdict.reason})`, 403);
|
|
359
|
+
}
|
|
360
|
+
return true;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** Fill the four-element subject for a frame/frame-like record (host write). */
|
|
364
|
+
export function attachFrameSubject(frame, { subject = {}, readers = [], userId = null, projectId = null } = {}) {
|
|
365
|
+
const merged = normalizeRoleSubject({
|
|
366
|
+
sessionId: subject.sessionId ?? frame?.sessionId ?? null,
|
|
367
|
+
userId: subject.userId ?? userId,
|
|
368
|
+
projectId: subject.projectId ?? projectId,
|
|
369
|
+
readers: Array.isArray(subject.readers) ? subject.readers : readers,
|
|
370
|
+
private: subject.private === true || frame?.private === true,
|
|
371
|
+
});
|
|
372
|
+
return { ...frame, subject: merged };
|
|
373
|
+
}
|
package/src/router/aliases.json
CHANGED
|
@@ -2,30 +2,181 @@
|
|
|
2
2
|
"version": 1,
|
|
3
3
|
"description": "动作名归一表:canonical 用设计文档的点号写法,legacy 是 0.8.0-rc.1 之前的旧名。judge 一律用 canonical 比对;输出同时保留 legacy 作为兼容别名,旧调用方不用改。",
|
|
4
4
|
"actions": [
|
|
5
|
-
{
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
{
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
{
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
5
|
+
{
|
|
6
|
+
"canonical": "acp_status",
|
|
7
|
+
"legacy": "acp_status",
|
|
8
|
+
"aliases": [
|
|
9
|
+
"acp_status",
|
|
10
|
+
"context.status",
|
|
11
|
+
"acp.status"
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"canonical": "read",
|
|
16
|
+
"legacy": "read",
|
|
17
|
+
"aliases": [
|
|
18
|
+
"read",
|
|
19
|
+
"file.read"
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"canonical": "grep",
|
|
24
|
+
"legacy": "grep",
|
|
25
|
+
"aliases": [
|
|
26
|
+
"grep",
|
|
27
|
+
"file.grep"
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"canonical": "worker_spawn",
|
|
32
|
+
"legacy": "worker_spawn",
|
|
33
|
+
"aliases": [
|
|
34
|
+
"worker_spawn",
|
|
35
|
+
"worker.spawn",
|
|
36
|
+
"workflow"
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"canonical": "worker_status",
|
|
41
|
+
"legacy": "worker_status",
|
|
42
|
+
"aliases": [
|
|
43
|
+
"worker_status",
|
|
44
|
+
"worker.status"
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"canonical": "guard.refuse",
|
|
49
|
+
"legacy": "guard.refuse",
|
|
50
|
+
"aliases": [
|
|
51
|
+
"guard.refuse",
|
|
52
|
+
"refuse",
|
|
53
|
+
"guard_refuse"
|
|
54
|
+
]
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"canonical": "guard.dryRun",
|
|
58
|
+
"legacy": "guard_dry_run",
|
|
59
|
+
"aliases": [
|
|
60
|
+
"guard.dryRun",
|
|
61
|
+
"guard_dry_run",
|
|
62
|
+
"dry_run",
|
|
63
|
+
"dry-run"
|
|
64
|
+
]
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"canonical": "journal.append",
|
|
68
|
+
"legacy": "journal.append",
|
|
69
|
+
"aliases": [
|
|
70
|
+
"journal.append",
|
|
71
|
+
"journal_append"
|
|
72
|
+
]
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"canonical": "procedures.draft",
|
|
76
|
+
"legacy": "procedures.draft",
|
|
77
|
+
"aliases": [
|
|
78
|
+
"procedures.draft",
|
|
79
|
+
"procedures_draft",
|
|
80
|
+
"memory_procedure"
|
|
81
|
+
]
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"canonical": "topics.write",
|
|
85
|
+
"legacy": "topics.write",
|
|
86
|
+
"aliases": [
|
|
87
|
+
"topics.write",
|
|
88
|
+
"topics_write",
|
|
89
|
+
"topics.write_card"
|
|
90
|
+
]
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"canonical": "taskboard.update",
|
|
94
|
+
"legacy": "taskboard.update",
|
|
95
|
+
"aliases": [
|
|
96
|
+
"taskboard.update",
|
|
97
|
+
"taskboard_update"
|
|
98
|
+
]
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"canonical": "session.handover",
|
|
102
|
+
"legacy": "session_handover",
|
|
103
|
+
"aliases": [
|
|
104
|
+
"session_handover",
|
|
105
|
+
"session.handover",
|
|
106
|
+
"handover"
|
|
107
|
+
]
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"canonical": "session.compact",
|
|
111
|
+
"legacy": "session_compact",
|
|
112
|
+
"aliases": [
|
|
113
|
+
"session_compact",
|
|
114
|
+
"session.compact",
|
|
115
|
+
"compress"
|
|
116
|
+
]
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"canonical": "reply.only",
|
|
120
|
+
"legacy": "reply.only",
|
|
121
|
+
"aliases": [
|
|
122
|
+
"reply.only",
|
|
123
|
+
"reply_only",
|
|
124
|
+
"chat"
|
|
125
|
+
]
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
"canonical": "reply.no_write",
|
|
129
|
+
"legacy": "reply.no_write",
|
|
130
|
+
"aliases": [
|
|
131
|
+
"reply.no_write",
|
|
132
|
+
"reply_no_write",
|
|
133
|
+
"no_write"
|
|
134
|
+
]
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
"canonical": "reply.ask",
|
|
138
|
+
"legacy": "reply_ask",
|
|
139
|
+
"aliases": [
|
|
140
|
+
"reply.ask",
|
|
141
|
+
"reply_ask",
|
|
142
|
+
"ask_user"
|
|
143
|
+
]
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
"canonical": "memory.distillPropose",
|
|
147
|
+
"legacy": "memory_distill_propose",
|
|
148
|
+
"aliases": [
|
|
149
|
+
"memory.distillPropose",
|
|
150
|
+
"memory_distill_propose",
|
|
151
|
+
"propose_distill"
|
|
152
|
+
]
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
"canonical": "memory.delete",
|
|
156
|
+
"legacy": "memory_delete",
|
|
157
|
+
"aliases": [
|
|
158
|
+
"memory.delete",
|
|
159
|
+
"memory_delete",
|
|
160
|
+
"forget_memory"
|
|
161
|
+
]
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
"canonical": "identity.hold",
|
|
165
|
+
"legacy": "identity_hold",
|
|
166
|
+
"aliases": [
|
|
167
|
+
"identity.hold",
|
|
168
|
+
"identity_hold",
|
|
169
|
+
"identity_consent_hold"
|
|
170
|
+
]
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
"canonical": "memory.save_local",
|
|
174
|
+
"legacy": "memory_save_local",
|
|
175
|
+
"aliases": [
|
|
176
|
+
"memory.save_local",
|
|
177
|
+
"memory_save_local",
|
|
178
|
+
"save_local"
|
|
179
|
+
]
|
|
180
|
+
}
|
|
30
181
|
]
|
|
31
182
|
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// (dsh-session :1672-1687)给出按 surface 重放的消息序列。
|
|
11
11
|
// - dispose 在 create() 的返回值上,走与 unload 相同的 teardown(:1740-1746)。
|
|
12
12
|
//
|
|
13
|
-
//
|
|
13
|
+
// 评审 P0-2 的教训:旧实现调 handle.followup / handle.whenIdle / handle.result,
|
|
14
14
|
// 在真宿主上直接 E_AGENT_IDLE(handle.followup is not a function)。
|
|
15
15
|
//
|
|
16
16
|
// 顺序仍然是 create -> followup -> whenIdle -> dispose(finally),任何一步失败
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// anchor.mjs — 预算账本 / 审计链之外的第三个「锚」
|
|
2
2
|
//
|
|
3
|
-
//
|
|
3
|
+
// 为什么需要它(评审 F10):账本和审计各自都是自证 hash 链。攻击者如果有写权限,
|
|
4
4
|
// 可以同时重算两条链(把 used 30 改成 5),两条链各自自洽、互相对得上,旧版
|
|
5
5
|
// start() 就放行了。锚文件把「某一时刻两条链的 (seq, last_hash, used)」再封一次,
|
|
6
6
|
// 并且自己也是一条只追加 hash 链;重算账本+审计而不同时重算锚,就会在锚的
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
// 1. 只追加,不重写;崩溃后文件仍可逐行解析。
|
|
5
5
|
// 2. 每条记录都有固定信封:v / seq / ts / run_id / kind,其余字段可选。
|
|
6
6
|
// 3. 每条记录带 CRC32(crc 字段),读取时可发现「被截断/篡改但仍能 JSON.parse」
|
|
7
|
-
// 的记录 ——
|
|
7
|
+
// 的记录 —— 评审 P04 的根因是旧实现只跳过「解析不了」的行,截断记录被当成
|
|
8
8
|
// 合法数据静默丢掉,预算回放于是少算。
|
|
9
9
|
// 4. hash chain:每条记录带 prev_hash + hash(sha256),删中间整行 / 改字段 /
|
|
10
|
-
// 缺号都会在 readAudit() 的 chainBreaks
|
|
10
|
+
// 缺号都会在 readAudit() 的 chainBreaks 里暴露(评审 P0-4)。
|
|
11
11
|
// 5. seq 单调递增;重启时从所有轮转文件里续号,而不是从 1 重来。
|
|
12
12
|
// 6. 每次写都 fsync,保证「动作已发生」与「审计已落盘」尽可能同生共死。
|
|
13
13
|
// 7. 超过 maxBytes 时轮转:base -> .1 -> .2 ...(最多 maxRotations 个)。
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
//
|
|
3
3
|
// 为什么需要它:Router 的预算上限是「每个 run 目录」的,不是「每个进程实例」的。
|
|
4
4
|
// 旧实现把 tokensUsed 放在 Router 实例内存里,两个实例各读同一个审计文件、
|
|
5
|
-
// 各自以为还剩 100,结果 75 + 75 = 150
|
|
5
|
+
// 各自以为还剩 100,结果 75 + 75 = 150 超支(评审 P02);重启时又从审计里
|
|
6
6
|
// 取 max(tokens_total) 而不是累加(P03),只恢复了一部分。
|
|
7
7
|
//
|
|
8
8
|
// 账本是一个只追加的 JSONL 文件(默认与 queue 同目录的 budget.ledger.jsonl):
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// - reserve 是全有或全无:剩余不够 callMaxTokens 就返回 E_BUDGET_EXCEEDED,
|
|
14
14
|
// 不写任何记录(绝不出现「先花了再发现超了」);
|
|
15
15
|
// - settle 是硬上限:实花只记 min(请求值, 预留, 预算剩余),超出部分拒绝并
|
|
16
|
-
// 返回 E_BUDGET_OVERSHOOT
|
|
16
|
+
// 返回 E_BUDGET_OVERSHOOT(评审 P0-1:60+60 重试不得记 120);
|
|
17
17
|
// - reserve 记录带 pid / started_at / lease_expires_at;崩溃遗留的 active 预留
|
|
18
18
|
// 在下次启动或下一 tick 时按「pid 不存在」或「租约过期」回收(P0-2);
|
|
19
19
|
// - 每条记录带 prev_hash + hash 的链,删整行/改字段会在下次读时暴露(P0-4);
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// memory.mjs — 只读记忆快照
|
|
2
2
|
//
|
|
3
|
-
// 原型只读一个本地 JSON/Markdown
|
|
4
|
-
//
|
|
3
|
+
// 原型只读一个本地 JSON/Markdown 快照,不直接打开外部记忆库的 SQLite:
|
|
4
|
+
// 外部记忆库是 node:sqlite 的私有 schema(见 SPIKE.md 的证据),后台面不应绕过
|
|
5
5
|
// 它的写入/迁移逻辑去读。生产接线有两条正路(SPIKE.md §3):
|
|
6
|
-
// a) 走 ctx.agents 起一个 agent
|
|
6
|
+
// a) 走 ctx.agents 起一个 agent,让外部记忆插件的 agent/pre-step 自动注入;
|
|
7
7
|
// b) 读宿主公开的记忆根(longlive 的 topics/ journal/ 明文 markdown)。
|
|
8
8
|
//
|
|
9
9
|
// 这里演示 (b) 的最小形态,并把「读不到」当成正常情况(fail-open),
|
|
@@ -53,7 +53,7 @@ export class MemoryReader {
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
/** 把记忆事实拼进 system
|
|
56
|
+
/** 把记忆事实拼进 system 提示(原型级;生产应交给外部记忆插件的 pre-step)。 */
|
|
57
57
|
export function memoryToSystemPrompt(facts) {
|
|
58
58
|
if (!facts || facts.length === 0) return '你是 Router 后台面的执行体。当前没有可用记忆。';
|
|
59
59
|
return [
|