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,1312 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { commitOperationOutput, deriveOperationOutputKey } from "./artifact-output.js";
|
|
3
|
+
import { copyProviderChunk } from "./artifact-stream.js";
|
|
4
|
+
/**
|
|
5
|
+
* The transfer coordinator is deliberately provider agnostic. A provider
|
|
6
|
+
* supplies one already-authorized, exact artifact stream; this module owns
|
|
7
|
+
* the local effect, its durable intent, and its collision/recovery rules.
|
|
8
|
+
*
|
|
9
|
+
* The absolute destination is kept in the active call only. It is included
|
|
10
|
+
* in the keyed destination evidence supplied by the caller, but is never
|
|
11
|
+
* copied into an intent, receipt, lookup, or error.
|
|
12
|
+
*/
|
|
13
|
+
export const ARTIFACT_TRANSFER_SCHEMA_VERSION = "chatgpt.browser_control.artifact_transfer.v1";
|
|
14
|
+
export const ARTIFACT_TRANSFER_INTENT_SCHEMA_VERSION = "chatgpt.browser_control.artifact_transfer_intent.v1";
|
|
15
|
+
export const ARTIFACT_TRANSFER_RECEIPT_SCHEMA_VERSION = "chatgpt.browser_control.artifact_transfer_receipt.v1";
|
|
16
|
+
const SCHEMA_VERSION = ARTIFACT_TRANSFER_SCHEMA_VERSION;
|
|
17
|
+
const INTENT_SCHEMA_VERSION = ARTIFACT_TRANSFER_INTENT_SCHEMA_VERSION;
|
|
18
|
+
const RECEIPT_SCHEMA_VERSION = ARTIFACT_TRANSFER_RECEIPT_SCHEMA_VERSION;
|
|
19
|
+
const DIGEST_PATTERN = /^hmac-sha256:[0-9a-f]{64}$/;
|
|
20
|
+
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}$/;
|
|
21
|
+
const SHA256_PATTERN = /^[0-9a-f]{64}$/;
|
|
22
|
+
const OUTPUT_KEY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
23
|
+
const CODE_PATTERN = /^[a-z][a-z0-9_]{0,63}$/;
|
|
24
|
+
const INSTANT_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
25
|
+
const DEFAULT_MAX_BYTES = 128 * 1024 * 1024;
|
|
26
|
+
const MAX_MAX_BYTES = 512 * 1024 * 1024;
|
|
27
|
+
const DEFAULT_MAX_STRING_BYTES = 4096;
|
|
28
|
+
const MAX_MAX_STRING_BYTES = 16 * 1024;
|
|
29
|
+
const DEFAULT_MAX_COUNT = 256;
|
|
30
|
+
const MAX_MAX_COUNT = 1024;
|
|
31
|
+
const DEFAULT_MAX_DEPTH = 8;
|
|
32
|
+
const MAX_MAX_DEPTH = 32;
|
|
33
|
+
const MAX_GRAPH_NODES = 1024;
|
|
34
|
+
const MAX_DEADLINE_AT = Date.UTC(2100, 0, 1);
|
|
35
|
+
const MAX_TIMER_DELAY_MS = 2_147_483_647;
|
|
36
|
+
const PROVIDER_OPEN_TIMEOUT_MS = 30_000;
|
|
37
|
+
const PROVIDER_CLEANUP_TIMEOUT_MS = 30_000;
|
|
38
|
+
export class ArtifactTransferError extends Error {
|
|
39
|
+
code;
|
|
40
|
+
constructor(code, message) {
|
|
41
|
+
super(message);
|
|
42
|
+
this.code = code;
|
|
43
|
+
this.name = "ArtifactTransferError";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const inFlight = new Map();
|
|
47
|
+
class ProviderBoundaryError extends Error {
|
|
48
|
+
boundary;
|
|
49
|
+
constructor(boundary) {
|
|
50
|
+
super(boundary === "timeout" ? "The artifact provider exceeded its operation deadline." : "The artifact provider was aborted.");
|
|
51
|
+
this.boundary = boundary;
|
|
52
|
+
this.name = "ProviderBoundaryError";
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function armProviderBoundary(prepared, fallbackTimeoutMs) {
|
|
56
|
+
const signal = prepared.signal;
|
|
57
|
+
let remaining;
|
|
58
|
+
if (prepared.deadlineAt !== undefined) {
|
|
59
|
+
const baseline = prepared.now();
|
|
60
|
+
if (prepared.clockFaulted()) {
|
|
61
|
+
throw new ArtifactTransferError("operation_clock_invalid", "Operation clock became invalid.");
|
|
62
|
+
}
|
|
63
|
+
remaining = prepared.deadlineAt - baseline;
|
|
64
|
+
}
|
|
65
|
+
else if (fallbackTimeoutMs !== undefined) {
|
|
66
|
+
remaining = fallbackTimeoutMs;
|
|
67
|
+
}
|
|
68
|
+
if (signal === undefined && remaining === undefined)
|
|
69
|
+
return undefined;
|
|
70
|
+
let resolveBoundary;
|
|
71
|
+
let timer;
|
|
72
|
+
let cancelled = false;
|
|
73
|
+
const promise = new Promise(resolve => {
|
|
74
|
+
resolveBoundary = resolve;
|
|
75
|
+
});
|
|
76
|
+
const listener = () => trigger("aborted");
|
|
77
|
+
const removeListener = () => {
|
|
78
|
+
if (signal === undefined)
|
|
79
|
+
return;
|
|
80
|
+
try {
|
|
81
|
+
signal.removeEventListener("abort", listener);
|
|
82
|
+
}
|
|
83
|
+
catch { /* fail closed through the boundary */ }
|
|
84
|
+
};
|
|
85
|
+
const clearTimer = () => {
|
|
86
|
+
if (timer !== undefined)
|
|
87
|
+
clearTimeout(timer);
|
|
88
|
+
timer = undefined;
|
|
89
|
+
};
|
|
90
|
+
function trigger(boundary) {
|
|
91
|
+
if (cancelled)
|
|
92
|
+
return;
|
|
93
|
+
cancelled = true;
|
|
94
|
+
clearTimer();
|
|
95
|
+
removeListener();
|
|
96
|
+
resolveBoundary(boundary);
|
|
97
|
+
}
|
|
98
|
+
const schedule = (delay) => {
|
|
99
|
+
if (cancelled)
|
|
100
|
+
return;
|
|
101
|
+
timer = setTimeout(() => {
|
|
102
|
+
if (delay > MAX_TIMER_DELAY_MS)
|
|
103
|
+
schedule(delay - MAX_TIMER_DELAY_MS);
|
|
104
|
+
else
|
|
105
|
+
trigger("timeout");
|
|
106
|
+
}, Math.min(delay, MAX_TIMER_DELAY_MS));
|
|
107
|
+
};
|
|
108
|
+
if (signal !== undefined) {
|
|
109
|
+
if (isSignalAborted(signal)) {
|
|
110
|
+
trigger("aborted");
|
|
111
|
+
return { promise, cancel: () => undefined };
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
signal.addEventListener("abort", listener, { once: true });
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
trigger("aborted");
|
|
118
|
+
return { promise, cancel: () => undefined };
|
|
119
|
+
}
|
|
120
|
+
if (isSignalAborted(signal))
|
|
121
|
+
trigger("aborted");
|
|
122
|
+
}
|
|
123
|
+
if (!cancelled && remaining !== undefined) {
|
|
124
|
+
if (!Number.isFinite(remaining) || remaining <= 0)
|
|
125
|
+
trigger("timeout");
|
|
126
|
+
else
|
|
127
|
+
schedule(remaining);
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
promise,
|
|
131
|
+
cancel: () => {
|
|
132
|
+
if (cancelled)
|
|
133
|
+
return;
|
|
134
|
+
cancelled = true;
|
|
135
|
+
clearTimer();
|
|
136
|
+
removeListener();
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function observeLateProviderValue(value, onLateValue) {
|
|
141
|
+
if (onLateValue === undefined)
|
|
142
|
+
return;
|
|
143
|
+
try {
|
|
144
|
+
void Promise.resolve().then(() => onLateValue(value)).catch(() => undefined);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
// A late provider capability is already outside the operation authority;
|
|
148
|
+
// cleanup failures remain redacted and cannot alter the durable outcome.
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
async function awaitProviderOperation(prepared, operation, fallbackTimeoutMs, onLateValue) {
|
|
152
|
+
const providerPromise = Promise.resolve().then(operation);
|
|
153
|
+
let valueSettled = false;
|
|
154
|
+
let valueForLate;
|
|
155
|
+
let boundaryTriggered = false;
|
|
156
|
+
let lateObserved = false;
|
|
157
|
+
const settled = providerPromise.then(value => {
|
|
158
|
+
valueSettled = true;
|
|
159
|
+
valueForLate = value;
|
|
160
|
+
if (boundaryTriggered && !lateObserved) {
|
|
161
|
+
lateObserved = true;
|
|
162
|
+
observeLateProviderValue(value, onLateValue);
|
|
163
|
+
}
|
|
164
|
+
return { kind: "value", value };
|
|
165
|
+
}, error => ({ kind: "error", error }));
|
|
166
|
+
let boundary;
|
|
167
|
+
try {
|
|
168
|
+
boundary = armProviderBoundary(prepared, fallbackTimeoutMs);
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
if (onLateValue !== undefined) {
|
|
172
|
+
void providerPromise.then(value => observeLateProviderValue(value, onLateValue), () => undefined);
|
|
173
|
+
}
|
|
174
|
+
throw error;
|
|
175
|
+
}
|
|
176
|
+
let outcome;
|
|
177
|
+
try {
|
|
178
|
+
outcome = boundary === undefined
|
|
179
|
+
? await settled
|
|
180
|
+
: await Promise.race([settled, boundary.promise]);
|
|
181
|
+
}
|
|
182
|
+
finally {
|
|
183
|
+
boundary?.cancel();
|
|
184
|
+
}
|
|
185
|
+
if (outcome === "timeout" || outcome === "aborted") {
|
|
186
|
+
boundaryTriggered = true;
|
|
187
|
+
if (valueSettled && !lateObserved) {
|
|
188
|
+
lateObserved = true;
|
|
189
|
+
observeLateProviderValue(valueForLate, onLateValue);
|
|
190
|
+
}
|
|
191
|
+
else if (!valueSettled && onLateValue !== undefined) {
|
|
192
|
+
void providerPromise.then(value => {
|
|
193
|
+
if (lateObserved)
|
|
194
|
+
return;
|
|
195
|
+
lateObserved = true;
|
|
196
|
+
observeLateProviderValue(value, onLateValue);
|
|
197
|
+
}, () => undefined);
|
|
198
|
+
}
|
|
199
|
+
throw new ProviderBoundaryError(outcome);
|
|
200
|
+
}
|
|
201
|
+
if (outcome.kind === "error")
|
|
202
|
+
throw outcome.error;
|
|
203
|
+
if (prepared.clockFaulted())
|
|
204
|
+
throw new ArtifactTransferError("operation_clock_invalid", "Operation clock became invalid.");
|
|
205
|
+
return outcome.value;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Transfer one exact operation-owned artifact. This function is safe to
|
|
209
|
+
* invoke repeatedly with the same action identity: completed receipts are
|
|
210
|
+
* replayed without provider access, and concurrent identical calls share one
|
|
211
|
+
* source/commit attempt.
|
|
212
|
+
*/
|
|
213
|
+
export async function transferOperationArtifact(options) {
|
|
214
|
+
const prepared = await prepare(options);
|
|
215
|
+
const initialProgress = freshProgress();
|
|
216
|
+
if (isCancelledOrExpired(prepared)) {
|
|
217
|
+
if (prepared.clockFaulted())
|
|
218
|
+
return uncertainResult(initialProgress, "operation_state_corrupt");
|
|
219
|
+
return preIntentAbortResult(prepared, initialProgress);
|
|
220
|
+
}
|
|
221
|
+
const flightKey = `${prepared.operationId}\0${prepared.transferActionId}`;
|
|
222
|
+
const identity = transferIdentity(prepared);
|
|
223
|
+
const existing = inFlight.get(flightKey);
|
|
224
|
+
if (existing !== undefined) {
|
|
225
|
+
if (existing.identity !== identity)
|
|
226
|
+
return conflictResult(initialProgress);
|
|
227
|
+
return existing.promise;
|
|
228
|
+
}
|
|
229
|
+
const promise = runTransfer(prepared);
|
|
230
|
+
inFlight.set(flightKey, { identity, promise });
|
|
231
|
+
try {
|
|
232
|
+
return await promise;
|
|
233
|
+
}
|
|
234
|
+
finally {
|
|
235
|
+
const current = inFlight.get(flightKey);
|
|
236
|
+
if (current?.promise === promise)
|
|
237
|
+
inFlight.delete(flightKey);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
/** Short additive alias for callers that prefer the noun-first operation name. */
|
|
241
|
+
export const transferArtifact = transferOperationArtifact;
|
|
242
|
+
async function runTransfer(prepared) {
|
|
243
|
+
const progress = {
|
|
244
|
+
intentPersistence: "not_attempted",
|
|
245
|
+
receiptPersistence: "not_attempted"
|
|
246
|
+
};
|
|
247
|
+
try {
|
|
248
|
+
let durable;
|
|
249
|
+
try {
|
|
250
|
+
durable = validateDurableState(await prepared.readActionState(prepared.lookup), prepared);
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
if (error instanceof ArtifactTransferError && error.code === "operation_request_mismatch") {
|
|
254
|
+
return failureResult(error.code, progress);
|
|
255
|
+
}
|
|
256
|
+
// A receipt without its causal intent, malformed records, and unknown
|
|
257
|
+
// journal fields are corruption, not an invitation to retry the source.
|
|
258
|
+
return failureResult("operation_state_corrupt", progress);
|
|
259
|
+
}
|
|
260
|
+
if (durable !== undefined) {
|
|
261
|
+
progress.intentPersistence = "durable";
|
|
262
|
+
if (durable.receipt !== undefined) {
|
|
263
|
+
progress.receiptPersistence = "durable";
|
|
264
|
+
return await reconcileDurableReceipt(prepared, durable.receipt, progress);
|
|
265
|
+
}
|
|
266
|
+
// An intent without an exact receipt is evidence that the source handoff
|
|
267
|
+
// may already have occurred. There is no safe source retry. The local
|
|
268
|
+
// output primitive can only prove a leftover when an exact receipt gives
|
|
269
|
+
// it bytes+hash. Close the durable action with an explicit partial
|
|
270
|
+
// receipt so the parent operation can complete truthfully without ever
|
|
271
|
+
// reopening the provider source.
|
|
272
|
+
if (durable.intent !== undefined) {
|
|
273
|
+
progress.intentAt = durable.intent.intentAt;
|
|
274
|
+
return await persistUncertainReceipt(prepared, "artifact_transfer_partial", progress);
|
|
275
|
+
}
|
|
276
|
+
return failureResult("operation_state_corrupt", progress);
|
|
277
|
+
}
|
|
278
|
+
const expiredBeforeIntent = isCancelledOrExpired(prepared);
|
|
279
|
+
if (prepared.clockFaulted())
|
|
280
|
+
return uncertainResult(progress, "operation_state_corrupt");
|
|
281
|
+
if (expiredBeforeIntent) {
|
|
282
|
+
return preIntentAbortResult(prepared, progress);
|
|
283
|
+
}
|
|
284
|
+
const intent = makeIntent(prepared);
|
|
285
|
+
progress.intentAt = intent.intentAt;
|
|
286
|
+
const intentResolution = await persistIntentAndConverge(prepared, intent);
|
|
287
|
+
progress.intentPersistence = intentResolution.persistence;
|
|
288
|
+
if (intentResolution.blockerCode === "operation_request_mismatch") {
|
|
289
|
+
return failureResult(intentResolution.blockerCode, progress);
|
|
290
|
+
}
|
|
291
|
+
if (intentResolution.persistence === "indeterminate") {
|
|
292
|
+
return uncertainResult(progress, intentResolution.blockerCode ?? "operation_state_corrupt");
|
|
293
|
+
}
|
|
294
|
+
if (intentResolution.durableState?.receipt !== undefined) {
|
|
295
|
+
progress.receiptPersistence = "durable";
|
|
296
|
+
return await reconcileDurableReceipt(prepared, intentResolution.durableState.receipt, progress);
|
|
297
|
+
}
|
|
298
|
+
if (intentResolution.durableState?.intent !== undefined) {
|
|
299
|
+
progress.intentAt = intentResolution.durableState.intent.intentAt;
|
|
300
|
+
}
|
|
301
|
+
if (prepared.clockFaulted())
|
|
302
|
+
return uncertainResult(progress, "operation_state_corrupt");
|
|
303
|
+
const expiredAfterIntent = isCancelledOrExpired(prepared);
|
|
304
|
+
if (prepared.clockFaulted())
|
|
305
|
+
return uncertainResult(progress, "operation_state_corrupt");
|
|
306
|
+
if (expiredAfterIntent) {
|
|
307
|
+
return await persistUncertainReceipt(prepared, cancellationCode(prepared), progress);
|
|
308
|
+
}
|
|
309
|
+
let source;
|
|
310
|
+
try {
|
|
311
|
+
const raw = await awaitProviderOperation(prepared, () => prepared.openSource(sourceRequest(prepared)), PROVIDER_OPEN_TIMEOUT_MS, value => closeArtifactSourceBestEffort(value));
|
|
312
|
+
source = safeSource(raw, prepared.maxBytes);
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
return await persistUncertainReceipt(prepared, "artifact_transfer_partial", progress);
|
|
316
|
+
}
|
|
317
|
+
let output;
|
|
318
|
+
let outputFailed = false;
|
|
319
|
+
try {
|
|
320
|
+
output = await commitOperationOutput(commitOptions(prepared, source));
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
outputFailed = true;
|
|
324
|
+
}
|
|
325
|
+
// `artifact-output` may reject the destination during preflight without
|
|
326
|
+
// ever requesting a source chunk. Explicitly close the one-shot provider
|
|
327
|
+
// capability so its file handle and request temp are released on every
|
|
328
|
+
// branch, including that zero-read path.
|
|
329
|
+
const sourceClosed = await closeArtifactSource(source, prepared);
|
|
330
|
+
if (outputFailed || output === undefined) {
|
|
331
|
+
return await persistUncertainReceipt(prepared, "output_commit_indeterminate", progress);
|
|
332
|
+
}
|
|
333
|
+
if (!sourceClosed) {
|
|
334
|
+
return await persistUncertainReceipt(prepared, "artifact_transfer_partial", progress);
|
|
335
|
+
}
|
|
336
|
+
const mapped = mapOutput(prepared, output, progress);
|
|
337
|
+
return await persistResultReceipt(prepared, mapped.receipt, mapped.outcome, progress);
|
|
338
|
+
}
|
|
339
|
+
catch (error) {
|
|
340
|
+
// In particular, a testable/provider clock can fail after intent or after
|
|
341
|
+
// a local output commit. Do not throw, retry, or claim a write was lost;
|
|
342
|
+
// the only truthful result at this boundary is uncertain.
|
|
343
|
+
if (error instanceof ArtifactTransferError && error.code === "operation_request_mismatch") {
|
|
344
|
+
return failureResult(error.code, progress);
|
|
345
|
+
}
|
|
346
|
+
return uncertainResult(progress, "operation_state_corrupt");
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
function freshProgress() {
|
|
350
|
+
return {
|
|
351
|
+
intentPersistence: "not_attempted",
|
|
352
|
+
receiptPersistence: "not_attempted"
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
async function persistIntentAndConverge(prepared, intent) {
|
|
356
|
+
try {
|
|
357
|
+
await prepared.persistIntent(intent);
|
|
358
|
+
return { persistence: "durable" };
|
|
359
|
+
}
|
|
360
|
+
catch {
|
|
361
|
+
// A journal adapter may have committed the record before reporting an
|
|
362
|
+
// error. Re-read and require the exact intent before calling it durable.
|
|
363
|
+
let afterFailure;
|
|
364
|
+
try {
|
|
365
|
+
afterFailure = validateDurableState(await prepared.readActionState(prepared.lookup), prepared);
|
|
366
|
+
}
|
|
367
|
+
catch (error) {
|
|
368
|
+
return {
|
|
369
|
+
persistence: "indeterminate",
|
|
370
|
+
blockerCode: error instanceof ArtifactTransferError && error.code === "operation_request_mismatch"
|
|
371
|
+
? "operation_request_mismatch"
|
|
372
|
+
: "operation_state_corrupt"
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
if (afterFailure?.intent !== undefined && sameIntent(afterFailure.intent, intent)) {
|
|
376
|
+
return { persistence: "durable", durableState: afterFailure };
|
|
377
|
+
}
|
|
378
|
+
// Undefined state is not proof that the failed write did not commit.
|
|
379
|
+
// Treat every non-converged write as an uncertain boundary.
|
|
380
|
+
return { persistence: "indeterminate", blockerCode: "operation_state_corrupt" };
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
async function reconcileDurableReceipt(prepared, receipt, progress) {
|
|
384
|
+
if (receipt.status !== "transferred" || receipt.bytes === undefined || receipt.sha256 === undefined || receipt.outputKey === undefined) {
|
|
385
|
+
return resultFromReceipt(receipt, "uncertain", true, progress);
|
|
386
|
+
}
|
|
387
|
+
// The source callback is intentionally not called. Passing a guard stream
|
|
388
|
+
// lets artifact-output inspect/reconcile an exact final or verified temp;
|
|
389
|
+
// if neither exists, the guard prevents a blind source retry.
|
|
390
|
+
let output;
|
|
391
|
+
try {
|
|
392
|
+
output = await commitOperationOutput(commitOptions(prepared, noSourceRetry(), {
|
|
393
|
+
bytes: receipt.bytes,
|
|
394
|
+
sha256: receipt.sha256
|
|
395
|
+
}));
|
|
396
|
+
}
|
|
397
|
+
catch {
|
|
398
|
+
return resultFromReceipt(receipt, "uncertain", true, progress);
|
|
399
|
+
}
|
|
400
|
+
if (output.status === "reconciled" || output.status === "committed") {
|
|
401
|
+
// Replay must be byte-for-byte receipt stable. The local primitive only
|
|
402
|
+
// proves the persisted receipt's expected output; it must not rewrite its
|
|
403
|
+
// observedAt or any other durable field while replaying.
|
|
404
|
+
return resultFromReceipt(receipt, "satisfied", true, progress);
|
|
405
|
+
}
|
|
406
|
+
if (output.status === "collision")
|
|
407
|
+
return resultFromReceipt(receipt, "not_satisfied", true, progress, "output_collision");
|
|
408
|
+
return resultFromReceipt(receipt, "uncertain", true, progress, "output_commit_indeterminate");
|
|
409
|
+
}
|
|
410
|
+
async function persistUncertainReceipt(prepared, blockerCode, progress) {
|
|
411
|
+
let observedAt;
|
|
412
|
+
try {
|
|
413
|
+
observedAt = preparedTimestamp(prepared, progress.intentAt);
|
|
414
|
+
}
|
|
415
|
+
catch {
|
|
416
|
+
return uncertainResult(progress, "operation_state_corrupt");
|
|
417
|
+
}
|
|
418
|
+
const receipt = {
|
|
419
|
+
schemaVersion: RECEIPT_SCHEMA_VERSION,
|
|
420
|
+
operationId: prepared.operationId,
|
|
421
|
+
requestDigest: prepared.requestDigest,
|
|
422
|
+
targetBindingDigest: prepared.targetBindingDigest,
|
|
423
|
+
assistantTurnId: prepared.assistantTurnId,
|
|
424
|
+
sourceIdentityDigest: prepared.sourceIdentityDigest,
|
|
425
|
+
kind: prepared.kind,
|
|
426
|
+
ordinal: prepared.ordinal,
|
|
427
|
+
transferActionId: prepared.transferActionId,
|
|
428
|
+
destinationIdentityDigest: prepared.destinationIdentityDigest,
|
|
429
|
+
outputKey: prepared.outputKey,
|
|
430
|
+
status: "partial",
|
|
431
|
+
blockerCode,
|
|
432
|
+
observedAt
|
|
433
|
+
};
|
|
434
|
+
return await persistResultReceipt(prepared, receipt, "uncertain", progress);
|
|
435
|
+
}
|
|
436
|
+
function uncertainResult(progress, blockerCode, receipt) {
|
|
437
|
+
return freezeRecord({
|
|
438
|
+
schemaVersion: SCHEMA_VERSION,
|
|
439
|
+
outcome: "uncertain",
|
|
440
|
+
replayed: false,
|
|
441
|
+
intentPersistence: progress.intentPersistence,
|
|
442
|
+
receiptPersistence: progress.receiptPersistence,
|
|
443
|
+
...(receipt === undefined ? {} : { receipt: freezeRecord({ ...receipt }) }),
|
|
444
|
+
blockerCode
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
async function persistResultReceipt(prepared, receipt, outcome, progress) {
|
|
448
|
+
// Durable records are scalar, path-free snapshots. Freeze the exact
|
|
449
|
+
// record before handing it to an adapter or returning it so neither side
|
|
450
|
+
// can mutate the authority after validation.
|
|
451
|
+
receipt = freezeRecord({ ...receipt });
|
|
452
|
+
try {
|
|
453
|
+
await prepared.persistReceipt(receipt);
|
|
454
|
+
progress.receiptPersistence = "durable";
|
|
455
|
+
return resultFromReceipt(receipt, outcome, false, progress);
|
|
456
|
+
}
|
|
457
|
+
catch {
|
|
458
|
+
// As with intent, a receipt adapter can commit and then throw. Only an
|
|
459
|
+
// exact read-back makes the persistence durable; otherwise expose the
|
|
460
|
+
// receipt candidate but keep the write indeterminate and never retry the
|
|
461
|
+
// provider/source.
|
|
462
|
+
let afterFailure;
|
|
463
|
+
try {
|
|
464
|
+
afterFailure = validateDurableState(await prepared.readActionState(prepared.lookup), prepared);
|
|
465
|
+
}
|
|
466
|
+
catch {
|
|
467
|
+
progress.receiptPersistence = "indeterminate";
|
|
468
|
+
return uncertainResult(progress, "operation_state_corrupt", receipt);
|
|
469
|
+
}
|
|
470
|
+
if (afterFailure?.receipt !== undefined && sameReceipt(afterFailure.receipt, receipt)) {
|
|
471
|
+
progress.receiptPersistence = "durable";
|
|
472
|
+
return resultFromReceipt(afterFailure.receipt, outcome, false, progress);
|
|
473
|
+
}
|
|
474
|
+
progress.receiptPersistence = "indeterminate";
|
|
475
|
+
return uncertainResult(progress, "operation_state_corrupt", receipt);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
function mapOutput(prepared, output, progress) {
|
|
479
|
+
const observedAt = preparedTimestamp(prepared, progress.intentAt);
|
|
480
|
+
if (output.status === "committed" || output.status === "reconciled") {
|
|
481
|
+
return {
|
|
482
|
+
outcome: "satisfied",
|
|
483
|
+
receipt: freezeRecord({
|
|
484
|
+
schemaVersion: RECEIPT_SCHEMA_VERSION,
|
|
485
|
+
operationId: prepared.operationId,
|
|
486
|
+
requestDigest: prepared.requestDigest,
|
|
487
|
+
targetBindingDigest: prepared.targetBindingDigest,
|
|
488
|
+
assistantTurnId: prepared.assistantTurnId,
|
|
489
|
+
sourceIdentityDigest: prepared.sourceIdentityDigest,
|
|
490
|
+
kind: prepared.kind,
|
|
491
|
+
ordinal: prepared.ordinal,
|
|
492
|
+
transferActionId: prepared.transferActionId,
|
|
493
|
+
destinationIdentityDigest: prepared.destinationIdentityDigest,
|
|
494
|
+
outputKey: output.outputKey,
|
|
495
|
+
bytes: output.bytes,
|
|
496
|
+
sha256: output.sha256,
|
|
497
|
+
status: "transferred",
|
|
498
|
+
observedAt
|
|
499
|
+
})
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
const blockerCode = output.status === "collision"
|
|
503
|
+
? "output_collision"
|
|
504
|
+
: output.reason === "commit_indeterminate" || output.reason.startsWith("temp_")
|
|
505
|
+
? "output_commit_indeterminate"
|
|
506
|
+
: output.reason === "destination_invalid"
|
|
507
|
+
? "output_commit_indeterminate"
|
|
508
|
+
: "artifact_transfer_partial";
|
|
509
|
+
// The output primitive reports a zero/empty digest for preflight and other
|
|
510
|
+
// boundaries where no stream prefix was observed. Do not turn those
|
|
511
|
+
// placeholders into durable transfer facts. Stream-side failures retain
|
|
512
|
+
// only the prefix that the primitive actually hashed.
|
|
513
|
+
const observedPrefix = new Set([
|
|
514
|
+
"source_aborted", "source_invalid", "source_mismatch", "source_read_failed",
|
|
515
|
+
"byte_limit_exceeded", "write_failed", "file_sync_failed"
|
|
516
|
+
]).has(output.reason)
|
|
517
|
+
? { bytes: output.bytes, sha256: output.sha256 }
|
|
518
|
+
: {};
|
|
519
|
+
return {
|
|
520
|
+
outcome: output.status === "collision" ? "not_satisfied" : "uncertain",
|
|
521
|
+
receipt: freezeRecord({
|
|
522
|
+
schemaVersion: RECEIPT_SCHEMA_VERSION,
|
|
523
|
+
operationId: prepared.operationId,
|
|
524
|
+
requestDigest: prepared.requestDigest,
|
|
525
|
+
targetBindingDigest: prepared.targetBindingDigest,
|
|
526
|
+
assistantTurnId: prepared.assistantTurnId,
|
|
527
|
+
sourceIdentityDigest: prepared.sourceIdentityDigest,
|
|
528
|
+
kind: prepared.kind,
|
|
529
|
+
ordinal: prepared.ordinal,
|
|
530
|
+
transferActionId: prepared.transferActionId,
|
|
531
|
+
destinationIdentityDigest: prepared.destinationIdentityDigest,
|
|
532
|
+
outputKey: output.outputKey,
|
|
533
|
+
...observedPrefix,
|
|
534
|
+
status: output.status === "collision" ? "blocked" : "partial",
|
|
535
|
+
blockerCode,
|
|
536
|
+
observedAt
|
|
537
|
+
})
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
function resultFromReceipt(receipt, outcome, replayed, progress, overrideBlocker) {
|
|
541
|
+
return freezeRecord({
|
|
542
|
+
schemaVersion: SCHEMA_VERSION,
|
|
543
|
+
outcome,
|
|
544
|
+
replayed,
|
|
545
|
+
intentPersistence: progress.intentPersistence,
|
|
546
|
+
receiptPersistence: progress.receiptPersistence,
|
|
547
|
+
receipt,
|
|
548
|
+
...(overrideBlocker === undefined && receipt.blockerCode === undefined ? {} : {
|
|
549
|
+
blockerCode: overrideBlocker ?? receipt.blockerCode
|
|
550
|
+
})
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
function failureResult(blockerCode, progress) {
|
|
554
|
+
return freezeRecord({
|
|
555
|
+
schemaVersion: SCHEMA_VERSION,
|
|
556
|
+
outcome: "not_satisfied",
|
|
557
|
+
replayed: false,
|
|
558
|
+
intentPersistence: progress.intentPersistence,
|
|
559
|
+
receiptPersistence: progress.receiptPersistence,
|
|
560
|
+
blockerCode
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
function conflictResult(progress) {
|
|
564
|
+
return failureResult("operation_request_mismatch", progress);
|
|
565
|
+
}
|
|
566
|
+
function preIntentAbortResult(prepared, progress) {
|
|
567
|
+
return failureResult(cancellationCode(prepared), progress);
|
|
568
|
+
}
|
|
569
|
+
function cancellationCode(prepared) {
|
|
570
|
+
return isSignalAborted(prepared.signal) ? "operation_cancelled" : "operation_timeout";
|
|
571
|
+
}
|
|
572
|
+
function makeIntent(prepared) {
|
|
573
|
+
return freezeRecord({
|
|
574
|
+
schemaVersion: INTENT_SCHEMA_VERSION,
|
|
575
|
+
operationId: prepared.operationId,
|
|
576
|
+
requestDigest: prepared.requestDigest,
|
|
577
|
+
targetBindingDigest: prepared.targetBindingDigest,
|
|
578
|
+
assistantTurnId: prepared.assistantTurnId,
|
|
579
|
+
sourceIdentityDigest: prepared.sourceIdentityDigest,
|
|
580
|
+
kind: prepared.kind,
|
|
581
|
+
ordinal: prepared.ordinal,
|
|
582
|
+
transferActionId: prepared.transferActionId,
|
|
583
|
+
destinationIdentityDigest: prepared.destinationIdentityDigest,
|
|
584
|
+
actionKind: "local_output_commit",
|
|
585
|
+
repeatPolicy: "reconcile_local_effect",
|
|
586
|
+
intentAt: preparedTimestamp(prepared)
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
function sourceRequest(prepared) {
|
|
590
|
+
return freezeRecord({
|
|
591
|
+
operationId: prepared.operationId,
|
|
592
|
+
requestDigest: prepared.requestDigest,
|
|
593
|
+
targetBindingDigest: prepared.targetBindingDigest,
|
|
594
|
+
assistantTurnId: prepared.assistantTurnId,
|
|
595
|
+
sourceIdentityDigest: prepared.sourceIdentityDigest,
|
|
596
|
+
kind: prepared.kind,
|
|
597
|
+
ordinal: prepared.ordinal,
|
|
598
|
+
transferActionId: prepared.transferActionId,
|
|
599
|
+
destinationIdentityDigest: prepared.destinationIdentityDigest
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
function commitOptions(prepared, source, expected) {
|
|
603
|
+
return {
|
|
604
|
+
operationId: prepared.operationId,
|
|
605
|
+
artifactIdentity: artifactIdentity(prepared),
|
|
606
|
+
outputDirectory: prepared.outputDirectory,
|
|
607
|
+
source,
|
|
608
|
+
maxBytes: prepared.maxBytes,
|
|
609
|
+
...(expected === undefined ? {} : { expected }),
|
|
610
|
+
...(prepared.signal === undefined ? {} : { signal: prepared.signal }),
|
|
611
|
+
...(prepared.deadlineAt === undefined ? {} : { deadlineAt: prepared.deadlineAt }),
|
|
612
|
+
// Artifact-output owns the local effect deadline, but it must use this
|
|
613
|
+
// operation's guarded clock. A backwards/invalid sample is not allowed
|
|
614
|
+
// to become a fresh filesystem timestamp or deadline decision.
|
|
615
|
+
now: () => {
|
|
616
|
+
const value = prepared.now();
|
|
617
|
+
if (prepared.clockFaulted())
|
|
618
|
+
throw new ArtifactTransferError("operation_clock_invalid", "Operation clock became invalid.");
|
|
619
|
+
return value;
|
|
620
|
+
},
|
|
621
|
+
...(prepared.extensionHint === undefined ? {} : { extensionHint: prepared.extensionHint }),
|
|
622
|
+
...(prepared.mimeTypeHint === undefined ? {} : { mimeTypeHint: prepared.mimeTypeHint })
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
function artifactIdentity(prepared) {
|
|
626
|
+
// Fixed property order is intentional: this is an opaque local key input,
|
|
627
|
+
// not a durable/public authority. It contains no destination path.
|
|
628
|
+
return JSON.stringify({
|
|
629
|
+
assistantTurnId: prepared.assistantTurnId,
|
|
630
|
+
sourceIdentityDigest: prepared.sourceIdentityDigest,
|
|
631
|
+
kind: prepared.kind,
|
|
632
|
+
ordinal: prepared.ordinal,
|
|
633
|
+
transferActionId: prepared.transferActionId,
|
|
634
|
+
destinationIdentityDigest: prepared.destinationIdentityDigest,
|
|
635
|
+
maxBytes: prepared.maxBytes
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
function transferIdentity(prepared) {
|
|
639
|
+
return [
|
|
640
|
+
prepared.operationId,
|
|
641
|
+
prepared.requestDigest,
|
|
642
|
+
prepared.targetBindingDigest,
|
|
643
|
+
prepared.assistantTurnId,
|
|
644
|
+
prepared.sourceIdentityDigest,
|
|
645
|
+
prepared.kind,
|
|
646
|
+
String(prepared.ordinal),
|
|
647
|
+
prepared.transferActionId,
|
|
648
|
+
prepared.destinationIdentityDigest,
|
|
649
|
+
String(prepared.maxBytes),
|
|
650
|
+
prepared.outputKey
|
|
651
|
+
].join("\0");
|
|
652
|
+
}
|
|
653
|
+
async function prepare(options) {
|
|
654
|
+
const record = snapshotRecord(options, "options");
|
|
655
|
+
assertAllowedKeys(record, [
|
|
656
|
+
"operationId", "requestDigest", "targetBindingDigest", "assistantTurnId", "sourceIdentityDigest",
|
|
657
|
+
"kind", "ordinal", "transferActionId", "outputDirectory", "evidenceDigest", "openSource", "journal",
|
|
658
|
+
"signal", "deadlineAt", "now", "limits", "extensionHint", "mimeTypeHint"
|
|
659
|
+
]);
|
|
660
|
+
const operationId = requiredString(record, "operationId", UUID_PATTERN, 128);
|
|
661
|
+
const requestDigest = requiredString(record, "requestDigest", DIGEST_PATTERN, 128);
|
|
662
|
+
const targetBindingDigest = requiredString(record, "targetBindingDigest", DIGEST_PATTERN, 128);
|
|
663
|
+
const assistantTurnId = requiredOpaqueId(record, "assistantTurnId");
|
|
664
|
+
const sourceIdentityDigest = requiredString(record, "sourceIdentityDigest", DIGEST_PATTERN, 128);
|
|
665
|
+
const kind = requiredKind(record.kind);
|
|
666
|
+
const limits = readLimits(record.limits);
|
|
667
|
+
// The caller-provided depth bound is meaningful only if applied to the
|
|
668
|
+
// complete option graph. Validate it after reading the scalar limits so a
|
|
669
|
+
// deeply nested/proxy-backed callback record cannot bypass the advertised
|
|
670
|
+
// bound.
|
|
671
|
+
assertGraphBounds(record, new Set(), 0, limits.maxDepth);
|
|
672
|
+
const ordinal = requiredOrdinal(record.ordinal, limits.maxCount);
|
|
673
|
+
const transferActionId = requiredString(record, "transferActionId", UUID_PATTERN, 128);
|
|
674
|
+
const outputDirectory = requiredString(record, "outputDirectory", undefined, limits.maxStringBytes);
|
|
675
|
+
if (!isAbsolutePath(outputDirectory))
|
|
676
|
+
throw invalidOptions();
|
|
677
|
+
const canonicalDestination = resolve(outputDirectory);
|
|
678
|
+
if (byteLength(canonicalDestination) > limits.maxStringBytes)
|
|
679
|
+
throw invalidOptions();
|
|
680
|
+
const evidenceDigest = requiredFunction(record, "evidenceDigest");
|
|
681
|
+
const openSource = requiredFunction(record, "openSource");
|
|
682
|
+
const journal = readJournal(record.journal);
|
|
683
|
+
const signal = readOptionalSignal(record.signal);
|
|
684
|
+
const deadlineAt = readOptionalDeadline(record.deadlineAt);
|
|
685
|
+
const clock = readNow(record.now);
|
|
686
|
+
if (deadlineAt !== undefined && deadlineAt > MAX_DEADLINE_AT)
|
|
687
|
+
throw invalidOptions();
|
|
688
|
+
if (limits.maxDepth < 2)
|
|
689
|
+
throw invalidOptions();
|
|
690
|
+
const extensionHint = readOptionalString(record, "extensionHint", limits.maxStringBytes);
|
|
691
|
+
const mimeTypeHint = readOptionalString(record, "mimeTypeHint", limits.maxStringBytes);
|
|
692
|
+
let destinationIdentityDigest;
|
|
693
|
+
try {
|
|
694
|
+
destinationIdentityDigest = await evidenceDigest("artifact-destination", freezeRecord({
|
|
695
|
+
schemaVersion: SCHEMA_VERSION,
|
|
696
|
+
operationId,
|
|
697
|
+
requestDigest,
|
|
698
|
+
targetBindingDigest,
|
|
699
|
+
assistantTurnId,
|
|
700
|
+
sourceIdentityDigest,
|
|
701
|
+
kind,
|
|
702
|
+
ordinal,
|
|
703
|
+
transferActionId,
|
|
704
|
+
canonicalDestination
|
|
705
|
+
}));
|
|
706
|
+
}
|
|
707
|
+
catch {
|
|
708
|
+
throw new ArtifactTransferError("invalid_evidence_digest", "Destination evidence could not be derived.");
|
|
709
|
+
}
|
|
710
|
+
if (!DIGEST_PATTERN.test(destinationIdentityDigest))
|
|
711
|
+
throw invalidOptions();
|
|
712
|
+
let outputKey;
|
|
713
|
+
try {
|
|
714
|
+
outputKey = deriveOperationOutputKey({
|
|
715
|
+
operationId,
|
|
716
|
+
artifactIdentity: JSON.stringify({ assistantTurnId, sourceIdentityDigest, kind, ordinal, transferActionId, destinationIdentityDigest, maxBytes: limits.maxBytes }),
|
|
717
|
+
...(extensionHint === undefined ? {} : { extensionHint }),
|
|
718
|
+
...(mimeTypeHint === undefined ? {} : { mimeTypeHint })
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
catch {
|
|
722
|
+
throw invalidOptions();
|
|
723
|
+
}
|
|
724
|
+
return {
|
|
725
|
+
operationId,
|
|
726
|
+
requestDigest,
|
|
727
|
+
targetBindingDigest,
|
|
728
|
+
assistantTurnId,
|
|
729
|
+
sourceIdentityDigest,
|
|
730
|
+
kind,
|
|
731
|
+
ordinal,
|
|
732
|
+
transferActionId,
|
|
733
|
+
outputDirectory: canonicalDestination,
|
|
734
|
+
destinationIdentityDigest,
|
|
735
|
+
outputKey,
|
|
736
|
+
...(extensionHint === undefined ? {} : { extensionHint }),
|
|
737
|
+
...(mimeTypeHint === undefined ? {} : { mimeTypeHint }),
|
|
738
|
+
maxBytes: limits.maxBytes,
|
|
739
|
+
maxCount: limits.maxCount,
|
|
740
|
+
maxDepth: limits.maxDepth,
|
|
741
|
+
...(signal === undefined ? {} : { signal }),
|
|
742
|
+
...(deadlineAt === undefined ? {} : { deadlineAt }),
|
|
743
|
+
now: clock.now,
|
|
744
|
+
clockFaulted: clock.faulted,
|
|
745
|
+
openSource,
|
|
746
|
+
readActionState: journal.readActionState,
|
|
747
|
+
persistIntent: journal.persistIntent,
|
|
748
|
+
persistReceipt: journal.persistReceipt,
|
|
749
|
+
lookup: freezeRecord({
|
|
750
|
+
operationId,
|
|
751
|
+
requestDigest,
|
|
752
|
+
targetBindingDigest,
|
|
753
|
+
assistantTurnId,
|
|
754
|
+
sourceIdentityDigest,
|
|
755
|
+
kind,
|
|
756
|
+
ordinal,
|
|
757
|
+
transferActionId,
|
|
758
|
+
destinationIdentityDigest
|
|
759
|
+
})
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
function readLimits(value) {
|
|
763
|
+
if (value === undefined)
|
|
764
|
+
return { maxBytes: DEFAULT_MAX_BYTES, maxStringBytes: DEFAULT_MAX_STRING_BYTES, maxCount: DEFAULT_MAX_COUNT, maxDepth: DEFAULT_MAX_DEPTH };
|
|
765
|
+
const record = snapshotRecord(value, "limits");
|
|
766
|
+
assertAllowedKeys(record, ["maxBytes", "maxStringBytes", "maxCount", "maxDepth"]);
|
|
767
|
+
const maxBytes = optionalBoundedNumber(record, "maxBytes", DEFAULT_MAX_BYTES, 0, MAX_MAX_BYTES);
|
|
768
|
+
const maxStringBytes = optionalBoundedNumber(record, "maxStringBytes", DEFAULT_MAX_STRING_BYTES, 32, MAX_MAX_STRING_BYTES);
|
|
769
|
+
const maxCount = optionalBoundedNumber(record, "maxCount", DEFAULT_MAX_COUNT, 1, MAX_MAX_COUNT);
|
|
770
|
+
const maxDepth = optionalBoundedNumber(record, "maxDepth", DEFAULT_MAX_DEPTH, 2, MAX_MAX_DEPTH);
|
|
771
|
+
return { maxBytes, maxStringBytes, maxCount, maxDepth };
|
|
772
|
+
}
|
|
773
|
+
function readJournal(value) {
|
|
774
|
+
const record = snapshotRecord(value, "journal");
|
|
775
|
+
assertAllowedKeys(record, ["readActionState", "persistIntent", "persistReceipt"]);
|
|
776
|
+
const readActionState = requiredFunction(record, "readActionState");
|
|
777
|
+
const persistIntent = requiredFunction(record, "persistIntent");
|
|
778
|
+
const persistReceipt = requiredFunction(record, "persistReceipt");
|
|
779
|
+
return {
|
|
780
|
+
readActionState: (lookup) => Reflect.apply(readActionState, value, [freezeRecord({ ...lookup })]),
|
|
781
|
+
persistIntent: (intent) => Reflect.apply(persistIntent, value, [freezeRecord({ ...intent })]),
|
|
782
|
+
persistReceipt: (receipt) => Reflect.apply(persistReceipt, value, [freezeRecord({ ...receipt })])
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
function validateDurableState(value, prepared) {
|
|
786
|
+
if (value === undefined)
|
|
787
|
+
return undefined;
|
|
788
|
+
const record = snapshotRecord(value, "durable state", prepared.maxDepth);
|
|
789
|
+
const allowed = new Set(["intent", "receipt"]);
|
|
790
|
+
for (const key of Object.keys(record))
|
|
791
|
+
if (!allowed.has(key))
|
|
792
|
+
throw invalidOptions();
|
|
793
|
+
const intentValue = record.intent;
|
|
794
|
+
const receiptValue = record.receipt;
|
|
795
|
+
if (intentValue === undefined && receiptValue === undefined)
|
|
796
|
+
throw invalidOptions();
|
|
797
|
+
const intent = intentValue === undefined ? undefined : validateIntent(intentValue, prepared);
|
|
798
|
+
const receipt = receiptValue === undefined ? undefined : validateReceipt(receiptValue, prepared);
|
|
799
|
+
// A receipt records completion of an intent-bound transfer. Accepting a
|
|
800
|
+
// receipt without its causal intent would make an untrusted/partial journal
|
|
801
|
+
// record look replayable and would erase the source handoff boundary.
|
|
802
|
+
if (receipt !== undefined && intent === undefined)
|
|
803
|
+
throw invalidOptions();
|
|
804
|
+
if (intent !== undefined && receipt !== undefined && !sameIdentity(intent, receipt))
|
|
805
|
+
throw invalidOptions();
|
|
806
|
+
if (intent !== undefined && receipt !== undefined && Date.parse(receipt.observedAt) < Date.parse(intent.intentAt)) {
|
|
807
|
+
// A durable observation predating its causal intent is evidence of a
|
|
808
|
+
// backwards/untrusted clock (or journal corruption), never a replayable
|
|
809
|
+
// completion.
|
|
810
|
+
throw invalidOptions();
|
|
811
|
+
}
|
|
812
|
+
return freezeRecord({
|
|
813
|
+
...(intent === undefined ? {} : { intent }),
|
|
814
|
+
...(receipt === undefined ? {} : { receipt })
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
function validateIntent(value, prepared) {
|
|
818
|
+
const record = snapshotExactRecord(value, [
|
|
819
|
+
"schemaVersion", "operationId", "requestDigest", "targetBindingDigest", "assistantTurnId",
|
|
820
|
+
"sourceIdentityDigest", "kind", "ordinal", "transferActionId", "destinationIdentityDigest",
|
|
821
|
+
"actionKind", "repeatPolicy", "intentAt"
|
|
822
|
+
], prepared.maxDepth);
|
|
823
|
+
const intent = record;
|
|
824
|
+
if (intent.schemaVersion !== INTENT_SCHEMA_VERSION || intent.actionKind !== "local_output_commit" || intent.repeatPolicy !== "reconcile_local_effect")
|
|
825
|
+
throw invalidOptions();
|
|
826
|
+
validateIdentityFields(intent, prepared);
|
|
827
|
+
if (!isTimestamp(intent.intentAt))
|
|
828
|
+
throw invalidOptions();
|
|
829
|
+
return freezeRecord({ ...intent });
|
|
830
|
+
}
|
|
831
|
+
function validateReceipt(value, prepared) {
|
|
832
|
+
const record = snapshotRecord(value, "receipt", prepared.maxDepth);
|
|
833
|
+
const allowed = new Set([
|
|
834
|
+
"schemaVersion", "operationId", "requestDigest", "targetBindingDigest", "assistantTurnId",
|
|
835
|
+
"sourceIdentityDigest", "kind", "ordinal", "transferActionId", "destinationIdentityDigest",
|
|
836
|
+
"outputKey", "bytes", "sha256", "status", "blockerCode", "observedAt"
|
|
837
|
+
]);
|
|
838
|
+
for (const key of Object.keys(record))
|
|
839
|
+
if (!allowed.has(key))
|
|
840
|
+
throw invalidOptions();
|
|
841
|
+
const receipt = record;
|
|
842
|
+
if (receipt.schemaVersion !== RECEIPT_SCHEMA_VERSION || !isArtifactStatus(receipt.status) || !isTimestamp(receipt.observedAt))
|
|
843
|
+
throw invalidOptions();
|
|
844
|
+
validateIdentityFields(receipt, prepared);
|
|
845
|
+
if (receipt.outputKey !== undefined && !OUTPUT_KEY_PATTERN.test(receipt.outputKey))
|
|
846
|
+
throw invalidOptions();
|
|
847
|
+
if (receipt.outputKey !== undefined && receipt.outputKey !== prepared.outputKey)
|
|
848
|
+
throw new ArtifactTransferError("operation_request_mismatch", "Durable artifact output identity does not match the request.");
|
|
849
|
+
if (receipt.bytes !== undefined && (!Number.isSafeInteger(receipt.bytes) || receipt.bytes < 0 || receipt.bytes > prepared.maxBytes))
|
|
850
|
+
throw invalidOptions();
|
|
851
|
+
if (receipt.sha256 !== undefined && !SHA256_PATTERN.test(receipt.sha256))
|
|
852
|
+
throw invalidOptions();
|
|
853
|
+
if (receipt.blockerCode !== undefined && !CODE_PATTERN.test(receipt.blockerCode))
|
|
854
|
+
throw invalidOptions();
|
|
855
|
+
if (receipt.status === "transferred" && (receipt.outputKey === undefined || receipt.bytes === undefined || receipt.sha256 === undefined || receipt.blockerCode !== undefined))
|
|
856
|
+
throw invalidOptions();
|
|
857
|
+
if (receipt.status !== "transferred" && receipt.blockerCode === undefined)
|
|
858
|
+
throw invalidOptions();
|
|
859
|
+
return freezeRecord({ ...receipt });
|
|
860
|
+
}
|
|
861
|
+
function validateIdentityFields(value, prepared) {
|
|
862
|
+
if (value.operationId !== prepared.operationId
|
|
863
|
+
|| value.requestDigest !== prepared.requestDigest
|
|
864
|
+
|| value.targetBindingDigest !== prepared.targetBindingDigest
|
|
865
|
+
|| value.assistantTurnId !== prepared.assistantTurnId
|
|
866
|
+
|| value.sourceIdentityDigest !== prepared.sourceIdentityDigest
|
|
867
|
+
|| value.kind !== prepared.kind
|
|
868
|
+
|| value.ordinal !== prepared.ordinal
|
|
869
|
+
|| value.transferActionId !== prepared.transferActionId
|
|
870
|
+
|| value.destinationIdentityDigest !== prepared.destinationIdentityDigest)
|
|
871
|
+
throw new ArtifactTransferError("operation_request_mismatch", "Durable artifact transfer identity does not match the request.");
|
|
872
|
+
}
|
|
873
|
+
function sameIdentity(left, right) {
|
|
874
|
+
return sameTransferIdentity(left, right);
|
|
875
|
+
}
|
|
876
|
+
function sameTransferIdentity(left, right) {
|
|
877
|
+
return left.operationId === right.operationId
|
|
878
|
+
&& left.requestDigest === right.requestDigest
|
|
879
|
+
&& left.targetBindingDigest === right.targetBindingDigest
|
|
880
|
+
&& left.assistantTurnId === right.assistantTurnId
|
|
881
|
+
&& left.sourceIdentityDigest === right.sourceIdentityDigest
|
|
882
|
+
&& left.kind === right.kind
|
|
883
|
+
&& left.ordinal === right.ordinal
|
|
884
|
+
&& left.transferActionId === right.transferActionId
|
|
885
|
+
&& left.destinationIdentityDigest === right.destinationIdentityDigest;
|
|
886
|
+
}
|
|
887
|
+
function sameIntent(left, right) {
|
|
888
|
+
return left.schemaVersion === right.schemaVersion
|
|
889
|
+
&& left.actionKind === right.actionKind
|
|
890
|
+
&& left.repeatPolicy === right.repeatPolicy
|
|
891
|
+
&& left.intentAt === right.intentAt
|
|
892
|
+
&& sameTransferIdentity(left, right);
|
|
893
|
+
}
|
|
894
|
+
function sameReceipt(left, right) {
|
|
895
|
+
return left.schemaVersion === right.schemaVersion
|
|
896
|
+
&& left.outputKey === right.outputKey
|
|
897
|
+
&& left.bytes === right.bytes
|
|
898
|
+
&& left.sha256 === right.sha256
|
|
899
|
+
&& left.status === right.status
|
|
900
|
+
&& left.blockerCode === right.blockerCode
|
|
901
|
+
&& left.observedAt === right.observedAt
|
|
902
|
+
&& sameTransferIdentity(left, right);
|
|
903
|
+
}
|
|
904
|
+
async function closeArtifactSource(source, prepared) {
|
|
905
|
+
try {
|
|
906
|
+
const iteratorMethod = dataMethod(source, Symbol.asyncIterator);
|
|
907
|
+
if (typeof iteratorMethod !== "function")
|
|
908
|
+
return false;
|
|
909
|
+
const iterator = Reflect.apply(iteratorMethod, source, []);
|
|
910
|
+
const closeMethod = dataMethod(iterator, "return");
|
|
911
|
+
if (typeof closeMethod !== "function")
|
|
912
|
+
return false;
|
|
913
|
+
await awaitProviderOperation(prepared, () => Reflect.apply(closeMethod, iterator, []), PROVIDER_CLEANUP_TIMEOUT_MS);
|
|
914
|
+
return true;
|
|
915
|
+
}
|
|
916
|
+
catch {
|
|
917
|
+
return false;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
/**
|
|
921
|
+
* A late source capability arrived after the durable handoff boundary. Close
|
|
922
|
+
* it opportunistically, but never await it or let a malformed provider result
|
|
923
|
+
* escape as an unhandled rejection. The transfer remains uncertain and is
|
|
924
|
+
* never reopened based on this cleanup attempt.
|
|
925
|
+
*/
|
|
926
|
+
function closeArtifactSourceBestEffort(source) {
|
|
927
|
+
try {
|
|
928
|
+
const iteratorMethod = dataMethod(source, Symbol.asyncIterator);
|
|
929
|
+
if (typeof iteratorMethod !== "function")
|
|
930
|
+
return;
|
|
931
|
+
const iterator = Reflect.apply(iteratorMethod, source, []);
|
|
932
|
+
const closeMethod = dataMethod(iterator, "return");
|
|
933
|
+
if (typeof closeMethod !== "function")
|
|
934
|
+
return;
|
|
935
|
+
const result = Reflect.apply(closeMethod, iterator, []);
|
|
936
|
+
void Promise.resolve(result).then(() => undefined, () => undefined);
|
|
937
|
+
}
|
|
938
|
+
catch {
|
|
939
|
+
// The capability is already outside the request authority. There is no
|
|
940
|
+
// safe retry or durable fact to derive from a cleanup failure.
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
function safeSource(value, maxBytes) {
|
|
944
|
+
const iteratorMethod = dataMethod(value, Symbol.asyncIterator);
|
|
945
|
+
if (typeof iteratorMethod !== "function")
|
|
946
|
+
throw invalidOptions();
|
|
947
|
+
let iterator;
|
|
948
|
+
try {
|
|
949
|
+
iterator = Reflect.apply(iteratorMethod, value, []);
|
|
950
|
+
}
|
|
951
|
+
catch {
|
|
952
|
+
throw invalidOptions();
|
|
953
|
+
}
|
|
954
|
+
const nextMethod = dataMethod(iterator, "next");
|
|
955
|
+
if (typeof nextMethod !== "function")
|
|
956
|
+
throw invalidOptions();
|
|
957
|
+
const closeMethod = dataMethod(iterator, "return");
|
|
958
|
+
let closed = false;
|
|
959
|
+
const copyChunk = (value) => {
|
|
960
|
+
try {
|
|
961
|
+
// Make the copy before returning from the awaited `next` call. The
|
|
962
|
+
// provider retains ownership of the original view and may mutate it as
|
|
963
|
+
// soon as this boundary resolves. The internal marker lets the output
|
|
964
|
+
// sink consume this defensive copy without allocating a second one.
|
|
965
|
+
return copyProviderChunk(value, maxBytes);
|
|
966
|
+
}
|
|
967
|
+
catch {
|
|
968
|
+
throw new ArtifactTransferError("source_invalid", "Artifact source chunk is invalid.");
|
|
969
|
+
}
|
|
970
|
+
};
|
|
971
|
+
const close = async (value) => {
|
|
972
|
+
if (closed)
|
|
973
|
+
return { done: true, value: undefined };
|
|
974
|
+
closed = true;
|
|
975
|
+
if (typeof closeMethod !== "function")
|
|
976
|
+
return { done: true, value: undefined };
|
|
977
|
+
let raw;
|
|
978
|
+
try {
|
|
979
|
+
raw = await Reflect.apply(closeMethod, iterator, value === undefined ? [] : [value]);
|
|
980
|
+
}
|
|
981
|
+
catch {
|
|
982
|
+
throw new ArtifactTransferError("source_close_failed", "Artifact source cleanup failed.");
|
|
983
|
+
}
|
|
984
|
+
if (raw === undefined)
|
|
985
|
+
return { done: true, value: undefined };
|
|
986
|
+
try {
|
|
987
|
+
const result = snapshotExactRecord(raw, ["done", "value"]);
|
|
988
|
+
if (typeof result.done !== "boolean")
|
|
989
|
+
throw new Error("invalid done");
|
|
990
|
+
return result.done
|
|
991
|
+
? { done: true, value: undefined }
|
|
992
|
+
: { done: false, value: copyChunk(result.value) };
|
|
993
|
+
}
|
|
994
|
+
catch {
|
|
995
|
+
throw new ArtifactTransferError("source_close_failed", "Artifact source cleanup failed.");
|
|
996
|
+
}
|
|
997
|
+
};
|
|
998
|
+
const safeIterator = {
|
|
999
|
+
next: async () => {
|
|
1000
|
+
if (closed)
|
|
1001
|
+
return { done: true, value: undefined };
|
|
1002
|
+
let raw;
|
|
1003
|
+
try {
|
|
1004
|
+
raw = await Reflect.apply(nextMethod, iterator, []);
|
|
1005
|
+
}
|
|
1006
|
+
catch {
|
|
1007
|
+
try {
|
|
1008
|
+
await close();
|
|
1009
|
+
}
|
|
1010
|
+
catch { /* preserve the primary source error */ }
|
|
1011
|
+
throw new ArtifactTransferError("source_read_failed", "Artifact source could not be read.");
|
|
1012
|
+
}
|
|
1013
|
+
try {
|
|
1014
|
+
const result = snapshotExactRecord(raw, ["done", "value"]);
|
|
1015
|
+
if (typeof result.done !== "boolean")
|
|
1016
|
+
throw new Error("invalid done");
|
|
1017
|
+
if (result.done) {
|
|
1018
|
+
closed = true;
|
|
1019
|
+
return { done: true, value: undefined };
|
|
1020
|
+
}
|
|
1021
|
+
return { done: false, value: copyChunk(result.value) };
|
|
1022
|
+
}
|
|
1023
|
+
catch (error) {
|
|
1024
|
+
try {
|
|
1025
|
+
await close();
|
|
1026
|
+
}
|
|
1027
|
+
catch { /* preserve the primary source error */ }
|
|
1028
|
+
if (error instanceof ArtifactTransferError && error.code === "source_invalid")
|
|
1029
|
+
throw error;
|
|
1030
|
+
throw new ArtifactTransferError("source_invalid", "Artifact source result is invalid.");
|
|
1031
|
+
}
|
|
1032
|
+
},
|
|
1033
|
+
return: close
|
|
1034
|
+
};
|
|
1035
|
+
Object.freeze(safeIterator);
|
|
1036
|
+
return Object.freeze({ [Symbol.asyncIterator]: () => safeIterator });
|
|
1037
|
+
}
|
|
1038
|
+
function noSourceRetry() {
|
|
1039
|
+
const iterator = {
|
|
1040
|
+
next: async () => {
|
|
1041
|
+
throw new ArtifactTransferError("source_retry_forbidden", "Provider source retry is forbidden after durable intent.");
|
|
1042
|
+
},
|
|
1043
|
+
return: async () => ({ done: true, value: undefined })
|
|
1044
|
+
};
|
|
1045
|
+
Object.freeze(iterator);
|
|
1046
|
+
return Object.freeze({
|
|
1047
|
+
[Symbol.asyncIterator]: () => iterator
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
function dataMethod(value, key) {
|
|
1051
|
+
if (value === null || (typeof value !== "object" && typeof value !== "function"))
|
|
1052
|
+
return undefined;
|
|
1053
|
+
let current = value;
|
|
1054
|
+
try {
|
|
1055
|
+
for (let depth = 0; current !== null && depth < 16; depth += 1) {
|
|
1056
|
+
const descriptor = Object.getOwnPropertyDescriptor(current, key);
|
|
1057
|
+
if (descriptor !== undefined)
|
|
1058
|
+
return "value" in descriptor ? descriptor.value : undefined;
|
|
1059
|
+
current = Object.getPrototypeOf(current);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
catch {
|
|
1063
|
+
return undefined;
|
|
1064
|
+
}
|
|
1065
|
+
return undefined;
|
|
1066
|
+
}
|
|
1067
|
+
function snapshotRecord(value, label, maxDepth = MAX_MAX_DEPTH) {
|
|
1068
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
1069
|
+
throw invalidOptions();
|
|
1070
|
+
try {
|
|
1071
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1072
|
+
if (prototype !== Object.prototype && prototype !== null)
|
|
1073
|
+
throw invalidOptions();
|
|
1074
|
+
if (Object.getOwnPropertySymbols(value).length !== 0)
|
|
1075
|
+
throw invalidOptions();
|
|
1076
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
1077
|
+
const result = {};
|
|
1078
|
+
for (const [key, descriptor] of Object.entries(descriptors)) {
|
|
1079
|
+
if (!("value" in descriptor) || descriptor.get !== undefined || descriptor.set !== undefined)
|
|
1080
|
+
throw invalidOptions();
|
|
1081
|
+
result[key] = descriptor.value;
|
|
1082
|
+
}
|
|
1083
|
+
assertGraphBounds(value, new Set(), 0, maxDepth);
|
|
1084
|
+
void label;
|
|
1085
|
+
return result;
|
|
1086
|
+
}
|
|
1087
|
+
catch (error) {
|
|
1088
|
+
if (error instanceof ArtifactTransferError)
|
|
1089
|
+
throw error;
|
|
1090
|
+
throw invalidOptions();
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
function snapshotExactRecord(value, keys, maxDepth = MAX_MAX_DEPTH) {
|
|
1094
|
+
const record = snapshotRecord(value, "record", maxDepth);
|
|
1095
|
+
const expected = new Set(keys);
|
|
1096
|
+
if (Object.keys(record).length !== expected.size || Object.keys(record).some(key => !expected.has(key)))
|
|
1097
|
+
throw invalidOptions();
|
|
1098
|
+
return record;
|
|
1099
|
+
}
|
|
1100
|
+
/**
|
|
1101
|
+
* Walk only own data properties, never invoking accessors or traversing
|
|
1102
|
+
* prototypes. This bounds the option/journal/record graph while retaining
|
|
1103
|
+
* callback and AbortSignal objects as opaque capabilities once their own
|
|
1104
|
+
* descriptors have been inspected.
|
|
1105
|
+
*/
|
|
1106
|
+
function assertGraphBounds(value, seen, depth, maxDepth) {
|
|
1107
|
+
if (value === null || typeof value !== "object" || typeof value === "function")
|
|
1108
|
+
return;
|
|
1109
|
+
// Byte views are validated/copied at their own boundary; enumerating every
|
|
1110
|
+
// byte as a graph node would reject otherwise bounded chunks.
|
|
1111
|
+
if (ArrayBuffer.isView(value))
|
|
1112
|
+
return;
|
|
1113
|
+
if (seen.has(value))
|
|
1114
|
+
return;
|
|
1115
|
+
if (depth > maxDepth || seen.size >= MAX_GRAPH_NODES)
|
|
1116
|
+
throw invalidOptions();
|
|
1117
|
+
seen.add(value);
|
|
1118
|
+
let descriptors;
|
|
1119
|
+
try {
|
|
1120
|
+
descriptors = Object.getOwnPropertyDescriptors(value);
|
|
1121
|
+
}
|
|
1122
|
+
catch {
|
|
1123
|
+
throw invalidOptions();
|
|
1124
|
+
}
|
|
1125
|
+
for (const descriptor of Object.values(descriptors)) {
|
|
1126
|
+
if (!("value" in descriptor) || descriptor.get !== undefined || descriptor.set !== undefined)
|
|
1127
|
+
throw invalidOptions();
|
|
1128
|
+
assertGraphBounds(descriptor.value, seen, depth + 1, maxDepth);
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
function assertAllowedKeys(record, keys) {
|
|
1132
|
+
const allowed = new Set(keys);
|
|
1133
|
+
if (Object.keys(record).some(key => !allowed.has(key)))
|
|
1134
|
+
throw invalidOptions();
|
|
1135
|
+
}
|
|
1136
|
+
function requiredString(record, key, pattern, maxBytes) {
|
|
1137
|
+
const value = record[key];
|
|
1138
|
+
if (typeof value !== "string" || value.length === 0 || byteLength(value) > maxBytes || /[\u0000-\u001f\u007f]/u.test(value) || (pattern !== undefined && !pattern.test(value)))
|
|
1139
|
+
throw invalidOptions();
|
|
1140
|
+
return value;
|
|
1141
|
+
}
|
|
1142
|
+
function requiredOpaqueId(record, key) {
|
|
1143
|
+
const value = record[key];
|
|
1144
|
+
if (typeof value !== "string" || value.trim().length === 0 || value.length > 512 || /[\u0000-\u001f\u007f]/u.test(value))
|
|
1145
|
+
throw invalidOptions();
|
|
1146
|
+
return value;
|
|
1147
|
+
}
|
|
1148
|
+
function requiredKind(value) {
|
|
1149
|
+
if (value !== "file" && value !== "image" && value !== "other")
|
|
1150
|
+
throw invalidOptions();
|
|
1151
|
+
return value;
|
|
1152
|
+
}
|
|
1153
|
+
function requiredOrdinal(value, maxCount) {
|
|
1154
|
+
if (!Number.isSafeInteger(value) || value < 0 || value >= maxCount)
|
|
1155
|
+
throw invalidOptions();
|
|
1156
|
+
return value;
|
|
1157
|
+
}
|
|
1158
|
+
function requiredFunction(record, key) {
|
|
1159
|
+
const value = record[key];
|
|
1160
|
+
if (typeof value !== "function")
|
|
1161
|
+
throw invalidOptions();
|
|
1162
|
+
return value;
|
|
1163
|
+
}
|
|
1164
|
+
function readOptionalString(record, key, maxBytes) {
|
|
1165
|
+
const value = record[key];
|
|
1166
|
+
if (value === undefined)
|
|
1167
|
+
return undefined;
|
|
1168
|
+
if (typeof value !== "string" || byteLength(value) > maxBytes || /[\u0000-\u001f\u007f]/u.test(value))
|
|
1169
|
+
throw invalidOptions();
|
|
1170
|
+
return value;
|
|
1171
|
+
}
|
|
1172
|
+
function optionalBoundedNumber(record, key, fallback, minimum, maximum) {
|
|
1173
|
+
const value = record[key];
|
|
1174
|
+
if (value === undefined)
|
|
1175
|
+
return fallback;
|
|
1176
|
+
if (!Number.isSafeInteger(value) || value < minimum || value > maximum)
|
|
1177
|
+
throw invalidOptions();
|
|
1178
|
+
return value;
|
|
1179
|
+
}
|
|
1180
|
+
function readOptionalSignal(value) {
|
|
1181
|
+
if (value === undefined)
|
|
1182
|
+
return undefined;
|
|
1183
|
+
if (typeof AbortSignal !== "function" || value === null || typeof value !== "object")
|
|
1184
|
+
throw invalidOptions();
|
|
1185
|
+
try {
|
|
1186
|
+
if (!(value instanceof AbortSignal))
|
|
1187
|
+
throw invalidOptions();
|
|
1188
|
+
const own = Object.getOwnPropertyDescriptor(value, "aborted");
|
|
1189
|
+
if (own !== undefined)
|
|
1190
|
+
throw invalidOptions();
|
|
1191
|
+
const descriptor = Object.getOwnPropertyDescriptor(AbortSignal.prototype, "aborted");
|
|
1192
|
+
if (descriptor?.get === undefined || typeof descriptor.get.call(value) !== "boolean")
|
|
1193
|
+
throw invalidOptions();
|
|
1194
|
+
}
|
|
1195
|
+
catch (error) {
|
|
1196
|
+
if (error instanceof ArtifactTransferError)
|
|
1197
|
+
throw error;
|
|
1198
|
+
throw invalidOptions();
|
|
1199
|
+
}
|
|
1200
|
+
return value;
|
|
1201
|
+
}
|
|
1202
|
+
function readOptionalDeadline(value) {
|
|
1203
|
+
if (value === undefined)
|
|
1204
|
+
return undefined;
|
|
1205
|
+
if (!Number.isSafeInteger(value) || value < 0 || value > MAX_DEADLINE_AT)
|
|
1206
|
+
throw invalidOptions();
|
|
1207
|
+
return value;
|
|
1208
|
+
}
|
|
1209
|
+
function readNow(value) {
|
|
1210
|
+
if (value !== undefined && typeof value !== "function")
|
|
1211
|
+
throw invalidOptions();
|
|
1212
|
+
const source = value === undefined ? Date.now : value;
|
|
1213
|
+
let sample;
|
|
1214
|
+
try {
|
|
1215
|
+
sample = source();
|
|
1216
|
+
if (!Number.isSafeInteger(sample) || sample < 0 || sample > MAX_DEADLINE_AT)
|
|
1217
|
+
throw invalidOptions();
|
|
1218
|
+
}
|
|
1219
|
+
catch (error) {
|
|
1220
|
+
if (error instanceof ArtifactTransferError)
|
|
1221
|
+
throw error;
|
|
1222
|
+
throw invalidOptions();
|
|
1223
|
+
}
|
|
1224
|
+
let faulted = false;
|
|
1225
|
+
let lastValid = sample;
|
|
1226
|
+
const now = () => {
|
|
1227
|
+
if (faulted)
|
|
1228
|
+
return lastValid;
|
|
1229
|
+
try {
|
|
1230
|
+
const next = source();
|
|
1231
|
+
if (!Number.isSafeInteger(next) || next < 0 || next > MAX_DEADLINE_AT)
|
|
1232
|
+
throw new Error("invalid clock value");
|
|
1233
|
+
if (next < lastValid)
|
|
1234
|
+
throw new Error("clock moved backwards");
|
|
1235
|
+
lastValid = next;
|
|
1236
|
+
return next;
|
|
1237
|
+
}
|
|
1238
|
+
catch {
|
|
1239
|
+
// Retain the last valid sample so error handling can remain total and
|
|
1240
|
+
// deterministic. Callers inspect faulted() and return uncertainty
|
|
1241
|
+
// before using this value for a new durable fact.
|
|
1242
|
+
faulted = true;
|
|
1243
|
+
return lastValid;
|
|
1244
|
+
}
|
|
1245
|
+
};
|
|
1246
|
+
return { now, faulted: () => faulted };
|
|
1247
|
+
}
|
|
1248
|
+
function isCancelledOrExpired(prepared) {
|
|
1249
|
+
if (isSignalAborted(prepared.signal))
|
|
1250
|
+
return true;
|
|
1251
|
+
let current;
|
|
1252
|
+
try {
|
|
1253
|
+
current = prepared.now();
|
|
1254
|
+
}
|
|
1255
|
+
catch {
|
|
1256
|
+
return true;
|
|
1257
|
+
}
|
|
1258
|
+
if (prepared.clockFaulted())
|
|
1259
|
+
return true;
|
|
1260
|
+
return prepared.deadlineAt !== undefined && current >= prepared.deadlineAt;
|
|
1261
|
+
}
|
|
1262
|
+
function isSignalAborted(signal) {
|
|
1263
|
+
if (signal === undefined)
|
|
1264
|
+
return false;
|
|
1265
|
+
try {
|
|
1266
|
+
const descriptor = Object.getOwnPropertyDescriptor(AbortSignal.prototype, "aborted");
|
|
1267
|
+
return descriptor?.get?.call(signal) === true;
|
|
1268
|
+
}
|
|
1269
|
+
catch {
|
|
1270
|
+
return true;
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
function timestamp(value) {
|
|
1274
|
+
if (!Number.isSafeInteger(value) || value < 0 || value > MAX_DEADLINE_AT)
|
|
1275
|
+
throw invalidOptions();
|
|
1276
|
+
const instant = new Date(value).toISOString();
|
|
1277
|
+
if (!INSTANT_PATTERN.test(instant))
|
|
1278
|
+
throw invalidOptions();
|
|
1279
|
+
return instant;
|
|
1280
|
+
}
|
|
1281
|
+
function preparedTimestamp(prepared, notBefore) {
|
|
1282
|
+
const value = prepared.now();
|
|
1283
|
+
if (prepared.clockFaulted()) {
|
|
1284
|
+
throw new ArtifactTransferError("operation_clock_invalid", "Operation clock became invalid.");
|
|
1285
|
+
}
|
|
1286
|
+
const instant = timestamp(value);
|
|
1287
|
+
if (notBefore !== undefined && (!isTimestamp(notBefore) || Date.parse(instant) < Date.parse(notBefore))) {
|
|
1288
|
+
throw new ArtifactTransferError("operation_clock_invalid", "Operation clock moved before the durable intent.");
|
|
1289
|
+
}
|
|
1290
|
+
return instant;
|
|
1291
|
+
}
|
|
1292
|
+
function isTimestamp(value) {
|
|
1293
|
+
if (typeof value !== "string" || !INSTANT_PATTERN.test(value))
|
|
1294
|
+
return false;
|
|
1295
|
+
const parsed = Date.parse(value);
|
|
1296
|
+
return Number.isFinite(parsed) && new Date(parsed).toISOString() === value;
|
|
1297
|
+
}
|
|
1298
|
+
function isArtifactStatus(value) {
|
|
1299
|
+
return value === "transferred" || value === "partial" || value === "blocked";
|
|
1300
|
+
}
|
|
1301
|
+
function isAbsolutePath(value) {
|
|
1302
|
+
return resolve(value) === value || value.startsWith("/");
|
|
1303
|
+
}
|
|
1304
|
+
function byteLength(value) {
|
|
1305
|
+
return Buffer.byteLength(value, "utf8");
|
|
1306
|
+
}
|
|
1307
|
+
function invalidOptions() {
|
|
1308
|
+
return new ArtifactTransferError("invalid_options", "Artifact transfer options are invalid.");
|
|
1309
|
+
}
|
|
1310
|
+
function freezeRecord(value) {
|
|
1311
|
+
return Object.freeze(value);
|
|
1312
|
+
}
|