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.
Files changed (191) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +27 -0
  3. package/contracts/v1/fixtures/backend-capabilities.json +40 -1
  4. package/contracts/v1/fixtures/backend-compatibility.json +21 -0
  5. package/contracts/v1/fixtures/backend-version.json +6 -1
  6. package/contracts/v1/fixtures/operation-action-prepared-event.json +42 -0
  7. package/contracts/v1/fixtures/operation-action.json +13 -0
  8. package/contracts/v1/fixtures/operation-artifact-receipt.json +14 -0
  9. package/contracts/v1/fixtures/operation-artifact-transfer-intent-event.json +24 -0
  10. package/contracts/v1/fixtures/operation-artifact-transfer-receipt-event.json +26 -0
  11. package/contracts/v1/fixtures/operation-artifact-transfer-state.json +178 -0
  12. package/contracts/v1/fixtures/operation-blocker.json +10 -0
  13. package/contracts/v1/fixtures/operation-collect-request.json +17 -0
  14. package/contracts/v1/fixtures/operation-collect-result.json +42 -0
  15. package/contracts/v1/fixtures/operation-control-receipt.json +13 -0
  16. package/contracts/v1/fixtures/operation-control-request.json +18 -0
  17. package/contracts/v1/fixtures/operation-control-result.json +31 -0
  18. package/contracts/v1/fixtures/operation-event.json +18 -0
  19. package/contracts/v1/fixtures/operation-handle.json +10 -0
  20. package/contracts/v1/fixtures/operation-inspect-request.json +13 -0
  21. package/contracts/v1/fixtures/operation-inspect-result.json +205 -0
  22. package/contracts/v1/fixtures/operation-ownership-baseline-event.json +34 -0
  23. package/contracts/v1/fixtures/operation-receipt.json +32 -0
  24. package/contracts/v1/fixtures/operation-recovery-decision.json +7 -0
  25. package/contracts/v1/fixtures/operation-recovery-observation.json +12 -0
  26. package/contracts/v1/fixtures/operation-request.json +33 -0
  27. package/contracts/v1/fixtures/operation-state.json +144 -0
  28. package/contracts/v1/fixtures/operation-submission-witness-event.json +20 -0
  29. package/contracts/v1/fixtures/operation-submission-witness.json +11 -0
  30. package/contracts/v1/fixtures/operation-submit-result.json +16 -0
  31. package/contracts/v1/fixtures/operation-target-established-event.json +21 -0
  32. package/contracts/v1/manifest.json +155 -1
  33. package/contracts/v1/parity-suite.json +96 -1
  34. package/contracts/v1/schemas/backend-compatibility.schema.json +65 -0
  35. package/contracts/v1/schemas/backend-request.schema.json +8 -2
  36. package/contracts/v1/schemas/capabilities.schema.json +81 -1
  37. package/contracts/v1/schemas/manifest.schema.json +57 -0
  38. package/contracts/v1/schemas/operation-action.schema.json +155 -0
  39. package/contracts/v1/schemas/operation-artifact-receipt.schema.json +89 -0
  40. package/contracts/v1/schemas/operation-blocker.schema.json +70 -0
  41. package/contracts/v1/schemas/operation-collect-request.schema.json +46 -0
  42. package/contracts/v1/schemas/operation-collect-result.schema.json +74 -0
  43. package/contracts/v1/schemas/operation-control-receipt.schema.json +108 -0
  44. package/contracts/v1/schemas/operation-control-request.schema.json +111 -0
  45. package/contracts/v1/schemas/operation-control-result.schema.json +112 -0
  46. package/contracts/v1/schemas/operation-event.schema.json +756 -0
  47. package/contracts/v1/schemas/operation-handle.schema.json +55 -0
  48. package/contracts/v1/schemas/operation-inspect-request.schema.json +42 -0
  49. package/contracts/v1/schemas/operation-inspect-result.schema.json +1662 -0
  50. package/contracts/v1/schemas/operation-receipt.schema.json +153 -0
  51. package/contracts/v1/schemas/operation-recovery.schema.json +314 -0
  52. package/contracts/v1/schemas/operation-request.schema.json +158 -0
  53. package/contracts/v1/schemas/operation-state.schema.json +691 -0
  54. package/contracts/v1/schemas/operation-submission-witness.schema.json +48 -0
  55. package/contracts/v1/schemas/operation-submit-result.schema.json +94 -0
  56. package/contracts/v1/surface-drift-policy.json +50 -0
  57. package/contracts/v1/vectors/operation-request-digest-v1.json +43 -0
  58. package/dist/codex-chatgpt-control-backend.mjs +43897 -9094
  59. package/dist/codex-chatgpt-control.bundle.mjs +46360 -10262
  60. package/dist/src/backend/client.d.ts +103 -2
  61. package/dist/src/backend/client.js +1436 -97
  62. package/dist/src/backend/compatibility.d.ts +12 -0
  63. package/dist/src/backend/compatibility.js +208 -0
  64. package/dist/src/backend/protocol.d.ts +84 -1
  65. package/dist/src/backend/protocol.js +31 -5
  66. package/dist/src/backend/runtime-identity.d.ts +10 -0
  67. package/dist/src/backend/runtime-identity.js +141 -0
  68. package/dist/src/backend/session.d.ts +10 -2
  69. package/dist/src/backend/session.js +478 -7
  70. package/dist/src/backend/stdio-server.d.ts +4 -1
  71. package/dist/src/backend/stdio-server.js +240 -28
  72. package/dist/src/browser/active-composer-file-input.d.ts +9 -0
  73. package/dist/src/browser/active-composer-file-input.js +33 -0
  74. package/dist/src/browser/attach.d.ts +4 -1
  75. package/dist/src/browser/attach.js +136 -28
  76. package/dist/src/browser/downloads.d.ts +2 -0
  77. package/dist/src/client.d.ts +38 -0
  78. package/dist/src/client.js +1222 -59
  79. package/dist/src/commands/configuration.js +13 -7
  80. package/dist/src/commands/doctor.d.ts +1 -1
  81. package/dist/src/commands/doctor.js +26 -3
  82. package/dist/src/commands/experience.js +35 -0
  83. package/dist/src/commands/files.js +4 -27
  84. package/dist/src/commands/modes.js +49 -17
  85. package/dist/src/commands/power-discovery.d.ts +130 -0
  86. package/dist/src/commands/power-discovery.js +627 -0
  87. package/dist/src/commands/project-sources.js +325 -31
  88. package/dist/src/commands/sequence.js +59 -32
  89. package/dist/src/commands/session.js +7 -2
  90. package/dist/src/commands/work.js +31 -0
  91. package/dist/src/errors.d.ts +9 -0
  92. package/dist/src/errors.js +22 -0
  93. package/dist/src/index.d.ts +6 -0
  94. package/dist/src/index.js +9 -0
  95. package/dist/src/operations/artifact-output.d.ts +69 -0
  96. package/dist/src/operations/artifact-output.js +1748 -0
  97. package/dist/src/operations/artifact-stream.d.ts +19 -0
  98. package/dist/src/operations/artifact-stream.js +35 -0
  99. package/dist/src/operations/artifact-transfer.d.ts +149 -0
  100. package/dist/src/operations/artifact-transfer.js +1312 -0
  101. package/dist/src/operations/browser-adapter.d.ts +161 -0
  102. package/dist/src/operations/browser-adapter.js +2023 -0
  103. package/dist/src/operations/browser-observation.d.ts +110 -0
  104. package/dist/src/operations/browser-observation.js +1208 -0
  105. package/dist/src/operations/browser-target.d.ts +103 -0
  106. package/dist/src/operations/browser-target.js +489 -0
  107. package/dist/src/operations/canonical.d.ts +4 -0
  108. package/dist/src/operations/canonical.js +412 -0
  109. package/dist/src/operations/chatgpt-runtime.d.ts +72 -0
  110. package/dist/src/operations/chatgpt-runtime.js +1150 -0
  111. package/dist/src/operations/client.d.ts +138 -0
  112. package/dist/src/operations/client.js +1143 -0
  113. package/dist/src/operations/collector.d.ts +195 -0
  114. package/dist/src/operations/collector.js +1039 -0
  115. package/dist/src/operations/control.d.ts +372 -0
  116. package/dist/src/operations/control.js +1498 -0
  117. package/dist/src/operations/file-identity.d.ts +33 -0
  118. package/dist/src/operations/file-identity.js +153 -0
  119. package/dist/src/operations/handle.d.ts +19 -0
  120. package/dist/src/operations/handle.js +727 -0
  121. package/dist/src/operations/index.d.ts +22 -0
  122. package/dist/src/operations/index.js +22 -0
  123. package/dist/src/operations/journal.d.ts +142 -0
  124. package/dist/src/operations/journal.js +1931 -0
  125. package/dist/src/operations/production-attachments.d.ts +116 -0
  126. package/dist/src/operations/production-attachments.js +1018 -0
  127. package/dist/src/operations/production-chatgpt-artifacts.d.ts +74 -0
  128. package/dist/src/operations/production-chatgpt-artifacts.js +1146 -0
  129. package/dist/src/operations/production-chatgpt-attachments.d.ts +71 -0
  130. package/dist/src/operations/production-chatgpt-attachments.js +1563 -0
  131. package/dist/src/operations/production-configuration.d.ts +42 -0
  132. package/dist/src/operations/production-configuration.js +1363 -0
  133. package/dist/src/operations/production-primitives.d.ts +53 -0
  134. package/dist/src/operations/production-primitives.js +1073 -0
  135. package/dist/src/operations/production-work-steer.d.ts +203 -0
  136. package/dist/src/operations/production-work-steer.js +1218 -0
  137. package/dist/src/operations/recovery.d.ts +80 -0
  138. package/dist/src/operations/recovery.js +174 -0
  139. package/dist/src/operations/runtime-adapter.d.ts +118 -0
  140. package/dist/src/operations/runtime-adapter.js +786 -0
  141. package/dist/src/operations/send-once.d.ts +224 -0
  142. package/dist/src/operations/send-once.js +1081 -0
  143. package/dist/src/operations/service.d.ts +281 -0
  144. package/dist/src/operations/service.js +2810 -0
  145. package/dist/src/operations/staging.d.ts +136 -0
  146. package/dist/src/operations/staging.js +630 -0
  147. package/dist/src/operations/state-machine.d.ts +29 -0
  148. package/dist/src/operations/state-machine.js +2047 -0
  149. package/dist/src/operations/submission.d.ts +423 -0
  150. package/dist/src/operations/submission.js +1676 -0
  151. package/dist/src/operations/turn-ownership.d.ts +202 -0
  152. package/dist/src/operations/turn-ownership.js +700 -0
  153. package/dist/src/operations/types.d.ts +454 -0
  154. package/dist/src/operations/types.js +20 -0
  155. package/dist/src/operations/wire-requests.d.ts +21 -0
  156. package/dist/src/operations/wire-requests.js +396 -0
  157. package/dist/src/operations/wire-results.d.ts +97 -0
  158. package/dist/src/operations/wire-results.js +818 -0
  159. package/dist/src/runner/responses.d.ts +3 -1
  160. package/dist/src/runner/responses.js +19 -1
  161. package/dist/src/runner/result.js +227 -55
  162. package/dist/src/runner/types.d.ts +12 -0
  163. package/dist/src/runtime/command-routing.d.ts +149 -0
  164. package/dist/src/runtime/command-routing.js +431 -0
  165. package/dist/src/runtime/coordinated-browser.d.ts +24 -0
  166. package/dist/src/runtime/coordinated-browser.js +315 -0
  167. package/dist/src/runtime/coordinated-page.d.ts +39 -0
  168. package/dist/src/runtime/coordinated-page.js +634 -0
  169. package/dist/src/runtime/operation-context.d.ts +142 -0
  170. package/dist/src/runtime/operation-context.js +410 -0
  171. package/dist/src/runtime/runtime-session.d.ts +95 -0
  172. package/dist/src/runtime/runtime-session.js +314 -0
  173. package/dist/src/runtime/tab-coordinator.d.ts +200 -0
  174. package/dist/src/runtime/tab-coordinator.js +1200 -0
  175. package/dist/src/runtime/value-boundaries.d.ts +18 -0
  176. package/dist/src/runtime/value-boundaries.js +49 -0
  177. package/dist/src/safety/untrusted-output.js +2 -1
  178. package/dist/src/scripts/backend-server.js +4 -1
  179. package/dist/src/scripts/live-smoke/harness.js +79 -7
  180. package/dist/src/scripts/live-smoke/scenarios.d.ts +28 -0
  181. package/dist/src/scripts/live-smoke/scenarios.js +110 -18
  182. package/dist/src/scripts/live-smoke/types.d.ts +3 -0
  183. package/dist/src/scripts/release-canary-module.js +17 -4
  184. package/dist/src/types.d.ts +38 -1
  185. package/package.json +1 -1
  186. package/references/2026-08-16-transactional-operations.md +459 -0
  187. package/references/agents-runner.md +34 -0
  188. package/references/backend-protocol.md +73 -0
  189. package/references/python-parity.md +72 -0
  190. package/references/responses-adapter.md +19 -0
  191. package/references/streaming.md +6 -0
@@ -0,0 +1,1081 @@
1
+ import { sendButton } from "../dom/selectors.js";
2
+ import { validateSubmissionTargetEstablishment } from "./submission.js";
3
+ import { TURN_OWNERSHIP_SCHEMA_VERSION } from "./turn-ownership.js";
4
+ /**
5
+ * The operation Send primitive deliberately knows nothing about RuntimeEnv.
6
+ * A caller binds one immutable PageLike. The optional transaction wraps only
7
+ * the short precondition/read + activation section; postcondition probes are
8
+ * deliberately performed after it settles. Polling is bounded and every
9
+ * delay is awaited outside the caller-owned tab actor.
10
+ */
11
+ const DIGEST_PATTERN = /^hmac-sha256:[0-9a-f]{64}$/u;
12
+ const SAFE_ID_PATTERN = /^[A-Za-z0-9._:-]{1,512}$/u;
13
+ const MAX_ATTACHMENTS = 256;
14
+ const MAX_CALLBACK_ARRAY_ITEMS = 256;
15
+ const MAX_CALLBACK_STRING = 1024;
16
+ const DEFAULT_POSTCONDITION_ATTEMPTS = 8;
17
+ const MAX_POSTCONDITION_ATTEMPTS = 32;
18
+ const DEFAULT_POSTCONDITION_INTERVAL_MS = 50;
19
+ const MAX_POSTCONDITION_INTERVAL_MS = 5_000;
20
+ const DEFAULT_POSTCONDITION_TIMEOUT_MS = 3_000;
21
+ const MAX_POSTCONDITION_TIMEOUT_MS = 30_000;
22
+ const SUBMISSION_BLOCKER_CODES = new Set([
23
+ "operation_cancelled",
24
+ "operation_timeout",
25
+ "stale_handle",
26
+ "operation_state_corrupt",
27
+ "target_binding_mismatch",
28
+ "target_evidence_unavailable",
29
+ "configuration_drift",
30
+ "composer_drift",
31
+ "attachment_manifest_mismatch",
32
+ "ambiguous_file_handoff",
33
+ "ambiguous_submit",
34
+ "concurrent_user_turn",
35
+ "send_control_unavailable",
36
+ "journal_unavailable",
37
+ "port_protocol_violation",
38
+ "already_completed"
39
+ ]);
40
+ const CONTROL_BLOCKER_CODES = new Set([
41
+ "target_binding_mismatch",
42
+ "target_evidence_unavailable",
43
+ "configuration_drift",
44
+ "composer_drift",
45
+ "attachment_manifest_mismatch",
46
+ "concurrent_user_turn",
47
+ "send_control_unavailable",
48
+ "ambiguous_submit",
49
+ "port_protocol_violation"
50
+ ]);
51
+ class PreconditionValidationError extends Error {
52
+ code;
53
+ constructor(code) {
54
+ super("invalid precondition observation");
55
+ this.code = code;
56
+ this.name = "PreconditionValidationError";
57
+ }
58
+ }
59
+ /**
60
+ * Capture the complete pre-Send identity using reads only. The returned
61
+ * value is detached from provider objects and deeply frozen, so a caller can
62
+ * safely persist its redacted baseline after this function has returned.
63
+ */
64
+ export async function prepareSendOnce(request) {
65
+ let expected;
66
+ let base;
67
+ try {
68
+ validatePrepareRequest(request);
69
+ expected = cloneExpected(request.expected);
70
+ base = toSendOnceRequest(request, expected, "mutate_once");
71
+ }
72
+ catch {
73
+ return { status: "blocked", result: blocked("port_protocol_violation") };
74
+ }
75
+ const initialCancellation = cancellationCode(base);
76
+ if (initialCancellation !== undefined)
77
+ return { status: "blocked", result: blocked(initialCancellation) };
78
+ let readPromise;
79
+ const readOnce = () => {
80
+ readPromise ??= observePrecondition(base, expected);
81
+ return readPromise;
82
+ };
83
+ let first;
84
+ try {
85
+ first = request.transaction === undefined ? await readOnce() : await request.transaction(readOnce);
86
+ }
87
+ catch {
88
+ return { status: "blocked", result: blocked(cancellationCode(base) ?? "port_protocol_violation") };
89
+ }
90
+ if (first.status !== "exact")
91
+ return { status: "blocked", result: blocked(first.code, first.evidenceDigest) };
92
+ const afterReadCancellation = cancellationCode(base);
93
+ if (afterReadCancellation !== undefined)
94
+ return { status: "blocked", result: blocked(afterReadCancellation) };
95
+ return {
96
+ status: "prepared",
97
+ prepared: freezePrepared({
98
+ schemaVersion: "chatgpt.browser_control.send_once_prepared.v1",
99
+ operationId: request.operationId,
100
+ requestDigest: request.requestDigest,
101
+ surface: request.surface,
102
+ actionId: request.actionId,
103
+ expected,
104
+ observation: cloneExactPrecondition(first),
105
+ baseline: cloneBaseline(first.baseline)
106
+ })
107
+ };
108
+ }
109
+ /**
110
+ * Execute a prepared Send. This phase performs no journal/persistence work;
111
+ * its caller is responsible for atomically persisting the intent and the
112
+ * prepared ownership baseline before invoking it. Only the final exact read
113
+ * and one control activation occur while an optional tab actor is held.
114
+ */
115
+ export async function executePreparedSendOnce(request) {
116
+ let prepared;
117
+ let base;
118
+ try {
119
+ validateExecutePreparedRequest(request);
120
+ prepared = clonePrepared(request.prepared);
121
+ base = toSendOnceRequest({
122
+ page: request.page,
123
+ operationId: prepared.operationId,
124
+ requestDigest: prepared.requestDigest,
125
+ surface: prepared.surface,
126
+ actionId: prepared.actionId,
127
+ expected: prepared.expected,
128
+ observers: request.observers,
129
+ ...(request.signal === undefined ? {} : { signal: request.signal }),
130
+ ...(request.deadlineAt === undefined ? {} : { deadlineAt: request.deadlineAt })
131
+ }, prepared.expected, "mutate_once");
132
+ }
133
+ catch {
134
+ return { status: "blocked", result: blocked("port_protocol_violation") };
135
+ }
136
+ const initialCancellation = cancellationCode(base);
137
+ if (initialCancellation !== undefined)
138
+ return { status: "blocked", result: blocked(initialCancellation) };
139
+ let executionSettled = false;
140
+ let executionPromise;
141
+ const executeOnce = () => {
142
+ executionPromise ??= executePreparedShort(base, prepared).finally(() => {
143
+ executionSettled = true;
144
+ });
145
+ return executionPromise;
146
+ };
147
+ if (request.transaction === undefined)
148
+ return await executeOnce();
149
+ let wrapperFailed = false;
150
+ try {
151
+ // The wrapper's return value is deliberately ignored. The only valid
152
+ // result is the memoized callback promise owned by this module.
153
+ await request.transaction(executeOnce);
154
+ }
155
+ catch {
156
+ wrapperFailed = true;
157
+ }
158
+ if (executionPromise === undefined) {
159
+ return { status: "blocked", result: blocked(cancellationCode(base) ?? "port_protocol_violation") };
160
+ }
161
+ const wrapperSettledEarly = !executionSettled;
162
+ let settled;
163
+ try {
164
+ // A broken actor wrapper must never make us release effect authority while
165
+ // its callback is still reading or clicking. Await the callback itself to
166
+ // settlement even after the wrapper resolves or rejects early.
167
+ settled = await executionPromise;
168
+ }
169
+ catch {
170
+ return { status: "uncertain", result: uncertain(undefined, "provider") };
171
+ }
172
+ if (!wrapperFailed && !wrapperSettledEarly)
173
+ return settled;
174
+ if (settled.status === "blocked")
175
+ return settled;
176
+ return { status: "uncertain", result: uncertain(undefined, "provider") };
177
+ }
178
+ /** Read-only post-activation reconciliation. */
179
+ export async function verifyPreparedSendOnce(request) {
180
+ let prepared;
181
+ let base;
182
+ try {
183
+ validateVerifyRequest(request);
184
+ prepared = clonePrepared(request.prepared);
185
+ base = toSendOnceRequest({
186
+ page: request.page,
187
+ operationId: prepared.operationId,
188
+ requestDigest: prepared.requestDigest,
189
+ surface: prepared.surface,
190
+ actionId: prepared.actionId,
191
+ expected: prepared.expected,
192
+ observers: request.observers,
193
+ ...(request.signal === undefined ? {} : { signal: request.signal }),
194
+ ...(request.deadlineAt === undefined ? {} : { deadlineAt: request.deadlineAt })
195
+ }, prepared.expected, "mutate_once");
196
+ }
197
+ catch {
198
+ return request.mutationMayHaveOccurred ? uncertain(undefined, "caller") : blocked("port_protocol_violation");
199
+ }
200
+ const cancellation = cancellationCode(base);
201
+ // A post-activation cancellation/deadline cannot suppress the first
202
+ // observation: that bounded read is the only chance to reconcile an
203
+ // already-started mutation. Pre-activation verification remains a plain
204
+ // blocker.
205
+ if (cancellation !== undefined && !request.mutationMayHaveOccurred)
206
+ return blocked(cancellation);
207
+ return await observeExistingTurn(base, prepared.expected, cloneBaseline(prepared.baseline), request.activation, request.mutationMayHaveOccurred);
208
+ }
209
+ /** Read-only recovery for a durable Send intent after a restart/quarantine. */
210
+ export async function recoverSendOnce(request) {
211
+ let base;
212
+ let expected;
213
+ let durableBaseline;
214
+ try {
215
+ validateRecoverRequest(request);
216
+ expected = cloneExpected(request.expected);
217
+ durableBaseline = cloneOwnershipBaseline(request.durableBaseline);
218
+ base = toSendOnceRequest(request, expected, "observe_only");
219
+ }
220
+ catch {
221
+ return { status: "uncertain", quarantine: "caller" };
222
+ }
223
+ const baseline = turnBaselineFromOwnership(durableBaseline);
224
+ const cancellation = cancellationCode(base);
225
+ if (cancellation !== undefined)
226
+ return uncertain(undefined, "caller");
227
+ return await observeExistingTurn(base, expected, baseline, "not_attempted", true);
228
+ }
229
+ /**
230
+ * Compatibility composition wrapper. New orchestration should call the four
231
+ * explicit phases above. Crucially, the legacy persistence hook is invoked
232
+ * after prepare has returned and before execute acquires its optional actor;
233
+ * it is never called from inside a browser transaction.
234
+ */
235
+ export async function runSendOnce(request) {
236
+ let expected;
237
+ try {
238
+ validateRequest(request);
239
+ expected = cloneExpected(request.expected);
240
+ }
241
+ catch {
242
+ return blocked("port_protocol_violation");
243
+ }
244
+ const initialCancellation = cancellationCode(request);
245
+ if (initialCancellation !== undefined)
246
+ return blocked(initialCancellation);
247
+ if (request.mode === "observe_only" && request.durableBaseline !== undefined) {
248
+ return await recoverSendOnce({
249
+ page: request.page,
250
+ operationId: request.operationId,
251
+ requestDigest: request.requestDigest,
252
+ surface: request.surface,
253
+ actionId: request.actionId,
254
+ expected,
255
+ durableBaseline: request.durableBaseline,
256
+ observers: request.observers,
257
+ ...(request.signal === undefined ? {} : { signal: request.signal }),
258
+ ...(request.deadlineAt === undefined ? {} : { deadlineAt: request.deadlineAt })
259
+ });
260
+ }
261
+ // Do not pass request.transaction to prepare here. This compatibility
262
+ // wrapper intentionally keeps the durable write boundary visible: prepare
263
+ // returns before the caller's optional mutation actor is acquired.
264
+ const preparedResult = await prepareSendOnce({
265
+ page: request.page,
266
+ operationId: request.operationId,
267
+ requestDigest: request.requestDigest,
268
+ surface: request.surface,
269
+ actionId: request.actionId,
270
+ expected,
271
+ observers: request.observers,
272
+ ...(request.signal === undefined ? {} : { signal: request.signal }),
273
+ ...(request.deadlineAt === undefined ? {} : { deadlineAt: request.deadlineAt })
274
+ });
275
+ if (preparedResult.status === "blocked")
276
+ return preparedResult.result;
277
+ const prepared = preparedResult.prepared;
278
+ if (request.mode === "observe_only") {
279
+ return await verifyPreparedSendOnce({
280
+ page: request.page,
281
+ prepared,
282
+ observers: request.observers,
283
+ activation: "not_attempted",
284
+ mutationMayHaveOccurred: false,
285
+ ...(request.signal === undefined ? {} : { signal: request.signal }),
286
+ ...(request.deadlineAt === undefined ? {} : { deadlineAt: request.deadlineAt })
287
+ });
288
+ }
289
+ if (request.persistPreSendBaseline !== undefined) {
290
+ const ownershipBaseline = prepared.baseline.ownershipBaseline;
291
+ if (ownershipBaseline === undefined)
292
+ return blocked("target_evidence_unavailable");
293
+ try {
294
+ await request.persistPreSendBaseline(cloneOwnershipBaseline(ownershipBaseline));
295
+ }
296
+ catch {
297
+ return blocked("journal_unavailable");
298
+ }
299
+ const afterPersistenceCancellation = cancellationCode(request);
300
+ if (afterPersistenceCancellation !== undefined)
301
+ return uncertain(undefined, "caller");
302
+ }
303
+ const execution = await executePreparedSendOnce({
304
+ page: request.page,
305
+ prepared,
306
+ observers: request.observers,
307
+ ...(request.signal === undefined ? {} : { signal: request.signal }),
308
+ ...(request.deadlineAt === undefined ? {} : { deadlineAt: request.deadlineAt }),
309
+ ...(request.transaction === undefined ? {} : { transaction: request.transaction })
310
+ });
311
+ if (execution.status === "blocked" || execution.status === "uncertain")
312
+ return execution.result;
313
+ return await verifyPreparedSendOnce({
314
+ page: request.page,
315
+ prepared: execution.prepared,
316
+ observers: request.observers,
317
+ activation: execution.activation,
318
+ mutationMayHaveOccurred: execution.mutationMayHaveOccurred,
319
+ ...(request.signal === undefined ? {} : { signal: request.signal }),
320
+ ...(request.deadlineAt === undefined ? {} : { deadlineAt: request.deadlineAt })
321
+ });
322
+ }
323
+ /** Alias kept explicit for callers that name the operation after its action. */
324
+ export const executeSendOnce = runSendOnce;
325
+ async function executePreparedShort(request, prepared) {
326
+ const initialCancellation = cancellationCode(request);
327
+ if (initialCancellation !== undefined)
328
+ return { status: "blocked", result: blocked(initialCancellation) };
329
+ // This is the only final precondition read. It occurs after prepare has
330
+ // returned and after any caller-owned persistence, while still inside the
331
+ // short actor transaction supplied to executePreparedSendOnce.
332
+ const finalPrecondition = await observePrecondition(request, prepared.expected);
333
+ if (finalPrecondition.status !== "exact") {
334
+ return { status: "blocked", result: blocked(finalPrecondition.code, finalPrecondition.evidenceDigest) };
335
+ }
336
+ if (!samePreparedObservation(prepared, finalPrecondition)) {
337
+ return { status: "blocked", result: blocked("concurrent_user_turn") };
338
+ }
339
+ const control = await resolveSendControl(request.page);
340
+ if (control.status === "ambiguous")
341
+ return { status: "blocked", result: blocked("ambiguous_submit") };
342
+ if (control.status === "unavailable" || control.status === "not_ready")
343
+ return { status: "blocked", result: blocked("send_control_unavailable") };
344
+ if (control.status === "protocol_error")
345
+ return { status: "blocked", result: blocked("port_protocol_violation") };
346
+ const beforeActivationCancellation = cancellationCode(request);
347
+ if (beforeActivationCancellation !== undefined)
348
+ return { status: "blocked", result: blocked(beforeActivationCancellation) };
349
+ if (typeof control.locator.click !== "function")
350
+ return { status: "blocked", result: blocked("port_protocol_violation") };
351
+ // No timeout race and no cancellation race is permitted once this point is
352
+ // reached. Await the provider activation's settlement, then reconcile in a
353
+ // separate read-only phase. A bridge may have accepted the click even when
354
+ // it rejects its promise, hence activation_threw is still mutation-possible.
355
+ try {
356
+ await control.locator.click();
357
+ return {
358
+ status: "activated",
359
+ prepared,
360
+ baseline: cloneBaseline(prepared.baseline),
361
+ activation: "activated",
362
+ mutationMayHaveOccurred: true
363
+ };
364
+ }
365
+ catch {
366
+ return {
367
+ status: "activation_threw",
368
+ prepared,
369
+ baseline: cloneBaseline(prepared.baseline),
370
+ activation: "activation_threw",
371
+ mutationMayHaveOccurred: true
372
+ };
373
+ }
374
+ }
375
+ function toSendOnceRequest(request, expected, mode) {
376
+ return {
377
+ page: request.page,
378
+ operationId: request.operationId,
379
+ requestDigest: request.requestDigest,
380
+ surface: request.surface,
381
+ actionId: request.actionId,
382
+ mode: request.mode ?? mode,
383
+ expected,
384
+ observers: request.observers,
385
+ ...(request.signal === undefined ? {} : { signal: request.signal }),
386
+ ...(request.deadlineAt === undefined ? {} : { deadlineAt: request.deadlineAt })
387
+ };
388
+ }
389
+ function turnBaselineFromOwnership(value) {
390
+ const lastUserTurn = value.userTurns.at(-1);
391
+ return {
392
+ ...(lastUserTurn?.stableId === undefined ? {} : { userTurnId: lastUserTurn.stableId }),
393
+ userTurnEvidenceDigest: lastUserTurn?.evidenceDigest ?? value.snapshotDigest,
394
+ ownershipBaseline: cloneOwnershipBaseline(value)
395
+ };
396
+ }
397
+ function samePreparedObservation(prepared, current) {
398
+ return current.targetBindingDigest === prepared.observation.targetBindingDigest
399
+ && current.configurationReceiptDigest === prepared.observation.configurationReceiptDigest
400
+ && current.composerReceiptDigest === prepared.observation.composerReceiptDigest
401
+ && current.attachments.count === prepared.observation.attachments.count
402
+ && current.attachments.orderPolicy === prepared.observation.attachments.orderPolicy
403
+ && current.attachments.identityDigests.length === prepared.observation.attachments.identityDigests.length
404
+ && current.attachments.identityDigests.every((digest, index) => digest === prepared.observation.attachments.identityDigests[index])
405
+ && sameBaseline(prepared.baseline, current.baseline);
406
+ }
407
+ function cloneExactPrecondition(value) {
408
+ return {
409
+ status: "exact",
410
+ targetBindingDigest: value.targetBindingDigest,
411
+ configurationReceiptDigest: value.configurationReceiptDigest,
412
+ composerReceiptDigest: value.composerReceiptDigest,
413
+ attachments: {
414
+ count: value.attachments.count,
415
+ orderPolicy: "exact",
416
+ identityDigests: [...value.attachments.identityDigests]
417
+ },
418
+ baseline: cloneBaseline(value.baseline),
419
+ evidenceDigest: value.evidenceDigest
420
+ };
421
+ }
422
+ function freezePrepared(value) {
423
+ return deepFreeze(value);
424
+ }
425
+ function clonePrepared(value) {
426
+ validatePrepared(value);
427
+ return freezePrepared({
428
+ schemaVersion: value.schemaVersion,
429
+ operationId: value.operationId,
430
+ requestDigest: value.requestDigest,
431
+ surface: value.surface,
432
+ actionId: value.actionId,
433
+ expected: cloneExpected(value.expected),
434
+ observation: cloneExactPrecondition(value.observation),
435
+ baseline: cloneBaseline(value.baseline)
436
+ });
437
+ }
438
+ async function observePrecondition(request, expected) {
439
+ try {
440
+ const observation = await request.observers.observePrecondition({
441
+ page: request.page,
442
+ expected: cloneExpected(expected),
443
+ mode: request.mode,
444
+ ...(request.signal === undefined ? {} : { signal: request.signal }),
445
+ ...(request.deadlineAt === undefined ? {} : { deadlineAt: request.deadlineAt })
446
+ });
447
+ validatePreconditionObservation(observation, expected);
448
+ return observation;
449
+ }
450
+ catch (error) {
451
+ if (error instanceof PreconditionValidationError) {
452
+ return { status: "mismatch", code: error.code };
453
+ }
454
+ return { status: "unavailable", code: "port_protocol_violation" };
455
+ }
456
+ }
457
+ async function observeExistingTurn(request, expected, baseline, activation, mutationMayHaveOccurred = false) {
458
+ const bounds = postconditionBounds(request.observers);
459
+ const startedAt = Date.now();
460
+ let lastResult;
461
+ for (let attempt = 1; attempt <= bounds.maxAttempts; attempt += 1) {
462
+ // Once an activation has happened, cancellation never authorizes a retry.
463
+ // A proof observed before cancellation remains a valid receipt, but a
464
+ // cancelled read cannot be used to claim a negative result.
465
+ if (attempt > 1 && cancellationCode(request) !== undefined) {
466
+ return mutationMayHaveOccurred ? uncertain(evidenceOf(lastResult)) : blocked(cancellationCode(request));
467
+ }
468
+ if (Date.now() - startedAt >= bounds.timeoutMs) {
469
+ return mutationMayHaveOccurred ? uncertain(evidenceOf(lastResult)) : blocked("operation_timeout");
470
+ }
471
+ let observed;
472
+ try {
473
+ observed = await request.observers.observePostcondition({
474
+ page: request.page,
475
+ actionId: request.actionId,
476
+ expected: cloneExpected(expected),
477
+ mode: request.mode,
478
+ baseline: cloneBaseline(baseline),
479
+ activation,
480
+ attempt,
481
+ ...(request.signal === undefined ? {} : { signal: request.signal }),
482
+ ...(request.deadlineAt === undefined ? {} : { deadlineAt: request.deadlineAt })
483
+ });
484
+ const probe = normalizePostconditionProbe(observed);
485
+ validatePostcondition(probe.result, expected, request.mode, baseline);
486
+ lastResult = probe.result;
487
+ if (probe.result.status === "submitted" || probe.result.status === "already_submitted") {
488
+ return probe.result;
489
+ }
490
+ if (!probe.retryable || attempt === bounds.maxAttempts) {
491
+ return mutationMayHaveOccurred
492
+ ? uncertain(probe.result.evidenceDigest)
493
+ : probe.result;
494
+ }
495
+ }
496
+ catch {
497
+ return mutationMayHaveOccurred ? uncertain(evidenceOf(lastResult)) : blocked("port_protocol_violation");
498
+ }
499
+ const remaining = bounds.timeoutMs - (Date.now() - startedAt);
500
+ if (remaining <= 0) {
501
+ return mutationMayHaveOccurred ? uncertain(evidenceOf(lastResult)) : blocked("operation_timeout");
502
+ }
503
+ const delay = Math.min(bounds.intervalMs, remaining);
504
+ try {
505
+ await sleepOutsideActor(request.observers, delay, request.signal ?? new AbortController().signal);
506
+ }
507
+ catch {
508
+ return mutationMayHaveOccurred ? uncertain(evidenceOf(lastResult)) : blocked("operation_cancelled");
509
+ }
510
+ }
511
+ return mutationMayHaveOccurred ? uncertain(evidenceOf(lastResult)) : blocked("operation_timeout");
512
+ }
513
+ function normalizePostconditionProbe(value) {
514
+ if (isPlainRecord(value) && hasOwnDataProperty(value, "result") && hasOwnDataProperty(value, "retryable")) {
515
+ const record = value;
516
+ assertExactKeys(record, ["result", "retryable"]);
517
+ const result = record.result;
518
+ const retryable = record.retryable;
519
+ if (typeof retryable !== "boolean")
520
+ throw new Error("invalid postcondition retry flag");
521
+ if (!isPlainRecord(result))
522
+ throw new Error("invalid postcondition result");
523
+ return { result: result, retryable };
524
+ }
525
+ return { result: value, retryable: false };
526
+ }
527
+ function postconditionBounds(observers) {
528
+ const maxAttempts = observers.maxPostconditionAttempts ?? DEFAULT_POSTCONDITION_ATTEMPTS;
529
+ const intervalMs = observers.postconditionIntervalMs ?? DEFAULT_POSTCONDITION_INTERVAL_MS;
530
+ const timeoutMs = observers.postconditionTimeoutMs ?? DEFAULT_POSTCONDITION_TIMEOUT_MS;
531
+ if (!Number.isSafeInteger(maxAttempts) || maxAttempts < 1 || maxAttempts > MAX_POSTCONDITION_ATTEMPTS)
532
+ throw new Error("invalid postcondition attempts");
533
+ if (!Number.isSafeInteger(intervalMs) || intervalMs < 0 || intervalMs > MAX_POSTCONDITION_INTERVAL_MS)
534
+ throw new Error("invalid postcondition interval");
535
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > MAX_POSTCONDITION_TIMEOUT_MS)
536
+ throw new Error("invalid postcondition timeout");
537
+ return { maxAttempts, intervalMs, timeoutMs };
538
+ }
539
+ async function sleepOutsideActor(observers, milliseconds, signal) {
540
+ if (observers.sleep !== undefined) {
541
+ await observers.sleep(milliseconds, signal);
542
+ return;
543
+ }
544
+ await new Promise((resolve, reject) => {
545
+ if (signal.aborted) {
546
+ reject(new Error("operation cancelled"));
547
+ return;
548
+ }
549
+ let timer;
550
+ const onAbort = () => {
551
+ clearTimeout(timer);
552
+ signal.removeEventListener("abort", onAbort);
553
+ reject(new Error("operation cancelled"));
554
+ };
555
+ signal.addEventListener("abort", onAbort, { once: true });
556
+ timer = setTimeout(() => {
557
+ signal.removeEventListener("abort", onAbort);
558
+ resolve();
559
+ }, milliseconds);
560
+ });
561
+ }
562
+ function evidenceOf(result) {
563
+ return result === undefined ? undefined : result.evidenceDigest;
564
+ }
565
+ async function resolveSendControl(page) {
566
+ // Operation mode requires the locale registry's semantic role lookup. The
567
+ // legacy CSS fallback is intentionally not accepted for an exact-once
568
+ // mutation because its English substring selector is not identity-grade.
569
+ if (typeof page.getByRole !== "function")
570
+ return { status: "protocol_error" };
571
+ let locator;
572
+ try {
573
+ locator = sendButton(page);
574
+ }
575
+ catch {
576
+ return { status: "protocol_error" };
577
+ }
578
+ if (typeof locator.count !== "function")
579
+ return { status: "protocol_error" };
580
+ let count;
581
+ try {
582
+ count = await locator.count();
583
+ }
584
+ catch {
585
+ return { status: "unavailable" };
586
+ }
587
+ if (!Number.isSafeInteger(count) || count < 0 || count > MAX_CALLBACK_ARRAY_ITEMS)
588
+ return { status: "protocol_error" };
589
+ if (count === 0)
590
+ return { status: "unavailable" };
591
+ const visible = [];
592
+ for (let index = 0; index < count; index += 1) {
593
+ const candidate = count === 1 ? locator : locator.nth?.(index);
594
+ if (candidate === undefined || typeof candidate.isVisible !== "function")
595
+ return { status: "protocol_error" };
596
+ let isVisible;
597
+ try {
598
+ isVisible = await candidate.isVisible();
599
+ }
600
+ catch {
601
+ return { status: "unavailable" };
602
+ }
603
+ if (typeof isVisible !== "boolean")
604
+ return { status: "protocol_error" };
605
+ if (isVisible)
606
+ visible.push(candidate);
607
+ }
608
+ if (visible.length > 1)
609
+ return { status: "ambiguous" };
610
+ if (visible.length === 0)
611
+ return { status: "unavailable" };
612
+ const candidate = visible[0];
613
+ if (candidate === undefined || typeof candidate.evaluate !== "function")
614
+ return { status: "protocol_error" };
615
+ let enabled;
616
+ try {
617
+ enabled = await candidate.evaluate(element => {
618
+ const node = element;
619
+ const ariaDisabled = node.getAttribute("aria-disabled");
620
+ const disabledAttribute = node.hasAttribute("disabled");
621
+ const inert = node.hasAttribute("inert") || node.getAttribute("aria-hidden") === "true";
622
+ return node.disabled !== true && !disabledAttribute && ariaDisabled !== "true" && !inert;
623
+ });
624
+ }
625
+ catch {
626
+ return { status: "not_ready" };
627
+ }
628
+ if (typeof enabled !== "boolean")
629
+ return { status: "protocol_error" };
630
+ return enabled ? { status: "ready", locator: candidate } : { status: "not_ready" };
631
+ }
632
+ function validateRequest(request) {
633
+ if (!isPlainRecord(request))
634
+ throw new Error("invalid request");
635
+ if (!isPageLike(request.page))
636
+ throw new Error("invalid page");
637
+ if (!isSafeId(request.operationId) || !isDigest(request.requestDigest) || !isSafeId(request.actionId))
638
+ throw new Error("invalid identity");
639
+ if (request.surface !== "chat" && request.surface !== "work")
640
+ throw new Error("invalid surface");
641
+ if (request.mode !== "mutate_once" && request.mode !== "observe_only")
642
+ throw new Error("invalid mode");
643
+ validateExpected(request.expected);
644
+ if (request.expected.surface !== request.surface)
645
+ throw new Error("surface mismatch");
646
+ if (!isPlainRecord(request.observers) || typeof request.observers.observePrecondition !== "function" || typeof request.observers.observePostcondition !== "function")
647
+ throw new Error("invalid observers");
648
+ if (request.observers.sleep !== undefined && typeof request.observers.sleep !== "function")
649
+ throw new Error("invalid observer sleep");
650
+ if (request.observers.maxPostconditionAttempts !== undefined && (!Number.isSafeInteger(request.observers.maxPostconditionAttempts) || request.observers.maxPostconditionAttempts < 1 || request.observers.maxPostconditionAttempts > MAX_POSTCONDITION_ATTEMPTS))
651
+ throw new Error("invalid postcondition attempts");
652
+ if (request.observers.postconditionIntervalMs !== undefined && (!Number.isSafeInteger(request.observers.postconditionIntervalMs) || request.observers.postconditionIntervalMs < 0 || request.observers.postconditionIntervalMs > MAX_POSTCONDITION_INTERVAL_MS))
653
+ throw new Error("invalid postcondition interval");
654
+ if (request.observers.postconditionTimeoutMs !== undefined && (!Number.isSafeInteger(request.observers.postconditionTimeoutMs) || request.observers.postconditionTimeoutMs < 1 || request.observers.postconditionTimeoutMs > MAX_POSTCONDITION_TIMEOUT_MS))
655
+ throw new Error("invalid postcondition timeout");
656
+ if (request.signal !== undefined && !isAbortSignalLike(request.signal))
657
+ throw new Error("invalid signal");
658
+ if (request.deadlineAt !== undefined && (!Number.isSafeInteger(request.deadlineAt) || request.deadlineAt < 0))
659
+ throw new Error("invalid deadline");
660
+ if (request.transaction !== undefined && typeof request.transaction !== "function")
661
+ throw new Error("invalid transaction");
662
+ if (request.persistPreSendBaseline !== undefined && typeof request.persistPreSendBaseline !== "function")
663
+ throw new Error("invalid baseline persistence hook");
664
+ if (request.durableBaseline !== undefined) {
665
+ validateOwnershipBaseline(request.durableBaseline);
666
+ }
667
+ }
668
+ function validatePrepareRequest(request) {
669
+ if (!isPlainRecord(request))
670
+ throw new Error("invalid prepare request");
671
+ validateRequest({
672
+ page: request.page,
673
+ operationId: request.operationId,
674
+ requestDigest: request.requestDigest,
675
+ surface: request.surface,
676
+ actionId: request.actionId,
677
+ mode: "mutate_once",
678
+ expected: request.expected,
679
+ observers: request.observers,
680
+ ...(request.signal === undefined ? {} : { signal: request.signal }),
681
+ ...(request.deadlineAt === undefined ? {} : { deadlineAt: request.deadlineAt }),
682
+ ...(request.transaction === undefined ? {} : { transaction: request.transaction })
683
+ });
684
+ }
685
+ function validateExecutePreparedRequest(request) {
686
+ if (!isPlainRecord(request) || !isPageLike(request.page))
687
+ throw new Error("invalid execute request");
688
+ validatePrepared(request.prepared);
689
+ if (!isPlainRecord(request.observers) || typeof request.observers.observePrecondition !== "function" || typeof request.observers.observePostcondition !== "function")
690
+ throw new Error("invalid observers");
691
+ if (request.signal !== undefined && !isAbortSignalLike(request.signal))
692
+ throw new Error("invalid signal");
693
+ if (request.deadlineAt !== undefined && (!Number.isSafeInteger(request.deadlineAt) || request.deadlineAt < 0))
694
+ throw new Error("invalid deadline");
695
+ if (request.transaction !== undefined && typeof request.transaction !== "function")
696
+ throw new Error("invalid transaction");
697
+ }
698
+ function validateVerifyRequest(request) {
699
+ if (!isPlainRecord(request) || !isPageLike(request.page))
700
+ throw new Error("invalid verify request");
701
+ validatePrepared(request.prepared);
702
+ if (!isPlainRecord(request.observers) || typeof request.observers.observePrecondition !== "function" || typeof request.observers.observePostcondition !== "function")
703
+ throw new Error("invalid observers");
704
+ if (request.activation !== "not_attempted" && request.activation !== "activated" && request.activation !== "activation_threw")
705
+ throw new Error("invalid activation");
706
+ if (typeof request.mutationMayHaveOccurred !== "boolean")
707
+ throw new Error("invalid mutation boundary");
708
+ if (!request.mutationMayHaveOccurred && request.activation !== "not_attempted")
709
+ throw new Error("invalid non-mutating activation");
710
+ if (request.signal !== undefined && !isAbortSignalLike(request.signal))
711
+ throw new Error("invalid signal");
712
+ if (request.deadlineAt !== undefined && (!Number.isSafeInteger(request.deadlineAt) || request.deadlineAt < 0))
713
+ throw new Error("invalid deadline");
714
+ }
715
+ function validateRecoverRequest(request) {
716
+ if (!isPlainRecord(request))
717
+ throw new Error("invalid recover request");
718
+ validateRequest({
719
+ page: request.page,
720
+ operationId: request.operationId,
721
+ requestDigest: request.requestDigest,
722
+ surface: request.surface,
723
+ actionId: request.actionId,
724
+ mode: "observe_only",
725
+ expected: request.expected,
726
+ observers: request.observers,
727
+ durableBaseline: request.durableBaseline,
728
+ ...(request.signal === undefined ? {} : { signal: request.signal }),
729
+ ...(request.deadlineAt === undefined ? {} : { deadlineAt: request.deadlineAt })
730
+ });
731
+ }
732
+ function validatePrepared(value) {
733
+ if (!isPlainRecord(value))
734
+ throw new Error("invalid prepared value");
735
+ assertExactKeys(value, ["schemaVersion", "operationId", "requestDigest", "surface", "actionId", "expected", "observation", "baseline"]);
736
+ if (value.schemaVersion !== "chatgpt.browser_control.send_once_prepared.v1")
737
+ throw new Error("invalid prepared schema");
738
+ if (!isSafeId(value.operationId) || !isDigest(value.requestDigest) || !isSafeId(value.actionId))
739
+ throw new Error("invalid prepared identity");
740
+ if (value.surface !== "chat" && value.surface !== "work")
741
+ throw new Error("invalid prepared surface");
742
+ validateExpected(value.expected);
743
+ if (value.expected.surface !== value.surface)
744
+ throw new Error("prepared surface mismatch");
745
+ validatePreconditionObservation(value.observation, value.expected);
746
+ if (value.observation.status !== "exact")
747
+ throw new Error("prepared observation is not exact");
748
+ validateBaseline(value.baseline);
749
+ if (!sameBaseline(value.baseline, value.observation.baseline))
750
+ throw new Error("prepared baseline mismatch");
751
+ }
752
+ function validateExpected(value) {
753
+ if (!isPlainRecord(value))
754
+ throw new Error("invalid expected envelope");
755
+ assertExactKeys(value, ["surface", "targetBindingDigest", "configurationReceiptDigest", "composerReceiptDigest", "attachmentManifest"]);
756
+ if (value.surface !== "chat" && value.surface !== "work")
757
+ throw new Error("invalid expected surface");
758
+ for (const digest of [value.targetBindingDigest, value.configurationReceiptDigest, value.composerReceiptDigest]) {
759
+ if (!isDigest(digest))
760
+ throw new Error("invalid expected digest");
761
+ }
762
+ if (!isPlainRecord(value.attachmentManifest))
763
+ throw new Error("invalid attachment manifest");
764
+ assertExactKeys(value.attachmentManifest, ["count", "orderPolicy", "identities"]);
765
+ const manifest = value.attachmentManifest;
766
+ if (!Number.isSafeInteger(manifest.count) || manifest.count < 0 || manifest.count > MAX_ATTACHMENTS || manifest.orderPolicy !== "exact" || !Array.isArray(manifest.identities) || manifest.identities.length !== manifest.count)
767
+ throw new Error("invalid attachment manifest");
768
+ const seen = new Set();
769
+ manifest.identities.forEach((entry, index) => {
770
+ if (!isPlainRecord(entry))
771
+ throw new Error("invalid attachment identity");
772
+ assertExactKeys(entry, ["identityDigest", "ordinal"]);
773
+ if (entry.ordinal !== index || !isDigest(entry.identityDigest) || seen.has(entry.identityDigest))
774
+ throw new Error("invalid attachment identity");
775
+ seen.add(entry.identityDigest);
776
+ });
777
+ }
778
+ function validatePreconditionObservation(value, expected) {
779
+ if (!isPlainRecord(value) || typeof value.status !== "string")
780
+ throw new Error("invalid precondition");
781
+ if (value.status === "exact") {
782
+ assertExactKeys(value, ["status", "targetBindingDigest", "configurationReceiptDigest", "composerReceiptDigest", "attachments", "baseline", "evidenceDigest"]);
783
+ if (value.targetBindingDigest !== expected.targetBindingDigest)
784
+ throw new PreconditionValidationError("target_binding_mismatch");
785
+ if (value.configurationReceiptDigest !== expected.configurationReceiptDigest)
786
+ throw new PreconditionValidationError("configuration_drift");
787
+ if (value.composerReceiptDigest !== expected.composerReceiptDigest)
788
+ throw new PreconditionValidationError("composer_drift");
789
+ if (!isDigest(value.targetBindingDigest) || !isDigest(value.configurationReceiptDigest) || !isDigest(value.composerReceiptDigest) || !isDigest(value.evidenceDigest))
790
+ throw new Error("invalid precondition digest");
791
+ try {
792
+ validateAttachmentObservation(value.attachments, expected);
793
+ }
794
+ catch {
795
+ throw new PreconditionValidationError("attachment_manifest_mismatch");
796
+ }
797
+ validateBaseline(value.baseline);
798
+ return;
799
+ }
800
+ if (value.status !== "mismatch" && value.status !== "unavailable" && value.status !== "not_ready")
801
+ throw new Error("invalid precondition status");
802
+ assertExactKeys(value, ["status", "code", "evidenceDigest"]);
803
+ if (!CONTROL_BLOCKER_CODES.has(value.code))
804
+ throw new Error("invalid precondition code");
805
+ if (value.evidenceDigest !== undefined && !isDigest(value.evidenceDigest))
806
+ throw new Error("invalid precondition evidence");
807
+ }
808
+ function validateAttachmentObservation(value, expected) {
809
+ if (!isPlainRecord(value))
810
+ throw new Error("invalid attachment observation");
811
+ assertExactKeys(value, ["count", "orderPolicy", "identityDigests"]);
812
+ if (!Number.isSafeInteger(value.count) || value.count < 0 || value.count > MAX_ATTACHMENTS || value.orderPolicy !== "exact" || !Array.isArray(value.identityDigests) || value.identityDigests.length !== value.count || value.count !== expected.attachmentManifest.count)
813
+ throw new Error("attachment mismatch");
814
+ value.identityDigests.forEach((digest, index) => {
815
+ if (!isDigest(digest) || digest !== expected.attachmentManifest.identities[index]?.identityDigest)
816
+ throw new Error("attachment mismatch");
817
+ });
818
+ }
819
+ function validateBaseline(value) {
820
+ if (!isPlainRecord(value))
821
+ throw new Error("invalid baseline");
822
+ assertExactKeys(value, ["userTurnId", "userTurnEvidenceDigest", "ownershipBaseline"]);
823
+ if (!isDigest(value.userTurnEvidenceDigest))
824
+ throw new Error("invalid baseline evidence");
825
+ if (value.userTurnId !== undefined && !isSafeId(value.userTurnId))
826
+ throw new Error("invalid baseline id");
827
+ if (value.ownershipBaseline !== undefined)
828
+ validateOwnershipBaseline(value.ownershipBaseline);
829
+ }
830
+ function validatePostcondition(value, expected, mode, baseline) {
831
+ if (!isPlainRecord(value) || typeof value.status !== "string")
832
+ throw new Error("invalid postcondition");
833
+ if (value.status === "submitted" || value.status === "already_submitted") {
834
+ assertExactKeys(value, ["status", "targetBindingDigest", "evidenceDigest", "userTurnId", "userTurnEvidenceDigest", "postSendDeltaDigest", "assistantTurnId", "targetEstablishment"]);
835
+ if (mode === "observe_only" && value.status === "submitted")
836
+ throw new Error("observation claimed activation");
837
+ if (value.targetBindingDigest !== expected.targetBindingDigest || !isDigest(value.targetBindingDigest) || !isDigest(value.evidenceDigest) || !isDigest(value.userTurnEvidenceDigest) || !isDigest(value.postSendDeltaDigest) || !isSafeId(value.userTurnId))
838
+ throw new Error("invalid postcondition evidence");
839
+ if (baseline.userTurnId !== undefined && baseline.userTurnId === value.userTurnId)
840
+ throw new Error("postcondition turn did not advance");
841
+ if (value.assistantTurnId !== undefined && !isSafeId(value.assistantTurnId))
842
+ throw new Error("invalid assistant identity");
843
+ if (value.targetEstablishment !== undefined) {
844
+ validateSubmissionTargetEstablishment(value.targetEstablishment, expected.targetBindingDigest);
845
+ if (value.targetEstablishment.userTurnId !== value.userTurnId || value.targetEstablishment.userTurnEvidenceDigest !== value.userTurnEvidenceDigest) {
846
+ throw new Error("target establishment turn mismatch");
847
+ }
848
+ if (value.targetEstablishment.postSendDeltaDigest !== value.postSendDeltaDigest) {
849
+ throw new Error("target establishment delta mismatch");
850
+ }
851
+ }
852
+ return;
853
+ }
854
+ if (value.status === "blocked") {
855
+ assertExactKeys(value, ["status", "blockerCode", "evidenceDigest"]);
856
+ if (!isSubmissionBlockerCode(value.blockerCode))
857
+ throw new Error("invalid blocker");
858
+ if (value.evidenceDigest !== undefined && !isDigest(value.evidenceDigest))
859
+ throw new Error("invalid blocker evidence");
860
+ return;
861
+ }
862
+ if (value.status === "uncertain") {
863
+ assertExactKeys(value, ["status", "evidenceDigest", "quarantine"]);
864
+ if (value.evidenceDigest !== undefined && !isDigest(value.evidenceDigest))
865
+ throw new Error("invalid uncertain evidence");
866
+ if (value.quarantine !== "provider" && value.quarantine !== "caller")
867
+ throw new Error("invalid quarantine");
868
+ return;
869
+ }
870
+ throw new Error("invalid postcondition status");
871
+ }
872
+ function blocked(code, evidenceDigest) {
873
+ return {
874
+ status: "blocked",
875
+ blockerCode: code,
876
+ ...(evidenceDigest === undefined ? {} : { evidenceDigest })
877
+ };
878
+ }
879
+ function uncertain(evidenceDigest, quarantine = "caller") {
880
+ return {
881
+ status: "uncertain",
882
+ quarantine,
883
+ ...(evidenceDigest === undefined ? {} : { evidenceDigest })
884
+ };
885
+ }
886
+ function cancellationCode(request) {
887
+ if (request.signal?.aborted)
888
+ return "operation_cancelled";
889
+ if (request.deadlineAt !== undefined && Date.now() >= request.deadlineAt)
890
+ return "operation_timeout";
891
+ return undefined;
892
+ }
893
+ function sameBaseline(a, b) {
894
+ return a.userTurnId === b.userTurnId
895
+ && a.userTurnEvidenceDigest === b.userTurnEvidenceDigest
896
+ && canonicalBaseline(a.ownershipBaseline) === canonicalBaseline(b.ownershipBaseline);
897
+ }
898
+ function canonicalBaseline(value) {
899
+ if (value === undefined)
900
+ return "undefined";
901
+ // Ownership baselines are validated, cloned, and limited to primitive
902
+ // redacted identities before this comparison. JSON serialization therefore
903
+ // gives a deterministic structural comparison without retaining provider
904
+ // objects or accepting an attacker-controlled toJSON hook.
905
+ return JSON.stringify(value);
906
+ }
907
+ function cloneBaseline(value) {
908
+ return {
909
+ ...(value.userTurnId === undefined ? {} : { userTurnId: value.userTurnId }),
910
+ userTurnEvidenceDigest: value.userTurnEvidenceDigest,
911
+ ...(value.ownershipBaseline === undefined ? {} : { ownershipBaseline: cloneOwnershipBaseline(value.ownershipBaseline) })
912
+ };
913
+ }
914
+ function cloneOwnershipBaseline(value) {
915
+ const cloneIdentity = (identity) => {
916
+ if (typeof identity !== "object" || identity === null || !("status" in identity))
917
+ return identity;
918
+ return identity.status === "available"
919
+ ? { status: "available", value: identity.value }
920
+ : { status: "unavailable", reason: identity.reason };
921
+ };
922
+ const cloneTurn = (turn) => ({
923
+ ...(turn.stableId === undefined ? {} : { stableId: turn.stableId }),
924
+ evidenceDigest: turn.evidenceDigest,
925
+ structureDigest: turn.structureDigest,
926
+ ordinal: turn.ordinal,
927
+ ...(turn.parentStableId === undefined ? {} : { parentStableId: turn.parentStableId }),
928
+ ...(turn.branchStableId === undefined ? {} : { branchStableId: turn.branchStableId }),
929
+ ...(turn.state === undefined ? {} : { state: turn.state }),
930
+ ...(turn.artifactEvidenceDigests === undefined ? {} : { artifactEvidenceDigests: [...turn.artifactEvidenceDigests] })
931
+ });
932
+ return {
933
+ schemaVersion: value.schemaVersion,
934
+ snapshotDigest: value.snapshotDigest,
935
+ target: {
936
+ provider: cloneIdentity(value.target.provider),
937
+ browser: cloneIdentity(value.target.browser),
938
+ tab: cloneIdentity(value.target.tab),
939
+ thread: cloneIdentity(value.target.thread),
940
+ conversation: cloneIdentity(value.target.conversation),
941
+ canonicalThreadUrl: cloneIdentity(value.target.canonicalThreadUrl),
942
+ authoritativeTabClaim: cloneIdentity(value.target.authoritativeTabClaim),
943
+ coordinationScope: value.target.coordinationScope
944
+ },
945
+ userTurns: value.userTurns.map(cloneTurn),
946
+ assistantTurns: value.assistantTurns.map(cloneTurn),
947
+ completeness: "complete"
948
+ };
949
+ }
950
+ function deepFreeze(value) {
951
+ if (value === null || (typeof value !== "object" && typeof value !== "function"))
952
+ return value;
953
+ const seen = new WeakSet();
954
+ const visit = (current) => {
955
+ if (current === null || (typeof current !== "object" && typeof current !== "function"))
956
+ return;
957
+ const object = current;
958
+ if (seen.has(object))
959
+ return;
960
+ seen.add(object);
961
+ for (const descriptor of Object.values(Object.getOwnPropertyDescriptors(object))) {
962
+ if ("value" in descriptor)
963
+ visit(descriptor.value);
964
+ }
965
+ Object.freeze(object);
966
+ };
967
+ visit(value);
968
+ return value;
969
+ }
970
+ function validateOwnershipBaseline(value) {
971
+ if (!isPlainRecord(value) || value.schemaVersion !== TURN_OWNERSHIP_SCHEMA_VERSION || value.completeness !== "complete") {
972
+ throw new Error("invalid ownership baseline");
973
+ }
974
+ assertExactKeys(value, ["schemaVersion", "snapshotDigest", "target", "userTurns", "assistantTurns", "completeness"]);
975
+ if (!isDigest(value.snapshotDigest))
976
+ throw new Error("invalid ownership baseline digest");
977
+ if (!isPlainRecord(value.target))
978
+ throw new Error("invalid ownership baseline target");
979
+ assertExactKeys(value.target, ["provider", "browser", "tab", "thread", "conversation", "canonicalThreadUrl", "authoritativeTabClaim", "coordinationScope"]);
980
+ if (value.target.coordinationScope !== "process" && value.target.coordinationScope !== "provider")
981
+ throw new Error("invalid ownership baseline scope");
982
+ for (const key of ["provider", "browser", "tab", "thread", "conversation", "canonicalThreadUrl", "authoritativeTabClaim"]) {
983
+ const identity = value.target[key];
984
+ if (!isPlainRecord(identity) || (identity.status !== "available" && identity.status !== "unavailable"))
985
+ throw new Error("invalid ownership identity");
986
+ if (identity.status === "available") {
987
+ assertExactKeys(identity, ["status", "value"]);
988
+ if (!isSafeId(identity.value) && key !== "canonicalThreadUrl")
989
+ throw new Error("invalid ownership identity value");
990
+ if (key === "canonicalThreadUrl" && (typeof identity.value !== "string" || identity.value.length > 4096))
991
+ throw new Error("invalid ownership URL");
992
+ }
993
+ else {
994
+ assertExactKeys(identity, ["status", "reason"]);
995
+ if (identity.reason !== "not_exposed" && identity.reason !== "not_observed" && identity.reason !== "redacted")
996
+ throw new Error("invalid ownership identity reason");
997
+ }
998
+ }
999
+ for (const [turns, kind] of [[value.userTurns, "user"], [value.assistantTurns, "assistant"]]) {
1000
+ if (!Array.isArray(turns) || turns.length > 256)
1001
+ throw new Error("invalid ownership turn bound");
1002
+ turns.forEach((turn, index) => {
1003
+ if (!isPlainRecord(turn))
1004
+ throw new Error("invalid ownership turn");
1005
+ assertExactKeys(turn, ["stableId", "evidenceDigest", "structureDigest", "ordinal", "parentStableId", "branchStableId", "state", "artifactEvidenceDigests"]);
1006
+ if (turn.ordinal !== index || !isDigest(turn.evidenceDigest) || !isDigest(turn.structureDigest))
1007
+ throw new Error("invalid ownership turn");
1008
+ if (turn.stableId !== undefined && !isSafeId(turn.stableId))
1009
+ throw new Error("invalid ownership turn id");
1010
+ if (kind === "user" && (turn.state !== undefined || turn.parentStableId !== undefined || turn.branchStableId !== undefined))
1011
+ throw new Error("invalid user baseline turn");
1012
+ if (kind === "assistant" && turn.state !== "generating" && turn.state !== "terminal")
1013
+ throw new Error("invalid assistant baseline turn");
1014
+ const artifactEvidence = turn.artifactEvidenceDigests ?? [];
1015
+ if (!Array.isArray(artifactEvidence) || artifactEvidence.length > 32 || !artifactEvidence.every(value => typeof value === "string" && isDigest(value)))
1016
+ throw new Error("invalid ownership artifact evidence");
1017
+ });
1018
+ }
1019
+ }
1020
+ function cloneExpected(value) {
1021
+ return {
1022
+ surface: value.surface,
1023
+ targetBindingDigest: value.targetBindingDigest,
1024
+ configurationReceiptDigest: value.configurationReceiptDigest,
1025
+ composerReceiptDigest: value.composerReceiptDigest,
1026
+ attachmentManifest: {
1027
+ count: value.attachmentManifest.count,
1028
+ orderPolicy: "exact",
1029
+ identities: value.attachmentManifest.identities.map(identity => ({
1030
+ identityDigest: identity.identityDigest,
1031
+ ordinal: identity.ordinal
1032
+ }))
1033
+ }
1034
+ };
1035
+ }
1036
+ function isPageLike(value) {
1037
+ return value !== null && typeof value === "object" && !Array.isArray(value);
1038
+ }
1039
+ function isAbortSignalLike(value) {
1040
+ return value !== null && typeof value === "object" && !Array.isArray(value)
1041
+ && typeof value.aborted === "boolean"
1042
+ && typeof value.addEventListener === "function"
1043
+ && typeof value.removeEventListener === "function";
1044
+ }
1045
+ function isPlainRecord(value) {
1046
+ if (value === null || typeof value !== "object" || Array.isArray(value))
1047
+ return false;
1048
+ try {
1049
+ const prototype = Object.getPrototypeOf(value);
1050
+ if (prototype !== Object.prototype && prototype !== null)
1051
+ return false;
1052
+ for (const descriptor of Object.values(Object.getOwnPropertyDescriptors(value))) {
1053
+ if (!("value" in descriptor) || descriptor.get !== undefined || descriptor.set !== undefined)
1054
+ return false;
1055
+ }
1056
+ return true;
1057
+ }
1058
+ catch {
1059
+ return false;
1060
+ }
1061
+ }
1062
+ function hasOwnDataProperty(value, key) {
1063
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
1064
+ return descriptor !== undefined && "value" in descriptor;
1065
+ }
1066
+ function assertExactKeys(value, allowed) {
1067
+ const allowedKeys = new Set(allowed);
1068
+ for (const key of Object.keys(value)) {
1069
+ if (!allowedKeys.has(key))
1070
+ throw new Error("unsupported field");
1071
+ }
1072
+ }
1073
+ function isSafeId(value) {
1074
+ return typeof value === "string" && value.length <= MAX_CALLBACK_STRING && SAFE_ID_PATTERN.test(value) && !/[\u0000-\u001f\u007f]/u.test(value);
1075
+ }
1076
+ function isDigest(value) {
1077
+ return typeof value === "string" && DIGEST_PATTERN.test(value);
1078
+ }
1079
+ function isSubmissionBlockerCode(value) {
1080
+ return typeof value === "string" && SUBMISSION_BLOCKER_CODES.has(value);
1081
+ }