@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.
Files changed (289) hide show
  1. package/CHANGELOG.md +70 -1508
  2. package/LICENSE +1 -1
  3. package/QUICKSTART.md +34 -13
  4. package/README.en.md +18 -7
  5. package/README.md +34 -9
  6. package/SPEC.md +5 -5
  7. package/cordis.patch.yml +23 -9
  8. package/docs/ANSWER-GUARD.md +92 -0
  9. package/docs/ARCHITECTURE.md +4 -3
  10. package/docs/AUTO-RECALL-WIRING.md +1 -1
  11. package/docs/AUTO-RECALL.md +15 -9
  12. package/docs/CONFIG.md +168 -35
  13. package/docs/ERRORS.md +17 -5
  14. package/docs/FAQ.md +7 -7
  15. package/docs/FEATURES.md +96 -28
  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 +4 -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 +47 -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 +760 -0
  41. package/src/activity/model.js +351 -0
  42. package/src/activity/store.js +644 -0
  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 +171 -47
  58. package/src/deps.js +6 -4
  59. package/src/features.js +182 -6
  60. package/src/heartbeat.js +156 -23
  61. package/src/identity/handover.js +52 -1
  62. package/src/identity/index.js +13 -5
  63. package/src/identity/inject-position.js +2 -2
  64. package/src/index.js +97 -7
  65. package/src/lifecycle/gap-clocks.js +328 -0
  66. package/src/lifecycle/index.js +269 -0
  67. package/src/live.js +22 -2
  68. package/src/memory/activity-capture.js +229 -0
  69. package/src/memory/anchors.js +194 -0
  70. package/src/memory/audit-records.js +222 -0
  71. package/src/memory/auto-cards.js +401 -0
  72. package/src/memory/capture.js +442 -0
  73. package/src/memory/card-anchors.js +203 -0
  74. package/src/memory/conflicts.js +106 -0
  75. package/src/memory/decay.js +311 -0
  76. package/src/memory/distill.js +471 -0
  77. package/src/memory/event-index.js +917 -0
  78. package/src/memory/evidence-chain.js +295 -0
  79. package/src/memory/evidence.js +145 -0
  80. package/src/memory/filter-runner.js +1 -1
  81. package/src/memory/filter.js +5 -1
  82. package/src/memory/forget-v2.js +410 -0
  83. package/src/memory/forget.js +71 -0
  84. package/src/memory/frames-activation.js +247 -0
  85. package/src/memory/fts-query.js +174 -0
  86. package/src/memory/governance.js +604 -0
  87. package/src/memory/index.js +1 -0
  88. package/src/memory/l2-gate.js +169 -0
  89. package/src/memory/lock.js +121 -0
  90. package/src/memory/migrate.js +139 -0
  91. package/src/memory/recall-v2.js +223 -0
  92. package/src/memory/recall.js +65 -2
  93. package/src/memory/records.js +425 -0
  94. package/src/memory/retention.js +128 -0
  95. package/src/memory/review.js +124 -0
  96. package/src/memory/trust.js +88 -0
  97. package/src/memory/version-chain.js +264 -0
  98. package/src/memory/wal.js +137 -0
  99. package/src/memory/working-frame.js +516 -0
  100. package/src/metrics/classify.mjs +5 -3
  101. package/src/metrics/index.mjs +21 -1
  102. package/src/metrics/prestep-latency.mjs +300 -0
  103. package/src/metrics/store.mjs +25 -0
  104. package/src/policy-profile.js +18 -3
  105. package/src/recall/anaphora.mjs +87 -0
  106. package/src/recall/cards.mjs +7 -0
  107. package/src/recall/embedding.mjs +8 -8
  108. package/src/recall/entities.mjs +112 -0
  109. package/src/recall/index.js +235 -19
  110. package/src/recall/inject-order.mjs +10 -8
  111. package/src/recall/privacy.mjs +105 -16
  112. package/src/recall/recall-core.mjs +116 -10
  113. package/src/recall/shared-visibility.js +302 -0
  114. package/src/recall/time-normalizer.mjs +196 -0
  115. package/src/role/activation.js +191 -0
  116. package/src/role/attention.js +318 -0
  117. package/src/role/base.js +36 -0
  118. package/src/role/channels.js +423 -0
  119. package/src/role/index.js +425 -0
  120. package/src/role/interfaces.js +115 -0
  121. package/src/role/personality.js +370 -0
  122. package/src/role/readers.js +151 -0
  123. package/src/role/relationships.js +477 -0
  124. package/src/role/store.js +202 -0
  125. package/src/role/writeprotect.js +373 -0
  126. package/src/router/aliases.json +176 -25
  127. package/src/router/background/agent-runner.mjs +1 -1
  128. package/src/router/background/anchor.mjs +1 -1
  129. package/src/router/background/audit.mjs +2 -2
  130. package/src/router/background/budget-ledger.mjs +2 -2
  131. package/src/router/background/host-sim.mjs +1 -1
  132. package/src/router/background/memory.mjs +4 -4
  133. package/src/router/background/queue.mjs +1 -1
  134. package/src/router/background/retry.mjs +1 -1
  135. package/src/router/background/router.mjs +1 -1
  136. package/src/router/background/stream.mjs +1 -1
  137. package/src/router/background/tool-policy.mjs +1 -1
  138. package/src/router/design-headings.json +5 -5
  139. package/src/router/face/config.mjs +1 -1
  140. package/src/router/face/identity.mjs +3 -3
  141. package/src/router/judge.mjs +8 -7
  142. package/src/router/policies/generic.json +21 -4
  143. package/src/router/route.js +109 -22
  144. package/src/router/router.mjs +10 -6
  145. package/src/router/routing-rules.json +260 -445
  146. package/src/router/run-sim.mjs +8 -8
  147. package/src/router/runtime.js +23 -12
  148. package/src/router/service/board.mjs +95 -15
  149. package/src/router/service/config.mjs +5 -5
  150. package/src/router/service/context.mjs +1 -1
  151. package/src/router/service/hint.mjs +14 -0
  152. package/src/router/service/queue.mjs +1 -1
  153. package/src/router/service/service.mjs +27 -11
  154. package/src/router/shadow.mjs +4 -3
  155. package/src/surgery-recovery.js +2 -2
  156. package/src/tool-output.js +2 -2
  157. package/src/tools.js +39 -7
  158. package/src/truncate-visible.js +2 -2
  159. package/src/ui/actions.js +1312 -0
  160. package/src/ui/panel-api.js +367 -0
  161. package/src/ui/panel-auth.js +251 -0
  162. package/src/ui/panel-integrity.js +243 -0
  163. package/src/ui/routes-readonly.js +695 -0
  164. package/src/ui/routes.js +3 -0
  165. package/docs/DEV.md +0 -279
  166. package/docs/PRESENCE-BUS-WIRING.md +0 -491
  167. package/docs/PRESENCE-BUS.md +0 -694
  168. package/docs/SURGERY.md +0 -186
  169. package/scripts/mark-core-cards.mjs +0 -289
  170. package/scripts/pack.mjs +0 -392
  171. package/scripts/router-proposals.mjs +0 -386
  172. package/src/router/fixtures/M1.json +0 -16
  173. package/src/router/fixtures/M2.json +0 -16
  174. package/src/router/fixtures/M3.json +0 -20
  175. package/src/router/fixtures/M4.json +0 -20
  176. package/src/router/fixtures/M5.json +0 -45
  177. package/src/router/fixtures/M6.json +0 -53
  178. package/src/router/fixtures/M7.json +0 -46
  179. package/src/router/fixtures/S1.json +0 -20
  180. package/src/router/fixtures/S2.json +0 -23
  181. package/src/router/fixtures/S3.json +0 -20
  182. package/src/router/fixtures/S4.json +0 -34
  183. package/src/router/fixtures/S5.json +0 -43
  184. package/src/router/fixtures/S6.json +0 -47
  185. package/src/router/fixtures/T1.json +0 -23
  186. package/src/router/fixtures/T2.json +0 -16
  187. package/src/router/fixtures/T3.json +0 -19
  188. package/src/router/fixtures/T4.json +0 -16
  189. package/src/router/fixtures/X1.json +0 -17
  190. package/src/router/fixtures/X2.json +0 -20
  191. package/src/router/gen-design-headings.mjs +0 -107
  192. package/src/router/policies/owner-rulings-2026-11-03.json +0 -62
  193. package/test/auto-recall.test.mjs +0 -474
  194. package/test/auto-truncate.test.mjs +0 -931
  195. package/test/autobind.test.mjs +0 -385
  196. package/test/bus.test.mjs +0 -1572
  197. package/test/cli-guard.test.mjs +0 -198
  198. package/test/conclusion.test.mjs +0 -408
  199. package/test/config-schema.test.mjs +0 -204
  200. package/test/d6-output.test.mjs +0 -194
  201. package/test/d6.test.mjs +0 -395
  202. package/test/deps.test.mjs +0 -342
  203. package/test/doctor-setup.test.mjs +0 -297
  204. package/test/embedding-recall.test.mjs +0 -347
  205. package/test/feature-flags.test.mjs +0 -568
  206. package/test/filter-shadow.test.mjs +0 -170
  207. package/test/fixtures/baseline-0.8.0-registration.json +0 -556
  208. package/test/fixtures/d6-header-tools.json +0 -1
  209. package/test/fixtures/lock-child.mjs +0 -67
  210. package/test/fixtures/pre-step-container.mjs +0 -100
  211. package/test/fixtures/privacy-memory/topics/pii-unmarked.md +0 -9
  212. package/test/fixtures/privacy-memory/topics/secret-local-only.md +0 -11
  213. package/test/fixtures/profile-session-longlive-config.json +0 -20
  214. package/test/fixtures/proposal-cas-child.mjs +0 -66
  215. package/test/host-engine-status.test.mjs +0 -307
  216. package/test/hot-truncate.test.mjs +0 -1349
  217. package/test/identity-card-marker.test.mjs +0 -307
  218. package/test/identity-router/run.mjs +0 -721
  219. package/test/integration-pre-step.test.mjs +0 -553
  220. package/test/live.test.mjs +0 -1020
  221. package/test/maintenance-schedule.test.mjs +0 -538
  222. package/test/memory.test.mjs +0 -2828
  223. package/test/metrics.test.mjs +0 -478
  224. package/test/orchestrator.test.mjs +0 -266
  225. package/test/p1_probe.mjs +0 -97
  226. package/test/packaging.test.mjs +0 -182
  227. package/test/portability.test.mjs +0 -343
  228. package/test/rc4-regressions.test.mjs +0 -562
  229. package/test/rc5-regressions.test.mjs +0 -197
  230. package/test/rc6-chinese.test.mjs +0 -394
  231. package/test/router-1100/apply-off.test.mjs +0 -175
  232. package/test/router-1100/budget-reconcile.test.mjs +0 -215
  233. package/test/router-1100/config-context.test.mjs +0 -211
  234. package/test/router-1100/helpers.mjs +0 -129
  235. package/test/router-1100/hint-envelope.test.mjs +0 -150
  236. package/test/router-1100/lock-crossproc.test.mjs +0 -206
  237. package/test/router-1100/proposal-cas-crossproc.test.mjs +0 -184
  238. package/test/router-1100/router-apply.test.mjs +0 -372
  239. package/test/router-1100/router-fixb.test.mjs +0 -602
  240. package/test/router-1100/router-proposals.test.mjs +0 -405
  241. package/test/router-1100/router-queue-lease-wal.test.mjs +0 -355
  242. package/test/router-1100/router-security-cr.test.mjs +0 -333
  243. package/test/router-1100/service-lifecycle.test.mjs +0 -220
  244. package/test/router-1100/tools-contract.test.mjs +0 -164
  245. package/test/router-1100/turn-reflect-tick.test.mjs +0 -89
  246. package/test/router-1100/v6-contract.test.mjs +0 -158
  247. package/test/router-1100/v6b-contract.test.mjs +0 -293
  248. package/test/router-1100/v6c-contract.test.mjs +0 -249
  249. package/test/router-1100/wal-rotation.test.mjs +0 -375
  250. package/test/router-1100/wp1-config.test.mjs +0 -170
  251. package/test/router-1100/wp1-default-off.test.mjs +0 -77
  252. package/test/router-1100/wp1-model-status.test.mjs +0 -90
  253. package/test/router-1100/wp1-status.test.mjs +0 -220
  254. package/test/router-1100/wp1-tools.test.mjs +0 -146
  255. package/test/router-1100/wp5-cli.test.mjs +0 -240
  256. package/test/router-1100/wp5-model-auto.test.mjs +0 -418
  257. package/test/router-1100/wp6-merge.test.mjs +0 -386
  258. package/test/router-1100/wp6-wake.test.mjs +0 -502
  259. package/test/router-background/audit.test.mjs +0 -37
  260. package/test/router-background/budget-ledger.test.mjs +0 -330
  261. package/test/router-background/cross-process.test.mjs +0 -222
  262. package/test/router-background/fixtures/cross-child.mjs +0 -85
  263. package/test/router-background/fixtures/real-shapes.mjs +0 -239
  264. package/test/router-background/router.test.mjs +0 -1044
  265. package/test/router-background/stream-retry.test.mjs +0 -358
  266. package/test/router-background/tool-agent.test.mjs +0 -384
  267. package/test/router-face/bridge.test.mjs +0 -169
  268. package/test/router-face/face.test.mjs +0 -316
  269. package/test/router-failclosed.test.mjs +0 -466
  270. package/test/router-identity.test.mjs +0 -384
  271. package/test/router-l1-l2.contract.test.mjs +0 -1075
  272. package/test/router-memory-tools.test.mjs +0 -262
  273. package/test/router-policy.test.mjs +0 -225
  274. package/test/router-runtime.test.mjs +0 -438
  275. package/test/router-shadow.test.mjs +0 -287
  276. package/test/router.test.mjs +0 -903
  277. package/test/run-rc.mjs +0 -299
  278. package/test/run.mjs +0 -2469
  279. package/test/safety-defaults.test.mjs +0 -179
  280. package/test/seeded-load.test.mjs +0 -436
  281. package/test/session-original.test.mjs +0 -338
  282. package/test/surgery-recovery.test.mjs +0 -936
  283. package/test/t7_drive.mjs +0 -151
  284. package/test/tool-output.test.mjs +0 -393
  285. package/test/tool-schema.test.mjs +0 -177
  286. package/test/tools_contract.test.mjs +0 -716
  287. package/test/truncate-visible.test.mjs +0 -1150
  288. package/test/ui-flags.test.mjs +0 -477
  289. package/test/v4-hardening.test.mjs +0 -427
@@ -20,7 +20,7 @@
20
20
  // anything and never touches Task Scheduler.
21
21
  //
22
22
  // node scripts/session-watch-drill.mjs
23
- // node scripts/session-watch-drill.mjs --workdir C:\temp\my-drill --clean
23
+ // node scripts/session-watch-drill.mjs --workdir <drill-dir> --clean
24
24
  //
25
25
  // stdout is one JSON document; the same document is written to
26
26
  // <workdir>/drill-result.json. Exit 0 = every check passed, 1 = a check failed,
@@ -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
+ }