harnery 0.45.2 → 0.46.1
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/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/rules/claim-conflict.d.ts.map +1 -1
- package/dist/core/agents/rules/claim-conflict.js +36 -16
- package/dist/core/agents/rules/stop-hook.d.ts +6 -0
- package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
- package/dist/core/agents/rules/stop-hook.js +20 -1
- 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/config.d.ts +39 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +63 -0
- 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/validate.d.ts.map +1 -1
- package/dist/core/events/v3/validate.js +36 -1
- 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 +103 -5
- 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 +0 -23
- package/dist/core/hooks/resolve/transcript.d.ts +12 -0
- package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
- package/dist/core/hooks/resolve/transcript.js +54 -0
- 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/semantic/contract.d.ts +5 -5
- 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/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/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/rules/claim-conflict.ts +50 -18
- package/src/core/agents/rules/stop-hook.ts +28 -1
- 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/config.ts +85 -0
- 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/validate.ts +42 -1
- package/src/core/hooks/adapter/native-parent.ts +109 -0
- package/src/core/hooks/cli.ts +110 -4
- package/src/core/hooks/resolve/owner.ts +0 -23
- package/src/core/hooks/resolve/transcript.ts +61 -0
- 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/storage/modes.ts +6 -0
- package/src/core/storage/segments.ts +5 -4
- 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
|
@@ -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) {
|
|
@@ -45,8 +45,49 @@ interface RecoveryShape {
|
|
|
45
45
|
elapsed_upper_bound_ms?: ObservationShape;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
type EventVariantSchema = (typeof EventV3Schema.anyOf)[number];
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Every union member requires its own literal `event_type`, so only the member
|
|
52
|
+
* named by a value's `event_type` can accept it. Checking that one member gives
|
|
53
|
+
* the union's verdict without trying the other variants, which more than halves
|
|
54
|
+
* a full-ledger read. Returns null, falling back to the whole union, if a member
|
|
55
|
+
* ever stops carrying a required unique literal.
|
|
56
|
+
*/
|
|
57
|
+
let eventVariantsByType: Map<string, EventVariantSchema> | null | undefined;
|
|
58
|
+
|
|
59
|
+
function eventVariantsByTypeV3(): Map<string, EventVariantSchema> | null {
|
|
60
|
+
if (eventVariantsByType !== undefined) return eventVariantsByType;
|
|
61
|
+
const variants = new Map<string, EventVariantSchema>();
|
|
62
|
+
for (const variant of EventV3Schema.anyOf) {
|
|
63
|
+
const literal = (variant.properties as Record<string, { const?: unknown }>).event_type?.const;
|
|
64
|
+
if (
|
|
65
|
+
typeof literal !== "string" ||
|
|
66
|
+
!(variant.required as readonly string[] | undefined)?.includes("event_type") ||
|
|
67
|
+
variants.has(literal)
|
|
68
|
+
) {
|
|
69
|
+
eventVariantsByType = null;
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
variants.set(literal, variant);
|
|
73
|
+
}
|
|
74
|
+
eventVariantsByType = variants;
|
|
75
|
+
return variants;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function checkEventSchemaV3(value: unknown): boolean {
|
|
79
|
+
const variants = eventVariantsByTypeV3();
|
|
80
|
+
if (!variants) return Value.Check(EventV3Schema, value);
|
|
81
|
+
const eventType =
|
|
82
|
+
typeof value === "object" && value !== null
|
|
83
|
+
? (value as { event_type?: unknown }).event_type
|
|
84
|
+
: undefined;
|
|
85
|
+
const variant = typeof eventType === "string" ? variants.get(eventType) : undefined;
|
|
86
|
+
return variant !== undefined && Value.Check(variant, value);
|
|
87
|
+
}
|
|
88
|
+
|
|
48
89
|
export function validateEventV3(value: unknown): EventV3ValidationResult {
|
|
49
|
-
if (!
|
|
90
|
+
if (!checkEventSchemaV3(value)) {
|
|
50
91
|
return {
|
|
51
92
|
ok: false,
|
|
52
93
|
issues: [...Value.Errors(EventV3Schema, value)]
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { closeSync, constants, fstatSync, lstatSync, openSync, readSync } from "node:fs";
|
|
3
|
+
import { discoverCodexSessionTranscript } from "./runtime-telemetry.ts";
|
|
4
|
+
|
|
5
|
+
export type NativeParentObservation =
|
|
6
|
+
| { state: "observed"; parent_session_id: string; source_witness: string }
|
|
7
|
+
| { state: "none"; reason: string; source_witness: string }
|
|
8
|
+
| { state: "unknown"; reason: string };
|
|
9
|
+
|
|
10
|
+
export interface NativeParentRequest {
|
|
11
|
+
adapter: string;
|
|
12
|
+
native_session_id?: string;
|
|
13
|
+
transcript_path?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface NativeParentReaderOptions {
|
|
17
|
+
discoverTranscript?: (sessionId: string, transcriptPath?: string) => string | undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const HEADER_LIMIT = 1024 * 1024;
|
|
21
|
+
const NATIVE_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
22
|
+
const ORDINARY_SOURCES = new Set(["cli", "exec", "vscode", "app-server"]);
|
|
23
|
+
|
|
24
|
+
function object(value: unknown): Record<string, unknown> | undefined {
|
|
25
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
26
|
+
? (value as Record<string, unknown>)
|
|
27
|
+
: undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Native relationship evidence only; no prompt bodies, state writes or recipient authority. */
|
|
31
|
+
export function readNativeParentReference(
|
|
32
|
+
request: NativeParentRequest,
|
|
33
|
+
options: NativeParentReaderOptions = {},
|
|
34
|
+
): NativeParentObservation {
|
|
35
|
+
if (request.adapter !== "codex") return unknown("native_parent_adapter_unsupported");
|
|
36
|
+
const child = request.native_session_id;
|
|
37
|
+
if (!child || !NATIVE_ID.test(child)) return unknown("native_child_identity_unavailable");
|
|
38
|
+
let fd: number | undefined;
|
|
39
|
+
try {
|
|
40
|
+
const path = (options.discoverTranscript ?? discoverCodexSessionTranscript)(
|
|
41
|
+
child,
|
|
42
|
+
request.transcript_path,
|
|
43
|
+
);
|
|
44
|
+
if (!path) return unknown("native_parent_transcript_unavailable");
|
|
45
|
+
const entry = lstatSync(path);
|
|
46
|
+
if (!entry.isFile() || entry.isSymbolicLink()) return unknown("native_parent_not_regular");
|
|
47
|
+
fd = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
48
|
+
const before = fstatSync(fd);
|
|
49
|
+
if (!before.isFile() || before.dev !== entry.dev || before.ino !== entry.ino) {
|
|
50
|
+
return unknown("native_parent_file_changed");
|
|
51
|
+
}
|
|
52
|
+
const header = readHeader(fd);
|
|
53
|
+
if (!header) return unknown("native_parent_header_incomplete");
|
|
54
|
+
// Appending later rows is normal. The same header must remain unchanged.
|
|
55
|
+
const checked = readHeader(fd);
|
|
56
|
+
const after = lstatSync(path);
|
|
57
|
+
if (
|
|
58
|
+
!checked?.equals(header) ||
|
|
59
|
+
!after.isFile() ||
|
|
60
|
+
after.isSymbolicLink() ||
|
|
61
|
+
before.dev !== after.dev ||
|
|
62
|
+
before.ino !== after.ino
|
|
63
|
+
) {
|
|
64
|
+
return unknown("native_parent_file_changed");
|
|
65
|
+
}
|
|
66
|
+
const row = object(JSON.parse(header.toString("utf8")));
|
|
67
|
+
const payload = object(row?.payload);
|
|
68
|
+
if (row?.type !== "session_meta" || payload?.id !== child) {
|
|
69
|
+
return unknown("native_parent_child_mismatch");
|
|
70
|
+
}
|
|
71
|
+
const source_witness = createHash("sha256").update(header).digest("hex");
|
|
72
|
+
if (typeof payload.source === "string" && ORDINARY_SOURCES.has(payload.source)) {
|
|
73
|
+
return { state: "none", reason: "native_ordinary_session", source_witness };
|
|
74
|
+
}
|
|
75
|
+
const source = object(payload.source);
|
|
76
|
+
const spawn = object(object(source?.subagent)?.thread_spawn);
|
|
77
|
+
const parent = spawn?.parent_thread_id;
|
|
78
|
+
if (typeof parent !== "string" || !NATIVE_ID.test(parent)) {
|
|
79
|
+
return unknown("native_parent_not_reported");
|
|
80
|
+
}
|
|
81
|
+
if (parent.toLowerCase() === child.toLowerCase())
|
|
82
|
+
return unknown("native_parent_self_reference");
|
|
83
|
+
return { state: "observed", parent_session_id: parent, source_witness };
|
|
84
|
+
} catch {
|
|
85
|
+
return unknown("native_parent_metadata_unreadable");
|
|
86
|
+
} finally {
|
|
87
|
+
if (fd !== undefined) closeSync(fd);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function readHeader(fd: number): Buffer | undefined {
|
|
92
|
+
const chunks: Buffer[] = [];
|
|
93
|
+
let offset = 0;
|
|
94
|
+
while (offset < HEADER_LIMIT) {
|
|
95
|
+
const buffer = Buffer.alloc(Math.min(16 * 1024, HEADER_LIMIT - offset));
|
|
96
|
+
const bytes = readSync(fd, buffer, 0, buffer.length, offset);
|
|
97
|
+
if (bytes === 0) return undefined;
|
|
98
|
+
const data = buffer.subarray(0, bytes);
|
|
99
|
+
const newline = data.indexOf(10);
|
|
100
|
+
if (newline >= 0) return Buffer.concat([...chunks, data.subarray(0, newline)]);
|
|
101
|
+
chunks.push(data);
|
|
102
|
+
offset += bytes;
|
|
103
|
+
}
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function unknown(reason: string): NativeParentObservation {
|
|
108
|
+
return { state: "unknown", reason };
|
|
109
|
+
}
|