@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
@@ -16,7 +16,7 @@ exit /b 0
16
16
 
17
17
  :usage
18
18
  echo Usage:
19
- echo surgery-run.cmd -SessionPath "C:\...\session.v4.jsonl.zstd" -AppPath "<app folder>\DeepSeek Harness.exe" -Confirm TRUNCATE-VISIBLE [options]
19
+ echo surgery-run.cmd -SessionPath "<session-file>" -AppPath "<app folder>\DeepSeek Harness.exe" -Confirm TRUNCATE-VISIBLE [options]
20
20
  echo.
21
21
  echo Options:
22
22
  echo -KeepTurns N default 12
@@ -75,7 +75,7 @@
75
75
  powershell.exe -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass `
76
76
  -File scripts\unattended-surgery.ps1 `
77
77
  -SessionPath "%USERPROFILE%\.dsh\sessions\scope\abc\session.v4.jsonl.zstd" `
78
- -AppPath "C:\Program Files\DSH\DSH.exe" -Confirm TRUNCATE-VISIBLE
78
+ -AppPath "<path-to-DSH-host.exe>" -Confirm TRUNCATE-VISIBLE
79
79
  #>
80
80
  [CmdletBinding()]
81
81
  param(
@@ -576,8 +576,11 @@ if ($RestartDelaySeconds -lt 0 -or $RestartDelaySeconds -gt 86400) {
576
576
  if (-not $DryRun -and $Confirm -ne 'TRUNCATE-VISIBLE') {
577
577
  Write-StderrAndExit "Refusing to cut: pass -Confirm TRUNCATE-VISIBLE (or use -DryRun)." 2
578
578
  }
579
+ if (-not $DryRun -and -not $AppPath -and $env:DSH_APP_PATH) {
580
+ $AppPath = $env:DSH_APP_PATH
581
+ }
579
582
  if (-not $DryRun -and -not $AppPath) {
580
- Write-StderrAndExit "Refusing to cut: pass -AppPath <DSH host exe> so the host can be restarted." 2
583
+ Write-StderrAndExit "Refusing to cut: pass -AppPath <dsh host exe> or set DSH_APP_PATH so the host can be restarted." 2
581
584
  }
582
585
  if ($AppPath) {
583
586
  if (-not (Test-Path -LiteralPath $AppPath -PathType Leaf)) {
@@ -0,0 +1,138 @@
1
+ #!/usr/bin/env node
2
+ // scripts/verify-card-anchors.mjs — 1.8.0 AutoCard anchor liveness check.
3
+ //
4
+ // node scripts/verify-card-anchors.mjs [--memory-root <dir>] [--sessions-root <dir>]
5
+ // [--all | --sample N] [--json] [--out <file>]
6
+ //
7
+ // Reads `.longlive/index/cards.jsonl`, loads the original L0 session for every
8
+ // card anchor, recomputes turnDigest, and labels each card live / stale / missing.
9
+ // Exit 1 when any card is not live (unless --allow-stale for inspection runs).
10
+
11
+ import fs from 'node:fs';
12
+ import os from 'node:os';
13
+ import path from 'node:path';
14
+
15
+ import { parseSession } from '../src/codec.js';
16
+ import { discoverSessionLogs, extractTurns } from '../src/memory/event-index.js';
17
+ import { readAutoCards, cardsPathOf } from '../src/memory/auto-cards.js';
18
+
19
+ function dshHome() {
20
+ const raw = process.env.DSH_HOME;
21
+ return typeof raw === 'string' && raw.trim() !== '' ? path.resolve(raw.trim()) : path.join(os.homedir(), '.dsh');
22
+ }
23
+
24
+ function defaultMemoryRoot() {
25
+ for (const envName of ['DSH_LONGLIVE_MEMORY_ROOT', 'DSH_MEM']) {
26
+ const value = process.env[envName];
27
+ if (typeof value === 'string' && value.trim() !== '') return path.resolve(value.trim());
28
+ }
29
+ return path.join(dshHome(), 'longlive-memory');
30
+ }
31
+
32
+ function parseArgs(argv) {
33
+ const out = { memoryRoot: '', sessionsRoot: '', sample: 0, json: false, out: '', allowStale: false };
34
+ for (let i = 0; i < argv.length; i += 1) {
35
+ const arg = argv[i];
36
+ const match = /^--([a-z-]+)(?:=(.*))?$/.exec(arg);
37
+ if (!match) continue;
38
+ const [, key, inline] = match;
39
+ const value = inline !== undefined ? inline : (argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[++i] : '');
40
+ if (key === 'memory-root') out.memoryRoot = value;
41
+ else if (key === 'sessions-root') out.sessionsRoot = value;
42
+ else if (key === 'sample') out.sample = Number.parseInt(value, 10) || 0;
43
+ else if (key === 'json') out.json = true;
44
+ else if (key === 'out') out.out = value;
45
+ else if (key === 'allow-stale') out.allowStale = true;
46
+ }
47
+ return out;
48
+ }
49
+
50
+ /** Discover session files without parsing them (avoid loading 900MB at once). */
51
+ function sessionFiles(sessionsRoot) {
52
+ const files = new Map();
53
+ for (const item of discoverSessionLogs([sessionsRoot])) files.set(item.sessionId, item.file);
54
+ return files;
55
+ }
56
+
57
+ export function verifyCards({ memoryRoot, sessionsRoot } = {}) {
58
+ const root = path.resolve(String(memoryRoot ?? ''));
59
+ const cards = readAutoCards(root);
60
+ const files = sessionFiles(path.resolve(String(sessionsRoot ?? '')));
61
+ const items = [];
62
+ const bySession = new Map();
63
+ for (const card of cards) {
64
+ const ids = [card.sessionId, card.anchor?.sessionId].filter(Boolean);
65
+ const sessionId = ids[0];
66
+ if (!bySession.has(sessionId)) bySession.set(sessionId, []);
67
+ bySession.get(sessionId).push(card);
68
+ }
69
+ for (const [sessionId, group] of bySession) {
70
+ let parsed = null;
71
+ const file = files.get(sessionId);
72
+ if (file) {
73
+ try { parsed = parseSession(fs.readFileSync(file)); } catch { parsed = null; }
74
+ }
75
+ // Use the same per-turn extraction as the index builder; turnWindowOf would
76
+ // include preamble events differently and produce false stale digests.
77
+ const records = new Map();
78
+ if (parsed) {
79
+ try {
80
+ for (const record of extractTurns(parsed.header, parsed.events, parsed.frames, parsed.texts)) {
81
+ records.set(record.turn, record);
82
+ }
83
+ } catch { /* unreadable session => missing below */ }
84
+ }
85
+ for (const card of group) {
86
+ const record = records.get(card.turn);
87
+ if (!record) {
88
+ items.push({
89
+ cardId: card.cardId, eventId: card.eventId, sessionId: card.sessionId, turn: card.turn,
90
+ status: 'missing', code: parsed ? 'E_TURN_MISSING' : 'E_SESSION_MISSING',
91
+ expectedDigest: card.anchor.digest, actualDigest: null,
92
+ });
93
+ continue;
94
+ }
95
+ const anchor = card.anchor;
96
+ const sameWindow = anchor.seqStart === record.seqStart && anchor.seqEnd === record.seqEnd;
97
+ const live = sameWindow && anchor.digest === record.digest;
98
+ items.push({
99
+ cardId: card.cardId, eventId: card.eventId, sessionId: card.sessionId, turn: card.turn,
100
+ status: live ? 'live' : 'stale',
101
+ code: live ? 'OK' : (sameWindow ? 'E_TURN_DIGEST_MISMATCH' : 'E_TURN_WINDOW_MISMATCH'),
102
+ expectedDigest: anchor.digest,
103
+ actualDigest: record.digest,
104
+ });
105
+ }
106
+ }
107
+ const counts = { total: items.length, live: 0, stale: 0, missing: 0 };
108
+ for (const item of items) counts[item.status] = (counts[item.status] ?? 0) + 1;
109
+ return { schema: 'card-anchors/v1', memoryRoot: root, cardsFile: cardsPathOf(root), sessions: files.size, counts, items };
110
+ }
111
+
112
+ const isMain = process.argv[1] && path.resolve(process.argv[1]) === path.resolve(new URL(import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, '$1'));
113
+ if (isMain) {
114
+ const args = parseArgs(process.argv.slice(2));
115
+ const memoryRoot = args.memoryRoot !== '' ? args.memoryRoot : defaultMemoryRoot();
116
+ const sessionsRoot = args.sessionsRoot !== '' ? args.sessionsRoot : path.join(dshHome(), 'sessions');
117
+ const report = verifyCards({ memoryRoot, sessionsRoot });
118
+ let items = report.items;
119
+ if (args.sample > 0 && items.length > args.sample) {
120
+ items = [...items].sort((a, b) => a.cardId.localeCompare(b.cardId)).slice(0, args.sample);
121
+ }
122
+ const output = { ...report, items, sampled: args.sample > 0 && args.sample < report.items.length };
123
+ const body = args.json || args.out !== '';
124
+ if (body) {
125
+ const text = JSON.stringify(output, null, 2);
126
+ if (args.json) console.log(text);
127
+ if (args.out !== '') {
128
+ fs.mkdirSync(path.dirname(path.resolve(args.out)), { recursive: true });
129
+ fs.writeFileSync(path.resolve(args.out), `${text}\n`, 'utf8');
130
+ }
131
+ } else {
132
+ console.log(`[verify-card-anchors] total=${output.counts.total} live=${output.counts.live} stale=${output.counts.stale} missing=${output.counts.missing}`);
133
+ for (const item of items.filter((x) => x.status !== 'live').slice(0, 20)) {
134
+ console.log(` ${item.status} ${item.cardId} ${item.sessionId}#${item.turn} ${item.code}`);
135
+ }
136
+ }
137
+ if ((output.counts.stale > 0 || output.counts.missing > 0) && !args.allowStale) process.exitCode = 1;
138
+ }
@@ -0,0 +1,125 @@
1
+ // src/actions/audit.js — HMAC 链式审计(M2-G / §12.4 / §12.7)
2
+ //
3
+ // 一行一个 JSON:
4
+ // {seq,ts,actorId,action,target,beforeHash,afterHash,prevHash,hmac}
5
+ // - seq 严格 +1(从 1 起),prevHash 链接上一行 hmac(首行 GENESIS);
6
+ // - hmac = HMAC-SHA256(auditKey, 规范化前 8 字段),auditKey **仅宿主内存**;
7
+ // - 逆序/断链/篡改 → E_AUDIT_BROKEN(校验失败,绝不静默);
8
+ // - actorId 只记哈希前缀,正文不记(隐私)。
9
+ // 审计 key 不落盘:跨重启延续审计链需要独立服务账号 + OS 凭据库(§13 第二层,1.6.9 不做)。
10
+
11
+ import path from 'node:path';
12
+
13
+ import { GENESIS_HASH, actionsDir, appendJsonl, hmacHexFor, readJsonl, sha256Hex, stableStringify } from './fs.js';
14
+
15
+ export const AUDIT_SCHEMA = 'audit/v1';
16
+ export const AUDIT_FILE = 'actions.log';
17
+
18
+ export class AuditError extends Error {
19
+ constructor(code, message, details = undefined) {
20
+ super(message);
21
+ this.name = 'AuditError';
22
+ this.code = code;
23
+ if (details !== undefined) this.details = details;
24
+ }
25
+ }
26
+
27
+ const AUDIT_FILE_NAME = AUDIT_FILE;
28
+
29
+ export function auditFilePath(memoryRoot) {
30
+ return path.join(actionsDir(memoryRoot), AUDIT_FILE_NAME);
31
+ }
32
+
33
+ /** Canonical string of the 8 signed fields (order fixed; target stringified stably). */
34
+ export function canonicalAuditBody(entry) {
35
+ return [
36
+ String(entry.seq),
37
+ String(entry.ts),
38
+ String(entry.actorId),
39
+ String(entry.action),
40
+ stableStringify(entry.target ?? null),
41
+ String(entry.beforeHash ?? ''),
42
+ String(entry.afterHash ?? ''),
43
+ String(entry.prevHash ?? ''),
44
+ ].join('\n');
45
+ }
46
+
47
+ /** Host-only actor identity: hashed prefix only, never the raw id. */
48
+ export function hashActorId(raw) {
49
+ return `host:${sha256Hex(String(raw ?? 'host')).slice(0, 8)}`;
50
+ }
51
+
52
+ const hmacFor = hmacHexFor;
53
+
54
+ /**
55
+ * Create the audit chain writer/verifier.
56
+ *
57
+ * @param {object} options
58
+ * @param {string} options.memoryRoot
59
+ * @param {Buffer|string} options.auditKey host-memory key (32B Buffer preferred)
60
+ * @param {() => Date} [options.now]
61
+ * @returns {{append:Function, readAll:Function, verify:Function, latest:Function, file:string}}
62
+ */
63
+ export function createAuditLog(options = {}) {
64
+ const memoryRoot = String(options.memoryRoot ?? '');
65
+ const auditKey = options.auditKey;
66
+ const now = typeof options.now === 'function' ? options.now : () => new Date();
67
+ const file = auditFilePath(memoryRoot);
68
+
69
+ function readAll() {
70
+ const read = readJsonl(file);
71
+ return { rows: Array.isArray(read.rows) ? read.rows : [], bad: read.bad ?? 0, exists: read.exists === true };
72
+ }
73
+
74
+ function verify(rows = readAll().rows) {
75
+ let prevHash = GENESIS_HASH;
76
+ let prevSeq = 0;
77
+ for (let index = 0; index < rows.length; index += 1) {
78
+ const entry = rows[index];
79
+ if (entry === null || typeof entry !== 'object') {
80
+ return { ok: false, code: 'E_AUDIT_BROKEN', at: index, reason: 'not-an-object' };
81
+ }
82
+ if (entry.seq !== prevSeq + 1) {
83
+ return { ok: false, code: 'E_AUDIT_BROKEN', at: index, reason: `seq ${entry.seq} != ${prevSeq + 1}` };
84
+ }
85
+ if (entry.prevHash !== undefined && entry.prevHash !== prevHash) {
86
+ return { ok: false, code: 'E_AUDIT_BROKEN', at: index, reason: 'prevHash mismatch' };
87
+ }
88
+ const expected = hmacFor(auditKey, canonicalAuditBody(entry));
89
+ if (entry.hmac !== expected) {
90
+ return { ok: false, code: 'E_AUDIT_BROKEN', at: index, reason: 'hmac mismatch' };
91
+ }
92
+ prevHash = entry.hmac;
93
+ prevSeq = entry.seq;
94
+ }
95
+ return { ok: true, rows: rows.length, lastSeq: prevSeq, lastHash: prevHash };
96
+ }
97
+
98
+ function append({ actorId = 'host', action, target = null, beforeHash = '', afterHash = '' } = {}) {
99
+ const current = readAll();
100
+ const checked = verify(current.rows);
101
+ if (!checked.ok) throw new AuditError('E_AUDIT_BROKEN', '审计链已损坏:拒绝继续追加。', checked);
102
+ const seq = (current.rows.length > 0 ? current.rows[current.rows.length - 1].seq : 0) + 1;
103
+ const prevHash = current.rows.length > 0 ? current.rows[current.rows.length - 1].hmac : GENESIS_HASH;
104
+ const entry = {
105
+ seq,
106
+ ts: now().toISOString(),
107
+ actorId: String(actorId),
108
+ action: String(action),
109
+ target,
110
+ beforeHash: String(beforeHash ?? ''),
111
+ afterHash: String(afterHash ?? ''),
112
+ prevHash,
113
+ };
114
+ entry.hmac = hmacFor(auditKey, canonicalAuditBody(entry));
115
+ const written = appendJsonl(file, entry);
116
+ return { ok: true, seq, entry, file, offset: written.offset };
117
+ }
118
+
119
+ function latest() {
120
+ const { rows } = readAll();
121
+ return rows.length > 0 ? rows[rows.length - 1] : null;
122
+ }
123
+
124
+ return { file, readAll, verify, append, latest };
125
+ }
@@ -0,0 +1,122 @@
1
+ // src/actions/evidence.js — evidence 私有写路径 + HMAC 验签(N38 三写 probe 的靶子)
2
+ //
3
+ // evidence 包只由宿主写:写盘前用**仅宿主内存**的 auditKey 签名,原子写
4
+ // `<memoryRoot>/.longlive/evidence/latest.json`,并把 `packageSha256` 一起落盘供复核。
5
+ // 模型/子脑没有 token、没有 auditKey:
6
+ // - 走动作门 → 401 `E_NO_CAP`(见 panel-auth.js);
7
+ // - 伪造证据(自己拼 JSON + 假签名)→ 验签失败 `E_HMAC_INVALID`,不落盘、不覆盖真证据。
8
+ // 本模块不读取任何凭据文件,也不把 key 写进返回值。
9
+
10
+ import path from 'node:path';
11
+
12
+ import { hmacHexFor, readJsonFile, sha256Hex, stableStringify, writeJsonAtomic } from './fs.js';
13
+
14
+ export const EVIDENCE_SCHEMA = 'evidence-package/v1';
15
+ export const EVIDENCE_DIR = 'evidence';
16
+ export const EVIDENCE_FILE = 'latest.json';
17
+
18
+ export class EvidenceError extends Error {
19
+ constructor(code, message, status = 403, details = undefined) {
20
+ super(message);
21
+ this.name = 'EvidenceError';
22
+ this.code = code;
23
+ this.status = status;
24
+ if (details !== undefined) this.details = details;
25
+ }
26
+ }
27
+
28
+ export function evidenceFilePath(memoryRoot) {
29
+ return path.join(String(memoryRoot ?? ''), '.longlive', EVIDENCE_DIR, EVIDENCE_FILE);
30
+ }
31
+
32
+ function canonicalEvidencePayload(pkg) {
33
+ return stableStringify(pkg ?? null);
34
+ }
35
+
36
+ /**
37
+ * Create the host-side evidence writer.
38
+ *
39
+ * @param {object} options
40
+ * @param {string} options.memoryRoot
41
+ * @param {Buffer|string} options.auditKey host-memory signing key (never serialized)
42
+ * @param {() => Date} [options.now]
43
+ */
44
+ export function createEvidenceWriter(options = {}) {
45
+ const memoryRoot = String(options.memoryRoot ?? '');
46
+ const auditKey = options.auditKey;
47
+ const now = typeof options.now === 'function' ? options.now : () => new Date();
48
+ const file = evidenceFilePath(memoryRoot);
49
+
50
+ if (!auditKey) throw new EvidenceError('E_NO_CAP', '缺少宿主签名 key:evidence 只允许宿主进程写。', 401);
51
+
52
+ function sign(pkg) {
53
+ return hmacHexFor(auditKey, canonicalEvidencePayload(pkg));
54
+ }
55
+
56
+ function persist(pkg, signature, source) {
57
+ const packageSha256 = sha256Hex(canonicalEvidencePayload(pkg));
58
+ const record = {
59
+ schemaVersion: EVIDENCE_SCHEMA,
60
+ package: pkg,
61
+ packageSha256,
62
+ signature,
63
+ signatureSource: source,
64
+ writtenAt: now().toISOString(),
65
+ };
66
+ writeJsonAtomic(file, record);
67
+ return {
68
+ ok: true,
69
+ path: file,
70
+ packageSha256,
71
+ fileSha256: sha256Hex(JSON.stringify(record)),
72
+ };
73
+ }
74
+
75
+ /** Host path: sign with the in-memory key, then write (N38 host probe). */
76
+ function writeHost(pkg) {
77
+ if (pkg === null || typeof pkg !== 'object') {
78
+ throw new EvidenceError('E_EVIDENCE_INVALID', 'evidence 包必须是对象。', 422);
79
+ }
80
+ return persist(pkg, sign(pkg), 'host');
81
+ }
82
+
83
+ /**
84
+ * Any caller may submit a package + signature, but only a host-signed one
85
+ * verifies. A forged signature fails closed with `E_HMAC_INVALID`.
86
+ */
87
+ function submitSigned({ pkg, signature } = {}) {
88
+ if (pkg === null || typeof pkg !== 'object') {
89
+ throw new EvidenceError('E_EVIDENCE_INVALID', 'evidence 包必须是对象。', 422);
90
+ }
91
+ if (typeof signature !== 'string' || !/^[0-9a-f]{64}$/i.test(signature)) {
92
+ throw new EvidenceError('E_HMAC_INVALID', 'evidence 签名缺失或格式非法:拒绝写入。', 403);
93
+ }
94
+ const expected = sign(pkg);
95
+ if (signature.toLowerCase() !== expected) {
96
+ throw new EvidenceError('E_HMAC_INVALID', 'evidence 验签失败:拒绝写入(真证据未被覆盖)。', 403);
97
+ }
98
+ return persist(pkg, expected, 'host');
99
+ }
100
+
101
+ /** Re-read + re-verify the on-disk evidence (used by probe/action returns). */
102
+ function readLatest() {
103
+ const read = readJsonFile(file);
104
+ if (!read.ok) return { ok: false, present: false, code: read.code };
105
+ const record = read.value;
106
+ const packageSha256 = record?.package === undefined ? null : sha256Hex(canonicalEvidencePayload(record.package));
107
+ const signatureValid = typeof record?.signature === 'string'
108
+ && record.signature === sign(record.package);
109
+ return {
110
+ ok: signatureValid && packageSha256 === record.packageSha256,
111
+ present: true,
112
+ schemaVersion: record?.schemaVersion ?? null,
113
+ package: record?.package ?? null,
114
+ packageSha256,
115
+ recordedSha256: record?.packageSha256 ?? null,
116
+ signatureValid,
117
+ writtenAt: record?.writtenAt ?? null,
118
+ };
119
+ }
120
+
121
+ return { file, writeHost, submitSigned, readLatest, signPackage: sign };
122
+ }
@@ -0,0 +1,162 @@
1
+ // src/actions/fs.js — 动作层私有文件原语(1.6.9 Gate-1)
2
+ //
3
+ // 动作/审计/证据只写宿主私有目录:`<memoryRoot>/.longlive/{actions,evidence,distill,protected.json,...}`。
4
+ // 全部写入走 tmp + fsync + 读回校验 + rename;读取对坏行只计数不抛错。
5
+ // 不依赖任何第三方包,不联网。
6
+
7
+ import crypto from 'node:crypto';
8
+ import fs from 'node:fs';
9
+ import path from 'node:path';
10
+
11
+ export const ACTIONS_DIR = 'actions';
12
+ export const GENESIS_HASH = '0'.repeat(64);
13
+
14
+ export function sha256Hex(value) {
15
+ return crypto.createHash('sha256').update(typeof value === 'string' ? value : Buffer.from(value)).digest('hex');
16
+ }
17
+
18
+ /** HMAC-SHA256 hex. Used by the audit chain and the evidence signature. */
19
+ export function hmacHexFor(key, text) {
20
+ return crypto.createHmac('sha256', key).update(String(text), 'utf8').digest('hex');
21
+ }
22
+
23
+ export function stableStringify(value) {
24
+ if (value === null || typeof value !== 'object') return JSON.stringify(value);
25
+ if (Array.isArray(value)) return `[${value.map((item) => stableStringify(item)).join(',')}]`;
26
+ const keys = Object.keys(value).sort();
27
+ return `{${keys.map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(',')}}`;
28
+ }
29
+
30
+ export function ensureDir(dir) {
31
+ fs.mkdirSync(dir, { recursive: true });
32
+ }
33
+
34
+ /** Atomic JSON write (tmp + fsync + read-back + rename). */
35
+ export function writeJsonAtomic(file, value) {
36
+ const dir = path.dirname(file);
37
+ ensureDir(dir);
38
+ const text = `${JSON.stringify(value, null, 2)}\n`;
39
+ const tmp = path.join(dir, `.${path.basename(file)}.${process.pid}.${crypto.randomBytes(4).toString('hex')}.tmp`);
40
+ const fd = fs.openSync(tmp, 'w');
41
+ try {
42
+ fs.writeFileSync(fd, text, 'utf8');
43
+ try {
44
+ fs.fsyncSync(fd);
45
+ } catch {
46
+ /* best effort on some filesystems */
47
+ }
48
+ } finally {
49
+ try {
50
+ fs.closeSync(fd);
51
+ } catch {
52
+ /* ignore */
53
+ }
54
+ }
55
+ const readBack = fs.readFileSync(tmp, 'utf8');
56
+ if (readBack !== text) {
57
+ try {
58
+ fs.rmSync(tmp, { force: true });
59
+ } catch {
60
+ /* ignore */
61
+ }
62
+ throw new Error('E_WRITE_READBACK');
63
+ }
64
+ fs.renameSync(tmp, file);
65
+ return { file, bytes: Buffer.byteLength(text, 'utf8') };
66
+ }
67
+
68
+ export function readJsonFile(file) {
69
+ try {
70
+ return { ok: true, value: JSON.parse(fs.readFileSync(file, 'utf8')) };
71
+ } catch (error) {
72
+ return { ok: false, code: error?.code === 'ENOENT' ? 'ENOENT' : (error?.code ?? 'E_READ') };
73
+ }
74
+ }
75
+
76
+ /** Append one JSON line, fsynced. Returns the byte offset before the append. */
77
+ export function appendJsonl(file, value) {
78
+ ensureDir(path.dirname(file));
79
+ const text = `${JSON.stringify(value)}\n`;
80
+ const before = fs.existsSync(file) ? fs.statSync(file).size : 0;
81
+ const fd = fs.openSync(file, 'a');
82
+ try {
83
+ fs.writeFileSync(fd, text, 'utf8');
84
+ try {
85
+ fs.fsyncSync(fd);
86
+ } catch {
87
+ /* best effort */
88
+ }
89
+ } finally {
90
+ try {
91
+ fs.closeSync(fd);
92
+ } catch {
93
+ /* ignore */
94
+ }
95
+ }
96
+ return { file, bytes: Buffer.byteLength(text, 'utf8'), offset: before };
97
+ }
98
+
99
+ /** Read JSONL defensively: `{rows, bad, exists}`; never throws on bad lines. */
100
+ export function readJsonl(file) {
101
+ let text = '';
102
+ try {
103
+ text = fs.readFileSync(file, 'utf8');
104
+ } catch (error) {
105
+ return { rows: [], bad: 0, exists: false, code: error?.code === 'ENOENT' ? 'ENOENT' : (error?.code ?? 'E_READ') };
106
+ }
107
+ const rows = [];
108
+ let bad = 0;
109
+ for (const line of text.replace(/^\uFEFF/, '').split(/\r?\n/)) {
110
+ if (line.trim() === '') continue;
111
+ try {
112
+ rows.push(JSON.parse(line));
113
+ } catch {
114
+ bad += 1;
115
+ }
116
+ }
117
+ return { rows, bad, exists: true };
118
+ }
119
+
120
+ /** Atomic JSONL rewrite (tmp + read-back + rename). */
121
+ export function writeJsonlAtomic(file, rows) {
122
+ const dir = path.dirname(file);
123
+ ensureDir(dir);
124
+ const text = Array.isArray(rows) && rows.length > 0 ? `${rows.map((row) => JSON.stringify(row)).join('\n')}\n` : '';
125
+ const tmp = path.join(dir, `.${path.basename(file)}.${process.pid}.${crypto.randomBytes(4).toString('hex')}.tmp`);
126
+ const fd = fs.openSync(tmp, 'w');
127
+ try {
128
+ fs.writeFileSync(fd, text, 'utf8');
129
+ try {
130
+ fs.fsyncSync(fd);
131
+ } catch {
132
+ /* best effort */
133
+ }
134
+ } finally {
135
+ try {
136
+ fs.closeSync(fd);
137
+ } catch {
138
+ /* ignore */
139
+ }
140
+ }
141
+ const readBack = fs.readFileSync(tmp, 'utf8');
142
+ if (readBack !== text) {
143
+ try {
144
+ fs.rmSync(tmp, { force: true });
145
+ } catch {
146
+ /* ignore */
147
+ }
148
+ throw new Error('E_WRITE_READBACK');
149
+ }
150
+ fs.renameSync(tmp, file);
151
+ return { file, bytes: Buffer.byteLength(text, 'utf8') };
152
+ }
153
+
154
+ export function newId(prefix) {
155
+ const stamp = new Date().toISOString().replace(/[-:TZ.]/g, '').slice(0, 14);
156
+ return `${prefix}_${stamp}_${crypto.randomBytes(3).toString('hex')}`;
157
+ }
158
+
159
+ /** Directory of this memory root's private action state. */
160
+ export function actionsDir(memoryRoot) {
161
+ return path.join(String(memoryRoot ?? ''), '.longlive', ACTIONS_DIR);
162
+ }