codex-chatgpt-control 0.5.1-alpha.2 → 0.5.1-alpha.3
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 +15 -0
- package/README.md +27 -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/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 +155 -1
- package/contracts/v1/parity-suite.json +96 -1
- 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 +43897 -9094
- package/dist/codex-chatgpt-control.bundle.mjs +46360 -10262
- 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/downloads.d.ts +2 -0
- package/dist/src/client.d.ts +38 -0
- package/dist/src/client.js +1222 -59
- package/dist/src/commands/configuration.js +13 -7
- package/dist/src/commands/doctor.d.ts +1 -1
- package/dist/src/commands/doctor.js +26 -3
- package/dist/src/commands/experience.js +35 -0
- package/dist/src/commands/files.js +4 -27
- package/dist/src/commands/modes.js +49 -17
- package/dist/src/commands/power-discovery.d.ts +130 -0
- package/dist/src/commands/power-discovery.js +627 -0
- package/dist/src/commands/project-sources.js +325 -31
- package/dist/src/commands/sequence.js +59 -32
- package/dist/src/commands/session.js +7 -2
- package/dist/src/commands/work.js +31 -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 +161 -0
- package/dist/src/operations/browser-adapter.js +2023 -0
- package/dist/src/operations/browser-observation.d.ts +110 -0
- package/dist/src/operations/browser-observation.js +1208 -0
- package/dist/src/operations/browser-target.d.ts +103 -0
- package/dist/src/operations/browser-target.js +489 -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 +1150 -0
- package/dist/src/operations/client.d.ts +138 -0
- package/dist/src/operations/client.js +1143 -0
- package/dist/src/operations/collector.d.ts +195 -0
- package/dist/src/operations/collector.js +1039 -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 +22 -0
- package/dist/src/operations/index.js +22 -0
- package/dist/src/operations/journal.d.ts +142 -0
- package/dist/src/operations/journal.js +1931 -0
- package/dist/src/operations/production-attachments.d.ts +116 -0
- package/dist/src/operations/production-attachments.js +1018 -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 +71 -0
- package/dist/src/operations/production-chatgpt-attachments.js +1563 -0
- package/dist/src/operations/production-configuration.d.ts +42 -0
- package/dist/src/operations/production-configuration.js +1363 -0
- package/dist/src/operations/production-primitives.d.ts +53 -0
- package/dist/src/operations/production-primitives.js +1073 -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 +118 -0
- package/dist/src/operations/runtime-adapter.js +786 -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 +281 -0
- package/dist/src/operations/service.js +2810 -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/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 +39 -0
- package/dist/src/runtime/coordinated-page.js +634 -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/untrusted-output.js +2 -1
- package/dist/src/scripts/backend-server.js +4 -1
- package/dist/src/scripts/live-smoke/harness.js +79 -7
- package/dist/src/scripts/live-smoke/scenarios.d.ts +28 -0
- package/dist/src/scripts/live-smoke/scenarios.js +110 -18
- 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 +1 -1
- package/references/2026-08-16-transactional-operations.md +459 -0
- package/references/agents-runner.md +34 -0
- package/references/backend-protocol.md +73 -0
- package/references/python-parity.md +72 -0
- package/references/responses-adapter.md +19 -0
- package/references/streaming.md +6 -0
|
@@ -0,0 +1,2023 @@
|
|
|
1
|
+
import { getProcessTabCoordinator } from "../runtime/tab-coordinator.js";
|
|
2
|
+
import { OperationRuntimeContext } from "../runtime/operation-context.js";
|
|
3
|
+
import { observeBrowserPage } from "./browser-observation.js";
|
|
4
|
+
import { bindBrowserTarget } from "./browser-target.js";
|
|
5
|
+
import { canonicalJson } from "./canonical.js";
|
|
6
|
+
import { executePreparedSendOnce, prepareSendOnce, recoverSendOnce, runSendOnce, verifyPreparedSendOnce } from "./send-once.js";
|
|
7
|
+
import { revalidateOperationFile } from "./file-identity.js";
|
|
8
|
+
import { CONTROL_COORDINATOR_SCHEMA_VERSION, CONTROL_POSTCONDITION_RETRY_POLICY } from "./control.js";
|
|
9
|
+
import { transferOperationArtifact } from "./artifact-transfer.js";
|
|
10
|
+
export class OperationBrowserAdapterError extends Error {
|
|
11
|
+
code;
|
|
12
|
+
constructor(code) {
|
|
13
|
+
super(code === "input_file_changed"
|
|
14
|
+
? "An operation input file changed before the file handoff; no browser handoff was attempted."
|
|
15
|
+
: "The operation browser adapter could not prove the requested browser action safely.");
|
|
16
|
+
this.name = "OperationBrowserAdapterError";
|
|
17
|
+
this.code = code;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const DIGEST_PATTERN = /^hmac-sha256:[0-9a-f]{64}$/u;
|
|
21
|
+
const OPAQUE_ID_PATTERN = /^[A-Za-z0-9._:-]{1,512}$/u;
|
|
22
|
+
const OPAQUE_THREAD_URL_PATTERN = /^https:\/\/opaque\.invalid\/thread\/[0-9a-f]{64}$/u;
|
|
23
|
+
const CLAIM_EVIDENCE_DIGEST_DOMAIN = "codex-chatgpt-control/tab-claim-evidence/v1";
|
|
24
|
+
const MAX_DEADLINE_AT = Date.UTC(2100, 0, 1);
|
|
25
|
+
const DEFAULT_TRANSACTION_TIMEOUT_MS = 30_000;
|
|
26
|
+
const MAX_TRANSACTION_TIMEOUT_MS = 120_000;
|
|
27
|
+
function isAbsolutePath(value) {
|
|
28
|
+
return value.startsWith("/")
|
|
29
|
+
|| /^[A-Za-z]:[\\/]/u.test(value)
|
|
30
|
+
|| value.startsWith("\\\\");
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Build the operation browser adapter over one captured page.
|
|
34
|
+
*
|
|
35
|
+
* The adapter is deliberately a composition layer: journal persistence stays
|
|
36
|
+
* in OperationService, browser observations are supplied by narrow injected
|
|
37
|
+
* primitives (or the conservative browser-observation default), and every
|
|
38
|
+
* non-repeatable call is made at one explicit site. No method reads
|
|
39
|
+
* RuntimeEnv.page and no method retries a browser mutation.
|
|
40
|
+
*/
|
|
41
|
+
export function createOperationBrowserAdapter(options) {
|
|
42
|
+
options = normalizeAdapterOptions(options);
|
|
43
|
+
const page = options.page;
|
|
44
|
+
const runtimeCapture = options.runtimeContext?.capture();
|
|
45
|
+
if (runtimeCapture !== undefined && runtimeCapture.page !== page) {
|
|
46
|
+
throw new OperationBrowserAdapterError("page_affinity_mismatch");
|
|
47
|
+
}
|
|
48
|
+
const coordinator = options.coordinator ?? getProcessTabCoordinator();
|
|
49
|
+
const transactionTimeoutMs = options.transactionTimeoutMs ?? DEFAULT_TRANSACTION_TIMEOUT_MS;
|
|
50
|
+
const bindings = new Map();
|
|
51
|
+
const collectorContexts = new Map();
|
|
52
|
+
let recoveryPromise;
|
|
53
|
+
const ensureRecovered = async (operationId, requestDigest, surface, signal) => {
|
|
54
|
+
const recovery = options.recovery;
|
|
55
|
+
if (recovery === undefined) {
|
|
56
|
+
throw new OperationBrowserAdapterError("target_binding_mismatch");
|
|
57
|
+
}
|
|
58
|
+
assertNativeAbortSignal(signal);
|
|
59
|
+
if (operationId !== recovery.operationId
|
|
60
|
+
|| requestDigest !== recovery.requestDigest
|
|
61
|
+
|| surface !== recovery.surface) {
|
|
62
|
+
throw new OperationBrowserAdapterError("target_binding_mismatch");
|
|
63
|
+
}
|
|
64
|
+
recoveryPromise ??= hydrateRecoveredTarget(options, recovery, page, coordinator);
|
|
65
|
+
const binding = await recoveryPromise;
|
|
66
|
+
bindings.set(recovery.operationId, binding);
|
|
67
|
+
return binding;
|
|
68
|
+
};
|
|
69
|
+
const resolveTarget = async (request) => {
|
|
70
|
+
if (options.recovery !== undefined) {
|
|
71
|
+
try {
|
|
72
|
+
const binding = await ensureRecovered(request.operationId, request.requestDigest, request.surface, request.signal);
|
|
73
|
+
return Object.freeze({ target: binding.target });
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
if (error instanceof OperationBrowserAdapterError)
|
|
77
|
+
throw error;
|
|
78
|
+
throw new OperationBrowserAdapterError(errorCode(error));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
let probe;
|
|
82
|
+
try {
|
|
83
|
+
probe = await resolveProbe(options, request);
|
|
84
|
+
assertProbePage(page, probe.page);
|
|
85
|
+
if (request.target.type === "new") {
|
|
86
|
+
if (probe.targetLifecycle !== undefined && probe.targetLifecycle !== "new_pending") {
|
|
87
|
+
throw new OperationBrowserAdapterError("target_binding_mismatch");
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
else if (probe.targetLifecycle === "new_pending") {
|
|
91
|
+
throw new OperationBrowserAdapterError("target_binding_mismatch");
|
|
92
|
+
}
|
|
93
|
+
assertStaticTarget(request.target, probe.evidence, options.resolveTargetEvidence !== undefined);
|
|
94
|
+
if (runtimeCapture !== undefined) {
|
|
95
|
+
assertRuntimeAffinity(runtimeCapture, page, probe);
|
|
96
|
+
}
|
|
97
|
+
const owner = Object.freeze({
|
|
98
|
+
...options.owner,
|
|
99
|
+
operationId: request.operationId
|
|
100
|
+
});
|
|
101
|
+
const input = {
|
|
102
|
+
page,
|
|
103
|
+
evidence: probe.evidence,
|
|
104
|
+
...(request.target.type === "new"
|
|
105
|
+
? { targetLifecycle: "new_pending" }
|
|
106
|
+
: probe.targetLifecycle === undefined ? {} : { targetLifecycle: probe.targetLifecycle }),
|
|
107
|
+
...((probe.newTargetAnchorDigest ?? options.newTargetAnchorDigest) === undefined
|
|
108
|
+
? {}
|
|
109
|
+
: { newTargetAnchorDigest: probe.newTargetAnchorDigest ?? options.newTargetAnchorDigest }),
|
|
110
|
+
...((probe.blankTaskEvidenceDigest ?? options.blankTaskEvidenceDigest) === undefined
|
|
111
|
+
? {}
|
|
112
|
+
: { blankTaskEvidenceDigest: probe.blankTaskEvidenceDigest ?? options.blankTaskEvidenceDigest }),
|
|
113
|
+
...(probe.authoritativeClaim === undefined ? {} : { authoritativeClaim: probe.authoritativeClaim }),
|
|
114
|
+
capabilities: {
|
|
115
|
+
...(options.capabilities ?? {}),
|
|
116
|
+
...(probe.capabilities ?? {})
|
|
117
|
+
},
|
|
118
|
+
evidenceDigest: options.evidenceDigest,
|
|
119
|
+
owner,
|
|
120
|
+
coordinator
|
|
121
|
+
};
|
|
122
|
+
const binding = bindBrowserTarget(input);
|
|
123
|
+
const previous = bindings.get(request.operationId);
|
|
124
|
+
if (previous !== undefined && canonicalJson(previous.target) !== canonicalJson(binding.target)) {
|
|
125
|
+
throw new OperationBrowserAdapterError("target_binding_mismatch");
|
|
126
|
+
}
|
|
127
|
+
bindings.set(request.operationId, binding);
|
|
128
|
+
return Object.freeze({ target: binding.target });
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
if (error instanceof OperationBrowserAdapterError)
|
|
132
|
+
throw error;
|
|
133
|
+
throw new OperationBrowserAdapterError(errorCode(error));
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
const observeStaging = async (request) => {
|
|
137
|
+
const binding = bindingFor(bindings, request.operationId, request.targetBindingDigest);
|
|
138
|
+
if (binding === undefined)
|
|
139
|
+
return unavailableStage("target");
|
|
140
|
+
try {
|
|
141
|
+
return await runReadTransaction(binding, request.operationId, async (context) => {
|
|
142
|
+
if (options.submission?.observeStaging === undefined)
|
|
143
|
+
return unavailableStage("unknown");
|
|
144
|
+
return await options.submission.observeStaging(request, context.page, context.target);
|
|
145
|
+
}, options.observeCurrentTarget, options.evidenceDigest, { timeoutMs: transactionTimeoutMs });
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return unavailableStage("target");
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
const executeFileHandoffOnce = async (request) => {
|
|
152
|
+
const binding = bindingFor(bindings, request.operationId, request.targetBindingDigest);
|
|
153
|
+
if (binding === undefined)
|
|
154
|
+
return { status: "not_satisfied", blockerCode: "target_binding_mismatch" };
|
|
155
|
+
const files = matchFileManifest(options.files, options.fileManifestDigest, request.manifest);
|
|
156
|
+
if (files === undefined || options.submission?.handoffFiles === undefined) {
|
|
157
|
+
return { status: "not_satisfied", blockerCode: "attachment_manifest_mismatch" };
|
|
158
|
+
}
|
|
159
|
+
// Target proof gets its own short read transaction. Hashing/revalidation
|
|
160
|
+
// stays outside the actor, so a large file cannot monopolize the tab.
|
|
161
|
+
try {
|
|
162
|
+
await runReadTransaction(binding, request.operationId, async () => undefined, options.observeCurrentTarget, options.evidenceDigest, boundedRequest(request.signal, request.deadlineAt, transactionTimeoutMs, "operation-file-handoff-target"));
|
|
163
|
+
for (const identity of files) {
|
|
164
|
+
if (request.signal?.aborted || request.deadlineAt !== undefined && Date.now() >= request.deadlineAt) {
|
|
165
|
+
// A durable file-handoff intent already exists by the time this port
|
|
166
|
+
// is called. Even a cancellation before the provider mutation is
|
|
167
|
+
// therefore observation-only; never let a caller retry the handoff.
|
|
168
|
+
return { status: "uncertain", quarantine: "caller" };
|
|
169
|
+
}
|
|
170
|
+
try {
|
|
171
|
+
await revalidateOperationFile(identity);
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return { status: "not_satisfied", blockerCode: "input_file_changed" };
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (request.signal?.aborted || request.deadlineAt !== undefined && Date.now() >= request.deadlineAt) {
|
|
178
|
+
return { status: "uncertain", quarantine: "caller" };
|
|
179
|
+
}
|
|
180
|
+
// This is the sole file-handoff mutation site. There is no catch/retry
|
|
181
|
+
// branch that invokes handoffFiles a second time.
|
|
182
|
+
return await runMutationTransaction(binding, request.operationId, async (context) => {
|
|
183
|
+
const providerRequest = Object.freeze({
|
|
184
|
+
...request,
|
|
185
|
+
signal: context.acquisition.signal,
|
|
186
|
+
...(context.acquisition.timing.deadlineAt === undefined
|
|
187
|
+
? (request.deadlineAt === undefined ? {} : { deadlineAt: request.deadlineAt })
|
|
188
|
+
: { deadlineAt: context.acquisition.timing.deadlineAt })
|
|
189
|
+
});
|
|
190
|
+
const result = await options.submission.handoffFiles(providerRequest, files, context.page, context.target);
|
|
191
|
+
// A provider may finish with a success-looking acknowledgement after
|
|
192
|
+
// the coordinator has already aborted its in-flight mutation. The
|
|
193
|
+
// acknowledgement is no longer safe to treat as exact; recovery must
|
|
194
|
+
// observe the live attachment surface and may not repeat the handoff.
|
|
195
|
+
return context.acquisition.signal.aborted
|
|
196
|
+
? { status: "uncertain", quarantine: "caller" }
|
|
197
|
+
: result;
|
|
198
|
+
}, options.observeCurrentTarget, options.evidenceDigest, boundedRequest(request.signal, request.deadlineAt, transactionTimeoutMs, "operation-file-handoff", "mutation"));
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
return request.signal?.aborted || request.deadlineAt !== undefined && Date.now() >= request.deadlineAt
|
|
202
|
+
? { status: "uncertain", quarantine: "caller" }
|
|
203
|
+
: { status: "uncertain", quarantine: "provider" };
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
const observeAttachments = async (request) => {
|
|
207
|
+
const binding = bindingFor(bindings, request.operationId, request.targetBindingDigest);
|
|
208
|
+
if (binding === undefined)
|
|
209
|
+
return { status: "unavailable" };
|
|
210
|
+
try {
|
|
211
|
+
return await runReadTransaction(binding, request.operationId, async (context) => {
|
|
212
|
+
if (options.submission?.observeAttachments === undefined)
|
|
213
|
+
return { status: "unavailable" };
|
|
214
|
+
return await options.submission.observeAttachments(request, context.page, context.target);
|
|
215
|
+
}, options.observeCurrentTarget, options.evidenceDigest, { timeoutMs: transactionTimeoutMs });
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
return { status: "unavailable" };
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
/**
|
|
222
|
+
* Send's four phase ports deliberately share only an operation-local abort
|
|
223
|
+
* controller. The controller is linked to both the caller and whichever
|
|
224
|
+
* coordinator transaction is currently executing. This lets the provider
|
|
225
|
+
* see the coordinator's cancellation without making the durable service
|
|
226
|
+
* aware of a page, actor, or bridge object.
|
|
227
|
+
*/
|
|
228
|
+
const prepareSend = async (request) => {
|
|
229
|
+
const binding = bindingFor(bindings, request.operationId, request.expected.targetBindingDigest);
|
|
230
|
+
if (binding === undefined)
|
|
231
|
+
return blockedPrepareSend("target_binding_mismatch");
|
|
232
|
+
const baseObservers = options.submission?.sendObservers;
|
|
233
|
+
if (baseObservers === undefined)
|
|
234
|
+
return blockedPrepareSend("target_evidence_unavailable");
|
|
235
|
+
const linked = createLinkedAbortController(request.signal);
|
|
236
|
+
const send = createPhaseSendObservers(binding, request.operationId, baseObservers, linked.controller, options.observeCurrentTarget, options.evidenceDigest, transactionTimeoutMs);
|
|
237
|
+
try {
|
|
238
|
+
const result = await prepareSendOnce({
|
|
239
|
+
page,
|
|
240
|
+
operationId: request.operationId,
|
|
241
|
+
requestDigest: request.requestDigest,
|
|
242
|
+
surface: request.surface,
|
|
243
|
+
actionId: request.actionId,
|
|
244
|
+
expected: request.expected,
|
|
245
|
+
observers: send.observers,
|
|
246
|
+
signal: linked.controller.signal,
|
|
247
|
+
...(request.deadlineAt === undefined ? {} : { deadlineAt: request.deadlineAt }),
|
|
248
|
+
transaction: callback => runSendTransaction(binding, request.operationId, linked.controller, options.observeCurrentTarget, options.evidenceDigest, request.deadlineAt, transactionTimeoutMs, "operation-send-prepare", "read", context => send.withContext(context, callback))
|
|
249
|
+
});
|
|
250
|
+
if (result.status === "blocked")
|
|
251
|
+
return resultToPrepareSend(result.result);
|
|
252
|
+
try {
|
|
253
|
+
return {
|
|
254
|
+
status: "prepared",
|
|
255
|
+
prepared: submissionPreparedFromSendOnce(result.prepared, request.expected)
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
return blockedPrepareSend("port_protocol_violation");
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
return blockedPrepareSend(phaseBlocker(linked.controller.signal, request.deadlineAt));
|
|
264
|
+
}
|
|
265
|
+
finally {
|
|
266
|
+
linked.cleanup();
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
const executePreparedSend = async (request) => {
|
|
270
|
+
const binding = bindingFor(bindings, request.operationId, request.expected.targetBindingDigest);
|
|
271
|
+
if (binding === undefined)
|
|
272
|
+
return blockedExecuteSend("target_binding_mismatch");
|
|
273
|
+
const baseObservers = options.submission?.sendObservers;
|
|
274
|
+
if (baseObservers === undefined)
|
|
275
|
+
return blockedExecuteSend("target_evidence_unavailable");
|
|
276
|
+
let prepared;
|
|
277
|
+
try {
|
|
278
|
+
prepared = normalizeSubmissionPreparedSend(request.prepared, request.expected, {
|
|
279
|
+
operationId: request.operationId,
|
|
280
|
+
requestDigest: request.requestDigest,
|
|
281
|
+
surface: request.surface,
|
|
282
|
+
actionId: request.actionId
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
return blockedExecuteSend("port_protocol_violation");
|
|
287
|
+
}
|
|
288
|
+
const linked = createLinkedAbortController(request.signal);
|
|
289
|
+
const send = createPhaseSendObservers(binding, request.operationId, baseObservers, linked.controller, options.observeCurrentTarget, options.evidenceDigest, transactionTimeoutMs);
|
|
290
|
+
try {
|
|
291
|
+
const result = await executePreparedSendOnce({
|
|
292
|
+
page,
|
|
293
|
+
prepared: prepared.prepared,
|
|
294
|
+
observers: send.observers,
|
|
295
|
+
signal: linked.controller.signal,
|
|
296
|
+
...(request.deadlineAt === undefined ? {} : { deadlineAt: request.deadlineAt }),
|
|
297
|
+
transaction: callback => runSendTransaction(binding, request.operationId, linked.controller, options.observeCurrentTarget, options.evidenceDigest, request.deadlineAt, transactionTimeoutMs, "operation-send-execute", "mutation", context => send.withContext(context, callback))
|
|
298
|
+
});
|
|
299
|
+
if (result.status === "blocked" || result.status === "uncertain")
|
|
300
|
+
return result;
|
|
301
|
+
return {
|
|
302
|
+
status: result.status,
|
|
303
|
+
activation: result.activation,
|
|
304
|
+
mutationMayHaveOccurred: true
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
return {
|
|
309
|
+
status: "uncertain",
|
|
310
|
+
result: { status: "uncertain", quarantine: "caller" }
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
finally {
|
|
314
|
+
linked.cleanup();
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
const verifyPreparedSend = async (request) => {
|
|
318
|
+
const binding = bindingFor(bindings, request.operationId, request.expected.targetBindingDigest);
|
|
319
|
+
if (binding === undefined)
|
|
320
|
+
return { status: "blocked", blockerCode: "target_binding_mismatch" };
|
|
321
|
+
const baseObservers = options.submission?.sendObservers;
|
|
322
|
+
if (baseObservers === undefined)
|
|
323
|
+
return { status: "uncertain", quarantine: "provider" };
|
|
324
|
+
let prepared;
|
|
325
|
+
try {
|
|
326
|
+
prepared = normalizeSubmissionPreparedSend(request.prepared, request.expected, {
|
|
327
|
+
operationId: request.operationId,
|
|
328
|
+
requestDigest: request.requestDigest,
|
|
329
|
+
surface: request.surface,
|
|
330
|
+
actionId: request.actionId
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
catch {
|
|
334
|
+
return { status: "uncertain", quarantine: "caller" };
|
|
335
|
+
}
|
|
336
|
+
const linked = createLinkedAbortController(request.signal);
|
|
337
|
+
const send = createPhaseSendObservers(binding, request.operationId, baseObservers, linked.controller, options.observeCurrentTarget, options.evidenceDigest, transactionTimeoutMs);
|
|
338
|
+
try {
|
|
339
|
+
const result = await verifyPreparedSendOnce({
|
|
340
|
+
page,
|
|
341
|
+
prepared: prepared.prepared,
|
|
342
|
+
observers: send.observers,
|
|
343
|
+
activation: request.activation,
|
|
344
|
+
mutationMayHaveOccurred: request.mutationMayHaveOccurred,
|
|
345
|
+
signal: linked.controller.signal,
|
|
346
|
+
...(request.deadlineAt === undefined ? {} : { deadlineAt: request.deadlineAt })
|
|
347
|
+
});
|
|
348
|
+
markEstablished(binding, result);
|
|
349
|
+
return result;
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
return { status: "uncertain", quarantine: "caller" };
|
|
353
|
+
}
|
|
354
|
+
finally {
|
|
355
|
+
linked.cleanup();
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
const recoverSend = async (request) => {
|
|
359
|
+
let binding = bindingFor(bindings, request.operationId, request.expected.targetBindingDigest);
|
|
360
|
+
if (binding === undefined && options.recovery !== undefined) {
|
|
361
|
+
try {
|
|
362
|
+
binding = await ensureRecovered(request.operationId, request.requestDigest, request.surface, request.signal ?? options.recovery.signal);
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
return { status: "blocked", blockerCode: "target_binding_mismatch" };
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
if (binding === undefined)
|
|
369
|
+
return { status: "blocked", blockerCode: "target_binding_mismatch" };
|
|
370
|
+
const baseObservers = options.submission?.sendObservers;
|
|
371
|
+
if (baseObservers === undefined)
|
|
372
|
+
return { status: "uncertain", quarantine: "provider" };
|
|
373
|
+
const linked = createLinkedAbortController(request.signal);
|
|
374
|
+
const send = createPhaseSendObservers(binding, request.operationId, baseObservers, linked.controller, options.observeCurrentTarget, options.evidenceDigest, transactionTimeoutMs);
|
|
375
|
+
try {
|
|
376
|
+
const result = await recoverSendOnce({
|
|
377
|
+
page,
|
|
378
|
+
operationId: request.operationId,
|
|
379
|
+
requestDigest: request.requestDigest,
|
|
380
|
+
surface: request.surface,
|
|
381
|
+
actionId: request.actionId,
|
|
382
|
+
expected: request.expected,
|
|
383
|
+
durableBaseline: request.durableBaseline,
|
|
384
|
+
observers: send.observers,
|
|
385
|
+
signal: linked.controller.signal,
|
|
386
|
+
...(request.deadlineAt === undefined ? {} : { deadlineAt: request.deadlineAt })
|
|
387
|
+
});
|
|
388
|
+
markEstablished(binding, result);
|
|
389
|
+
return result;
|
|
390
|
+
}
|
|
391
|
+
catch {
|
|
392
|
+
return { status: "uncertain", quarantine: "caller" };
|
|
393
|
+
}
|
|
394
|
+
finally {
|
|
395
|
+
linked.cleanup();
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
const executeFinalTabTransaction = async (request) => {
|
|
399
|
+
const binding = bindingFor(bindings, request.operationId, request.expected.targetBindingDigest);
|
|
400
|
+
if (binding === undefined) {
|
|
401
|
+
return { status: "blocked", blockerCode: "target_binding_mismatch" };
|
|
402
|
+
}
|
|
403
|
+
const baseObservers = options.submission?.sendObservers;
|
|
404
|
+
if (baseObservers === undefined)
|
|
405
|
+
return { status: "blocked", blockerCode: "target_evidence_unavailable" };
|
|
406
|
+
// The precondition read is part of the short activation transaction. Each
|
|
407
|
+
// postcondition probe, by contrast, reacquires the same tab actor for one
|
|
408
|
+
// bounded read and releases it before SendOnce waits for the next probe.
|
|
409
|
+
// The provider primitive is intentionally one-shot: it must not poll or
|
|
410
|
+
// sleep while this callback owns the actor.
|
|
411
|
+
const observers = {
|
|
412
|
+
observePrecondition: baseObservers.observePrecondition,
|
|
413
|
+
observePostcondition: async (postconditionRequest) => {
|
|
414
|
+
const probe = await runReadTransaction(binding, request.operationId, context => baseObservers.observePostcondition({
|
|
415
|
+
...postconditionRequest,
|
|
416
|
+
page: context.page,
|
|
417
|
+
// A probe is owned by this individual short read transaction, not
|
|
418
|
+
// by the earlier activation transaction. Provider code must see
|
|
419
|
+
// the current actor's cancellation/deadline so an in-flight read
|
|
420
|
+
// cannot outlive its coordinator quarantine unnoticed.
|
|
421
|
+
signal: context.acquisition.signal,
|
|
422
|
+
...(context.acquisition.timing.deadlineAt === undefined
|
|
423
|
+
? {}
|
|
424
|
+
: { deadlineAt: context.acquisition.timing.deadlineAt })
|
|
425
|
+
}), options.observeCurrentTarget, options.evidenceDigest, boundedRequest(postconditionRequest.signal, postconditionRequest.deadlineAt, transactionTimeoutMs, "operation-send-postcondition"), true);
|
|
426
|
+
// Provider observers are one-shot reads. A missing/temporarily
|
|
427
|
+
// ambiguous delta is a retryable observation miss; no browser
|
|
428
|
+
// mutation is ever retried here. Definitive blockers stay terminal.
|
|
429
|
+
if (isPlainPostconditionResult(probe)
|
|
430
|
+
&& probe.status === "blocked"
|
|
431
|
+
&& (probe.blockerCode === "target_evidence_unavailable" || probe.blockerCode === "ambiguous_submit")) {
|
|
432
|
+
return { result: probe, retryable: true };
|
|
433
|
+
}
|
|
434
|
+
return probe;
|
|
435
|
+
},
|
|
436
|
+
...(baseObservers.sleep === undefined ? {} : { sleep: baseObservers.sleep }),
|
|
437
|
+
...(baseObservers.maxPostconditionAttempts === undefined ? {} : { maxPostconditionAttempts: baseObservers.maxPostconditionAttempts }),
|
|
438
|
+
...(baseObservers.postconditionIntervalMs === undefined ? {} : { postconditionIntervalMs: baseObservers.postconditionIntervalMs }),
|
|
439
|
+
...(baseObservers.postconditionTimeoutMs === undefined ? {} : { postconditionTimeoutMs: baseObservers.postconditionTimeoutMs })
|
|
440
|
+
};
|
|
441
|
+
const operationController = new AbortController();
|
|
442
|
+
const sourceAbortCleanup = [];
|
|
443
|
+
if (request.signal !== undefined) {
|
|
444
|
+
const abortFromCaller = () => operationController.abort(request.signal?.reason);
|
|
445
|
+
if (request.signal.aborted)
|
|
446
|
+
operationController.abort(request.signal.reason);
|
|
447
|
+
else {
|
|
448
|
+
request.signal.addEventListener("abort", abortFromCaller, { once: true });
|
|
449
|
+
sourceAbortCleanup.push(() => request.signal?.removeEventListener("abort", abortFromCaller));
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
try {
|
|
453
|
+
const transaction = async (callback) => await binding.withTabTransaction(boundedRequest(request.signal, request.deadlineAt, transactionTimeoutMs, request.mode === "mutate_once" ? "operation-send" : "operation-send-observe", request.mode === "mutate_once" ? "mutation" : "read"), async (context) => {
|
|
454
|
+
const abortFromCoordinator = () => operationController.abort(context.acquisition.signal.reason);
|
|
455
|
+
if (context.acquisition.signal.aborted)
|
|
456
|
+
operationController.abort(context.acquisition.signal.reason);
|
|
457
|
+
else
|
|
458
|
+
context.acquisition.signal.addEventListener("abort", abortFromCoordinator, { once: true });
|
|
459
|
+
try {
|
|
460
|
+
const current = await readCurrentTarget(binding, request.operationId, context.page, options.observeCurrentTarget, options.evidenceDigest, context.acquisition.signal, context.acquisition.timing.deadlineAt);
|
|
461
|
+
binding.assertCurrent(current.evidence, current.authoritativeClaim);
|
|
462
|
+
return await callback();
|
|
463
|
+
}
|
|
464
|
+
finally {
|
|
465
|
+
context.acquisition.signal.removeEventListener("abort", abortFromCoordinator);
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
const result = await runSendOnce({
|
|
469
|
+
page,
|
|
470
|
+
operationId: request.operationId,
|
|
471
|
+
requestDigest: request.requestDigest,
|
|
472
|
+
surface: request.surface,
|
|
473
|
+
actionId: request.actionId,
|
|
474
|
+
mode: request.mode,
|
|
475
|
+
expected: request.expected,
|
|
476
|
+
observers,
|
|
477
|
+
signal: operationController.signal,
|
|
478
|
+
...(request.deadlineAt === undefined ? { deadlineAt: MAX_DEADLINE_AT } : { deadlineAt: request.deadlineAt }),
|
|
479
|
+
...(request.persistPreSendBaseline === undefined
|
|
480
|
+
? {}
|
|
481
|
+
: { persistPreSendBaseline: request.persistPreSendBaseline }),
|
|
482
|
+
...(request.durableBaseline === undefined
|
|
483
|
+
? {}
|
|
484
|
+
: { durableBaseline: request.durableBaseline }),
|
|
485
|
+
transaction
|
|
486
|
+
});
|
|
487
|
+
if (binding.markTargetEstablished !== undefined
|
|
488
|
+
&& (result.status === "submitted" || result.status === "already_submitted")
|
|
489
|
+
&& result.targetEstablishment !== undefined) {
|
|
490
|
+
binding.markTargetEstablished({
|
|
491
|
+
conversationId: result.targetEstablishment.conversationId,
|
|
492
|
+
canonicalThreadUrl: result.targetEstablishment.canonicalThreadUrl
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
return result;
|
|
496
|
+
}
|
|
497
|
+
catch {
|
|
498
|
+
return request.mode === "mutate_once"
|
|
499
|
+
? { status: "uncertain", quarantine: "provider" }
|
|
500
|
+
: { status: "blocked", blockerCode: "target_evidence_unavailable" };
|
|
501
|
+
}
|
|
502
|
+
finally {
|
|
503
|
+
for (const cleanup of sourceAbortCleanup)
|
|
504
|
+
cleanup();
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
const readContext = async (request) => {
|
|
508
|
+
if (options.recovery !== undefined) {
|
|
509
|
+
await ensureRecovered(request.operationId, request.requestDigest, options.recovery.surface, request.signal);
|
|
510
|
+
}
|
|
511
|
+
const binding = bindingFor(bindings, request.operationId, request.targetBindingDigest);
|
|
512
|
+
if (binding === undefined)
|
|
513
|
+
throw new OperationBrowserAdapterError("target_binding_mismatch");
|
|
514
|
+
if (options.collector?.readContext === undefined) {
|
|
515
|
+
throw new OperationBrowserAdapterError("unsupported_browser_primitive");
|
|
516
|
+
}
|
|
517
|
+
try {
|
|
518
|
+
const context = await runReadTransaction(binding, request.operationId, transaction => options.collector.readContext(request, transaction.page, transaction.target), options.observeCurrentTarget, options.evidenceDigest, {
|
|
519
|
+
signal: request.signal,
|
|
520
|
+
timeoutMs: transactionTimeoutMs,
|
|
521
|
+
label: "operation-collect-context"
|
|
522
|
+
});
|
|
523
|
+
collectorContexts.set(request.operationId, context);
|
|
524
|
+
return context;
|
|
525
|
+
}
|
|
526
|
+
catch {
|
|
527
|
+
throw new OperationBrowserAdapterError("target_evidence_unavailable");
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
const observe = async (request) => {
|
|
531
|
+
if (options.recovery !== undefined) {
|
|
532
|
+
await ensureRecovered(request.operationId, request.requestDigest, options.recovery.surface, request.signal);
|
|
533
|
+
}
|
|
534
|
+
const binding = bindingFor(bindings, request.operationId, request.targetBindingDigest);
|
|
535
|
+
const context = collectorContexts.get(request.operationId);
|
|
536
|
+
if (binding === undefined || context === undefined)
|
|
537
|
+
throw new OperationBrowserAdapterError("target_binding_mismatch");
|
|
538
|
+
try {
|
|
539
|
+
return await binding.withTabTransaction(boundedRequest(request.signal, request.deadlineAt, transactionTimeoutMs, "operation-collect-observe"), async (transaction) => {
|
|
540
|
+
const current = await readCurrentTarget(binding, request.operationId, transaction.page, options.observeCurrentTarget, options.evidenceDigest, transaction.acquisition.signal, transaction.acquisition.timing.deadlineAt);
|
|
541
|
+
binding.assertCurrent(current.evidence, current.authoritativeClaim);
|
|
542
|
+
const observation = options.collector?.observe === undefined
|
|
543
|
+
? await defaultCollectorObservation(request, transaction, context, options.evidenceDigest, current.authoritativeClaim)
|
|
544
|
+
: await options.collector.observe(request, transaction.page, transaction.target, context);
|
|
545
|
+
// The observation itself carries the exact target. Do not use a
|
|
546
|
+
// page-wide latest turn or a prompt similarity fallback.
|
|
547
|
+
binding.assertCurrent(observation.snapshot.target, current.authoritativeClaim);
|
|
548
|
+
return observation;
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
catch {
|
|
552
|
+
throw new OperationBrowserAdapterError("target_evidence_unavailable");
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
const sleep = async (milliseconds, signal) => {
|
|
556
|
+
if (options.collector?.sleep !== undefined)
|
|
557
|
+
return await options.collector.sleep(milliseconds, signal);
|
|
558
|
+
await sleepOutsideCoordinator(milliseconds, signal);
|
|
559
|
+
};
|
|
560
|
+
const observeTurn = async (request) => {
|
|
561
|
+
if (options.recovery !== undefined) {
|
|
562
|
+
await ensureRecovered(request.operationId, request.parentRequestDigest, options.recovery.surface, request.signal);
|
|
563
|
+
}
|
|
564
|
+
const binding = bindingFor(bindings, request.operationId, request.targetBindingDigest);
|
|
565
|
+
if (binding === undefined)
|
|
566
|
+
return { status: "uncertain", reason: "target_mismatch" };
|
|
567
|
+
if (options.control?.observeTurn === undefined)
|
|
568
|
+
return { status: "uncertain", reason: "unavailable" };
|
|
569
|
+
try {
|
|
570
|
+
return await runReadTransaction(binding, request.operationId, transaction => options.control.observeTurn(request, transaction.page, transaction.target), options.observeCurrentTarget, options.evidenceDigest, boundedRequest(request.signal, request.deadlineAt, transactionTimeoutMs, "operation-control-observe"));
|
|
571
|
+
}
|
|
572
|
+
catch {
|
|
573
|
+
return { status: "uncertain", reason: "unavailable" };
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
const executeControlOnce = async (request) => {
|
|
577
|
+
if (options.recovery !== undefined) {
|
|
578
|
+
await ensureRecovered(request.operationId, request.parentRequestDigest, options.recovery.surface, request.signal);
|
|
579
|
+
}
|
|
580
|
+
const binding = bindingFor(bindings, request.operationId, request.targetBindingDigest);
|
|
581
|
+
if (binding === undefined)
|
|
582
|
+
return { status: "uncertain", blockerCode: "target_binding_mismatch" };
|
|
583
|
+
if (options.control?.executeOnce === undefined)
|
|
584
|
+
return { status: "uncertain", blockerCode: "send_control_unavailable" };
|
|
585
|
+
try {
|
|
586
|
+
return await binding.withTabTransaction(boundedRequest(request.signal, request.deadlineAt, transactionTimeoutMs, `operation-${request.action}`, "control"), async (transaction) => await (async () => {
|
|
587
|
+
const current = await readCurrentTarget(binding, request.operationId, transaction.page, options.observeCurrentTarget, options.evidenceDigest, transaction.acquisition.signal, transaction.acquisition.timing.deadlineAt);
|
|
588
|
+
binding.assertCurrent(current.evidence, current.authoritativeClaim);
|
|
589
|
+
return await options.control.executeOnce(request, transaction.page, transaction.target);
|
|
590
|
+
})());
|
|
591
|
+
}
|
|
592
|
+
catch {
|
|
593
|
+
return { status: "uncertain", blockerCode: "send_control_unavailable" };
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
const observePostcondition = async (request) => {
|
|
597
|
+
if (options.recovery !== undefined) {
|
|
598
|
+
await ensureRecovered(request.operationId, request.parentRequestDigest, options.recovery.surface, request.signal);
|
|
599
|
+
}
|
|
600
|
+
const binding = bindingFor(bindings, request.operationId, request.targetBindingDigest);
|
|
601
|
+
if (binding === undefined)
|
|
602
|
+
return { status: "uncertain", blockerCode: "target_binding_mismatch" };
|
|
603
|
+
if (options.control?.observePostcondition === undefined)
|
|
604
|
+
return { status: "uncertain", blockerCode: "send_control_unavailable" };
|
|
605
|
+
try {
|
|
606
|
+
return await runReadTransaction(binding, request.operationId, transaction => options.control.observePostcondition(request, transaction.page, transaction.target), options.observeCurrentTarget, options.evidenceDigest, boundedRequest(request.signal, request.deadlineAt, transactionTimeoutMs, "operation-control-postcondition"));
|
|
607
|
+
}
|
|
608
|
+
catch {
|
|
609
|
+
return { status: "uncertain", blockerCode: "send_control_unavailable" };
|
|
610
|
+
}
|
|
611
|
+
};
|
|
612
|
+
const getSteerBinding = async (operationId, requestDigest, signal) => await steerBindingFor(bindings, ensureRecovered, options.recovery, operationId, requestDigest, signal);
|
|
613
|
+
/**
|
|
614
|
+
* Work steer is deliberately separate from Stop's compatibility port. A
|
|
615
|
+
* preparation is one short read transaction and returns only the provider's
|
|
616
|
+
* prompt-free prepared capability. The caller persists that capability and
|
|
617
|
+
* its baseline before it can invoke executeSteerPrepared.
|
|
618
|
+
*/
|
|
619
|
+
const prepareSteer = async (request) => {
|
|
620
|
+
const binding = await getSteerBinding(request.parentOperationId, request.parentRequestDigest, request.signal);
|
|
621
|
+
if (binding === undefined)
|
|
622
|
+
return blockedSteerPhase(request, "prepare", "target_binding_mismatch", false, "none");
|
|
623
|
+
const provider = options.control?.prepareSteer;
|
|
624
|
+
if (provider === undefined)
|
|
625
|
+
return blockedSteerPhase(request, "prepare", "backend_unavailable", true, "none");
|
|
626
|
+
try {
|
|
627
|
+
const result = await runReadTransaction(binding, request.parentOperationId, context => provider({
|
|
628
|
+
schemaVersion: request.schemaVersion,
|
|
629
|
+
parentOperationId: request.parentOperationId,
|
|
630
|
+
parentRequestDigest: request.parentRequestDigest,
|
|
631
|
+
parentTargetBindingDigest: request.parentTargetBindingDigest,
|
|
632
|
+
controlActionId: request.controlActionId,
|
|
633
|
+
requestDigest: request.requestDigest,
|
|
634
|
+
expectedAssistantTurnId: request.expectedAssistantTurnId,
|
|
635
|
+
signal: context.acquisition.signal,
|
|
636
|
+
deadlineAt: context.acquisition.timing.deadlineAt ?? request.deadlineAt
|
|
637
|
+
}, context.page, context.target), options.observeCurrentTarget, options.evidenceDigest, boundedRequest(request.signal, request.deadlineAt, transactionTimeoutMs, "operation-control-steer-prepare"));
|
|
638
|
+
return normalizeSteerProviderResult(result, request, "prepare");
|
|
639
|
+
}
|
|
640
|
+
catch (error) {
|
|
641
|
+
return blockedSteerPhase(request, "prepare", phaseFailureCode(request.signal, request.deadlineAt, error), true, "none");
|
|
642
|
+
}
|
|
643
|
+
};
|
|
644
|
+
/**
|
|
645
|
+
* Execute is the only Work-steer mutation site. `runSteerControlTransaction`
|
|
646
|
+
* keeps the callback promise alive until provider settlement even when the
|
|
647
|
+
* coordinator has already rejected the public transaction at its deadline.
|
|
648
|
+
* That quarantine is what prevents a late fill/click from overlapping a
|
|
649
|
+
* subsequent operation on the shared page.
|
|
650
|
+
*/
|
|
651
|
+
const executeSteerPrepared = async (request) => {
|
|
652
|
+
const operationId = request.prepared.parentOperationId;
|
|
653
|
+
const binding = await getSteerBinding(operationId, request.prepared.parentRequestDigest, request.signal);
|
|
654
|
+
if (binding === undefined)
|
|
655
|
+
return blockedSteerPhase(request, "execute_prepared", "target_binding_mismatch", false, "none");
|
|
656
|
+
const provider = options.control?.executeSteerPrepared;
|
|
657
|
+
if (provider === undefined)
|
|
658
|
+
return blockedSteerPhase(request, "execute_prepared", "backend_unavailable", false, "none", request.prepared);
|
|
659
|
+
try {
|
|
660
|
+
const result = await runSteerControlTransaction(binding, operationId, request.signal, request.deadlineAt, transactionTimeoutMs, options.observeCurrentTarget, options.evidenceDigest, context => provider({
|
|
661
|
+
schemaVersion: request.schemaVersion,
|
|
662
|
+
prepared: request.prepared,
|
|
663
|
+
signal: context.acquisition.signal,
|
|
664
|
+
deadlineAt: context.acquisition.timing.deadlineAt ?? request.deadlineAt
|
|
665
|
+
}, context.page, context.target));
|
|
666
|
+
return normalizeSteerProviderResult(result, request, "execute_prepared", request.prepared);
|
|
667
|
+
}
|
|
668
|
+
catch (error) {
|
|
669
|
+
return uncertainSteerPhase(request, "execute_prepared", request.prepared, phaseFailureCode(request.signal, request.deadlineAt, error), "provider");
|
|
670
|
+
}
|
|
671
|
+
};
|
|
672
|
+
/** Verification is a bounded read and releases the tab actor before any
|
|
673
|
+
* caller-level retry or persistence work. */
|
|
674
|
+
const verifySteer = async (request) => {
|
|
675
|
+
const operationId = request.prepared.parentOperationId;
|
|
676
|
+
const binding = await getSteerBinding(operationId, request.prepared.parentRequestDigest, request.signal);
|
|
677
|
+
if (binding === undefined)
|
|
678
|
+
return uncertainSteerPhase(request, "verify", request.prepared, "target_binding_mismatch", "caller");
|
|
679
|
+
const provider = options.control?.verifySteer;
|
|
680
|
+
if (provider === undefined)
|
|
681
|
+
return uncertainSteerPhase(request, "verify", request.prepared, "backend_unavailable", "provider");
|
|
682
|
+
try {
|
|
683
|
+
const result = await runReadTransaction(binding, operationId, context => provider({
|
|
684
|
+
schemaVersion: request.schemaVersion,
|
|
685
|
+
prepared: request.prepared,
|
|
686
|
+
signal: context.acquisition.signal,
|
|
687
|
+
deadlineAt: context.acquisition.timing.deadlineAt ?? request.deadlineAt
|
|
688
|
+
}, context.page, context.target), options.observeCurrentTarget, options.evidenceDigest, boundedRequest(request.signal, request.deadlineAt, transactionTimeoutMs, "operation-control-steer-verify"));
|
|
689
|
+
return normalizeSteerProviderResult(result, request, "verify", request.prepared);
|
|
690
|
+
}
|
|
691
|
+
catch (error) {
|
|
692
|
+
return uncertainSteerPhase(request, "verify", request.prepared, phaseFailureCode(request.signal, request.deadlineAt, error), "caller");
|
|
693
|
+
}
|
|
694
|
+
};
|
|
695
|
+
/**
|
|
696
|
+
* Recovery is observation-only. It may hydrate a durable target binding,
|
|
697
|
+
* but it never calls prepare, execute, composer fill, or Send/click.
|
|
698
|
+
*/
|
|
699
|
+
const recoverSteer = async (request) => {
|
|
700
|
+
const operationId = request.prepared.parentOperationId;
|
|
701
|
+
const binding = await getSteerBinding(operationId, request.prepared.parentRequestDigest, request.signal);
|
|
702
|
+
if (binding === undefined)
|
|
703
|
+
return uncertainSteerPhase(request, "recovery", request.prepared, "target_binding_mismatch", "caller");
|
|
704
|
+
const provider = options.control?.recoverSteer;
|
|
705
|
+
if (provider === undefined)
|
|
706
|
+
return uncertainSteerPhase(request, "recovery", request.prepared, "backend_unavailable", "provider");
|
|
707
|
+
try {
|
|
708
|
+
const result = await runReadTransaction(binding, operationId, context => provider({
|
|
709
|
+
schemaVersion: request.schemaVersion,
|
|
710
|
+
prepared: request.prepared,
|
|
711
|
+
baseline: request.baseline,
|
|
712
|
+
signal: context.acquisition.signal,
|
|
713
|
+
deadlineAt: context.acquisition.timing.deadlineAt ?? request.deadlineAt
|
|
714
|
+
}, context.page, context.target), options.observeCurrentTarget, options.evidenceDigest, boundedRequest(request.signal, request.deadlineAt, transactionTimeoutMs, "operation-control-steer-recovery"));
|
|
715
|
+
return normalizeSteerProviderResult(result, request, "recovery", request.prepared);
|
|
716
|
+
}
|
|
717
|
+
catch (error) {
|
|
718
|
+
return uncertainSteerPhase(request, "recovery", request.prepared, phaseFailureCode(request.signal, request.deadlineAt, error), "caller");
|
|
719
|
+
}
|
|
720
|
+
};
|
|
721
|
+
const submission = Object.freeze({
|
|
722
|
+
observeStaging,
|
|
723
|
+
executeFileHandoffOnce,
|
|
724
|
+
observeAttachments,
|
|
725
|
+
prepareSend,
|
|
726
|
+
executePreparedSend,
|
|
727
|
+
verifyPreparedSend,
|
|
728
|
+
recoverSend,
|
|
729
|
+
executeFinalTabTransaction
|
|
730
|
+
});
|
|
731
|
+
const collector = Object.freeze({ readContext, observe, sleep });
|
|
732
|
+
const control = options.control === undefined
|
|
733
|
+
? undefined
|
|
734
|
+
: Object.freeze({
|
|
735
|
+
observeTurn,
|
|
736
|
+
executeOnce: executeControlOnce,
|
|
737
|
+
observePostcondition,
|
|
738
|
+
postconditionRetry: CONTROL_POSTCONDITION_RETRY_POLICY,
|
|
739
|
+
prepareSteer,
|
|
740
|
+
executeSteerPrepared,
|
|
741
|
+
verifySteer,
|
|
742
|
+
recoverSteer
|
|
743
|
+
});
|
|
744
|
+
const staging = options.staging === undefined
|
|
745
|
+
? undefined
|
|
746
|
+
: Object.freeze({
|
|
747
|
+
readCurrent: async (callbackRequest) => {
|
|
748
|
+
const binding = bindings.get(callbackRequest.operationId);
|
|
749
|
+
if (binding === undefined)
|
|
750
|
+
return unavailableStagingObservation(callbackRequest);
|
|
751
|
+
return await runReadTransaction(binding, callbackRequest.operationId, transaction => options.staging.readCurrent === undefined
|
|
752
|
+
? unavailableStagingObservation(callbackRequest)
|
|
753
|
+
: options.staging.readCurrent({ ...callbackRequest, page: transaction.page, target: transaction.target }), options.observeCurrentTarget, options.evidenceDigest, boundedRequest(callbackRequest.signal, callbackRequest.deadlineAt, transactionTimeoutMs, "operation-staging-read"));
|
|
754
|
+
},
|
|
755
|
+
observe: async (callbackRequest) => {
|
|
756
|
+
const binding = bindings.get(callbackRequest.operationId);
|
|
757
|
+
if (binding === undefined)
|
|
758
|
+
return unavailableStagingObservation(callbackRequest);
|
|
759
|
+
return await runReadTransaction(binding, callbackRequest.operationId, transaction => options.staging.observe === undefined
|
|
760
|
+
? unavailableStagingObservation(callbackRequest)
|
|
761
|
+
: options.staging.observe({ ...callbackRequest, page: transaction.page, target: transaction.target }), options.observeCurrentTarget, options.evidenceDigest, boundedRequest(callbackRequest.signal, callbackRequest.deadlineAt, transactionTimeoutMs, "operation-staging-observe"));
|
|
762
|
+
},
|
|
763
|
+
mutateOnce: async (callbackRequest) => {
|
|
764
|
+
const binding = bindings.get(callbackRequest.operationId);
|
|
765
|
+
if (binding === undefined)
|
|
766
|
+
throw new OperationBrowserAdapterError("target_binding_mismatch");
|
|
767
|
+
return await runMutationTransaction(binding, callbackRequest.operationId, transaction => options.staging.mutateOnce === undefined
|
|
768
|
+
? Promise.reject(new OperationBrowserAdapterError("unsupported_browser_primitive"))
|
|
769
|
+
: options.staging.mutateOnce({ ...callbackRequest, page: transaction.page, target: transaction.target }), options.observeCurrentTarget, options.evidenceDigest, boundedRequest(callbackRequest.signal, callbackRequest.deadlineAt, transactionTimeoutMs, "operation-staging-mutate", "mutation"));
|
|
770
|
+
}
|
|
771
|
+
});
|
|
772
|
+
const artifacts = options.artifacts === undefined || options.outputDirectory === undefined
|
|
773
|
+
? undefined
|
|
774
|
+
: Object.freeze({
|
|
775
|
+
transfer: async (request) => {
|
|
776
|
+
const binding = bindingFor(bindings, request.operationId, request.targetBindingDigest);
|
|
777
|
+
if (binding === undefined) {
|
|
778
|
+
throw new OperationBrowserAdapterError("target_binding_mismatch");
|
|
779
|
+
}
|
|
780
|
+
// `acquireDownload` is the only browser phase. Its promise is fully
|
|
781
|
+
// settled before this callback invokes `materializeDownload`, so a
|
|
782
|
+
// large stream or filesystem commit never owns the tab actor.
|
|
783
|
+
return await transferOperationArtifact({
|
|
784
|
+
operationId: request.operationId,
|
|
785
|
+
requestDigest: request.requestDigest,
|
|
786
|
+
targetBindingDigest: request.targetBindingDigest,
|
|
787
|
+
assistantTurnId: request.assistantTurnId,
|
|
788
|
+
sourceIdentityDigest: request.sourceIdentityDigest,
|
|
789
|
+
kind: request.kind,
|
|
790
|
+
ordinal: request.ordinal,
|
|
791
|
+
transferActionId: request.transferActionId,
|
|
792
|
+
outputDirectory: options.outputDirectory,
|
|
793
|
+
evidenceDigest: options.evidenceDigest,
|
|
794
|
+
signal: request.signal,
|
|
795
|
+
deadlineAt: request.deadlineAt,
|
|
796
|
+
...(request.mimeTypeHint === undefined ? {} : { mimeTypeHint: request.mimeTypeHint }),
|
|
797
|
+
openSource: async (sourceRequest) => {
|
|
798
|
+
const download = await runMutationTransaction(binding, request.operationId, context => options.artifacts.acquireDownload({
|
|
799
|
+
...sourceRequest,
|
|
800
|
+
signal: request.signal,
|
|
801
|
+
deadlineAt: request.deadlineAt
|
|
802
|
+
}, context.page, context.target), options.observeCurrentTarget, options.evidenceDigest, boundedRequest(request.signal, request.deadlineAt, transactionTimeoutMs, "operation-artifact-acquire", "mutation"));
|
|
803
|
+
// The transaction above has released the actor. This local
|
|
804
|
+
// phase must never call into the page or browser bridge.
|
|
805
|
+
return await options.artifacts.materializeDownload(download);
|
|
806
|
+
},
|
|
807
|
+
journal: request.journal
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
});
|
|
811
|
+
const adapter = Object.freeze({
|
|
812
|
+
resolveTarget,
|
|
813
|
+
submission,
|
|
814
|
+
collector,
|
|
815
|
+
...(artifacts === undefined ? {} : { artifacts }),
|
|
816
|
+
...(staging === undefined ? {} : { staging }),
|
|
817
|
+
...(control === undefined ? {} : { control }),
|
|
818
|
+
});
|
|
819
|
+
return adapter;
|
|
820
|
+
}
|
|
821
|
+
function normalizeAdapterOptions(value) {
|
|
822
|
+
if (!isPlainDataRecord(value))
|
|
823
|
+
throw new OperationBrowserAdapterError("adapter_incomplete");
|
|
824
|
+
try {
|
|
825
|
+
const page = readOwnData(value, "page");
|
|
826
|
+
const runtimeContext = readOwnData(value, "runtimeContext");
|
|
827
|
+
const owner = readOwnData(value, "owner");
|
|
828
|
+
const coordinator = readOwnData(value, "coordinator");
|
|
829
|
+
const evidenceDigest = readOwnData(value, "evidenceDigest");
|
|
830
|
+
const targetEvidence = readOwnData(value, "targetEvidence");
|
|
831
|
+
const newTargetAnchorDigest = readOwnData(value, "newTargetAnchorDigest");
|
|
832
|
+
const blankTaskEvidenceDigest = readOwnData(value, "blankTaskEvidenceDigest");
|
|
833
|
+
const resolveTargetEvidence = readOwnData(value, "resolveTargetEvidence");
|
|
834
|
+
const observeCurrentTarget = readOwnData(value, "observeCurrentTarget");
|
|
835
|
+
const capabilities = readOwnData(value, "capabilities");
|
|
836
|
+
const authoritativeClaim = readOwnData(value, "authoritativeClaim");
|
|
837
|
+
const transactionTimeoutMs = readOwnData(value, "transactionTimeoutMs");
|
|
838
|
+
const files = readOwnData(value, "files");
|
|
839
|
+
const fileManifestDigest = readOwnData(value, "fileManifestDigest");
|
|
840
|
+
const submission = readOwnData(value, "submission");
|
|
841
|
+
const staging = readOwnData(value, "staging");
|
|
842
|
+
const collector = readOwnData(value, "collector");
|
|
843
|
+
const control = readOwnData(value, "control");
|
|
844
|
+
const artifacts = readOwnData(value, "artifacts");
|
|
845
|
+
const outputDirectory = readOwnData(value, "outputDirectory");
|
|
846
|
+
const recovery = readOwnData(value, "recovery");
|
|
847
|
+
const normalizedRecovery = recovery === undefined
|
|
848
|
+
? undefined
|
|
849
|
+
: (() => {
|
|
850
|
+
validateRecoveryContext(recovery);
|
|
851
|
+
return normalizeRecoveryContext(recovery);
|
|
852
|
+
})();
|
|
853
|
+
const snapshot = {
|
|
854
|
+
page,
|
|
855
|
+
runtimeContext,
|
|
856
|
+
owner: cloneFrozenData(owner),
|
|
857
|
+
coordinator,
|
|
858
|
+
evidenceDigest,
|
|
859
|
+
targetEvidence: targetEvidence === undefined ? undefined : cloneFrozenData(targetEvidence),
|
|
860
|
+
newTargetAnchorDigest,
|
|
861
|
+
blankTaskEvidenceDigest,
|
|
862
|
+
resolveTargetEvidence,
|
|
863
|
+
observeCurrentTarget,
|
|
864
|
+
capabilities: capabilities === undefined ? undefined : cloneFrozenData(capabilities),
|
|
865
|
+
authoritativeClaim: authoritativeClaim === undefined ? undefined : cloneFrozenData(authoritativeClaim),
|
|
866
|
+
transactionTimeoutMs,
|
|
867
|
+
files: files === undefined ? undefined : cloneFrozenData(files),
|
|
868
|
+
fileManifestDigest,
|
|
869
|
+
submission: submission === undefined ? undefined : cloneFrozenProviderValue(submission),
|
|
870
|
+
staging: staging === undefined ? undefined : cloneFrozenProviderValue(staging),
|
|
871
|
+
collector: collector === undefined ? undefined : cloneFrozenProviderValue(collector),
|
|
872
|
+
control: control === undefined ? undefined : cloneFrozenProviderValue(control),
|
|
873
|
+
artifacts: artifacts === undefined ? undefined : cloneFrozenProviderValue(artifacts),
|
|
874
|
+
outputDirectory,
|
|
875
|
+
recovery: normalizedRecovery
|
|
876
|
+
};
|
|
877
|
+
const normalized = Object.freeze(snapshot);
|
|
878
|
+
validateOptions(normalized);
|
|
879
|
+
return normalized;
|
|
880
|
+
}
|
|
881
|
+
catch (error) {
|
|
882
|
+
if (error instanceof OperationBrowserAdapterError)
|
|
883
|
+
throw error;
|
|
884
|
+
throw new OperationBrowserAdapterError("adapter_incomplete");
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
function validateOptions(options) {
|
|
888
|
+
if (!isPlainDataRecord(options) || options.page === null || typeof options.page !== "object") {
|
|
889
|
+
throw new OperationBrowserAdapterError("adapter_incomplete");
|
|
890
|
+
}
|
|
891
|
+
if (typeof options.evidenceDigest !== "function")
|
|
892
|
+
throw new OperationBrowserAdapterError("adapter_incomplete");
|
|
893
|
+
if (!options.owner || typeof options.owner.backendSessionId !== "string")
|
|
894
|
+
throw new OperationBrowserAdapterError("adapter_incomplete");
|
|
895
|
+
if (options.runtimeContext !== undefined && !(options.runtimeContext instanceof OperationRuntimeContext)) {
|
|
896
|
+
throw new OperationBrowserAdapterError("adapter_incomplete");
|
|
897
|
+
}
|
|
898
|
+
if (options.recovery !== undefined) {
|
|
899
|
+
validateRecoveryContext(options.recovery);
|
|
900
|
+
if (options.observeCurrentTarget === undefined) {
|
|
901
|
+
throw new OperationBrowserAdapterError("target_evidence_unavailable");
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
else if (options.targetEvidence === undefined && options.resolveTargetEvidence === undefined) {
|
|
905
|
+
throw new OperationBrowserAdapterError("target_evidence_unavailable");
|
|
906
|
+
}
|
|
907
|
+
if (options.files !== undefined && !Array.isArray(options.files))
|
|
908
|
+
throw new OperationBrowserAdapterError("adapter_incomplete");
|
|
909
|
+
if (options.recovery !== undefined
|
|
910
|
+
&& (options.artifacts !== undefined || options.outputDirectory !== undefined)) {
|
|
911
|
+
throw new OperationBrowserAdapterError("adapter_incomplete");
|
|
912
|
+
}
|
|
913
|
+
if (options.artifacts !== undefined) {
|
|
914
|
+
if (!isPlainDataRecord(options.artifacts)
|
|
915
|
+
|| typeof readOwnData(options.artifacts, "acquireDownload") !== "function"
|
|
916
|
+
|| typeof readOwnData(options.artifacts, "materializeDownload") !== "function") {
|
|
917
|
+
throw new OperationBrowserAdapterError("adapter_incomplete");
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
if (options.outputDirectory !== undefined
|
|
921
|
+
&& (typeof options.outputDirectory !== "string"
|
|
922
|
+
|| options.outputDirectory.length === 0
|
|
923
|
+
|| options.outputDirectory.length > 4096
|
|
924
|
+
|| !isAbsolutePath(options.outputDirectory)
|
|
925
|
+
|| /[\u0000-\u001f\u007f]/u.test(options.outputDirectory))) {
|
|
926
|
+
throw new OperationBrowserAdapterError("adapter_incomplete");
|
|
927
|
+
}
|
|
928
|
+
if (options.transactionTimeoutMs !== undefined
|
|
929
|
+
&& (!Number.isSafeInteger(options.transactionTimeoutMs) || options.transactionTimeoutMs < 1 || options.transactionTimeoutMs > MAX_TRANSACTION_TIMEOUT_MS)) {
|
|
930
|
+
throw new OperationBrowserAdapterError("adapter_incomplete");
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
function validateRecoveryContext(value) {
|
|
934
|
+
if (!isPlainDataRecord(value))
|
|
935
|
+
throw new OperationBrowserAdapterError("adapter_incomplete");
|
|
936
|
+
if (readOwnData(value, "operationId") === undefined
|
|
937
|
+
|| readOwnData(value, "requestDigest") === undefined
|
|
938
|
+
|| readOwnData(value, "surface") === undefined
|
|
939
|
+
|| readOwnData(value, "target") === undefined
|
|
940
|
+
|| readOwnData(value, "signal") === undefined) {
|
|
941
|
+
throw new OperationBrowserAdapterError("adapter_incomplete");
|
|
942
|
+
}
|
|
943
|
+
const operationId = readOwnData(value, "operationId");
|
|
944
|
+
const requestDigest = readOwnData(value, "requestDigest");
|
|
945
|
+
const surface = readOwnData(value, "surface");
|
|
946
|
+
const target = readOwnData(value, "target");
|
|
947
|
+
const signal = readOwnData(value, "signal");
|
|
948
|
+
if (typeof operationId !== "string"
|
|
949
|
+
|| !OPAQUE_ID_PATTERN.test(operationId)
|
|
950
|
+
|| typeof requestDigest !== "string"
|
|
951
|
+
|| !DIGEST_PATTERN.test(requestDigest)
|
|
952
|
+
|| (surface !== "chat" && surface !== "work")
|
|
953
|
+
|| target === null
|
|
954
|
+
|| typeof target !== "object"
|
|
955
|
+
|| Array.isArray(target)) {
|
|
956
|
+
throw new OperationBrowserAdapterError("adapter_incomplete");
|
|
957
|
+
}
|
|
958
|
+
assertNativeAbortSignal(signal);
|
|
959
|
+
if (!isSafeDataGraph(target) || !isPlainDataRecord(target)) {
|
|
960
|
+
throw new OperationBrowserAdapterError("adapter_incomplete");
|
|
961
|
+
}
|
|
962
|
+
const lifecycle = readOwnData(target, "targetLifecycle");
|
|
963
|
+
if (lifecycle === "new_pending") {
|
|
964
|
+
throw new OperationBrowserAdapterError("target_binding_mismatch");
|
|
965
|
+
}
|
|
966
|
+
if (lifecycle !== undefined && lifecycle !== "fixed" && lifecycle !== "new_established") {
|
|
967
|
+
throw new OperationBrowserAdapterError("adapter_incomplete");
|
|
968
|
+
}
|
|
969
|
+
const providerId = readOwnData(target, "providerId");
|
|
970
|
+
const browserId = readOwnData(target, "browserId");
|
|
971
|
+
const tabId = readOwnData(target, "tabId");
|
|
972
|
+
const coordinationScope = readOwnData(target, "coordinationScope");
|
|
973
|
+
const evidenceProfile = readOwnData(target, "evidenceProfile");
|
|
974
|
+
if (typeof providerId !== "string"
|
|
975
|
+
|| !OPAQUE_ID_PATTERN.test(providerId)
|
|
976
|
+
|| typeof browserId !== "string"
|
|
977
|
+
|| !OPAQUE_ID_PATTERN.test(browserId)
|
|
978
|
+
|| typeof tabId !== "string"
|
|
979
|
+
|| !OPAQUE_ID_PATTERN.test(tabId)
|
|
980
|
+
|| (coordinationScope !== "process" && coordinationScope !== "provider")
|
|
981
|
+
|| !isPlainDataRecord(evidenceProfile)) {
|
|
982
|
+
throw new OperationBrowserAdapterError("adapter_incomplete");
|
|
983
|
+
}
|
|
984
|
+
const conversationId = readOwnData(target, "conversationId");
|
|
985
|
+
const canonicalThreadUrl = readOwnData(target, "canonicalThreadUrl");
|
|
986
|
+
if (typeof conversationId !== "string"
|
|
987
|
+
|| !OPAQUE_ID_PATTERN.test(conversationId)
|
|
988
|
+
|| typeof canonicalThreadUrl !== "string"
|
|
989
|
+
|| !OPAQUE_THREAD_URL_PATTERN.test(canonicalThreadUrl)) {
|
|
990
|
+
throw new OperationBrowserAdapterError("adapter_incomplete");
|
|
991
|
+
}
|
|
992
|
+
if (lifecycle === "new_established" && !isPlainDataRecord(readOwnData(target, "targetEstablishment"))) {
|
|
993
|
+
throw new OperationBrowserAdapterError("adapter_incomplete");
|
|
994
|
+
}
|
|
995
|
+
const claimDigest = readOwnData(target, "tabClaimEvidenceDigest");
|
|
996
|
+
if (claimDigest !== undefined && (typeof claimDigest !== "string" || !DIGEST_PATTERN.test(claimDigest))) {
|
|
997
|
+
throw new OperationBrowserAdapterError("adapter_incomplete");
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
function normalizeRecoveryContext(value) {
|
|
1001
|
+
try {
|
|
1002
|
+
const record = value;
|
|
1003
|
+
const operationId = readOwnData(record, "operationId");
|
|
1004
|
+
const requestDigest = readOwnData(record, "requestDigest");
|
|
1005
|
+
const surface = readOwnData(record, "surface");
|
|
1006
|
+
const targetValue = readOwnData(record, "target");
|
|
1007
|
+
const signal = readOwnData(record, "signal");
|
|
1008
|
+
if (targetValue === undefined || signal === undefined)
|
|
1009
|
+
throw new Error("incomplete recovery context");
|
|
1010
|
+
const target = cloneFrozenData(targetValue);
|
|
1011
|
+
return Object.freeze({
|
|
1012
|
+
operationId: operationId,
|
|
1013
|
+
requestDigest: requestDigest,
|
|
1014
|
+
surface: surface,
|
|
1015
|
+
target,
|
|
1016
|
+
signal: signal
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
catch {
|
|
1020
|
+
throw new OperationBrowserAdapterError("adapter_incomplete");
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
async function hydrateRecoveredTarget(options, recovery, page, coordinator) {
|
|
1024
|
+
const observeCurrentTarget = options.observeCurrentTarget;
|
|
1025
|
+
if (observeCurrentTarget === undefined) {
|
|
1026
|
+
throw new OperationBrowserAdapterError("target_evidence_unavailable");
|
|
1027
|
+
}
|
|
1028
|
+
assertNativeAbortSignal(recovery.signal);
|
|
1029
|
+
let observed;
|
|
1030
|
+
try {
|
|
1031
|
+
observed = normalizeCurrentTargetResult(await observeCurrentTarget({
|
|
1032
|
+
page,
|
|
1033
|
+
operationId: recovery.operationId,
|
|
1034
|
+
target: recovery.target,
|
|
1035
|
+
signal: recovery.signal
|
|
1036
|
+
}));
|
|
1037
|
+
}
|
|
1038
|
+
catch (error) {
|
|
1039
|
+
if (error instanceof OperationBrowserAdapterError)
|
|
1040
|
+
throw error;
|
|
1041
|
+
throw new OperationBrowserAdapterError("target_evidence_unavailable");
|
|
1042
|
+
}
|
|
1043
|
+
assertRecoveredTargetIdentity(recovery.target, observed, options);
|
|
1044
|
+
const lifecycle = recovery.target.targetLifecycle ?? "fixed";
|
|
1045
|
+
let bound;
|
|
1046
|
+
try {
|
|
1047
|
+
bound = bindBrowserTarget({
|
|
1048
|
+
page,
|
|
1049
|
+
evidence: observed.evidence,
|
|
1050
|
+
...(lifecycle === "new_established" ? { targetLifecycle: "new_established" } : {}),
|
|
1051
|
+
...(recovery.target.coordinationScope !== "provider" || observed.authoritativeClaim === undefined
|
|
1052
|
+
? {}
|
|
1053
|
+
: { authoritativeClaim: observed.authoritativeClaim }),
|
|
1054
|
+
...(options.capabilities === undefined ? {} : { capabilities: options.capabilities }),
|
|
1055
|
+
evidenceDigest: options.evidenceDigest,
|
|
1056
|
+
owner: Object.freeze({ ...options.owner, operationId: recovery.operationId }),
|
|
1057
|
+
coordinator,
|
|
1058
|
+
...(recovery.target.userTurnBaselineDigest === undefined ? {} : { userTurnBaselineDigest: recovery.target.userTurnBaselineDigest }),
|
|
1059
|
+
...(recovery.target.assistantTurnBaselineDigest === undefined ? {} : { assistantTurnBaselineDigest: recovery.target.assistantTurnBaselineDigest }),
|
|
1060
|
+
...(recovery.target.configurationReceiptDigest === undefined ? {} : { configurationReceiptDigest: recovery.target.configurationReceiptDigest })
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
catch (error) {
|
|
1064
|
+
if (error instanceof OperationBrowserAdapterError)
|
|
1065
|
+
throw error;
|
|
1066
|
+
throw new OperationBrowserAdapterError("target_binding_mismatch");
|
|
1067
|
+
}
|
|
1068
|
+
return preserveRecoveredTarget(bound, recovery.target);
|
|
1069
|
+
}
|
|
1070
|
+
function assertRecoveredTargetIdentity(target, observed, options) {
|
|
1071
|
+
const evidence = observed.evidence;
|
|
1072
|
+
compareRecoveredIdentity(evidence.provider, target.providerId);
|
|
1073
|
+
compareRecoveredIdentity(evidence.browser, target.browserId);
|
|
1074
|
+
compareRecoveredIdentity(evidence.tab, target.tabId);
|
|
1075
|
+
compareRecoveredIdentity(evidence.conversation, target.conversationId);
|
|
1076
|
+
compareRecoveredIdentity(evidence.canonicalThreadUrl, target.canonicalThreadUrl);
|
|
1077
|
+
if (target.coordinationScope !== "provider")
|
|
1078
|
+
return;
|
|
1079
|
+
const claim = observed.authoritativeClaim;
|
|
1080
|
+
if (claim === undefined
|
|
1081
|
+
|| target.tabClaimEvidenceDigest === undefined
|
|
1082
|
+
|| options.capabilities?.stableProviderId !== true
|
|
1083
|
+
|| options.capabilities.stableBrowserId !== true
|
|
1084
|
+
|| options.capabilities.stableTabId !== true
|
|
1085
|
+
|| options.capabilities.concurrentTabs !== true
|
|
1086
|
+
|| options.capabilities.authoritativeTabClaim !== true) {
|
|
1087
|
+
throw new OperationBrowserAdapterError("target_binding_mismatch");
|
|
1088
|
+
}
|
|
1089
|
+
let claimDigest;
|
|
1090
|
+
try {
|
|
1091
|
+
claimDigest = options.evidenceDigest(CLAIM_EVIDENCE_DIGEST_DOMAIN, {
|
|
1092
|
+
token: claim.token,
|
|
1093
|
+
epoch: claim.epoch
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
catch {
|
|
1097
|
+
throw new OperationBrowserAdapterError("target_binding_mismatch");
|
|
1098
|
+
}
|
|
1099
|
+
if (claimDigest !== target.tabClaimEvidenceDigest) {
|
|
1100
|
+
throw new OperationBrowserAdapterError("target_binding_mismatch");
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
function compareRecoveredIdentity(observed, expected) {
|
|
1104
|
+
if (expected === undefined || observed.status !== "available" || observed.value !== expected) {
|
|
1105
|
+
throw new OperationBrowserAdapterError("target_binding_mismatch");
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
function preserveRecoveredTarget(binding, target) {
|
|
1109
|
+
const assertCurrent = (evidence, claim, allowNewTargetEstablishment = false) => {
|
|
1110
|
+
binding.assertCurrent(evidence, claim, allowNewTargetEstablishment);
|
|
1111
|
+
compareRecoveredIdentity(evidence.provider, target.providerId);
|
|
1112
|
+
compareRecoveredIdentity(evidence.browser, target.browserId);
|
|
1113
|
+
compareRecoveredIdentity(evidence.tab, target.tabId);
|
|
1114
|
+
compareRecoveredIdentity(evidence.conversation, target.conversationId);
|
|
1115
|
+
compareRecoveredIdentity(evidence.canonicalThreadUrl, target.canonicalThreadUrl);
|
|
1116
|
+
};
|
|
1117
|
+
return Object.freeze({
|
|
1118
|
+
page: binding.page,
|
|
1119
|
+
target,
|
|
1120
|
+
targetEvidenceDigest: binding.targetEvidenceDigest,
|
|
1121
|
+
evidence: binding.evidence,
|
|
1122
|
+
capabilities: binding.capabilities,
|
|
1123
|
+
resource: binding.resource,
|
|
1124
|
+
owner: binding.owner,
|
|
1125
|
+
assertPage: binding.assertPage,
|
|
1126
|
+
assertCurrent,
|
|
1127
|
+
withTabTransaction: (transactionOptions, callback) => binding.withTabTransaction(transactionOptions, context => callback(Object.freeze({
|
|
1128
|
+
...context,
|
|
1129
|
+
target,
|
|
1130
|
+
assertCurrent
|
|
1131
|
+
})))
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
function cloneFrozenData(value, seen = new Set()) {
|
|
1135
|
+
if (value === null || typeof value !== "object") {
|
|
1136
|
+
if (typeof value === "function" || typeof value === "symbol" || typeof value === "bigint") {
|
|
1137
|
+
throw new Error("unsupported recovery value");
|
|
1138
|
+
}
|
|
1139
|
+
return value;
|
|
1140
|
+
}
|
|
1141
|
+
if (seen.has(value))
|
|
1142
|
+
throw new Error("cyclic recovery value");
|
|
1143
|
+
seen.add(value);
|
|
1144
|
+
try {
|
|
1145
|
+
if (Array.isArray(value)) {
|
|
1146
|
+
const result = value.map(item => cloneFrozenData(item, seen));
|
|
1147
|
+
seen.delete(value);
|
|
1148
|
+
return Object.freeze(result);
|
|
1149
|
+
}
|
|
1150
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1151
|
+
if (prototype !== Object.prototype && prototype !== null)
|
|
1152
|
+
throw new Error("non-plain recovery value");
|
|
1153
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
1154
|
+
// Use a null-prototype destination and explicit definitions so an own
|
|
1155
|
+
// `__proto__` data key cannot invoke the legacy setter and disappear.
|
|
1156
|
+
const result = Object.create(null);
|
|
1157
|
+
for (const key of Object.keys(descriptors)) {
|
|
1158
|
+
const descriptor = descriptors[key];
|
|
1159
|
+
if (descriptor === undefined || !("value" in descriptor) || descriptor.get !== undefined || descriptor.set !== undefined) {
|
|
1160
|
+
throw new Error("accessor recovery value");
|
|
1161
|
+
}
|
|
1162
|
+
Object.defineProperty(result, key, {
|
|
1163
|
+
value: cloneFrozenData(descriptor.value, seen),
|
|
1164
|
+
enumerable: descriptor.enumerable ?? false,
|
|
1165
|
+
writable: true,
|
|
1166
|
+
configurable: true
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
seen.delete(value);
|
|
1170
|
+
return Object.freeze(result);
|
|
1171
|
+
}
|
|
1172
|
+
catch (error) {
|
|
1173
|
+
seen.delete(value);
|
|
1174
|
+
throw error;
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
/** Clone provider primitive records without ever reading an accessor/get trap. */
|
|
1178
|
+
function cloneFrozenProviderValue(value, seen = new Set()) {
|
|
1179
|
+
if (value === null || typeof value !== "object")
|
|
1180
|
+
return value;
|
|
1181
|
+
if (seen.has(value))
|
|
1182
|
+
throw new Error("cyclic provider value");
|
|
1183
|
+
seen.add(value);
|
|
1184
|
+
try {
|
|
1185
|
+
if (Array.isArray(value)) {
|
|
1186
|
+
const result = value.map(item => cloneFrozenProviderValue(item, seen));
|
|
1187
|
+
seen.delete(value);
|
|
1188
|
+
return Object.freeze(result);
|
|
1189
|
+
}
|
|
1190
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1191
|
+
if (prototype !== Object.prototype && prototype !== null)
|
|
1192
|
+
throw new Error("non-plain provider value");
|
|
1193
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
1194
|
+
const result = Object.create(null);
|
|
1195
|
+
for (const key of Object.keys(descriptors)) {
|
|
1196
|
+
const descriptor = descriptors[key];
|
|
1197
|
+
if (descriptor === undefined || !("value" in descriptor) || descriptor.get !== undefined || descriptor.set !== undefined) {
|
|
1198
|
+
throw new Error("accessor provider value");
|
|
1199
|
+
}
|
|
1200
|
+
Object.defineProperty(result, key, {
|
|
1201
|
+
value: cloneFrozenProviderValue(descriptor.value, seen),
|
|
1202
|
+
enumerable: descriptor.enumerable ?? false,
|
|
1203
|
+
writable: true,
|
|
1204
|
+
configurable: true
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
seen.delete(value);
|
|
1208
|
+
return Object.freeze(result);
|
|
1209
|
+
}
|
|
1210
|
+
catch (error) {
|
|
1211
|
+
seen.delete(value);
|
|
1212
|
+
throw error;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
function assertNativeAbortSignal(value) {
|
|
1216
|
+
if (value === null || typeof value !== "object" || typeof AbortSignal !== "function") {
|
|
1217
|
+
throw new OperationBrowserAdapterError("adapter_incomplete");
|
|
1218
|
+
}
|
|
1219
|
+
const getter = Object.getOwnPropertyDescriptor(AbortSignal.prototype, "aborted")?.get;
|
|
1220
|
+
if (getter === undefined)
|
|
1221
|
+
throw new OperationBrowserAdapterError("adapter_incomplete");
|
|
1222
|
+
try {
|
|
1223
|
+
if (typeof Reflect.apply(getter, value, []) !== "boolean")
|
|
1224
|
+
throw new Error("invalid signal");
|
|
1225
|
+
}
|
|
1226
|
+
catch {
|
|
1227
|
+
throw new OperationBrowserAdapterError("adapter_incomplete");
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
async function resolveProbe(options, request) {
|
|
1231
|
+
if (options.resolveTargetEvidence !== undefined) {
|
|
1232
|
+
const value = await options.resolveTargetEvidence({
|
|
1233
|
+
page: options.page,
|
|
1234
|
+
operationId: request.operationId,
|
|
1235
|
+
requestDigest: request.requestDigest,
|
|
1236
|
+
surface: request.surface,
|
|
1237
|
+
target: request.target,
|
|
1238
|
+
signal: request.signal
|
|
1239
|
+
});
|
|
1240
|
+
return normalizeTargetProbe(value);
|
|
1241
|
+
}
|
|
1242
|
+
if (options.targetEvidence === undefined)
|
|
1243
|
+
throw new OperationBrowserAdapterError("target_evidence_unavailable");
|
|
1244
|
+
return normalizeTargetProbe(Object.freeze({
|
|
1245
|
+
page: options.page,
|
|
1246
|
+
evidence: options.targetEvidence,
|
|
1247
|
+
...(options.newTargetAnchorDigest === undefined ? {} : { newTargetAnchorDigest: options.newTargetAnchorDigest }),
|
|
1248
|
+
...(options.blankTaskEvidenceDigest === undefined ? {} : { blankTaskEvidenceDigest: options.blankTaskEvidenceDigest }),
|
|
1249
|
+
...(options.authoritativeClaim === undefined ? {} : { authoritativeClaim: options.authoritativeClaim }),
|
|
1250
|
+
...(options.capabilities === undefined ? {} : { capabilities: options.capabilities })
|
|
1251
|
+
}));
|
|
1252
|
+
}
|
|
1253
|
+
/**
|
|
1254
|
+
* Read resolver output through own data descriptors only. Provider adapters
|
|
1255
|
+
* are an untrusted boundary: a getter or proxy trap must not run while the
|
|
1256
|
+
* operation is deciding whether the Send target is safe.
|
|
1257
|
+
*/
|
|
1258
|
+
function normalizeTargetProbe(value) {
|
|
1259
|
+
if (!isPlainDataRecord(value))
|
|
1260
|
+
throw new OperationBrowserAdapterError("target_evidence_unavailable");
|
|
1261
|
+
const page = readOwnData(value, "page");
|
|
1262
|
+
const evidence = readOwnData(value, "evidence");
|
|
1263
|
+
const targetLifecycle = readOwnData(value, "targetLifecycle");
|
|
1264
|
+
const newTargetAnchorDigest = readOwnData(value, "newTargetAnchorDigest");
|
|
1265
|
+
const blankTaskEvidenceDigest = readOwnData(value, "blankTaskEvidenceDigest");
|
|
1266
|
+
const authoritativeClaim = readOwnData(value, "authoritativeClaim");
|
|
1267
|
+
const capabilities = readOwnData(value, "capabilities");
|
|
1268
|
+
const allowed = new Set([
|
|
1269
|
+
"page",
|
|
1270
|
+
"evidence",
|
|
1271
|
+
"targetLifecycle",
|
|
1272
|
+
"newTargetAnchorDigest",
|
|
1273
|
+
"blankTaskEvidenceDigest",
|
|
1274
|
+
"authoritativeClaim",
|
|
1275
|
+
"capabilities"
|
|
1276
|
+
]);
|
|
1277
|
+
try {
|
|
1278
|
+
for (const key of Object.keys(value))
|
|
1279
|
+
if (!allowed.has(key))
|
|
1280
|
+
throw new Error("unsupported probe field");
|
|
1281
|
+
}
|
|
1282
|
+
catch {
|
|
1283
|
+
throw new OperationBrowserAdapterError("target_evidence_unavailable");
|
|
1284
|
+
}
|
|
1285
|
+
if (evidence === undefined)
|
|
1286
|
+
throw new OperationBrowserAdapterError("target_evidence_unavailable");
|
|
1287
|
+
if (!isSafeDataGraph(evidence) || (authoritativeClaim !== undefined && !isSafeDataGraph(authoritativeClaim)) || (capabilities !== undefined && !isSafeDataGraph(capabilities))) {
|
|
1288
|
+
throw new OperationBrowserAdapterError("target_evidence_unavailable");
|
|
1289
|
+
}
|
|
1290
|
+
if (targetLifecycle !== undefined
|
|
1291
|
+
&& targetLifecycle !== "fixed"
|
|
1292
|
+
&& targetLifecycle !== "new_pending"
|
|
1293
|
+
&& targetLifecycle !== "new_established") {
|
|
1294
|
+
throw new OperationBrowserAdapterError("target_evidence_unavailable");
|
|
1295
|
+
}
|
|
1296
|
+
return Object.freeze({
|
|
1297
|
+
...(page === undefined ? {} : { page: page }),
|
|
1298
|
+
evidence: evidence,
|
|
1299
|
+
...(targetLifecycle === undefined ? {} : { targetLifecycle }),
|
|
1300
|
+
...(newTargetAnchorDigest === undefined ? {} : { newTargetAnchorDigest: newTargetAnchorDigest }),
|
|
1301
|
+
...(blankTaskEvidenceDigest === undefined ? {} : { blankTaskEvidenceDigest: blankTaskEvidenceDigest }),
|
|
1302
|
+
...(authoritativeClaim === undefined ? {} : { authoritativeClaim: authoritativeClaim }),
|
|
1303
|
+
...(capabilities === undefined ? {} : { capabilities: capabilities })
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
1306
|
+
function isPlainDataRecord(value) {
|
|
1307
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
1308
|
+
return false;
|
|
1309
|
+
try {
|
|
1310
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1311
|
+
if (prototype !== Object.prototype && prototype !== null)
|
|
1312
|
+
return false;
|
|
1313
|
+
for (const descriptor of Object.values(Object.getOwnPropertyDescriptors(value))) {
|
|
1314
|
+
if (!("value" in descriptor) || descriptor.get !== undefined || descriptor.set !== undefined)
|
|
1315
|
+
return false;
|
|
1316
|
+
}
|
|
1317
|
+
return true;
|
|
1318
|
+
}
|
|
1319
|
+
catch {
|
|
1320
|
+
return false;
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
function readOwnData(value, key) {
|
|
1324
|
+
try {
|
|
1325
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
1326
|
+
return descriptor !== undefined && "value" in descriptor && descriptor.get === undefined && descriptor.set === undefined
|
|
1327
|
+
? descriptor.value
|
|
1328
|
+
: undefined;
|
|
1329
|
+
}
|
|
1330
|
+
catch {
|
|
1331
|
+
return undefined;
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
function isSafeDataGraph(value, seen = new Set(), depth = 0) {
|
|
1335
|
+
if (value === null || (typeof value !== "object" && typeof value !== "function"))
|
|
1336
|
+
return true;
|
|
1337
|
+
if (typeof value === "function")
|
|
1338
|
+
return true;
|
|
1339
|
+
if (depth > 16 || seen.has(value))
|
|
1340
|
+
return depth <= 16;
|
|
1341
|
+
seen.add(value);
|
|
1342
|
+
try {
|
|
1343
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1344
|
+
if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null)
|
|
1345
|
+
return false;
|
|
1346
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
1347
|
+
if (Object.keys(descriptors).length > 1024)
|
|
1348
|
+
return false;
|
|
1349
|
+
for (const descriptor of Object.values(descriptors)) {
|
|
1350
|
+
if (!("value" in descriptor) || descriptor.get !== undefined || descriptor.set !== undefined)
|
|
1351
|
+
return false;
|
|
1352
|
+
if (!isSafeDataGraph(descriptor.value, seen, depth + 1))
|
|
1353
|
+
return false;
|
|
1354
|
+
}
|
|
1355
|
+
return true;
|
|
1356
|
+
}
|
|
1357
|
+
catch {
|
|
1358
|
+
return false;
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
function assertProbePage(boundPage, probedPage) {
|
|
1362
|
+
if (probedPage !== undefined && probedPage !== boundPage)
|
|
1363
|
+
throw new OperationBrowserAdapterError("page_affinity_mismatch");
|
|
1364
|
+
}
|
|
1365
|
+
function assertRuntimeAffinity(runtimeCapture, page, probe) {
|
|
1366
|
+
const tabId = probe.evidence.tab.status === "available" ? probe.evidence.tab.value : undefined;
|
|
1367
|
+
const claim = probe.authoritativeClaim === undefined
|
|
1368
|
+
? undefined
|
|
1369
|
+
: { status: "available", token: probe.authoritativeClaim.token, epoch: probe.authoritativeClaim.epoch };
|
|
1370
|
+
runtimeCapture.assertPageAffinity(page, {
|
|
1371
|
+
...(tabId === undefined ? {} : { tabId }),
|
|
1372
|
+
...(claim === undefined ? {} : { authoritativeClaim: claim })
|
|
1373
|
+
});
|
|
1374
|
+
}
|
|
1375
|
+
function assertStaticTarget(target, evidence, hasResolver) {
|
|
1376
|
+
if (hasResolver)
|
|
1377
|
+
return;
|
|
1378
|
+
switch (target.type) {
|
|
1379
|
+
case "selected_tab":
|
|
1380
|
+
return;
|
|
1381
|
+
case "tab_id":
|
|
1382
|
+
if (evidence.tab.status === "available" && evidence.tab.value === target.tabId)
|
|
1383
|
+
return;
|
|
1384
|
+
break;
|
|
1385
|
+
case "conversation_id":
|
|
1386
|
+
if (evidence.conversation.status === "available" && evidence.conversation.value === target.conversationId)
|
|
1387
|
+
return;
|
|
1388
|
+
break;
|
|
1389
|
+
case "new":
|
|
1390
|
+
case "url":
|
|
1391
|
+
break;
|
|
1392
|
+
}
|
|
1393
|
+
throw new OperationBrowserAdapterError("target_evidence_unavailable");
|
|
1394
|
+
}
|
|
1395
|
+
function bindingFor(bindings, operationId, _targetBindingDigest) {
|
|
1396
|
+
const binding = bindings.get(operationId);
|
|
1397
|
+
// The journal owner computes the durable target-binding digest after the
|
|
1398
|
+
// target-bound event is appended. `bindBrowserTarget` also emits a local
|
|
1399
|
+
// evidence digest, but those domains intentionally differ. Operation ID is
|
|
1400
|
+
// therefore the only safe adapter lookup key here; OperationService has
|
|
1401
|
+
// already authenticated the caller handle before reaching these ports.
|
|
1402
|
+
if (binding === undefined)
|
|
1403
|
+
return undefined;
|
|
1404
|
+
return binding;
|
|
1405
|
+
}
|
|
1406
|
+
function unavailableStage(reason) {
|
|
1407
|
+
return { status: "unavailable", reason };
|
|
1408
|
+
}
|
|
1409
|
+
function unavailableStagingObservation(request) {
|
|
1410
|
+
return {
|
|
1411
|
+
status: "unavailable",
|
|
1412
|
+
desiredStateDigest: request.desiredStateDigest,
|
|
1413
|
+
blockerCode: "target_evidence_unavailable"
|
|
1414
|
+
};
|
|
1415
|
+
}
|
|
1416
|
+
function matchFileManifest(identities, manifestDigest, manifest) {
|
|
1417
|
+
if (manifest.count === 0)
|
|
1418
|
+
return [];
|
|
1419
|
+
if (identities === undefined || identities.length !== manifest.count || manifestDigest === undefined)
|
|
1420
|
+
return undefined;
|
|
1421
|
+
const sorted = [...identities];
|
|
1422
|
+
for (let ordinal = 0; ordinal < manifest.count; ordinal += 1) {
|
|
1423
|
+
const identity = sorted[ordinal];
|
|
1424
|
+
const expected = manifest.identities[ordinal];
|
|
1425
|
+
if (identity === undefined || expected === undefined || expected.ordinal !== ordinal)
|
|
1426
|
+
return undefined;
|
|
1427
|
+
let digest;
|
|
1428
|
+
try {
|
|
1429
|
+
digest = manifestDigest(ordinal, identity.manifest);
|
|
1430
|
+
}
|
|
1431
|
+
catch {
|
|
1432
|
+
return undefined;
|
|
1433
|
+
}
|
|
1434
|
+
if (digest !== expected.identityDigest || !DIGEST_PATTERN.test(digest))
|
|
1435
|
+
return undefined;
|
|
1436
|
+
}
|
|
1437
|
+
return Object.freeze(sorted);
|
|
1438
|
+
}
|
|
1439
|
+
async function runReadTransaction(binding, operationId, callback, observeCurrentTarget, evidenceDigest, transactionOptions = { timeoutMs: DEFAULT_TRANSACTION_TIMEOUT_MS }, allowNewTargetEstablishment = false) {
|
|
1440
|
+
return await binding.withTabTransaction({ ...transactionOptions, priority: transactionOptions.priority ?? "read", label: transactionOptions.label ?? "operation-read" }, async (context) => {
|
|
1441
|
+
const current = await readCurrentTarget(binding, operationId, context.page, observeCurrentTarget, evidenceDigest, context.acquisition.signal, context.acquisition.timing.deadlineAt);
|
|
1442
|
+
binding.assertCurrent(current.evidence, current.authoritativeClaim, allowNewTargetEstablishment);
|
|
1443
|
+
return await callback(context);
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
async function runMutationTransaction(binding, operationId, callback, observeCurrentTarget, evidenceDigest, transactionOptions = { timeoutMs: DEFAULT_TRANSACTION_TIMEOUT_MS }) {
|
|
1447
|
+
return await binding.withTabTransaction({ ...transactionOptions, priority: transactionOptions.priority ?? "mutation", label: transactionOptions.label ?? "operation-mutation" }, async (context) => {
|
|
1448
|
+
const current = await readCurrentTarget(binding, operationId, context.page, observeCurrentTarget, evidenceDigest, context.acquisition.signal, context.acquisition.timing.deadlineAt);
|
|
1449
|
+
binding.assertCurrent(current.evidence, current.authoritativeClaim);
|
|
1450
|
+
return await callback(context);
|
|
1451
|
+
});
|
|
1452
|
+
}
|
|
1453
|
+
/** Resolve one durable identity from either a fresh or prepared phase call. */
|
|
1454
|
+
function steerPhaseIdentity(request) {
|
|
1455
|
+
if ("prepared" in request) {
|
|
1456
|
+
return {
|
|
1457
|
+
parentOperationId: request.prepared.parentOperationId,
|
|
1458
|
+
parentRequestDigest: request.prepared.parentRequestDigest,
|
|
1459
|
+
parentTargetBindingDigest: request.prepared.parentTargetBindingDigest,
|
|
1460
|
+
controlActionId: request.prepared.controlActionId,
|
|
1461
|
+
requestDigest: request.prepared.requestDigest,
|
|
1462
|
+
expectedAssistantTurnId: request.prepared.expectedAssistantTurnId
|
|
1463
|
+
};
|
|
1464
|
+
}
|
|
1465
|
+
return {
|
|
1466
|
+
parentOperationId: request.parentOperationId,
|
|
1467
|
+
parentRequestDigest: request.parentRequestDigest,
|
|
1468
|
+
parentTargetBindingDigest: request.parentTargetBindingDigest,
|
|
1469
|
+
controlActionId: request.controlActionId,
|
|
1470
|
+
requestDigest: request.requestDigest,
|
|
1471
|
+
expectedAssistantTurnId: request.expectedAssistantTurnId
|
|
1472
|
+
};
|
|
1473
|
+
}
|
|
1474
|
+
function steerPhaseBase(request, phase, prepared) {
|
|
1475
|
+
const identity = steerPhaseIdentity(request);
|
|
1476
|
+
const selected = prepared ?? ("prepared" in request ? request.prepared : undefined);
|
|
1477
|
+
return {
|
|
1478
|
+
schemaVersion: CONTROL_COORDINATOR_SCHEMA_VERSION,
|
|
1479
|
+
phase,
|
|
1480
|
+
parentOperationId: identity.parentOperationId,
|
|
1481
|
+
parentRequestDigest: identity.parentRequestDigest,
|
|
1482
|
+
parentTargetBindingDigest: identity.parentTargetBindingDigest,
|
|
1483
|
+
controlActionId: identity.controlActionId,
|
|
1484
|
+
action: "steer",
|
|
1485
|
+
requestDigest: identity.requestDigest,
|
|
1486
|
+
expectedAssistantTurnId: identity.expectedAssistantTurnId,
|
|
1487
|
+
...(selected === undefined ? {} : {
|
|
1488
|
+
assistantBranchId: selected.assistantBranchId,
|
|
1489
|
+
assistantParentTurnId: selected.assistantParentTurnId,
|
|
1490
|
+
baselineSnapshotDigest: selected.baselineSnapshotDigest,
|
|
1491
|
+
preparedDigest: selected.preparedDigest
|
|
1492
|
+
})
|
|
1493
|
+
};
|
|
1494
|
+
}
|
|
1495
|
+
function blockedSteerPhase(request, phase, blockerCode, observationRequired, mutationBoundary, prepared, evidenceDigest) {
|
|
1496
|
+
return Object.freeze({
|
|
1497
|
+
...steerPhaseBase(request, phase, prepared),
|
|
1498
|
+
status: "blocked",
|
|
1499
|
+
blockerCode,
|
|
1500
|
+
observationRequired,
|
|
1501
|
+
mutationBoundary,
|
|
1502
|
+
...(evidenceDigest === undefined ? {} : { evidenceDigest })
|
|
1503
|
+
});
|
|
1504
|
+
}
|
|
1505
|
+
function uncertainSteerPhase(request, phase, prepared, blockerCode, quarantine, evidenceDigest) {
|
|
1506
|
+
return Object.freeze({
|
|
1507
|
+
...steerPhaseBase(request, phase, prepared),
|
|
1508
|
+
status: "uncertain",
|
|
1509
|
+
blockerCode,
|
|
1510
|
+
observationRequired: true,
|
|
1511
|
+
mutationBoundary: "control_may_have_occurred",
|
|
1512
|
+
quarantine,
|
|
1513
|
+
...(evidenceDigest === undefined ? {} : { evidenceDigest })
|
|
1514
|
+
});
|
|
1515
|
+
}
|
|
1516
|
+
/** Do not expose provider diagnostics or untrusted extra fields downstream. */
|
|
1517
|
+
function normalizeSteerProviderResult(value, request, phase, prepared) {
|
|
1518
|
+
const clone = cloneFrozenData(value);
|
|
1519
|
+
if (!isPlainDataRecord(clone))
|
|
1520
|
+
throw new Error("invalid steer phase result");
|
|
1521
|
+
const expected = steerPhaseBase(request, phase, prepared);
|
|
1522
|
+
for (const [key, expectedValue] of Object.entries(expected)) {
|
|
1523
|
+
if (readOwnData(clone, key) !== expectedValue)
|
|
1524
|
+
throw new Error("steer phase identity mismatch");
|
|
1525
|
+
}
|
|
1526
|
+
const status = readOwnData(clone, "status");
|
|
1527
|
+
const observationRequired = readOwnData(clone, "observationRequired");
|
|
1528
|
+
const mutationBoundary = readOwnData(clone, "mutationBoundary");
|
|
1529
|
+
if (typeof observationRequired !== "boolean"
|
|
1530
|
+
|| (mutationBoundary !== "none" && mutationBoundary !== "control_may_have_occurred")) {
|
|
1531
|
+
throw new Error("invalid steer phase boundary");
|
|
1532
|
+
}
|
|
1533
|
+
const evidenceDigest = readOwnData(clone, "evidenceDigest");
|
|
1534
|
+
if (evidenceDigest !== undefined && (typeof evidenceDigest !== "string" || !DIGEST_PATTERN.test(evidenceDigest))) {
|
|
1535
|
+
throw new Error("invalid steer evidence");
|
|
1536
|
+
}
|
|
1537
|
+
const blockerCode = readOwnData(clone, "blockerCode");
|
|
1538
|
+
if (blockerCode !== undefined && (typeof blockerCode !== "string" || !/^[a-z][a-z0-9_]{0,63}$/u.test(blockerCode))) {
|
|
1539
|
+
throw new Error("invalid steer blocker");
|
|
1540
|
+
}
|
|
1541
|
+
if (status === "prepared") {
|
|
1542
|
+
if (phase !== "prepare" || observationRequired !== false || mutationBoundary !== "none")
|
|
1543
|
+
throw new Error("invalid steer preparation");
|
|
1544
|
+
const preparedValue = readOwnData(clone, "prepared");
|
|
1545
|
+
if (!isPlainDataRecord(preparedValue))
|
|
1546
|
+
throw new Error("missing steer preparation");
|
|
1547
|
+
assertSteerPreparedIdentity(preparedValue, steerPhaseIdentity(request));
|
|
1548
|
+
return Object.freeze({
|
|
1549
|
+
...expected,
|
|
1550
|
+
status: "prepared",
|
|
1551
|
+
observationRequired: false,
|
|
1552
|
+
mutationBoundary: "none",
|
|
1553
|
+
prepared: cloneFrozenData(preparedValue)
|
|
1554
|
+
});
|
|
1555
|
+
}
|
|
1556
|
+
if (status === "executed") {
|
|
1557
|
+
if (phase !== "execute_prepared" || prepared === undefined || observationRequired !== true || mutationBoundary !== "control_may_have_occurred") {
|
|
1558
|
+
throw new Error("invalid steer execution");
|
|
1559
|
+
}
|
|
1560
|
+
return Object.freeze({
|
|
1561
|
+
...expected,
|
|
1562
|
+
status: "executed",
|
|
1563
|
+
observationRequired: true,
|
|
1564
|
+
mutationBoundary: "control_may_have_occurred"
|
|
1565
|
+
});
|
|
1566
|
+
}
|
|
1567
|
+
if (status === "satisfied") {
|
|
1568
|
+
if ((phase !== "verify" && phase !== "recovery") || prepared === undefined || observationRequired !== false || mutationBoundary !== "control_may_have_occurred") {
|
|
1569
|
+
throw new Error("invalid steer satisfaction");
|
|
1570
|
+
}
|
|
1571
|
+
const receipt = readOwnData(clone, "receipt");
|
|
1572
|
+
if (!isPlainDataRecord(receipt))
|
|
1573
|
+
throw new Error("missing steer receipt");
|
|
1574
|
+
assertSteerReceiptIdentity(receipt, prepared);
|
|
1575
|
+
return Object.freeze({
|
|
1576
|
+
...expected,
|
|
1577
|
+
status: "satisfied",
|
|
1578
|
+
observationRequired: false,
|
|
1579
|
+
mutationBoundary: "control_may_have_occurred",
|
|
1580
|
+
receipt: cloneFrozenData(receipt)
|
|
1581
|
+
});
|
|
1582
|
+
}
|
|
1583
|
+
if (status === "blocked") {
|
|
1584
|
+
if (typeof blockerCode !== "string" || readOwnData(clone, "quarantine") !== undefined)
|
|
1585
|
+
throw new Error("invalid steer blocker result");
|
|
1586
|
+
if (phase === "prepare" && mutationBoundary !== "none")
|
|
1587
|
+
throw new Error("preparation crossed mutation boundary");
|
|
1588
|
+
return Object.freeze({
|
|
1589
|
+
...expected,
|
|
1590
|
+
status: "blocked",
|
|
1591
|
+
blockerCode,
|
|
1592
|
+
observationRequired,
|
|
1593
|
+
mutationBoundary,
|
|
1594
|
+
...(evidenceDigest === undefined ? {} : { evidenceDigest })
|
|
1595
|
+
});
|
|
1596
|
+
}
|
|
1597
|
+
if (status === "uncertain") {
|
|
1598
|
+
const quarantine = readOwnData(clone, "quarantine");
|
|
1599
|
+
if (typeof blockerCode !== "string"
|
|
1600
|
+
|| observationRequired !== true
|
|
1601
|
+
|| mutationBoundary !== "control_may_have_occurred"
|
|
1602
|
+
|| (quarantine !== "caller" && quarantine !== "provider")
|
|
1603
|
+
|| phase === "prepare") {
|
|
1604
|
+
throw new Error("invalid steer uncertainty");
|
|
1605
|
+
}
|
|
1606
|
+
return Object.freeze({
|
|
1607
|
+
...expected,
|
|
1608
|
+
status: "uncertain",
|
|
1609
|
+
blockerCode,
|
|
1610
|
+
observationRequired: true,
|
|
1611
|
+
mutationBoundary: "control_may_have_occurred",
|
|
1612
|
+
quarantine,
|
|
1613
|
+
...(evidenceDigest === undefined ? {} : { evidenceDigest })
|
|
1614
|
+
});
|
|
1615
|
+
}
|
|
1616
|
+
throw new Error("invalid steer phase status");
|
|
1617
|
+
}
|
|
1618
|
+
function assertSteerPreparedIdentity(value, expected) {
|
|
1619
|
+
if (readOwnData(value, "schemaVersion") !== CONTROL_COORDINATOR_SCHEMA_VERSION
|
|
1620
|
+
|| readOwnData(value, "parentOperationId") !== expected.parentOperationId
|
|
1621
|
+
|| readOwnData(value, "parentRequestDigest") !== expected.parentRequestDigest
|
|
1622
|
+
|| readOwnData(value, "parentTargetBindingDigest") !== expected.parentTargetBindingDigest
|
|
1623
|
+
|| readOwnData(value, "controlActionId") !== expected.controlActionId
|
|
1624
|
+
|| readOwnData(value, "action") !== "steer"
|
|
1625
|
+
|| readOwnData(value, "requestDigest") !== expected.requestDigest
|
|
1626
|
+
|| readOwnData(value, "expectedAssistantTurnId") !== expected.expectedAssistantTurnId) {
|
|
1627
|
+
throw new Error("steer prepared identity mismatch");
|
|
1628
|
+
}
|
|
1629
|
+
for (const key of ["assistantBranchId", "assistantParentTurnId", "baselineSnapshotDigest", "preparedDigest"]) {
|
|
1630
|
+
const field = readOwnData(value, key);
|
|
1631
|
+
if (typeof field !== "string" || field.length === 0)
|
|
1632
|
+
throw new Error("steer prepared evidence is invalid");
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
function assertSteerReceiptIdentity(value, prepared) {
|
|
1636
|
+
if (readOwnData(value, "schemaVersion") !== CONTROL_COORDINATOR_SCHEMA_VERSION
|
|
1637
|
+
|| readOwnData(value, "baselineSnapshotDigest") !== prepared.baselineSnapshotDigest
|
|
1638
|
+
|| readOwnData(value, "preparedDigest") !== prepared.preparedDigest
|
|
1639
|
+
|| readOwnData(value, "assistantTurnId") !== prepared.expectedAssistantTurnId
|
|
1640
|
+
|| readOwnData(value, "assistantBranchId") !== prepared.assistantBranchId
|
|
1641
|
+
|| readOwnData(value, "assistantParentTurnId") !== prepared.assistantParentTurnId) {
|
|
1642
|
+
throw new Error("steer receipt identity mismatch");
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
function phaseFailureCode(signal, deadlineAt, error) {
|
|
1646
|
+
if (signal.aborted)
|
|
1647
|
+
return "operation_cancelled";
|
|
1648
|
+
const code = error !== null && typeof error === "object" && "code" in error
|
|
1649
|
+
? error.code
|
|
1650
|
+
: undefined;
|
|
1651
|
+
if (code === "deadline_exceeded")
|
|
1652
|
+
return "operation_timeout";
|
|
1653
|
+
if (code === "aborted")
|
|
1654
|
+
return "operation_cancelled";
|
|
1655
|
+
if (Date.now() >= deadlineAt)
|
|
1656
|
+
return "operation_timeout";
|
|
1657
|
+
if (code === "navigation_mismatch" || code === "claim_mismatch" || code === "page_mismatch" || code === "target_binding_mismatch") {
|
|
1658
|
+
return "target_binding_mismatch";
|
|
1659
|
+
}
|
|
1660
|
+
return "target_evidence_unavailable";
|
|
1661
|
+
}
|
|
1662
|
+
/**
|
|
1663
|
+
* Rehydrate a missing binding only through the durable recovery context. The
|
|
1664
|
+
* helper never selects a new page or target and never performs a mutation.
|
|
1665
|
+
*/
|
|
1666
|
+
async function steerBindingFor(bindings, ensureRecovered, recovery, operationId, requestDigest, signal) {
|
|
1667
|
+
const binding = bindingFor(bindings, operationId, undefined);
|
|
1668
|
+
if (binding !== undefined || recovery === undefined)
|
|
1669
|
+
return binding;
|
|
1670
|
+
try {
|
|
1671
|
+
await ensureRecovered(operationId, requestDigest, recovery.surface, signal);
|
|
1672
|
+
}
|
|
1673
|
+
catch {
|
|
1674
|
+
return undefined;
|
|
1675
|
+
}
|
|
1676
|
+
return bindingFor(bindings, operationId, undefined);
|
|
1677
|
+
}
|
|
1678
|
+
/**
|
|
1679
|
+
* Unlike a plain `withTabTransaction` call, retain and await the provider
|
|
1680
|
+
* callback when the coordinator rejects its public promise at a deadline.
|
|
1681
|
+
*/
|
|
1682
|
+
async function runSteerControlTransaction(binding, operationId, signal, deadlineAt, transactionTimeoutMs, observeCurrentTarget, evidenceDigest, callback) {
|
|
1683
|
+
let workPromise;
|
|
1684
|
+
const transaction = binding.withTabTransaction(boundedRequest(signal, deadlineAt, transactionTimeoutMs, "operation-control-steer-execute", "control"), async (context) => {
|
|
1685
|
+
workPromise = (async () => {
|
|
1686
|
+
const current = await readCurrentTarget(binding, operationId, context.page, observeCurrentTarget, evidenceDigest, context.acquisition.signal, context.acquisition.timing.deadlineAt);
|
|
1687
|
+
binding.assertCurrent(current.evidence, current.authoritativeClaim);
|
|
1688
|
+
return await callback(context);
|
|
1689
|
+
})();
|
|
1690
|
+
return await workPromise;
|
|
1691
|
+
});
|
|
1692
|
+
try {
|
|
1693
|
+
return await transaction;
|
|
1694
|
+
}
|
|
1695
|
+
catch (error) {
|
|
1696
|
+
if (workPromise !== undefined)
|
|
1697
|
+
await workPromise.catch(() => undefined);
|
|
1698
|
+
throw error;
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
/** Link caller cancellation to a phase-local signal without retaining it. */
|
|
1702
|
+
function createLinkedAbortController(source) {
|
|
1703
|
+
const controller = new AbortController();
|
|
1704
|
+
if (source === undefined)
|
|
1705
|
+
return { controller, cleanup: () => undefined };
|
|
1706
|
+
const onAbort = () => {
|
|
1707
|
+
if (!controller.signal.aborted)
|
|
1708
|
+
controller.abort(source.reason);
|
|
1709
|
+
};
|
|
1710
|
+
if (source.aborted)
|
|
1711
|
+
onAbort();
|
|
1712
|
+
else
|
|
1713
|
+
source.addEventListener("abort", onAbort, { once: true });
|
|
1714
|
+
return {
|
|
1715
|
+
controller,
|
|
1716
|
+
cleanup: () => source.removeEventListener("abort", onAbort)
|
|
1717
|
+
};
|
|
1718
|
+
}
|
|
1719
|
+
/**
|
|
1720
|
+
* Adapt one provider's one-shot Send observers to the four phase protocol.
|
|
1721
|
+
* Precondition reads use the phase's active actor; each postcondition probe
|
|
1722
|
+
* acquires a fresh read actor and releases it before SendOnce sleeps/polls.
|
|
1723
|
+
*/
|
|
1724
|
+
function createPhaseSendObservers(binding, operationId, baseObservers, controller, observeCurrentTarget, evidenceDigest, transactionTimeoutMs) {
|
|
1725
|
+
let activeContext;
|
|
1726
|
+
const observers = {
|
|
1727
|
+
observePrecondition: async (request) => {
|
|
1728
|
+
const context = activeContext;
|
|
1729
|
+
const signal = context?.acquisition.signal ?? controller.signal;
|
|
1730
|
+
return await baseObservers.observePrecondition({
|
|
1731
|
+
...request,
|
|
1732
|
+
...(context === undefined ? {} : { page: context.page }),
|
|
1733
|
+
signal,
|
|
1734
|
+
...(context?.acquisition.timing.deadlineAt === undefined
|
|
1735
|
+
? {}
|
|
1736
|
+
: { deadlineAt: context.acquisition.timing.deadlineAt })
|
|
1737
|
+
});
|
|
1738
|
+
},
|
|
1739
|
+
observePostcondition: async (request) => {
|
|
1740
|
+
const probe = await runSendTransaction(binding, operationId, controller, observeCurrentTarget, evidenceDigest, request.deadlineAt, transactionTimeoutMs, "operation-send-postcondition", "read", context => baseObservers.observePostcondition({
|
|
1741
|
+
...request,
|
|
1742
|
+
page: context.page,
|
|
1743
|
+
signal: context.acquisition.signal,
|
|
1744
|
+
...(context.acquisition.timing.deadlineAt === undefined
|
|
1745
|
+
? {}
|
|
1746
|
+
: { deadlineAt: context.acquisition.timing.deadlineAt })
|
|
1747
|
+
}));
|
|
1748
|
+
if (isPlainPostconditionResult(probe)
|
|
1749
|
+
&& probe.status === "blocked"
|
|
1750
|
+
&& (probe.blockerCode === "target_evidence_unavailable" || probe.blockerCode === "ambiguous_submit")) {
|
|
1751
|
+
return { result: probe, retryable: true };
|
|
1752
|
+
}
|
|
1753
|
+
return probe;
|
|
1754
|
+
},
|
|
1755
|
+
...(baseObservers.sleep === undefined ? {} : { sleep: baseObservers.sleep }),
|
|
1756
|
+
...(baseObservers.maxPostconditionAttempts === undefined ? {} : { maxPostconditionAttempts: baseObservers.maxPostconditionAttempts }),
|
|
1757
|
+
...(baseObservers.postconditionIntervalMs === undefined ? {} : { postconditionIntervalMs: baseObservers.postconditionIntervalMs }),
|
|
1758
|
+
...(baseObservers.postconditionTimeoutMs === undefined ? {} : { postconditionTimeoutMs: baseObservers.postconditionTimeoutMs })
|
|
1759
|
+
};
|
|
1760
|
+
return {
|
|
1761
|
+
observers,
|
|
1762
|
+
withContext: async (context, callback) => {
|
|
1763
|
+
activeContext = context;
|
|
1764
|
+
try {
|
|
1765
|
+
return await callback();
|
|
1766
|
+
}
|
|
1767
|
+
finally {
|
|
1768
|
+
if (activeContext === context)
|
|
1769
|
+
activeContext = undefined;
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
};
|
|
1773
|
+
}
|
|
1774
|
+
/**
|
|
1775
|
+
* Run a single target revalidation plus provider callback in one coordinator
|
|
1776
|
+
* transaction. The coordinator may reject its public promise at a deadline
|
|
1777
|
+
* while its callback is still in flight; retain the callback promise and await
|
|
1778
|
+
* it before propagating that error so an activation can never be overlapped.
|
|
1779
|
+
*/
|
|
1780
|
+
async function runSendTransaction(binding, operationId, controller, observeCurrentTarget, evidenceDigest, requestedDeadlineAt, transactionTimeoutMs, label, priority, callback) {
|
|
1781
|
+
let workPromise;
|
|
1782
|
+
const transaction = binding.withTabTransaction(boundedRequest(controller.signal, requestedDeadlineAt, transactionTimeoutMs, label, priority), async (context) => {
|
|
1783
|
+
const abortFromCoordinator = () => {
|
|
1784
|
+
if (!controller.signal.aborted)
|
|
1785
|
+
controller.abort(context.acquisition.signal.reason);
|
|
1786
|
+
};
|
|
1787
|
+
if (context.acquisition.signal.aborted)
|
|
1788
|
+
abortFromCoordinator();
|
|
1789
|
+
else
|
|
1790
|
+
context.acquisition.signal.addEventListener("abort", abortFromCoordinator, { once: true });
|
|
1791
|
+
workPromise = (async () => {
|
|
1792
|
+
const current = await readCurrentTarget(binding, operationId, context.page, observeCurrentTarget, evidenceDigest, context.acquisition.signal, context.acquisition.timing.deadlineAt);
|
|
1793
|
+
binding.assertCurrent(current.evidence, current.authoritativeClaim);
|
|
1794
|
+
return await callback(context);
|
|
1795
|
+
})();
|
|
1796
|
+
try {
|
|
1797
|
+
return await workPromise;
|
|
1798
|
+
}
|
|
1799
|
+
finally {
|
|
1800
|
+
context.acquisition.signal.removeEventListener("abort", abortFromCoordinator);
|
|
1801
|
+
}
|
|
1802
|
+
});
|
|
1803
|
+
try {
|
|
1804
|
+
return await transaction;
|
|
1805
|
+
}
|
|
1806
|
+
catch (error) {
|
|
1807
|
+
// `workPromise` is set before target proof/provider work starts. Awaiting it
|
|
1808
|
+
// here preserves coordinator quarantine semantics without releasing an
|
|
1809
|
+
// actor while the browser call is still running.
|
|
1810
|
+
if (workPromise !== undefined)
|
|
1811
|
+
await workPromise.catch(() => undefined);
|
|
1812
|
+
throw error;
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
function blockedPrepareSend(blockerCode) {
|
|
1816
|
+
return { status: "blocked", result: { status: "blocked", blockerCode } };
|
|
1817
|
+
}
|
|
1818
|
+
function resultToPrepareSend(result) {
|
|
1819
|
+
return { status: "blocked", result };
|
|
1820
|
+
}
|
|
1821
|
+
function blockedExecuteSend(blockerCode) {
|
|
1822
|
+
return { status: "blocked", result: { status: "blocked", blockerCode } };
|
|
1823
|
+
}
|
|
1824
|
+
function phaseBlocker(signal, deadlineAt) {
|
|
1825
|
+
if (signal.aborted)
|
|
1826
|
+
return "operation_cancelled";
|
|
1827
|
+
if (deadlineAt !== undefined && Date.now() >= deadlineAt)
|
|
1828
|
+
return "operation_timeout";
|
|
1829
|
+
return "port_protocol_violation";
|
|
1830
|
+
}
|
|
1831
|
+
function submissionPreparedFromSendOnce(prepared, expected) {
|
|
1832
|
+
const baseline = prepared.baseline.ownershipBaseline;
|
|
1833
|
+
if (baseline === undefined)
|
|
1834
|
+
throw new Error("missing durable Send baseline");
|
|
1835
|
+
return normalizeSubmissionPreparedSend({
|
|
1836
|
+
prepared: cloneFrozenData(prepared),
|
|
1837
|
+
baseline: cloneFrozenData(baseline),
|
|
1838
|
+
evidenceDigest: prepared.observation.evidenceDigest
|
|
1839
|
+
}, expected, {
|
|
1840
|
+
operationId: prepared.operationId,
|
|
1841
|
+
requestDigest: prepared.requestDigest,
|
|
1842
|
+
surface: prepared.surface,
|
|
1843
|
+
actionId: prepared.actionId
|
|
1844
|
+
});
|
|
1845
|
+
}
|
|
1846
|
+
function normalizeSubmissionPreparedSend(value, expected, identity) {
|
|
1847
|
+
const clone = cloneFrozenData(value);
|
|
1848
|
+
if (!isPlainDataRecord(clone))
|
|
1849
|
+
throw new Error("invalid opaque Send capability");
|
|
1850
|
+
assertExactDataKeys(clone, ["prepared", "baseline", "evidenceDigest"]);
|
|
1851
|
+
const prepared = readOwnData(clone, "prepared");
|
|
1852
|
+
const baseline = readOwnData(clone, "baseline");
|
|
1853
|
+
const evidence = readOwnData(clone, "evidenceDigest");
|
|
1854
|
+
if (!isPlainDataRecord(prepared) || !isPlainDataRecord(baseline) || typeof evidence !== "string" || !DIGEST_PATTERN.test(evidence)) {
|
|
1855
|
+
throw new Error("invalid opaque Send capability");
|
|
1856
|
+
}
|
|
1857
|
+
assertExactDataKeys(prepared, ["schemaVersion", "operationId", "requestDigest", "surface", "actionId", "expected", "observation", "baseline"]);
|
|
1858
|
+
if (readOwnData(prepared, "schemaVersion") !== "chatgpt.browser_control.send_once_prepared.v1"
|
|
1859
|
+
|| readOwnData(prepared, "operationId") !== identity.operationId
|
|
1860
|
+
|| readOwnData(prepared, "requestDigest") !== identity.requestDigest
|
|
1861
|
+
|| readOwnData(prepared, "surface") !== identity.surface
|
|
1862
|
+
|| readOwnData(prepared, "actionId") !== identity.actionId
|
|
1863
|
+
|| canonicalJson(readOwnData(prepared, "expected")) !== canonicalJson(expected)) {
|
|
1864
|
+
throw new Error("opaque Send identity mismatch");
|
|
1865
|
+
}
|
|
1866
|
+
const preparedBaseline = readOwnData(prepared, "baseline");
|
|
1867
|
+
if (!isPlainDataRecord(preparedBaseline)) {
|
|
1868
|
+
throw new Error("missing opaque Send baseline");
|
|
1869
|
+
}
|
|
1870
|
+
const ownershipBaseline = readOwnData(preparedBaseline, "ownershipBaseline");
|
|
1871
|
+
if (!isPlainDataRecord(ownershipBaseline) || canonicalJson(ownershipBaseline) !== canonicalJson(baseline)) {
|
|
1872
|
+
throw new Error("opaque Send baseline mismatch");
|
|
1873
|
+
}
|
|
1874
|
+
const observation = readOwnData(prepared, "observation");
|
|
1875
|
+
if (!isPlainDataRecord(observation) || readOwnData(observation, "status") !== "exact" || readOwnData(observation, "evidenceDigest") !== evidence) {
|
|
1876
|
+
throw new Error("opaque Send observation mismatch");
|
|
1877
|
+
}
|
|
1878
|
+
return clone;
|
|
1879
|
+
}
|
|
1880
|
+
function assertExactDataKeys(value, keys) {
|
|
1881
|
+
const allowed = new Set(keys);
|
|
1882
|
+
for (const key of Object.keys(value)) {
|
|
1883
|
+
if (!allowed.has(key))
|
|
1884
|
+
throw new Error("unsupported opaque Send field");
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
function markEstablished(binding, result) {
|
|
1888
|
+
if (binding.markTargetEstablished !== undefined
|
|
1889
|
+
&& (result.status === "submitted" || result.status === "already_submitted")
|
|
1890
|
+
&& result.targetEstablishment !== undefined) {
|
|
1891
|
+
binding.markTargetEstablished({
|
|
1892
|
+
conversationId: result.targetEstablishment.conversationId,
|
|
1893
|
+
canonicalThreadUrl: result.targetEstablishment.canonicalThreadUrl
|
|
1894
|
+
});
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
async function readCurrentTarget(binding, operationId, page, observeCurrentTarget, evidenceDigest, signal = new AbortController().signal, deadlineAt) {
|
|
1898
|
+
if (observeCurrentTarget !== undefined) {
|
|
1899
|
+
const observed = await observeCurrentTarget({ page, operationId, target: binding.target, signal, ...(deadlineAt === undefined ? {} : { deadlineAt }) });
|
|
1900
|
+
return normalizeCurrentTargetResult(observed);
|
|
1901
|
+
}
|
|
1902
|
+
// The default read is a bounded, read-only browser observation. Provider
|
|
1903
|
+
// implementations can inject a narrower identity probe when their DOM
|
|
1904
|
+
// exposes stable target evidence without a complete turn snapshot.
|
|
1905
|
+
const target = binding.target;
|
|
1906
|
+
const boundThreadId = binding.evidence.thread.status === "available"
|
|
1907
|
+
? binding.evidence.thread.value
|
|
1908
|
+
: undefined;
|
|
1909
|
+
const boundClaim = binding.evidence.authoritativeTabClaim.status === "available"
|
|
1910
|
+
? binding.evidence.authoritativeTabClaim.value
|
|
1911
|
+
: undefined;
|
|
1912
|
+
const result = await observeBrowserPage(page, {
|
|
1913
|
+
operationId,
|
|
1914
|
+
target: {
|
|
1915
|
+
providerId: target.providerId,
|
|
1916
|
+
browserId: target.browserId,
|
|
1917
|
+
tabId: target.tabId,
|
|
1918
|
+
coordinationScope: target.coordinationScope,
|
|
1919
|
+
...(boundClaim === undefined ? {} : { authoritativeTabClaim: boundClaim }),
|
|
1920
|
+
...(target.targetLifecycle === undefined ? {} : { targetLifecycle: target.targetLifecycle }),
|
|
1921
|
+
...(target.conversationId === undefined ? {} : { expectedConversationId: target.conversationId }),
|
|
1922
|
+
...(boundThreadId === undefined ? {} : { expectedThreadId: boundThreadId })
|
|
1923
|
+
},
|
|
1924
|
+
// An injected digest is required for target evidence. Never manufacture a
|
|
1925
|
+
// placeholder digest: unavailable evidence must remain unavailable.
|
|
1926
|
+
evidenceDigest: evidenceDigest ?? (() => {
|
|
1927
|
+
throw new OperationBrowserAdapterError("target_evidence_unavailable");
|
|
1928
|
+
}),
|
|
1929
|
+
responseContent: "metadata"
|
|
1930
|
+
});
|
|
1931
|
+
return { evidence: result.snapshot.target };
|
|
1932
|
+
}
|
|
1933
|
+
function normalizeCurrentTargetResult(value) {
|
|
1934
|
+
if (!isPlainDataRecord(value))
|
|
1935
|
+
throw new OperationBrowserAdapterError("target_evidence_unavailable");
|
|
1936
|
+
const evidence = readOwnData(value, "evidence");
|
|
1937
|
+
if (evidence === undefined)
|
|
1938
|
+
throw new OperationBrowserAdapterError("target_evidence_unavailable");
|
|
1939
|
+
const authoritativeClaim = readOwnData(value, "authoritativeClaim");
|
|
1940
|
+
if (!isSafeDataGraph(evidence) || (authoritativeClaim !== undefined && !isSafeDataGraph(authoritativeClaim))) {
|
|
1941
|
+
throw new OperationBrowserAdapterError("target_evidence_unavailable");
|
|
1942
|
+
}
|
|
1943
|
+
try {
|
|
1944
|
+
for (const key of Object.keys(value)) {
|
|
1945
|
+
if (key !== "evidence" && key !== "authoritativeClaim") {
|
|
1946
|
+
throw new Error("unsupported current target field");
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
catch {
|
|
1951
|
+
throw new OperationBrowserAdapterError("target_evidence_unavailable");
|
|
1952
|
+
}
|
|
1953
|
+
return Object.freeze({
|
|
1954
|
+
evidence: evidence,
|
|
1955
|
+
...(authoritativeClaim === undefined ? {} : { authoritativeClaim: authoritativeClaim })
|
|
1956
|
+
});
|
|
1957
|
+
}
|
|
1958
|
+
function boundedRequest(signal, requestedDeadlineAt, transactionTimeoutMs, label, priority = "read") {
|
|
1959
|
+
const localDeadline = Date.now() + transactionTimeoutMs;
|
|
1960
|
+
return {
|
|
1961
|
+
priority,
|
|
1962
|
+
signal: signal ?? new AbortController().signal,
|
|
1963
|
+
deadlineAt: Math.min(requestedDeadlineAt ?? MAX_DEADLINE_AT, localDeadline),
|
|
1964
|
+
label
|
|
1965
|
+
};
|
|
1966
|
+
}
|
|
1967
|
+
async function defaultCollectorObservation(request, transaction, context, evidenceDigest, authoritativeClaim) {
|
|
1968
|
+
const target = transaction.target;
|
|
1969
|
+
const observation = await observeBrowserPage(transaction.page, {
|
|
1970
|
+
operationId: request.operationId,
|
|
1971
|
+
target: {
|
|
1972
|
+
providerId: target.providerId,
|
|
1973
|
+
browserId: target.browserId,
|
|
1974
|
+
tabId: target.tabId,
|
|
1975
|
+
coordinationScope: target.coordinationScope,
|
|
1976
|
+
...(authoritativeClaim === undefined ? {} : { authoritativeTabClaim: authoritativeClaim.token }),
|
|
1977
|
+
...(target.conversationId === undefined ? {} : { expectedConversationId: target.conversationId })
|
|
1978
|
+
},
|
|
1979
|
+
evidenceDigest,
|
|
1980
|
+
responseContent: request.responseContent,
|
|
1981
|
+
...(request.responseFormat === undefined ? {} : { responseFormat: request.responseFormat }),
|
|
1982
|
+
...(context.baseline === undefined ? {} : { baseline: context.baseline }),
|
|
1983
|
+
...(context.prior?.assistantTurnId === undefined ? {} : {
|
|
1984
|
+
terminalAssistantTurnId: context.prior.assistantTurnId,
|
|
1985
|
+
...(request.responseContent === "include" ? { rawAssistantTurnId: context.prior.assistantTurnId } : {})
|
|
1986
|
+
})
|
|
1987
|
+
});
|
|
1988
|
+
return {
|
|
1989
|
+
schemaVersion: "chatgpt.browser_control.collector.v1",
|
|
1990
|
+
snapshot: observation.snapshot,
|
|
1991
|
+
...(observation.terminal === undefined ? {} : { terminal: observation.terminal })
|
|
1992
|
+
};
|
|
1993
|
+
}
|
|
1994
|
+
function sleepOutsideCoordinator(milliseconds, signal) {
|
|
1995
|
+
if (!Number.isSafeInteger(milliseconds) || milliseconds < 0 || milliseconds > 60_000) {
|
|
1996
|
+
return Promise.reject(new OperationBrowserAdapterError("adapter_incomplete"));
|
|
1997
|
+
}
|
|
1998
|
+
if (signal.aborted)
|
|
1999
|
+
return Promise.reject(new OperationBrowserAdapterError("browser_bridge_unavailable"));
|
|
2000
|
+
return new Promise((resolve, reject) => {
|
|
2001
|
+
const timer = setTimeout(() => {
|
|
2002
|
+
signal.removeEventListener("abort", onAbort);
|
|
2003
|
+
resolve();
|
|
2004
|
+
}, milliseconds);
|
|
2005
|
+
const onAbort = () => {
|
|
2006
|
+
clearTimeout(timer);
|
|
2007
|
+
signal.removeEventListener("abort", onAbort);
|
|
2008
|
+
reject(new OperationBrowserAdapterError("browser_bridge_unavailable"));
|
|
2009
|
+
};
|
|
2010
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
2011
|
+
});
|
|
2012
|
+
}
|
|
2013
|
+
function isPlainPostconditionResult(value) {
|
|
2014
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
2015
|
+
return false;
|
|
2016
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, "status");
|
|
2017
|
+
return descriptor !== undefined && "value" in descriptor && typeof descriptor.value === "string";
|
|
2018
|
+
}
|
|
2019
|
+
function errorCode(error) {
|
|
2020
|
+
if (error instanceof OperationBrowserAdapterError)
|
|
2021
|
+
return error.code;
|
|
2022
|
+
return "target_evidence_unavailable";
|
|
2023
|
+
}
|