@jjh32456/dsh-longlive 1.6.8 → 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.
Files changed (288) hide show
  1. package/CHANGELOG.md +69 -1539
  2. package/LICENSE +1 -1
  3. package/QUICKSTART.md +33 -12
  4. package/README.en.md +17 -7
  5. package/README.md +29 -10
  6. package/SPEC.md +5 -5
  7. package/cordis.patch.yml +17 -12
  8. package/docs/ANSWER-GUARD.md +92 -0
  9. package/docs/ARCHITECTURE.md +3 -3
  10. package/docs/AUTO-RECALL-WIRING.md +1 -1
  11. package/docs/AUTO-RECALL.md +15 -9
  12. package/docs/CONFIG.md +143 -37
  13. package/docs/ERRORS.md +17 -5
  14. package/docs/FAQ.md +7 -7
  15. package/docs/FEATURES.md +66 -29
  16. package/docs/MIGRATION-1.6.0.md +2 -2
  17. package/docs/OPERATIONS.md +6 -6
  18. package/docs/PANEL-INTEGRATION.md +6 -0
  19. package/docs/PRIVACY.md +88 -0
  20. package/docs/QUICKSTART.md +5 -5
  21. package/docs/README.en.md +3 -2
  22. package/docs/RESIDENT-WATCHER.md +1 -1
  23. package/docs/SESSION-WATCH.md +6 -6
  24. package/docs/TROUBLESHOOTING.md +4 -4
  25. package/package.json +42 -117
  26. package/runbook.md +2 -2
  27. package/scripts/backfill-auto-cards.mjs +98 -0
  28. package/scripts/install-surgery-task.ps1 +5 -2
  29. package/scripts/lib/session-watch-fixture.mjs +1 -1
  30. package/scripts/longlive-doctor.mjs +6 -0
  31. package/scripts/migrate-180.mjs +163 -0
  32. package/scripts/recall-index-build.mjs +128 -0
  33. package/scripts/session-watch-drill.mjs +1 -1
  34. package/scripts/surgery-run.cmd +1 -1
  35. package/scripts/unattended-surgery.ps1 +5 -2
  36. package/scripts/verify-card-anchors.mjs +138 -0
  37. package/src/actions/audit.js +125 -0
  38. package/src/actions/evidence.js +122 -0
  39. package/src/actions/fs.js +162 -0
  40. package/src/activity/index.js +129 -18
  41. package/src/activity/model.js +11 -1
  42. package/src/activity/store.js +22 -3
  43. package/src/activity/thinking.js +176 -0
  44. package/src/adapters/constants.js +11 -0
  45. package/src/adapters/engram/data/aliases.json +51 -0
  46. package/src/adapters/engram/data/policy.json +8 -0
  47. package/src/adapters/engram/data/routing-rules.json +546 -0
  48. package/src/adapters/engram/index.js +235 -0
  49. package/src/adapters/registry.js +315 -0
  50. package/src/answer/audit.js +193 -0
  51. package/src/answer/evidence.js +30 -0
  52. package/src/answer/guard.js +1370 -0
  53. package/src/answer/index.js +1067 -0
  54. package/src/answer/query.js +111 -0
  55. package/src/answer/second-hop.mjs +101 -0
  56. package/src/auto-truncate.js +1 -1
  57. package/src/config.js +145 -48
  58. package/src/deps.js +6 -4
  59. package/src/features.js +158 -8
  60. package/src/identity/index.js +13 -5
  61. package/src/identity/inject-position.js +2 -2
  62. package/src/index.js +97 -7
  63. package/src/lifecycle/gap-clocks.js +328 -0
  64. package/src/lifecycle/index.js +269 -0
  65. package/src/memory/activity-capture.js +229 -0
  66. package/src/memory/anchors.js +194 -0
  67. package/src/memory/audit-records.js +222 -0
  68. package/src/memory/auto-cards.js +401 -0
  69. package/src/memory/capture.js +442 -0
  70. package/src/memory/card-anchors.js +203 -0
  71. package/src/memory/conflicts.js +106 -0
  72. package/src/memory/decay.js +311 -0
  73. package/src/memory/distill.js +471 -0
  74. package/src/memory/event-index.js +917 -0
  75. package/src/memory/evidence-chain.js +295 -0
  76. package/src/memory/evidence.js +145 -0
  77. package/src/memory/filter-runner.js +1 -1
  78. package/src/memory/filter.js +5 -1
  79. package/src/memory/forget-v2.js +410 -0
  80. package/src/memory/forget.js +71 -0
  81. package/src/memory/frames-activation.js +247 -0
  82. package/src/memory/fts-query.js +174 -0
  83. package/src/memory/governance.js +604 -0
  84. package/src/memory/index.js +1 -0
  85. package/src/memory/l2-gate.js +169 -0
  86. package/src/memory/lock.js +121 -0
  87. package/src/memory/migrate.js +139 -0
  88. package/src/memory/recall-v2.js +223 -0
  89. package/src/memory/recall.js +65 -2
  90. package/src/memory/records.js +425 -0
  91. package/src/memory/retention.js +128 -0
  92. package/src/memory/review.js +124 -0
  93. package/src/memory/trust.js +88 -0
  94. package/src/memory/version-chain.js +264 -0
  95. package/src/memory/wal.js +137 -0
  96. package/src/memory/working-frame.js +516 -0
  97. package/src/metrics/classify.mjs +5 -3
  98. package/src/metrics/index.mjs +21 -1
  99. package/src/metrics/prestep-latency.mjs +300 -0
  100. package/src/metrics/store.mjs +25 -0
  101. package/src/policy-profile.js +18 -3
  102. package/src/recall/anaphora.mjs +87 -0
  103. package/src/recall/cards.mjs +7 -0
  104. package/src/recall/embedding.mjs +8 -8
  105. package/src/recall/entities.mjs +112 -0
  106. package/src/recall/index.js +235 -19
  107. package/src/recall/inject-order.mjs +10 -8
  108. package/src/recall/privacy.mjs +1 -1
  109. package/src/recall/recall-core.mjs +116 -10
  110. package/src/recall/shared-visibility.js +302 -0
  111. package/src/recall/time-normalizer.mjs +196 -0
  112. package/src/role/activation.js +191 -0
  113. package/src/role/attention.js +318 -0
  114. package/src/role/base.js +36 -0
  115. package/src/role/channels.js +423 -0
  116. package/src/role/index.js +425 -0
  117. package/src/role/interfaces.js +115 -0
  118. package/src/role/personality.js +370 -0
  119. package/src/role/readers.js +151 -0
  120. package/src/role/relationships.js +477 -0
  121. package/src/role/store.js +202 -0
  122. package/src/role/writeprotect.js +373 -0
  123. package/src/router/aliases.json +176 -25
  124. package/src/router/background/agent-runner.mjs +1 -1
  125. package/src/router/background/anchor.mjs +1 -1
  126. package/src/router/background/audit.mjs +2 -2
  127. package/src/router/background/budget-ledger.mjs +2 -2
  128. package/src/router/background/host-sim.mjs +1 -1
  129. package/src/router/background/memory.mjs +4 -4
  130. package/src/router/background/queue.mjs +1 -1
  131. package/src/router/background/retry.mjs +1 -1
  132. package/src/router/background/router.mjs +1 -1
  133. package/src/router/background/stream.mjs +1 -1
  134. package/src/router/background/tool-policy.mjs +1 -1
  135. package/src/router/design-headings.json +5 -5
  136. package/src/router/face/config.mjs +1 -1
  137. package/src/router/face/identity.mjs +3 -3
  138. package/src/router/judge.mjs +8 -7
  139. package/src/router/policies/generic.json +21 -4
  140. package/src/router/route.js +109 -22
  141. package/src/router/router.mjs +10 -6
  142. package/src/router/routing-rules.json +260 -445
  143. package/src/router/run-sim.mjs +8 -8
  144. package/src/router/runtime.js +23 -12
  145. package/src/router/service/config.mjs +5 -5
  146. package/src/router/service/context.mjs +1 -1
  147. package/src/router/service/queue.mjs +1 -1
  148. package/src/router/shadow.mjs +4 -3
  149. package/src/surgery-recovery.js +2 -2
  150. package/src/tool-output.js +2 -2
  151. package/src/tools.js +39 -7
  152. package/src/truncate-visible.js +2 -2
  153. package/src/ui/actions.js +1312 -0
  154. package/src/ui/panel-api.js +367 -0
  155. package/src/ui/panel-auth.js +251 -0
  156. package/src/ui/panel-integrity.js +243 -0
  157. package/src/ui/routes-readonly.js +695 -0
  158. package/src/ui/routes.js +3 -0
  159. package/docs/DEV.md +0 -279
  160. package/docs/PRESENCE-BUS-WIRING.md +0 -491
  161. package/docs/PRESENCE-BUS.md +0 -694
  162. package/docs/SURGERY.md +0 -186
  163. package/scripts/mark-core-cards.mjs +0 -289
  164. package/scripts/pack.mjs +0 -392
  165. package/scripts/router-proposals.mjs +0 -386
  166. package/src/router/fixtures/M1.json +0 -16
  167. package/src/router/fixtures/M2.json +0 -16
  168. package/src/router/fixtures/M3.json +0 -20
  169. package/src/router/fixtures/M4.json +0 -20
  170. package/src/router/fixtures/M5.json +0 -45
  171. package/src/router/fixtures/M6.json +0 -53
  172. package/src/router/fixtures/M7.json +0 -46
  173. package/src/router/fixtures/S1.json +0 -20
  174. package/src/router/fixtures/S2.json +0 -23
  175. package/src/router/fixtures/S3.json +0 -20
  176. package/src/router/fixtures/S4.json +0 -34
  177. package/src/router/fixtures/S5.json +0 -43
  178. package/src/router/fixtures/S6.json +0 -47
  179. package/src/router/fixtures/T1.json +0 -23
  180. package/src/router/fixtures/T2.json +0 -16
  181. package/src/router/fixtures/T3.json +0 -19
  182. package/src/router/fixtures/T4.json +0 -16
  183. package/src/router/fixtures/X1.json +0 -17
  184. package/src/router/fixtures/X2.json +0 -20
  185. package/src/router/gen-design-headings.mjs +0 -107
  186. package/src/router/policies/owner-rulings-2026-11-03.json +0 -62
  187. package/test/activity-realtime.test.mjs +0 -959
  188. package/test/activity-stream.test.mjs +0 -512
  189. package/test/auto-recall.test.mjs +0 -474
  190. package/test/auto-truncate.test.mjs +0 -931
  191. package/test/autobind.test.mjs +0 -399
  192. package/test/bus.test.mjs +0 -1572
  193. package/test/cli-guard.test.mjs +0 -198
  194. package/test/conclusion.test.mjs +0 -408
  195. package/test/config-schema.test.mjs +0 -210
  196. package/test/d6-output.test.mjs +0 -194
  197. package/test/d6.test.mjs +0 -395
  198. package/test/deps.test.mjs +0 -342
  199. package/test/doctor-setup.test.mjs +0 -297
  200. package/test/embedding-recall.test.mjs +0 -347
  201. package/test/feature-flags.test.mjs +0 -585
  202. package/test/filter-shadow.test.mjs +0 -170
  203. package/test/fixtures/activity-append-child.mjs +0 -29
  204. package/test/fixtures/baseline-0.8.0-registration.json +0 -556
  205. package/test/fixtures/d6-header-tools.json +0 -1
  206. package/test/fixtures/lock-child.mjs +0 -67
  207. package/test/fixtures/pre-step-container.mjs +0 -100
  208. package/test/fixtures/privacy-memory/topics/pii-unmarked.md +0 -9
  209. package/test/fixtures/privacy-memory/topics/secret-local-only.md +0 -11
  210. package/test/fixtures/profile-session-longlive-config.json +0 -20
  211. package/test/fixtures/proposal-cas-child.mjs +0 -66
  212. package/test/host-engine-status.test.mjs +0 -307
  213. package/test/hot-truncate.test.mjs +0 -1349
  214. package/test/identity-card-marker.test.mjs +0 -307
  215. package/test/identity-router/run.mjs +0 -721
  216. package/test/integration-pre-step.test.mjs +0 -553
  217. package/test/live.test.mjs +0 -1023
  218. package/test/maintenance-schedule.test.mjs +0 -538
  219. package/test/memory.test.mjs +0 -2828
  220. package/test/metrics.test.mjs +0 -478
  221. package/test/orchestrator.test.mjs +0 -266
  222. package/test/p1_probe.mjs +0 -97
  223. package/test/packaging.test.mjs +0 -184
  224. package/test/portability.test.mjs +0 -343
  225. package/test/rc4-regressions.test.mjs +0 -562
  226. package/test/rc5-regressions.test.mjs +0 -197
  227. package/test/rc6-chinese.test.mjs +0 -394
  228. package/test/router-1100/apply-off.test.mjs +0 -175
  229. package/test/router-1100/budget-reconcile.test.mjs +0 -215
  230. package/test/router-1100/config-context.test.mjs +0 -211
  231. package/test/router-1100/helpers.mjs +0 -129
  232. package/test/router-1100/hint-envelope.test.mjs +0 -150
  233. package/test/router-1100/lock-crossproc.test.mjs +0 -206
  234. package/test/router-1100/proposal-cas-crossproc.test.mjs +0 -184
  235. package/test/router-1100/router-apply.test.mjs +0 -372
  236. package/test/router-1100/router-fixb.test.mjs +0 -602
  237. package/test/router-1100/router-proposals.test.mjs +0 -405
  238. package/test/router-1100/router-queue-lease-wal.test.mjs +0 -355
  239. package/test/router-1100/router-security-cr.test.mjs +0 -333
  240. package/test/router-1100/service-lifecycle.test.mjs +0 -220
  241. package/test/router-1100/tools-contract.test.mjs +0 -164
  242. package/test/router-1100/turn-reflect-tick.test.mjs +0 -89
  243. package/test/router-1100/v6-contract.test.mjs +0 -158
  244. package/test/router-1100/v6b-contract.test.mjs +0 -293
  245. package/test/router-1100/v6c-contract.test.mjs +0 -249
  246. package/test/router-1100/wal-rotation.test.mjs +0 -375
  247. package/test/router-1100/wp1-config.test.mjs +0 -170
  248. package/test/router-1100/wp1-default-off.test.mjs +0 -77
  249. package/test/router-1100/wp1-model-status.test.mjs +0 -90
  250. package/test/router-1100/wp1-status.test.mjs +0 -220
  251. package/test/router-1100/wp1-tools.test.mjs +0 -146
  252. package/test/router-1100/wp5-cli.test.mjs +0 -240
  253. package/test/router-1100/wp5-model-auto.test.mjs +0 -418
  254. package/test/router-1100/wp6-merge.test.mjs +0 -386
  255. package/test/router-1100/wp6-wake.test.mjs +0 -502
  256. package/test/router-background/audit.test.mjs +0 -37
  257. package/test/router-background/budget-ledger.test.mjs +0 -330
  258. package/test/router-background/cross-process.test.mjs +0 -222
  259. package/test/router-background/fixtures/cross-child.mjs +0 -85
  260. package/test/router-background/fixtures/real-shapes.mjs +0 -239
  261. package/test/router-background/router.test.mjs +0 -1076
  262. package/test/router-background/stream-retry.test.mjs +0 -358
  263. package/test/router-background/tool-agent.test.mjs +0 -384
  264. package/test/router-face/bridge.test.mjs +0 -169
  265. package/test/router-face/face.test.mjs +0 -316
  266. package/test/router-failclosed.test.mjs +0 -466
  267. package/test/router-identity.test.mjs +0 -384
  268. package/test/router-l1-l2.contract.test.mjs +0 -1075
  269. package/test/router-memory-tools.test.mjs +0 -262
  270. package/test/router-policy.test.mjs +0 -225
  271. package/test/router-runtime.test.mjs +0 -438
  272. package/test/router-shadow.test.mjs +0 -287
  273. package/test/router.test.mjs +0 -903
  274. package/test/run-rc.mjs +0 -315
  275. package/test/run.mjs +0 -2469
  276. package/test/safety-defaults.test.mjs +0 -181
  277. package/test/seeded-load.test.mjs +0 -436
  278. package/test/session-original.test.mjs +0 -338
  279. package/test/state-truth.test.mjs +0 -937
  280. package/test/surgery-recovery.test.mjs +0 -936
  281. package/test/t7_drive.mjs +0 -151
  282. package/test/tool-output.test.mjs +0 -393
  283. package/test/tool-schema.test.mjs +0 -177
  284. package/test/tools_contract.test.mjs +0 -716
  285. package/test/truncate-visible.test.mjs +0 -1150
  286. package/test/ui-flags.test.mjs +0 -477
  287. package/test/v4-hardening.test.mjs +0 -427
  288. package/test/version-consistency.test.mjs +0 -286
@@ -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
+ }
@@ -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
- { "canonical": "acp_status", "legacy": "acp_status", "aliases": ["acp_status", "context.status", "acp.status"] },
6
- { "canonical": "read", "legacy": "read", "aliases": ["read", "file.read"] },
7
- { "canonical": "grep", "legacy": "grep", "aliases": ["grep", "file.grep"] },
8
- { "canonical": "worker_spawn", "legacy": "worker_spawn", "aliases": ["worker_spawn", "worker.spawn", "workflow"] },
9
- { "canonical": "worker_status", "legacy": "worker_status", "aliases": ["worker_status", "worker.status"] },
10
- { "canonical": "guard.refuse", "legacy": "guard.refuse", "aliases": ["guard.refuse", "refuse", "guard_refuse"] },
11
- { "canonical": "guard.dryRun", "legacy": "guard_dry_run", "aliases": ["guard.dryRun", "guard_dry_run", "dry_run", "dry-run"] },
12
- { "canonical": "engram.search", "legacy": "engram_search", "aliases": ["engram_search", "engram.search", "memory_recall"] },
13
- { "canonical": "engram.save", "legacy": "engram_save", "aliases": ["engram_save", "engram.save", "save_memory"] },
14
- { "canonical": "engram.facts.replace", "legacy": "engram_facts", "aliases": ["engram_facts", "engram.facts", "engram.facts.replace"] },
15
- { "canonical": "journal.append", "legacy": "journal.append", "aliases": ["journal.append", "journal_append"] },
16
- { "canonical": "procedures.draft", "legacy": "procedures.draft", "aliases": ["procedures.draft", "procedures_draft", "memory_procedure"] },
17
- { "canonical": "topics.write", "legacy": "topics.write", "aliases": ["topics.write", "topics_write", "topics.write_card"] },
18
- { "canonical": "taskboard.update", "legacy": "taskboard.update", "aliases": ["taskboard.update", "taskboard_update"] },
19
- { "canonical": "session.handover", "legacy": "session_handover", "aliases": ["session_handover", "session.handover", "handover"] },
20
- { "canonical": "session.compact", "legacy": "session_compact", "aliases": ["session_compact", "session.compact", "compress"] },
21
- { "canonical": "reply.only", "legacy": "reply.only", "aliases": ["reply.only", "reply_only", "chat"] },
22
- { "canonical": "reply.no_write", "legacy": "reply.no_write", "aliases": ["reply.no_write", "reply_no_write", "no_write"] },
23
- { "canonical": "reply.ask", "legacy": "reply_ask", "aliases": ["reply.ask", "reply_ask", "ask_user"] },
24
- { "canonical": "memory.save_local", "legacy": "memory_save_local", "aliases": ["memory.save_local", "memory_save_local", "save_local"] },
25
- { "canonical": "memory.distillPropose", "legacy": "memory_distill_propose", "aliases": ["memory.distillPropose", "memory_distill_propose", "propose_distill"] },
26
- { "canonical": "memory.delete", "legacy": "memory_delete", "aliases": ["memory.delete", "memory_delete", "forget_memory"] },
27
- { "canonical": "engram.forget", "legacy": "engram_forget", "aliases": ["engram.forget", "engram_forget", "forget"] },
28
- { "canonical": "engram.profile.edit", "legacy": "engram_profile_edit", "aliases": ["engram.profile.edit", "engram_profile_edit", "profile_edit"] },
29
- { "canonical": "identity.hold", "legacy": "identity_hold", "aliases": ["identity.hold", "identity_hold", "identity_consent_hold"] }
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
- // 红队 P0-2 的教训:旧实现调 handle.followup / handle.whenIdle / handle.result,
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
- // 为什么需要它(红队 F10):账本和审计各自都是自证 hash 链。攻击者如果有写权限,
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
- // 的记录 —— 红队 P04 的根因是旧实现只跳过「解析不了」的行,截断记录被当成
7
+ // 的记录 —— 评审 P04 的根因是旧实现只跳过「解析不了」的行,截断记录被当成
8
8
  // 合法数据静默丢掉,预算回放于是少算。
9
9
  // 4. hash chain:每条记录带 prev_hash + hash(sha256),删中间整行 / 改字段 /
10
- // 缺号都会在 readAudit() 的 chainBreaks 里暴露(红队 P0-4)。
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 超支(红队 P02);重启时又从审计里
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(红队 P0-1:60+60 重试不得记 120);
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,6 +1,6 @@
1
1
  // host-sim.mjs — 真宿主形状的最小仿真器(测试 fixture 用)
2
2
  //
3
- // 第二轮红队的核心批评:旧 sim 的「形状」是自造的,测试只能证明 sim 与
3
+ // 第二轮评审的核心批评:旧 sim 的「形状」是自造的,测试只能证明 sim 与
4
4
  // 自己一致。这一版按宿主 checkout 里抽出的真实签名
5
5
  // 重写,测试断言的都是真宿主会给出/接受的东西:
6
6
  //
@@ -1,9 +1,9 @@
1
1
  // memory.mjs — 只读记忆快照
2
2
  //
3
- // 原型只读一个本地 JSON/Markdown 快照,不直接打开 engram 的 SQLite:
4
- // engram 库是 node:sqlite 的私有 schema(见 SPIKE.md 的证据),后台面不应绕过
3
+ // 原型只读一个本地 JSON/Markdown 快照,不直接打开外部记忆库的 SQLite:
4
+ // 外部记忆库是 node:sqlite 的私有 schema(见 SPIKE.md 的证据),后台面不应绕过
5
5
  // 它的写入/迁移逻辑去读。生产接线有两条正路(SPIKE.md §3):
6
- // a) 走 ctx.agents 起一个 agent,让 engram 的 agent/pre-step 自动注入;
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 提示(原型级;生产应交给 engram 的 pre-step)。 */
56
+ /** 把记忆事实拼进 system 提示(原型级;生产应交给外部记忆插件的 pre-step)。 */
57
57
  export function memoryToSystemPrompt(facts) {
58
58
  if (!facts || facts.length === 0) return '你是 Router 后台面的执行体。当前没有可用记忆。';
59
59
  return [