harnery 0.45.1 → 0.46.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/dist/commander.d.ts +13 -0
- package/dist/commander.d.ts.map +1 -1
- package/dist/commands/admission.d.ts.map +1 -1
- package/dist/commands/admission.js +214 -27
- package/dist/commands/agents.d.ts +4 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +102 -25
- package/dist/commands/browse.d.ts +3 -0
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +172 -8
- package/dist/commands/fetch.js +1 -0
- package/dist/commands/ledger-v3.d.ts.map +1 -1
- package/dist/commands/ledger-v3.js +8 -2
- package/dist/commands/presence.d.ts.map +1 -1
- package/dist/commands/presence.js +40 -0
- package/dist/commands/qa-run.d.ts.map +1 -1
- package/dist/commands/qa-run.js +4 -3
- package/dist/commands/review-pack.d.ts.map +1 -1
- package/dist/commands/review-pack.js +33 -6
- package/dist/commands/workflow.d.ts.map +1 -1
- package/dist/commands/workflow.js +6 -45
- package/dist/core/agents/coord-client.d.ts.map +1 -1
- package/dist/core/agents/coord-client.js +0 -18
- package/dist/core/agents/index.d.ts +1 -0
- package/dist/core/agents/index.d.ts.map +1 -1
- package/dist/core/agents/index.js +1 -0
- package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
- package/dist/core/agents/live-authority-v3.js +11 -4
- package/dist/core/agents/render/session-context.d.ts +0 -6
- package/dist/core/agents/render/session-context.d.ts.map +1 -1
- package/dist/core/agents/render/session-context.js +1 -6
- package/dist/core/agents/session-events.d.ts.map +1 -1
- package/dist/core/agents/session-events.js +12 -4
- package/dist/core/agents/state/heartbeat-reader.d.ts +3 -0
- package/dist/core/agents/state/heartbeat-reader.d.ts.map +1 -1
- package/dist/core/agents/state/live-coordination-view.d.ts +3 -1
- package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
- package/dist/core/agents/state/live-coordination-view.js +6 -2
- package/dist/core/agents/state/parent-reference-contract.d.ts +16 -0
- package/dist/core/agents/state/parent-reference-contract.d.ts.map +1 -0
- package/dist/core/agents/state/parent-reference-contract.js +1 -0
- package/dist/core/agents/state/parent-reference.d.ts +18 -0
- package/dist/core/agents/state/parent-reference.d.ts.map +1 -0
- package/dist/core/agents/state/parent-reference.js +119 -0
- package/dist/core/agents/state/pidmap.js +2 -2
- package/dist/core/agents/state/proc-start.d.ts +20 -31
- package/dist/core/agents/state/proc-start.d.ts.map +1 -1
- package/dist/core/agents/state/proc-start.js +37 -70
- package/dist/core/agents/state/service-process.d.ts +61 -0
- package/dist/core/agents/state/service-process.d.ts.map +1 -0
- package/dist/core/agents/state/service-process.js +143 -0
- package/dist/core/config.d.ts +39 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +63 -0
- package/dist/core/context/index.d.ts +6 -4
- package/dist/core/context/index.d.ts.map +1 -1
- package/dist/core/context/index.js +194 -256
- package/dist/core/context/observation.d.ts +9 -9
- package/dist/core/context/observation.d.ts.map +1 -1
- package/dist/core/context/observation.js +37 -6
- package/dist/core/diagnostics/path-identity.d.ts +1 -1
- package/dist/core/diagnostics/path-identity.d.ts.map +1 -1
- package/dist/core/diagnostics/path-identity.js +51 -6
- package/dist/core/events/v3/canonical.js +43 -14
- package/dist/core/events/v3/index.d.ts +1 -1
- package/dist/core/events/v3/index.d.ts.map +1 -1
- package/dist/core/events/v3/index.js +1 -1
- package/dist/core/events/v3/producers/recorder.d.ts +34 -0
- package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/recorder.js +117 -13
- package/dist/core/events/v3/reader.d.ts.map +1 -1
- package/dist/core/events/v3/reader.js +53 -13
- package/dist/core/events/v3/runtime-build.d.ts +14 -0
- package/dist/core/events/v3/runtime-build.d.ts.map +1 -1
- package/dist/core/events/v3/runtime-build.js +44 -0
- package/dist/core/events/v3/validate.d.ts.map +1 -1
- package/dist/core/events/v3/validate.js +36 -1
- package/dist/core/governor/index.d.ts +2 -1
- package/dist/core/governor/index.d.ts.map +1 -1
- package/dist/core/governor/index.js +2 -1
- package/dist/core/governor/service-read.d.ts +3 -10
- package/dist/core/governor/service-read.d.ts.map +1 -1
- package/dist/core/governor/service-read.js +10 -94
- package/dist/core/governor/service.d.ts +3 -7
- package/dist/core/governor/service.d.ts.map +1 -1
- package/dist/core/governor/service.js +17 -182
- package/dist/core/hooks/adapter/native-parent.d.ts +23 -0
- package/dist/core/hooks/adapter/native-parent.d.ts.map +1 -0
- package/dist/core/hooks/adapter/native-parent.js +91 -0
- package/dist/core/hooks/cli.js +79 -4
- package/dist/core/hooks/resolve/owner.d.ts +0 -11
- package/dist/core/hooks/resolve/owner.d.ts.map +1 -1
- package/dist/core/hooks/resolve/owner.js +2 -25
- package/dist/core/hooks/shell-admission.d.ts +84 -0
- package/dist/core/hooks/shell-admission.d.ts.map +1 -0
- package/dist/core/hooks/shell-admission.js +622 -0
- package/dist/core/hooks/shell-commit-tree.d.ts +2 -0
- package/dist/core/hooks/shell-commit-tree.d.ts.map +1 -0
- package/dist/core/hooks/shell-commit-tree.js +104 -0
- package/dist/core/inbox/contract.d.ts +12 -0
- package/dist/core/inbox/contract.d.ts.map +1 -1
- package/dist/core/inbox/contract.js +16 -0
- package/dist/core/inbox/peer-messages.d.ts +41 -16
- package/dist/core/inbox/peer-messages.d.ts.map +1 -1
- package/dist/core/inbox/peer-messages.js +372 -15
- package/dist/core/inbox/service.d.ts +8 -1
- package/dist/core/inbox/service.d.ts.map +1 -1
- package/dist/core/inbox/service.js +83 -3
- package/dist/core/inbox/targets.d.ts +17 -3
- package/dist/core/inbox/targets.d.ts.map +1 -1
- package/dist/core/inbox/targets.js +77 -2
- package/dist/core/presence/git.d.ts +18 -12
- package/dist/core/presence/git.d.ts.map +1 -1
- package/dist/core/presence/git.js +32 -38
- package/dist/core/presence/index.d.ts +2 -2
- package/dist/core/presence/index.d.ts.map +1 -1
- package/dist/core/presence/index.js +8 -6
- package/dist/core/presence/mail.d.ts +133 -0
- package/dist/core/presence/mail.d.ts.map +1 -0
- package/dist/core/presence/mail.js +464 -0
- package/dist/core/resources/sampler.js +2 -2
- package/dist/core/resources/service-status.d.ts +2 -14
- package/dist/core/resources/service-status.d.ts.map +1 -1
- package/dist/core/resources/service-status.js +3 -23
- package/dist/core/resources/service.d.ts +3 -7
- package/dist/core/resources/service.d.ts.map +1 -1
- package/dist/core/resources/service.js +12 -101
- package/dist/core/resources/status.d.ts.map +1 -1
- package/dist/core/resources/status.js +3 -2
- package/dist/core/semantic/contract.d.ts +5 -5
- package/dist/core/semantic/service.d.ts.map +1 -1
- package/dist/core/semantic/service.js +9 -2
- package/dist/core/servers/index.d.ts +0 -2
- package/dist/core/servers/index.d.ts.map +1 -1
- package/dist/core/servers/index.js +3 -18
- package/dist/core/storage/modes.d.ts +4 -0
- package/dist/core/storage/modes.d.ts.map +1 -1
- package/dist/core/storage/modes.js +4 -0
- package/dist/core/storage/segments.d.ts.map +1 -1
- package/dist/core/storage/segments.js +5 -4
- package/dist/core/supervisor/service.d.ts +4 -7
- package/dist/core/supervisor/service.d.ts.map +1 -1
- package/dist/core/supervisor/service.js +23 -122
- package/dist/core/supervisor/services.js +2 -2
- package/dist/core/supervisor/status.d.ts +3 -17
- package/dist/core/supervisor/status.d.ts.map +1 -1
- package/dist/core/supervisor/status.js +7 -70
- package/dist/core/workflow/engine.js +2 -1
- package/dist/core/workflow/index.d.ts +3 -4
- package/dist/core/workflow/index.d.ts.map +1 -1
- package/dist/core/workflow/index.js +2 -2
- package/dist/core/workflow/proof.d.ts.map +1 -1
- package/dist/core/workflow/proof.js +30 -74
- package/dist/core/workflow/resume-lock.d.ts +17 -0
- package/dist/core/workflow/resume-lock.d.ts.map +1 -0
- package/dist/core/workflow/resume-lock.js +158 -0
- package/dist/core/workflow/run-state.d.ts +0 -21
- package/dist/core/workflow/run-state.d.ts.map +1 -1
- package/dist/core/workflow/run-state.js +3 -174
- package/dist/core/workflow/workspaces/leases.d.ts +4 -0
- package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/leases.js +1 -1
- package/dist/core/workflow/workspaces/local-git.js +91 -210
- package/dist/core/workflow/workspaces/ownership.d.ts +0 -7
- package/dist/core/workflow/workspaces/ownership.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/ownership.js +3 -130
- package/dist/core/workflow/workspaces/reclaim.d.ts +4 -7
- package/dist/core/workflow/workspaces/reclaim.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/reclaim.js +0 -4
- package/dist/core/workflow/workspaces/recovery.d.ts +1 -1
- package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/recovery.js +1 -1
- package/dist/core/workflow/workspaces/state.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/state.js +1 -4
- package/dist/lib/admission-history.d.ts +96 -0
- package/dist/lib/admission-history.d.ts.map +1 -0
- package/dist/lib/admission-history.js +196 -0
- package/dist/lib/admission.d.ts +56 -7
- package/dist/lib/admission.d.ts.map +1 -1
- package/dist/lib/admission.js +161 -19
- package/dist/lib/browser/client.d.ts +18 -0
- package/dist/lib/browser/client.d.ts.map +1 -1
- package/dist/lib/browser/client.js +100 -12
- package/dist/lib/browser/content-checks.d.ts +3 -1
- package/dist/lib/browser/content-checks.d.ts.map +1 -1
- package/dist/lib/browser/content-checks.js +10 -2
- package/dist/lib/browser/critique.d.ts +32 -0
- package/dist/lib/browser/critique.d.ts.map +1 -1
- package/dist/lib/browser/critique.js +72 -3
- package/dist/lib/browser/geometry.d.ts +2 -0
- package/dist/lib/browser/geometry.d.ts.map +1 -1
- package/dist/lib/browser/geometry.js +61 -15
- package/dist/lib/browser/index.d.ts +1 -1
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/index.js +1 -1
- package/dist/lib/browser/page-review-contracts.d.ts +6 -1
- package/dist/lib/browser/page-review-contracts.d.ts.map +1 -1
- package/dist/lib/browser/page-review-contracts.js +6 -1
- package/dist/lib/browser/page-review-judge.d.ts +3 -1
- package/dist/lib/browser/page-review-judge.d.ts.map +1 -1
- package/dist/lib/browser/page-review-judge.js +41 -11
- package/dist/lib/browser/page-review-pack.d.ts +3 -0
- package/dist/lib/browser/page-review-pack.d.ts.map +1 -1
- package/dist/lib/browser/page-review-pack.js +5 -2
- package/dist/lib/browser/qa-run-contracts.d.ts +11 -0
- package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -1
- package/dist/lib/browser/qa-run-contracts.js +11 -4
- package/dist/lib/browser/qa-run.d.ts.map +1 -1
- package/dist/lib/browser/qa-run.js +73 -16
- package/dist/lib/browser/runts.d.ts +4 -2
- package/dist/lib/browser/runts.d.ts.map +1 -1
- package/dist/lib/browser/runts.js +84 -33
- package/dist/lib/browser/windows-chrome.d.ts +14 -0
- package/dist/lib/browser/windows-chrome.d.ts.map +1 -1
- package/dist/lib/browser/windows-chrome.js +42 -0
- package/dist/lib/browser/windows-login-sync.d.ts +110 -0
- package/dist/lib/browser/windows-login-sync.d.ts.map +1 -0
- package/dist/lib/browser/windows-login-sync.js +171 -0
- package/dist/lib/cookies/client.d.ts +30 -5
- package/dist/lib/cookies/client.d.ts.map +1 -1
- package/dist/lib/cookies/client.js +55 -10
- package/dist/lib/cookies/index.d.ts +1 -1
- package/dist/lib/cookies/index.d.ts.map +1 -1
- package/dist/lib/cookies/index.js +1 -1
- package/dist/lib/docs-metadata-sync.d.ts.map +1 -1
- package/dist/lib/docs-metadata-sync.js +41 -2
- package/dist/lib/durable-job.d.ts +8 -0
- package/dist/lib/durable-job.d.ts.map +1 -1
- package/dist/lib/durable-job.js +10 -0
- package/dist/lib/http/client.d.ts +9 -8
- package/dist/lib/http/client.d.ts.map +1 -1
- package/dist/lib/http/client.js +44 -2
- package/dist/lib/instructions/templates.js +4 -4
- package/package.json +2 -2
- package/src/commander.ts +13 -0
- package/src/commands/admission.ts +272 -29
- package/src/commands/agents.ts +132 -27
- package/src/commands/browse.ts +208 -6
- package/src/commands/fetch.ts +1 -0
- package/src/commands/ledger-v3.ts +8 -2
- package/src/commands/presence.ts +49 -0
- package/src/commands/qa-run.ts +4 -3
- package/src/commands/review-pack.ts +41 -5
- package/src/commands/workflow.ts +8 -58
- package/src/core/agents/coord-client.ts +0 -20
- package/src/core/agents/index.ts +6 -0
- package/src/core/agents/live-authority-v3.ts +11 -4
- package/src/core/agents/render/session-context.ts +1 -7
- package/src/core/agents/session-events.ts +18 -3
- package/src/core/agents/state/heartbeat-reader.ts +3 -0
- package/src/core/agents/state/live-coordination-view.ts +13 -6
- package/src/core/agents/state/parent-reference-contract.ts +14 -0
- package/src/core/agents/state/parent-reference.ts +179 -0
- package/src/core/agents/state/pidmap.ts +2 -2
- package/src/core/agents/state/proc-start.ts +41 -69
- package/src/core/agents/state/service-process.ts +217 -0
- package/src/core/config.ts +85 -0
- package/src/core/context/index.ts +207 -272
- package/src/core/context/observation.ts +40 -9
- package/src/core/diagnostics/path-identity.ts +52 -6
- package/src/core/events/v3/canonical.ts +46 -13
- package/src/core/events/v3/index.ts +3 -0
- package/src/core/events/v3/producers/recorder.ts +151 -13
- package/src/core/events/v3/reader.ts +55 -11
- package/src/core/events/v3/runtime-build.ts +62 -0
- package/src/core/events/v3/validate.ts +42 -1
- package/src/core/governor/index.ts +1 -2
- package/src/core/governor/service-read.ts +15 -114
- package/src/core/governor/service.ts +36 -220
- package/src/core/hooks/adapter/native-parent.ts +109 -0
- package/src/core/hooks/cli.ts +83 -4
- package/src/core/hooks/resolve/owner.ts +2 -25
- package/src/core/hooks/shell-admission.ts +634 -0
- package/src/core/hooks/shell-commit-tree.ts +102 -0
- package/src/core/inbox/contract.ts +18 -0
- package/src/core/inbox/peer-messages.ts +501 -15
- package/src/core/inbox/service.ts +108 -3
- package/src/core/inbox/targets.ts +94 -3
- package/src/core/presence/git.ts +45 -37
- package/src/core/presence/index.ts +16 -8
- package/src/core/presence/mail.ts +620 -0
- package/src/core/resources/sampler.ts +3 -2
- package/src/core/resources/service-status.ts +6 -35
- package/src/core/resources/service.ts +23 -133
- package/src/core/resources/status.ts +3 -2
- package/src/core/semantic/service.ts +8 -2
- package/src/core/servers/index.ts +3 -23
- package/src/core/storage/modes.ts +6 -0
- package/src/core/storage/segments.ts +5 -4
- package/src/core/supervisor/service.ts +33 -155
- package/src/core/supervisor/services.ts +2 -2
- package/src/core/supervisor/status.ts +10 -85
- package/src/core/workflow/engine.ts +1 -1
- package/src/core/workflow/index.ts +2 -13
- package/src/core/workflow/proof.ts +35 -83
- package/src/core/workflow/resume-lock.ts +179 -0
- package/src/core/workflow/run-state.ts +3 -207
- package/src/core/workflow/workspaces/leases.ts +1 -1
- package/src/core/workflow/workspaces/local-git.ts +120 -239
- package/src/core/workflow/workspaces/ownership.ts +3 -158
- package/src/core/workflow/workspaces/reclaim.ts +3 -9
- package/src/core/workflow/workspaces/recovery.ts +1 -1
- package/src/core/workflow/workspaces/state.ts +1 -5
- package/src/lib/admission-history.ts +267 -0
- package/src/lib/admission.ts +217 -19
- package/src/lib/browser/client.ts +147 -35
- package/src/lib/browser/content-checks.ts +14 -3
- package/src/lib/browser/critique.ts +113 -3
- package/src/lib/browser/geometry.ts +63 -14
- package/src/lib/browser/index.ts +5 -0
- package/src/lib/browser/page-review-contracts.ts +6 -1
- package/src/lib/browser/page-review-judge.ts +58 -13
- package/src/lib/browser/page-review-pack.ts +13 -2
- package/src/lib/browser/qa-run-contracts.ts +30 -4
- package/src/lib/browser/qa-run.ts +78 -17
- package/src/lib/browser/runts.ts +85 -37
- package/src/lib/browser/windows-chrome.ts +50 -0
- package/src/lib/browser/windows-login-sync.ts +275 -0
- package/src/lib/cookies/client.ts +62 -10
- package/src/lib/cookies/index.ts +3 -0
- package/src/lib/docs-metadata-sync.ts +44 -2
- package/src/lib/durable-job.ts +15 -0
- package/src/lib/http/client.ts +63 -1
- package/src/lib/instructions/templates.ts +4 -4
- package/dist/core/agents/state/process-handle.d.ts +0 -26
- package/dist/core/agents/state/process-handle.d.ts.map +0 -1
- package/dist/core/agents/state/process-handle.js +0 -121
- package/dist/core/workflow/recovery-audit.d.ts +0 -30
- package/dist/core/workflow/recovery-audit.d.ts.map +0 -1
- package/dist/core/workflow/recovery-audit.js +0 -116
- package/src/core/agents/state/process-handle.ts +0 -165
- package/src/core/workflow/recovery-audit.ts +0 -181
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Repository observation contract shared by context recovery and workflow proof.
|
|
3
3
|
*
|
|
4
|
-
* This module
|
|
5
|
-
* dashboard, proof readers) can use it without reaching the
|
|
6
|
-
* snapshot code in ./index.ts.
|
|
4
|
+
* This module runs no Git and touches no files, so read-only consumers (the
|
|
5
|
+
* Codec dashboard, proof readers) can use it without reaching the
|
|
6
|
+
* Git-running snapshot code in ./index.ts.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { isAbsolute } from "node:path";
|
|
10
|
+
|
|
9
11
|
export type RepoObservation = "ok" | "unavailable" | "partial";
|
|
10
12
|
|
|
11
13
|
export interface RepoSnapshot {
|
|
@@ -20,10 +22,39 @@ export interface RepoSnapshot {
|
|
|
20
22
|
dirty_paths_truncated?: boolean;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
/**
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Every reason two snapshots cannot establish repository drift. Only an empty
|
|
27
|
+
* list lets a caller compare branch, HEAD and dirty paths; anything else
|
|
28
|
+
* leaves the whole comparison unknown.
|
|
29
|
+
*/
|
|
30
|
+
export function repoComparisonProblems(
|
|
31
|
+
a: RepoSnapshot,
|
|
32
|
+
b: RepoSnapshot,
|
|
33
|
+
labels: readonly [string, string],
|
|
34
|
+
): string[] {
|
|
35
|
+
const sides = [
|
|
36
|
+
[labels[0], a],
|
|
37
|
+
[labels[1], b],
|
|
38
|
+
] as const;
|
|
39
|
+
const failed = sides.flatMap(([label, repo]) =>
|
|
40
|
+
repo.observation === "ok"
|
|
41
|
+
? []
|
|
42
|
+
: [
|
|
43
|
+
// Snapshots recorded before observations existed carry none.
|
|
44
|
+
`${label} snapshot is ${repo.observation ?? "unavailable"}${repo.observation_detail ? ` (${repo.observation_detail})` : ""}`,
|
|
45
|
+
],
|
|
46
|
+
);
|
|
47
|
+
if (failed.length > 0) return failed;
|
|
48
|
+
const problems: string[] = [];
|
|
49
|
+
if (a.dirty_paths_truncated || b.dirty_paths_truncated) {
|
|
50
|
+
problems.push("dirty path list is truncated");
|
|
51
|
+
}
|
|
52
|
+
if (!a.root || !b.root || !isAbsolute(a.root) || !isAbsolute(b.root)) {
|
|
53
|
+
problems.push("repository root is unknown");
|
|
54
|
+
} else if (a.root !== b.root) {
|
|
55
|
+
problems.push("repository roots differ");
|
|
56
|
+
}
|
|
57
|
+
if (!a.branch?.trim() || !b.branch?.trim()) problems.push("branch is unknown");
|
|
58
|
+
if (!a.head?.trim() || !b.head?.trim()) problems.push("HEAD is unknown");
|
|
59
|
+
return problems;
|
|
29
60
|
}
|
|
@@ -1,15 +1,61 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHmac, randomBytes } from "node:crypto";
|
|
2
|
+
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { PERSONAL_DIR_MODE, PERSONAL_FILE_MODE } from "../storage/modes.ts";
|
|
2
6
|
|
|
3
7
|
const HOME_PATH =
|
|
4
8
|
/(?:[A-Za-z]:[\\/]Users[\\/](?:[^\\/\r\n\0:"<>|]+(?=[\\/])|[^\\/\s\0:]+)|\/home\/[^/\s\0:]+|\/Users\/[^/\s\0:]+)/g;
|
|
5
9
|
|
|
6
|
-
|
|
10
|
+
const KEY_PATTERN = /^[0-9a-f]{64}$/;
|
|
11
|
+
let cachedKey: Buffer | undefined;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Per-install secret for home-path pseudonyms. An unkeyed digest would let
|
|
15
|
+
* anyone holding a bundle recover the user name by hashing guesses, so the
|
|
16
|
+
* key stays in local state and never enters a bundle. Sanitized values carry
|
|
17
|
+
* the pseudonym itself, so replay elsewhere needs no key.
|
|
18
|
+
*/
|
|
19
|
+
function identityKeyPath(): string {
|
|
20
|
+
const xdg = process.env.XDG_STATE_HOME;
|
|
21
|
+
const base = xdg?.trim() ? xdg : join(homedir(), ".local", "state");
|
|
22
|
+
return join(base, "harnery", "diagnostic-identity.key");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function readKey(path: string): Buffer | undefined {
|
|
26
|
+
try {
|
|
27
|
+
const text = readFileSync(path, "utf8").trim();
|
|
28
|
+
return KEY_PATTERN.test(text) ? Buffer.from(text, "hex") : undefined;
|
|
29
|
+
} catch {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function identityKey(): Buffer {
|
|
35
|
+
if (cachedKey) return cachedKey;
|
|
36
|
+
const path = identityKeyPath();
|
|
37
|
+
let key = readKey(path);
|
|
38
|
+
if (!key) {
|
|
39
|
+
const fresh = randomBytes(32);
|
|
40
|
+
try {
|
|
41
|
+
mkdirSync(dirname(path), { recursive: true, mode: PERSONAL_DIR_MODE });
|
|
42
|
+
writeFileSync(path, `${fresh.toString("hex")}\n`, { flag: "wx", mode: PERSONAL_FILE_MODE });
|
|
43
|
+
chmodSync(path, PERSONAL_FILE_MODE);
|
|
44
|
+
key = fresh;
|
|
45
|
+
} catch {
|
|
46
|
+
// A concurrent writer won the race, or state is unwritable. Prefer the
|
|
47
|
+
// stored key; otherwise stay private with a process-local key.
|
|
48
|
+
key = readKey(path) ?? fresh;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
cachedKey = key;
|
|
52
|
+
return key;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** One stable, non-reversible representation for private home paths in values and identity inputs. */
|
|
7
56
|
export function canonicalizeDiagnosticIdentity(value: string): string {
|
|
8
57
|
return value.replace(HOME_PATH, (home) => {
|
|
9
|
-
const digest =
|
|
10
|
-
.update("harnery-diagnostic-home-v1\0")
|
|
11
|
-
.update(home)
|
|
12
|
-
.digest("hex");
|
|
58
|
+
const digest = createHmac("sha256", identityKey()).update(home).digest("hex");
|
|
13
59
|
return `[REDACTED:home:${digest}]`;
|
|
14
60
|
});
|
|
15
61
|
}
|
|
@@ -104,10 +104,19 @@ export function normalizeNativeIdV3(
|
|
|
104
104
|
return `hid_${digest}`;
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
/**
|
|
108
|
+
* NFC leaves ASCII unchanged, so ASCII strings and key sets skip normalization
|
|
109
|
+
* and the collision map. Every reader re-serializes each ledger frame to prove
|
|
110
|
+
* it is canonical, which makes this the hottest function on a cold read.
|
|
111
|
+
*/
|
|
112
|
+
const NON_ASCII = /[\u0080-\uffff]/;
|
|
113
|
+
|
|
107
114
|
function serialize(value: unknown, ancestors: Set<object>): string {
|
|
108
115
|
if (value === null) return "null";
|
|
109
116
|
if (typeof value === "boolean") return value ? "true" : "false";
|
|
110
|
-
if (typeof value === "string")
|
|
117
|
+
if (typeof value === "string") {
|
|
118
|
+
return JSON.stringify(NON_ASCII.test(value) ? value.normalize("NFC") : value);
|
|
119
|
+
}
|
|
111
120
|
if (typeof value === "number") {
|
|
112
121
|
if (!Number.isFinite(value)) throw new Error("canonical JSON rejects non-finite numbers");
|
|
113
122
|
return JSON.stringify(Object.is(value, -0) ? 0 : value);
|
|
@@ -119,29 +128,53 @@ function serialize(value: unknown, ancestors: Set<object>): string {
|
|
|
119
128
|
ancestors.add(value);
|
|
120
129
|
try {
|
|
121
130
|
if (Array.isArray(value)) {
|
|
122
|
-
|
|
131
|
+
let out = "[";
|
|
132
|
+
for (let index = 0; index < value.length; index++) {
|
|
133
|
+
if (index > 0) out += ",";
|
|
134
|
+
out += serialize(value[index], ancestors);
|
|
135
|
+
}
|
|
136
|
+
return `${out}]`;
|
|
123
137
|
}
|
|
124
138
|
const prototype = Object.getPrototypeOf(value);
|
|
125
139
|
if (prototype !== Object.prototype && prototype !== null) {
|
|
126
140
|
throw new Error("canonical JSON accepts only plain objects");
|
|
127
141
|
}
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
throw new Error("canonical JSON key collision after Unicode normalization");
|
|
133
|
-
}
|
|
134
|
-
normalized.set(normalizedKey, item);
|
|
142
|
+
const object = value as Record<string, unknown>;
|
|
143
|
+
const keys = Object.keys(object);
|
|
144
|
+
if (keys.some((key) => NON_ASCII.test(key))) {
|
|
145
|
+
return serializeNormalizedObject(object, ancestors);
|
|
135
146
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
147
|
+
keys.sort(compareUtf16);
|
|
148
|
+
let out = "{";
|
|
149
|
+
for (let index = 0; index < keys.length; index++) {
|
|
150
|
+
const key = keys[index] as string;
|
|
151
|
+
if (index > 0) out += ",";
|
|
152
|
+
out += `${JSON.stringify(key)}:${serialize(object[key], ancestors)}`;
|
|
153
|
+
}
|
|
154
|
+
return `${out}}`;
|
|
140
155
|
} finally {
|
|
141
156
|
ancestors.delete(value);
|
|
142
157
|
}
|
|
143
158
|
}
|
|
144
159
|
|
|
160
|
+
function serializeNormalizedObject(
|
|
161
|
+
object: Record<string, unknown>,
|
|
162
|
+
ancestors: Set<object>,
|
|
163
|
+
): string {
|
|
164
|
+
const normalized = new Map<string, unknown>();
|
|
165
|
+
for (const [key, item] of Object.entries(object)) {
|
|
166
|
+
const normalizedKey = key.normalize("NFC");
|
|
167
|
+
if (normalized.has(normalizedKey)) {
|
|
168
|
+
throw new Error("canonical JSON key collision after Unicode normalization");
|
|
169
|
+
}
|
|
170
|
+
normalized.set(normalizedKey, item);
|
|
171
|
+
}
|
|
172
|
+
const keys = [...normalized.keys()].sort(compareUtf16);
|
|
173
|
+
return `{${keys
|
|
174
|
+
.map((key) => `${JSON.stringify(key)}:${serialize(normalized.get(key), ancestors)}`)
|
|
175
|
+
.join(",")}}`;
|
|
176
|
+
}
|
|
177
|
+
|
|
145
178
|
function compareUtf16(left: string, right: string): number {
|
|
146
179
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
147
180
|
}
|
|
@@ -247,7 +247,9 @@ export {
|
|
|
247
247
|
type DrainHookIntakeSpoolV3Result,
|
|
248
248
|
drainHookIntakeSpoolV3,
|
|
249
249
|
type HookProducerStateRecordV3,
|
|
250
|
+
type HookProducerStateScanV3,
|
|
250
251
|
type HookProducerStateV3,
|
|
252
|
+
type InvalidHookProducerStateFileV3,
|
|
251
253
|
listHookProducerStateRecordsV3,
|
|
252
254
|
type RecordApprovedSessionEndV3Input,
|
|
253
255
|
type RecordApprovedSessionEndV3Result,
|
|
@@ -256,6 +258,7 @@ export {
|
|
|
256
258
|
readHookProducerStateV3,
|
|
257
259
|
recordApprovedSessionEndV3,
|
|
258
260
|
recordHookSignalV3,
|
|
261
|
+
scanHookProducerStateRecordsV3,
|
|
259
262
|
} from "./producers/recorder.ts";
|
|
260
263
|
export {
|
|
261
264
|
type ClaimProjectionV3,
|
|
@@ -465,7 +465,18 @@ function sessionHashForSignal(
|
|
|
465
465
|
// single live producer authority instead of hashing the instance into a
|
|
466
466
|
// second state path. Ambiguous or absent matches keep the fail-closed fallback.
|
|
467
467
|
if (adapterBehavior(input.adapter).sessionIdentityOnStartOnly) {
|
|
468
|
-
const
|
|
468
|
+
const scan = scanHookProducerStateRecordsV3(input.coordRoot);
|
|
469
|
+
// This instance's own state is unreadable: fail closed rather than hash
|
|
470
|
+
// the instance into a second state path beside it.
|
|
471
|
+
if (
|
|
472
|
+
scan.invalid.some(
|
|
473
|
+
(file) => file.adapter === input.adapter && file.instance_id === input.instance_id,
|
|
474
|
+
)
|
|
475
|
+
) {
|
|
476
|
+
throw new Error("V3 producer state is invalid");
|
|
477
|
+
}
|
|
478
|
+
for (const file of scan.invalid) recordInvalidProducerStateDiagnostic(input.coordRoot, file);
|
|
479
|
+
const matches = scan.records.filter(
|
|
469
480
|
({ state }) => state.adapter === input.adapter && state.instance_id === input.instance_id,
|
|
470
481
|
);
|
|
471
482
|
if (matches.length === 1) {
|
|
@@ -3476,15 +3487,44 @@ export function salvageOpenSpansV3(input: SalvageOpenSpansV3Input): SalvageOpenS
|
|
|
3476
3487
|
}
|
|
3477
3488
|
}
|
|
3478
3489
|
|
|
3479
|
-
|
|
3490
|
+
/** One producer-state file the validator refused during a listing scan. */
|
|
3491
|
+
export interface InvalidHookProducerStateFileV3 {
|
|
3492
|
+
path: string;
|
|
3493
|
+
adapter: EventAdapterIdV3;
|
|
3494
|
+
session_hash: `hid_${string}`;
|
|
3495
|
+
reason: string;
|
|
3496
|
+
/**
|
|
3497
|
+
* The file's raw `instance_id` when it parses as JSON and carries a
|
|
3498
|
+
* well-formed one. Readers that target one instance use it to keep failing
|
|
3499
|
+
* closed on that instance's own invalid state.
|
|
3500
|
+
*/
|
|
3501
|
+
instance_id?: `inst_${string}`;
|
|
3502
|
+
}
|
|
3503
|
+
|
|
3504
|
+
export interface HookProducerStateScanV3 {
|
|
3505
|
+
records: HookProducerStateRecordV3[];
|
|
3506
|
+
invalid: InvalidHookProducerStateFileV3[];
|
|
3507
|
+
}
|
|
3508
|
+
|
|
3509
|
+
/**
|
|
3510
|
+
* Scan every producer-state file of the active epoch. A file the validator
|
|
3511
|
+
* refuses is reported in `invalid` instead of failing the scan, so one
|
|
3512
|
+
* session's bad state cannot take coordination away from every other session
|
|
3513
|
+
* in the checkout. An unsafe adapter directory still throws.
|
|
3514
|
+
*/
|
|
3515
|
+
export function scanHookProducerStateRecordsV3(
|
|
3480
3516
|
coordRoot: string,
|
|
3481
3517
|
options: { includeTerminal?: boolean } = {},
|
|
3482
|
-
):
|
|
3518
|
+
): HookProducerStateScanV3 {
|
|
3483
3519
|
const control = readEventV3ControlState(coordRoot);
|
|
3484
|
-
if (control.state !== "candidate" && control.state !== "active")
|
|
3520
|
+
if (control.state !== "candidate" && control.state !== "active") {
|
|
3521
|
+
return { records: [], invalid: [] };
|
|
3522
|
+
}
|
|
3523
|
+
const genesisId = control.genesis.event.payload.genesis_id;
|
|
3485
3524
|
const producerRoot = join(resolve(coordRoot), EVENT_V3_LEDGER_RELATIVE_ROOT, "private-producers");
|
|
3486
|
-
if (!existsSync(producerRoot)) return [];
|
|
3525
|
+
if (!existsSync(producerRoot)) return { records: [], invalid: [] };
|
|
3487
3526
|
const records: HookProducerStateRecordV3[] = [];
|
|
3527
|
+
const invalid: InvalidHookProducerStateFileV3[] = [];
|
|
3488
3528
|
for (const adapter of EVENT_ADAPTER_IDS_V3) {
|
|
3489
3529
|
const directory = join(producerRoot, adapter);
|
|
3490
3530
|
if (!existsSync(directory)) continue;
|
|
@@ -3496,19 +3536,86 @@ export function listHookProducerStateRecordsV3(
|
|
|
3496
3536
|
/^hid_[a-f0-9]{64}\.json$/.test(entry),
|
|
3497
3537
|
)) {
|
|
3498
3538
|
const path = join(directory, name);
|
|
3499
|
-
|
|
3539
|
+
let state: HookProducerStateV3;
|
|
3540
|
+
try {
|
|
3541
|
+
state = readProducerState(path);
|
|
3542
|
+
} catch (error) {
|
|
3543
|
+
const raw = rawProducerStateIdentity(path);
|
|
3544
|
+
// A refused file from a replaced epoch would have been skipped anyway.
|
|
3545
|
+
if (raw.epoch_genesis_id !== undefined && raw.epoch_genesis_id !== genesisId) continue;
|
|
3546
|
+
invalid.push({
|
|
3547
|
+
path,
|
|
3548
|
+
adapter,
|
|
3549
|
+
session_hash: name.slice(0, -".json".length) as `hid_${string}`,
|
|
3550
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
3551
|
+
...(raw.instance_id ? { instance_id: raw.instance_id } : {}),
|
|
3552
|
+
});
|
|
3553
|
+
continue;
|
|
3554
|
+
}
|
|
3500
3555
|
// Skip authorities left behind by a replaced epoch: their sequences and
|
|
3501
3556
|
// causal links belong to the archived ledger, not this one.
|
|
3502
|
-
if (state.epoch_genesis_id !==
|
|
3557
|
+
if (state.epoch_genesis_id !== genesisId) continue;
|
|
3503
3558
|
if (!options.includeTerminal && state.terminal) continue;
|
|
3504
3559
|
records.push({ path, modified_at_ms: lstatSync(path).mtimeMs, state });
|
|
3505
3560
|
}
|
|
3506
3561
|
}
|
|
3507
|
-
|
|
3562
|
+
records.sort(
|
|
3508
3563
|
(left, right) =>
|
|
3509
3564
|
left.state.generation_id.localeCompare(right.state.generation_id) ||
|
|
3510
3565
|
left.path.localeCompare(right.path),
|
|
3511
3566
|
);
|
|
3567
|
+
invalid.sort((left, right) => left.path.localeCompare(right.path));
|
|
3568
|
+
return { records, invalid };
|
|
3569
|
+
}
|
|
3570
|
+
|
|
3571
|
+
/**
|
|
3572
|
+
* List the valid producer states of the active epoch. An invalid file is
|
|
3573
|
+
* skipped and recorded as a `producer_state_invalid_skipped` diagnostic that
|
|
3574
|
+
* names its adapter and session hash; readers of that one session's state
|
|
3575
|
+
* still fail closed through `readHookProducerStateV3`.
|
|
3576
|
+
*/
|
|
3577
|
+
export function listHookProducerStateRecordsV3(
|
|
3578
|
+
coordRoot: string,
|
|
3579
|
+
options: { includeTerminal?: boolean } = {},
|
|
3580
|
+
): HookProducerStateRecordV3[] {
|
|
3581
|
+
const scan = scanHookProducerStateRecordsV3(coordRoot, options);
|
|
3582
|
+
for (const file of scan.invalid) recordInvalidProducerStateDiagnostic(coordRoot, file);
|
|
3583
|
+
return scan.records;
|
|
3584
|
+
}
|
|
3585
|
+
|
|
3586
|
+
function recordInvalidProducerStateDiagnostic(
|
|
3587
|
+
coordRoot: string,
|
|
3588
|
+
file: InvalidHookProducerStateFileV3,
|
|
3589
|
+
): void {
|
|
3590
|
+
writeProducerDiagnosticV3(coordRoot, "producer_state_invalid_skipped", {
|
|
3591
|
+
adapter: file.adapter,
|
|
3592
|
+
session_hash: file.session_hash,
|
|
3593
|
+
reason: file.reason,
|
|
3594
|
+
...(file.instance_id ? { instance_id: file.instance_id } : {}),
|
|
3595
|
+
});
|
|
3596
|
+
}
|
|
3597
|
+
|
|
3598
|
+
function rawProducerStateIdentity(path: string): {
|
|
3599
|
+
instance_id?: `inst_${string}`;
|
|
3600
|
+
epoch_genesis_id?: string;
|
|
3601
|
+
} {
|
|
3602
|
+
try {
|
|
3603
|
+
const metadata = lstatSync(path);
|
|
3604
|
+
if (!metadata.isFile() || metadata.isSymbolicLink()) return {};
|
|
3605
|
+
const parsed = JSON.parse(readFileSync(path, "utf8")) as Record<string, unknown>;
|
|
3606
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
3607
|
+
return {
|
|
3608
|
+
...(typeof parsed.instance_id === "string" &&
|
|
3609
|
+
/^inst_[a-zA-Z0-9._-]{1,128}$/.test(parsed.instance_id)
|
|
3610
|
+
? { instance_id: parsed.instance_id as `inst_${string}` }
|
|
3611
|
+
: {}),
|
|
3612
|
+
...(typeof parsed.epoch_genesis_id === "string"
|
|
3613
|
+
? { epoch_genesis_id: parsed.epoch_genesis_id }
|
|
3614
|
+
: {}),
|
|
3615
|
+
};
|
|
3616
|
+
} catch {
|
|
3617
|
+
return {};
|
|
3618
|
+
}
|
|
3512
3619
|
}
|
|
3513
3620
|
|
|
3514
3621
|
export interface ReanchorArchivedHookProducersV3Input {
|
|
@@ -4277,6 +4384,15 @@ function acquireStateLease(coordRoot: string, statePath: string) {
|
|
|
4277
4384
|
};
|
|
4278
4385
|
}
|
|
4279
4386
|
|
|
4387
|
+
/** Test seam: publish a producer state through the real write-side validator. */
|
|
4388
|
+
export function publishHookProducerStateForTestV3(
|
|
4389
|
+
coordRoot: string,
|
|
4390
|
+
path: string,
|
|
4391
|
+
state: HookProducerStateV3,
|
|
4392
|
+
): void {
|
|
4393
|
+
publishProducerState(coordRoot, path, state);
|
|
4394
|
+
}
|
|
4395
|
+
|
|
4280
4396
|
function publishProducerState(coordRoot: string, path: string, state: HookProducerStateV3): void {
|
|
4281
4397
|
const control = readEventV3ControlState(coordRoot);
|
|
4282
4398
|
if (
|
|
@@ -4285,11 +4401,19 @@ function publishProducerState(coordRoot: string, path: string, state: HookProduc
|
|
|
4285
4401
|
) {
|
|
4286
4402
|
return;
|
|
4287
4403
|
}
|
|
4404
|
+
const serialized = JSON.stringify(state);
|
|
4405
|
+
// Validate the exact bytes about to land with the reader's own validator.
|
|
4406
|
+
// The previous file stays in place when this refuses.
|
|
4407
|
+
try {
|
|
4408
|
+
validateProducerStateDocument(JSON.parse(serialized));
|
|
4409
|
+
} catch {
|
|
4410
|
+
throw new Error("V3 producer state is invalid; refusing to publish");
|
|
4411
|
+
}
|
|
4288
4412
|
const temporary = `${path}.tmp-${process.pid}-${randomUUID()}`;
|
|
4289
4413
|
let fd: number | undefined;
|
|
4290
4414
|
try {
|
|
4291
4415
|
fd = openSync(temporary, "wx", stateFileMode());
|
|
4292
|
-
writeFileSync(fd, `${
|
|
4416
|
+
writeFileSync(fd, `${serialized}\n`, "utf8");
|
|
4293
4417
|
fsyncSync(fd);
|
|
4294
4418
|
closeSync(fd);
|
|
4295
4419
|
fd = undefined;
|
|
@@ -4314,6 +4438,16 @@ function readProducerState(path: string): HookProducerStateV3 {
|
|
|
4314
4438
|
} catch {
|
|
4315
4439
|
throw new Error("V3 producer state is unreadable");
|
|
4316
4440
|
}
|
|
4441
|
+
return validateProducerStateDocument(parsed);
|
|
4442
|
+
}
|
|
4443
|
+
|
|
4444
|
+
/**
|
|
4445
|
+
* The single producer-state validator. Readers run it on every file they
|
|
4446
|
+
* load, and `publishProducerState` runs it on the serialized document before
|
|
4447
|
+
* the rename, so a state no reader would accept fails in the producing hook
|
|
4448
|
+
* instead of in every later reader of the checkout.
|
|
4449
|
+
*/
|
|
4450
|
+
function validateProducerStateDocument(parsed: unknown): HookProducerStateV3 {
|
|
4317
4451
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
4318
4452
|
throw new Error("V3 producer state is invalid");
|
|
4319
4453
|
}
|
|
@@ -4589,8 +4723,10 @@ function validPendingRuntimeContext(value: unknown): value is PendingRuntimeCont
|
|
|
4589
4723
|
Number.isFinite(Date.parse(pending.terminal_observed_at)) &&
|
|
4590
4724
|
typeof pending.turn_id === "string" &&
|
|
4591
4725
|
/^tid_[a-f0-9]{64}$/.test(pending.turn_id) &&
|
|
4592
|
-
|
|
4593
|
-
|
|
4726
|
+
// Same shapes the event contract accepts: workflow children carry
|
|
4727
|
+
// stableScopeId hashes (run_<64 hex>), not UUIDs.
|
|
4728
|
+
(pending.run_id === undefined || /^run_[a-zA-Z0-9._-]{1,128}$/.test(pending.run_id)) &&
|
|
4729
|
+
(pending.workflow_id === undefined || /^wf_[a-zA-Z0-9._-]{1,128}$/.test(pending.workflow_id)) &&
|
|
4594
4730
|
(pending.workflow_agent_id === undefined ||
|
|
4595
4731
|
/^[a-zA-Z0-9][a-zA-Z0-9._:/+-]{0,127}$/.test(pending.workflow_agent_id)) &&
|
|
4596
4732
|
typeof pending.native_session_id === "string" &&
|
|
@@ -4606,9 +4742,11 @@ function validPendingRuntimeContext(value: unknown): value is PendingRuntimeCont
|
|
|
4606
4742
|
(pending.runtime_version === undefined ||
|
|
4607
4743
|
(typeof pending.runtime_version === "string" &&
|
|
4608
4744
|
/^[a-zA-Z0-9][a-zA-Z0-9._:/+-]{0,127}$/.test(pending.runtime_version))) &&
|
|
4745
|
+
// Externally driven reconciles (`deferRetryLimit`) keep counting past
|
|
4746
|
+
// RUNTIME_CONTEXT_RETRY_LIMIT until their own final attempt, and publish
|
|
4747
|
+
// those counts, so the reader must accept them.
|
|
4609
4748
|
Number.isSafeInteger(pending.attempts) &&
|
|
4610
|
-
pending.attempts >= 0
|
|
4611
|
-
pending.attempts < RUNTIME_CONTEXT_RETRY_LIMIT
|
|
4749
|
+
pending.attempts >= 0
|
|
4612
4750
|
);
|
|
4613
4751
|
}
|
|
4614
4752
|
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
readEventV3SegmentManifest,
|
|
20
20
|
} from "./catalog.ts";
|
|
21
21
|
import type { EventV3 } from "./contract.ts";
|
|
22
|
+
import { readAdvanceableControlWitnessV3 } from "./control-witness.ts";
|
|
22
23
|
import { EVENT_V3_SCHEMA_DIGEST } from "./generated.ts";
|
|
23
24
|
import {
|
|
24
25
|
type EventV3RecoveryFailure,
|
|
@@ -442,7 +443,9 @@ export function readLedgerV3(
|
|
|
442
443
|
}
|
|
443
444
|
|
|
444
445
|
const discovered = discoverLedgerFramesV3(coordRoot);
|
|
445
|
-
const validated =
|
|
446
|
+
const validated =
|
|
447
|
+
validateWitnessedFramesV3(coordRoot, storage, discovered, options) ??
|
|
448
|
+
validateLedgerFramesV3(discovered.frames, options);
|
|
446
449
|
const recovery = discoverRecoveryReceiptsV3(coordRoot);
|
|
447
450
|
const diagnostics = [
|
|
448
451
|
...discovered.diagnostics,
|
|
@@ -514,9 +517,40 @@ export function readLedgerFramesV3(
|
|
|
514
517
|
return validateLedgerFramesV3(frames, options).result;
|
|
515
518
|
}
|
|
516
519
|
|
|
520
|
+
/**
|
|
521
|
+
* Cold-process fast path. Writers advance an authenticated validation
|
|
522
|
+
* checkpoint under the append lease after every drain, so when the witness
|
|
523
|
+
* names exactly the storage this read observed, each frame already passed the
|
|
524
|
+
* contract and canonical-byte checks that dominate a full read. Those two
|
|
525
|
+
* checks are skipped; every ledger-level rule still runs and rebuilds the
|
|
526
|
+
* validator state. The result is used only if that state reproduces the
|
|
527
|
+
* witnessed checkpoint byte for byte with no diagnostics and storage did not
|
|
528
|
+
* move during the read; anything else returns undefined and the caller runs
|
|
529
|
+
* the canonical full validation.
|
|
530
|
+
*/
|
|
531
|
+
function validateWitnessedFramesV3(
|
|
532
|
+
coordRoot: string,
|
|
533
|
+
storage: LedgerStorageVersionV3,
|
|
534
|
+
discovered: DiscoveredFramesV3,
|
|
535
|
+
options: ReadLedgerV3Options,
|
|
536
|
+
): { result: ReadLedgerV3Result; state: LedgerValidationStateV3 } | undefined {
|
|
537
|
+
if (!discovered.active || discovered.diagnostics.length > 0) return undefined;
|
|
538
|
+
const witness = readAdvanceableControlWitnessV3(coordRoot);
|
|
539
|
+
if (!witness || witness.payload.storage.fingerprint !== sha256V3(storage.fingerprint)) {
|
|
540
|
+
return undefined;
|
|
541
|
+
}
|
|
542
|
+
const trusted = validateLedgerFramesV3(discovered.frames, options, true);
|
|
543
|
+
if (trusted.state.diagnostics.length > 0) return undefined;
|
|
544
|
+
const reproduced = checkpointFromValidationStateV3(trusted.state, discovered.active);
|
|
545
|
+
if (canonicalJsonV3(reproduced) !== canonicalJsonV3(witness.checkpoint)) return undefined;
|
|
546
|
+
if (ledgerStorageVersionV3(coordRoot).fingerprint !== storage.fingerprint) return undefined;
|
|
547
|
+
return trusted;
|
|
548
|
+
}
|
|
549
|
+
|
|
517
550
|
function validateLedgerFramesV3(
|
|
518
551
|
frames: readonly LedgerFrameV3[],
|
|
519
552
|
options: ReadLedgerV3Options,
|
|
553
|
+
witnessed = false,
|
|
520
554
|
): { result: ReadLedgerV3Result; state: LedgerValidationStateV3 } {
|
|
521
555
|
const state: LedgerValidationStateV3 = {
|
|
522
556
|
diagnostics: [],
|
|
@@ -530,13 +564,18 @@ function validateLedgerFramesV3(
|
|
|
530
564
|
clocks: new Map(),
|
|
531
565
|
event_count: 0,
|
|
532
566
|
};
|
|
533
|
-
validateLedgerFramesIntoStateV3(frames, state);
|
|
567
|
+
validateLedgerFramesIntoStateV3(frames, state, witnessed);
|
|
534
568
|
return { result: finishLedgerValidationV3(state, options, ledgerFramesBytesV3(frames)), state };
|
|
535
569
|
}
|
|
536
570
|
|
|
571
|
+
/**
|
|
572
|
+
* `witnessed` skips only the per-frame contract and canonical-byte checks, and
|
|
573
|
+
* only validateWitnessedFramesV3 may pass it, after authenticating the frames.
|
|
574
|
+
*/
|
|
537
575
|
function validateLedgerFramesIntoStateV3(
|
|
538
576
|
frames: readonly LedgerFrameV3[],
|
|
539
577
|
state: LedgerValidationStateV3,
|
|
578
|
+
witnessed = false,
|
|
540
579
|
): void {
|
|
541
580
|
state.first_position ??= frames[0]?.position;
|
|
542
581
|
const {
|
|
@@ -576,17 +615,22 @@ function validateLedgerFramesIntoStateV3(
|
|
|
576
615
|
diagnostics.push(diagnostic("unsupported_major", frame));
|
|
577
616
|
continue;
|
|
578
617
|
}
|
|
579
|
-
|
|
580
|
-
if (
|
|
581
|
-
|
|
582
|
-
|
|
618
|
+
let event: EventV3;
|
|
619
|
+
if (witnessed) {
|
|
620
|
+
event = parsed as EventV3;
|
|
621
|
+
} else {
|
|
622
|
+
const validation = validateEventV3(parsed);
|
|
623
|
+
if (!validation.ok || !validation.event) {
|
|
624
|
+
diagnostics.push(diagnostic("invalid_contract", frame));
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
627
|
+
event = validation.event;
|
|
628
|
+
if (canonicalJsonV3(event) !== frame.raw) {
|
|
629
|
+
diagnostics.push(diagnostic("noncanonical_frame", frame, event.event_id));
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
583
632
|
}
|
|
584
|
-
const event = validation.event;
|
|
585
633
|
const shape = event as unknown as EventShape;
|
|
586
|
-
if (canonicalJsonV3(event) !== frame.raw) {
|
|
587
|
-
diagnostics.push(diagnostic("noncanonical_frame", frame, shape.event_id));
|
|
588
|
-
continue;
|
|
589
|
-
}
|
|
590
634
|
const priorFrame = seenIds.get(shape.event_id);
|
|
591
635
|
if (priorFrame !== undefined) {
|
|
592
636
|
if (priorFrame !== frame.raw) {
|
|
@@ -40,3 +40,65 @@ export function currentHarneryRuntimeBuild(): string {
|
|
|
40
40
|
export function harneryPackageRoot(): string {
|
|
41
41
|
return resolve(dirname(fileURLToPath(import.meta.url)), "../../../..");
|
|
42
42
|
}
|
|
43
|
+
|
|
44
|
+
export interface HarneryBuild {
|
|
45
|
+
version: string;
|
|
46
|
+
revision: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The installed package version and runtime revision; "unknown" when unreadable. */
|
|
50
|
+
export function installedHarneryBuild(): HarneryBuild {
|
|
51
|
+
let version = "unknown";
|
|
52
|
+
let revision = "unknown";
|
|
53
|
+
try {
|
|
54
|
+
const manifest = JSON.parse(
|
|
55
|
+
readFileSync(resolve(harneryPackageRoot(), "package.json"), "utf8"),
|
|
56
|
+
) as { version?: unknown };
|
|
57
|
+
if (typeof manifest.version === "string" && manifest.version.length <= 100)
|
|
58
|
+
version = manifest.version;
|
|
59
|
+
} catch {
|
|
60
|
+
// Missing package metadata cannot establish that a running build is current.
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
revision = currentHarneryRuntimeBuild();
|
|
64
|
+
} catch {
|
|
65
|
+
// Status still reports the unavailable build instead of guessing a match.
|
|
66
|
+
}
|
|
67
|
+
return { version, revision };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface ServiceBuildStatus {
|
|
71
|
+
running_build: HarneryBuild | null;
|
|
72
|
+
installed_build: HarneryBuild;
|
|
73
|
+
restart_needed: boolean;
|
|
74
|
+
build_status: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** A missing or unknown running build, or any difference, means restart. */
|
|
78
|
+
export function serviceBuildStatus(recorded: unknown): ServiceBuildStatus {
|
|
79
|
+
const installed_build = installedHarneryBuild();
|
|
80
|
+
const candidate = recorded as Partial<HarneryBuild> | undefined;
|
|
81
|
+
const known = (value: unknown): value is string =>
|
|
82
|
+
typeof value === "string" && value.length > 0 && value.length <= 100 && value !== "unknown";
|
|
83
|
+
const running_build =
|
|
84
|
+
candidate && known(candidate.version) && known(candidate.revision)
|
|
85
|
+
? { version: candidate.version, revision: candidate.revision }
|
|
86
|
+
: null;
|
|
87
|
+
const restart_needed =
|
|
88
|
+
running_build === null ||
|
|
89
|
+
installed_build.version === "unknown" ||
|
|
90
|
+
installed_build.revision === "unknown" ||
|
|
91
|
+
running_build.version !== installed_build.version ||
|
|
92
|
+
running_build.revision !== installed_build.revision;
|
|
93
|
+
return {
|
|
94
|
+
running_build,
|
|
95
|
+
installed_build,
|
|
96
|
+
restart_needed,
|
|
97
|
+
build_status:
|
|
98
|
+
running_build === null
|
|
99
|
+
? "running build unknown; restart needed"
|
|
100
|
+
: restart_needed
|
|
101
|
+
? "restart needed"
|
|
102
|
+
: "current",
|
|
103
|
+
};
|
|
104
|
+
}
|