codex-chatgpt-control 0.5.1-alpha.2 → 0.5.1-alpha.4
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 +42 -0
- package/README.md +43 -0
- package/contracts/v1/fixtures/backend-capabilities.json +40 -1
- package/contracts/v1/fixtures/backend-compatibility.json +21 -0
- package/contracts/v1/fixtures/backend-version.json +6 -1
- package/contracts/v1/fixtures/download-blocked-by-browser.json +22 -0
- package/contracts/v1/fixtures/download-receipt-failed.json +22 -0
- package/contracts/v1/fixtures/download-receipt-timeout.json +22 -0
- package/contracts/v1/fixtures/journal-rpc-indeterminate.json +32 -0
- package/contracts/v1/fixtures/journal-rpc-unsupported-platform.json +32 -0
- package/contracts/v1/fixtures/journal-runtime-unavailable.json +32 -0
- package/contracts/v1/fixtures/operation-action-prepared-event.json +42 -0
- package/contracts/v1/fixtures/operation-action.json +13 -0
- package/contracts/v1/fixtures/operation-artifact-receipt.json +14 -0
- package/contracts/v1/fixtures/operation-artifact-transfer-intent-event.json +24 -0
- package/contracts/v1/fixtures/operation-artifact-transfer-receipt-event.json +26 -0
- package/contracts/v1/fixtures/operation-artifact-transfer-state.json +178 -0
- package/contracts/v1/fixtures/operation-blocker.json +10 -0
- package/contracts/v1/fixtures/operation-collect-request.json +17 -0
- package/contracts/v1/fixtures/operation-collect-result.json +42 -0
- package/contracts/v1/fixtures/operation-control-receipt.json +13 -0
- package/contracts/v1/fixtures/operation-control-request.json +18 -0
- package/contracts/v1/fixtures/operation-control-result.json +31 -0
- package/contracts/v1/fixtures/operation-event.json +18 -0
- package/contracts/v1/fixtures/operation-handle.json +10 -0
- package/contracts/v1/fixtures/operation-inspect-request.json +13 -0
- package/contracts/v1/fixtures/operation-inspect-result.json +205 -0
- package/contracts/v1/fixtures/operation-ownership-baseline-event.json +34 -0
- package/contracts/v1/fixtures/operation-receipt.json +32 -0
- package/contracts/v1/fixtures/operation-recovery-decision.json +7 -0
- package/contracts/v1/fixtures/operation-recovery-observation.json +12 -0
- package/contracts/v1/fixtures/operation-request.json +33 -0
- package/contracts/v1/fixtures/operation-state.json +144 -0
- package/contracts/v1/fixtures/operation-submission-witness-event.json +20 -0
- package/contracts/v1/fixtures/operation-submission-witness.json +11 -0
- package/contracts/v1/fixtures/operation-submit-result.json +16 -0
- package/contracts/v1/fixtures/operation-target-established-event.json +21 -0
- package/contracts/v1/manifest.json +185 -1
- package/contracts/v1/parity-suite.json +106 -8
- package/contracts/v1/schemas/backend-compatibility.schema.json +65 -0
- package/contracts/v1/schemas/backend-request.schema.json +8 -2
- package/contracts/v1/schemas/capabilities.schema.json +81 -1
- package/contracts/v1/schemas/manifest.schema.json +57 -0
- package/contracts/v1/schemas/operation-action.schema.json +155 -0
- package/contracts/v1/schemas/operation-artifact-receipt.schema.json +89 -0
- package/contracts/v1/schemas/operation-blocker.schema.json +70 -0
- package/contracts/v1/schemas/operation-collect-request.schema.json +46 -0
- package/contracts/v1/schemas/operation-collect-result.schema.json +74 -0
- package/contracts/v1/schemas/operation-control-receipt.schema.json +108 -0
- package/contracts/v1/schemas/operation-control-request.schema.json +111 -0
- package/contracts/v1/schemas/operation-control-result.schema.json +112 -0
- package/contracts/v1/schemas/operation-event.schema.json +756 -0
- package/contracts/v1/schemas/operation-handle.schema.json +55 -0
- package/contracts/v1/schemas/operation-inspect-request.schema.json +42 -0
- package/contracts/v1/schemas/operation-inspect-result.schema.json +1662 -0
- package/contracts/v1/schemas/operation-receipt.schema.json +153 -0
- package/contracts/v1/schemas/operation-recovery.schema.json +314 -0
- package/contracts/v1/schemas/operation-request.schema.json +158 -0
- package/contracts/v1/schemas/operation-state.schema.json +691 -0
- package/contracts/v1/schemas/operation-submission-witness.schema.json +48 -0
- package/contracts/v1/schemas/operation-submit-result.schema.json +94 -0
- package/contracts/v1/surface-drift-policy.json +50 -0
- package/contracts/v1/vectors/operation-request-digest-v1.json +43 -0
- package/dist/codex-chatgpt-control-backend.mjs +45882 -9361
- package/dist/codex-chatgpt-control-journal.mjs +5547 -0
- package/dist/codex-chatgpt-control-live-smoke.bundle.mjs +51677 -0
- package/dist/codex-chatgpt-control-release-canary.bundle.mjs +52104 -0
- package/dist/codex-chatgpt-control.bundle.mjs +48392 -10260
- package/dist/src/backend/client.d.ts +103 -2
- package/dist/src/backend/client.js +1436 -97
- package/dist/src/backend/compatibility.d.ts +12 -0
- package/dist/src/backend/compatibility.js +208 -0
- package/dist/src/backend/protocol.d.ts +84 -1
- package/dist/src/backend/protocol.js +31 -5
- package/dist/src/backend/runtime-identity.d.ts +10 -0
- package/dist/src/backend/runtime-identity.js +141 -0
- package/dist/src/backend/session.d.ts +10 -2
- package/dist/src/backend/session.js +478 -7
- package/dist/src/backend/stdio-server.d.ts +4 -1
- package/dist/src/backend/stdio-server.js +240 -28
- package/dist/src/browser/active-composer-file-input.d.ts +9 -0
- package/dist/src/browser/active-composer-file-input.js +33 -0
- package/dist/src/browser/attach.d.ts +4 -1
- package/dist/src/browser/attach.js +136 -28
- package/dist/src/browser/chatgpt-url.d.ts +2 -0
- package/dist/src/browser/chatgpt-url.js +9 -0
- package/dist/src/browser/downloads.d.ts +17 -1
- package/dist/src/browser/downloads.js +150 -28
- package/dist/src/browser/page-state.js +6 -0
- package/dist/src/client.d.ts +43 -0
- package/dist/src/client.js +1279 -59
- package/dist/src/commands/artifacts.js +6 -4
- package/dist/src/commands/chat-popover.d.ts +37 -0
- package/dist/src/commands/chat-popover.js +537 -0
- package/dist/src/commands/configuration.d.ts +5 -0
- package/dist/src/commands/configuration.js +134 -21
- package/dist/src/commands/context.js +10 -3
- package/dist/src/commands/doctor.d.ts +1 -1
- package/dist/src/commands/doctor.js +27 -4
- package/dist/src/commands/experience.d.ts +5 -0
- package/dist/src/commands/experience.js +104 -21
- package/dist/src/commands/files.js +14 -33
- package/dist/src/commands/modes.js +85 -21
- package/dist/src/commands/power-discovery.d.ts +130 -0
- package/dist/src/commands/power-discovery.js +669 -0
- package/dist/src/commands/project-sources.js +325 -31
- package/dist/src/commands/sequence.js +59 -32
- package/dist/src/commands/session.js +19 -6
- package/dist/src/commands/work.js +31 -0
- package/dist/src/dom/composer-text.d.ts +6 -0
- package/dist/src/dom/composer-text.js +85 -0
- package/dist/src/errors.d.ts +9 -0
- package/dist/src/errors.js +22 -0
- package/dist/src/index.d.ts +6 -0
- package/dist/src/index.js +9 -0
- package/dist/src/operations/artifact-output.d.ts +69 -0
- package/dist/src/operations/artifact-output.js +1748 -0
- package/dist/src/operations/artifact-stream.d.ts +19 -0
- package/dist/src/operations/artifact-stream.js +35 -0
- package/dist/src/operations/artifact-transfer.d.ts +149 -0
- package/dist/src/operations/artifact-transfer.js +1312 -0
- package/dist/src/operations/browser-adapter.d.ts +167 -0
- package/dist/src/operations/browser-adapter.js +2103 -0
- package/dist/src/operations/browser-observation.d.ts +110 -0
- package/dist/src/operations/browser-observation.js +1256 -0
- package/dist/src/operations/browser-target.d.ts +105 -0
- package/dist/src/operations/browser-target.js +531 -0
- package/dist/src/operations/canonical.d.ts +4 -0
- package/dist/src/operations/canonical.js +412 -0
- package/dist/src/operations/chatgpt-runtime.d.ts +72 -0
- package/dist/src/operations/chatgpt-runtime.js +1232 -0
- package/dist/src/operations/client.d.ts +138 -0
- package/dist/src/operations/client.js +1145 -0
- package/dist/src/operations/collector.d.ts +195 -0
- package/dist/src/operations/collector.js +1039 -0
- package/dist/src/operations/configuration-routing.d.ts +4 -0
- package/dist/src/operations/configuration-routing.js +14 -0
- package/dist/src/operations/control.d.ts +372 -0
- package/dist/src/operations/control.js +1498 -0
- package/dist/src/operations/file-identity.d.ts +33 -0
- package/dist/src/operations/file-identity.js +153 -0
- package/dist/src/operations/handle.d.ts +19 -0
- package/dist/src/operations/handle.js +727 -0
- package/dist/src/operations/index.d.ts +25 -0
- package/dist/src/operations/index.js +25 -0
- package/dist/src/operations/journal-authority.d.ts +12 -0
- package/dist/src/operations/journal-authority.js +1 -0
- package/dist/src/operations/journal-rpc-client.d.ts +12 -0
- package/dist/src/operations/journal-rpc-client.js +225 -0
- package/dist/src/operations/journal-rpc-protocol.d.ts +32 -0
- package/dist/src/operations/journal-rpc-protocol.js +214 -0
- package/dist/src/operations/journal-rpc-server.d.ts +15 -0
- package/dist/src/operations/journal-rpc-server.js +272 -0
- package/dist/src/operations/journal.d.ts +143 -0
- package/dist/src/operations/journal.js +1957 -0
- package/dist/src/operations/production-attachments.d.ts +118 -0
- package/dist/src/operations/production-attachments.js +1035 -0
- package/dist/src/operations/production-chatgpt-artifacts.d.ts +74 -0
- package/dist/src/operations/production-chatgpt-artifacts.js +1146 -0
- package/dist/src/operations/production-chatgpt-attachments.d.ts +80 -0
- package/dist/src/operations/production-chatgpt-attachments.js +1639 -0
- package/dist/src/operations/production-configuration.d.ts +42 -0
- package/dist/src/operations/production-configuration.js +1544 -0
- package/dist/src/operations/production-primitives.d.ts +53 -0
- package/dist/src/operations/production-primitives.js +856 -0
- package/dist/src/operations/production-work-steer.d.ts +203 -0
- package/dist/src/operations/production-work-steer.js +1218 -0
- package/dist/src/operations/recovery.d.ts +80 -0
- package/dist/src/operations/recovery.js +174 -0
- package/dist/src/operations/runtime-adapter.d.ts +126 -0
- package/dist/src/operations/runtime-adapter.js +802 -0
- package/dist/src/operations/send-once.d.ts +224 -0
- package/dist/src/operations/send-once.js +1081 -0
- package/dist/src/operations/service.d.ts +286 -0
- package/dist/src/operations/service.js +2831 -0
- package/dist/src/operations/staging.d.ts +136 -0
- package/dist/src/operations/staging.js +630 -0
- package/dist/src/operations/state-machine.d.ts +29 -0
- package/dist/src/operations/state-machine.js +2047 -0
- package/dist/src/operations/submission.d.ts +423 -0
- package/dist/src/operations/submission.js +1676 -0
- package/dist/src/operations/transactional-chat-power.d.ts +17 -0
- package/dist/src/operations/transactional-chat-power.js +212 -0
- package/dist/src/operations/turn-ownership.d.ts +202 -0
- package/dist/src/operations/turn-ownership.js +700 -0
- package/dist/src/operations/types.d.ts +454 -0
- package/dist/src/operations/types.js +20 -0
- package/dist/src/operations/wire-requests.d.ts +21 -0
- package/dist/src/operations/wire-requests.js +396 -0
- package/dist/src/operations/wire-results.d.ts +97 -0
- package/dist/src/operations/wire-results.js +818 -0
- package/dist/src/runner/responses.d.ts +3 -1
- package/dist/src/runner/responses.js +19 -1
- package/dist/src/runner/result.js +227 -55
- package/dist/src/runner/types.d.ts +12 -0
- package/dist/src/runtime/command-routing.d.ts +149 -0
- package/dist/src/runtime/command-routing.js +431 -0
- package/dist/src/runtime/coordinated-browser.d.ts +24 -0
- package/dist/src/runtime/coordinated-browser.js +315 -0
- package/dist/src/runtime/coordinated-page.d.ts +41 -0
- package/dist/src/runtime/coordinated-page.js +651 -0
- package/dist/src/runtime/operation-context.d.ts +142 -0
- package/dist/src/runtime/operation-context.js +410 -0
- package/dist/src/runtime/runtime-session.d.ts +95 -0
- package/dist/src/runtime/runtime-session.js +314 -0
- package/dist/src/runtime/tab-coordinator.d.ts +200 -0
- package/dist/src/runtime/tab-coordinator.js +1200 -0
- package/dist/src/runtime/value-boundaries.d.ts +18 -0
- package/dist/src/runtime/value-boundaries.js +49 -0
- package/dist/src/safety/blockers.js +8 -1
- package/dist/src/safety/untrusted-output.js +2 -1
- package/dist/src/scripts/backend-server.js +4 -1
- package/dist/src/scripts/capture-surface-profile.js +7 -2
- package/dist/src/scripts/journal-server.d.ts +2 -0
- package/dist/src/scripts/journal-server.js +37 -0
- package/dist/src/scripts/live-smoke/harness.js +79 -7
- package/dist/src/scripts/live-smoke/scenarios.d.ts +42 -1
- package/dist/src/scripts/live-smoke/scenarios.js +147 -30
- package/dist/src/scripts/live-smoke/transactional.d.ts +17 -0
- package/dist/src/scripts/live-smoke/transactional.js +254 -0
- package/dist/src/scripts/live-smoke/types.d.ts +3 -0
- package/dist/src/scripts/release-canary-module.js +17 -4
- package/dist/src/types.d.ts +38 -1
- package/package.json +8 -3
- package/references/2026-08-16-transactional-operations.md +459 -0
- package/references/2026-09-06-journal-service.md +140 -0
- package/references/agents-runner.md +34 -0
- package/references/backend-protocol.md +176 -0
- package/references/python-parity.md +137 -0
- package/references/responses-adapter.md +19 -0
- package/references/streaming.md +6 -0
|
@@ -0,0 +1,2047 @@
|
|
|
1
|
+
import { OPERATION_ARTIFACT_RECEIPT_SCHEMA_VERSION, OPERATION_ARTIFACT_TRANSFER_INTENT_SCHEMA_VERSION, OPERATION_ARTIFACT_TRANSFER_RECEIPT_SCHEMA_VERSION, OPERATION_RECEIPT_SCHEMA_VERSION, OPERATION_SCHEMA_VERSION, OPERATION_OWNERSHIP_BASELINE_SCHEMA_VERSION, OPERATION_SUBMISSION_WITNESS_SCHEMA_VERSION } from "./types.js";
|
|
2
|
+
import { canonicalJson } from "./canonical.js";
|
|
3
|
+
const BOUNDARY_RANK = {
|
|
4
|
+
none: 0,
|
|
5
|
+
handoff_may_have_occurred: 1,
|
|
6
|
+
send_may_have_occurred: 2,
|
|
7
|
+
control_may_have_occurred: 3
|
|
8
|
+
};
|
|
9
|
+
const LEGAL_EDGES = {
|
|
10
|
+
prepared: new Set(["handoff_pending", "ready", "uncertain"]),
|
|
11
|
+
handoff_pending: new Set(["ready", "uncertain"]),
|
|
12
|
+
ready: new Set(["send_pending", "uncertain"]),
|
|
13
|
+
send_pending: new Set(["submitted", "uncertain"]),
|
|
14
|
+
submitted: new Set(["generating", "capturing", "uncertain"]),
|
|
15
|
+
generating: new Set(["capturing", "uncertain"]),
|
|
16
|
+
capturing: new Set(["uncertain"]),
|
|
17
|
+
completed: new Set(),
|
|
18
|
+
uncertain: new Set(["ready", "submitted", "generating", "capturing"])
|
|
19
|
+
};
|
|
20
|
+
const ACTION_POLICY = {
|
|
21
|
+
status_read: "read_only",
|
|
22
|
+
configuration_set: "reconcile_set_to_value",
|
|
23
|
+
tool_set: "reconcile_set_to_value",
|
|
24
|
+
composer_set: "reconcile_set_to_value",
|
|
25
|
+
power_discovery: "read_only",
|
|
26
|
+
power_select: "reconcile_set_to_value",
|
|
27
|
+
file_handoff: "observe_only_after_intent",
|
|
28
|
+
send: "observe_only_after_intent",
|
|
29
|
+
work_steer: "observe_only_after_intent",
|
|
30
|
+
stop: "observe_only_after_intent",
|
|
31
|
+
download: "reconcile_local_effect",
|
|
32
|
+
local_output_commit: "reconcile_local_effect",
|
|
33
|
+
clipboard_capture_restore: "reconcile_local_effect"
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Copy only the closed capture contract into durable state. In particular,
|
|
37
|
+
* the request-local outputDirectory is intentionally not read or returned.
|
|
38
|
+
* Missing capture options preserve the historical collect defaults while the
|
|
39
|
+
* response format receives its explicit Markdown default.
|
|
40
|
+
*/
|
|
41
|
+
export function durableCapturePolicyFromRequest(capture) {
|
|
42
|
+
return {
|
|
43
|
+
responseContent: capture?.responseContent ?? "include",
|
|
44
|
+
responseFormat: capture?.responseFormat ?? "markdown",
|
|
45
|
+
artifacts: capture?.artifacts ?? "receipt_only"
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const ACTION_PHASES = {
|
|
49
|
+
status_read: new Set(["prepared", "handoff_pending", "ready", "send_pending", "submitted", "generating", "capturing", "completed", "uncertain"]),
|
|
50
|
+
configuration_set: new Set(["prepared"]),
|
|
51
|
+
tool_set: new Set(["prepared"]),
|
|
52
|
+
composer_set: new Set(["prepared"]),
|
|
53
|
+
power_discovery: new Set(["prepared"]),
|
|
54
|
+
power_select: new Set(["prepared"]),
|
|
55
|
+
file_handoff: new Set(["prepared"]),
|
|
56
|
+
send: new Set(["ready"]),
|
|
57
|
+
work_steer: new Set(["generating"]),
|
|
58
|
+
stop: new Set(["generating"]),
|
|
59
|
+
download: new Set(["capturing"]),
|
|
60
|
+
local_output_commit: new Set(["capturing"]),
|
|
61
|
+
clipboard_capture_restore: new Set(["capturing"])
|
|
62
|
+
};
|
|
63
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
64
|
+
const DIGEST_PATTERN = /^hmac-sha256:[0-9a-f]{64}$/;
|
|
65
|
+
const SHA256_PATTERN = /^[0-9a-f]{64}$/;
|
|
66
|
+
const CODE_PATTERN = /^[a-z][a-z0-9_]{0,63}$/;
|
|
67
|
+
const TRANSFER_OUTPUT_KEY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
68
|
+
const FINISH_REASON_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
69
|
+
const ARTIFACT_KEY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
70
|
+
const MIME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9!#$&^_.+/-]{0,126}$/;
|
|
71
|
+
const MAX_ARTIFACTS = 32;
|
|
72
|
+
const MAX_BASELINE_TURNS = 256;
|
|
73
|
+
const MAX_BASELINE_ARTIFACTS_PER_TURN = 32;
|
|
74
|
+
const MAX_SUBMISSION_WITNESSES = 64;
|
|
75
|
+
function isSingleIntentKind(kind) {
|
|
76
|
+
return kind === "file_handoff" || kind === "send";
|
|
77
|
+
}
|
|
78
|
+
export class OperationStateError extends Error {
|
|
79
|
+
code;
|
|
80
|
+
constructor(code, message) {
|
|
81
|
+
super(message);
|
|
82
|
+
this.code = code;
|
|
83
|
+
this.name = "OperationStateError";
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
export function assertOperationId(value, label = "operationId") {
|
|
87
|
+
if (typeof value !== "string" || !UUID_PATTERN.test(value)) {
|
|
88
|
+
throw new OperationStateError("invalid_operation_id", `${label} must be a canonical UUID.`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
export function reduceOperationEvents(events) {
|
|
92
|
+
let state;
|
|
93
|
+
for (let index = 0; index < events.length; index += 1) {
|
|
94
|
+
state = applyOperationEvent(state, events[index], index + 1);
|
|
95
|
+
}
|
|
96
|
+
if (state === undefined) {
|
|
97
|
+
throw new OperationStateError("empty_operation_log", "Operation log does not contain an operation_created event.");
|
|
98
|
+
}
|
|
99
|
+
return state;
|
|
100
|
+
}
|
|
101
|
+
export function applyOperationEvent(state, event, revision) {
|
|
102
|
+
assertOperationEventShape(event);
|
|
103
|
+
if (event.type === "operation_created") {
|
|
104
|
+
if (state !== undefined || revision !== 1) {
|
|
105
|
+
throw new OperationStateError("duplicate_operation_created", "operation_created must be the first and only creation event.");
|
|
106
|
+
}
|
|
107
|
+
assertOperationId(event.operationId);
|
|
108
|
+
assertDigest(event.requestDigest, "requestDigest");
|
|
109
|
+
assertTimestamp(event.createdAt, "createdAt");
|
|
110
|
+
if (event.surface !== "chat" && event.surface !== "work") {
|
|
111
|
+
throw new OperationStateError("invalid_operation_surface", "Operation surface must be chat or work.");
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
schemaVersion: OPERATION_SCHEMA_VERSION,
|
|
115
|
+
operationId: event.operationId,
|
|
116
|
+
requestDigest: event.requestDigest,
|
|
117
|
+
surface: event.surface,
|
|
118
|
+
phase: "prepared",
|
|
119
|
+
mutationBoundary: "none",
|
|
120
|
+
revision,
|
|
121
|
+
createdAt: event.createdAt,
|
|
122
|
+
updatedAt: event.createdAt,
|
|
123
|
+
actions: {},
|
|
124
|
+
ownershipBaselines: {},
|
|
125
|
+
artifactTransfers: {},
|
|
126
|
+
...(event.capturePolicy === undefined ? {} : {
|
|
127
|
+
capturePolicy: event.capturePolicy,
|
|
128
|
+
// Keep the existing responseFormat projection for old collector and
|
|
129
|
+
// inspect consumers while the path-free policy becomes authoritative.
|
|
130
|
+
responseFormat: event.capturePolicy.responseFormat
|
|
131
|
+
})
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
if (state === undefined) {
|
|
135
|
+
throw new OperationStateError("missing_operation_created", "Operation event occurred before operation_created.");
|
|
136
|
+
}
|
|
137
|
+
if (revision !== state.revision + 1) {
|
|
138
|
+
throw new OperationStateError("revision_gap", `Expected revision ${state.revision + 1}, received ${revision}.`);
|
|
139
|
+
}
|
|
140
|
+
switch (event.type) {
|
|
141
|
+
case "target_bound":
|
|
142
|
+
assertTimestamp(event.observedAt, "observedAt");
|
|
143
|
+
assertTimestampNotBefore(state.updatedAt, event.observedAt, "observedAt");
|
|
144
|
+
return withRevision({ ...state, target: validatedTarget(state, event.target) }, revision, event.observedAt);
|
|
145
|
+
case "target_established":
|
|
146
|
+
assertTimestamp(event.establishment.observedAt, "target_established.observedAt");
|
|
147
|
+
assertTimestampNotBefore(state.updatedAt, event.establishment.observedAt, "target_established.observedAt");
|
|
148
|
+
return withRevision({ ...state, target: establishTarget(state, event.establishment) }, revision, event.establishment.observedAt);
|
|
149
|
+
case "ownership_baseline":
|
|
150
|
+
assertTimestamp(event.baseline.observedAt, "ownership_baseline.observedAt");
|
|
151
|
+
assertTimestampNotBefore(state.updatedAt, event.baseline.observedAt, "ownership_baseline.observedAt");
|
|
152
|
+
return withRevision(applyOwnershipBaseline(state, event.baseline), revision, event.baseline.observedAt);
|
|
153
|
+
case "submission_witness":
|
|
154
|
+
assertTimestamp(event.witness.observedAt, "submission_witness.observedAt");
|
|
155
|
+
assertTimestampNotBefore(state.updatedAt, event.witness.observedAt, "submission_witness.observedAt");
|
|
156
|
+
return withRevision(applySubmissionWitness(state, event.witness), revision, event.witness.observedAt);
|
|
157
|
+
case "action_intent":
|
|
158
|
+
assertTimestamp(event.intentAt, "intentAt");
|
|
159
|
+
assertTimestampNotBefore(state.updatedAt, event.intentAt, "intentAt");
|
|
160
|
+
return withRevision(applyActionIntent(state, event.action, revision, event.intentAt), revision, event.intentAt);
|
|
161
|
+
case "action_prepared": {
|
|
162
|
+
assertTimestamp(event.intentAt, "intentAt");
|
|
163
|
+
assertTimestamp(event.baseline.observedAt, "action_prepared.baseline.observedAt");
|
|
164
|
+
assertTimestampNotBefore(state.updatedAt, event.intentAt, "intentAt");
|
|
165
|
+
if (event.baseline.observedAt !== event.intentAt) {
|
|
166
|
+
throw new OperationStateError("action_prepared_timestamp_mismatch", "Atomic action preparation requires baseline.observedAt to equal intentAt.");
|
|
167
|
+
}
|
|
168
|
+
assertPreparedActionBaselineIdentity(event.action, event.baseline, state);
|
|
169
|
+
const withAction = applyActionIntent(state, event.action, revision, event.intentAt);
|
|
170
|
+
const withBaseline = applyOwnershipBaseline(withAction, event.baseline);
|
|
171
|
+
return withRevision(withBaseline, revision, event.intentAt);
|
|
172
|
+
}
|
|
173
|
+
case "artifact_transfer_intent":
|
|
174
|
+
assertTimestamp(event.intent.intentAt, "artifact_transfer_intent.intentAt");
|
|
175
|
+
assertTimestampNotBefore(state.updatedAt, event.intent.intentAt, "artifact_transfer_intent.intentAt");
|
|
176
|
+
return withRevision(applyArtifactTransferIntent(state, event.intent, revision), revision, event.intent.intentAt);
|
|
177
|
+
case "artifact_transfer_receipt":
|
|
178
|
+
assertTimestamp(event.receipt.observedAt, "artifact_transfer_receipt.observedAt");
|
|
179
|
+
assertTimestampNotBefore(state.updatedAt, event.receipt.observedAt, "artifact_transfer_receipt.observedAt");
|
|
180
|
+
return withRevision(applyArtifactTransferReceipt(state, event.receipt, revision), revision, event.receipt.observedAt);
|
|
181
|
+
case "action_receipt":
|
|
182
|
+
assertTimestamp(event.observedAt, "observedAt");
|
|
183
|
+
assertTimestampNotBefore(state.updatedAt, event.observedAt, "observedAt");
|
|
184
|
+
return withRevision(applyActionReceipt(state, event, revision), revision, event.observedAt);
|
|
185
|
+
case "phase_changed":
|
|
186
|
+
assertTimestamp(event.observedAt, "observedAt");
|
|
187
|
+
assertTimestampNotBefore(state.updatedAt, event.observedAt, "observedAt");
|
|
188
|
+
return withRevision(applyPhaseChange(state, event), revision, event.observedAt);
|
|
189
|
+
case "blocker_observed":
|
|
190
|
+
assertTimestamp(event.blocker.observedAt, "blocker.observedAt");
|
|
191
|
+
assertTimestampNotBefore(state.updatedAt, event.blocker.observedAt, "blocker.observedAt");
|
|
192
|
+
assertDigest(event.blocker.messageDigest, "blocker.messageDigest");
|
|
193
|
+
validateBlockerObservation(event.blocker);
|
|
194
|
+
return withRevision({ ...state, lastBlocker: event.blocker }, revision, event.blocker.observedAt);
|
|
195
|
+
case "receipt_completed":
|
|
196
|
+
assertTimestamp(event.observedAt, "observedAt");
|
|
197
|
+
assertTimestampNotBefore(state.updatedAt, event.observedAt, "observedAt");
|
|
198
|
+
return withRevision(applyCompletedReceipt(state, event.receipt, event.observedAt), revision, event.observedAt);
|
|
199
|
+
case "content_availability_changed":
|
|
200
|
+
assertTimestamp(event.observedAt, "observedAt");
|
|
201
|
+
assertTimestampNotBefore(state.updatedAt, event.observedAt, "observedAt");
|
|
202
|
+
if (typeof event.available !== "boolean") {
|
|
203
|
+
throw new OperationStateError("invalid_content_availability", "Content availability must be boolean.");
|
|
204
|
+
}
|
|
205
|
+
if (state.receipt === undefined) {
|
|
206
|
+
throw new OperationStateError("receipt_missing", "Content availability cannot change before a terminal receipt exists.");
|
|
207
|
+
}
|
|
208
|
+
if (state.phase !== "completed") {
|
|
209
|
+
throw new OperationStateError("receipt_phase_invalid", "Content availability can change only on a completed operation.");
|
|
210
|
+
}
|
|
211
|
+
if (event.available || !state.receipt.contentAvailable) {
|
|
212
|
+
throw new OperationStateError("content_availability_not_monotonic", "Content availability may only transition once from available to unavailable.");
|
|
213
|
+
}
|
|
214
|
+
return withRevision({
|
|
215
|
+
...state,
|
|
216
|
+
receipt: { ...state.receipt, contentAvailable: event.available }
|
|
217
|
+
}, revision, event.observedAt);
|
|
218
|
+
default: {
|
|
219
|
+
const unknownEvent = event;
|
|
220
|
+
throw new OperationStateError("unknown_operation_event", `Unknown operation event type: ${String(unknownEvent.type)}.`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Rejects unknown or request-only fields before an event can enter the durable
|
|
226
|
+
* journal. TypeScript's structural types do not protect JavaScript callers at
|
|
227
|
+
* runtime, so the privacy boundary must be closed explicitly.
|
|
228
|
+
*/
|
|
229
|
+
export function assertOperationEventShape(value) {
|
|
230
|
+
assertExactRecord(value, "operation event", ["type"], ["type"], true);
|
|
231
|
+
const event = value;
|
|
232
|
+
switch (event.type) {
|
|
233
|
+
case "operation_created":
|
|
234
|
+
assertExactRecord(event, "operation_created", ["type", "operationId", "requestDigest", "surface", "createdAt", "capturePolicy"], ["type", "operationId", "requestDigest", "surface", "createdAt"]);
|
|
235
|
+
if (event.capturePolicy !== undefined)
|
|
236
|
+
assertDurableCapturePolicyShape(event.capturePolicy);
|
|
237
|
+
return;
|
|
238
|
+
case "target_bound":
|
|
239
|
+
assertExactRecord(event, "target_bound", ["type", "target", "observedAt"], ["type", "target", "observedAt"]);
|
|
240
|
+
assertTargetShape(event.target);
|
|
241
|
+
return;
|
|
242
|
+
case "target_established":
|
|
243
|
+
assertExactRecord(event, "target_established", ["type", "establishment"], ["type", "establishment"]);
|
|
244
|
+
assertTargetEstablishmentShape(event.establishment);
|
|
245
|
+
return;
|
|
246
|
+
case "ownership_baseline":
|
|
247
|
+
assertExactRecord(event, "ownership_baseline", ["type", "baseline"], ["type", "baseline"]);
|
|
248
|
+
assertOwnershipBaselineShape(event.baseline);
|
|
249
|
+
return;
|
|
250
|
+
case "submission_witness":
|
|
251
|
+
assertExactRecord(event, "submission_witness", ["type", "witness"], ["type", "witness"]);
|
|
252
|
+
assertSubmissionWitnessShape(event.witness);
|
|
253
|
+
return;
|
|
254
|
+
case "action_intent":
|
|
255
|
+
assertExactRecord(event, "action_intent", ["type", "action", "intentAt"], ["type", "action", "intentAt"]);
|
|
256
|
+
assertActionIntentShape(event.action);
|
|
257
|
+
return;
|
|
258
|
+
case "action_prepared":
|
|
259
|
+
assertExactRecord(event, "action_prepared", ["type", "action", "intentAt", "baseline"], ["type", "action", "intentAt", "baseline"]);
|
|
260
|
+
assertActionIntentShape(event.action);
|
|
261
|
+
assertOwnershipBaselineShape(event.baseline);
|
|
262
|
+
return;
|
|
263
|
+
case "artifact_transfer_intent":
|
|
264
|
+
assertExactRecord(event, "artifact_transfer_intent", ["type", "intent"], ["type", "intent"]);
|
|
265
|
+
assertArtifactTransferIntentShape(event.intent);
|
|
266
|
+
return;
|
|
267
|
+
case "artifact_transfer_receipt":
|
|
268
|
+
assertExactRecord(event, "artifact_transfer_receipt", ["type", "receipt"], ["type", "receipt"]);
|
|
269
|
+
assertArtifactTransferReceiptShape(event.receipt);
|
|
270
|
+
return;
|
|
271
|
+
case "action_receipt":
|
|
272
|
+
assertExactRecord(event, "action_receipt", ["type", "actionId", "outcome", "evidenceDigest", "blockerCode", "observedAt"], ["type", "actionId", "outcome", "observedAt"]);
|
|
273
|
+
return;
|
|
274
|
+
case "phase_changed":
|
|
275
|
+
assertExactRecord(event, "phase_changed", ["type", "from", "to", "mutationBoundary", "causeActionId", "evidenceDigest", "observedAt"], ["type", "from", "to", "mutationBoundary", "observedAt"]);
|
|
276
|
+
return;
|
|
277
|
+
case "blocker_observed":
|
|
278
|
+
assertExactRecord(event, "blocker_observed", ["type", "blocker"], ["type", "blocker"]);
|
|
279
|
+
assertBlockerObservationShape(event.blocker);
|
|
280
|
+
return;
|
|
281
|
+
case "receipt_completed":
|
|
282
|
+
assertExactRecord(event, "receipt_completed", ["type", "receipt", "observedAt"], ["type", "receipt", "observedAt"]);
|
|
283
|
+
assertReceiptShape(event.receipt);
|
|
284
|
+
return;
|
|
285
|
+
case "content_availability_changed":
|
|
286
|
+
assertExactRecord(event, "content_availability_changed", ["type", "available", "observedAt"], ["type", "available", "observedAt"]);
|
|
287
|
+
return;
|
|
288
|
+
default:
|
|
289
|
+
throw new OperationStateError("unknown_operation_event", `Unknown operation event type: ${String(event.type)}.`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
/** Validates the closed durable-state shape used by authenticated compaction. */
|
|
293
|
+
export function assertOperationStateShape(value) {
|
|
294
|
+
assertExactRecord(value, "operation state", ["schemaVersion", "operationId", "requestDigest", "surface", "phase", "mutationBoundary", "revision", "createdAt", "updatedAt", "capturePolicy", "responseFormat", "target", "actions", "ownershipBaseline", "ownershipBaselines", "artifactTransfers", "submissionWitnesses", "submissionWitness", "lastBlocker", "receipt"], ["schemaVersion", "operationId", "requestDigest", "surface", "phase", "mutationBoundary", "revision", "createdAt", "updatedAt", "actions"]);
|
|
295
|
+
const state = value;
|
|
296
|
+
if (state.schemaVersion !== OPERATION_SCHEMA_VERSION) {
|
|
297
|
+
throw new OperationStateError("unsupported_operation_state", "Operation state schemaVersion is unsupported.");
|
|
298
|
+
}
|
|
299
|
+
assertOperationId(state.operationId);
|
|
300
|
+
assertDigest(state.requestDigest, "state.requestDigest");
|
|
301
|
+
if (state.surface !== "chat" && state.surface !== "work") {
|
|
302
|
+
throw new OperationStateError("invalid_operation_surface", "Operation state surface must be chat or work.");
|
|
303
|
+
}
|
|
304
|
+
if (!(typeof state.phase === "string" && state.phase in LEGAL_EDGES)) {
|
|
305
|
+
throw new OperationStateError("invalid_operation_phase", "Operation state phase is unsupported.");
|
|
306
|
+
}
|
|
307
|
+
if (!(typeof state.mutationBoundary === "string" && state.mutationBoundary in BOUNDARY_RANK)) {
|
|
308
|
+
throw new OperationStateError("invalid_mutation_boundary", "Operation state mutationBoundary is unsupported.");
|
|
309
|
+
}
|
|
310
|
+
if (!Number.isSafeInteger(state.revision) || state.revision < 1) {
|
|
311
|
+
throw new OperationStateError("invalid_operation_revision", "Operation state revision must be a positive safe integer.");
|
|
312
|
+
}
|
|
313
|
+
assertTimestamp(state.createdAt, "state.createdAt");
|
|
314
|
+
assertTimestamp(state.updatedAt, "state.updatedAt");
|
|
315
|
+
assertTimestampNotBefore(state.createdAt, state.updatedAt, "state.updatedAt");
|
|
316
|
+
if (state.responseFormat !== undefined && state.responseFormat !== "markdown" && state.responseFormat !== "text") {
|
|
317
|
+
throw new OperationStateError("invalid_response_format", "Operation responseFormat must be markdown or text.");
|
|
318
|
+
}
|
|
319
|
+
if (state.capturePolicy !== undefined) {
|
|
320
|
+
assertDurableCapturePolicyShape(state.capturePolicy);
|
|
321
|
+
if (state.responseFormat !== undefined && state.responseFormat !== state.capturePolicy.responseFormat) {
|
|
322
|
+
throw new OperationStateError("capture_policy_format_mismatch", "Operation responseFormat conflicts with its durable capture policy.");
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (state.target !== undefined)
|
|
326
|
+
assertTargetShape(state.target);
|
|
327
|
+
if (!isPlainRecord(state.actions)) {
|
|
328
|
+
throw new OperationStateError("invalid_operation_state", "Operation state actions must be an object.");
|
|
329
|
+
}
|
|
330
|
+
for (const [actionId, action] of Object.entries(state.actions)) {
|
|
331
|
+
assertOperationId(actionId, "action map key");
|
|
332
|
+
assertActionRecordShape(action);
|
|
333
|
+
if (action.actionId !== actionId) {
|
|
334
|
+
throw new OperationStateError("invalid_operation_state", "Operation action map key must match actionId.");
|
|
335
|
+
}
|
|
336
|
+
assertPersistedActionRecord(action, state);
|
|
337
|
+
}
|
|
338
|
+
if (state.target !== undefined)
|
|
339
|
+
validateTargetValues(state.target);
|
|
340
|
+
if (state.ownershipBaseline !== undefined) {
|
|
341
|
+
assertOwnershipBaselineShape(state.ownershipBaseline);
|
|
342
|
+
validateOwnershipBaselineValues(state.ownershipBaseline, state);
|
|
343
|
+
}
|
|
344
|
+
if (state.ownershipBaselines !== undefined) {
|
|
345
|
+
if (!isPlainRecord(state.ownershipBaselines)) {
|
|
346
|
+
throw new OperationStateError("invalid_operation_state", "Operation ownershipBaselines must be an object.");
|
|
347
|
+
}
|
|
348
|
+
for (const actionId of Object.keys(state.ownershipBaselines)) {
|
|
349
|
+
const descriptor = Object.getOwnPropertyDescriptor(state.ownershipBaselines, actionId);
|
|
350
|
+
if (descriptor === undefined || !Object.hasOwn(descriptor, "value") || descriptor.get !== undefined || descriptor.set !== undefined) {
|
|
351
|
+
throw new OperationStateError("invalid_operation_shape", "Operation ownershipBaselines contains an unsafe property (accessor).");
|
|
352
|
+
}
|
|
353
|
+
const baseline = descriptor.value;
|
|
354
|
+
assertOperationId(actionId, "ownershipBaselines map key");
|
|
355
|
+
assertOwnershipBaselineShape(baseline);
|
|
356
|
+
if (baseline.actionId !== actionId) {
|
|
357
|
+
throw new OperationStateError("invalid_operation_state", "Ownership baseline map key must match actionId.");
|
|
358
|
+
}
|
|
359
|
+
validateOwnershipBaselineValues(baseline, state);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
if (state.artifactTransfers !== undefined) {
|
|
363
|
+
assertArtifactTransfersStateShape(state.artifactTransfers);
|
|
364
|
+
for (const [transferActionId, transfer] of Object.entries(state.artifactTransfers)) {
|
|
365
|
+
validateArtifactTransferStateValues(transferActionId, transfer, state);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
validateSubmissionWitnessCollection(state);
|
|
369
|
+
if (state.lastBlocker !== undefined) {
|
|
370
|
+
assertBlockerObservationShape(state.lastBlocker);
|
|
371
|
+
validateBlockerObservation(state.lastBlocker);
|
|
372
|
+
}
|
|
373
|
+
if (state.receipt !== undefined) {
|
|
374
|
+
assertReceiptShape(state.receipt);
|
|
375
|
+
validateReceiptValues(state.receipt, state);
|
|
376
|
+
}
|
|
377
|
+
if (state.phase === "completed" && state.receipt === undefined) {
|
|
378
|
+
throw new OperationStateError("receipt_missing", "Completed operation state requires a terminal receipt.");
|
|
379
|
+
}
|
|
380
|
+
if (state.phase !== "completed" && state.receipt !== undefined) {
|
|
381
|
+
throw new OperationStateError("receipt_phase_invalid", "Only completed operation state may contain a terminal receipt.");
|
|
382
|
+
}
|
|
383
|
+
validateStateCoherence(state);
|
|
384
|
+
}
|
|
385
|
+
export function requiredRepeatPolicy(kind) {
|
|
386
|
+
return ACTION_POLICY[kind];
|
|
387
|
+
}
|
|
388
|
+
export function boundaryForAction(kind) {
|
|
389
|
+
if (kind === "file_handoff")
|
|
390
|
+
return "handoff_may_have_occurred";
|
|
391
|
+
if (kind === "send")
|
|
392
|
+
return "send_may_have_occurred";
|
|
393
|
+
if (kind === "work_steer" || kind === "stop")
|
|
394
|
+
return "control_may_have_occurred";
|
|
395
|
+
return undefined;
|
|
396
|
+
}
|
|
397
|
+
function assertPreparedActionBaselineIdentity(action, baseline, state) {
|
|
398
|
+
if (action.kind !== "send" && action.kind !== "work_steer") {
|
|
399
|
+
throw new OperationStateError("action_prepared_kind_invalid", "Atomic action preparation is supported only for send or work_steer.");
|
|
400
|
+
}
|
|
401
|
+
if (baseline.actionId !== action.actionId) {
|
|
402
|
+
throw new OperationStateError("action_prepared_action_mismatch", "Atomic action preparation baseline must name the prepared action.");
|
|
403
|
+
}
|
|
404
|
+
if (baseline.operationId !== state.operationId || baseline.requestDigest !== state.requestDigest) {
|
|
405
|
+
throw new OperationStateError("ownership_baseline_identity_mismatch", "Atomic action preparation baseline must use the parent operation identity.");
|
|
406
|
+
}
|
|
407
|
+
if (action.targetDigest === undefined || action.targetDigest !== baseline.targetBindingDigest) {
|
|
408
|
+
throw new OperationStateError("ownership_baseline_target_mismatch", "Atomic action preparation baseline must match the exact action target digest.");
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
function applyActionIntent(state, action, revision, intentAt) {
|
|
412
|
+
assertOperationId(action.actionId, "actionId");
|
|
413
|
+
assertDigest(action.requestDigest, "action.requestDigest");
|
|
414
|
+
if (action.kind !== "stop" && action.kind !== "work_steer" && action.requestDigest !== state.requestDigest) {
|
|
415
|
+
throw new OperationStateError("action_request_mismatch", "Only caller-owned control actions may carry a request digest distinct from the parent operation.");
|
|
416
|
+
}
|
|
417
|
+
if (!(action.kind in ACTION_POLICY)) {
|
|
418
|
+
throw new OperationStateError("invalid_action_kind", `Unknown operation action kind: ${String(action.kind)}.`);
|
|
419
|
+
}
|
|
420
|
+
if (state.actions[action.actionId] !== undefined) {
|
|
421
|
+
throw new OperationStateError("duplicate_action_intent", `Action ${action.actionId} already has an intent.`);
|
|
422
|
+
}
|
|
423
|
+
if (action.repeatPolicy !== requiredRepeatPolicy(action.kind)) {
|
|
424
|
+
throw new OperationStateError("invalid_repeat_policy", `Action ${action.kind} requires ${requiredRepeatPolicy(action.kind)}, received ${action.repeatPolicy}.`);
|
|
425
|
+
}
|
|
426
|
+
if (!ACTION_PHASES[action.kind].has(state.phase)) {
|
|
427
|
+
throw new OperationStateError("action_phase_invalid", `Action ${action.kind} cannot begin while the operation is ${state.phase}.`);
|
|
428
|
+
}
|
|
429
|
+
if (action.kind !== "status_read" && state.target === undefined) {
|
|
430
|
+
throw new OperationStateError("target_not_bound", `Action ${action.kind} requires a durable target binding.`);
|
|
431
|
+
}
|
|
432
|
+
if (action.parentActionId !== undefined && state.actions[action.parentActionId] === undefined) {
|
|
433
|
+
throw new OperationStateError("unknown_parent_action", `Parent action ${action.parentActionId} is not recorded.`);
|
|
434
|
+
}
|
|
435
|
+
if (action.parentActionId !== undefined)
|
|
436
|
+
assertOperationId(action.parentActionId, "parentActionId");
|
|
437
|
+
if (action.kind !== "status_read" && action.targetDigest === undefined) {
|
|
438
|
+
throw new OperationStateError("action_target_missing", `Action ${action.kind} requires the exact target binding digest.`);
|
|
439
|
+
}
|
|
440
|
+
if (action.targetDigest !== undefined)
|
|
441
|
+
assertDigest(action.targetDigest, "action.targetDigest");
|
|
442
|
+
if (isSingleIntentKind(action.kind) &&
|
|
443
|
+
Object.values(state.actions).some(existing => existing.kind === action.kind)) {
|
|
444
|
+
throw new OperationStateError("nonrepeatable_action_already_intended", `Operation already contains a non-repeatable ${action.kind} intent.`);
|
|
445
|
+
}
|
|
446
|
+
const record = {
|
|
447
|
+
...action,
|
|
448
|
+
intentRevision: revision,
|
|
449
|
+
intentAt
|
|
450
|
+
};
|
|
451
|
+
const actionBoundary = boundaryForAction(action.kind);
|
|
452
|
+
const mutationBoundary = actionBoundary !== undefined && BOUNDARY_RANK[actionBoundary] > BOUNDARY_RANK[state.mutationBoundary]
|
|
453
|
+
? actionBoundary
|
|
454
|
+
: state.mutationBoundary;
|
|
455
|
+
return { ...state, mutationBoundary, actions: { ...state.actions, [action.actionId]: record } };
|
|
456
|
+
}
|
|
457
|
+
function applyActionReceipt(state, event, revision) {
|
|
458
|
+
const action = state.actions[event.actionId];
|
|
459
|
+
if (action === undefined) {
|
|
460
|
+
throw new OperationStateError("action_intent_missing", `Action ${event.actionId} has no durable intent.`);
|
|
461
|
+
}
|
|
462
|
+
if (action.outcome !== undefined) {
|
|
463
|
+
throw new OperationStateError("duplicate_action_receipt", `Action ${event.actionId} already has a receipt.`);
|
|
464
|
+
}
|
|
465
|
+
if (event.outcome !== "satisfied" && event.outcome !== "not_satisfied" && event.outcome !== "uncertain") {
|
|
466
|
+
throw new OperationStateError("invalid_action_outcome", `Unknown action outcome: ${String(event.outcome)}.`);
|
|
467
|
+
}
|
|
468
|
+
assertTimestampNotBefore(action.intentAt, event.observedAt, "action receipt observedAt");
|
|
469
|
+
if (event.outcome === "satisfied" && event.evidenceDigest === undefined) {
|
|
470
|
+
throw new OperationStateError("action_evidence_missing", `Satisfied action ${event.actionId} requires evidenceDigest.`);
|
|
471
|
+
}
|
|
472
|
+
if (event.evidenceDigest !== undefined)
|
|
473
|
+
assertDigest(event.evidenceDigest, "action.evidenceDigest");
|
|
474
|
+
if (event.blockerCode !== undefined && !CODE_PATTERN.test(event.blockerCode)) {
|
|
475
|
+
throw new OperationStateError("invalid_blocker_code", "Action blockerCode must be a bounded canonical code.");
|
|
476
|
+
}
|
|
477
|
+
const updated = {
|
|
478
|
+
...action,
|
|
479
|
+
outcome: event.outcome,
|
|
480
|
+
receiptRevision: revision,
|
|
481
|
+
receiptAt: event.observedAt
|
|
482
|
+
};
|
|
483
|
+
if (event.evidenceDigest !== undefined)
|
|
484
|
+
updated.evidenceDigest = event.evidenceDigest;
|
|
485
|
+
if (event.blockerCode !== undefined)
|
|
486
|
+
updated.blockerCode = event.blockerCode;
|
|
487
|
+
return { ...state, actions: { ...state.actions, [event.actionId]: updated } };
|
|
488
|
+
}
|
|
489
|
+
function applySubmissionWitness(state, witness) {
|
|
490
|
+
// A legacy snapshot may contain only the original Send projection. When a
|
|
491
|
+
// new Work steer witness is appended, materialize that legacy entry into the
|
|
492
|
+
// keyed collection so the collection remains a complete causal ledger.
|
|
493
|
+
const existingWitnesses = state.submissionWitnesses === undefined
|
|
494
|
+
? state.submissionWitness === undefined
|
|
495
|
+
? {}
|
|
496
|
+
: { [state.submissionWitness.actionId]: state.submissionWitness }
|
|
497
|
+
: state.submissionWitnesses;
|
|
498
|
+
assertSubmissionWitnessesMapShape(existingWitnesses);
|
|
499
|
+
validateSubmissionWitnessCollection({ ...state, submissionWitnesses: existingWitnesses });
|
|
500
|
+
const existing = existingWitnesses[witness.actionId];
|
|
501
|
+
if (existing !== undefined) {
|
|
502
|
+
if (canonicalJson(existing) !== canonicalJson(witness)) {
|
|
503
|
+
throw new OperationStateError("submission_witness_conflict", `Submission witness for action ${witness.actionId} conflicts with the durable record.`);
|
|
504
|
+
}
|
|
505
|
+
// Same-action replay is an idempotent confirmation. The journal revision
|
|
506
|
+
// still advances in the caller, but no semantic projection changes.
|
|
507
|
+
return state.submissionWitnesses === undefined
|
|
508
|
+
? { ...state, submissionWitnesses: existingWitnesses }
|
|
509
|
+
: state;
|
|
510
|
+
}
|
|
511
|
+
if (Object.keys(existingWitnesses).length >= MAX_SUBMISSION_WITNESSES) {
|
|
512
|
+
throw new OperationStateError("submission_witness_limit", `An operation may record at most ${MAX_SUBMISSION_WITNESSES} submission witnesses.`);
|
|
513
|
+
}
|
|
514
|
+
const candidateState = {
|
|
515
|
+
...state,
|
|
516
|
+
submissionWitnesses: { ...existingWitnesses, [witness.actionId]: witness }
|
|
517
|
+
};
|
|
518
|
+
validateSubmissionWitnessValues(witness, candidateState);
|
|
519
|
+
if (witness.actionKind === "send") {
|
|
520
|
+
if (state.submissionWitness !== undefined && canonicalJson(state.submissionWitness) !== canonicalJson(witness)) {
|
|
521
|
+
throw new OperationStateError("submission_witness_projection_mismatch", "The original Send submission witness must match its legacy projection exactly.");
|
|
522
|
+
}
|
|
523
|
+
return { ...candidateState, submissionWitness: witness };
|
|
524
|
+
}
|
|
525
|
+
return candidateState;
|
|
526
|
+
}
|
|
527
|
+
function applyOwnershipBaseline(state, baseline) {
|
|
528
|
+
validateOwnershipBaselineValues(baseline, state);
|
|
529
|
+
const existing = state.ownershipBaselines?.[baseline.actionId];
|
|
530
|
+
if (existing !== undefined) {
|
|
531
|
+
throw new OperationStateError("ownership_baseline_duplicate", `An operation may record only one immutable ownership baseline for action ${baseline.actionId}.`);
|
|
532
|
+
}
|
|
533
|
+
const action = state.actions[baseline.actionId];
|
|
534
|
+
if (action?.kind === "send" && state.ownershipBaseline !== undefined) {
|
|
535
|
+
throw new OperationStateError("ownership_baseline_duplicate", "An operation may record only one immutable pre-Send ownership baseline.");
|
|
536
|
+
}
|
|
537
|
+
const ownershipBaselines = {
|
|
538
|
+
...(state.ownershipBaselines ?? {}),
|
|
539
|
+
[baseline.actionId]: baseline
|
|
540
|
+
};
|
|
541
|
+
return {
|
|
542
|
+
...state,
|
|
543
|
+
ownershipBaselines,
|
|
544
|
+
...(action?.kind === "send" && state.ownershipBaseline === undefined
|
|
545
|
+
? { ownershipBaseline: baseline }
|
|
546
|
+
: {})
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
function applyArtifactTransferIntent(state, intent, revision) {
|
|
550
|
+
if (state.capturePolicy?.artifacts !== "transfer") {
|
|
551
|
+
throw new OperationStateError("artifact_transfer_policy_mismatch", "Artifact transfer intent requires the immutable transfer capture policy.");
|
|
552
|
+
}
|
|
553
|
+
if (state.phase !== "capturing") {
|
|
554
|
+
throw new OperationStateError("artifact_transfer_phase_invalid", "Artifact transfer intent requires a capturing operation.");
|
|
555
|
+
}
|
|
556
|
+
validateArtifactTransferIntentValues(intent, state);
|
|
557
|
+
const transfers = state.artifactTransfers ?? {};
|
|
558
|
+
assertArtifactTransfersStateShape(transfers);
|
|
559
|
+
if (transfers[intent.transferActionId] !== undefined) {
|
|
560
|
+
throw new OperationStateError("artifact_transfer_intent_duplicate", "Artifact transfer intent is already durable.");
|
|
561
|
+
}
|
|
562
|
+
const tuple = artifactTransferTuple(intent);
|
|
563
|
+
for (const transfer of Object.values(transfers)) {
|
|
564
|
+
if (transfer.intent !== undefined && artifactTransferTuple(transfer.intent) === tuple) {
|
|
565
|
+
throw new OperationStateError("artifact_transfer_duplicate_tuple", "An artifact transfer tuple may only be transferred once.");
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
const action = {
|
|
569
|
+
actionId: intent.transferActionId,
|
|
570
|
+
kind: "local_output_commit",
|
|
571
|
+
repeatPolicy: "reconcile_local_effect",
|
|
572
|
+
requestDigest: intent.requestDigest,
|
|
573
|
+
targetDigest: intent.targetBindingDigest,
|
|
574
|
+
intentRevision: revision,
|
|
575
|
+
intentAt: intent.intentAt
|
|
576
|
+
};
|
|
577
|
+
const withAction = applyActionIntent(state, action, revision, intent.intentAt);
|
|
578
|
+
return {
|
|
579
|
+
...withAction,
|
|
580
|
+
artifactTransfers: {
|
|
581
|
+
...transfers,
|
|
582
|
+
[intent.transferActionId]: { intent }
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
function applyArtifactTransferReceipt(state, receipt, revision) {
|
|
587
|
+
if (state.phase !== "capturing") {
|
|
588
|
+
throw new OperationStateError("artifact_transfer_phase_invalid", "Artifact transfer receipt requires a capturing operation.");
|
|
589
|
+
}
|
|
590
|
+
const transfers = state.artifactTransfers ?? {};
|
|
591
|
+
assertArtifactTransfersStateShape(transfers);
|
|
592
|
+
const transfer = transfers[receipt.transferActionId];
|
|
593
|
+
if (transfer?.intent === undefined) {
|
|
594
|
+
throw new OperationStateError("artifact_transfer_intent_missing", "Artifact transfer receipt requires its durable intent.");
|
|
595
|
+
}
|
|
596
|
+
if (transfer.receipt !== undefined) {
|
|
597
|
+
throw new OperationStateError("artifact_transfer_receipt_duplicate", "Artifact transfer receipt is already durable.");
|
|
598
|
+
}
|
|
599
|
+
validateArtifactTransferReceiptValues(receipt, transfer.intent, state);
|
|
600
|
+
if (receipt.observedAt < transfer.intent.intentAt) {
|
|
601
|
+
throw new OperationStateError("artifact_transfer_timestamp_regression", "Artifact transfer receipt cannot precede its intent.");
|
|
602
|
+
}
|
|
603
|
+
const outcome = receipt.status === "transferred"
|
|
604
|
+
? "satisfied"
|
|
605
|
+
: receipt.status === "blocked" && receipt.blockerCode === "output_collision"
|
|
606
|
+
? "not_satisfied"
|
|
607
|
+
: "uncertain";
|
|
608
|
+
const actionEvent = {
|
|
609
|
+
type: "action_receipt",
|
|
610
|
+
actionId: receipt.transferActionId,
|
|
611
|
+
outcome,
|
|
612
|
+
observedAt: receipt.observedAt,
|
|
613
|
+
...(receipt.status === "transferred" ? { evidenceDigest: receipt.destinationIdentityDigest } : {}),
|
|
614
|
+
...(receipt.blockerCode === undefined ? {} : { blockerCode: receipt.blockerCode })
|
|
615
|
+
};
|
|
616
|
+
const withAction = applyActionReceipt(state, actionEvent, revision);
|
|
617
|
+
return {
|
|
618
|
+
...withAction,
|
|
619
|
+
artifactTransfers: {
|
|
620
|
+
...transfers,
|
|
621
|
+
[receipt.transferActionId]: { intent: transfer.intent, receipt }
|
|
622
|
+
}
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
function validateArtifactTransferIdentityValues(value, state) {
|
|
626
|
+
if (value.operationId !== state.operationId || value.requestDigest !== state.requestDigest) {
|
|
627
|
+
throw new OperationStateError("artifact_transfer_identity_mismatch", "Artifact transfer operation identity does not match state.");
|
|
628
|
+
}
|
|
629
|
+
if (state.target === undefined) {
|
|
630
|
+
throw new OperationStateError("target_not_bound", "Artifact transfer requires a durable target binding.");
|
|
631
|
+
}
|
|
632
|
+
const originalSend = Object.values(state.actions).find(action => action.kind === "send");
|
|
633
|
+
if (originalSend?.targetDigest !== value.targetBindingDigest) {
|
|
634
|
+
throw new OperationStateError("artifact_transfer_target_mismatch", "Artifact transfer targetBindingDigest must match the durable original Send target.");
|
|
635
|
+
}
|
|
636
|
+
assertDigest(value.targetBindingDigest, "artifactTransfer.targetBindingDigest");
|
|
637
|
+
assertStableIdentifier(value.assistantTurnId, "artifactTransfer.assistantTurnId");
|
|
638
|
+
assertDigest(value.sourceIdentityDigest, "artifactTransfer.sourceIdentityDigest");
|
|
639
|
+
if (value.kind !== "file" && value.kind !== "image" && value.kind !== "other") {
|
|
640
|
+
throw new OperationStateError("artifact_transfer_kind_invalid", "Artifact transfer kind is unsupported.");
|
|
641
|
+
}
|
|
642
|
+
if (!Number.isSafeInteger(value.ordinal) || value.ordinal < 0) {
|
|
643
|
+
throw new OperationStateError("artifact_transfer_ordinal_invalid", "Artifact transfer ordinal must be a non-negative safe integer.");
|
|
644
|
+
}
|
|
645
|
+
assertOperationId(value.transferActionId, "artifactTransfer.transferActionId");
|
|
646
|
+
assertDigest(value.destinationIdentityDigest, "artifactTransfer.destinationIdentityDigest");
|
|
647
|
+
}
|
|
648
|
+
function validateArtifactTransferIntentValues(intent, state) {
|
|
649
|
+
if (intent.schemaVersion !== OPERATION_ARTIFACT_TRANSFER_INTENT_SCHEMA_VERSION) {
|
|
650
|
+
throw new OperationStateError("artifact_transfer_intent_schema_invalid", "Artifact transfer intent schemaVersion is unsupported.");
|
|
651
|
+
}
|
|
652
|
+
if (intent.actionKind !== "local_output_commit" || intent.repeatPolicy !== "reconcile_local_effect") {
|
|
653
|
+
throw new OperationStateError("artifact_transfer_action_policy_invalid", "Artifact transfer intent action policy is unsupported.");
|
|
654
|
+
}
|
|
655
|
+
assertTimestamp(intent.intentAt, "artifactTransfer.intentAt");
|
|
656
|
+
validateArtifactTransferIdentityValues(intent, state);
|
|
657
|
+
}
|
|
658
|
+
function validateArtifactTransferReceiptValues(receipt, intent, state) {
|
|
659
|
+
if (receipt.schemaVersion !== OPERATION_ARTIFACT_TRANSFER_RECEIPT_SCHEMA_VERSION) {
|
|
660
|
+
throw new OperationStateError("artifact_transfer_receipt_schema_invalid", "Artifact transfer receipt schemaVersion is unsupported.");
|
|
661
|
+
}
|
|
662
|
+
validateArtifactTransferIdentityValues(receipt, state);
|
|
663
|
+
if (receipt.operationId !== intent.operationId
|
|
664
|
+
|| receipt.requestDigest !== intent.requestDigest
|
|
665
|
+
|| receipt.targetBindingDigest !== intent.targetBindingDigest
|
|
666
|
+
|| receipt.assistantTurnId !== intent.assistantTurnId
|
|
667
|
+
|| receipt.sourceIdentityDigest !== intent.sourceIdentityDigest
|
|
668
|
+
|| receipt.kind !== intent.kind
|
|
669
|
+
|| receipt.ordinal !== intent.ordinal
|
|
670
|
+
|| receipt.transferActionId !== intent.transferActionId
|
|
671
|
+
|| receipt.destinationIdentityDigest !== intent.destinationIdentityDigest) {
|
|
672
|
+
throw new OperationStateError("artifact_transfer_identity_mismatch", "Artifact transfer receipt identity does not match its intent.");
|
|
673
|
+
}
|
|
674
|
+
if (receipt.outputKey !== undefined && (typeof receipt.outputKey !== "string" || !TRANSFER_OUTPUT_KEY_PATTERN.test(receipt.outputKey))) {
|
|
675
|
+
throw new OperationStateError("artifact_transfer_output_key_invalid", "Artifact transfer outputKey is unsupported.");
|
|
676
|
+
}
|
|
677
|
+
if (receipt.bytes !== undefined && (!Number.isSafeInteger(receipt.bytes) || receipt.bytes < 0)) {
|
|
678
|
+
throw new OperationStateError("artifact_transfer_bytes_invalid", "Artifact transfer bytes must be a non-negative safe integer.");
|
|
679
|
+
}
|
|
680
|
+
if (receipt.sha256 !== undefined && (typeof receipt.sha256 !== "string" || !SHA256_PATTERN.test(receipt.sha256))) {
|
|
681
|
+
throw new OperationStateError("artifact_transfer_sha256_invalid", "Artifact transfer sha256 must be lowercase hexadecimal.");
|
|
682
|
+
}
|
|
683
|
+
if (receipt.blockerCode !== undefined && (typeof receipt.blockerCode !== "string" || !CODE_PATTERN.test(receipt.blockerCode))) {
|
|
684
|
+
throw new OperationStateError("artifact_transfer_blocker_invalid", "Artifact transfer blockerCode is unsupported.");
|
|
685
|
+
}
|
|
686
|
+
if (receipt.status !== "transferred" && receipt.status !== "partial" && receipt.status !== "blocked") {
|
|
687
|
+
throw new OperationStateError("artifact_transfer_status_invalid", "Artifact transfer status is unsupported.");
|
|
688
|
+
}
|
|
689
|
+
if (receipt.status === "transferred" && (receipt.outputKey === undefined || receipt.bytes === undefined || receipt.sha256 === undefined || receipt.blockerCode !== undefined)) {
|
|
690
|
+
throw new OperationStateError("artifact_transfer_receipt_incomplete", "Transferred artifact receipts require outputKey, bytes, and sha256 without a blocker.");
|
|
691
|
+
}
|
|
692
|
+
if (receipt.status !== "transferred" && receipt.blockerCode === undefined) {
|
|
693
|
+
throw new OperationStateError("artifact_transfer_blocker_missing", "Partial or blocked artifact receipts require blockerCode.");
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
function artifactTransferTuple(intent) {
|
|
697
|
+
return canonicalJson({
|
|
698
|
+
operationId: intent.operationId,
|
|
699
|
+
requestDigest: intent.requestDigest,
|
|
700
|
+
targetBindingDigest: intent.targetBindingDigest,
|
|
701
|
+
assistantTurnId: intent.assistantTurnId,
|
|
702
|
+
sourceIdentityDigest: intent.sourceIdentityDigest,
|
|
703
|
+
kind: intent.kind,
|
|
704
|
+
ordinal: intent.ordinal,
|
|
705
|
+
destinationIdentityDigest: intent.destinationIdentityDigest
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
function assertArtifactTransferIntentShape(value) {
|
|
709
|
+
assertExactRecord(value, "artifact transfer intent", ["schemaVersion", "operationId", "requestDigest", "targetBindingDigest", "assistantTurnId", "sourceIdentityDigest", "kind", "ordinal", "transferActionId", "destinationIdentityDigest", "actionKind", "repeatPolicy", "intentAt"], ["schemaVersion", "operationId", "requestDigest", "targetBindingDigest", "assistantTurnId", "sourceIdentityDigest", "kind", "ordinal", "transferActionId", "destinationIdentityDigest", "actionKind", "repeatPolicy", "intentAt"]);
|
|
710
|
+
if (value.schemaVersion !== OPERATION_ARTIFACT_TRANSFER_INTENT_SCHEMA_VERSION)
|
|
711
|
+
throw new OperationStateError("artifact_transfer_intent_schema_invalid", "Artifact transfer intent schemaVersion is unsupported.");
|
|
712
|
+
assertOperationId(value.operationId, "artifactTransfer.operationId");
|
|
713
|
+
assertDigest(value.requestDigest, "artifactTransfer.requestDigest");
|
|
714
|
+
assertDigest(value.targetBindingDigest, "artifactTransfer.targetBindingDigest");
|
|
715
|
+
assertStableIdentifier(value.assistantTurnId, "artifactTransfer.assistantTurnId");
|
|
716
|
+
assertDigest(value.sourceIdentityDigest, "artifactTransfer.sourceIdentityDigest");
|
|
717
|
+
if (value.kind !== "file" && value.kind !== "image" && value.kind !== "other")
|
|
718
|
+
throw new OperationStateError("artifact_transfer_kind_invalid", "Artifact transfer kind is unsupported.");
|
|
719
|
+
if (!Number.isSafeInteger(value.ordinal) || value.ordinal < 0)
|
|
720
|
+
throw new OperationStateError("artifact_transfer_ordinal_invalid", "Artifact transfer ordinal must be non-negative.");
|
|
721
|
+
assertOperationId(value.transferActionId, "artifactTransfer.transferActionId");
|
|
722
|
+
assertDigest(value.destinationIdentityDigest, "artifactTransfer.destinationIdentityDigest");
|
|
723
|
+
if (value.actionKind !== "local_output_commit" || value.repeatPolicy !== "reconcile_local_effect")
|
|
724
|
+
throw new OperationStateError("artifact_transfer_action_policy_invalid", "Artifact transfer intent action policy is unsupported.");
|
|
725
|
+
assertTimestamp(value.intentAt, "artifactTransfer.intentAt");
|
|
726
|
+
}
|
|
727
|
+
function assertArtifactTransferReceiptShape(value) {
|
|
728
|
+
assertExactRecord(value, "artifact transfer receipt", ["schemaVersion", "operationId", "requestDigest", "targetBindingDigest", "assistantTurnId", "sourceIdentityDigest", "kind", "ordinal", "transferActionId", "destinationIdentityDigest", "outputKey", "bytes", "sha256", "status", "blockerCode", "observedAt"], ["schemaVersion", "operationId", "requestDigest", "targetBindingDigest", "assistantTurnId", "sourceIdentityDigest", "kind", "ordinal", "transferActionId", "destinationIdentityDigest", "status", "observedAt"]);
|
|
729
|
+
if (value.schemaVersion !== OPERATION_ARTIFACT_TRANSFER_RECEIPT_SCHEMA_VERSION)
|
|
730
|
+
throw new OperationStateError("artifact_transfer_receipt_schema_invalid", "Artifact transfer receipt schemaVersion is unsupported.");
|
|
731
|
+
assertOperationId(value.operationId, "artifactTransfer.operationId");
|
|
732
|
+
assertDigest(value.requestDigest, "artifactTransfer.requestDigest");
|
|
733
|
+
assertDigest(value.targetBindingDigest, "artifactTransfer.targetBindingDigest");
|
|
734
|
+
assertStableIdentifier(value.assistantTurnId, "artifactTransfer.assistantTurnId");
|
|
735
|
+
assertDigest(value.sourceIdentityDigest, "artifactTransfer.sourceIdentityDigest");
|
|
736
|
+
if (value.kind !== "file" && value.kind !== "image" && value.kind !== "other")
|
|
737
|
+
throw new OperationStateError("artifact_transfer_kind_invalid", "Artifact transfer kind is unsupported.");
|
|
738
|
+
if (!Number.isSafeInteger(value.ordinal) || value.ordinal < 0)
|
|
739
|
+
throw new OperationStateError("artifact_transfer_ordinal_invalid", "Artifact transfer ordinal must be non-negative.");
|
|
740
|
+
assertOperationId(value.transferActionId, "artifactTransfer.transferActionId");
|
|
741
|
+
assertDigest(value.destinationIdentityDigest, "artifactTransfer.destinationIdentityDigest");
|
|
742
|
+
if (value.outputKey !== undefined && (typeof value.outputKey !== "string" || !TRANSFER_OUTPUT_KEY_PATTERN.test(value.outputKey)))
|
|
743
|
+
throw new OperationStateError("artifact_transfer_output_key_invalid", "Artifact transfer outputKey is unsupported.");
|
|
744
|
+
if (value.bytes !== undefined && (!Number.isSafeInteger(value.bytes) || value.bytes < 0))
|
|
745
|
+
throw new OperationStateError("artifact_transfer_bytes_invalid", "Artifact transfer bytes must be non-negative.");
|
|
746
|
+
if (value.sha256 !== undefined && (typeof value.sha256 !== "string" || !SHA256_PATTERN.test(value.sha256)))
|
|
747
|
+
throw new OperationStateError("artifact_transfer_sha256_invalid", "Artifact transfer sha256 is unsupported.");
|
|
748
|
+
if (value.status !== "transferred" && value.status !== "partial" && value.status !== "blocked")
|
|
749
|
+
throw new OperationStateError("artifact_transfer_status_invalid", "Artifact transfer status is unsupported.");
|
|
750
|
+
if (value.blockerCode !== undefined && (typeof value.blockerCode !== "string" || !CODE_PATTERN.test(value.blockerCode)))
|
|
751
|
+
throw new OperationStateError("artifact_transfer_blocker_invalid", "Artifact transfer blockerCode is unsupported.");
|
|
752
|
+
assertTimestamp(value.observedAt, "artifactTransfer.observedAt");
|
|
753
|
+
if (value.status === "transferred" && (value.outputKey === undefined || value.bytes === undefined || value.sha256 === undefined || value.blockerCode !== undefined))
|
|
754
|
+
throw new OperationStateError("artifact_transfer_receipt_incomplete", "Transferred artifact receipts require outputKey, bytes, and sha256 without a blocker.");
|
|
755
|
+
if (value.status !== "transferred" && value.blockerCode === undefined)
|
|
756
|
+
throw new OperationStateError("artifact_transfer_blocker_missing", "Partial or blocked artifact receipts require blockerCode.");
|
|
757
|
+
}
|
|
758
|
+
function assertArtifactTransfersStateShape(value) {
|
|
759
|
+
if (!isPlainRecord(value))
|
|
760
|
+
throw new OperationStateError("invalid_operation_state", "Operation artifactTransfers must be an object.");
|
|
761
|
+
for (const transferActionId of Object.keys(value)) {
|
|
762
|
+
assertOperationId(transferActionId, "artifactTransfers map key");
|
|
763
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, transferActionId);
|
|
764
|
+
if (descriptor === undefined || !Object.hasOwn(descriptor, "value") || descriptor.get !== undefined || descriptor.set !== undefined)
|
|
765
|
+
throw new OperationStateError("invalid_operation_shape", "Operation artifactTransfers contains an unsafe property (accessor).");
|
|
766
|
+
const transfer = descriptor.value;
|
|
767
|
+
assertExactRecord(transfer, "artifact transfer state", ["intent", "receipt"], ["intent"]);
|
|
768
|
+
assertArtifactTransferIntentShape(transfer.intent);
|
|
769
|
+
if (transfer.receipt !== undefined)
|
|
770
|
+
assertArtifactTransferReceiptShape(transfer.receipt);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
function validateArtifactTransferStateValues(transferActionId, transfer, state) {
|
|
774
|
+
if (transfer.intent === undefined || transfer.intent.transferActionId !== transferActionId) {
|
|
775
|
+
throw new OperationStateError("artifact_transfer_map_mismatch", "Artifact transfer map key must match its intent transferActionId.");
|
|
776
|
+
}
|
|
777
|
+
validateArtifactTransferIntentValues(transfer.intent, state);
|
|
778
|
+
const action = state.actions[transferActionId];
|
|
779
|
+
if (action === undefined
|
|
780
|
+
|| action.kind !== "local_output_commit"
|
|
781
|
+
|| action.repeatPolicy !== "reconcile_local_effect"
|
|
782
|
+
|| action.requestDigest !== transfer.intent.requestDigest
|
|
783
|
+
|| action.targetDigest !== transfer.intent.targetBindingDigest
|
|
784
|
+
|| action.intentAt !== transfer.intent.intentAt) {
|
|
785
|
+
throw new OperationStateError("artifact_transfer_action_mismatch", "Artifact transfer intent must match its generic local-output action.");
|
|
786
|
+
}
|
|
787
|
+
if (transfer.receipt === undefined) {
|
|
788
|
+
if (action.outcome !== undefined)
|
|
789
|
+
throw new OperationStateError("artifact_transfer_action_mismatch", "Unreceipted artifact transfer cannot have a settled generic action.");
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
validateArtifactTransferReceiptValues(transfer.receipt, transfer.intent, state);
|
|
793
|
+
if (transfer.receipt.observedAt < transfer.intent.intentAt)
|
|
794
|
+
throw new OperationStateError("artifact_transfer_timestamp_regression", "Artifact transfer receipt cannot precede its intent.");
|
|
795
|
+
const expectedOutcome = transfer.receipt.status === "transferred"
|
|
796
|
+
? "satisfied"
|
|
797
|
+
: transfer.receipt.status === "blocked" && transfer.receipt.blockerCode === "output_collision"
|
|
798
|
+
? "not_satisfied"
|
|
799
|
+
: "uncertain";
|
|
800
|
+
if (action.outcome !== expectedOutcome || action.receiptAt !== transfer.receipt.observedAt)
|
|
801
|
+
throw new OperationStateError("artifact_transfer_action_mismatch", "Artifact transfer receipt must match its generic action receipt.");
|
|
802
|
+
if (expectedOutcome === "satisfied" && action.evidenceDigest !== transfer.receipt.destinationIdentityDigest)
|
|
803
|
+
throw new OperationStateError("artifact_transfer_action_mismatch", "Transferred artifact receipt evidence must match destination identity.");
|
|
804
|
+
if (expectedOutcome !== "satisfied" && action.blockerCode !== transfer.receipt.blockerCode)
|
|
805
|
+
throw new OperationStateError("artifact_transfer_action_mismatch", "Artifact transfer blocker must match its generic action receipt.");
|
|
806
|
+
}
|
|
807
|
+
function applyPhaseChange(state, event) {
|
|
808
|
+
if (!(event.from in LEGAL_EDGES) || !(event.to in LEGAL_EDGES)) {
|
|
809
|
+
throw new OperationStateError("invalid_operation_phase", "Phase transition contains an unknown phase.");
|
|
810
|
+
}
|
|
811
|
+
if (!(event.mutationBoundary in BOUNDARY_RANK)) {
|
|
812
|
+
throw new OperationStateError("invalid_mutation_boundary", "Phase transition contains an unknown mutation boundary.");
|
|
813
|
+
}
|
|
814
|
+
if (event.evidenceDigest !== undefined)
|
|
815
|
+
assertDigest(event.evidenceDigest, "phase.evidenceDigest");
|
|
816
|
+
if (state.target === undefined && event.to !== "uncertain") {
|
|
817
|
+
throw new OperationStateError("target_not_bound", `Transition to ${event.to} requires a durable target binding.`);
|
|
818
|
+
}
|
|
819
|
+
if (state.target?.targetLifecycle === "new_pending"
|
|
820
|
+
&& (event.to === "submitted" || event.to === "generating" || event.to === "capturing" || event.to === "completed")) {
|
|
821
|
+
throw new OperationStateError("new_target_not_established", "A pending new target must be established from post-Send evidence before submission can be reported.");
|
|
822
|
+
}
|
|
823
|
+
if (event.from !== state.phase) {
|
|
824
|
+
throw new OperationStateError("phase_mismatch", `Phase event expected ${event.from}, current phase is ${state.phase}.`);
|
|
825
|
+
}
|
|
826
|
+
if (!LEGAL_EDGES[state.phase].has(event.to)) {
|
|
827
|
+
throw new OperationStateError("illegal_phase_transition", `Illegal operation transition ${state.phase} -> ${event.to}.`);
|
|
828
|
+
}
|
|
829
|
+
if (event.mutationBoundary !== state.mutationBoundary) {
|
|
830
|
+
throw new OperationStateError("mutation_boundary_mismatch", `Phase transition must preserve the durable mutation boundary ${state.mutationBoundary}.`);
|
|
831
|
+
}
|
|
832
|
+
const cause = event.causeActionId === undefined ? undefined : state.actions[event.causeActionId];
|
|
833
|
+
if (event.causeActionId !== undefined && cause === undefined) {
|
|
834
|
+
throw new OperationStateError("transition_cause_missing", `Transition cause ${event.causeActionId} is not recorded.`);
|
|
835
|
+
}
|
|
836
|
+
assertCausalTransition(state, event.to, event.mutationBoundary, cause, event.evidenceDigest);
|
|
837
|
+
return { ...state, phase: event.to, mutationBoundary: event.mutationBoundary };
|
|
838
|
+
}
|
|
839
|
+
function assertCausalTransition(state, to, boundary, cause, evidenceDigest) {
|
|
840
|
+
if (state.phase === "uncertain") {
|
|
841
|
+
if (cause === undefined || cause.outcome !== "satisfied") {
|
|
842
|
+
throw new OperationStateError("uncertain_recovery_unproven", "Recovery from uncertain requires a satisfied causal action receipt.");
|
|
843
|
+
}
|
|
844
|
+
if (to === "ready")
|
|
845
|
+
requireCause(cause, "file_handoff", "satisfied");
|
|
846
|
+
if (to === "generating" || to === "capturing")
|
|
847
|
+
requireCause(cause, "send", "satisfied");
|
|
848
|
+
requireEvidence(evidenceDigest ?? cause.evidenceDigest);
|
|
849
|
+
}
|
|
850
|
+
if (to === "handoff_pending") {
|
|
851
|
+
requireUnreceiptedCause(cause, "file_handoff");
|
|
852
|
+
requireBoundary(boundary, "handoff_may_have_occurred");
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
if (to === "send_pending") {
|
|
856
|
+
requireUnreceiptedCause(cause, "send");
|
|
857
|
+
requireBoundary(boundary, "send_may_have_occurred");
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
if (to === "submitted") {
|
|
861
|
+
if (cause?.kind !== "send") {
|
|
862
|
+
throw new OperationStateError("invalid_transition_cause", "submitted requires the causal original Send action.");
|
|
863
|
+
}
|
|
864
|
+
requireSatisfiedCause(cause, evidenceDigest);
|
|
865
|
+
requireDurableOriginalSendOwnership(state);
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
if (to === "ready" && state.phase === "prepared") {
|
|
869
|
+
requireEvidence(evidenceDigest);
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
if (to === "ready" && state.phase === "handoff_pending") {
|
|
873
|
+
requireCause(cause, "file_handoff", "satisfied");
|
|
874
|
+
requireEvidence(evidenceDigest);
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
if (to === "generating" || to === "capturing" || to === "completed") {
|
|
878
|
+
requireEvidence(evidenceDigest);
|
|
879
|
+
requireDurableOriginalSendOwnership(state);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
function applyCompletedReceipt(state, receipt, observedAt) {
|
|
883
|
+
if (state.phase !== "capturing" && state.phase !== "uncertain") {
|
|
884
|
+
throw new OperationStateError("receipt_phase_invalid", "Terminal receipt can only be persisted while capturing or after ownership recovery.");
|
|
885
|
+
}
|
|
886
|
+
if (receipt.operationId !== state.operationId || receipt.requestDigest !== state.requestDigest) {
|
|
887
|
+
throw new OperationStateError("receipt_identity_mismatch", "Terminal receipt does not match the operation identity.");
|
|
888
|
+
}
|
|
889
|
+
if (state.receipt !== undefined) {
|
|
890
|
+
throw new OperationStateError("duplicate_terminal_receipt", "Operation already has a terminal receipt.");
|
|
891
|
+
}
|
|
892
|
+
if (state.target === undefined) {
|
|
893
|
+
throw new OperationStateError("target_not_bound", "Terminal receipt requires a durable target binding.");
|
|
894
|
+
}
|
|
895
|
+
if (state.target.targetLifecycle === "new_pending") {
|
|
896
|
+
throw new OperationStateError("new_target_not_established", "Terminal receipt requires an established new-target identity.");
|
|
897
|
+
}
|
|
898
|
+
requireDurableOriginalSendOwnership(state);
|
|
899
|
+
validateReceiptValues(receipt, state);
|
|
900
|
+
assertTimestampNotBefore(receipt.completedAt, observedAt, "receipt observedAt");
|
|
901
|
+
return { ...state, phase: "completed", receipt };
|
|
902
|
+
}
|
|
903
|
+
function assertPersistedActionRecord(action, state) {
|
|
904
|
+
assertOperationId(action.actionId, "actionId");
|
|
905
|
+
if (!(action.kind in ACTION_POLICY) || action.repeatPolicy !== requiredRepeatPolicy(action.kind)) {
|
|
906
|
+
throw new OperationStateError("invalid_repeat_policy", "Persisted action kind and repeat policy are inconsistent.");
|
|
907
|
+
}
|
|
908
|
+
assertDigest(action.requestDigest, "action.requestDigest");
|
|
909
|
+
if (action.kind !== "stop" && action.kind !== "work_steer" && action.requestDigest !== state.requestDigest) {
|
|
910
|
+
throw new OperationStateError("action_request_mismatch", "Only caller-owned control actions may carry a request digest distinct from the parent operation.");
|
|
911
|
+
}
|
|
912
|
+
if (action.parentActionId !== undefined) {
|
|
913
|
+
assertOperationId(action.parentActionId, "action.parentActionId");
|
|
914
|
+
if (state.actions[action.parentActionId] === undefined) {
|
|
915
|
+
throw new OperationStateError("unknown_parent_action", "Persisted action parent is not present in the operation ledger.");
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
if (action.targetDigest !== undefined)
|
|
919
|
+
assertDigest(action.targetDigest, "action.targetDigest");
|
|
920
|
+
if (action.kind !== "status_read" && (state.target === undefined || action.targetDigest === undefined)) {
|
|
921
|
+
throw new OperationStateError("action_target_missing", "Persisted mutating or target-bound action requires a durable target digest.");
|
|
922
|
+
}
|
|
923
|
+
if (!Number.isSafeInteger(action.intentRevision) || action.intentRevision < 1 || action.intentRevision > state.revision) {
|
|
924
|
+
throw new OperationStateError("invalid_action_revision", "Action intent revision is outside the durable state revision.");
|
|
925
|
+
}
|
|
926
|
+
assertTimestamp(action.intentAt, "action.intentAt");
|
|
927
|
+
assertTimestampNotBefore(action.intentAt, state.updatedAt, "state.updatedAt");
|
|
928
|
+
if (action.outcome === undefined) {
|
|
929
|
+
if (action.receiptRevision !== undefined || action.receiptAt !== undefined || action.evidenceDigest !== undefined || action.blockerCode !== undefined) {
|
|
930
|
+
throw new OperationStateError("action_receipt_incomplete", "Unsettled action cannot contain receipt-only fields.");
|
|
931
|
+
}
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
if (action.outcome !== "satisfied" && action.outcome !== "not_satisfied" && action.outcome !== "uncertain") {
|
|
935
|
+
throw new OperationStateError("invalid_action_outcome", "Persisted action outcome is unsupported.");
|
|
936
|
+
}
|
|
937
|
+
if (!Number.isSafeInteger(action.receiptRevision)
|
|
938
|
+
|| action.receiptRevision <= action.intentRevision
|
|
939
|
+
|| action.receiptRevision > state.revision
|
|
940
|
+
|| action.receiptAt === undefined) {
|
|
941
|
+
throw new OperationStateError("action_receipt_incomplete", "Settled action requires a later bounded receipt revision and timestamp.");
|
|
942
|
+
}
|
|
943
|
+
assertTimestamp(action.receiptAt, "action.receiptAt");
|
|
944
|
+
assertTimestampNotBefore(action.intentAt, action.receiptAt, "action.receiptAt");
|
|
945
|
+
assertTimestampNotBefore(action.receiptAt, state.updatedAt, "state.updatedAt");
|
|
946
|
+
if (action.evidenceDigest !== undefined)
|
|
947
|
+
assertDigest(action.evidenceDigest, "action.evidenceDigest");
|
|
948
|
+
if (action.outcome === "satisfied" && action.evidenceDigest === undefined) {
|
|
949
|
+
throw new OperationStateError("action_evidence_missing", "Satisfied action requires durable evidence.");
|
|
950
|
+
}
|
|
951
|
+
if (action.blockerCode !== undefined && !CODE_PATTERN.test(action.blockerCode)) {
|
|
952
|
+
throw new OperationStateError("invalid_blocker_code", "Action blockerCode must be a bounded canonical code.");
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
function validateBlockerObservation(blocker) {
|
|
956
|
+
if (!CODE_PATTERN.test(blocker.code)) {
|
|
957
|
+
throw new OperationStateError("invalid_blocker_code", "Blocker code must be a bounded canonical code.");
|
|
958
|
+
}
|
|
959
|
+
assertDigest(blocker.messageDigest, "blocker.messageDigest");
|
|
960
|
+
if (typeof blocker.recoverable !== "boolean") {
|
|
961
|
+
throw new OperationStateError("invalid_blocker_recoverability", "Blocker recoverable must be boolean.");
|
|
962
|
+
}
|
|
963
|
+
assertTimestamp(blocker.observedAt, "blocker.observedAt");
|
|
964
|
+
}
|
|
965
|
+
function validateReceiptValues(receipt, state) {
|
|
966
|
+
if (receipt.schemaVersion !== OPERATION_RECEIPT_SCHEMA_VERSION) {
|
|
967
|
+
throw new OperationStateError("unsupported_operation_receipt", "Terminal receipt schemaVersion is unsupported.");
|
|
968
|
+
}
|
|
969
|
+
if (state.capturePolicy !== undefined
|
|
970
|
+
&& receipt.responseFormat !== state.capturePolicy.responseFormat) {
|
|
971
|
+
throw new OperationStateError("receipt_capture_policy_mismatch", "Terminal receipt format does not match the immutable capture policy.");
|
|
972
|
+
}
|
|
973
|
+
assertOperationId(receipt.operationId);
|
|
974
|
+
if (receipt.operationId !== state.operationId || receipt.requestDigest !== state.requestDigest) {
|
|
975
|
+
throw new OperationStateError("receipt_identity_mismatch", "Terminal receipt does not match the operation identity.");
|
|
976
|
+
}
|
|
977
|
+
assertDigest(receipt.requestDigest, "receipt.requestDigest");
|
|
978
|
+
assertDigest(receipt.targetBindingDigest, "receipt.targetBindingDigest");
|
|
979
|
+
assertDigest(receipt.userTurnEvidenceDigest, "receipt.userTurnEvidenceDigest");
|
|
980
|
+
assertDigest(receipt.ownershipEvidenceDigest, "receipt.ownershipEvidenceDigest");
|
|
981
|
+
assertStableIdentifier(receipt.userTurnId, "receipt.userTurnId");
|
|
982
|
+
assertStableIdentifier(receipt.assistantTurnId, "receipt.assistantTurnId");
|
|
983
|
+
if (!FINISH_REASON_PATTERN.test(receipt.finishReason)) {
|
|
984
|
+
throw new OperationStateError("receipt_finish_reason_invalid", "Terminal receipt finishReason must be a bounded canonical value.");
|
|
985
|
+
}
|
|
986
|
+
if (typeof receipt.contentAvailable !== "boolean") {
|
|
987
|
+
throw new OperationStateError("receipt_content_availability_invalid", "Terminal receipt contentAvailable must be boolean.");
|
|
988
|
+
}
|
|
989
|
+
const hasResponseDigest = receipt.responseDigest !== undefined;
|
|
990
|
+
const hasResponseBytes = receipt.responseBytes !== undefined;
|
|
991
|
+
if (hasResponseDigest !== hasResponseBytes || (receipt.contentAvailable && !hasResponseDigest)) {
|
|
992
|
+
throw new OperationStateError("receipt_response_metadata_incomplete", "Response digest and byte count must be paired and present when content is available.");
|
|
993
|
+
}
|
|
994
|
+
if (receipt.responseDigest !== undefined)
|
|
995
|
+
assertDigest(receipt.responseDigest, "receipt.responseDigest");
|
|
996
|
+
if (receipt.responseBytes !== undefined && (!Number.isSafeInteger(receipt.responseBytes) || receipt.responseBytes < 0)) {
|
|
997
|
+
throw new OperationStateError("receipt_response_bytes_invalid", "Terminal receipt responseBytes must be a non-negative safe integer.");
|
|
998
|
+
}
|
|
999
|
+
assertTimestamp(receipt.completedAt, "receipt.completedAt");
|
|
1000
|
+
assertTimestampNotBefore(state.createdAt, receipt.completedAt, "receipt.completedAt");
|
|
1001
|
+
if (state.phase === "completed")
|
|
1002
|
+
assertTimestampNotBefore(receipt.completedAt, state.updatedAt, "state.updatedAt");
|
|
1003
|
+
const submitAction = Object.values(state.actions).find(action => action.kind === "send");
|
|
1004
|
+
if (submitAction?.targetDigest === undefined || submitAction.targetDigest !== receipt.targetBindingDigest) {
|
|
1005
|
+
throw new OperationStateError("receipt_target_mismatch", "Terminal receipt does not match the submitted target binding.");
|
|
1006
|
+
}
|
|
1007
|
+
if (submitAction.outcome !== "satisfied") {
|
|
1008
|
+
throw new OperationStateError("receipt_submission_unproven", "Terminal receipt requires a satisfied original Send action.");
|
|
1009
|
+
}
|
|
1010
|
+
if (!Array.isArray(receipt.artifacts) || receipt.artifacts.length > MAX_ARTIFACTS) {
|
|
1011
|
+
throw new OperationStateError("receipt_artifacts_invalid", `Terminal receipt may contain at most ${MAX_ARTIFACTS} artifacts.`);
|
|
1012
|
+
}
|
|
1013
|
+
const artifactKeys = new Set();
|
|
1014
|
+
const artifactOrdinals = new Set();
|
|
1015
|
+
for (const artifact of receipt.artifacts) {
|
|
1016
|
+
if (artifact.schemaVersion !== OPERATION_ARTIFACT_RECEIPT_SCHEMA_VERSION) {
|
|
1017
|
+
throw new OperationStateError("unsupported_artifact_receipt", "Artifact receipt schemaVersion is unsupported.");
|
|
1018
|
+
}
|
|
1019
|
+
if (artifact.operationId !== receipt.operationId) {
|
|
1020
|
+
throw new OperationStateError("artifact_operation_mismatch", "Artifact receipt must belong to the terminal operation.");
|
|
1021
|
+
}
|
|
1022
|
+
if (artifact.assistantTurnId !== receipt.assistantTurnId) {
|
|
1023
|
+
throw new OperationStateError("artifact_turn_mismatch", "Artifact receipt must belong to the terminal assistant turn.");
|
|
1024
|
+
}
|
|
1025
|
+
if (!ARTIFACT_KEY_PATTERN.test(artifact.artifactKey) || artifactKeys.has(artifact.artifactKey)) {
|
|
1026
|
+
throw new OperationStateError("artifact_key_invalid", "Artifact receipt keys must be bounded canonical values and unique.");
|
|
1027
|
+
}
|
|
1028
|
+
artifactKeys.add(artifact.artifactKey);
|
|
1029
|
+
assertDigest(artifact.sourceIdentityDigest, "artifact.sourceIdentityDigest");
|
|
1030
|
+
if (artifact.kind !== "file" && artifact.kind !== "image" && artifact.kind !== "other") {
|
|
1031
|
+
throw new OperationStateError("artifact_kind_invalid", "Artifact receipt kind is unsupported.");
|
|
1032
|
+
}
|
|
1033
|
+
if (artifact.status !== "available" && artifact.status !== "transferred" && artifact.status !== "partial" && artifact.status !== "blocked") {
|
|
1034
|
+
throw new OperationStateError("artifact_status_invalid", "Artifact receipt status is unsupported.");
|
|
1035
|
+
}
|
|
1036
|
+
if (!Number.isSafeInteger(artifact.ordinal)
|
|
1037
|
+
|| artifact.ordinal < 0
|
|
1038
|
+
|| artifact.ordinal >= MAX_ARTIFACTS
|
|
1039
|
+
|| artifactOrdinals.has(artifact.ordinal)) {
|
|
1040
|
+
throw new OperationStateError("artifact_ordinal_invalid", `Artifact receipt ordinals must be unique safe integers from 0 through ${MAX_ARTIFACTS - 1}.`);
|
|
1041
|
+
}
|
|
1042
|
+
artifactOrdinals.add(artifact.ordinal);
|
|
1043
|
+
if (artifact.outputKey !== undefined && !isSafeRelativeOutputKey(artifact.outputKey)) {
|
|
1044
|
+
throw new OperationStateError("artifact_output_key_invalid", "Artifact outputKey must be a safe relative opaque key.");
|
|
1045
|
+
}
|
|
1046
|
+
if (artifact.mimeType !== undefined && !MIME_PATTERN.test(artifact.mimeType)) {
|
|
1047
|
+
throw new OperationStateError("artifact_mime_type_invalid", "Artifact MIME type must be a bounded canonical value.");
|
|
1048
|
+
}
|
|
1049
|
+
if (artifact.bytes !== undefined && (!Number.isSafeInteger(artifact.bytes) || artifact.bytes < 0)) {
|
|
1050
|
+
throw new OperationStateError("artifact_bytes_invalid", "Artifact receipt bytes must be a non-negative safe integer.");
|
|
1051
|
+
}
|
|
1052
|
+
if (artifact.sha256 !== undefined && !SHA256_PATTERN.test(artifact.sha256)) {
|
|
1053
|
+
throw new OperationStateError("artifact_digest_invalid", "Artifact receipt sha256 must be lowercase hexadecimal.");
|
|
1054
|
+
}
|
|
1055
|
+
if (artifact.blockerCode !== undefined && !CODE_PATTERN.test(artifact.blockerCode)) {
|
|
1056
|
+
throw new OperationStateError("artifact_blocker_invalid", "Artifact blockerCode must be a bounded canonical code.");
|
|
1057
|
+
}
|
|
1058
|
+
if (artifact.status === "transferred" && (artifact.outputKey === undefined || artifact.bytes === undefined || artifact.sha256 === undefined)) {
|
|
1059
|
+
throw new OperationStateError("artifact_transfer_receipt_incomplete", "Transferred artifacts require outputKey, bytes, and sha256.");
|
|
1060
|
+
}
|
|
1061
|
+
if ((artifact.status === "partial" || artifact.status === "blocked") && artifact.blockerCode === undefined) {
|
|
1062
|
+
throw new OperationStateError("artifact_blocker_missing", "Partial or blocked artifacts require blockerCode.");
|
|
1063
|
+
}
|
|
1064
|
+
if ((artifact.status === "available" || artifact.status === "transferred") && artifact.blockerCode !== undefined) {
|
|
1065
|
+
throw new OperationStateError("artifact_blocker_unexpected", "Available or transferred artifacts cannot contain a blockerCode.");
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
validateTerminalArtifactCapturePolicy(receipt, state);
|
|
1069
|
+
}
|
|
1070
|
+
/**
|
|
1071
|
+
* Enforce the durable artifact obligation at the only point where an
|
|
1072
|
+
* operation may become terminal. Transfer state is deliberately keyed by a
|
|
1073
|
+
* transfer action (which also includes a destination identity), while the
|
|
1074
|
+
* terminal receipt is keyed by the exact provider artifact identity. The
|
|
1075
|
+
* latter therefore needs a separate one-to-one reconciliation here.
|
|
1076
|
+
*
|
|
1077
|
+
* A missing capture policy is a legacy state shape. It retains the historic
|
|
1078
|
+
* receipt-only validation and is intentionally not upgraded or downgraded by
|
|
1079
|
+
* this function. New transfer records, however, are never legal without an
|
|
1080
|
+
* explicit durable transfer policy (enforced when their intent is applied and
|
|
1081
|
+
* in validateStateCoherence above).
|
|
1082
|
+
*/
|
|
1083
|
+
function validateTerminalArtifactCapturePolicy(receipt, state) {
|
|
1084
|
+
const policy = state.capturePolicy?.artifacts;
|
|
1085
|
+
if (policy === undefined)
|
|
1086
|
+
return;
|
|
1087
|
+
const transfers = state.artifactTransfers ?? {};
|
|
1088
|
+
const transferEntries = Object.entries(transfers);
|
|
1089
|
+
if (policy === "receipt_only") {
|
|
1090
|
+
if (transferEntries.length > 0) {
|
|
1091
|
+
throw new OperationStateError("artifact_transfer_policy_mismatch", "Receipt-only completion cannot contain durable artifact transfers.");
|
|
1092
|
+
}
|
|
1093
|
+
for (const artifact of receipt.artifacts) {
|
|
1094
|
+
if (artifact.status !== "available" || artifact.outputKey !== undefined) {
|
|
1095
|
+
throw new OperationStateError("artifact_transfer_policy_mismatch", "Receipt-only completion cannot contain transfer-enriched artifacts.");
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
if (policy !== "transfer") {
|
|
1101
|
+
// The durable policy shape validator should make this unreachable, but
|
|
1102
|
+
// keep the terminal gate fail-closed if it is ever called directly.
|
|
1103
|
+
throw new OperationStateError("invalid_capture_policy", "Terminal artifact policy is unsupported.");
|
|
1104
|
+
}
|
|
1105
|
+
const transferByArtifact = new Map();
|
|
1106
|
+
for (const [transferActionId, transfer] of transferEntries) {
|
|
1107
|
+
if (transfer.receipt === undefined) {
|
|
1108
|
+
throw new OperationStateError("artifact_transfer_unsettled", `Transfer ${transferActionId} has no durable receipt before terminal completion.`);
|
|
1109
|
+
}
|
|
1110
|
+
const identity = artifactTransferArtifactIdentity(transfer.intent);
|
|
1111
|
+
if (transferByArtifact.has(identity)) {
|
|
1112
|
+
throw new OperationStateError("artifact_transfer_duplicate_artifact", "Terminal completion requires at most one settled transfer per exact artifact identity.");
|
|
1113
|
+
}
|
|
1114
|
+
transferByArtifact.set(identity, transfer);
|
|
1115
|
+
}
|
|
1116
|
+
const receiptIdentities = new Set();
|
|
1117
|
+
for (const artifact of receipt.artifacts) {
|
|
1118
|
+
if (artifact.status === "available") {
|
|
1119
|
+
throw new OperationStateError("artifact_transfer_unsettled", "Transfer-policy completion cannot contain an artifact that remains available.");
|
|
1120
|
+
}
|
|
1121
|
+
const identity = artifactReceiptIdentity(artifact);
|
|
1122
|
+
if (receiptIdentities.has(identity)) {
|
|
1123
|
+
throw new OperationStateError("artifact_transfer_duplicate_artifact", "Terminal receipt contains duplicate exact artifact identities.");
|
|
1124
|
+
}
|
|
1125
|
+
receiptIdentities.add(identity);
|
|
1126
|
+
const transfer = transferByArtifact.get(identity);
|
|
1127
|
+
if (transfer === undefined || transfer.receipt === undefined) {
|
|
1128
|
+
throw new OperationStateError("artifact_transfer_intent_missing", "Every terminal transfer-policy artifact requires one matching durable transfer receipt.");
|
|
1129
|
+
}
|
|
1130
|
+
assertTerminalArtifactMatchesTransfer(artifact, transfer.receipt);
|
|
1131
|
+
}
|
|
1132
|
+
if (transferByArtifact.size !== receiptIdentities.size) {
|
|
1133
|
+
throw new OperationStateError("artifact_transfer_extra", "Every durable transfer must have one matching terminal artifact receipt.");
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
function artifactReceiptIdentity(artifact) {
|
|
1137
|
+
return canonicalJson({
|
|
1138
|
+
operationId: artifact.operationId,
|
|
1139
|
+
assistantTurnId: artifact.assistantTurnId,
|
|
1140
|
+
sourceIdentityDigest: artifact.sourceIdentityDigest,
|
|
1141
|
+
kind: artifact.kind,
|
|
1142
|
+
ordinal: artifact.ordinal
|
|
1143
|
+
});
|
|
1144
|
+
}
|
|
1145
|
+
function artifactTransferArtifactIdentity(intent) {
|
|
1146
|
+
return canonicalJson({
|
|
1147
|
+
operationId: intent.operationId,
|
|
1148
|
+
assistantTurnId: intent.assistantTurnId,
|
|
1149
|
+
sourceIdentityDigest: intent.sourceIdentityDigest,
|
|
1150
|
+
kind: intent.kind,
|
|
1151
|
+
ordinal: intent.ordinal
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
function assertTerminalArtifactMatchesTransfer(artifact, transfer) {
|
|
1155
|
+
if (artifact.status !== transfer.status
|
|
1156
|
+
|| artifact.outputKey !== transfer.outputKey
|
|
1157
|
+
|| artifact.bytes !== transfer.bytes
|
|
1158
|
+
|| artifact.sha256 !== transfer.sha256
|
|
1159
|
+
|| artifact.blockerCode !== transfer.blockerCode) {
|
|
1160
|
+
throw new OperationStateError("artifact_transfer_receipt_mismatch", "Terminal artifact status and transfer receipt facts must match exactly.");
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
function validateStateCoherence(state) {
|
|
1164
|
+
if (state.target?.targetLifecycle === "new_pending"
|
|
1165
|
+
&& (state.phase === "submitted" || state.phase === "generating" || state.phase === "capturing" || state.phase === "completed")) {
|
|
1166
|
+
throw new OperationStateError("new_target_not_established", "A pending new target cannot have a submitted, generating, capturing, or completed state.");
|
|
1167
|
+
}
|
|
1168
|
+
const revisions = new Set();
|
|
1169
|
+
const singleIntentKinds = new Set();
|
|
1170
|
+
let expectedBoundary = "none";
|
|
1171
|
+
for (const action of Object.values(state.actions)) {
|
|
1172
|
+
if (revisions.has(action.intentRevision) || (action.receiptRevision !== undefined && revisions.has(action.receiptRevision))) {
|
|
1173
|
+
throw new OperationStateError("duplicate_action_revision", "Action ledger revisions must be unique.");
|
|
1174
|
+
}
|
|
1175
|
+
revisions.add(action.intentRevision);
|
|
1176
|
+
if (action.receiptRevision !== undefined)
|
|
1177
|
+
revisions.add(action.receiptRevision);
|
|
1178
|
+
if (isSingleIntentKind(action.kind)) {
|
|
1179
|
+
if (singleIntentKinds.has(action.kind)) {
|
|
1180
|
+
throw new OperationStateError("nonrepeatable_action_already_intended", "Persisted operation contains duplicate operation-singleton action kinds.");
|
|
1181
|
+
}
|
|
1182
|
+
singleIntentKinds.add(action.kind);
|
|
1183
|
+
}
|
|
1184
|
+
const boundary = boundaryForAction(action.kind);
|
|
1185
|
+
if (boundary !== undefined && BOUNDARY_RANK[boundary] > BOUNDARY_RANK[expectedBoundary])
|
|
1186
|
+
expectedBoundary = boundary;
|
|
1187
|
+
}
|
|
1188
|
+
if (state.mutationBoundary !== expectedBoundary) {
|
|
1189
|
+
throw new OperationStateError("mutation_boundary_inconsistent", "Persisted mutation boundary does not match the durable action ledger.");
|
|
1190
|
+
}
|
|
1191
|
+
const actions = Object.values(state.actions);
|
|
1192
|
+
const hasHandoff = actions.some(action => action.kind === "file_handoff");
|
|
1193
|
+
const submitActions = actions.filter(action => action.kind === "send");
|
|
1194
|
+
const hasSubmit = submitActions.length > 0;
|
|
1195
|
+
validateSubmissionWitnessCollection(state);
|
|
1196
|
+
const ownershipBaselines = state.ownershipBaselines ?? {};
|
|
1197
|
+
for (const [actionId, baseline] of Object.entries(ownershipBaselines)) {
|
|
1198
|
+
const action = state.actions[actionId];
|
|
1199
|
+
if (action === undefined || (action.kind !== "send" && action.kind !== "work_steer")) {
|
|
1200
|
+
throw new OperationStateError("ownership_baseline_action_missing", "Per-action ownership baselines require a durable Send or steer action.");
|
|
1201
|
+
}
|
|
1202
|
+
if (baseline.actionId !== actionId) {
|
|
1203
|
+
throw new OperationStateError("ownership_baseline_map_mismatch", "Per-action ownership baseline key must match its actionId.");
|
|
1204
|
+
}
|
|
1205
|
+
if (action.kind === "send") {
|
|
1206
|
+
if (state.ownershipBaseline === undefined || canonicalJson(state.ownershipBaseline) !== canonicalJson(baseline)) {
|
|
1207
|
+
throw new OperationStateError("ownership_baseline_projection_mismatch", "The Send ownership baseline must match the compatibility projection.");
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
if (state.ownershipBaseline !== undefined) {
|
|
1212
|
+
const projected = ownershipBaselines[state.ownershipBaseline.actionId];
|
|
1213
|
+
if (projected !== undefined && canonicalJson(projected) !== canonicalJson(state.ownershipBaseline)) {
|
|
1214
|
+
throw new OperationStateError("ownership_baseline_projection_mismatch", "The ownership baseline compatibility projection conflicts with its per-action record.");
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
if (state.artifactTransfers !== undefined) {
|
|
1218
|
+
assertArtifactTransfersStateShape(state.artifactTransfers);
|
|
1219
|
+
if (Object.keys(state.artifactTransfers).length > 0
|
|
1220
|
+
&& state.capturePolicy?.artifacts !== "transfer") {
|
|
1221
|
+
throw new OperationStateError("artifact_transfer_policy_mismatch", "Durable artifact transfers require the immutable transfer capture policy.");
|
|
1222
|
+
}
|
|
1223
|
+
const seenTuples = new Set();
|
|
1224
|
+
for (const [transferActionId, transfer] of Object.entries(state.artifactTransfers)) {
|
|
1225
|
+
validateArtifactTransferStateValues(transferActionId, transfer, state);
|
|
1226
|
+
const intent = transfer.intent;
|
|
1227
|
+
const tuple = artifactTransferTuple(intent);
|
|
1228
|
+
if (seenTuples.has(tuple)) {
|
|
1229
|
+
throw new OperationStateError("artifact_transfer_duplicate_tuple", "An artifact transfer tuple may only be transferred once.");
|
|
1230
|
+
}
|
|
1231
|
+
seenTuples.add(tuple);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
if (state.target?.targetLifecycle === "new_established") {
|
|
1235
|
+
const establishment = state.target.targetEstablishment;
|
|
1236
|
+
const submit = submitActions.length === 1 ? submitActions[0] : undefined;
|
|
1237
|
+
if (establishment === undefined || submit === undefined) {
|
|
1238
|
+
throw new OperationStateError("new_target_establishment_send_missing", "An established new target requires exactly one durable Send intent.");
|
|
1239
|
+
}
|
|
1240
|
+
if (establishment.causalSendActionId !== submit.actionId
|
|
1241
|
+
|| establishment.targetBindingDigest !== submit.targetDigest) {
|
|
1242
|
+
throw new OperationStateError("new_target_establishment_target_mismatch", "Established target identity does not match the durable Send action.");
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
if (state.phase === "handoff_pending" && !hasHandoff) {
|
|
1246
|
+
throw new OperationStateError("operation_state_inconsistent", "handoff_pending requires a durable file-handoff intent.");
|
|
1247
|
+
}
|
|
1248
|
+
if (state.phase === "send_pending" && !hasSubmit) {
|
|
1249
|
+
throw new OperationStateError("operation_state_inconsistent", "send_pending requires the durable original Send intent.");
|
|
1250
|
+
}
|
|
1251
|
+
if (["submitted", "generating", "capturing", "completed"].includes(state.phase)) {
|
|
1252
|
+
requireDurableOriginalSendOwnership(state);
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
/**
|
|
1256
|
+
* Terminal ownership is never reconstructed from a generic Send receipt. The
|
|
1257
|
+
* original Send must have its complete pre-action baseline and immutable
|
|
1258
|
+
* post-action witness in the keyed ledgers before an owned phase can be
|
|
1259
|
+
* entered. Compatibility projections are checked as mirrors only; snapshots
|
|
1260
|
+
* that predate the keyed proof fail closed because no migration can recreate
|
|
1261
|
+
* evidence that was never durably observed.
|
|
1262
|
+
*/
|
|
1263
|
+
function requireDurableOriginalSendOwnership(state) {
|
|
1264
|
+
const sendActions = Object.values(state.actions).filter(action => action.kind === "send");
|
|
1265
|
+
if (sendActions.length !== 1) {
|
|
1266
|
+
throw new OperationStateError("operation_state_inconsistent", "Owned submission state requires exactly one durable original Send intent.");
|
|
1267
|
+
}
|
|
1268
|
+
const send = sendActions[0];
|
|
1269
|
+
if (send.outcome !== "satisfied") {
|
|
1270
|
+
throw new OperationStateError("operation_state_inconsistent", "Owned submission state requires a satisfied original Send action.");
|
|
1271
|
+
}
|
|
1272
|
+
const baseline = state.ownershipBaselines?.[send.actionId];
|
|
1273
|
+
if (baseline === undefined) {
|
|
1274
|
+
throw new OperationStateError("ownership_baseline_missing", "Owned submission state requires the keyed pre-Send ownership baseline.");
|
|
1275
|
+
}
|
|
1276
|
+
const witness = state.submissionWitnesses?.[send.actionId];
|
|
1277
|
+
if (witness === undefined) {
|
|
1278
|
+
throw new OperationStateError("submission_witness_missing", "Owned submission state requires the keyed original Send submission witness.");
|
|
1279
|
+
}
|
|
1280
|
+
if (baseline.actionId !== send.actionId
|
|
1281
|
+
|| baseline.operationId !== state.operationId
|
|
1282
|
+
|| baseline.requestDigest !== state.requestDigest
|
|
1283
|
+
|| baseline.targetBindingDigest !== send.targetDigest
|
|
1284
|
+
|| witness.actionId !== send.actionId
|
|
1285
|
+
|| witness.actionKind !== "send"
|
|
1286
|
+
|| witness.targetBindingDigest !== send.targetDigest
|
|
1287
|
+
|| witness.baselineSnapshotDigest !== baseline.baseline.snapshotDigest) {
|
|
1288
|
+
throw new OperationStateError("submission_ownership_mismatch", "Original Send ownership baseline and witness do not match the durable action identity.");
|
|
1289
|
+
}
|
|
1290
|
+
if (state.ownershipBaseline === undefined
|
|
1291
|
+
|| canonicalJson(state.ownershipBaseline) !== canonicalJson(baseline)
|
|
1292
|
+
|| state.submissionWitness === undefined
|
|
1293
|
+
|| canonicalJson(state.submissionWitness) !== canonicalJson(witness)) {
|
|
1294
|
+
throw new OperationStateError("submission_ownership_projection_mismatch", "Original Send ownership compatibility projections must mirror the keyed durable proof.");
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
function validatedTarget(state, target) {
|
|
1298
|
+
validateTargetValues(target);
|
|
1299
|
+
if (state.target === undefined && target.targetLifecycle === "new_established") {
|
|
1300
|
+
throw new OperationStateError("new_target_establishment_order", "A new target must be bound as pending before its target_established event.");
|
|
1301
|
+
}
|
|
1302
|
+
if (state.target !== undefined && canonicalJson(state.target) !== canonicalJson(target)) {
|
|
1303
|
+
throw new OperationStateError("target_binding_mismatch", "Operation target binding is immutable.");
|
|
1304
|
+
}
|
|
1305
|
+
return target;
|
|
1306
|
+
}
|
|
1307
|
+
function establishTarget(state, establishment) {
|
|
1308
|
+
const target = state.target;
|
|
1309
|
+
if (target === undefined) {
|
|
1310
|
+
throw new OperationStateError("target_not_bound", "Target establishment requires a durable target anchor.");
|
|
1311
|
+
}
|
|
1312
|
+
const lifecycle = target.targetLifecycle ?? "fixed";
|
|
1313
|
+
if (lifecycle === "fixed") {
|
|
1314
|
+
throw new OperationStateError("fixed_target_establishment", "A fixed target cannot be established as a new conversation.");
|
|
1315
|
+
}
|
|
1316
|
+
if (lifecycle === "new_established") {
|
|
1317
|
+
throw new OperationStateError("target_already_established", "A new target can be established only once.");
|
|
1318
|
+
}
|
|
1319
|
+
validateTargetEstablishmentValues({
|
|
1320
|
+
...establishment,
|
|
1321
|
+
// The nested record is checked again below against the send intent and
|
|
1322
|
+
// current target. Keeping the full record here makes the event/state
|
|
1323
|
+
// shapes closed and lets authenticated snapshots replay without context.
|
|
1324
|
+
}, {
|
|
1325
|
+
...target,
|
|
1326
|
+
targetLifecycle: "new_established",
|
|
1327
|
+
conversationId: establishment.conversationId,
|
|
1328
|
+
canonicalThreadUrl: establishment.canonicalThreadUrl,
|
|
1329
|
+
targetEstablishment: establishment,
|
|
1330
|
+
evidenceProfile: {
|
|
1331
|
+
...target.evidenceProfile,
|
|
1332
|
+
stableConversationId: "required",
|
|
1333
|
+
stableUserTurnId: "required"
|
|
1334
|
+
}
|
|
1335
|
+
});
|
|
1336
|
+
assertOperationId(establishment.causalSendActionId, "target_established.causalSendActionId");
|
|
1337
|
+
const send = state.actions[establishment.causalSendActionId];
|
|
1338
|
+
if (send === undefined || send.kind !== "send") {
|
|
1339
|
+
throw new OperationStateError("target_establishment_send_missing", "Target establishment requires the causal original Send intent.");
|
|
1340
|
+
}
|
|
1341
|
+
if (send.outcome === "not_satisfied") {
|
|
1342
|
+
throw new OperationStateError("target_establishment_send_rejected", "Target establishment cannot follow a rejected Send intent.");
|
|
1343
|
+
}
|
|
1344
|
+
if (send.targetDigest !== establishment.targetBindingDigest) {
|
|
1345
|
+
throw new OperationStateError("target_establishment_target_mismatch", "Target establishment target digest does not match the causal Send intent.");
|
|
1346
|
+
}
|
|
1347
|
+
if (establishment.observedAt < send.intentAt) {
|
|
1348
|
+
throw new OperationStateError("target_establishment_before_send", "Target establishment cannot precede the durable Send intent.");
|
|
1349
|
+
}
|
|
1350
|
+
if (state.phase === "prepared" || state.phase === "handoff_pending" || state.phase === "completed") {
|
|
1351
|
+
throw new OperationStateError("target_establishment_phase_invalid", "Target establishment requires a durable Send lifecycle phase.");
|
|
1352
|
+
}
|
|
1353
|
+
const establishedTarget = {
|
|
1354
|
+
...target,
|
|
1355
|
+
targetLifecycle: "new_established",
|
|
1356
|
+
conversationId: establishment.conversationId,
|
|
1357
|
+
canonicalThreadUrl: establishment.canonicalThreadUrl,
|
|
1358
|
+
evidenceProfile: {
|
|
1359
|
+
...target.evidenceProfile,
|
|
1360
|
+
stableConversationId: "required",
|
|
1361
|
+
stableUserTurnId: "required"
|
|
1362
|
+
},
|
|
1363
|
+
targetEstablishment: establishment
|
|
1364
|
+
};
|
|
1365
|
+
validateTargetValues(establishedTarget);
|
|
1366
|
+
return establishedTarget;
|
|
1367
|
+
}
|
|
1368
|
+
function validateTargetValues(target) {
|
|
1369
|
+
for (const [label, value] of [["providerId", target.providerId], ["browserId", target.browserId], ["tabId", target.tabId]]) {
|
|
1370
|
+
assertStableIdentifier(value, label);
|
|
1371
|
+
}
|
|
1372
|
+
if (target.conversationId !== undefined)
|
|
1373
|
+
assertStableIdentifier(target.conversationId, "conversationId");
|
|
1374
|
+
for (const [label, digest] of [
|
|
1375
|
+
["userTurnBaselineDigest", target.userTurnBaselineDigest],
|
|
1376
|
+
["assistantTurnBaselineDigest", target.assistantTurnBaselineDigest],
|
|
1377
|
+
["configurationReceiptDigest", target.configurationReceiptDigest]
|
|
1378
|
+
]) {
|
|
1379
|
+
if (digest !== undefined)
|
|
1380
|
+
assertDigest(digest, `target.${label}`);
|
|
1381
|
+
}
|
|
1382
|
+
const profile = target.evidenceProfile;
|
|
1383
|
+
if (!profile ||
|
|
1384
|
+
(profile.providerIdentity !== "required" && profile.providerIdentity !== "unavailable") ||
|
|
1385
|
+
(profile.stableTabId !== "required" && profile.stableTabId !== "unavailable") ||
|
|
1386
|
+
(profile.stableConversationId !== "required" && profile.stableConversationId !== "unavailable") ||
|
|
1387
|
+
(profile.stableUserTurnId !== "required" && profile.stableUserTurnId !== "unavailable") ||
|
|
1388
|
+
(profile.authoritativeTabClaim !== "required" && profile.authoritativeTabClaim !== "unavailable") ||
|
|
1389
|
+
typeof profile.replacementTabRecovery !== "boolean") {
|
|
1390
|
+
throw new OperationStateError("invalid_target_evidence_profile", "Target evidence profile is invalid.");
|
|
1391
|
+
}
|
|
1392
|
+
if (profile.stableConversationId === "required" && target.conversationId === undefined) {
|
|
1393
|
+
throw new OperationStateError("target_conversation_missing", "Required stable conversation identity is absent.");
|
|
1394
|
+
}
|
|
1395
|
+
if (target.coordinationScope !== "process" && target.coordinationScope !== "provider") {
|
|
1396
|
+
throw new OperationStateError("invalid_coordination_scope", "Target coordinationScope must be process or provider.");
|
|
1397
|
+
}
|
|
1398
|
+
if (target.tabClaimEvidenceDigest !== undefined) {
|
|
1399
|
+
assertDigest(target.tabClaimEvidenceDigest, "target.tabClaimEvidenceDigest");
|
|
1400
|
+
}
|
|
1401
|
+
if (target.coordinationScope === "provider" &&
|
|
1402
|
+
(profile.authoritativeTabClaim !== "required" || target.tabClaimEvidenceDigest === undefined)) {
|
|
1403
|
+
throw new OperationStateError("target_claim_evidence_missing", "Provider-scoped coordination requires authoritative tab-claim evidence.");
|
|
1404
|
+
}
|
|
1405
|
+
if (target.canonicalThreadUrl !== undefined)
|
|
1406
|
+
assertCanonicalThreadUrl(target.canonicalThreadUrl);
|
|
1407
|
+
const lifecycle = target.targetLifecycle ?? "fixed";
|
|
1408
|
+
if (lifecycle !== "fixed" && lifecycle !== "new_pending" && lifecycle !== "new_established") {
|
|
1409
|
+
throw new OperationStateError("invalid_target_lifecycle", "Target lifecycle must be fixed, new_pending, or new_established.");
|
|
1410
|
+
}
|
|
1411
|
+
if (target.newTargetAnchorDigest !== undefined)
|
|
1412
|
+
assertDigest(target.newTargetAnchorDigest, "target.newTargetAnchorDigest");
|
|
1413
|
+
if (target.blankTaskEvidenceDigest !== undefined)
|
|
1414
|
+
assertDigest(target.blankTaskEvidenceDigest, "target.blankTaskEvidenceDigest");
|
|
1415
|
+
if (lifecycle === "fixed") {
|
|
1416
|
+
if (target.newTargetAnchorDigest !== undefined || target.blankTaskEvidenceDigest !== undefined || target.targetEstablishment !== undefined) {
|
|
1417
|
+
throw new OperationStateError("fixed_target_new_identity", "Fixed targets cannot contain new-conversation identity fields.");
|
|
1418
|
+
}
|
|
1419
|
+
return;
|
|
1420
|
+
}
|
|
1421
|
+
if (target.newTargetAnchorDigest === undefined || target.blankTaskEvidenceDigest === undefined) {
|
|
1422
|
+
throw new OperationStateError("new_target_anchor_missing", "New targets require an immutable anchor and blank-task evidence digest.");
|
|
1423
|
+
}
|
|
1424
|
+
if (target.canonicalThreadUrl !== undefined || target.conversationId !== undefined) {
|
|
1425
|
+
if (lifecycle === "new_pending") {
|
|
1426
|
+
throw new OperationStateError("new_target_identity_early", "A pending new target cannot contain provider conversation identity before establishment.");
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
if (lifecycle === "new_pending") {
|
|
1430
|
+
if (target.targetEstablishment !== undefined) {
|
|
1431
|
+
throw new OperationStateError("new_target_establishment_early", "A pending new target cannot contain an establishment record.");
|
|
1432
|
+
}
|
|
1433
|
+
if (target.evidenceProfile.stableConversationId !== "unavailable") {
|
|
1434
|
+
throw new OperationStateError("new_target_conversation_profile", "A pending new target must mark stable conversation identity unavailable.");
|
|
1435
|
+
}
|
|
1436
|
+
if (target.evidenceProfile.stableUserTurnId !== "unavailable") {
|
|
1437
|
+
throw new OperationStateError("new_target_user_profile", "A pending new target must mark stable user-turn identity unavailable.");
|
|
1438
|
+
}
|
|
1439
|
+
return;
|
|
1440
|
+
}
|
|
1441
|
+
const establishment = target.targetEstablishment;
|
|
1442
|
+
if (establishment === undefined) {
|
|
1443
|
+
throw new OperationStateError("new_target_establishment_missing", "An established new target requires its durable establishment record.");
|
|
1444
|
+
}
|
|
1445
|
+
validateTargetEstablishmentValues(establishment, target);
|
|
1446
|
+
if (target.evidenceProfile.stableConversationId !== "required" || target.evidenceProfile.stableUserTurnId !== "required") {
|
|
1447
|
+
throw new OperationStateError("new_target_established_profile", "An established new target must require stable conversation and user-turn identity.");
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
function validateTargetEstablishmentValues(establishment, target) {
|
|
1451
|
+
assertDigest(establishment.targetBindingDigest, "targetEstablishment.targetBindingDigest");
|
|
1452
|
+
assertDigest(establishment.anchorDigest, "targetEstablishment.anchorDigest");
|
|
1453
|
+
assertOperationId(establishment.causalSendActionId, "targetEstablishment.causalSendActionId");
|
|
1454
|
+
assertStableIdentifier(establishment.conversationId, "targetEstablishment.conversationId");
|
|
1455
|
+
assertCanonicalThreadUrl(establishment.canonicalThreadUrl);
|
|
1456
|
+
assertStableIdentifier(establishment.userTurnId, "targetEstablishment.userTurnId");
|
|
1457
|
+
assertDigest(establishment.userTurnEvidenceDigest, "targetEstablishment.userTurnEvidenceDigest");
|
|
1458
|
+
if (establishment.postSendDeltaDigest !== undefined) {
|
|
1459
|
+
assertDigest(establishment.postSendDeltaDigest, "targetEstablishment.postSendDeltaDigest");
|
|
1460
|
+
}
|
|
1461
|
+
assertDigest(establishment.evidenceDigest, "targetEstablishment.evidenceDigest");
|
|
1462
|
+
assertTimestamp(establishment.observedAt, "targetEstablishment.observedAt");
|
|
1463
|
+
if (establishment.anchorDigest !== target.newTargetAnchorDigest) {
|
|
1464
|
+
throw new OperationStateError("new_target_anchor_mismatch", "Target establishment does not match the immutable new-target anchor.");
|
|
1465
|
+
}
|
|
1466
|
+
if (target.conversationId !== establishment.conversationId || target.canonicalThreadUrl !== establishment.canonicalThreadUrl) {
|
|
1467
|
+
throw new OperationStateError("new_target_identity_mismatch", "Target establishment identity disagrees with the durable target binding.");
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
function assertSubmissionWitnessesMapShape(value) {
|
|
1471
|
+
if (!isPlainRecord(value)) {
|
|
1472
|
+
throw new OperationStateError("invalid_operation_state", "Operation submissionWitnesses must be an object.");
|
|
1473
|
+
}
|
|
1474
|
+
const keys = Object.keys(value);
|
|
1475
|
+
if (keys.length > MAX_SUBMISSION_WITNESSES) {
|
|
1476
|
+
throw new OperationStateError("submission_witness_limit", `An operation may record at most ${MAX_SUBMISSION_WITNESSES} submission witnesses.`);
|
|
1477
|
+
}
|
|
1478
|
+
for (const actionId of keys) {
|
|
1479
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, actionId);
|
|
1480
|
+
if (descriptor === undefined
|
|
1481
|
+
|| !Object.hasOwn(descriptor, "value")
|
|
1482
|
+
|| descriptor.get !== undefined
|
|
1483
|
+
|| descriptor.set !== undefined) {
|
|
1484
|
+
throw new OperationStateError("invalid_operation_shape", "Operation submissionWitnesses contains an unsafe property (accessor).");
|
|
1485
|
+
}
|
|
1486
|
+
assertOperationId(actionId, "submissionWitnesses map key");
|
|
1487
|
+
assertSubmissionWitnessShape(descriptor.value);
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
function validateSubmissionWitnessCollection(state) {
|
|
1491
|
+
if (state.submissionWitnesses !== undefined) {
|
|
1492
|
+
assertSubmissionWitnessesMapShape(state.submissionWitnesses);
|
|
1493
|
+
for (const [actionId, witness] of Object.entries(state.submissionWitnesses)) {
|
|
1494
|
+
if (witness.actionId !== actionId) {
|
|
1495
|
+
throw new OperationStateError("submission_witness_map_mismatch", "Submission witness map key must match its actionId.");
|
|
1496
|
+
}
|
|
1497
|
+
validateSubmissionWitnessValues(witness, state);
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
if (state.submissionWitness !== undefined) {
|
|
1501
|
+
assertSubmissionWitnessShape(state.submissionWitness);
|
|
1502
|
+
if (state.submissionWitness.actionKind !== "send") {
|
|
1503
|
+
throw new OperationStateError("submission_witness_projection_mismatch", "The legacy submissionWitness field must project the original Send witness.");
|
|
1504
|
+
}
|
|
1505
|
+
validateSubmissionWitnessValues(state.submissionWitness, state);
|
|
1506
|
+
if (state.submissionWitnesses !== undefined) {
|
|
1507
|
+
const projected = state.submissionWitnesses[state.submissionWitness.actionId];
|
|
1508
|
+
if (projected === undefined || canonicalJson(projected) !== canonicalJson(state.submissionWitness)) {
|
|
1509
|
+
throw new OperationStateError("submission_witness_projection_mismatch", "The original Send submission witness must match its keyed projection exactly.");
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
if (state.submissionWitnesses !== undefined) {
|
|
1514
|
+
const sendWitnesses = Object.values(state.submissionWitnesses).filter(witness => witness.actionKind === "send");
|
|
1515
|
+
if (sendWitnesses.length > 1) {
|
|
1516
|
+
throw new OperationStateError("submission_witness_duplicate_send", "An operation may record only one original Send submission witness.");
|
|
1517
|
+
}
|
|
1518
|
+
if (sendWitnesses.length === 1 && (state.submissionWitness === undefined
|
|
1519
|
+
|| canonicalJson(state.submissionWitness) !== canonicalJson(sendWitnesses[0]))) {
|
|
1520
|
+
throw new OperationStateError("submission_witness_projection_mismatch", "The original Send submission witness must be retained through the legacy projection.");
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
function assertSubmissionWitnessShape(value) {
|
|
1525
|
+
assertExactRecord(value, "operation submission witness", [
|
|
1526
|
+
"schemaVersion",
|
|
1527
|
+
"actionId",
|
|
1528
|
+
"actionKind",
|
|
1529
|
+
"targetBindingDigest",
|
|
1530
|
+
"baselineSnapshotDigest",
|
|
1531
|
+
"postSendDeltaDigest",
|
|
1532
|
+
"operationUserEvidenceDigest",
|
|
1533
|
+
"userTurnId",
|
|
1534
|
+
"observedAt"
|
|
1535
|
+
], [
|
|
1536
|
+
"schemaVersion",
|
|
1537
|
+
"actionId",
|
|
1538
|
+
"actionKind",
|
|
1539
|
+
"targetBindingDigest",
|
|
1540
|
+
"baselineSnapshotDigest",
|
|
1541
|
+
"postSendDeltaDigest",
|
|
1542
|
+
"operationUserEvidenceDigest",
|
|
1543
|
+
"observedAt"
|
|
1544
|
+
]);
|
|
1545
|
+
}
|
|
1546
|
+
function validateSubmissionWitnessValues(witness, state) {
|
|
1547
|
+
if (witness.schemaVersion !== OPERATION_SUBMISSION_WITNESS_SCHEMA_VERSION) {
|
|
1548
|
+
throw new OperationStateError("unsupported_submission_witness", "Submission witness schemaVersion is unsupported.");
|
|
1549
|
+
}
|
|
1550
|
+
assertOperationId(witness.actionId, "submissionWitness.actionId");
|
|
1551
|
+
if (witness.actionKind !== "send" && witness.actionKind !== "work_steer") {
|
|
1552
|
+
throw new OperationStateError("invalid_submission_witness", "Submission witness actionKind is unsupported.");
|
|
1553
|
+
}
|
|
1554
|
+
assertDigest(witness.targetBindingDigest, "submissionWitness.targetBindingDigest");
|
|
1555
|
+
assertDigest(witness.baselineSnapshotDigest, "submissionWitness.baselineSnapshotDigest");
|
|
1556
|
+
assertDigest(witness.postSendDeltaDigest, "submissionWitness.postSendDeltaDigest");
|
|
1557
|
+
assertDigest(witness.operationUserEvidenceDigest, "submissionWitness.operationUserEvidenceDigest");
|
|
1558
|
+
if (witness.userTurnId !== undefined)
|
|
1559
|
+
assertStableIdentifier(witness.userTurnId, "submissionWitness.userTurnId");
|
|
1560
|
+
assertTimestamp(witness.observedAt, "submissionWitness.observedAt");
|
|
1561
|
+
const action = state.actions[witness.actionId];
|
|
1562
|
+
if (action === undefined || action.kind !== witness.actionKind) {
|
|
1563
|
+
throw new OperationStateError("submission_witness_action_missing", "Submission witness must name its durable causal action.");
|
|
1564
|
+
}
|
|
1565
|
+
if (action.targetDigest !== witness.targetBindingDigest) {
|
|
1566
|
+
throw new OperationStateError("submission_witness_target_mismatch", "Submission witness target does not match its causal action.");
|
|
1567
|
+
}
|
|
1568
|
+
if (action.outcome === "not_satisfied" || action.outcome === "uncertain") {
|
|
1569
|
+
throw new OperationStateError("submission_witness_action_unproven", "Submission witness cannot follow an unsatisfied or uncertain action.");
|
|
1570
|
+
}
|
|
1571
|
+
if (witness.observedAt < action.intentAt) {
|
|
1572
|
+
throw new OperationStateError("submission_witness_before_action", "Submission witness cannot precede its causal action intent.");
|
|
1573
|
+
}
|
|
1574
|
+
if (state.target === undefined) {
|
|
1575
|
+
throw new OperationStateError("target_not_bound", "Submission witness requires a durable target binding.");
|
|
1576
|
+
}
|
|
1577
|
+
// New states must carry a per-action baseline entry. A legacy state that
|
|
1578
|
+
// predates the map may still use the original Send compatibility wrapper;
|
|
1579
|
+
// do not let that fallback hide a missing steer baseline in a mapped state.
|
|
1580
|
+
const witnessBaseline = state.ownershipBaselines === undefined
|
|
1581
|
+
? (state.ownershipBaseline?.actionId === witness.actionId ? state.ownershipBaseline : undefined)
|
|
1582
|
+
: state.ownershipBaselines[witness.actionId];
|
|
1583
|
+
if (witnessBaseline === undefined) {
|
|
1584
|
+
throw new OperationStateError("ownership_baseline_missing", "Submission witness requires the durable ownership baseline for its causal action.");
|
|
1585
|
+
}
|
|
1586
|
+
if (witnessBaseline.operationId !== state.operationId
|
|
1587
|
+
|| witnessBaseline.requestDigest !== state.requestDigest
|
|
1588
|
+
|| witnessBaseline.targetBindingDigest !== witness.targetBindingDigest
|
|
1589
|
+
|| witnessBaseline.actionId !== witness.actionId
|
|
1590
|
+
|| witnessBaseline.baseline.snapshotDigest !== witness.baselineSnapshotDigest) {
|
|
1591
|
+
throw new OperationStateError("ownership_baseline_mismatch", "Submission witness does not match the durable ownership baseline.");
|
|
1592
|
+
}
|
|
1593
|
+
if (state.target.targetLifecycle === "new_pending") {
|
|
1594
|
+
throw new OperationStateError("new_target_establishment_required", "A pending new target must be established before its submission witness is durable.");
|
|
1595
|
+
}
|
|
1596
|
+
const establishment = state.target.targetEstablishment;
|
|
1597
|
+
if (establishment !== undefined && establishment.causalSendActionId === witness.actionId) {
|
|
1598
|
+
if (establishment.targetBindingDigest !== witness.targetBindingDigest
|
|
1599
|
+
|| establishment.userTurnEvidenceDigest !== witness.operationUserEvidenceDigest
|
|
1600
|
+
|| establishment.postSendDeltaDigest === undefined
|
|
1601
|
+
|| establishment.postSendDeltaDigest !== witness.postSendDeltaDigest
|
|
1602
|
+
|| (witness.userTurnId !== undefined && establishment.userTurnId !== witness.userTurnId)) {
|
|
1603
|
+
throw new OperationStateError("submission_witness_establishment_mismatch", "Submission witness conflicts with durable target establishment.");
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
function requireUnreceiptedCause(cause, kind) {
|
|
1608
|
+
requireCause(cause, kind, undefined);
|
|
1609
|
+
if (cause.outcome !== undefined) {
|
|
1610
|
+
throw new OperationStateError("transition_cause_already_settled", `Transition requires an unreceipted ${kind} intent.`);
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
function withRevision(state, revision, updatedAt) {
|
|
1614
|
+
return { ...state, revision, updatedAt };
|
|
1615
|
+
}
|
|
1616
|
+
function requireCause(cause, kind, outcome) {
|
|
1617
|
+
if (cause?.kind !== kind || (outcome !== undefined && cause.outcome !== outcome)) {
|
|
1618
|
+
throw new OperationStateError("invalid_transition_cause", `Transition requires ${kind}${outcome === undefined ? "" : ` with ${outcome} receipt`}.`);
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
function requireSatisfiedCause(cause, evidenceDigest) {
|
|
1622
|
+
if (cause.outcome !== "satisfied") {
|
|
1623
|
+
throw new OperationStateError("transition_cause_unproven", `Action ${cause.actionId} has not been satisfied.`);
|
|
1624
|
+
}
|
|
1625
|
+
requireEvidence(evidenceDigest ?? cause.evidenceDigest);
|
|
1626
|
+
}
|
|
1627
|
+
function requireBoundary(actual, required) {
|
|
1628
|
+
if (BOUNDARY_RANK[actual] < BOUNDARY_RANK[required]) {
|
|
1629
|
+
throw new OperationStateError("mutation_boundary_missing", `Transition requires boundary ${required}.`);
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
function requireEvidence(value) {
|
|
1633
|
+
if (value === undefined || value.length === 0) {
|
|
1634
|
+
throw new OperationStateError("transition_evidence_missing", "Transition requires causal evidenceDigest.");
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
function assertDigest(value, label) {
|
|
1638
|
+
if (typeof value !== "string" || !DIGEST_PATTERN.test(value)) {
|
|
1639
|
+
throw new OperationStateError("invalid_digest", `${label} must be a canonical lowercase hmac-sha256 digest.`);
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
function assertTimestamp(value, label) {
|
|
1643
|
+
const parsed = typeof value === "string" ? Date.parse(value) : Number.NaN;
|
|
1644
|
+
if (typeof value !== "string"
|
|
1645
|
+
|| !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value)
|
|
1646
|
+
|| !Number.isFinite(parsed)
|
|
1647
|
+
|| new Date(parsed).toISOString() !== value) {
|
|
1648
|
+
throw new OperationStateError("invalid_timestamp", `${label} must be a canonical UTC ISO-8601 timestamp.`);
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
function assertTimestampNotBefore(earlier, later, label) {
|
|
1652
|
+
if (later < earlier) {
|
|
1653
|
+
throw new OperationStateError("timestamp_regression", `${label} cannot precede the prior durable event.`);
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
function assertStableIdentifier(value, label) {
|
|
1657
|
+
if (typeof value !== "string" ||
|
|
1658
|
+
value.trim().length === 0 ||
|
|
1659
|
+
value.length > 512 ||
|
|
1660
|
+
/[\u0000-\u001f\u007f]/u.test(value)) {
|
|
1661
|
+
throw new OperationStateError("invalid_target_binding", `${label} must be a bounded stable identifier.`);
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
function assertCanonicalThreadUrl(value) {
|
|
1665
|
+
if (typeof value !== "string" || value.length > 4096) {
|
|
1666
|
+
throw new OperationStateError("invalid_target_url", "canonicalThreadUrl must be a bounded absolute HTTP(S) URL.");
|
|
1667
|
+
}
|
|
1668
|
+
let parsed;
|
|
1669
|
+
try {
|
|
1670
|
+
parsed = new URL(value);
|
|
1671
|
+
}
|
|
1672
|
+
catch {
|
|
1673
|
+
throw new OperationStateError("invalid_target_url", "canonicalThreadUrl must be a bounded absolute HTTP(S) URL.");
|
|
1674
|
+
}
|
|
1675
|
+
if (parsed.protocol !== "https:" ||
|
|
1676
|
+
parsed.username !== "" ||
|
|
1677
|
+
parsed.password !== "" ||
|
|
1678
|
+
parsed.search !== "" ||
|
|
1679
|
+
parsed.hash !== "" ||
|
|
1680
|
+
parsed.toString() !== value) {
|
|
1681
|
+
throw new OperationStateError("invalid_target_url", "canonicalThreadUrl must be a canonical HTTPS URL without credentials, query, or fragment.");
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
function isSafeRelativeOutputKey(value) {
|
|
1685
|
+
return typeof value === "string"
|
|
1686
|
+
&& value.length > 0
|
|
1687
|
+
&& value.length <= 128
|
|
1688
|
+
&& /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(value);
|
|
1689
|
+
}
|
|
1690
|
+
function assertTargetShape(value) {
|
|
1691
|
+
assertExactRecord(value, "operation target", [
|
|
1692
|
+
"providerId",
|
|
1693
|
+
"browserId",
|
|
1694
|
+
"tabId",
|
|
1695
|
+
"coordinationScope",
|
|
1696
|
+
"tabClaimEvidenceDigest",
|
|
1697
|
+
"canonicalThreadUrl",
|
|
1698
|
+
"conversationId",
|
|
1699
|
+
"userTurnBaselineDigest",
|
|
1700
|
+
"assistantTurnBaselineDigest",
|
|
1701
|
+
"configurationReceiptDigest",
|
|
1702
|
+
"evidenceProfile",
|
|
1703
|
+
"targetLifecycle",
|
|
1704
|
+
"newTargetAnchorDigest",
|
|
1705
|
+
"blankTaskEvidenceDigest",
|
|
1706
|
+
"targetEstablishment"
|
|
1707
|
+
], ["providerId", "browserId", "tabId", "coordinationScope", "evidenceProfile"]);
|
|
1708
|
+
const target = value;
|
|
1709
|
+
assertExactRecord(target.evidenceProfile, "operation target evidence profile", ["providerIdentity", "stableTabId", "stableConversationId", "stableUserTurnId", "authoritativeTabClaim", "replacementTabRecovery"], ["providerIdentity", "stableTabId", "stableConversationId", "stableUserTurnId", "authoritativeTabClaim", "replacementTabRecovery"]);
|
|
1710
|
+
if (target.targetEstablishment !== undefined)
|
|
1711
|
+
assertTargetEstablishmentShape(target.targetEstablishment);
|
|
1712
|
+
}
|
|
1713
|
+
function assertTargetEstablishmentShape(value) {
|
|
1714
|
+
assertExactRecord(value, "operation target establishment", [
|
|
1715
|
+
"targetBindingDigest",
|
|
1716
|
+
"anchorDigest",
|
|
1717
|
+
"causalSendActionId",
|
|
1718
|
+
"conversationId",
|
|
1719
|
+
"canonicalThreadUrl",
|
|
1720
|
+
"userTurnId",
|
|
1721
|
+
"userTurnEvidenceDigest",
|
|
1722
|
+
"postSendDeltaDigest",
|
|
1723
|
+
"evidenceDigest",
|
|
1724
|
+
"observedAt"
|
|
1725
|
+
], [
|
|
1726
|
+
"targetBindingDigest",
|
|
1727
|
+
"anchorDigest",
|
|
1728
|
+
"causalSendActionId",
|
|
1729
|
+
"conversationId",
|
|
1730
|
+
"canonicalThreadUrl",
|
|
1731
|
+
"userTurnId",
|
|
1732
|
+
"userTurnEvidenceDigest",
|
|
1733
|
+
"evidenceDigest",
|
|
1734
|
+
"observedAt"
|
|
1735
|
+
]);
|
|
1736
|
+
}
|
|
1737
|
+
/** Closed-shape validation for the redacted pre-Send baseline. */
|
|
1738
|
+
export function assertOwnershipBaselineShape(value) {
|
|
1739
|
+
assertExactRecord(value, "operation ownership baseline", ["schemaVersion", "operationId", "requestDigest", "targetBindingDigest", "actionId", "baseline", "observedAt"], ["schemaVersion", "operationId", "requestDigest", "targetBindingDigest", "actionId", "baseline", "observedAt"]);
|
|
1740
|
+
if (value.schemaVersion !== OPERATION_OWNERSHIP_BASELINE_SCHEMA_VERSION) {
|
|
1741
|
+
throw new OperationStateError("unsupported_ownership_baseline", "Ownership baseline schemaVersion is unsupported.");
|
|
1742
|
+
}
|
|
1743
|
+
assertOperationId(value.operationId, "ownershipBaseline.operationId");
|
|
1744
|
+
assertDigest(value.requestDigest, "ownershipBaseline.requestDigest");
|
|
1745
|
+
assertDigest(value.targetBindingDigest, "ownershipBaseline.targetBindingDigest");
|
|
1746
|
+
assertOperationId(value.actionId, "ownershipBaseline.actionId");
|
|
1747
|
+
assertTimestamp(value.observedAt, "ownershipBaseline.observedAt");
|
|
1748
|
+
assertOwnershipBaselineSnapshotShape(value.baseline);
|
|
1749
|
+
}
|
|
1750
|
+
function assertOwnershipBaselineSnapshotShape(value) {
|
|
1751
|
+
assertExactRecord(value, "ownership baseline snapshot", ["schemaVersion", "snapshotDigest", "target", "userTurns", "assistantTurns", "completeness"], ["schemaVersion", "snapshotDigest", "target", "userTurns", "assistantTurns", "completeness"]);
|
|
1752
|
+
if (value.schemaVersion !== "chatgpt.browser_control.turn_ownership.v1" || value.completeness !== "complete") {
|
|
1753
|
+
throw new OperationStateError("invalid_ownership_baseline", "Ownership baseline must be a complete normalized snapshot.");
|
|
1754
|
+
}
|
|
1755
|
+
assertDigest(value.snapshotDigest, "ownershipBaseline.baseline.snapshotDigest");
|
|
1756
|
+
assertOwnershipTargetEvidenceShape(value.target, "ownershipBaseline.baseline.target");
|
|
1757
|
+
assertOwnershipTurnsShape(value.userTurns, "user", "ownershipBaseline.baseline.userTurns");
|
|
1758
|
+
assertOwnershipTurnsShape(value.assistantTurns, "assistant", "ownershipBaseline.baseline.assistantTurns");
|
|
1759
|
+
}
|
|
1760
|
+
function assertOwnershipTargetEvidenceShape(value, label) {
|
|
1761
|
+
assertExactRecord(value, label, ["provider", "browser", "tab", "thread", "conversation", "canonicalThreadUrl", "authoritativeTabClaim", "coordinationScope"], ["provider", "browser", "tab", "thread", "conversation", "canonicalThreadUrl", "authoritativeTabClaim", "coordinationScope"]);
|
|
1762
|
+
for (const key of ["provider", "browser", "tab", "thread", "conversation", "authoritativeTabClaim"]) {
|
|
1763
|
+
assertOwnershipIdentityEvidenceShape(value[key], `${label}.${key}`);
|
|
1764
|
+
}
|
|
1765
|
+
assertOwnershipIdentityEvidenceShape(value.canonicalThreadUrl, `${label}.canonicalThreadUrl`, true);
|
|
1766
|
+
if (value.coordinationScope !== "process" && value.coordinationScope !== "provider") {
|
|
1767
|
+
throw new OperationStateError("invalid_ownership_baseline", `${label}.coordinationScope is invalid.`);
|
|
1768
|
+
}
|
|
1769
|
+
if (value.coordinationScope === "provider" && value.authoritativeTabClaim.status !== "available") {
|
|
1770
|
+
throw new OperationStateError("invalid_ownership_baseline", `${label} requires provider claim evidence.`);
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
function assertOwnershipIdentityEvidenceShape(value, label, allowUrl = false) {
|
|
1774
|
+
if (!isPlainRecord(value) || (value.status !== "available" && value.status !== "unavailable")) {
|
|
1775
|
+
throw new OperationStateError("invalid_ownership_baseline", `${label} availability is invalid.`);
|
|
1776
|
+
}
|
|
1777
|
+
if (value.status === "available") {
|
|
1778
|
+
assertExactRecord(value, label, ["status", "value"], ["status", "value"]);
|
|
1779
|
+
if (typeof value.value !== "string" || value.value.length < 1 || value.value.length > (allowUrl ? 4096 : 512) || /[\u0000-\u001f\u007f]/u.test(value.value)) {
|
|
1780
|
+
throw new OperationStateError("invalid_ownership_baseline", `${label}.value is not bounded evidence.`);
|
|
1781
|
+
}
|
|
1782
|
+
if (allowUrl) {
|
|
1783
|
+
let parsed;
|
|
1784
|
+
try {
|
|
1785
|
+
parsed = new URL(value.value);
|
|
1786
|
+
}
|
|
1787
|
+
catch {
|
|
1788
|
+
throw new OperationStateError("invalid_ownership_baseline", `${label}.value is not a URL.`);
|
|
1789
|
+
}
|
|
1790
|
+
if (parsed.protocol !== "https:" || parsed.username !== "" || parsed.password !== "" || parsed.search !== "" || parsed.hash !== "") {
|
|
1791
|
+
throw new OperationStateError("invalid_ownership_baseline", `${label}.value is not a canonical HTTPS URL.`);
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
else {
|
|
1796
|
+
assertExactRecord(value, label, ["status", "reason"], ["status", "reason"]);
|
|
1797
|
+
if (value.reason !== "not_exposed" && value.reason !== "not_observed" && value.reason !== "redacted") {
|
|
1798
|
+
throw new OperationStateError("invalid_ownership_baseline", `${label}.reason is invalid.`);
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
function assertOwnershipTurnsShape(value, kind, label) {
|
|
1803
|
+
if (!Array.isArray(value) || value.length > MAX_BASELINE_TURNS) {
|
|
1804
|
+
throw new OperationStateError("invalid_ownership_baseline", `${label} exceeds the bounded turn limit.`);
|
|
1805
|
+
}
|
|
1806
|
+
const stableIds = new Set();
|
|
1807
|
+
const evidence = new Set();
|
|
1808
|
+
value.forEach((turnValue, index) => {
|
|
1809
|
+
assertExactRecord(turnValue, `${label}[${index}]`, ["stableId", "evidenceDigest", "structureDigest", "ordinal", "parentStableId", "branchStableId", "state", "artifactEvidenceDigests"], ["evidenceDigest", "structureDigest", "ordinal"]);
|
|
1810
|
+
if (turnValue.ordinal !== index)
|
|
1811
|
+
throw new OperationStateError("invalid_ownership_baseline", `${label} ordinals must be contiguous.`);
|
|
1812
|
+
assertDigest(turnValue.evidenceDigest, `${label}[${index}].evidenceDigest`);
|
|
1813
|
+
assertDigest(turnValue.structureDigest, `${label}[${index}].structureDigest`);
|
|
1814
|
+
if (turnValue.stableId !== undefined) {
|
|
1815
|
+
assertStableIdentifier(turnValue.stableId, `${label}[${index}].stableId`);
|
|
1816
|
+
if (stableIds.has(turnValue.stableId))
|
|
1817
|
+
throw new OperationStateError("invalid_ownership_baseline", `${label} contains duplicate stable IDs.`);
|
|
1818
|
+
stableIds.add(turnValue.stableId);
|
|
1819
|
+
}
|
|
1820
|
+
if (kind === "user" && (turnValue.state !== undefined || turnValue.parentStableId !== undefined || turnValue.branchStableId !== undefined)) {
|
|
1821
|
+
throw new OperationStateError("invalid_ownership_baseline", "User baseline turns cannot carry assistant lineage.");
|
|
1822
|
+
}
|
|
1823
|
+
if (kind === "assistant" && turnValue.state !== "generating" && turnValue.state !== "terminal") {
|
|
1824
|
+
throw new OperationStateError("invalid_ownership_baseline", "Assistant baseline turns require a bounded state.");
|
|
1825
|
+
}
|
|
1826
|
+
if (turnValue.parentStableId !== undefined)
|
|
1827
|
+
assertStableIdentifier(turnValue.parentStableId, `${label}[${index}].parentStableId`);
|
|
1828
|
+
if (turnValue.branchStableId !== undefined)
|
|
1829
|
+
assertStableIdentifier(turnValue.branchStableId, `${label}[${index}].branchStableId`);
|
|
1830
|
+
const artifacts = turnValue.artifactEvidenceDigests ?? [];
|
|
1831
|
+
if (!Array.isArray(artifacts) || artifacts.length > MAX_BASELINE_ARTIFACTS_PER_TURN) {
|
|
1832
|
+
throw new OperationStateError("invalid_ownership_baseline", `${label}[${index}] artifacts exceed the bounded limit.`);
|
|
1833
|
+
}
|
|
1834
|
+
const artifactSet = new Set();
|
|
1835
|
+
for (const artifact of artifacts) {
|
|
1836
|
+
assertDigest(artifact, `${label}[${index}].artifactEvidenceDigests[]`);
|
|
1837
|
+
if (artifactSet.has(artifact))
|
|
1838
|
+
throw new OperationStateError("invalid_ownership_baseline", `${label}[${index}] has duplicate artifact evidence.`);
|
|
1839
|
+
artifactSet.add(artifact);
|
|
1840
|
+
}
|
|
1841
|
+
if (turnValue.stableId === undefined) {
|
|
1842
|
+
const turnEvidenceDigest = turnValue.evidenceDigest;
|
|
1843
|
+
if (evidence.has(turnEvidenceDigest))
|
|
1844
|
+
throw new OperationStateError("invalid_ownership_baseline", `${label} has duplicate id-less evidence.`);
|
|
1845
|
+
evidence.add(turnEvidenceDigest);
|
|
1846
|
+
}
|
|
1847
|
+
});
|
|
1848
|
+
}
|
|
1849
|
+
function validateOwnershipBaselineValues(baseline, state) {
|
|
1850
|
+
if (baseline.operationId !== state.operationId
|
|
1851
|
+
|| baseline.requestDigest !== state.requestDigest)
|
|
1852
|
+
throw new OperationStateError("ownership_baseline_identity_mismatch", "Ownership baseline operation identity does not match state.");
|
|
1853
|
+
const action = state.actions[baseline.actionId];
|
|
1854
|
+
if (action === undefined || (action.kind !== "send" && action.kind !== "work_steer")) {
|
|
1855
|
+
throw new OperationStateError("ownership_baseline_action_missing", "Ownership baseline requires a durable Send or steer action intent.");
|
|
1856
|
+
}
|
|
1857
|
+
if (action.targetDigest !== baseline.targetBindingDigest) {
|
|
1858
|
+
throw new OperationStateError("ownership_baseline_target_mismatch", "Ownership baseline target does not match its causal action.");
|
|
1859
|
+
}
|
|
1860
|
+
const target = state.target;
|
|
1861
|
+
if (target === undefined)
|
|
1862
|
+
throw new OperationStateError("target_not_bound", "Ownership baseline requires a durable target binding.");
|
|
1863
|
+
const identity = (value) => value.status === "available" ? value.value : undefined;
|
|
1864
|
+
// A new-target baseline is intentionally captured before the provider
|
|
1865
|
+
// allocates conversation identity. Once establishment is durable, the
|
|
1866
|
+
// mutable target may contain a conversation ID/URL that the immutable
|
|
1867
|
+
// baseline could not have observed. Provider/browser/tab and coordination
|
|
1868
|
+
// identities remain stable and are still checked below.
|
|
1869
|
+
const fixedTarget = (target.targetLifecycle ?? "fixed") === "fixed";
|
|
1870
|
+
const baselineCanonicalThreadUrl = baseline.baseline.target.canonicalThreadUrl;
|
|
1871
|
+
// Work-steer preparation deliberately redacts the URL even when the
|
|
1872
|
+
// durable fixed target retains it. This is the one narrowly scoped
|
|
1873
|
+
// unavailable identity that may still prove ownership: all other URL
|
|
1874
|
+
// unavailability (and every other identity mismatch) remains a failure.
|
|
1875
|
+
const redactedWorkSteerUrl = action.kind === "work_steer"
|
|
1876
|
+
&& baselineCanonicalThreadUrl.status === "unavailable"
|
|
1877
|
+
&& baselineCanonicalThreadUrl.reason === "redacted";
|
|
1878
|
+
const canonicalThreadUrlMatches = !fixedTarget
|
|
1879
|
+
? true
|
|
1880
|
+
: redactedWorkSteerUrl
|
|
1881
|
+
? true
|
|
1882
|
+
: target.canonicalThreadUrl !== undefined
|
|
1883
|
+
? baselineCanonicalThreadUrl.status === "available"
|
|
1884
|
+
&& baselineCanonicalThreadUrl.value === target.canonicalThreadUrl
|
|
1885
|
+
// Preserve the historical optional URL behavior for Send, while a
|
|
1886
|
+
// fixed-target Work steer must still carry the mandated redaction.
|
|
1887
|
+
: action.kind !== "work_steer";
|
|
1888
|
+
if (identity(baseline.baseline.target.provider) !== target.providerId
|
|
1889
|
+
|| identity(baseline.baseline.target.browser) !== target.browserId
|
|
1890
|
+
|| identity(baseline.baseline.target.tab) !== target.tabId
|
|
1891
|
+
|| baseline.baseline.target.coordinationScope !== target.coordinationScope
|
|
1892
|
+
|| (fixedTarget && target.conversationId !== undefined && identity(baseline.baseline.target.conversation) !== target.conversationId)
|
|
1893
|
+
|| !canonicalThreadUrlMatches) {
|
|
1894
|
+
throw new OperationStateError("ownership_baseline_target_mismatch", "Ownership baseline target evidence disagrees with the durable target binding.");
|
|
1895
|
+
}
|
|
1896
|
+
const rejectedWorkSteer = action.kind === "work_steer" && action.outcome === "not_satisfied";
|
|
1897
|
+
if (action.outcome !== undefined && action.outcome !== "satisfied" && !rejectedWorkSteer) {
|
|
1898
|
+
throw new OperationStateError("ownership_baseline_action_unproven", "Ownership baseline cannot follow an uncertain or rejected action.");
|
|
1899
|
+
}
|
|
1900
|
+
if (baseline.observedAt < action.intentAt) {
|
|
1901
|
+
throw new OperationStateError("ownership_baseline_before_action", "Ownership baseline cannot precede its causal action intent.");
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
function assertActionIntentShape(value) {
|
|
1905
|
+
assertExactRecord(value, "operation action intent", ["actionId", "kind", "repeatPolicy", "requestDigest", "parentActionId", "targetDigest"], ["actionId", "kind", "repeatPolicy", "requestDigest"]);
|
|
1906
|
+
}
|
|
1907
|
+
function assertActionRecordShape(value) {
|
|
1908
|
+
assertExactRecord(value, "operation action record", [
|
|
1909
|
+
"actionId",
|
|
1910
|
+
"kind",
|
|
1911
|
+
"repeatPolicy",
|
|
1912
|
+
"requestDigest",
|
|
1913
|
+
"parentActionId",
|
|
1914
|
+
"targetDigest",
|
|
1915
|
+
"intentRevision",
|
|
1916
|
+
"intentAt",
|
|
1917
|
+
"outcome",
|
|
1918
|
+
"receiptRevision",
|
|
1919
|
+
"receiptAt",
|
|
1920
|
+
"evidenceDigest",
|
|
1921
|
+
"blockerCode"
|
|
1922
|
+
], ["actionId", "kind", "repeatPolicy", "requestDigest", "intentRevision", "intentAt"]);
|
|
1923
|
+
}
|
|
1924
|
+
function assertBlockerObservationShape(value) {
|
|
1925
|
+
assertExactRecord(value, "operation blocker observation", ["code", "messageDigest", "recoverable", "observedAt"], ["code", "messageDigest", "recoverable", "observedAt"]);
|
|
1926
|
+
}
|
|
1927
|
+
function assertReceiptShape(value) {
|
|
1928
|
+
assertExactRecord(value, "operation receipt", [
|
|
1929
|
+
"schemaVersion",
|
|
1930
|
+
"operationId",
|
|
1931
|
+
"requestDigest",
|
|
1932
|
+
"targetBindingDigest",
|
|
1933
|
+
"userTurnId",
|
|
1934
|
+
"userTurnEvidenceDigest",
|
|
1935
|
+
"assistantTurnId",
|
|
1936
|
+
"ownershipEvidenceDigest",
|
|
1937
|
+
"responseDigest",
|
|
1938
|
+
"responseBytes",
|
|
1939
|
+
"responseFormat",
|
|
1940
|
+
"finishReason",
|
|
1941
|
+
"contentAvailable",
|
|
1942
|
+
"artifacts",
|
|
1943
|
+
"completedAt"
|
|
1944
|
+
], [
|
|
1945
|
+
"schemaVersion",
|
|
1946
|
+
"operationId",
|
|
1947
|
+
"requestDigest",
|
|
1948
|
+
"targetBindingDigest",
|
|
1949
|
+
"userTurnId",
|
|
1950
|
+
"userTurnEvidenceDigest",
|
|
1951
|
+
"assistantTurnId",
|
|
1952
|
+
"ownershipEvidenceDigest",
|
|
1953
|
+
"finishReason",
|
|
1954
|
+
"contentAvailable",
|
|
1955
|
+
"artifacts",
|
|
1956
|
+
"completedAt"
|
|
1957
|
+
]);
|
|
1958
|
+
const receipt = value;
|
|
1959
|
+
if (receipt.responseFormat !== undefined && receipt.responseFormat !== "markdown" && receipt.responseFormat !== "text") {
|
|
1960
|
+
throw new OperationStateError("invalid_response_format", "Operation receipt responseFormat must be markdown or text.");
|
|
1961
|
+
}
|
|
1962
|
+
if (!Array.isArray(receipt.artifacts)) {
|
|
1963
|
+
throw new OperationStateError("invalid_operation_receipt", "Operation receipt artifacts must be an array.");
|
|
1964
|
+
}
|
|
1965
|
+
for (const artifact of receipt.artifacts)
|
|
1966
|
+
assertArtifactShape(artifact);
|
|
1967
|
+
}
|
|
1968
|
+
function assertArtifactShape(value) {
|
|
1969
|
+
assertExactRecord(value, "operation artifact receipt", [
|
|
1970
|
+
"schemaVersion",
|
|
1971
|
+
"operationId",
|
|
1972
|
+
"artifactKey",
|
|
1973
|
+
"assistantTurnId",
|
|
1974
|
+
"sourceIdentityDigest",
|
|
1975
|
+
"kind",
|
|
1976
|
+
"ordinal",
|
|
1977
|
+
"outputKey",
|
|
1978
|
+
"mimeType",
|
|
1979
|
+
"bytes",
|
|
1980
|
+
"sha256",
|
|
1981
|
+
"status",
|
|
1982
|
+
"blockerCode"
|
|
1983
|
+
], ["schemaVersion", "operationId", "artifactKey", "assistantTurnId", "sourceIdentityDigest", "kind", "ordinal", "status"]);
|
|
1984
|
+
}
|
|
1985
|
+
/** Validate the closed, path-free policy carried by creation/state records. */
|
|
1986
|
+
export function assertDurableCapturePolicyShape(value) {
|
|
1987
|
+
assertExactRecord(value, "operation durable capture policy", ["responseContent", "responseFormat", "artifacts"], ["responseContent", "responseFormat", "artifacts"]);
|
|
1988
|
+
const policy = value;
|
|
1989
|
+
// Do not invoke hostile accessors while validating a durable boundary.
|
|
1990
|
+
for (const key of ["responseContent", "responseFormat", "artifacts"]) {
|
|
1991
|
+
let descriptor;
|
|
1992
|
+
try {
|
|
1993
|
+
descriptor = Object.getOwnPropertyDescriptor(policy, key);
|
|
1994
|
+
}
|
|
1995
|
+
catch {
|
|
1996
|
+
throw new OperationStateError("invalid_operation_shape", "Operation durable capture policy could not be read safely.");
|
|
1997
|
+
}
|
|
1998
|
+
if (descriptor === undefined || !("value" in descriptor) || descriptor.get !== undefined || descriptor.set !== undefined) {
|
|
1999
|
+
throw new OperationStateError("invalid_operation_shape", "Operation durable capture policy contains an unsafe property.");
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
if (policy.responseContent !== "include" && policy.responseContent !== "metadata") {
|
|
2003
|
+
throw new OperationStateError("invalid_capture_policy", "Operation durable capture responseContent is invalid.");
|
|
2004
|
+
}
|
|
2005
|
+
if (policy.responseFormat !== "markdown" && policy.responseFormat !== "text") {
|
|
2006
|
+
throw new OperationStateError("invalid_capture_policy", "Operation durable capture responseFormat is invalid.");
|
|
2007
|
+
}
|
|
2008
|
+
if (policy.artifacts !== "receipt_only" && policy.artifacts !== "transfer") {
|
|
2009
|
+
throw new OperationStateError("invalid_capture_policy", "Operation durable capture artifacts policy is invalid.");
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
function assertExactRecord(value, label, allowed, required, allowUnknownUntilTypeDispatch = false) {
|
|
2013
|
+
if (!isPlainRecord(value)) {
|
|
2014
|
+
throw new OperationStateError("invalid_operation_shape", `${label} must be a plain object.`);
|
|
2015
|
+
}
|
|
2016
|
+
const allowedKeys = new Set(allowed);
|
|
2017
|
+
for (const key of Object.keys(value)) {
|
|
2018
|
+
let descriptor;
|
|
2019
|
+
try {
|
|
2020
|
+
descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
2021
|
+
}
|
|
2022
|
+
catch {
|
|
2023
|
+
throw new OperationStateError("invalid_operation_shape", `${label} contains an unreadable property.`);
|
|
2024
|
+
}
|
|
2025
|
+
if (descriptor === undefined || !Object.hasOwn(descriptor, "value") || descriptor.get !== undefined || descriptor.set !== undefined) {
|
|
2026
|
+
throw new OperationStateError("invalid_operation_shape", `${label} contains an unsafe property (accessor).`);
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
if (!allowUnknownUntilTypeDispatch) {
|
|
2030
|
+
for (const key of Object.keys(value)) {
|
|
2031
|
+
if (!allowedKeys.has(key)) {
|
|
2032
|
+
throw new OperationStateError("unknown_operation_field", `${label} contains unsupported field ${key}.`);
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
for (const key of required) {
|
|
2037
|
+
if (!Object.hasOwn(value, key)) {
|
|
2038
|
+
throw new OperationStateError("missing_operation_field", `${label} is missing required field ${key}.`);
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
function isPlainRecord(value) {
|
|
2043
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
2044
|
+
return false;
|
|
2045
|
+
const prototype = Object.getPrototypeOf(value);
|
|
2046
|
+
return prototype === Object.prototype || prototype === null;
|
|
2047
|
+
}
|