pi-daddy 0.22.0 → 0.23.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 +30 -0
- package/contracts/daily-view/v1/README.md +114 -0
- package/contracts/daily-view/v1/fixtures/view.json +525 -0
- package/contracts/daily-view/v1/fixtures/view.txt +75 -0
- package/contracts/daily-view/v1/fixtures/work.jsonl +17 -0
- package/contracts/daily-view/v1/p03/README.md +26 -0
- package/contracts/daily-view/v1/p03/fixtures/retained-executions.json +241 -0
- package/contracts/daily-view/v1/p03/projection.schema.json +194 -0
- package/contracts/daily-view/v1/p03/provenance.json +21 -0
- package/contracts/dashboard-host/v1/README.md +176 -0
- package/contracts/debrief/v1/README.md +146 -0
- package/contracts/debrief/v1/intervention/README.md +32 -0
- package/contracts/debrief/v1/intervention/fixtures/artifact-3ee11136beb864f518252e5024dae9197421fbf97b6e981d3243c782b5c44bd0.txt +1 -0
- package/contracts/debrief/v1/intervention/fixtures/artifact-58905d872dd3c54a9705f67b05fa82edbad7af6476ae8993faf1a7b9fadfb905.txt +1 -0
- package/contracts/debrief/v1/intervention/fixtures/assessment.json +29 -0
- package/contracts/debrief/v1/intervention/fixtures/blind-view.json +20 -0
- package/contracts/debrief/v1/intervention/fixtures/choice.json +7 -0
- package/contracts/debrief/v1/intervention/fixtures/manifest.json +57 -0
- package/contracts/debrief/v1/intervention/fixtures/reveal.json +39 -0
- package/contracts/debrief/v1/provenance.json +89 -0
- package/contracts/debrief/v1/work-capture/README.md +38 -0
- package/contracts/debrief/v1/work-capture/fixtures/batch.json +17 -0
- package/contracts/debrief/v1/work-capture/fixtures/cases.json +184 -0
- package/contracts/debrief/v1/work-capture/fixtures/review.json +184 -0
- package/contracts/debrief/v1/work-capture/review-request.schema.json +41 -0
- package/contracts/debrief/v1/work-capture/work-case.schema.json +190 -0
- package/contracts/debrief/v2/README.md +71 -0
- package/contracts/debrief/v2/input-pin.json +66 -0
- package/contracts/debrief/v2/intervention-upstream.md +40 -0
- package/contracts/debrief/v2/work-signals-upstream.md +22 -0
- package/contracts/dispatch-control/v1/README.md +109 -0
- package/contracts/dispatch-control/v1/request.schema.json +19 -0
- package/contracts/effect-profile/v1/README.md +124 -0
- package/contracts/execution-retention/v1/README.md +109 -0
- package/contracts/execution-retention/v2/README.md +148 -0
- package/contracts/execution-retention/v2/fixtures/interrupted-native-bytes.json +91 -0
- package/contracts/execution-retention/v2/fixtures/missing-native-session.json +91 -0
- package/contracts/execution-retention/v2/fixtures/native-file-unknown-branch.json +91 -0
- package/contracts/execution-retention/v2/fixtures/native-id-without-bytes.json +91 -0
- package/contracts/execution-retention/v2/fixtures/native-live-branch.json +90 -0
- package/contracts/execution-retention/v2/fixtures/replaced-native-session.json +92 -0
- package/contracts/execution-retention/v2/fixtures/result-82c620cdfeb8ca6b858cf653d7f5e2eedca5315d6244216d60e8b36f03223d6a.bin +1 -0
- package/contracts/execution-retention/v2/fixtures/session-2ec1f134bc739984434f7fca689e1c3d1c39fce2dcadae709e9b6faedfcc4ac7.bin +3 -0
- package/contracts/execution-retention/v2/fixtures/session-9bdb4bd16a901da31580acf056bda525225259e15ec409e0410480109011772f.bin +3 -0
- package/contracts/execution-retention/v2/manifest.schema.json +858 -0
- package/contracts/experiment/v1/README.md +164 -0
- package/contracts/factory-order/v1/README.md +151 -0
- package/contracts/factory-order/v1/adoption-pin.json +8 -0
- package/contracts/intent-control/v1/README.md +135 -0
- package/contracts/intent-control/v1/request.schema.json +1096 -0
- package/contracts/intent-control/v2/README.md +34 -0
- package/contracts/intent-control/v2/request.schema.json +1070 -0
- package/contracts/ledger/v4/README.md +241 -0
- package/contracts/ledger/v4/fixtures/layout-options.json +428 -0
- package/contracts/ledger/v4/fixtures/work-acceptance.json +57 -0
- package/contracts/ledger/v4/fixtures/work-occurrence.json +44 -0
- package/contracts/ledger/v4/fixtures/work-revision.json +26 -0
- package/contracts/ledger/v4/fixtures/work-snapshot.json +73 -0
- package/contracts/ledger/v4/ledger-event.schema.json +162 -0
- package/contracts/ordinary-control/v1/README.md +59 -0
- package/contracts/producer-ipc/v1/README.md +137 -0
- package/dist/check-runner.d.ts +5 -0
- package/dist/check-runner.d.ts.map +1 -1
- package/dist/check-runner.js +16 -2
- package/dist/check-runner.js.map +1 -1
- package/dist/control-journal.d.ts +20 -0
- package/dist/control-journal.d.ts.map +1 -0
- package/dist/control-journal.js +73 -0
- package/dist/control-journal.js.map +1 -0
- package/dist/daily-view-input.d.ts +44 -0
- package/dist/daily-view-input.d.ts.map +1 -0
- package/dist/daily-view-input.js +68 -0
- package/dist/daily-view-input.js.map +1 -0
- package/dist/daily-view-render.d.ts +4 -0
- package/dist/daily-view-render.d.ts.map +1 -0
- package/dist/daily-view-render.js +41 -0
- package/dist/daily-view-render.js.map +1 -0
- package/dist/daily-view.d.ts +74 -0
- package/dist/daily-view.d.ts.map +1 -0
- package/dist/daily-view.js +170 -0
- package/dist/daily-view.js.map +1 -0
- package/dist/dashboard-cli.d.ts +26 -1
- package/dist/dashboard-cli.d.ts.map +1 -1
- package/dist/dashboard-cli.js +121 -3
- package/dist/dashboard-cli.js.map +1 -1
- package/dist/dashboard-harness.d.ts +19 -0
- package/dist/dashboard-harness.d.ts.map +1 -0
- package/dist/dashboard-harness.js +75 -0
- package/dist/dashboard-harness.js.map +1 -0
- package/dist/dashboard-herdr.d.ts.map +1 -1
- package/dist/dashboard-herdr.js +7 -2
- package/dist/dashboard-herdr.js.map +1 -1
- package/dist/dashboard-host-contract.d.ts +111 -0
- package/dist/dashboard-host-contract.d.ts.map +1 -0
- package/dist/dashboard-host-contract.js +14 -0
- package/dist/dashboard-host-contract.js.map +1 -0
- package/dist/dashboard-host-transport.d.ts +15 -0
- package/dist/dashboard-host-transport.d.ts.map +1 -0
- package/dist/dashboard-host-transport.js +122 -0
- package/dist/dashboard-host-transport.js.map +1 -0
- package/dist/dashboard-host.d.ts +149 -0
- package/dist/dashboard-host.d.ts.map +1 -0
- package/dist/dashboard-host.js +329 -0
- package/dist/dashboard-host.js.map +1 -0
- package/dist/dashboard-observation.d.ts +28 -0
- package/dist/dashboard-observation.d.ts.map +1 -0
- package/dist/dashboard-observation.js +86 -0
- package/dist/dashboard-observation.js.map +1 -0
- package/dist/debrief-contract.d.ts +65 -0
- package/dist/debrief-contract.d.ts.map +1 -0
- package/dist/debrief-contract.js +151 -0
- package/dist/debrief-contract.js.map +1 -0
- package/dist/debrief-fixture.d.ts +16 -0
- package/dist/debrief-fixture.d.ts.map +1 -0
- package/dist/debrief-fixture.js +41 -0
- package/dist/debrief-fixture.js.map +1 -0
- package/dist/debrief-host.d.ts +59 -0
- package/dist/debrief-host.d.ts.map +1 -0
- package/dist/debrief-host.js +49 -0
- package/dist/debrief-host.js.map +1 -0
- package/dist/debrief-render.d.ts +5 -0
- package/dist/debrief-render.d.ts.map +1 -0
- package/dist/debrief-render.js +65 -0
- package/dist/debrief-render.js.map +1 -0
- package/dist/debrief.d.ts +90 -0
- package/dist/debrief.d.ts.map +1 -0
- package/dist/debrief.js +263 -0
- package/dist/debrief.js.map +1 -0
- package/dist/delegate-types.d.ts +2 -0
- package/dist/delegate-types.d.ts.map +1 -1
- package/dist/delegate-types.js.map +1 -1
- package/dist/delegate.d.ts.map +1 -1
- package/dist/delegate.js +1 -0
- package/dist/delegate.js.map +1 -1
- package/dist/dispatch-control.d.ts +45 -0
- package/dist/dispatch-control.d.ts.map +1 -0
- package/dist/dispatch-control.js +76 -0
- package/dist/dispatch-control.js.map +1 -0
- package/dist/effect-profile-runtime.d.ts +40 -0
- package/dist/effect-profile-runtime.d.ts.map +1 -0
- package/dist/effect-profile-runtime.js +51 -0
- package/dist/effect-profile-runtime.js.map +1 -0
- package/dist/effect-profile.d.ts +40 -0
- package/dist/effect-profile.d.ts.map +1 -0
- package/dist/effect-profile.js +152 -0
- package/dist/effect-profile.js.map +1 -0
- package/dist/execution-retention.d.ts +90 -0
- package/dist/execution-retention.d.ts.map +1 -0
- package/dist/execution-retention.js +247 -0
- package/dist/execution-retention.js.map +1 -0
- package/dist/experiment-contract.d.ts +56 -0
- package/dist/experiment-contract.d.ts.map +1 -0
- package/dist/experiment-contract.js +80 -0
- package/dist/experiment-contract.js.map +1 -0
- package/dist/experiment-state.d.ts +22 -0
- package/dist/experiment-state.d.ts.map +1 -0
- package/dist/experiment-state.js +79 -0
- package/dist/experiment-state.js.map +1 -0
- package/dist/experiment-store.d.ts +28 -0
- package/dist/experiment-store.d.ts.map +1 -0
- package/dist/experiment-store.js +130 -0
- package/dist/experiment-store.js.map +1 -0
- package/dist/experiment.d.ts +65 -0
- package/dist/experiment.d.ts.map +1 -0
- package/dist/experiment.js +357 -0
- package/dist/experiment.js.map +1 -0
- package/dist/factory-contract.d.ts +79 -0
- package/dist/factory-contract.d.ts.map +1 -0
- package/dist/factory-contract.js +89 -0
- package/dist/factory-contract.js.map +1 -0
- package/dist/factory-migration.d.ts +20 -0
- package/dist/factory-migration.d.ts.map +1 -0
- package/dist/factory-migration.js +55 -0
- package/dist/factory-migration.js.map +1 -0
- package/dist/factory-order.d.ts +848 -0
- package/dist/factory-order.d.ts.map +1 -0
- package/dist/factory-order.js +83 -0
- package/dist/factory-order.js.map +1 -0
- package/dist/factory-registry.d.ts +121 -0
- package/dist/factory-registry.d.ts.map +1 -0
- package/dist/factory-registry.js +133 -0
- package/dist/factory-registry.js.map +1 -0
- package/dist/file-lock.d.ts +8 -2
- package/dist/file-lock.d.ts.map +1 -1
- package/dist/file-lock.js +78 -19
- package/dist/file-lock.js.map +1 -1
- package/dist/herdr-poll.d.ts +8 -0
- package/dist/herdr-poll.d.ts.map +1 -1
- package/dist/herdr-poll.js +23 -0
- package/dist/herdr-poll.js.map +1 -1
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -1
- package/dist/intent-application.d.ts +17 -0
- package/dist/intent-application.d.ts.map +1 -0
- package/dist/intent-application.js +113 -0
- package/dist/intent-application.js.map +1 -0
- package/dist/intent-control.d.ts +201 -0
- package/dist/intent-control.d.ts.map +1 -0
- package/dist/intent-control.js +110 -0
- package/dist/intent-control.js.map +1 -0
- package/dist/intent-scheduling.d.ts +9 -0
- package/dist/intent-scheduling.d.ts.map +1 -0
- package/dist/intent-scheduling.js +15 -0
- package/dist/intent-scheduling.js.map +1 -0
- package/dist/ledger-append.d.ts +15 -0
- package/dist/ledger-append.d.ts.map +1 -0
- package/dist/ledger-append.js +94 -0
- package/dist/ledger-append.js.map +1 -0
- package/dist/ledger.d.ts +11 -0
- package/dist/ledger.d.ts.map +1 -1
- package/dist/ledger.js +3 -7
- package/dist/ledger.js.map +1 -1
- package/dist/native-session-target.d.ts +9 -0
- package/dist/native-session-target.d.ts.map +1 -0
- package/dist/native-session-target.js +29 -0
- package/dist/native-session-target.js.map +1 -0
- package/dist/native-session.d.ts +59 -0
- package/dist/native-session.d.ts.map +1 -0
- package/dist/native-session.js +154 -0
- package/dist/native-session.js.map +1 -0
- package/dist/order-schedule.d.ts +37 -0
- package/dist/order-schedule.d.ts.map +1 -0
- package/dist/order-schedule.js +81 -0
- package/dist/order-schedule.js.map +1 -0
- package/dist/ordinary-children.d.ts +52 -0
- package/dist/ordinary-children.d.ts.map +1 -0
- package/dist/ordinary-children.js +93 -0
- package/dist/ordinary-children.js.map +1 -0
- package/dist/producer-ipc-contract.d.ts +29 -0
- package/dist/producer-ipc-contract.d.ts.map +1 -0
- package/dist/producer-ipc-contract.js +29 -0
- package/dist/producer-ipc-contract.js.map +1 -0
- package/dist/producer-ipc.d.ts +55 -0
- package/dist/producer-ipc.d.ts.map +1 -0
- package/dist/producer-ipc.js +143 -0
- package/dist/producer-ipc.js.map +1 -0
- package/dist/resource-budget.d.ts +334 -0
- package/dist/resource-budget.d.ts.map +1 -0
- package/dist/resource-budget.js +415 -0
- package/dist/resource-budget.js.map +1 -0
- package/dist/retention-contract.d.ts +73 -0
- package/dist/retention-contract.d.ts.map +1 -0
- package/dist/retention-contract.js +104 -0
- package/dist/retention-contract.js.map +1 -0
- package/dist/retention-json.d.ts +3 -0
- package/dist/retention-json.d.ts.map +1 -0
- package/dist/retention-json.js +32 -0
- package/dist/retention-json.js.map +1 -0
- package/dist/run-child.d.ts +4 -0
- package/dist/run-child.d.ts.map +1 -1
- package/dist/run-child.js +16 -2
- package/dist/run-child.js.map +1 -1
- package/dist/run-herdr.d.ts +6 -0
- package/dist/run-herdr.d.ts.map +1 -1
- package/dist/run-herdr.js +19 -6
- package/dist/run-herdr.js.map +1 -1
- package/dist/vendor/adoption.d.ts +129 -0
- package/dist/vendor/adoption.d.ts.map +1 -0
- package/dist/vendor/adoption.js +102 -0
- package/dist/vendor/adoption.js.map +1 -0
- package/dist/work-ledger-destination.d.ts +20 -0
- package/dist/work-ledger-destination.d.ts.map +1 -0
- package/dist/work-ledger-destination.js +131 -0
- package/dist/work-ledger-destination.js.map +1 -0
- package/dist/work-ledger-json.d.ts +25 -0
- package/dist/work-ledger-json.d.ts.map +1 -0
- package/dist/work-ledger-json.js +267 -0
- package/dist/work-ledger-json.js.map +1 -0
- package/dist/work-ledger-occurrences.d.ts +227 -0
- package/dist/work-ledger-occurrences.d.ts.map +1 -0
- package/dist/work-ledger-occurrences.js +115 -0
- package/dist/work-ledger-occurrences.js.map +1 -0
- package/dist/work-ledger-projection.d.ts +295 -0
- package/dist/work-ledger-projection.d.ts.map +1 -0
- package/dist/work-ledger-projection.js +237 -0
- package/dist/work-ledger-projection.js.map +1 -0
- package/dist/work-ledger-snapshot.d.ts +29 -0
- package/dist/work-ledger-snapshot.d.ts.map +1 -0
- package/dist/work-ledger-snapshot.js +267 -0
- package/dist/work-ledger-snapshot.js.map +1 -0
- package/dist/work-ledger-types.d.ts +261 -0
- package/dist/work-ledger-types.d.ts.map +1 -0
- package/dist/work-ledger-types.js +13 -0
- package/dist/work-ledger-types.js.map +1 -0
- package/dist/work-ledger-validation.d.ts +14 -0
- package/dist/work-ledger-validation.d.ts.map +1 -0
- package/dist/work-ledger-validation.js +226 -0
- package/dist/work-ledger-validation.js.map +1 -0
- package/dist/work-ledger.d.ts +43 -0
- package/dist/work-ledger.d.ts.map +1 -0
- package/dist/work-ledger.js +225 -0
- package/dist/work-ledger.js.map +1 -0
- package/extensions/delegate-chain.ts +1 -0
- package/extensions/delegation-native.ts +12 -0
- package/extensions/delegation.ts +4 -3
- package/extensions/execute-child.ts +34 -4
- package/extensions/grants.ts +2 -0
- package/extensions/ordinary-runtime.ts +14 -0
- package/extensions/run-delegation.ts +7 -7
- package/extensions/session.ts +3 -1
- package/package.json +78 -4
- package/src/check-runner.ts +20 -2
- package/src/control-journal.ts +26 -0
- package/src/daily-view-input.ts +59 -0
- package/src/daily-view-render.ts +44 -0
- package/src/daily-view.ts +152 -0
- package/src/dashboard-cli.ts +103 -4
- package/src/dashboard-harness.ts +28 -0
- package/src/dashboard-herdr.ts +7 -2
- package/src/dashboard-host-contract.ts +42 -0
- package/src/dashboard-host-transport.ts +38 -0
- package/src/dashboard-host.ts +168 -0
- package/src/dashboard-observation.ts +44 -0
- package/src/debrief-contract.ts +127 -0
- package/src/debrief-fixture.ts +29 -0
- package/src/debrief-host.ts +53 -0
- package/src/debrief-render.ts +49 -0
- package/src/debrief.ts +198 -0
- package/src/delegate-types.ts +2 -0
- package/src/delegate.ts +1 -0
- package/src/dispatch-control.ts +81 -0
- package/src/effect-profile-runtime.ts +46 -0
- package/src/effect-profile.ts +129 -0
- package/src/execution-retention.ts +230 -0
- package/src/experiment-contract.ts +66 -0
- package/src/experiment-state.ts +44 -0
- package/src/experiment-store.ts +90 -0
- package/src/experiment.ts +231 -0
- package/src/factory-contract.ts +47 -0
- package/src/factory-migration.ts +29 -0
- package/src/factory-order.ts +56 -0
- package/src/factory-registry.ts +60 -0
- package/src/file-lock.ts +47 -11
- package/src/herdr-poll.ts +18 -0
- package/src/index.ts +32 -0
- package/src/intent-application.ts +90 -0
- package/src/intent-control.ts +107 -0
- package/src/intent-scheduling.ts +14 -0
- package/src/ledger-append.ts +72 -0
- package/src/ledger.ts +3 -9
- package/src/native-session-target.ts +23 -0
- package/src/native-session.ts +139 -0
- package/src/order-schedule.ts +42 -0
- package/src/ordinary-children.ts +59 -0
- package/src/producer-ipc-contract.ts +26 -0
- package/src/producer-ipc.ts +107 -0
- package/src/resource-budget.ts +349 -0
- package/src/retention-contract.ts +84 -0
- package/src/retention-json.ts +20 -0
- package/src/run-child.ts +13 -2
- package/src/run-herdr.ts +11 -7
- package/src/vendor/adoption.ts +109 -0
- package/src/work-ledger-destination.ts +113 -0
- package/src/work-ledger-json.ts +228 -0
- package/src/work-ledger-occurrences.ts +99 -0
- package/src/work-ledger-projection.ts +262 -0
- package/src/work-ledger-snapshot.ts +246 -0
- package/src/work-ledger-types.ts +229 -0
- package/src/work-ledger-validation.ts +174 -0
- package/src/work-ledger.ts +186 -0
package/src/debrief.ts
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { reviewPage, reviewRequest, matchingHistory, validatedHistory, blindView, blindChoice, byteDigest, dataDigest, dataKey, detached, freeze,
|
|
2
|
+
sha, closed, type SignalSelection, type ReviewPort, type ReviewItem, type ReviewRequest, type BlindPort, type BlindView, type BlindChoice } from "./debrief-contract.ts";
|
|
3
|
+
export interface DebriefCheckpoint {
|
|
4
|
+
version: "debrief-checkpoint-v1" | "debrief-checkpoint-v2"; bindingDigest?: string; scope: string; offset: number; operatorIdentity: string | null; caseIds: string[]; blindDigest: string | null;
|
|
5
|
+
requests: ReviewRequest[]; choice: BlindChoice | null; revealRequested: boolean;
|
|
6
|
+
}
|
|
7
|
+
/** Host-owned attention/transport checkpoint, NOT a dashboard decision ledger. CAS must be durable/atomic. */
|
|
8
|
+
export interface DebriefPersistence {
|
|
9
|
+
durability: "host-owned" | "fixture-memory";
|
|
10
|
+
load(): unknown | Promise<unknown>;
|
|
11
|
+
compareAndSwap(expectedDigest: string | null, next: DebriefCheckpoint): void | Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
export interface DebriefHost {
|
|
14
|
+
scope: string; offset?: number; operatorIdentity?: string; reviewer: ReviewPort; blind?: BlindPort; persistence?: DebriefPersistence;
|
|
15
|
+
fixture?: boolean;
|
|
16
|
+
signals?: SignalSelection;
|
|
17
|
+
blindBinding?: { comparisonId: string; author: string };
|
|
18
|
+
}
|
|
19
|
+
export interface DebriefFrame {
|
|
20
|
+
version: "debrief-view-v1"; mode: "manual"; state: string; persistence: "host-checkpoint" | "session-only";
|
|
21
|
+
total: number; unexposed: number; budgetSpent: number | null; remainingBudget: number | null; cards: ({ kind: "case"; slot: number; caseManifestId: string; priorDecisionId: string | null;
|
|
22
|
+
summary: string; disposition: string; resolution: "unresolved" | "label-recorded"; actionEnabled: boolean } |
|
|
23
|
+
{ kind: "blind"; slot: number; variants: { label: string; text: string[] }[]; choice: BlindChoice | null; choiceConfirmed: boolean;
|
|
24
|
+
actionEnabled: boolean; revealed: unknown | null })[];
|
|
25
|
+
observation?: { batchId: string; id: string; issues: string[] };
|
|
26
|
+
fixture: boolean; acceptance: "not-assessed"; automatic: "unqualified"; freshness: "snapshot-unknown";
|
|
27
|
+
}
|
|
28
|
+
const presenters = new WeakSet<object>();
|
|
29
|
+
export const isDebriefPresenter = (p: unknown): p is DebriefPresenter => typeof p === "object" && p !== null && presenters.has(p);
|
|
30
|
+
export type DebriefPresenter = ReturnType<typeof createDebriefPresenter>;
|
|
31
|
+
const reasons: Record<string, string> = { repeat_without_progress: "Repeated attempts without recorded progress", economical_exemplar: "Candidate economical exemplar", coverage_gap: "Missing evidence coverage", overdue_checkpoint: "Declared checkpoint overdue", reopened_acceptance: "Previously accepted binding reopened", intent_conflict: "Declared objective intent conflict" };
|
|
32
|
+
function checkpoint(value: unknown, scope: string, offset: number, operatorIdentity: string | null, bindingDigest: string | null): DebriefCheckpoint | null {
|
|
33
|
+
if (value === null) return null;
|
|
34
|
+
const c = detached(value) as DebriefCheckpoint;
|
|
35
|
+
if (!c || Object.keys(c).sort().join() !== (bindingDigest ? "bindingDigest," : "") + "blindDigest,caseIds,choice,offset,operatorIdentity,requests,revealRequested,scope,version" || c.version !== (bindingDigest ? "debrief-checkpoint-v2" : "debrief-checkpoint-v1") || (bindingDigest !== null && c.bindingDigest !== bindingDigest) || c.scope !== scope || c.offset !== offset || c.operatorIdentity !== operatorIdentity || !Array.isArray(c.caseIds) ||
|
|
36
|
+
new Set(c.caseIds).size !== c.caseIds.length || c.caseIds.length + (c.blindDigest ? 1 : 0) > 5 || c.caseIds.some(id => !/^[a-f0-9]{64}$/.test(id)) ||
|
|
37
|
+
!(c.blindDigest === null || /^[a-f0-9]{64}$/.test(c.blindDigest)) || !Array.isArray(c.requests) || c.requests.length > c.caseIds.length || typeof c.revealRequested !== "boolean" ||
|
|
38
|
+
new Set(c.requests.map(r => r.caseManifestId)).size !== c.requests.length || c.requests.some(r => !c.caseIds.includes(reviewRequest(r).caseManifestId)) ||
|
|
39
|
+
(!c.blindDigest && (c.choice !== null || c.revealRequested)) || (c.revealRequested && !c.choice)) throw new Error("invalid host presentation checkpoint");
|
|
40
|
+
return c;
|
|
41
|
+
}
|
|
42
|
+
/** Manual host path only. Neither events nor caller-shaped idle/pause claims qualify automatic exposure. */
|
|
43
|
+
export function createDebriefPresenter(host: DebriefHost) {
|
|
44
|
+
if (typeof host.scope !== "string" || !host.scope || host.scope.length > 256) throw new Error("explicit host presentation scope required");
|
|
45
|
+
const scope = host.scope, fixture = host.fixture === true, offset = host.offset ?? 0, operatorIdentity = host.operatorIdentity ?? null;
|
|
46
|
+
if (operatorIdentity !== null && (typeof operatorIdentity !== "string" || !operatorIdentity || operatorIdentity.length > 512 || /[\u0000-\u001f\u007f]/.test(operatorIdentity))) throw new Error("invalid independent operator identity");
|
|
47
|
+
if (!Number.isSafeInteger(offset) || offset < 0 || offset > 4096) throw new Error("invalid independently selected queue offset");
|
|
48
|
+
const signals = host.signals ? freeze(detached(host.signals)) : undefined, blindBinding = host.blindBinding ? freeze(detached(host.blindBinding)) : undefined;
|
|
49
|
+
if (signals) { closed(signals, ["batchId", "observationId"]); if (!sha(signals.batchId) || !sha(signals.observationId)) throw new Error("exact signal selection required"); }
|
|
50
|
+
if (blindBinding) { closed(blindBinding, ["comparisonId", "author"]); if (!sha(blindBinding.comparisonId) || blindBinding.author !== operatorIdentity || !host.blind?.quality) throw new Error("independent durable blind binding/quality reader required"); }
|
|
51
|
+
const bindingDigest = signals || blindBinding ? dataDigest({ signals: signals ?? null, blind: blindBinding ?? null }) : null;
|
|
52
|
+
const reviewer: ReviewPort = Object.freeze({ list: host.reviewer.list.bind(host.reviewer), history: host.reviewer.history.bind(host.reviewer),
|
|
53
|
+
...(host.reviewer.decide ? { decide: host.reviewer.decide.bind(host.reviewer) } : {}) });
|
|
54
|
+
const blind: BlindPort | undefined = host.blind ? Object.freeze({ view: host.blind.view.bind(host.blind), readArtifact: host.blind.readArtifact.bind(host.blind),
|
|
55
|
+
choose: host.blind.choose.bind(host.blind), reveal: host.blind.reveal.bind(host.blind), ...(host.blind.quality ? { quality: host.blind.quality.bind(host.blind) } : {}) }) : undefined;
|
|
56
|
+
const persistence: DebriefPersistence | undefined = host.persistence ? Object.freeze({ durability: host.persistence.durability,
|
|
57
|
+
load: host.persistence.load.bind(host.persistence), compareAndSwap: host.persistence.compareAndSwap.bind(host.persistence) }) : undefined;
|
|
58
|
+
let saved: DebriefCheckpoint | null = null, items: ReviewItem[] = [], publicBlind: BlindView | null = null;
|
|
59
|
+
let checkpointKnown = !persistence;
|
|
60
|
+
let issues: string[] = [];
|
|
61
|
+
let total = 0, state = "closed", busy = false, poisoned = false, choiceConfirmed = false, revealed: unknown | null = null;
|
|
62
|
+
let variants: { label: string; text: string[] }[] = [];
|
|
63
|
+
const outcomes = new Map<string, { disposition: string; prior: string | null; resolution: "unresolved" | "label-recorded" }>();
|
|
64
|
+
const enabled = () => Boolean(operatorIdentity && persistence && (persistence.durability === "host-owned" || fixture) && reviewer.decide && !poisoned);
|
|
65
|
+
const persist = async (next: DebriefCheckpoint) => {
|
|
66
|
+
if (persistence) {
|
|
67
|
+
await persistence.compareAndSwap(saved ? dataDigest(saved) : null, detached(next));
|
|
68
|
+
const observed = checkpoint(await persistence.load(), scope, offset, operatorIdentity, bindingDigest);
|
|
69
|
+
if (!observed || dataDigest(observed) !== dataDigest(next)) throw new Error("checkpoint acknowledgement has no exact readback");
|
|
70
|
+
}
|
|
71
|
+
saved = freeze(detached(next));
|
|
72
|
+
};
|
|
73
|
+
const exclusive = async <T>(run: () => Promise<T>): Promise<T> => {
|
|
74
|
+
if (busy || poisoned) throw new Error("debrief busy or persistence unknown"); busy = true;
|
|
75
|
+
try { return await run(); } finally { busy = false; }
|
|
76
|
+
};
|
|
77
|
+
const reconcileItem = async (item: ReviewItem, request: ReviewRequest) => {
|
|
78
|
+
try {
|
|
79
|
+
const id = matchingHistory(await reviewer.history(item.caseManifestId), item, request, operatorIdentity ?? "");
|
|
80
|
+
outcomes.set(item.caseManifestId, { disposition: id ? request.disposition : "unresolved", prior: id ?? item.priorDecisionId,
|
|
81
|
+
resolution: id && !["skip", "uncertain"].includes(request.disposition) ? "label-recorded" : "unresolved" });
|
|
82
|
+
return id ? "recorded" : "stale-or-unknown";
|
|
83
|
+
} catch { outcomes.set(item.caseManifestId, { disposition: "unresolved", prior: item.priorDecisionId, resolution: "unresolved" }); return "unknown"; }
|
|
84
|
+
};
|
|
85
|
+
const api = {
|
|
86
|
+
async open(input: { mode: "manual" | "automatic"; userPresent: boolean; boundary?: string }) {
|
|
87
|
+
return exclusive(async () => {
|
|
88
|
+
if (input.mode !== "manual" || !input.userPresent || input.boundary === "busy") { state = "deferred: automatic/absent/busy boundary unqualified"; return api.view(); }
|
|
89
|
+
try {
|
|
90
|
+
if (persistence) {
|
|
91
|
+
const loaded = checkpoint(await persistence.load(), scope, offset, operatorIdentity, bindingDigest);
|
|
92
|
+
if (saved && (!loaded || dataKey(saved.caseIds) !== dataKey(loaded.caseIds) || saved.blindDigest !== loaded.blindDigest ||
|
|
93
|
+
saved.choice && dataKey(saved.choice) !== dataKey(loaded.choice) || saved.requests.some(r => !loaded.requests.some(n => dataKey(n) === dataKey(r))) || saved.revealRequested && !loaded.revealRequested)) throw new Error("checkpoint rollback/change; no new allowance");
|
|
94
|
+
saved = loaded; checkpointKnown = true;
|
|
95
|
+
}
|
|
96
|
+
publicBlind = blind ? blindView(await blind.view()) : null;
|
|
97
|
+
const page = reviewPage(await reviewer.list(offset, publicBlind ? 4 : 5), publicBlind ? 4 : 5, offset, signals); total = page.total; issues = page.issues ?? [];
|
|
98
|
+
if (!saved) await persist({ version: bindingDigest ? "debrief-checkpoint-v2" : "debrief-checkpoint-v1", ...(bindingDigest ? { bindingDigest } : {}), scope, offset, operatorIdentity, caseIds: page.items.map(i => i.caseManifestId), blindDigest: publicBlind ? dataDigest(publicBlind) : null, requests: [], choice: null, revealRequested: false });
|
|
99
|
+
if (saved!.blindDigest !== (publicBlind ? dataDigest(publicBlind) : null) || saved!.caseIds.some(id => !page.items.some(i => i.caseManifestId === id))) throw new Error("changed batch/blind identity; no fresh allowance");
|
|
100
|
+
items = saved!.caseIds.map(id => page.items.find(i => i.caseManifestId === id)!);
|
|
101
|
+
if (saved!.choice && publicBlind) blindChoice(saved!.choice, publicBlind);
|
|
102
|
+
for (const item of items) {
|
|
103
|
+
const latest = validatedHistory(await reviewer.history(item.caseManifestId), item);
|
|
104
|
+
const consistent = (latest?.id ?? null) === item.priorDecisionId && (latest?.disposition ?? "unresolved") === item.disposition;
|
|
105
|
+
outcomes.set(item.caseManifestId, { disposition: consistent ? item.disposition : "unresolved", prior: item.priorDecisionId, resolution: !consistent || ["unresolved", "skip", "uncertain"].includes(item.disposition) ? "unresolved" : "label-recorded" });
|
|
106
|
+
const request = saved!.requests.find(r => r.caseManifestId === item.caseManifestId); if (request) await reconcileItem(item, request);
|
|
107
|
+
}
|
|
108
|
+
variants = [];
|
|
109
|
+
for (const c of publicBlind?.cards ?? []) {
|
|
110
|
+
const text: string[] = [];
|
|
111
|
+
for (const digest of c.artifactDigests) {
|
|
112
|
+
const bytes = await blind!.readArtifact(c.label, digest);
|
|
113
|
+
if (!(bytes instanceof Uint8Array) || bytes.byteLength > 65536 || byteDigest(bytes) !== digest) throw new Error("blind artifact unavailable/mismatched");
|
|
114
|
+
let preview = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
115
|
+
if (fixture) preview = preview.replace(/Synthetic Layout [AB]/g, "Synthetic layout (fixture caption removed)");
|
|
116
|
+
text.push(preview.slice(0, 320) + (preview.length > 320 ? " [preview truncated]" : ""));
|
|
117
|
+
}
|
|
118
|
+
variants.push({ label: c.label, text });
|
|
119
|
+
}
|
|
120
|
+
if (blindBinding) {
|
|
121
|
+
choiceConfirmed = false; revealed = null;
|
|
122
|
+
const value = await blind!.quality!();
|
|
123
|
+
if (value !== null) {
|
|
124
|
+
const quality = blindChoice(value as BlindChoice, publicBlind!);
|
|
125
|
+
if (saved!.choice && dataKey(saved!.choice) !== dataKey(quality)) throw new Error("durable quality changed");
|
|
126
|
+
if (!saved!.choice) await persist({ ...saved!, choice: quality });
|
|
127
|
+
choiceConfirmed = true;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
state = "open"; return api.view();
|
|
131
|
+
} catch { state = "unavailable: host data/persistence unknown"; poisoned = true; return api.view(); }
|
|
132
|
+
});
|
|
133
|
+
},
|
|
134
|
+
close() { state = "closed"; return api.view(); },
|
|
135
|
+
view(): Readonly<DebriefFrame> {
|
|
136
|
+
const cards: DebriefFrame["cards"] = state === "open" ? items.map((item, i) => ({ kind: "case", slot: i + 1, caseManifestId: item.caseManifestId,
|
|
137
|
+
priorDecisionId: outcomes.get(item.caseManifestId)?.prior ?? item.priorDecisionId, summary: reasons[item.candidate.reason] ?? "Unresolved case",
|
|
138
|
+
disposition: outcomes.get(item.caseManifestId)?.disposition ?? "unresolved", resolution: outcomes.get(item.caseManifestId)?.resolution ?? "unresolved",
|
|
139
|
+
actionEnabled: enabled() && !saved?.requests.some(r => r.caseManifestId === item.caseManifestId) })) : [];
|
|
140
|
+
if (state === "open" && publicBlind) cards.push({ kind: "blind", slot: cards.length + 1, variants, choice: saved?.choice ?? null, choiceConfirmed,
|
|
141
|
+
actionEnabled: Boolean(blind && (fixture || operatorIdentity) && persistence && (persistence.durability === "host-owned" || fixture) && !poisoned), revealed });
|
|
142
|
+
return freeze(detached({ version: "debrief-view-v1", mode: "manual", state, persistence: persistence?.durability === "host-owned" ? "host-checkpoint" : "session-only", total,
|
|
143
|
+
unexposed: Math.max(0, total - items.length), budgetSpent: saved ? saved.caseIds.length + (saved.blindDigest ? 1 : 0) : poisoned || !checkpointKnown ? null : 0,
|
|
144
|
+
...(signals ? { observation: { batchId: signals.batchId, id: signals.observationId, issues } } : {}),
|
|
145
|
+
remainingBudget: !checkpointKnown || poisoned && !saved ? null : 5 - (saved ? saved.caseIds.length + (saved.blindDigest ? 1 : 0) : 0), cards, fixture, acceptance: "not-assessed", automatic: "unqualified", freshness: "snapshot-unknown" }));
|
|
146
|
+
},
|
|
147
|
+
async label(input: ReviewRequest) {
|
|
148
|
+
const request = reviewRequest(input);
|
|
149
|
+
return exclusive(async () => {
|
|
150
|
+
if (state !== "open" || !enabled()) throw new Error("genuine host writer/persistence unavailable");
|
|
151
|
+
const item = items.find(i => i.caseManifestId === request.caseManifestId); if (!item) throw new Error("case not exposed in this budget");
|
|
152
|
+
const prior = saved!.requests.find(r => r.caseManifestId === item.caseManifestId);
|
|
153
|
+
if (prior) { if (dataKey(prior) !== dataKey(request)) throw new Error("one immutable label request per card/pause"); return reconcileItem(item, request); }
|
|
154
|
+
if (request.priorDecisionId !== item.priorDecisionId) throw new Error("stale displayed label request");
|
|
155
|
+
try { await persist({ ...saved!, requests: [...saved!.requests, request] }); }
|
|
156
|
+
catch { poisoned = true; throw new Error("label request persistence unknown; no writer call"); }
|
|
157
|
+
outcomes.set(item.caseManifestId, { disposition: "unresolved", prior: item.priorDecisionId, resolution: "unresolved" });
|
|
158
|
+
try { await reviewer.decide!(request); } catch { return "unknown"; }
|
|
159
|
+
return reconcileItem(item, request);
|
|
160
|
+
});
|
|
161
|
+
},
|
|
162
|
+
async reconcile(caseManifestId: string) {
|
|
163
|
+
return exclusive(async () => {
|
|
164
|
+
const item = items.find(i => i.caseManifestId === caseManifestId), request = saved?.requests.find(r => r.caseManifestId === caseManifestId);
|
|
165
|
+
if (!item || !request) throw new Error("no exact pending request"); return reconcileItem(item, request);
|
|
166
|
+
});
|
|
167
|
+
},
|
|
168
|
+
async choose(input: BlindChoice) {
|
|
169
|
+
return exclusive(async () => {
|
|
170
|
+
if (state !== "open" || !blind || !publicBlind || !persistence || !fixture && !operatorIdentity || persistence.durability !== "host-owned" && !fixture) throw new Error("quality persistence/host unavailable");
|
|
171
|
+
const choice = blindChoice(input, publicBlind);
|
|
172
|
+
if (saved!.choice && dataKey(saved!.choice) !== dataKey(choice)) throw new Error("quality choice is locked");
|
|
173
|
+
try { if (!saved!.choice) await persist({ ...saved!, choice }); }
|
|
174
|
+
catch { poisoned = true; throw new Error("quality persistence unknown; no reveal"); }
|
|
175
|
+
const chosen = blindChoice(await blind.choose(choice) as BlindChoice, publicBlind);
|
|
176
|
+
if (dataKey(chosen) !== dataKey(choice)) throw new Error("quality acknowledgement mismatch");
|
|
177
|
+
if (blindBinding && dataKey(blindChoice(await blind.quality!() as BlindChoice, publicBlind)) !== dataKey(choice)) throw new Error("durable quality readback mismatch");
|
|
178
|
+
choiceConfirmed = true; return api.view();
|
|
179
|
+
});
|
|
180
|
+
},
|
|
181
|
+
async reveal() {
|
|
182
|
+
return exclusive(async () => {
|
|
183
|
+
if (state !== "open" || !blind || !saved?.choice || !choiceConfirmed || !persistence || !fixture && !operatorIdentity) throw new Error("confirmed quality choice required before reveal");
|
|
184
|
+
try { if (!saved.revealRequested) await persist({ ...saved, revealRequested: true }); }
|
|
185
|
+
catch { poisoned = true; throw new Error("reveal persistence unknown; no exposure"); }
|
|
186
|
+
if (blindBinding && dataKey(blindChoice(await blind.quality!() as BlindChoice, publicBlind!)) !== dataKey(saved.choice)) throw new Error("durable quality unavailable/changed");
|
|
187
|
+
const result = detached(await blind.reveal()) as Record<string, any>;
|
|
188
|
+
if (!result || Object.keys(result).sort().join() !== "arms,choice,manifestId,routingDefault" || result.routingDefault !== null || !Array.isArray(result.arms) || result.arms.length !== publicBlind!.cards.length || new Set(result.arms.map(a => a.label)).size !== result.arms.length || dataKey(blindChoice(result.choice, publicBlind!)) !== dataKey(saved.choice)) throw new Error("invalid reveal acknowledgement");
|
|
189
|
+
revealed = { arms: result.arms.map(a => {
|
|
190
|
+
if (Object.keys(a).sort().join() !== "armId,configuration,cost,costUnit,label" || !publicBlind!.cards.some(c => c.label === a.label) || !["wall_ms", "tool_calls", "usd"].includes(a.costUnit) || !(a.cost === null || typeof a.cost === "number" && Number.isFinite(a.cost) && a.cost >= 0) || typeof a.configuration !== "object" || a.configuration === null || Object.keys(a.configuration).sort().join() !== "configuration,effort,model,prompt,skill" || Object.values(a.configuration).some(v => typeof v !== "string")) throw new Error("invalid reveal arm");
|
|
191
|
+
return { label: a.label, configuration: a.configuration, cost: a.cost, costUnit: a.costUnit };
|
|
192
|
+
}), routingDefault: null, adoptionAuthorized: false };
|
|
193
|
+
return api.view();
|
|
194
|
+
});
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
presenters.add(api); return Object.freeze(api);
|
|
198
|
+
}
|
package/src/delegate-types.ts
CHANGED
|
@@ -82,6 +82,8 @@ export interface DelegationContext {
|
|
|
82
82
|
* non-interactive plan, because a governed child should not sit waiting for a human by accident.
|
|
83
83
|
*/
|
|
84
84
|
interactive?: boolean;
|
|
85
|
+
/** Explicit host-owned pi persistence target; never model-facing and never enabled by archive observation. */
|
|
86
|
+
sessionFile?: string;
|
|
85
87
|
/**
|
|
86
88
|
* Total descendants this session may still create (`src/fanout.ts`). Split among children by the caller.
|
|
87
89
|
*
|
package/src/delegate.ts
CHANGED
|
@@ -266,6 +266,7 @@ export function planDelegation(request: DelegationRequest, ctx: DelegationContex
|
|
|
266
266
|
skillPaths: ctx.skillPaths,
|
|
267
267
|
contextFiles: ctx.contextFiles,
|
|
268
268
|
systemPrompt,
|
|
269
|
+
sessionFile: ctx.sessionFile,
|
|
269
270
|
print: ctx.interactive ? false : undefined,
|
|
270
271
|
});
|
|
271
272
|
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { canonicalWorkJson, copyWorkJson, parseWorkJson } from "./work-ledger-json.ts";
|
|
3
|
+
|
|
4
|
+
export interface DispatchRequest {
|
|
5
|
+
version: "1.0"; requestId: string; bindingDigest: string; expectedRevision: number;
|
|
6
|
+
action: "pause-dispatch" | "resume-dispatch" | "cancel-execution" | "revise-scope" | "reprioritize" | "select-alternative";
|
|
7
|
+
targetExecutionId: string | null;
|
|
8
|
+
}
|
|
9
|
+
/** Independent trusted host declarations, NOT a wire request field or a file loaded by the controller. */
|
|
10
|
+
export interface DispatchAuthority { authorityDigest: string; requestDigests: readonly string[] }
|
|
11
|
+
export interface DispatchRecord {
|
|
12
|
+
request: Readonly<DispatchRequest>; digest: string;
|
|
13
|
+
decision: "approved" | "authority-unavailable" | "unsupported" | "stale" | "busy";
|
|
14
|
+
application: "pending" | "applied" | "not-applied";
|
|
15
|
+
outcome: "boundary-pending" | "paused" | "enabled" | "refused";
|
|
16
|
+
}
|
|
17
|
+
export interface DispatchState { revision: number; paused: boolean; records: DispatchRecord[] }
|
|
18
|
+
export interface DispatchSnapshot {
|
|
19
|
+
version: "dispatch-control-snapshot-v1"; bindingDigest: string; freshness: "snapshot-unknown"; revision: number; paused: boolean;
|
|
20
|
+
admission: "blocked-pending" | "paused" | "enabled"; acceptance: "not-assessed";
|
|
21
|
+
records: readonly Readonly<DispatchRecord>[];
|
|
22
|
+
}
|
|
23
|
+
const hex = (x: unknown): x is string => typeof x === "string" && /^[a-f0-9]{64}$/.test(x);
|
|
24
|
+
const id = (x: unknown): x is string => typeof x === "string" && /^[a-zA-Z0-9:_-]{1,128}$/.test(x);
|
|
25
|
+
export function controlShape(value: unknown, keys: string[]): asserts value is Record<string, unknown> {
|
|
26
|
+
if (!value || typeof value !== "object" || ![Object.prototype, null].includes(Object.getPrototypeOf(value)) ||
|
|
27
|
+
Reflect.ownKeys(value).length !== keys.length || keys.some(k => { const d = Object.getOwnPropertyDescriptor(value, k); return !d?.enumerable || !Object.hasOwn(d, "value"); })) throw new TypeError("closed dispatch control data required");
|
|
28
|
+
}
|
|
29
|
+
export function dispatchRequest(input: DispatchRequest): Readonly<DispatchRequest> {
|
|
30
|
+
controlShape(input, ["version", "requestId", "bindingDigest", "expectedRevision", "action", "targetExecutionId"]);
|
|
31
|
+
if (input.version !== "1.0" || !id(input.requestId) || !hex(input.bindingDigest) || !Number.isSafeInteger(input.expectedRevision) || input.expectedRevision < 0 || input.expectedRevision > 128 ||
|
|
32
|
+
!["pause-dispatch", "resume-dispatch", "cancel-execution", "revise-scope", "reprioritize", "select-alternative"].includes(input.action) ||
|
|
33
|
+
!(input.targetExecutionId === null || id(input.targetExecutionId)) || (input.action === "cancel-execution" ? input.targetExecutionId === null : input.targetExecutionId !== null)) throw new TypeError("invalid dispatch request");
|
|
34
|
+
return Object.freeze({ version: input.version, requestId: input.requestId, bindingDigest: input.bindingDigest,
|
|
35
|
+
expectedRevision: input.expectedRevision, action: input.action, targetExecutionId: input.targetExecutionId });
|
|
36
|
+
}
|
|
37
|
+
export function parseDispatchRequest(text: string): Readonly<DispatchRequest> {
|
|
38
|
+
if (typeof text !== "string" || Buffer.byteLength(text) > 4096) throw new TypeError("bounded dispatch JSON required");
|
|
39
|
+
return dispatchRequest(parseWorkJson(text) as unknown as DispatchRequest);
|
|
40
|
+
}
|
|
41
|
+
export function dispatchRequestDigest(input: DispatchRequest): string {
|
|
42
|
+
return createHash("sha256").update(canonicalWorkJson(copyWorkJson(dispatchRequest(input)))).digest("hex");
|
|
43
|
+
}
|
|
44
|
+
export function dispatchAuthority(input: DispatchAuthority | null): Readonly<DispatchAuthority> | null {
|
|
45
|
+
if (input === null) return null;
|
|
46
|
+
controlShape(input, ["authorityDigest", "requestDigests"]);
|
|
47
|
+
const copy = copyWorkJson(input.requestDigests);
|
|
48
|
+
if (!hex(input.authorityDigest) || !Array.isArray(copy) || copy.length > 128 || !copy.every(hex) || new Set(copy).size !== copy.length) throw new TypeError("invalid independent dispatch authority");
|
|
49
|
+
return Object.freeze({ authorityDigest: input.authorityDigest, requestDigests: Object.freeze([...copy] as string[]) });
|
|
50
|
+
}
|
|
51
|
+
export function dispatchDecision(state: DispatchState, request: DispatchRequest, authorized: boolean, otherPending = false): DispatchRecord["decision"] {
|
|
52
|
+
if (!authorized) return "authority-unavailable";
|
|
53
|
+
if (!["pause-dispatch", "resume-dispatch"].includes(request.action)) return "unsupported";
|
|
54
|
+
if (request.expectedRevision !== state.revision) return "stale";
|
|
55
|
+
if (otherPending || state.records.some(r => r.application === "pending")) return "busy";
|
|
56
|
+
return "approved";
|
|
57
|
+
}
|
|
58
|
+
/** Replay operational controls only; no work-intent or acceptance projection is maintained here. */
|
|
59
|
+
export function replayDispatch(state: DispatchState, event: Record<string, unknown>, bindingDigest: string, active: number, otherPending = false): void {
|
|
60
|
+
if (event.type === "control-request") {
|
|
61
|
+
controlShape(event, ["type", "request", "decision"]);
|
|
62
|
+
const request = dispatchRequest(event.request as DispatchRequest), decision = event.decision as DispatchRecord["decision"];
|
|
63
|
+
if (state.records.length >= 128 || request.bindingDigest !== bindingDigest || state.records.some(r => r.request.requestId === request.requestId) ||
|
|
64
|
+
!["approved", "authority-unavailable", "unsupported", "stale", "busy"].includes(decision) ||
|
|
65
|
+
dispatchDecision(state, request, decision !== "authority-unavailable", otherPending) !== decision) throw new Error("invalid dispatch decision sequence");
|
|
66
|
+
state.records.push({ request, digest: dispatchRequestDigest(request), decision,
|
|
67
|
+
application: decision === "approved" ? "pending" : "not-applied", outcome: decision === "approved" ? "boundary-pending" : "refused" });
|
|
68
|
+
if (decision === "approved") state.revision++;
|
|
69
|
+
} else {
|
|
70
|
+
controlShape(event, ["type", "requestId"]);
|
|
71
|
+
const record = state.records.find(r => r.request.requestId === event.requestId);
|
|
72
|
+
if (event.type !== "control-apply" || !record || record.application !== "pending" || active !== 0) throw new Error("unverified dispatch application boundary");
|
|
73
|
+
state.paused = record.request.action === "pause-dispatch";
|
|
74
|
+
record.application = "applied"; record.outcome = state.paused ? "paused" : "enabled";
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export function freezeDispatch(state: DispatchState, bindingDigest: string, otherPending = false): Readonly<DispatchSnapshot> {
|
|
78
|
+
return Object.freeze({ version: "dispatch-control-snapshot-v1", bindingDigest, freshness: "snapshot-unknown", revision: state.revision, paused: state.paused,
|
|
79
|
+
admission: otherPending || state.records.some(r => r.application === "pending") ? "blocked-pending" : state.paused ? "paused" : "enabled",
|
|
80
|
+
acceptance: "not-assessed", records: Object.freeze(state.records.map(r => Object.freeze({ ...r }))) });
|
|
81
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { lstat, readFile, realpath } from "node:fs/promises";
|
|
3
|
+
|
|
4
|
+
export const DIGEST_PROFILE = "linux-bwrap-digest-v1" as const;
|
|
5
|
+
export const DIGEST_WORKER = `const b=Buffer.from(process.argv[1],'base64');if(b.length>16384)process.exit(90);process.stdout.write(JSON.stringify({bytes:b.length,sha256:require('node:crypto').createHash('sha256').update(b).digest('hex')})+'\\n');`;
|
|
6
|
+
export interface DigestRuntime { node: string; fingerprints: Readonly<Record<string, string>> }
|
|
7
|
+
function executableDetails(path: string, st: { isFile(): boolean; mode: number; size: number }) {
|
|
8
|
+
return { path, regular: st.isFile(), mode: (st.mode & 0o7777).toString(8), size: st.size,
|
|
9
|
+
predicates: { notRegular: !st.isFile(), writable: (st.mode & 0o022) !== 0, oversize: st.size > 256 * 1024 * 1024 } };
|
|
10
|
+
}
|
|
11
|
+
/** Read-only independent observations; no installation, admission or namespace qualification. */
|
|
12
|
+
export async function inspectDigestPrerequisites() {
|
|
13
|
+
const observations = [];
|
|
14
|
+
for (const requested of [process.execPath, "/usr/bin/bwrap", "/usr/bin/prlimit"]) {
|
|
15
|
+
try { const path = requested === process.execPath ? await realpath(requested) : requested;
|
|
16
|
+
const details = executableDetails(path, await lstat(path)); observations.push({ requested, ...details, conforms: !Object.values(details.predicates).some(Boolean) });
|
|
17
|
+
} catch (error) { observations.push({ requested, conforms: false, error: { code: (error as NodeJS.ErrnoException).code ?? null, message: String(error) } }); }
|
|
18
|
+
}
|
|
19
|
+
return { version: "digest-prerequisites-v1", platform: process.platform, nodeVersion: process.version, execPath: process.execPath,
|
|
20
|
+
conforms: process.platform === "linux" && observations.every(o => o.conforms), observations, qualification: "not-assessed" };
|
|
21
|
+
}
|
|
22
|
+
export async function digestRuntime(): Promise<DigestRuntime> {
|
|
23
|
+
if (process.platform !== "linux") throw new Error("Linux user/mount/PID/network namespaces required");
|
|
24
|
+
const node = await realpath(process.execPath), fingerprints: Record<string, string> = {};
|
|
25
|
+
for (const path of [node, "/usr/bin/bwrap", "/usr/bin/prlimit"]) {
|
|
26
|
+
const st = await lstat(path);
|
|
27
|
+
if (!st.isFile() || (st.mode & 0o022) !== 0 || st.size > 256 * 1024 * 1024) {
|
|
28
|
+
const details = executableDetails(path, st), code = details.predicates.notRegular ? "RUNTIME_NOT_REGULAR" : details.predicates.writable ? "RUNTIME_WRITABLE" : "RUNTIME_TOO_LARGE";
|
|
29
|
+
throw Object.assign(new Error("unsupported mutable runtime executable"), { code, path, details });
|
|
30
|
+
}
|
|
31
|
+
fingerprints[path] = createHash("sha256").update(await readFile(path)).digest("hex");
|
|
32
|
+
}
|
|
33
|
+
return Object.freeze({ node, fingerprints: Object.freeze(fingerprints) });
|
|
34
|
+
}
|
|
35
|
+
/** Internal fixed boundary builder. No user command/environment/workspace/destination interface. */
|
|
36
|
+
export function digestNamespaceArgs(runtime: DigestRuntime, code: string, args: string[], fixture?: string): string[] {
|
|
37
|
+
return ["--unshare-all", "--die-with-parent", "--new-session", "--cap-drop", "ALL",
|
|
38
|
+
"--ro-bind", "/usr", "/usr", "--ro-bind", "/lib", "/lib", "--ro-bind", "/lib64", "/lib64",
|
|
39
|
+
"--dir", "/runtime", "--ro-bind", runtime.node, "/runtime/node",
|
|
40
|
+
...(fixture ? ["--ro-bind", fixture, "/input"] : []),
|
|
41
|
+
"--proc", "/proc", "--dev", "/dev", "--clearenv", "--chdir", "/", "--",
|
|
42
|
+
"/usr/bin/prlimit", "--cpu=2:2", "--nofile=64:64", "--", "/runtime/node",
|
|
43
|
+
// No arbitrary code enters the production worker. Permission flags are additional tripwires,
|
|
44
|
+
// not a claim that Node's permission model is a hostile-JavaScript sandbox.
|
|
45
|
+
...(fixture ? [] : ["--permission"]), "--max-old-space-size=32", "-e", code, ...args];
|
|
46
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdtemp, mkdir, readlink, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { runChild, type ChildRunResult } from "./run-child.ts";
|
|
5
|
+
import { runWithFinalizers } from "./finalization.ts";
|
|
6
|
+
import { beginExecutionRetention, retentionConfigurationDigest } from "./execution-retention.ts";
|
|
7
|
+
import { openResourceBudget, resourceBindingDigest, type AttemptDemand, type ResourcePermit, type GovernedBudgetBinding } from "./resource-budget.ts";
|
|
8
|
+
import { intentKey, type IntentAdmission } from "./intent-control.ts";
|
|
9
|
+
import { DIGEST_PROFILE, DIGEST_WORKER, digestNamespaceArgs, digestRuntime, type DigestRuntime } from "./effect-profile-runtime.ts";
|
|
10
|
+
export { DIGEST_PROFILE } from "./effect-profile-runtime.ts";
|
|
11
|
+
|
|
12
|
+
export interface DigestProfile { readonly profile: typeof DIGEST_PROFILE; readonly bindingDigest: string; readonly runtimeDigest: string }
|
|
13
|
+
export class EffectProfileUnavailableError extends Error {
|
|
14
|
+
constructor(message: string) { super(message); this.name = "EffectProfileUnavailableError"; }
|
|
15
|
+
}
|
|
16
|
+
const profiles = new WeakMap<DigestProfile, { runtime: DigestRuntime; budget: ReturnType<typeof openResourceBudget> }>();
|
|
17
|
+
export const isDigestProfile = (value: unknown): value is DigestProfile => typeof value === "object" && value !== null && profiles.has(value as DigestProfile);
|
|
18
|
+
const hash = (bytes: Uint8Array | string) => createHash("sha256").update(bytes).digest("hex");
|
|
19
|
+
function requireClean(result: ChildRunResult): void {
|
|
20
|
+
if (result.code !== 0 || result.spawnError || result.aborted || result.timedOut || result.truncated) throw new EffectProfileUnavailableError(`fixed native profile failed: ${result.spawnError ?? result.text ?? result.code}`);
|
|
21
|
+
}
|
|
22
|
+
const child = (runtime: DigestRuntime, code: string, args: string[], fixture?: string, signal?: AbortSignal) => runChild({
|
|
23
|
+
command: "/usr/bin/bwrap", args: digestNamespaceArgs(runtime, code, args, fixture), cwd: "/", env: {},
|
|
24
|
+
timeoutMs: 3000, killGraceMs: 50, hardDeadlineAt: Date.now() + 3500, maxOutputBytes: 4096, signal,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
/** Actual bounded native probes, not an executable-presence flag. Trusted fixture code only. */
|
|
28
|
+
export async function prepareDigestProfile(binding: GovernedBudgetBinding): Promise<Readonly<DigestProfile>> {
|
|
29
|
+
const budget = openResourceBudget(binding);
|
|
30
|
+
await budget.inspect(); // Required admission state failures remain failures, not an unavailable fallback.
|
|
31
|
+
const runtime = await digestRuntime();
|
|
32
|
+
const root = await mkdtemp(join(budget.binding.directory, "profile-probe-")), input = join(root, "input");
|
|
33
|
+
return runWithFinalizers(async () => {
|
|
34
|
+
await mkdir(input, { mode: 0o700 }); await writeFile(join(input, "allowed"), "owned-readable", { mode: 0o600 });
|
|
35
|
+
await writeFile(join(root, "outside"), "owned-outside", { mode: 0o600 });
|
|
36
|
+
const net = await readlink("/proc/self/ns/net");
|
|
37
|
+
const code = `const fs=require('node:fs');let failures=0;for(const fn of [()=>fs.readFileSync(${JSON.stringify(join(root, "outside"))}),()=>fs.writeFileSync('/input/denied','bad')]){try{fn()}catch(e){if(['ENOENT','EROFS'].includes(e.code))failures++}}if(failures!==2||fs.readFileSync('/input/allowed','utf8')!=='owned-readable'||fs.readlinkSync('/proc/self/ns/net')===${JSON.stringify(net)})process.exit(91);console.log('boundary-ok');`;
|
|
38
|
+
const boundary = await child(runtime, code, [], input); requireClean(boundary);
|
|
39
|
+
if (boundary.text.trim() !== "boundary-ok") throw new EffectProfileUnavailableError("native boundary probe did not establish its expected observations");
|
|
40
|
+
const permission = await child(runtime, `try{require('node:child_process').spawnSync('/runtime/node',['-e','process.exit(92)']);process.exit(93)}catch(e){if(e.code!=='ERR_ACCESS_DENIED')process.exit(94);console.log('descendant-denied')}`, []);
|
|
41
|
+
requireClean(permission);
|
|
42
|
+
if (permission.text.trim() !== "descendant-denied") throw new EffectProfileUnavailableError("descendant tripwire unavailable");
|
|
43
|
+
const sample = await child(runtime, DIGEST_WORKER, [Buffer.from("fixture").toString("base64")]); requireClean(sample);
|
|
44
|
+
if (sample.text !== JSON.stringify({ bytes: 7, sha256: hash("fixture") }) + "\n") throw new EffectProfileUnavailableError("fixed worker probe mismatch");
|
|
45
|
+
const result = Object.freeze({ profile: DIGEST_PROFILE, bindingDigest: resourceBindingDigest(budget.binding), runtimeDigest: retentionConfigurationDigest(runtime) });
|
|
46
|
+
profiles.set(result, { runtime, budget });
|
|
47
|
+
return result;
|
|
48
|
+
}, [{ label: "profile probe cleanup failed", run: () => rm(root, { recursive: true, force: true }) }]);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface DigestAttempt { attempt: Omit<AttemptDemand, "inputBytes" | "inputDigest">; bytes: Uint8Array; intent?: IntentAdmission }
|
|
52
|
+
/** A fixed, useful non-shell operation: hash at most 16 KiB. No writable destinations or provider calls.
|
|
53
|
+
* The opaque profile is bound to one budget. A claimed profile name/receipt cannot authorize a launch. */
|
|
54
|
+
export async function runDigestProfile(profile: DigestProfile, request: DigestAttempt, signal?: AbortSignal): Promise<Readonly<{
|
|
55
|
+
attemptId: string; output: ChildRunResult; digest: string | null;
|
|
56
|
+
}>> {
|
|
57
|
+
const prepared = profiles.get(profile);
|
|
58
|
+
if (!prepared || profile.profile !== DIGEST_PROFILE) throw new EffectProfileUnavailableError("unsupported or unprobed effect profile; no launch");
|
|
59
|
+
const requestKeys = ["attempt", "bytes", ...(request && Object.hasOwn(request, "intent") ? ["intent"] : [])];
|
|
60
|
+
if (!request || typeof request !== "object" || Reflect.ownKeys(request).length !== requestKeys.length ||
|
|
61
|
+
!requestKeys.every(k => { const d = Object.getOwnPropertyDescriptor(request, k); return d && d.enumerable && Object.hasOwn(d, "value"); }) ||
|
|
62
|
+
!(request.bytes instanceof Uint8Array) || request.bytes.byteLength > 16384) throw new TypeError("bounded byte-only request required; no destination, code, money or workspace override");
|
|
63
|
+
const bytes = Buffer.from(request.bytes), inputDigest = hash(bytes);
|
|
64
|
+
const selectedIntent: IntentAdmission | undefined = Object.hasOwn(request, "intent") ? JSON.parse(intentKey(request.intent)) : undefined;
|
|
65
|
+
const descriptors = Object.getOwnPropertyDescriptors(request.attempt);
|
|
66
|
+
const keys = ["attemptId", "orderId", "experimentId", "kind", "parentAttemptId"];
|
|
67
|
+
if (Reflect.ownKeys(request.attempt).length !== keys.length || keys.some(k => !descriptors[k]?.enumerable || !Object.hasOwn(descriptors[k], "value"))) throw new TypeError("closed attempt identity required");
|
|
68
|
+
const attempt = Object.fromEntries(keys.map(k => [k, descriptors[k].value])) as DigestAttempt["attempt"];
|
|
69
|
+
if (signal?.aborted) throw new EffectProfileUnavailableError("cancelled before reservation/launch");
|
|
70
|
+
if (retentionConfigurationDigest(await digestRuntime()) !== profile.runtimeDigest) throw new EffectProfileUnavailableError("runtime changed since the native profile probe");
|
|
71
|
+
const permit = await prepared.budget.reserve({ ...attempt, inputBytes: bytes.length, inputDigest }, selectedIntent);
|
|
72
|
+
return executeReserved(profile, prepared.runtime, attempt, bytes, permit, "digest", signal);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export type FixedDigestOperation = "digest" | "hold";
|
|
76
|
+
export interface ReservedDigestRun { readonly started: Promise<"spawned" | "settled-without-spawn">; run(signal?: AbortSignal): ReturnType<typeof runDigestProfile> }
|
|
77
|
+
/** Atomic all-attempt admission; only minted closures hold the original permits. No caller-supplied permit. */
|
|
78
|
+
export async function reserveDigestProfileBatch(profile: DigestProfile, requests: readonly { attempt: DigestAttempt["attempt"]; bytes: Uint8Array; operation: FixedDigestOperation }[]): Promise<readonly ReservedDigestRun[]> {
|
|
79
|
+
const prepared = profiles.get(profile);
|
|
80
|
+
if (!prepared || !Array.isArray(requests) || !requests.length || requests.length > 32 || Reflect.ownKeys(requests).length !== requests.length + 1 || Array.from({ length: requests.length }, (_, i) => Object.getOwnPropertyDescriptor(requests, String(i))).some(d => !d?.enumerable || !Object.hasOwn(d, "value"))) throw new EffectProfileUnavailableError("probed profile and bounded batch required");
|
|
81
|
+
const detached = requests.map(r => {
|
|
82
|
+
const descriptors = r && Object.getOwnPropertyDescriptors(r);
|
|
83
|
+
if (!descriptors || Reflect.ownKeys(r).length !== 3 || ["attempt", "bytes", "operation"].some(k => !descriptors[k]?.enumerable || !Object.hasOwn(descriptors[k], "value"))) throw new TypeError("closed data variant required");
|
|
84
|
+
if (!r || !["digest", "hold"].includes(r.operation) || !(r.bytes instanceof Uint8Array) || r.bytes.length > 16384 || Object.keys(r).sort().join() !== "attempt,bytes,operation") throw new TypeError("fixed byte-only variant required");
|
|
85
|
+
return { attempt: JSON.parse(intentKey(r.attempt)) as DigestAttempt["attempt"], bytes: Buffer.from(r.bytes), operation: r.operation };
|
|
86
|
+
});
|
|
87
|
+
if (retentionConfigurationDigest(await digestRuntime()) !== profile.runtimeDigest) throw new EffectProfileUnavailableError("runtime changed before batch");
|
|
88
|
+
const permits = await prepared.budget.reserveBatch(detached.map(r => ({ ...r.attempt, inputBytes: r.bytes.length, inputDigest: hash(r.bytes) })));
|
|
89
|
+
return Object.freeze(detached.map((r, i) => {
|
|
90
|
+
let used = false, resolveStarted!: (value: "spawned" | "settled-without-spawn") => void;
|
|
91
|
+
const started = { promise: new Promise<"spawned" | "settled-without-spawn">(resolve => { resolveStarted = resolve; }), resolve: (value: "spawned" | "settled-without-spawn") => resolveStarted(value) };
|
|
92
|
+
return Object.freeze({ started: started.promise, async run(signal?: AbortSignal) {
|
|
93
|
+
if (used) throw new Error("reserved execution already consumed; no replay"); used = true;
|
|
94
|
+
try { return await executeReserved(profile, prepared.runtime, r.attempt, r.bytes, permits[i], r.operation, signal, () => started.resolve("spawned")); }
|
|
95
|
+
finally { started.resolve("settled-without-spawn"); } // Settled-without-spawn also unblocks waiters; never a spawn/identity claim.
|
|
96
|
+
} });
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function executeReserved(profile: DigestProfile, runtime: DigestRuntime, attempt: DigestAttempt["attempt"], bytes: Buffer, permit: ResourcePermit, operation: FixedDigestOperation, signal?: AbortSignal, started?: () => void) {
|
|
101
|
+
const inputDigest = hash(bytes), code = operation === "digest" ? DIGEST_WORKER : "setInterval(()=>{},1000);";
|
|
102
|
+
// From this point every outcome costs the full reservation. No posthoc result refunds resources.
|
|
103
|
+
const retention = beginExecutionRetention({ executionId: attempt.attemptId, parentExecutionId: attempt.parentAttemptId,
|
|
104
|
+
childId: "digest", toolCallId: null, executor: "process", taskDigest: inputDigest, definitionDigest: hash(code),
|
|
105
|
+
configurationDigest: retentionConfigurationDigest({ profile: profile.profile, binding: profile.bindingDigest, runtime: profile.runtimeDigest }), workspaceId: null });
|
|
106
|
+
let output: ChildRunResult | undefined;
|
|
107
|
+
let outcome: "completed" | "failed" | "cancelled" = "failed";
|
|
108
|
+
let accountingFailed = false, spawned = false, exitObserved = false;
|
|
109
|
+
return runWithFinalizers(async () => {
|
|
110
|
+
if (!signal?.aborted && retentionConfigurationDigest(await digestRuntime()) !== profile.runtimeDigest) throw new EffectProfileUnavailableError("runtime changed after admission");
|
|
111
|
+
output = await runChild({ command: "/usr/bin/bwrap", args: digestNamespaceArgs(runtime, code, [bytes.toString("base64")]),
|
|
112
|
+
cwd: "/", env: {}, signal, timeoutMs: 3000, killGraceMs: 50, hardDeadlineAt: Date.now() + 3500, maxOutputBytes: 4096,
|
|
113
|
+
onSpawn: pid => { spawned = true; retention.native({ pid }); started?.(); }, onObservation: (stream, data) => retention.capture(stream, data) });
|
|
114
|
+
exitObserved = output.code !== null || Boolean(output.signal);
|
|
115
|
+
if (spawned && !exitObserved) throw new Error("owned executor exit unknown; reservation retained");
|
|
116
|
+
const completed = output.code === 0 && !output.aborted && !output.spawnError && !output.timedOut && !output.truncated;
|
|
117
|
+
if (completed && output.text !== JSON.stringify({ bytes: bytes.length, sha256: inputDigest }) + "\n") throw new Error("fixed worker result mismatch");
|
|
118
|
+
outcome = output.aborted && (!spawned || Boolean(output.signal)) ? "cancelled" : completed ? "completed" : "failed";
|
|
119
|
+
retention.capture("result", Buffer.from(output.text), true);
|
|
120
|
+
return Object.freeze({ attemptId: attempt.attemptId, output, digest: completed ? inputDigest : null });
|
|
121
|
+
}, [{ label: "required resource settlement failed", run: async () => {
|
|
122
|
+
// Only this live controller, after executor settlement, releases a slot. Cleanup failure still rejects.
|
|
123
|
+
// Crashes/lost controllers keep their slots; reopening has no expiry, receipt-credit or reclaim API.
|
|
124
|
+
try { if (spawned && !exitObserved) throw new Error("owned executor exit unknown; no settlement credit"); await permit.settle(outcome); } catch (error) { accountingFailed = true; throw error; }
|
|
125
|
+
} }, { label: "retention observation failed", run: () => {
|
|
126
|
+
retention.finish({ code: output?.code ?? null, signal: output?.signal ?? null, timedOut: output?.timedOut ?? false,
|
|
127
|
+
aborted: output?.aborted ?? Boolean(signal?.aborted), truncated: output?.truncated ?? false, failed: outcome !== "completed" || accountingFailed });
|
|
128
|
+
} }]);
|
|
129
|
+
}
|