@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.
- package/CHANGELOG.md +69 -1539
- package/LICENSE +1 -1
- package/QUICKSTART.md +33 -12
- package/README.en.md +17 -7
- package/README.md +29 -10
- package/SPEC.md +5 -5
- package/cordis.patch.yml +17 -12
- package/docs/ANSWER-GUARD.md +92 -0
- package/docs/ARCHITECTURE.md +3 -3
- package/docs/AUTO-RECALL-WIRING.md +1 -1
- package/docs/AUTO-RECALL.md +15 -9
- package/docs/CONFIG.md +143 -37
- package/docs/ERRORS.md +17 -5
- package/docs/FAQ.md +7 -7
- package/docs/FEATURES.md +66 -29
- package/docs/MIGRATION-1.6.0.md +2 -2
- package/docs/OPERATIONS.md +6 -6
- package/docs/PANEL-INTEGRATION.md +6 -0
- package/docs/PRIVACY.md +88 -0
- package/docs/QUICKSTART.md +5 -5
- package/docs/README.en.md +3 -2
- package/docs/RESIDENT-WATCHER.md +1 -1
- package/docs/SESSION-WATCH.md +6 -6
- package/docs/TROUBLESHOOTING.md +4 -4
- package/package.json +42 -117
- package/runbook.md +2 -2
- package/scripts/backfill-auto-cards.mjs +98 -0
- package/scripts/install-surgery-task.ps1 +5 -2
- package/scripts/lib/session-watch-fixture.mjs +1 -1
- package/scripts/longlive-doctor.mjs +6 -0
- package/scripts/migrate-180.mjs +163 -0
- package/scripts/recall-index-build.mjs +128 -0
- package/scripts/session-watch-drill.mjs +1 -1
- package/scripts/surgery-run.cmd +1 -1
- package/scripts/unattended-surgery.ps1 +5 -2
- package/scripts/verify-card-anchors.mjs +138 -0
- package/src/actions/audit.js +125 -0
- package/src/actions/evidence.js +122 -0
- package/src/actions/fs.js +162 -0
- package/src/activity/index.js +129 -18
- package/src/activity/model.js +11 -1
- package/src/activity/store.js +22 -3
- package/src/activity/thinking.js +176 -0
- package/src/adapters/constants.js +11 -0
- package/src/adapters/engram/data/aliases.json +51 -0
- package/src/adapters/engram/data/policy.json +8 -0
- package/src/adapters/engram/data/routing-rules.json +546 -0
- package/src/adapters/engram/index.js +235 -0
- package/src/adapters/registry.js +315 -0
- package/src/answer/audit.js +193 -0
- package/src/answer/evidence.js +30 -0
- package/src/answer/guard.js +1370 -0
- package/src/answer/index.js +1067 -0
- package/src/answer/query.js +111 -0
- package/src/answer/second-hop.mjs +101 -0
- package/src/auto-truncate.js +1 -1
- package/src/config.js +145 -48
- package/src/deps.js +6 -4
- package/src/features.js +158 -8
- package/src/identity/index.js +13 -5
- package/src/identity/inject-position.js +2 -2
- package/src/index.js +97 -7
- package/src/lifecycle/gap-clocks.js +328 -0
- package/src/lifecycle/index.js +269 -0
- package/src/memory/activity-capture.js +229 -0
- package/src/memory/anchors.js +194 -0
- package/src/memory/audit-records.js +222 -0
- package/src/memory/auto-cards.js +401 -0
- package/src/memory/capture.js +442 -0
- package/src/memory/card-anchors.js +203 -0
- package/src/memory/conflicts.js +106 -0
- package/src/memory/decay.js +311 -0
- package/src/memory/distill.js +471 -0
- package/src/memory/event-index.js +917 -0
- package/src/memory/evidence-chain.js +295 -0
- package/src/memory/evidence.js +145 -0
- package/src/memory/filter-runner.js +1 -1
- package/src/memory/filter.js +5 -1
- package/src/memory/forget-v2.js +410 -0
- package/src/memory/forget.js +71 -0
- package/src/memory/frames-activation.js +247 -0
- package/src/memory/fts-query.js +174 -0
- package/src/memory/governance.js +604 -0
- package/src/memory/index.js +1 -0
- package/src/memory/l2-gate.js +169 -0
- package/src/memory/lock.js +121 -0
- package/src/memory/migrate.js +139 -0
- package/src/memory/recall-v2.js +223 -0
- package/src/memory/recall.js +65 -2
- package/src/memory/records.js +425 -0
- package/src/memory/retention.js +128 -0
- package/src/memory/review.js +124 -0
- package/src/memory/trust.js +88 -0
- package/src/memory/version-chain.js +264 -0
- package/src/memory/wal.js +137 -0
- package/src/memory/working-frame.js +516 -0
- package/src/metrics/classify.mjs +5 -3
- package/src/metrics/index.mjs +21 -1
- package/src/metrics/prestep-latency.mjs +300 -0
- package/src/metrics/store.mjs +25 -0
- package/src/policy-profile.js +18 -3
- package/src/recall/anaphora.mjs +87 -0
- package/src/recall/cards.mjs +7 -0
- package/src/recall/embedding.mjs +8 -8
- package/src/recall/entities.mjs +112 -0
- package/src/recall/index.js +235 -19
- package/src/recall/inject-order.mjs +10 -8
- package/src/recall/privacy.mjs +1 -1
- package/src/recall/recall-core.mjs +116 -10
- package/src/recall/shared-visibility.js +302 -0
- package/src/recall/time-normalizer.mjs +196 -0
- package/src/role/activation.js +191 -0
- package/src/role/attention.js +318 -0
- package/src/role/base.js +36 -0
- package/src/role/channels.js +423 -0
- package/src/role/index.js +425 -0
- package/src/role/interfaces.js +115 -0
- package/src/role/personality.js +370 -0
- package/src/role/readers.js +151 -0
- package/src/role/relationships.js +477 -0
- package/src/role/store.js +202 -0
- package/src/role/writeprotect.js +373 -0
- package/src/router/aliases.json +176 -25
- package/src/router/background/agent-runner.mjs +1 -1
- package/src/router/background/anchor.mjs +1 -1
- package/src/router/background/audit.mjs +2 -2
- package/src/router/background/budget-ledger.mjs +2 -2
- package/src/router/background/host-sim.mjs +1 -1
- package/src/router/background/memory.mjs +4 -4
- package/src/router/background/queue.mjs +1 -1
- package/src/router/background/retry.mjs +1 -1
- package/src/router/background/router.mjs +1 -1
- package/src/router/background/stream.mjs +1 -1
- package/src/router/background/tool-policy.mjs +1 -1
- package/src/router/design-headings.json +5 -5
- package/src/router/face/config.mjs +1 -1
- package/src/router/face/identity.mjs +3 -3
- package/src/router/judge.mjs +8 -7
- package/src/router/policies/generic.json +21 -4
- package/src/router/route.js +109 -22
- package/src/router/router.mjs +10 -6
- package/src/router/routing-rules.json +260 -445
- package/src/router/run-sim.mjs +8 -8
- package/src/router/runtime.js +23 -12
- package/src/router/service/config.mjs +5 -5
- package/src/router/service/context.mjs +1 -1
- package/src/router/service/queue.mjs +1 -1
- package/src/router/shadow.mjs +4 -3
- package/src/surgery-recovery.js +2 -2
- package/src/tool-output.js +2 -2
- package/src/tools.js +39 -7
- package/src/truncate-visible.js +2 -2
- package/src/ui/actions.js +1312 -0
- package/src/ui/panel-api.js +367 -0
- package/src/ui/panel-auth.js +251 -0
- package/src/ui/panel-integrity.js +243 -0
- package/src/ui/routes-readonly.js +695 -0
- package/src/ui/routes.js +3 -0
- package/docs/DEV.md +0 -279
- package/docs/PRESENCE-BUS-WIRING.md +0 -491
- package/docs/PRESENCE-BUS.md +0 -694
- package/docs/SURGERY.md +0 -186
- package/scripts/mark-core-cards.mjs +0 -289
- package/scripts/pack.mjs +0 -392
- package/scripts/router-proposals.mjs +0 -386
- package/src/router/fixtures/M1.json +0 -16
- package/src/router/fixtures/M2.json +0 -16
- package/src/router/fixtures/M3.json +0 -20
- package/src/router/fixtures/M4.json +0 -20
- package/src/router/fixtures/M5.json +0 -45
- package/src/router/fixtures/M6.json +0 -53
- package/src/router/fixtures/M7.json +0 -46
- package/src/router/fixtures/S1.json +0 -20
- package/src/router/fixtures/S2.json +0 -23
- package/src/router/fixtures/S3.json +0 -20
- package/src/router/fixtures/S4.json +0 -34
- package/src/router/fixtures/S5.json +0 -43
- package/src/router/fixtures/S6.json +0 -47
- package/src/router/fixtures/T1.json +0 -23
- package/src/router/fixtures/T2.json +0 -16
- package/src/router/fixtures/T3.json +0 -19
- package/src/router/fixtures/T4.json +0 -16
- package/src/router/fixtures/X1.json +0 -17
- package/src/router/fixtures/X2.json +0 -20
- package/src/router/gen-design-headings.mjs +0 -107
- package/src/router/policies/owner-rulings-2026-11-03.json +0 -62
- package/test/activity-realtime.test.mjs +0 -959
- package/test/activity-stream.test.mjs +0 -512
- package/test/auto-recall.test.mjs +0 -474
- package/test/auto-truncate.test.mjs +0 -931
- package/test/autobind.test.mjs +0 -399
- package/test/bus.test.mjs +0 -1572
- package/test/cli-guard.test.mjs +0 -198
- package/test/conclusion.test.mjs +0 -408
- package/test/config-schema.test.mjs +0 -210
- package/test/d6-output.test.mjs +0 -194
- package/test/d6.test.mjs +0 -395
- package/test/deps.test.mjs +0 -342
- package/test/doctor-setup.test.mjs +0 -297
- package/test/embedding-recall.test.mjs +0 -347
- package/test/feature-flags.test.mjs +0 -585
- package/test/filter-shadow.test.mjs +0 -170
- package/test/fixtures/activity-append-child.mjs +0 -29
- package/test/fixtures/baseline-0.8.0-registration.json +0 -556
- package/test/fixtures/d6-header-tools.json +0 -1
- package/test/fixtures/lock-child.mjs +0 -67
- package/test/fixtures/pre-step-container.mjs +0 -100
- package/test/fixtures/privacy-memory/topics/pii-unmarked.md +0 -9
- package/test/fixtures/privacy-memory/topics/secret-local-only.md +0 -11
- package/test/fixtures/profile-session-longlive-config.json +0 -20
- package/test/fixtures/proposal-cas-child.mjs +0 -66
- package/test/host-engine-status.test.mjs +0 -307
- package/test/hot-truncate.test.mjs +0 -1349
- package/test/identity-card-marker.test.mjs +0 -307
- package/test/identity-router/run.mjs +0 -721
- package/test/integration-pre-step.test.mjs +0 -553
- package/test/live.test.mjs +0 -1023
- package/test/maintenance-schedule.test.mjs +0 -538
- package/test/memory.test.mjs +0 -2828
- package/test/metrics.test.mjs +0 -478
- package/test/orchestrator.test.mjs +0 -266
- package/test/p1_probe.mjs +0 -97
- package/test/packaging.test.mjs +0 -184
- package/test/portability.test.mjs +0 -343
- package/test/rc4-regressions.test.mjs +0 -562
- package/test/rc5-regressions.test.mjs +0 -197
- package/test/rc6-chinese.test.mjs +0 -394
- package/test/router-1100/apply-off.test.mjs +0 -175
- package/test/router-1100/budget-reconcile.test.mjs +0 -215
- package/test/router-1100/config-context.test.mjs +0 -211
- package/test/router-1100/helpers.mjs +0 -129
- package/test/router-1100/hint-envelope.test.mjs +0 -150
- package/test/router-1100/lock-crossproc.test.mjs +0 -206
- package/test/router-1100/proposal-cas-crossproc.test.mjs +0 -184
- package/test/router-1100/router-apply.test.mjs +0 -372
- package/test/router-1100/router-fixb.test.mjs +0 -602
- package/test/router-1100/router-proposals.test.mjs +0 -405
- package/test/router-1100/router-queue-lease-wal.test.mjs +0 -355
- package/test/router-1100/router-security-cr.test.mjs +0 -333
- package/test/router-1100/service-lifecycle.test.mjs +0 -220
- package/test/router-1100/tools-contract.test.mjs +0 -164
- package/test/router-1100/turn-reflect-tick.test.mjs +0 -89
- package/test/router-1100/v6-contract.test.mjs +0 -158
- package/test/router-1100/v6b-contract.test.mjs +0 -293
- package/test/router-1100/v6c-contract.test.mjs +0 -249
- package/test/router-1100/wal-rotation.test.mjs +0 -375
- package/test/router-1100/wp1-config.test.mjs +0 -170
- package/test/router-1100/wp1-default-off.test.mjs +0 -77
- package/test/router-1100/wp1-model-status.test.mjs +0 -90
- package/test/router-1100/wp1-status.test.mjs +0 -220
- package/test/router-1100/wp1-tools.test.mjs +0 -146
- package/test/router-1100/wp5-cli.test.mjs +0 -240
- package/test/router-1100/wp5-model-auto.test.mjs +0 -418
- package/test/router-1100/wp6-merge.test.mjs +0 -386
- package/test/router-1100/wp6-wake.test.mjs +0 -502
- package/test/router-background/audit.test.mjs +0 -37
- package/test/router-background/budget-ledger.test.mjs +0 -330
- package/test/router-background/cross-process.test.mjs +0 -222
- package/test/router-background/fixtures/cross-child.mjs +0 -85
- package/test/router-background/fixtures/real-shapes.mjs +0 -239
- package/test/router-background/router.test.mjs +0 -1076
- package/test/router-background/stream-retry.test.mjs +0 -358
- package/test/router-background/tool-agent.test.mjs +0 -384
- package/test/router-face/bridge.test.mjs +0 -169
- package/test/router-face/face.test.mjs +0 -316
- package/test/router-failclosed.test.mjs +0 -466
- package/test/router-identity.test.mjs +0 -384
- package/test/router-l1-l2.contract.test.mjs +0 -1075
- package/test/router-memory-tools.test.mjs +0 -262
- package/test/router-policy.test.mjs +0 -225
- package/test/router-runtime.test.mjs +0 -438
- package/test/router-shadow.test.mjs +0 -287
- package/test/router.test.mjs +0 -903
- package/test/run-rc.mjs +0 -315
- package/test/run.mjs +0 -2469
- package/test/safety-defaults.test.mjs +0 -181
- package/test/seeded-load.test.mjs +0 -436
- package/test/session-original.test.mjs +0 -338
- package/test/state-truth.test.mjs +0 -937
- package/test/surgery-recovery.test.mjs +0 -936
- package/test/t7_drive.mjs +0 -151
- package/test/tool-output.test.mjs +0 -393
- package/test/tool-schema.test.mjs +0 -177
- package/test/tools_contract.test.mjs +0 -716
- package/test/truncate-visible.test.mjs +0 -1150
- package/test/ui-flags.test.mjs +0 -477
- package/test/v4-hardening.test.mjs +0 -427
- package/test/version-consistency.test.mjs +0 -286
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
// src/ui/panel-integrity.js — M2-G 面板包完整性(R8 / §12.10-R8 / §12.11-R9)
|
|
2
|
+
//
|
|
3
|
+
// 宿主包管理器在安装/更新面板包时生成 `.integrity/manifest.sha256`
|
|
4
|
+
// (逐文件 `<sha256> <relative-path>` + 末尾 `#hmac-sha256 <hex>`)。**关键**:
|
|
5
|
+
// manifest 用**宿主进程内存密钥**(`panelIntegrityKey`,boot 时 32B CSPRNG 生成、
|
|
6
|
+
// 进程退出即失)做 HMAC 锚定;校验时重算 HMAC。仅凭公开的逐文件 sha256 重写
|
|
7
|
+
// manifest 无法生成可通过校验的 HMAC —— 门不能自己给自己盖章。
|
|
8
|
+
//
|
|
9
|
+
// 本模块负责:
|
|
10
|
+
// 1) 加载时校验一次(createIntegrityGuard 构造即校验);
|
|
11
|
+
// 2) 每次动作前再校验(checkBeforeAction);
|
|
12
|
+
// 3) manifest 缺失 / 无签名 / HMAC 不符 / 文件不一致 → 动作全禁用 + 401
|
|
13
|
+
// `E_PANEL_INTEGRITY`,**只读展示保留**(status 照常返回)。
|
|
14
|
+
// panelRoot 由宿主显式传入(默认 `<DSH_HOME>/longlive-panel`,见 panel-api.js,
|
|
15
|
+
// 不再落在 memoryRoot 里)。不要求 Authenticode 代码签名(Lead 已签残余风险)。
|
|
16
|
+
|
|
17
|
+
import crypto from 'node:crypto';
|
|
18
|
+
import fs from 'node:fs';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
|
|
21
|
+
export const INTEGRITY_DIR = '.integrity';
|
|
22
|
+
export const INTEGRITY_MANIFEST = 'manifest.sha256';
|
|
23
|
+
export const MANIFEST_VERSION = 'panel-integrity/v1';
|
|
24
|
+
export const HMAC_LINE_PREFIX = '#hmac-sha256 ';
|
|
25
|
+
|
|
26
|
+
export class PanelIntegrityError extends Error {
|
|
27
|
+
constructor(message, status = 401, statusPayload = undefined) {
|
|
28
|
+
super(message);
|
|
29
|
+
this.name = 'PanelIntegrityError';
|
|
30
|
+
this.code = 'E_PANEL_INTEGRITY';
|
|
31
|
+
this.status = status;
|
|
32
|
+
if (statusPayload !== undefined) this.statusPayload = statusPayload;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function manifestPathOf(panelRoot) {
|
|
37
|
+
return path.join(String(panelRoot ?? ''), INTEGRITY_DIR, INTEGRITY_MANIFEST);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function sha256OfFile(file) {
|
|
41
|
+
return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function hmacOfBody(body, signKey) {
|
|
45
|
+
return crypto.createHmac('sha256', signKey).update(`${MANIFEST_VERSION}\n${body}`, 'utf8').digest('hex');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Parse manifest text: file lines + mandatory host-HMAC trailer. */
|
|
49
|
+
export function parseManifest(text) {
|
|
50
|
+
const entries = [];
|
|
51
|
+
const bad = [];
|
|
52
|
+
let hmac = null;
|
|
53
|
+
for (const line of String(text ?? '').replace(/^\uFEFF/, '').split(/\r?\n/)) {
|
|
54
|
+
const trimmed = line.trim();
|
|
55
|
+
if (trimmed === '') continue;
|
|
56
|
+
if (trimmed.startsWith(HMAC_LINE_PREFIX.trim())) {
|
|
57
|
+
const value = trimmed.slice(HMAC_LINE_PREFIX.trim().length).trim();
|
|
58
|
+
if (/^[0-9a-f]{64}$/i.test(value)) hmac = value.toLowerCase();
|
|
59
|
+
else bad.push(trimmed);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (trimmed.startsWith('#')) continue;
|
|
63
|
+
const match = /^([0-9a-fA-F]{64})\s{2}(.+)$/.exec(trimmed);
|
|
64
|
+
if (!match) {
|
|
65
|
+
bad.push(trimmed);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
entries.push({ sha256: match[1].toLowerCase(), path: match[2].trim() });
|
|
69
|
+
}
|
|
70
|
+
return { entries, bad, hmac };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The unsigned body (what the HMAC covers). */
|
|
74
|
+
function manifestBodyOf(panelRoot, files = undefined) {
|
|
75
|
+
const root = path.resolve(String(panelRoot));
|
|
76
|
+
let rels = files;
|
|
77
|
+
if (!Array.isArray(rels)) {
|
|
78
|
+
rels = [];
|
|
79
|
+
const walk = (dir) => {
|
|
80
|
+
for (const name of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
81
|
+
const full = path.join(dir, name.name);
|
|
82
|
+
if (name.isDirectory()) {
|
|
83
|
+
if (name.name === INTEGRITY_DIR) continue;
|
|
84
|
+
walk(full);
|
|
85
|
+
} else if (name.isFile()) {
|
|
86
|
+
rels.push(path.relative(root, full).split(path.sep).join('/'));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
walk(root);
|
|
91
|
+
}
|
|
92
|
+
rels.sort();
|
|
93
|
+
return `${rels.map((rel) => `${sha256OfFile(path.join(root, rel))} ${rel}`).join('\n')}\n`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Compute manifest text.
|
|
98
|
+
*
|
|
99
|
+
* - `computeManifest(panelRoot, { signKey })` → **host-signed** manifest (only the
|
|
100
|
+
* host has the in-memory key; this is what install/update writes).
|
|
101
|
+
* - `computeManifest(panelRoot)` → public unsigned body only. Writing it back
|
|
102
|
+
* does NOT pass validation (no host key ⇒ no valid HMAC): `E_MANIFEST_UNSIGNED`.
|
|
103
|
+
*/
|
|
104
|
+
export function computeManifest(panelRoot, options = {}) {
|
|
105
|
+
if (Array.isArray(options)) {
|
|
106
|
+
// legacy shape: computeManifest(root, files[]) -> unsigned body
|
|
107
|
+
return manifestBodyOf(panelRoot, options);
|
|
108
|
+
}
|
|
109
|
+
const body = manifestBodyOf(panelRoot, options.files);
|
|
110
|
+
if (options.signKey === undefined || options.signKey === null) return body;
|
|
111
|
+
return `${body}${HMAC_LINE_PREFIX}${hmacOfBody(body, options.signKey)}\n`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Create the integrity guard.
|
|
116
|
+
*
|
|
117
|
+
* @param {object} options
|
|
118
|
+
* @param {string} options.panelRoot installed panel package root (outside memoryRoot).
|
|
119
|
+
* @param {Buffer|string} options.signKey host-memory panelIntegrityKey (required).
|
|
120
|
+
* @param {() => Date} [options.now]
|
|
121
|
+
* @returns guard with `status()`, `loadedStatus`, `checkBeforeAction()`,
|
|
122
|
+
* `writeSignedManifest()` (host install/update helper).
|
|
123
|
+
*/
|
|
124
|
+
export function createIntegrityGuard(options = {}) {
|
|
125
|
+
const panelRoot = typeof options.panelRoot === 'string' && options.panelRoot.trim() !== ''
|
|
126
|
+
? path.resolve(options.panelRoot)
|
|
127
|
+
: null;
|
|
128
|
+
const now = typeof options.now === 'function' ? options.now : () => new Date();
|
|
129
|
+
const signKey = options.signKey;
|
|
130
|
+
|
|
131
|
+
function fail(code, checkedAt, extra = {}) {
|
|
132
|
+
return { ok: false, manifestVersion: null, checkedAt, files: [], lastError: code, ...extra };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function verifyNow() {
|
|
136
|
+
const checkedAt = now().toISOString();
|
|
137
|
+
if (panelRoot === null) return fail('E_PANEL_ROOT_UNKNOWN', checkedAt);
|
|
138
|
+
if (signKey === undefined || signKey === null || (typeof signKey === 'string' && signKey === '')
|
|
139
|
+
|| (Buffer.isBuffer(signKey) && signKey.length === 0)) {
|
|
140
|
+
return fail('E_INTEGRITY_KEY_MISSING', checkedAt);
|
|
141
|
+
}
|
|
142
|
+
const file = manifestPathOf(panelRoot);
|
|
143
|
+
let text = '';
|
|
144
|
+
try {
|
|
145
|
+
text = fs.readFileSync(file, 'utf8');
|
|
146
|
+
} catch {
|
|
147
|
+
return fail('E_MANIFEST_MISSING', checkedAt);
|
|
148
|
+
}
|
|
149
|
+
const parsed = parseManifest(text);
|
|
150
|
+
if (parsed.hmac === null) {
|
|
151
|
+
// Garbage manifest vs. a well-formed but *unsigned* file list (the public
|
|
152
|
+
// `computeManifest()` rewrite attack): both fail closed, with distinct codes.
|
|
153
|
+
const invalid = parsed.bad.length > 0 || parsed.entries.length === 0;
|
|
154
|
+
return fail(invalid ? 'E_MANIFEST_INVALID' : 'E_MANIFEST_UNSIGNED', checkedAt);
|
|
155
|
+
}
|
|
156
|
+
// Recompute the HMAC over the exact body the manifest declares (entries +
|
|
157
|
+
// paths in file order — the signed form is canonical sorted). A public
|
|
158
|
+
// recompute of file hashes cannot know the host key, so rewriting the file
|
|
159
|
+
// list alone never validates.
|
|
160
|
+
const bodyText = `${parsed.entries.map((entry) => `${entry.sha256} ${entry.path}`).join('\n')}\n`;
|
|
161
|
+
const expected = hmacOfBody(bodyText, signKey);
|
|
162
|
+
if (expected !== parsed.hmac) {
|
|
163
|
+
return fail('E_MANIFEST_HMAC_INVALID', checkedAt);
|
|
164
|
+
}
|
|
165
|
+
if (parsed.bad.length > 0 || parsed.entries.length === 0) {
|
|
166
|
+
return fail(parsed.bad.length > 0 ? 'E_MANIFEST_INVALID' : 'E_MANIFEST_EMPTY', checkedAt);
|
|
167
|
+
}
|
|
168
|
+
const files = [];
|
|
169
|
+
let ok = true;
|
|
170
|
+
let lastError = null;
|
|
171
|
+
for (const entry of parsed.entries) {
|
|
172
|
+
const target = path.resolve(panelRoot, entry.path);
|
|
173
|
+
if (target !== panelRoot && !target.startsWith(`${panelRoot}${path.sep}`)) {
|
|
174
|
+
ok = false;
|
|
175
|
+
lastError = 'E_MANIFEST_PATH_ESCAPE';
|
|
176
|
+
files.push({ path: entry.path, ok: false, sha256: null });
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
let actual = null;
|
|
180
|
+
try {
|
|
181
|
+
actual = sha256OfFile(target);
|
|
182
|
+
} catch {
|
|
183
|
+
ok = false;
|
|
184
|
+
lastError = lastError ?? 'E_FILE_MISSING';
|
|
185
|
+
files.push({ path: entry.path, ok: false, sha256: null });
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const match = actual === entry.sha256;
|
|
189
|
+
if (!match) {
|
|
190
|
+
ok = false;
|
|
191
|
+
lastError = 'E_FILE_MISMATCH';
|
|
192
|
+
}
|
|
193
|
+
files.push({ path: entry.path, ok: match, sha256: actual });
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
ok,
|
|
197
|
+
manifestVersion: `panel+hmac:${parsed.hmac.slice(0, 16)}`,
|
|
198
|
+
checkedAt,
|
|
199
|
+
files,
|
|
200
|
+
lastError,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Host-only install/update helper: hash the current files and write a manifest
|
|
206
|
+
* signed with the in-memory host key. A caller without the key cannot produce
|
|
207
|
+
* a valid manifest, so this function is unusable as a bypass.
|
|
208
|
+
*/
|
|
209
|
+
function writeSignedManifest() {
|
|
210
|
+
if (panelRoot === null) {
|
|
211
|
+
return { ok: false, code: 'E_PANEL_ROOT_UNKNOWN' };
|
|
212
|
+
}
|
|
213
|
+
if (signKey === undefined || signKey === null) {
|
|
214
|
+
return { ok: false, code: 'E_INTEGRITY_KEY_MISSING' };
|
|
215
|
+
}
|
|
216
|
+
const text = computeManifest(panelRoot, { signKey });
|
|
217
|
+
const file = manifestPathOf(panelRoot);
|
|
218
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
219
|
+
fs.writeFileSync(file, text, 'utf8');
|
|
220
|
+
const parsed = parseManifest(text);
|
|
221
|
+
return { ok: true, path: file, hmac: parsed.hmac, body: text };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const loadedStatus = verifyNow();
|
|
225
|
+
|
|
226
|
+
function checkBeforeAction() {
|
|
227
|
+
const status = verifyNow();
|
|
228
|
+
if (!status.ok) {
|
|
229
|
+
throw new PanelIntegrityError('面板包完整性校验失败:动作已全部禁用(只读展示保留)。', 401, status);
|
|
230
|
+
}
|
|
231
|
+
return status;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
panelRoot,
|
|
236
|
+
loadedStatus,
|
|
237
|
+
status: verifyNow,
|
|
238
|
+
checkBeforeAction,
|
|
239
|
+
writeSignedManifest,
|
|
240
|
+
manifestPath: panelRoot === null ? null : manifestPathOf(panelRoot),
|
|
241
|
+
hasHostKey: signKey !== undefined && signKey !== null,
|
|
242
|
+
};
|
|
243
|
+
}
|