codex-chatgpt-control 0.5.1-alpha.2 → 0.5.1-alpha.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +43 -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/download-blocked-by-browser.json +22 -0
  7. package/contracts/v1/fixtures/download-receipt-failed.json +22 -0
  8. package/contracts/v1/fixtures/download-receipt-timeout.json +22 -0
  9. package/contracts/v1/fixtures/journal-rpc-indeterminate.json +32 -0
  10. package/contracts/v1/fixtures/journal-rpc-unsupported-platform.json +32 -0
  11. package/contracts/v1/fixtures/journal-runtime-unavailable.json +32 -0
  12. package/contracts/v1/fixtures/operation-action-prepared-event.json +42 -0
  13. package/contracts/v1/fixtures/operation-action.json +13 -0
  14. package/contracts/v1/fixtures/operation-artifact-receipt.json +14 -0
  15. package/contracts/v1/fixtures/operation-artifact-transfer-intent-event.json +24 -0
  16. package/contracts/v1/fixtures/operation-artifact-transfer-receipt-event.json +26 -0
  17. package/contracts/v1/fixtures/operation-artifact-transfer-state.json +178 -0
  18. package/contracts/v1/fixtures/operation-blocker.json +10 -0
  19. package/contracts/v1/fixtures/operation-collect-request.json +17 -0
  20. package/contracts/v1/fixtures/operation-collect-result.json +42 -0
  21. package/contracts/v1/fixtures/operation-control-receipt.json +13 -0
  22. package/contracts/v1/fixtures/operation-control-request.json +18 -0
  23. package/contracts/v1/fixtures/operation-control-result.json +31 -0
  24. package/contracts/v1/fixtures/operation-event.json +18 -0
  25. package/contracts/v1/fixtures/operation-handle.json +10 -0
  26. package/contracts/v1/fixtures/operation-inspect-request.json +13 -0
  27. package/contracts/v1/fixtures/operation-inspect-result.json +205 -0
  28. package/contracts/v1/fixtures/operation-ownership-baseline-event.json +34 -0
  29. package/contracts/v1/fixtures/operation-receipt.json +32 -0
  30. package/contracts/v1/fixtures/operation-recovery-decision.json +7 -0
  31. package/contracts/v1/fixtures/operation-recovery-observation.json +12 -0
  32. package/contracts/v1/fixtures/operation-request.json +33 -0
  33. package/contracts/v1/fixtures/operation-state.json +144 -0
  34. package/contracts/v1/fixtures/operation-submission-witness-event.json +20 -0
  35. package/contracts/v1/fixtures/operation-submission-witness.json +11 -0
  36. package/contracts/v1/fixtures/operation-submit-result.json +16 -0
  37. package/contracts/v1/fixtures/operation-target-established-event.json +21 -0
  38. package/contracts/v1/manifest.json +185 -1
  39. package/contracts/v1/parity-suite.json +106 -8
  40. package/contracts/v1/schemas/backend-compatibility.schema.json +65 -0
  41. package/contracts/v1/schemas/backend-request.schema.json +8 -2
  42. package/contracts/v1/schemas/capabilities.schema.json +81 -1
  43. package/contracts/v1/schemas/manifest.schema.json +57 -0
  44. package/contracts/v1/schemas/operation-action.schema.json +155 -0
  45. package/contracts/v1/schemas/operation-artifact-receipt.schema.json +89 -0
  46. package/contracts/v1/schemas/operation-blocker.schema.json +70 -0
  47. package/contracts/v1/schemas/operation-collect-request.schema.json +46 -0
  48. package/contracts/v1/schemas/operation-collect-result.schema.json +74 -0
  49. package/contracts/v1/schemas/operation-control-receipt.schema.json +108 -0
  50. package/contracts/v1/schemas/operation-control-request.schema.json +111 -0
  51. package/contracts/v1/schemas/operation-control-result.schema.json +112 -0
  52. package/contracts/v1/schemas/operation-event.schema.json +756 -0
  53. package/contracts/v1/schemas/operation-handle.schema.json +55 -0
  54. package/contracts/v1/schemas/operation-inspect-request.schema.json +42 -0
  55. package/contracts/v1/schemas/operation-inspect-result.schema.json +1662 -0
  56. package/contracts/v1/schemas/operation-receipt.schema.json +153 -0
  57. package/contracts/v1/schemas/operation-recovery.schema.json +314 -0
  58. package/contracts/v1/schemas/operation-request.schema.json +158 -0
  59. package/contracts/v1/schemas/operation-state.schema.json +691 -0
  60. package/contracts/v1/schemas/operation-submission-witness.schema.json +48 -0
  61. package/contracts/v1/schemas/operation-submit-result.schema.json +94 -0
  62. package/contracts/v1/surface-drift-policy.json +50 -0
  63. package/contracts/v1/vectors/operation-request-digest-v1.json +43 -0
  64. package/dist/codex-chatgpt-control-backend.mjs +45882 -9361
  65. package/dist/codex-chatgpt-control-journal.mjs +5547 -0
  66. package/dist/codex-chatgpt-control-live-smoke.bundle.mjs +51677 -0
  67. package/dist/codex-chatgpt-control-release-canary.bundle.mjs +52104 -0
  68. package/dist/codex-chatgpt-control.bundle.mjs +48392 -10260
  69. package/dist/src/backend/client.d.ts +103 -2
  70. package/dist/src/backend/client.js +1436 -97
  71. package/dist/src/backend/compatibility.d.ts +12 -0
  72. package/dist/src/backend/compatibility.js +208 -0
  73. package/dist/src/backend/protocol.d.ts +84 -1
  74. package/dist/src/backend/protocol.js +31 -5
  75. package/dist/src/backend/runtime-identity.d.ts +10 -0
  76. package/dist/src/backend/runtime-identity.js +141 -0
  77. package/dist/src/backend/session.d.ts +10 -2
  78. package/dist/src/backend/session.js +478 -7
  79. package/dist/src/backend/stdio-server.d.ts +4 -1
  80. package/dist/src/backend/stdio-server.js +240 -28
  81. package/dist/src/browser/active-composer-file-input.d.ts +9 -0
  82. package/dist/src/browser/active-composer-file-input.js +33 -0
  83. package/dist/src/browser/attach.d.ts +4 -1
  84. package/dist/src/browser/attach.js +136 -28
  85. package/dist/src/browser/chatgpt-url.d.ts +2 -0
  86. package/dist/src/browser/chatgpt-url.js +9 -0
  87. package/dist/src/browser/downloads.d.ts +17 -1
  88. package/dist/src/browser/downloads.js +150 -28
  89. package/dist/src/browser/page-state.js +6 -0
  90. package/dist/src/client.d.ts +43 -0
  91. package/dist/src/client.js +1279 -59
  92. package/dist/src/commands/artifacts.js +6 -4
  93. package/dist/src/commands/chat-popover.d.ts +37 -0
  94. package/dist/src/commands/chat-popover.js +537 -0
  95. package/dist/src/commands/configuration.d.ts +5 -0
  96. package/dist/src/commands/configuration.js +134 -21
  97. package/dist/src/commands/context.js +10 -3
  98. package/dist/src/commands/doctor.d.ts +1 -1
  99. package/dist/src/commands/doctor.js +27 -4
  100. package/dist/src/commands/experience.d.ts +5 -0
  101. package/dist/src/commands/experience.js +104 -21
  102. package/dist/src/commands/files.js +14 -33
  103. package/dist/src/commands/modes.js +85 -21
  104. package/dist/src/commands/power-discovery.d.ts +130 -0
  105. package/dist/src/commands/power-discovery.js +669 -0
  106. package/dist/src/commands/project-sources.js +325 -31
  107. package/dist/src/commands/sequence.js +59 -32
  108. package/dist/src/commands/session.js +19 -6
  109. package/dist/src/commands/work.js +31 -0
  110. package/dist/src/dom/composer-text.d.ts +6 -0
  111. package/dist/src/dom/composer-text.js +85 -0
  112. package/dist/src/errors.d.ts +9 -0
  113. package/dist/src/errors.js +22 -0
  114. package/dist/src/index.d.ts +6 -0
  115. package/dist/src/index.js +9 -0
  116. package/dist/src/operations/artifact-output.d.ts +69 -0
  117. package/dist/src/operations/artifact-output.js +1748 -0
  118. package/dist/src/operations/artifact-stream.d.ts +19 -0
  119. package/dist/src/operations/artifact-stream.js +35 -0
  120. package/dist/src/operations/artifact-transfer.d.ts +149 -0
  121. package/dist/src/operations/artifact-transfer.js +1312 -0
  122. package/dist/src/operations/browser-adapter.d.ts +167 -0
  123. package/dist/src/operations/browser-adapter.js +2103 -0
  124. package/dist/src/operations/browser-observation.d.ts +110 -0
  125. package/dist/src/operations/browser-observation.js +1256 -0
  126. package/dist/src/operations/browser-target.d.ts +105 -0
  127. package/dist/src/operations/browser-target.js +531 -0
  128. package/dist/src/operations/canonical.d.ts +4 -0
  129. package/dist/src/operations/canonical.js +412 -0
  130. package/dist/src/operations/chatgpt-runtime.d.ts +72 -0
  131. package/dist/src/operations/chatgpt-runtime.js +1232 -0
  132. package/dist/src/operations/client.d.ts +138 -0
  133. package/dist/src/operations/client.js +1145 -0
  134. package/dist/src/operations/collector.d.ts +195 -0
  135. package/dist/src/operations/collector.js +1039 -0
  136. package/dist/src/operations/configuration-routing.d.ts +4 -0
  137. package/dist/src/operations/configuration-routing.js +14 -0
  138. package/dist/src/operations/control.d.ts +372 -0
  139. package/dist/src/operations/control.js +1498 -0
  140. package/dist/src/operations/file-identity.d.ts +33 -0
  141. package/dist/src/operations/file-identity.js +153 -0
  142. package/dist/src/operations/handle.d.ts +19 -0
  143. package/dist/src/operations/handle.js +727 -0
  144. package/dist/src/operations/index.d.ts +25 -0
  145. package/dist/src/operations/index.js +25 -0
  146. package/dist/src/operations/journal-authority.d.ts +12 -0
  147. package/dist/src/operations/journal-authority.js +1 -0
  148. package/dist/src/operations/journal-rpc-client.d.ts +12 -0
  149. package/dist/src/operations/journal-rpc-client.js +225 -0
  150. package/dist/src/operations/journal-rpc-protocol.d.ts +32 -0
  151. package/dist/src/operations/journal-rpc-protocol.js +214 -0
  152. package/dist/src/operations/journal-rpc-server.d.ts +15 -0
  153. package/dist/src/operations/journal-rpc-server.js +272 -0
  154. package/dist/src/operations/journal.d.ts +143 -0
  155. package/dist/src/operations/journal.js +1957 -0
  156. package/dist/src/operations/production-attachments.d.ts +118 -0
  157. package/dist/src/operations/production-attachments.js +1035 -0
  158. package/dist/src/operations/production-chatgpt-artifacts.d.ts +74 -0
  159. package/dist/src/operations/production-chatgpt-artifacts.js +1146 -0
  160. package/dist/src/operations/production-chatgpt-attachments.d.ts +80 -0
  161. package/dist/src/operations/production-chatgpt-attachments.js +1639 -0
  162. package/dist/src/operations/production-configuration.d.ts +42 -0
  163. package/dist/src/operations/production-configuration.js +1544 -0
  164. package/dist/src/operations/production-primitives.d.ts +53 -0
  165. package/dist/src/operations/production-primitives.js +856 -0
  166. package/dist/src/operations/production-work-steer.d.ts +203 -0
  167. package/dist/src/operations/production-work-steer.js +1218 -0
  168. package/dist/src/operations/recovery.d.ts +80 -0
  169. package/dist/src/operations/recovery.js +174 -0
  170. package/dist/src/operations/runtime-adapter.d.ts +126 -0
  171. package/dist/src/operations/runtime-adapter.js +802 -0
  172. package/dist/src/operations/send-once.d.ts +224 -0
  173. package/dist/src/operations/send-once.js +1081 -0
  174. package/dist/src/operations/service.d.ts +286 -0
  175. package/dist/src/operations/service.js +2831 -0
  176. package/dist/src/operations/staging.d.ts +136 -0
  177. package/dist/src/operations/staging.js +630 -0
  178. package/dist/src/operations/state-machine.d.ts +29 -0
  179. package/dist/src/operations/state-machine.js +2047 -0
  180. package/dist/src/operations/submission.d.ts +423 -0
  181. package/dist/src/operations/submission.js +1676 -0
  182. package/dist/src/operations/transactional-chat-power.d.ts +17 -0
  183. package/dist/src/operations/transactional-chat-power.js +212 -0
  184. package/dist/src/operations/turn-ownership.d.ts +202 -0
  185. package/dist/src/operations/turn-ownership.js +700 -0
  186. package/dist/src/operations/types.d.ts +454 -0
  187. package/dist/src/operations/types.js +20 -0
  188. package/dist/src/operations/wire-requests.d.ts +21 -0
  189. package/dist/src/operations/wire-requests.js +396 -0
  190. package/dist/src/operations/wire-results.d.ts +97 -0
  191. package/dist/src/operations/wire-results.js +818 -0
  192. package/dist/src/runner/responses.d.ts +3 -1
  193. package/dist/src/runner/responses.js +19 -1
  194. package/dist/src/runner/result.js +227 -55
  195. package/dist/src/runner/types.d.ts +12 -0
  196. package/dist/src/runtime/command-routing.d.ts +149 -0
  197. package/dist/src/runtime/command-routing.js +431 -0
  198. package/dist/src/runtime/coordinated-browser.d.ts +24 -0
  199. package/dist/src/runtime/coordinated-browser.js +315 -0
  200. package/dist/src/runtime/coordinated-page.d.ts +41 -0
  201. package/dist/src/runtime/coordinated-page.js +651 -0
  202. package/dist/src/runtime/operation-context.d.ts +142 -0
  203. package/dist/src/runtime/operation-context.js +410 -0
  204. package/dist/src/runtime/runtime-session.d.ts +95 -0
  205. package/dist/src/runtime/runtime-session.js +314 -0
  206. package/dist/src/runtime/tab-coordinator.d.ts +200 -0
  207. package/dist/src/runtime/tab-coordinator.js +1200 -0
  208. package/dist/src/runtime/value-boundaries.d.ts +18 -0
  209. package/dist/src/runtime/value-boundaries.js +49 -0
  210. package/dist/src/safety/blockers.js +8 -1
  211. package/dist/src/safety/untrusted-output.js +2 -1
  212. package/dist/src/scripts/backend-server.js +4 -1
  213. package/dist/src/scripts/capture-surface-profile.js +7 -2
  214. package/dist/src/scripts/journal-server.d.ts +2 -0
  215. package/dist/src/scripts/journal-server.js +37 -0
  216. package/dist/src/scripts/live-smoke/harness.js +79 -7
  217. package/dist/src/scripts/live-smoke/scenarios.d.ts +42 -1
  218. package/dist/src/scripts/live-smoke/scenarios.js +147 -30
  219. package/dist/src/scripts/live-smoke/transactional.d.ts +17 -0
  220. package/dist/src/scripts/live-smoke/transactional.js +254 -0
  221. package/dist/src/scripts/live-smoke/types.d.ts +3 -0
  222. package/dist/src/scripts/release-canary-module.js +17 -4
  223. package/dist/src/types.d.ts +38 -1
  224. package/package.json +8 -3
  225. package/references/2026-08-16-transactional-operations.md +459 -0
  226. package/references/2026-09-06-journal-service.md +140 -0
  227. package/references/agents-runner.md +34 -0
  228. package/references/backend-protocol.md +176 -0
  229. package/references/python-parity.md +137 -0
  230. package/references/responses-adapter.md +19 -0
  231. package/references/streaming.md +6 -0
@@ -0,0 +1,1676 @@
1
+ import { assertDurableCapturePolicyShape, assertOwnershipBaselineShape } from "./state-machine.js";
2
+ import { OPERATION_ARTIFACT_RECEIPT_SCHEMA_VERSION, OPERATION_HANDLE_SCHEMA_VERSION, OPERATION_RECEIPT_SCHEMA_VERSION, OPERATION_SUBMISSION_WITNESS_SCHEMA_VERSION, OPERATION_SCHEMA_VERSION } from "./types.js";
3
+ /**
4
+ * The submission coordinator is deliberately an adapter-free core. A
5
+ * browser adapter owns all DOM/page primitives and supplies only these
6
+ * bounded, already-redacted observations and transactions. In particular,
7
+ * prompt text, local paths, display names, and file bytes never cross this
8
+ * interface.
9
+ */
10
+ const DIGEST_PATTERN = /^hmac-sha256:[0-9a-f]{64}$/;
11
+ const OPAQUE_ID_PATTERN = /^[A-Za-z0-9._:-]{1,512}$/;
12
+ const OPAQUE_KEY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
13
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
14
+ const SHA256_PATTERN = /^[0-9a-f]{64}$/;
15
+ const CODE_PATTERN = /^[a-z][a-z0-9_]{0,63}$/;
16
+ const OUTPUT_KEY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
17
+ const MIME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9!#$&^_.+/-]{0,126}$/;
18
+ const FINISH_REASON_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
19
+ const INSTANT_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
20
+ const MAX_ATTACHMENTS = 256;
21
+ // Keep the adapter boundary aligned with the shared receipt contract and the
22
+ // central state-machine validator. A looser limit here would let a malformed
23
+ // durable receipt bypass the operation service's first validation boundary.
24
+ const MAX_RECEIPT_ARTIFACTS = 32;
25
+ // Keep the absolute deadline in the representable, unambiguous ISO date
26
+ // range while rejecting numeric sentinels such as MAX_SAFE_INTEGER.
27
+ const MAX_DEADLINE_AT = Date.UTC(2100, 0, 1);
28
+ const POST_HANDOFF_OBSERVATION_ATTEMPTS = 20;
29
+ const POST_HANDOFF_OBSERVATION_INTERVAL_MS = 150;
30
+ // The fail-closed blocker branch still has to carry the public operation
31
+ // identity shape. These markers are deliberately not UUIDs/digests, so
32
+ // malformed or absent evidence cannot masquerade as authenticated identity.
33
+ const INVALID_OPERATION_ID = "invalid-operation";
34
+ const INVALID_DIGEST = "invalid-digest";
35
+ const PHASES = new Set([
36
+ "prepared", "handoff_pending", "ready", "send_pending", "submitted", "generating", "capturing", "completed", "uncertain"
37
+ ]);
38
+ const BOUNDARIES = new Set([
39
+ "none", "handoff_may_have_occurred", "send_may_have_occurred", "control_may_have_occurred"
40
+ ]);
41
+ const BOUNDARY_RANK = Object.freeze({
42
+ none: 0,
43
+ handoff_may_have_occurred: 1,
44
+ send_may_have_occurred: 2,
45
+ control_may_have_occurred: 3
46
+ });
47
+ export class SubmissionInputError extends Error {
48
+ code;
49
+ constructor(code, message) {
50
+ super(message);
51
+ this.code = code;
52
+ this.name = "SubmissionInputError";
53
+ }
54
+ }
55
+ /**
56
+ * Execute one operation-aware submission. All non-repeatable calls are
57
+ * preceded by a durable intent and all ambiguous outcomes become observation
58
+ * only. This function intentionally does not generate operation or action
59
+ * IDs; callers must provide the durable action IDs.
60
+ */
61
+ export async function runAtomicSubmission(operation, expected, ports, options = {}) {
62
+ const identity = safeIdentity(operation);
63
+ let envelope;
64
+ try {
65
+ envelope = validateInput(operation, expected, ports, options);
66
+ }
67
+ catch (error) {
68
+ const code = error instanceof SubmissionInputError ? error.code : "port_protocol_violation";
69
+ return blockedResult(identity, expected, code, false);
70
+ }
71
+ const base = {
72
+ operationId: operation.state.operationId,
73
+ requestDigest: operation.state.requestDigest,
74
+ surface: operation.state.surface,
75
+ targetBindingDigest: envelope.targetBindingDigest
76
+ };
77
+ const initialCancellation = cancellationCode(options);
78
+ if (initialCancellation !== undefined)
79
+ return cancelledResult(base, initialCancellation, operation.state.mutationBoundary);
80
+ const sendAction = findUniqueAction(operation.state, "send");
81
+ const handoffAction = findUniqueAction(operation.state, "file_handoff");
82
+ if (sendAction === "corrupt" || handoffAction === "corrupt") {
83
+ return blockedBase(base, "operation_state_corrupt", false, operation.state.mutationBoundary);
84
+ }
85
+ if (sendAction !== undefined && operation.actionIds.sendActionId !== sendAction.actionId
86
+ || handoffAction !== undefined && operation.actionIds.fileHandoffActionId !== handoffAction.actionId) {
87
+ return blockedBase(base, "operation_state_corrupt", false, operation.state.mutationBoundary);
88
+ }
89
+ if (!coherentSubmissionState(operation.state, sendAction, handoffAction, envelope.targetBindingDigest, envelope.attachmentManifest.count)) {
90
+ return blockedBase(base, "operation_state_corrupt", false, operation.state.mutationBoundary);
91
+ }
92
+ if (!targetBindingMatches(operation, envelope)) {
93
+ return blockedBase(base, "target_binding_mismatch", false, operation.state.mutationBoundary);
94
+ }
95
+ const requiresTargetEstablishment = operation.state.target?.targetLifecycle === "new_pending";
96
+ const durableBaseline = sendAction === undefined
97
+ ? operation.state.ownershipBaseline?.baseline
98
+ : (operation.state.ownershipBaselines?.[sendAction.actionId]?.baseline ?? operation.state.ownershipBaseline?.baseline);
99
+ // A durable receipt is already an idempotent observation. Do not touch the
100
+ // browser, even to "confirm" an operation the journal says is complete.
101
+ if (operation.state.receipt !== undefined) {
102
+ const receipt = operation.state.receipt;
103
+ if (operation.state.phase !== "completed" ||
104
+ receipt.operationId !== base.operationId ||
105
+ receipt.requestDigest !== base.requestDigest ||
106
+ receipt.targetBindingDigest !== envelope.targetBindingDigest ||
107
+ !isSafeId(receipt.userTurnId) ||
108
+ !isSafeId(receipt.assistantTurnId) ||
109
+ !isDigest(receipt.ownershipEvidenceDigest) ||
110
+ !isDigest(receipt.userTurnEvidenceDigest)) {
111
+ return blockedBase(base, "operation_state_corrupt", false, operation.state.mutationBoundary);
112
+ }
113
+ return {
114
+ ...base,
115
+ kind: "completed_receipt",
116
+ evidenceDigest: receipt.ownershipEvidenceDigest,
117
+ userTurnId: receipt.userTurnId,
118
+ userTurnEvidenceDigest: receipt.userTurnEvidenceDigest,
119
+ ...(sendAction === undefined ? {} : { actionId: sendAction.actionId }),
120
+ assistantTurnId: receipt.assistantTurnId
121
+ };
122
+ }
123
+ // Once a Send intent is durable, recovery is observation-only. It must not
124
+ // inspect or refill the composer/configuration/attachments first: those
125
+ // surfaces may legitimately have changed after the provider accepted Send.
126
+ if (sendAction !== undefined) {
127
+ if (durableBaseline === undefined) {
128
+ // Legacy records may still be readable, but they cannot authorize a
129
+ // restart observation because the exact pre-Send ownership anchor is
130
+ // absent. Never let this branch fall back to a current page snapshot.
131
+ return blockedBase(base, "target_evidence_unavailable", true, "send_may_have_occurred");
132
+ }
133
+ return reconcileExistingSend(base, envelope, sendAction, ports, { ...options, durableBaseline }, requiresTargetEstablishment);
134
+ }
135
+ const staging = await observeStaging(base, envelope, ports);
136
+ if (staging.kind !== "ok")
137
+ return staging.result;
138
+ let readyEvidenceDigest = staging.evidenceDigest;
139
+ let readyBoundary = operation.state.mutationBoundary;
140
+ let readyActionId;
141
+ if (envelope.attachmentManifest.count > 0) {
142
+ const attachments = await ensureAttachments(base, envelope, operation, handoffAction, ports, options);
143
+ if (attachments.kind !== "ok")
144
+ return attachments.result;
145
+ readyEvidenceDigest = attachments.evidenceDigest;
146
+ readyBoundary = attachments.mutationBoundary;
147
+ readyActionId = attachments.actionId;
148
+ }
149
+ else if (handoffAction !== undefined) {
150
+ return blockedBase(base, "operation_state_corrupt", false, operation.state.mutationBoundary);
151
+ }
152
+ if (operation.state.phase !== "prepared"
153
+ && operation.state.phase !== "ready"
154
+ && !(handoffAction !== undefined && (operation.state.phase === "handoff_pending" || operation.state.phase === "uncertain"))) {
155
+ return blockedBase(base, "stale_handle", false, operation.state.mutationBoundary);
156
+ }
157
+ if (operation.state.phase !== "ready") {
158
+ const handoffTransitionEvidence = readyActionId === undefined
159
+ ? {}
160
+ : handoffAction?.outcome === "satisfied"
161
+ ? { actionId: readyActionId }
162
+ : { actionId: readyActionId, actionOutcome: "satisfied" };
163
+ try {
164
+ await ports.persistReceiptEvidence({
165
+ kind: "phase",
166
+ operationId: base.operationId,
167
+ requestDigest: base.requestDigest,
168
+ surface: base.surface,
169
+ phase: "ready",
170
+ mutationBoundary: readyBoundary,
171
+ targetBindingDigest: envelope.targetBindingDigest,
172
+ evidenceDigest: readyEvidenceDigest,
173
+ ...handoffTransitionEvidence
174
+ });
175
+ }
176
+ catch {
177
+ return readyBoundary === "none"
178
+ ? blockedBase(base, "journal_unavailable", false, readyBoundary)
179
+ : uncertainBase(base, "journal_unavailable", readyBoundary);
180
+ }
181
+ }
182
+ const preSendCancellation = cancellationCode(options);
183
+ if (preSendCancellation !== undefined)
184
+ return cancelledResult(base, preSendCancellation, readyBoundary);
185
+ const sendActionId = operation.actionIds.sendActionId;
186
+ const sendPorts = requirePreparedSendPorts(ports);
187
+ if (sendPorts !== undefined)
188
+ return blockedBase(base, sendPorts, readyBoundary !== "none", readyBoundary);
189
+ // Phase 1: read-only provider preparation. The opaque provider capability
190
+ // stays in request-local memory; only its complete redacted baseline may
191
+ // cross the next boundary.
192
+ let preparedResult;
193
+ try {
194
+ preparedResult = await ports.prepareSend({
195
+ operationId: base.operationId,
196
+ requestDigest: base.requestDigest,
197
+ surface: base.surface,
198
+ actionId: sendActionId,
199
+ expected: cloneExpectedEnvelope(envelope),
200
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
201
+ ...(options.deadlineAt === undefined ? {} : { deadlineAt: options.deadlineAt })
202
+ });
203
+ validatePrepareSendResult(preparedResult, base, envelope, sendActionId);
204
+ }
205
+ catch {
206
+ return blockedBase(base, "port_protocol_violation", readyBoundary !== "none", readyBoundary);
207
+ }
208
+ if (preparedResult.status === "blocked") {
209
+ return blockedBase(base, preparedResult.result.blockerCode, readyBoundary !== "none", readyBoundary, preparedResult.result.evidenceDigest);
210
+ }
211
+ const prepared = preparedResult.prepared;
212
+ const prePersistenceCancellation = cancellationCode(options);
213
+ if (prePersistenceCancellation !== undefined)
214
+ return cancelledResult(base, prePersistenceCancellation, readyBoundary);
215
+ // Phase 2: one atomic durable event containing both the Send intent and the
216
+ // complete pre-Send baseline. No browser actor is held by this call.
217
+ let persistence;
218
+ try {
219
+ persistence = await ports.persistPreparedSend({
220
+ operationId: base.operationId,
221
+ durableRequestDigest: base.requestDigest,
222
+ requestDigest: base.requestDigest,
223
+ surface: base.surface,
224
+ actionId: sendActionId,
225
+ kind: "send",
226
+ repeatPolicy: "observe_only_after_intent",
227
+ targetBindingDigest: envelope.targetBindingDigest,
228
+ baseline: cloneOwnershipBaseline(prepared.baseline)
229
+ });
230
+ validatePreparedSendPersistenceResult(persistence);
231
+ }
232
+ catch {
233
+ // A journal port is responsible for converging a commit-then-throw by
234
+ // rereading the action+baseline prefix. If it cannot establish that
235
+ // result, quarantine the durable mutation boundary and never retry here.
236
+ return uncertainBase(base, "journal_unavailable", "send_may_have_occurred");
237
+ }
238
+ if (persistence.status === "not_committed") {
239
+ return blockedBase(base, persistence.blockerCode ?? "journal_unavailable", readyBoundary !== "none", readyBoundary, persistence.evidenceDigest);
240
+ }
241
+ if (persistence.status === "uncertain") {
242
+ return uncertainBase(base, "journal_unavailable", "send_may_have_occurred", persistence.evidenceDigest);
243
+ }
244
+ const cancellationAfterPreparedSend = cancellationCode(options);
245
+ if (cancellationAfterPreparedSend !== undefined) {
246
+ return uncertainBase(base, cancellationAfterPreparedSend, "send_may_have_occurred");
247
+ }
248
+ // A concurrent caller that already committed this action is observation
249
+ // only. It must not invoke executePreparedSend, even if this caller also
250
+ // completed an equivalent read-only prepare.
251
+ if (!persistence.executeAllowed) {
252
+ return await reconcileExistingSend(base, envelope, {
253
+ actionId: sendActionId,
254
+ kind: "send",
255
+ repeatPolicy: "observe_only_after_intent",
256
+ requestDigest: base.requestDigest,
257
+ targetDigest: envelope.targetBindingDigest,
258
+ intentRevision: operation.state.revision + 1,
259
+ intentAt: new Date().toISOString()
260
+ }, ports, { ...options, durableBaseline: prepared.baseline }, requiresTargetEstablishment);
261
+ }
262
+ // Phase 3: execute exactly once. This port owns only the final recheck and
263
+ // activation; it cannot persist and cannot poll while holding a tab actor.
264
+ let execution;
265
+ try {
266
+ execution = await ports.executePreparedSend({
267
+ operationId: base.operationId,
268
+ requestDigest: base.requestDigest,
269
+ surface: base.surface,
270
+ actionId: sendActionId,
271
+ expected: cloneExpectedEnvelope(envelope),
272
+ prepared,
273
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
274
+ ...(options.deadlineAt === undefined ? {} : { deadlineAt: options.deadlineAt })
275
+ });
276
+ validateExecutePreparedSendResult(execution, envelope);
277
+ }
278
+ catch {
279
+ execution = { status: "uncertain", result: { status: "uncertain", quarantine: "caller" } };
280
+ }
281
+ if (execution.status === "blocked") {
282
+ const evidence = blockerEvidence(base, envelope, execution.result.blockerCode, execution.result.evidenceDigest);
283
+ await persistBlockerBestEffort(ports, evidence);
284
+ return blockedBase(base, execution.result.blockerCode, true, "send_may_have_occurred", execution.result.evidenceDigest);
285
+ }
286
+ // Phase 4: verify/reconcile in a read-only port. This runs for both a
287
+ // successful activation and an acts-then-throws/unknown execution result;
288
+ // it is never allowed to call executePreparedSend again.
289
+ const verification = await verifyPreparedSendSafely(base, envelope, sendActionId, prepared, execution.status === "uncertain" ? "activation_threw" : execution.activation, ports, options);
290
+ if (verification.status === "submitted" || verification.status === "already_submitted") {
291
+ return await finishObserved(base, envelope, sendActionId, verification, ports, requiresTargetEstablishment);
292
+ }
293
+ const cancellationAfterExecution = cancellationCode(options);
294
+ const verificationCode = cancellationAfterExecution ?? (verification.status === "blocked" ? verification.blockerCode : "ambiguous_submit");
295
+ await persistBlockerBestEffort(ports, blockerEvidence(base, envelope, verificationCode, verification.evidenceDigest));
296
+ return uncertainBase(base, verificationCode, "send_may_have_occurred", verification.evidenceDigest);
297
+ }
298
+ async function observeStaging(base, expected, ports) {
299
+ const request = {
300
+ operationId: base.operationId,
301
+ requestDigest: base.requestDigest,
302
+ surface: base.surface,
303
+ targetBindingDigest: expected.targetBindingDigest,
304
+ configurationReceiptDigest: expected.configurationReceiptDigest,
305
+ composerReceiptDigest: expected.composerReceiptDigest
306
+ };
307
+ let observed;
308
+ try {
309
+ observed = await ports.observeStaging(request);
310
+ validateStageObservation(observed);
311
+ }
312
+ catch {
313
+ return { kind: "result", result: blockedBase(base, "port_protocol_violation", false, "none") };
314
+ }
315
+ if (observed.status === "exact")
316
+ return { kind: "ok", evidenceDigest: observed.evidenceDigest };
317
+ if (observed.status === "unavailable") {
318
+ return {
319
+ kind: "result",
320
+ result: blockedBase(base, observed.reason === "target" ? "target_evidence_unavailable" : "port_protocol_violation", false, "none", observed.evidenceDigest)
321
+ };
322
+ }
323
+ return {
324
+ kind: "result",
325
+ result: blockedBase(base, stageReasonToBlocker(observed.reason), false, "none", observed.evidenceDigest)
326
+ };
327
+ }
328
+ async function ensureAttachments(base, expected, operation, existingHandoff, ports, options) {
329
+ const request = {
330
+ operationId: base.operationId,
331
+ requestDigest: base.requestDigest,
332
+ surface: base.surface,
333
+ targetBindingDigest: expected.targetBindingDigest,
334
+ manifest: cloneManifest(expected.attachmentManifest)
335
+ };
336
+ let observed;
337
+ try {
338
+ observed = await ports.observeAttachments(request);
339
+ validateAttachmentObservation(observed, expected.attachmentManifest);
340
+ }
341
+ catch {
342
+ return { kind: "result", result: blockedBase(base, "port_protocol_violation", false, "none") };
343
+ }
344
+ if (existingHandoff !== undefined && (existingHandoff.targetDigest !== expected.targetBindingDigest ||
345
+ existingHandoff.requestDigest !== base.requestDigest ||
346
+ existingHandoff.repeatPolicy !== "observe_only_after_intent")) {
347
+ return { kind: "result", result: blockedBase(base, "operation_state_corrupt", true, "handoff_may_have_occurred") };
348
+ }
349
+ if (observed.status === "exact") {
350
+ return {
351
+ kind: "ok",
352
+ evidenceDigest: observed.evidenceDigest,
353
+ mutationBoundary: existingHandoff === undefined ? "none" : "handoff_may_have_occurred",
354
+ ...(existingHandoff === undefined ? {} : { actionId: existingHandoff.actionId })
355
+ };
356
+ }
357
+ // A delayed, ambiguous, or unavailable postcondition may represent a
358
+ // handoff already in flight by another actor. Without a durable intent in
359
+ // this operation, do not guess and start a duplicate non-repeatable call.
360
+ // The adapter can retry this read-only observation later.
361
+ if (existingHandoff === undefined && observed.status !== "absent") {
362
+ return {
363
+ kind: "result",
364
+ result: blockedBase(base, observed.status === "unavailable" ? "target_evidence_unavailable" : "attachment_manifest_mismatch", false, "none", observed.evidenceDigest)
365
+ };
366
+ }
367
+ let handoff = existingHandoff;
368
+ if (handoff === undefined) {
369
+ const actionId = operation.actionIds.fileHandoffActionId;
370
+ if (actionId === undefined) {
371
+ return { kind: "result", result: blockedBase(base, "operation_state_corrupt", false, "none") };
372
+ }
373
+ const cancellation = cancellationCode(options);
374
+ if (cancellation !== undefined)
375
+ return { kind: "result", result: cancelledResult(base, cancellation, "none") };
376
+ try {
377
+ await ports.persistActionIntent({
378
+ operationId: base.operationId,
379
+ requestDigest: base.requestDigest,
380
+ surface: base.surface,
381
+ actionId,
382
+ kind: "file_handoff",
383
+ repeatPolicy: "observe_only_after_intent",
384
+ targetBindingDigest: expected.targetBindingDigest
385
+ });
386
+ }
387
+ catch {
388
+ return { kind: "result", result: blockedBase(base, "journal_unavailable", false, "none") };
389
+ }
390
+ const cancellationAfterHandoffIntent = cancellationCode(options);
391
+ if (cancellationAfterHandoffIntent !== undefined) {
392
+ return {
393
+ kind: "result",
394
+ result: uncertainBase(base, cancellationAfterHandoffIntent, "handoff_may_have_occurred")
395
+ };
396
+ }
397
+ handoff = {
398
+ actionId,
399
+ kind: "file_handoff",
400
+ repeatPolicy: "observe_only_after_intent",
401
+ requestDigest: base.requestDigest,
402
+ targetDigest: expected.targetBindingDigest,
403
+ intentRevision: operation.state.revision + 1,
404
+ intentAt: new Date().toISOString()
405
+ };
406
+ }
407
+ if (handoff.targetDigest !== expected.targetBindingDigest || handoff.requestDigest !== base.requestDigest || handoff.repeatPolicy !== "observe_only_after_intent") {
408
+ return { kind: "result", result: blockedBase(base, "operation_state_corrupt", true, "handoff_may_have_occurred") };
409
+ }
410
+ // The presence of an intent, including one recovered after a crash, forbids
411
+ // calling executeFileHandoffOnce again. Existing intents are observation
412
+ // only; a newly persisted intent authorizes exactly this one call.
413
+ let handoffResult;
414
+ if (existingHandoff === undefined) {
415
+ try {
416
+ handoffResult = await ports.executeFileHandoffOnce({
417
+ operationId: base.operationId,
418
+ requestDigest: base.requestDigest,
419
+ surface: base.surface,
420
+ actionId: handoff.actionId,
421
+ targetBindingDigest: expected.targetBindingDigest,
422
+ manifest: cloneManifest(expected.attachmentManifest),
423
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
424
+ ...(options.deadlineAt === undefined ? {} : { deadlineAt: options.deadlineAt })
425
+ });
426
+ validateHandoffResult(handoffResult);
427
+ }
428
+ catch {
429
+ // An adapter rejection after the intent is indistinguishable from an
430
+ // acts-then-throws browser call. Reconcile once and never retry.
431
+ handoffResult = { status: "uncertain", quarantine: "caller" };
432
+ }
433
+ // A success acknowledgment is not proof that the provider rendered the
434
+ // requested manifest; every handoff outcome is reconciled exactly once.
435
+ observed = await observeAttachmentsAfterHandoff(ports, request, expected.attachmentManifest, options);
436
+ }
437
+ if (observed.status === "exact") {
438
+ return { kind: "ok", evidenceDigest: observed.evidenceDigest, mutationBoundary: "handoff_may_have_occurred", actionId: handoff.actionId };
439
+ }
440
+ if (handoffResult?.status === "not_satisfied" && handoffResult.blockerCode !== undefined) {
441
+ return {
442
+ kind: "result",
443
+ result: blockedBase(base, handoffResult.blockerCode, true, "handoff_may_have_occurred", observed.evidenceDigest ?? handoffResult.evidenceDigest)
444
+ };
445
+ }
446
+ const cancelled = cancellationCode(options);
447
+ if (cancelled !== undefined) {
448
+ return { kind: "result", result: uncertainBase(base, cancelled, "handoff_may_have_occurred", observed.evidenceDigest) };
449
+ }
450
+ return {
451
+ kind: "result",
452
+ result: uncertainBase(base, "ambiguous_file_handoff", "handoff_may_have_occurred", observed.evidenceDigest)
453
+ };
454
+ }
455
+ async function reconcileExistingSend(base, expected, sendAction, ports, options, requiresTargetEstablishment) {
456
+ if (sendAction.targetDigest !== expected.targetBindingDigest ||
457
+ sendAction.requestDigest !== base.requestDigest ||
458
+ sendAction.repeatPolicy !== "observe_only_after_intent") {
459
+ return blockedBase(base, "operation_state_corrupt", true, "send_may_have_occurred");
460
+ }
461
+ if (ports.recoverSend === undefined) {
462
+ return blockedBase(base, "port_protocol_violation", true, "send_may_have_occurred");
463
+ }
464
+ let final;
465
+ try {
466
+ final = await ports.recoverSend({
467
+ operationId: base.operationId,
468
+ requestDigest: base.requestDigest,
469
+ surface: base.surface,
470
+ actionId: sendAction.actionId,
471
+ expected: cloneExpectedEnvelope(expected),
472
+ durableBaseline: cloneOwnershipBaseline(options.durableBaseline),
473
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
474
+ ...(options.deadlineAt === undefined ? {} : { deadlineAt: options.deadlineAt })
475
+ });
476
+ validateFinalResult(final, expected, "observe_only");
477
+ }
478
+ catch {
479
+ return uncertainBase(base, "ambiguous_submit", "send_may_have_occurred");
480
+ }
481
+ if (final.status === "submitted" || final.status === "already_submitted") {
482
+ return await finishObserved(base, expected, sendAction.actionId, final, ports, requiresTargetEstablishment);
483
+ }
484
+ const cancelled = cancellationCode(options);
485
+ if (cancelled !== undefined)
486
+ return uncertainBase(base, cancelled, "send_may_have_occurred", final.evidenceDigest);
487
+ if (final.status === "blocked")
488
+ return blockedBase(base, final.blockerCode, true, "send_may_have_occurred", final.evidenceDigest);
489
+ return uncertainBase(base, "ambiguous_submit", "send_may_have_occurred", final.evidenceDigest);
490
+ }
491
+ function requirePreparedSendPorts(ports) {
492
+ if (typeof ports.prepareSend !== "function"
493
+ || typeof ports.persistPreparedSend !== "function"
494
+ || typeof ports.executePreparedSend !== "function"
495
+ || typeof ports.verifyPreparedSend !== "function") {
496
+ return "port_protocol_violation";
497
+ }
498
+ return undefined;
499
+ }
500
+ function validatePrepareSendResult(value, base, expected, actionId) {
501
+ if (!isPlainRecord(value) || (value.status !== "prepared" && value.status !== "blocked"))
502
+ throw new Error("invalid Send prepare result");
503
+ if (value.status === "blocked") {
504
+ assertExactRecord(value, ["status", "result"], ["status", "result"]);
505
+ validateFinalResult(value.result, expected, "mutate_once");
506
+ if (value.result.status !== "blocked")
507
+ throw new Error("invalid Send prepare blocker");
508
+ return;
509
+ }
510
+ assertExactRecord(value, ["status", "prepared"], ["status", "prepared"]);
511
+ if (!isPlainRecord(value.prepared))
512
+ throw new Error("invalid Send prepared value");
513
+ assertExactRecord(value.prepared, ["prepared", "baseline", "evidenceDigest"], ["prepared", "baseline", "evidenceDigest"]);
514
+ if (value.prepared.prepared === null || value.prepared.prepared === undefined)
515
+ throw new Error("missing opaque Send prepared value");
516
+ if (!isDigest(value.prepared.evidenceDigest))
517
+ throw new Error("invalid Send preparation evidence");
518
+ validateOwnershipBaselineForSend(value.prepared.baseline, base, expected, actionId);
519
+ }
520
+ function validatePreparedSendPersistenceResult(value) {
521
+ if (!isPlainRecord(value) || (value.status !== "committed" && value.status !== "not_committed" && value.status !== "uncertain"))
522
+ throw new Error("invalid Send persistence result");
523
+ if (value.status === "committed") {
524
+ if (typeof value.executeAllowed !== "boolean")
525
+ throw new Error("invalid Send persistence ownership");
526
+ assertExactRecord(value, ["status", "executeAllowed"], ["status", "executeAllowed"]);
527
+ return;
528
+ }
529
+ assertExactRecord(value, ["status", "blockerCode", "evidenceDigest"], ["status"]);
530
+ if (value.status === "not_committed" && value.blockerCode !== undefined && !isBlockerCode(value.blockerCode))
531
+ throw new Error("invalid Send persistence blocker");
532
+ if (value.evidenceDigest !== undefined && !isDigest(value.evidenceDigest))
533
+ throw new Error("invalid Send persistence evidence");
534
+ }
535
+ function validateExecutePreparedSendResult(value, expected) {
536
+ if (!isPlainRecord(value) || (value.status !== "activated" && value.status !== "activation_threw" && value.status !== "blocked" && value.status !== "uncertain"))
537
+ throw new Error("invalid Send execute result");
538
+ if (value.status === "blocked" || value.status === "uncertain") {
539
+ assertExactRecord(value, ["status", "result"], ["status", "result"]);
540
+ validateFinalResult(value.result, expected, "mutate_once");
541
+ if (value.result.status !== value.status)
542
+ throw new Error("invalid Send execute result status");
543
+ return;
544
+ }
545
+ assertExactRecord(value, ["status", "activation", "mutationMayHaveOccurred"], ["status", "activation", "mutationMayHaveOccurred"]);
546
+ if (value.activation !== value.status || value.mutationMayHaveOccurred !== true)
547
+ throw new Error("invalid Send execution boundary");
548
+ }
549
+ async function verifyPreparedSendSafely(base, expected, actionId, prepared, activation, ports, options) {
550
+ if (ports.verifyPreparedSend === undefined)
551
+ return { status: "uncertain", quarantine: "caller" };
552
+ try {
553
+ const result = await ports.verifyPreparedSend({
554
+ operationId: base.operationId,
555
+ requestDigest: base.requestDigest,
556
+ surface: base.surface,
557
+ actionId,
558
+ expected: cloneExpectedEnvelope(expected),
559
+ prepared,
560
+ activation,
561
+ mutationMayHaveOccurred: true,
562
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
563
+ ...(options.deadlineAt === undefined ? {} : { deadlineAt: options.deadlineAt })
564
+ });
565
+ validateFinalResult(result, expected, "mutate_once");
566
+ return result;
567
+ }
568
+ catch {
569
+ return { status: "uncertain", quarantine: "caller" };
570
+ }
571
+ }
572
+ function validateOwnershipBaselineForSend(baseline, base, expected, actionId) {
573
+ const wrapper = {
574
+ schemaVersion: "chatgpt.browser_control.operation_ownership_baseline.v1",
575
+ operationId: base.operationId,
576
+ requestDigest: base.requestDigest,
577
+ targetBindingDigest: expected.targetBindingDigest,
578
+ actionId,
579
+ baseline,
580
+ observedAt: new Date(0).toISOString()
581
+ };
582
+ // Use the shared closed validator for the nested baseline. The timestamp is
583
+ // only a shape-validation placeholder; the service supplies the immutable
584
+ // durable timestamp when it creates the action_prepared event.
585
+ assertOwnershipBaselineShape(wrapper);
586
+ }
587
+ async function finishObserved(base, expected, actionId, final, ports, requiresTargetEstablishment) {
588
+ if (requiresTargetEstablishment) {
589
+ const establishment = final.targetEstablishment;
590
+ if (establishment === undefined || ports.establishTarget === undefined) {
591
+ return uncertainBase(base, "ambiguous_submit", "send_may_have_occurred", final.evidenceDigest);
592
+ }
593
+ try {
594
+ validateSubmissionTargetEstablishment(establishment, expected.targetBindingDigest, actionId);
595
+ if (establishment.postSendDeltaDigest === undefined) {
596
+ return uncertainBase(base, "ambiguous_submit", "send_may_have_occurred", final.evidenceDigest);
597
+ }
598
+ if (establishment.userTurnId !== final.userTurnId || establishment.userTurnEvidenceDigest !== final.userTurnEvidenceDigest) {
599
+ return uncertainBase(base, "ambiguous_submit", "send_may_have_occurred", final.evidenceDigest);
600
+ }
601
+ }
602
+ catch {
603
+ return uncertainBase(base, "ambiguous_submit", "send_may_have_occurred", final.evidenceDigest);
604
+ }
605
+ try {
606
+ const persisted = await ports.establishTarget({
607
+ operationId: base.operationId,
608
+ requestDigest: base.requestDigest,
609
+ targetBindingDigest: establishment.targetBindingDigest,
610
+ anchorDigest: establishment.anchorDigest,
611
+ causalSendActionId: establishment.causalSendActionId,
612
+ conversationId: establishment.conversationId,
613
+ canonicalThreadUrl: establishment.canonicalThreadUrl,
614
+ userTurnId: establishment.userTurnId,
615
+ userTurnEvidenceDigest: establishment.userTurnEvidenceDigest,
616
+ postSendDeltaDigest: establishment.postSendDeltaDigest,
617
+ evidenceDigest: establishment.evidenceDigest,
618
+ ...(establishment.observedAt === undefined ? {} : { observedAt: establishment.observedAt })
619
+ });
620
+ validateDurableTargetEstablishmentResult(persisted, expected.targetBindingDigest, establishment);
621
+ }
622
+ catch {
623
+ return uncertainBase(base, "journal_unavailable", "send_may_have_occurred", final.evidenceDigest);
624
+ }
625
+ }
626
+ if (final.postSendDeltaDigest === undefined) {
627
+ return uncertainBase(base, "ambiguous_submit", "send_may_have_occurred", final.evidenceDigest);
628
+ }
629
+ const receipt = {
630
+ kind: "receipt",
631
+ operationId: base.operationId,
632
+ requestDigest: base.requestDigest,
633
+ surface: base.surface,
634
+ phase: "submitted",
635
+ mutationBoundary: "send_may_have_occurred",
636
+ targetBindingDigest: expected.targetBindingDigest,
637
+ evidenceDigest: final.evidenceDigest,
638
+ userTurnId: final.userTurnId,
639
+ userTurnEvidenceDigest: final.userTurnEvidenceDigest,
640
+ postSendDeltaDigest: final.postSendDeltaDigest,
641
+ ...(final.targetEstablishment?.observedAt === undefined ? {} : { observedAt: final.targetEstablishment.observedAt }),
642
+ ...(final.assistantTurnId === undefined ? {} : { assistantTurnId: final.assistantTurnId })
643
+ };
644
+ try {
645
+ await ports.persistReceiptEvidence(receipt);
646
+ }
647
+ catch {
648
+ return uncertainBase(base, "journal_unavailable", "send_may_have_occurred", final.evidenceDigest);
649
+ }
650
+ const resultBase = { ...base };
651
+ if (final.status === "already_submitted") {
652
+ return {
653
+ ...resultBase,
654
+ kind: "already_submitted",
655
+ actionId,
656
+ evidenceDigest: final.evidenceDigest,
657
+ userTurnId: final.userTurnId,
658
+ userTurnEvidenceDigest: final.userTurnEvidenceDigest,
659
+ ...(final.assistantTurnId === undefined ? {} : { assistantTurnId: final.assistantTurnId })
660
+ };
661
+ }
662
+ return {
663
+ ...resultBase,
664
+ kind: "submitted",
665
+ actionId,
666
+ evidenceDigest: final.evidenceDigest,
667
+ userTurnId: final.userTurnId,
668
+ userTurnEvidenceDigest: final.userTurnEvidenceDigest,
669
+ ...(final.assistantTurnId === undefined ? {} : { assistantTurnId: final.assistantTurnId })
670
+ };
671
+ }
672
+ async function observeAttachmentsSafely(ports, request, manifest) {
673
+ try {
674
+ const result = await ports.observeAttachments({ ...request, manifest: cloneManifest(request.manifest) });
675
+ validateAttachmentObservation(result, manifest);
676
+ return result;
677
+ }
678
+ catch {
679
+ return { status: "ambiguous" };
680
+ }
681
+ }
682
+ async function observeAttachmentsAfterHandoff(ports, request, manifest, options) {
683
+ let observed = { status: "ambiguous" };
684
+ for (let attempt = 0; attempt < POST_HANDOFF_OBSERVATION_ATTEMPTS; attempt += 1) {
685
+ observed = await observeAttachmentsSafely(ports, request, manifest);
686
+ if (observed.status === "exact" || observed.status === "mismatch")
687
+ return observed;
688
+ if (attempt + 1 >= POST_HANDOFF_OBSERVATION_ATTEMPTS || cancellationCode(options) !== undefined) {
689
+ return observed;
690
+ }
691
+ const budget = options.deadlineAt === undefined
692
+ ? POST_HANDOFF_OBSERVATION_INTERVAL_MS
693
+ : Math.max(0, options.deadlineAt - Date.now());
694
+ if (budget <= 0)
695
+ return observed;
696
+ await waitForPostHandoffObservation(Math.min(POST_HANDOFF_OBSERVATION_INTERVAL_MS, budget), options.signal);
697
+ }
698
+ return observed;
699
+ }
700
+ async function waitForPostHandoffObservation(milliseconds, signal) {
701
+ if (milliseconds <= 0 || signal?.aborted)
702
+ return;
703
+ await new Promise(resolve => {
704
+ let settled = false;
705
+ const finish = () => {
706
+ if (settled)
707
+ return;
708
+ settled = true;
709
+ clearTimeout(timer);
710
+ signal?.removeEventListener("abort", finish);
711
+ resolve();
712
+ };
713
+ const timer = setTimeout(finish, milliseconds);
714
+ signal?.addEventListener("abort", finish, { once: true });
715
+ if (signal?.aborted)
716
+ finish();
717
+ });
718
+ }
719
+ function validateInput(operation, expected, ports, options) {
720
+ // Reject accessor-backed plain data before any validation property access.
721
+ // This matters both for privacy (no hostile getter can expose a secret) and
722
+ // for fail-closed semantics (a getter must not influence the blocker
723
+ // identity that is returned to the caller).
724
+ if (hasAccessorInPlainData(operation)) {
725
+ throw new SubmissionInputError("operation_state_corrupt", "Operation snapshot contains accessor-backed identity data.");
726
+ }
727
+ if (hasAccessorInPlainData(expected) || hasAccessorInPlainData(options)) {
728
+ throw new SubmissionInputError("port_protocol_violation", "Submission input contains accessor-backed boundary data.");
729
+ }
730
+ if (!options || typeof options !== "object" || Array.isArray(options)) {
731
+ throw new SubmissionInputError("port_protocol_violation", "Submission options are invalid.");
732
+ }
733
+ assertExactRecord(options, ["signal", "deadlineAt"], []);
734
+ if (!operation || typeof operation !== "object" || !operation.state || !operation.handle || !operation.actionIds) {
735
+ throw new SubmissionInputError("port_protocol_violation", "Submission operation snapshot is invalid.");
736
+ }
737
+ assertExactRecord(operation, ["state", "handle", "actionIds"], ["state", "handle", "actionIds"]);
738
+ const envelope = sanitizeExpectedEnvelope(expected);
739
+ const state = operation.state;
740
+ const handle = operation.handle;
741
+ assertExactRecord(state, ["schemaVersion", "operationId", "requestDigest", "surface", "phase", "mutationBoundary", "revision", "createdAt", "updatedAt", "capturePolicy", "responseFormat", "target", "actions", "ownershipBaseline", "ownershipBaselines", "artifactTransfers", "submissionWitnesses", "lastBlocker", "receipt", "submissionWitness"], ["schemaVersion", "operationId", "requestDigest", "surface", "phase", "mutationBoundary", "revision", "createdAt", "updatedAt", "actions"]);
742
+ assertExactRecord(handle, ["schemaVersion", "operationId", "requestDigest", "surface", "revision", "phase", "mutationBoundary", "targetBindingDigest"], ["schemaVersion", "operationId", "requestDigest", "surface", "revision", "phase", "mutationBoundary"]);
743
+ assertExactRecord(operation.actionIds, ["sendActionId", "fileHandoffActionId"], ["sendActionId"]);
744
+ if (state.schemaVersion !== OPERATION_SCHEMA_VERSION || handle.schemaVersion !== OPERATION_HANDLE_SCHEMA_VERSION) {
745
+ throw new SubmissionInputError("operation_state_corrupt", "Operation schema versions are invalid.");
746
+ }
747
+ if (!isUuid(state.operationId) || !isUuid(handle.operationId) || state.operationId !== handle.operationId) {
748
+ throw new SubmissionInputError("operation_state_corrupt", "Operation identity is invalid.");
749
+ }
750
+ if (!isDigest(state.requestDigest) || state.requestDigest !== handle.requestDigest) {
751
+ throw new SubmissionInputError("operation_state_corrupt", "Operation request identity is invalid.");
752
+ }
753
+ if (state.surface !== "chat" && state.surface !== "work")
754
+ throw new SubmissionInputError("operation_state_corrupt", "Operation surface is invalid.");
755
+ if (state.responseFormat !== undefined && state.responseFormat !== "markdown" && state.responseFormat !== "text")
756
+ throw new SubmissionInputError("operation_state_corrupt", "Operation response format is invalid.");
757
+ if (state.capturePolicy !== undefined) {
758
+ try {
759
+ assertDurableCapturePolicyShape(state.capturePolicy);
760
+ }
761
+ catch {
762
+ throw new SubmissionInputError("operation_state_corrupt", "Operation durable capture policy is invalid.");
763
+ }
764
+ if (state.responseFormat !== undefined && state.responseFormat !== state.capturePolicy.responseFormat) {
765
+ throw new SubmissionInputError("operation_state_corrupt", "Operation capture policy format is inconsistent.");
766
+ }
767
+ }
768
+ if (!PHASES.has(state.phase) || !BOUNDARIES.has(state.mutationBoundary) || !Number.isSafeInteger(state.revision) || state.revision < 1) {
769
+ throw new SubmissionInputError("operation_state_corrupt", "Operation progress fields are invalid.");
770
+ }
771
+ if (handle.surface !== state.surface || handle.revision !== state.revision || handle.phase !== state.phase || handle.mutationBoundary !== state.mutationBoundary) {
772
+ throw new SubmissionInputError("stale_handle", "Operation handle is stale.");
773
+ }
774
+ if (envelope.surface !== state.surface || handle.targetBindingDigest !== envelope.targetBindingDigest) {
775
+ throw new SubmissionInputError("target_binding_mismatch", "Expected target binding does not match the operation.");
776
+ }
777
+ if (!isUuid(operation.actionIds.sendActionId))
778
+ throw new SubmissionInputError("operation_state_corrupt", "Send action identity is invalid.");
779
+ if (operation.actionIds.fileHandoffActionId !== undefined && !isUuid(operation.actionIds.fileHandoffActionId)) {
780
+ throw new SubmissionInputError("operation_state_corrupt", "File handoff action identity is invalid.");
781
+ }
782
+ if (operation.actionIds.fileHandoffActionId === operation.actionIds.sendActionId) {
783
+ throw new SubmissionInputError("operation_state_corrupt", "Non-repeatable action identities must be distinct.");
784
+ }
785
+ if (state.target === undefined || typeof state.target !== "object" || Array.isArray(state.target)) {
786
+ throw new SubmissionInputError("target_binding_mismatch", "Operation target binding is unavailable.");
787
+ }
788
+ validateTargetBinding(state.target);
789
+ if (state.ownershipBaseline !== undefined) {
790
+ try {
791
+ assertOwnershipBaselineShape(state.ownershipBaseline);
792
+ }
793
+ catch {
794
+ throw new SubmissionInputError("operation_state_corrupt", "Durable ownership baseline is invalid.");
795
+ }
796
+ if (state.ownershipBaseline.operationId !== state.operationId
797
+ || state.ownershipBaseline.requestDigest !== state.requestDigest
798
+ || state.ownershipBaseline.targetBindingDigest !== envelope.targetBindingDigest) {
799
+ throw new SubmissionInputError("operation_state_corrupt", "Durable ownership baseline identity is invalid.");
800
+ }
801
+ }
802
+ if (state.ownershipBaselines !== undefined) {
803
+ if (!state.ownershipBaselines || typeof state.ownershipBaselines !== "object" || Array.isArray(state.ownershipBaselines)) {
804
+ throw new SubmissionInputError("operation_state_corrupt", "Durable ownership baseline map is invalid.");
805
+ }
806
+ for (const [actionId, baseline] of Object.entries(state.ownershipBaselines)) {
807
+ if (!isUuid(actionId))
808
+ throw new SubmissionInputError("operation_state_corrupt", "Durable ownership baseline map key is invalid.");
809
+ try {
810
+ assertOwnershipBaselineShape(baseline);
811
+ }
812
+ catch {
813
+ throw new SubmissionInputError("operation_state_corrupt", "Durable ownership baseline map entry is invalid.");
814
+ }
815
+ if (baseline.operationId !== state.operationId
816
+ || baseline.requestDigest !== state.requestDigest
817
+ || baseline.actionId !== actionId
818
+ || baseline.targetBindingDigest !== envelope.targetBindingDigest) {
819
+ throw new SubmissionInputError("operation_state_corrupt", "Durable ownership baseline map identity is invalid.");
820
+ }
821
+ }
822
+ }
823
+ if (!state.actions || typeof state.actions !== "object" || Array.isArray(state.actions)) {
824
+ throw new SubmissionInputError("operation_state_corrupt", "Operation actions are invalid.");
825
+ }
826
+ for (const [actionId, action] of Object.entries(state.actions)) {
827
+ if (!isUuid(actionId) || !action || typeof action !== "object" || Array.isArray(action)) {
828
+ throw new SubmissionInputError("operation_state_corrupt", "Operation action identity is invalid.");
829
+ }
830
+ assertExactRecord(action, ["actionId", "kind", "repeatPolicy", "requestDigest", "parentActionId", "targetDigest", "intentRevision", "intentAt", "outcome", "receiptRevision", "receiptAt", "evidenceDigest", "blockerCode"], ["actionId", "kind", "repeatPolicy", "requestDigest", "intentRevision", "intentAt"]);
831
+ if (action.actionId !== actionId || !isUuid(action.actionId) || !isDigest(action.requestDigest)) {
832
+ throw new SubmissionInputError("operation_state_corrupt", "Operation action identity is invalid.");
833
+ }
834
+ validateActionRecord(action);
835
+ }
836
+ const recordedSend = findUniqueAction(state, "send");
837
+ const recordedHandoff = findUniqueAction(state, "file_handoff");
838
+ if (recordedSend === "corrupt" || recordedHandoff === "corrupt") {
839
+ throw new SubmissionInputError("operation_state_corrupt", "Operation contains duplicate non-repeatable actions.");
840
+ }
841
+ if (recordedSend !== undefined && operation.actionIds.sendActionId !== recordedSend.actionId) {
842
+ throw new SubmissionInputError("operation_state_corrupt", "Provided Send action identity does not match durable state.");
843
+ }
844
+ if (recordedHandoff !== undefined && operation.actionIds.fileHandoffActionId !== recordedHandoff.actionId) {
845
+ throw new SubmissionInputError("operation_state_corrupt", "Provided file handoff action identity does not match durable state.");
846
+ }
847
+ if (recordedHandoff === undefined && operation.actionIds.fileHandoffActionId !== undefined && envelope.attachmentManifest.count === 0) {
848
+ throw new SubmissionInputError("operation_state_corrupt", "Unused file handoff action identity is not allowed for an empty manifest.");
849
+ }
850
+ if (envelope.attachmentManifest.count > 0 && operation.actionIds.fileHandoffActionId === undefined) {
851
+ throw new SubmissionInputError("operation_state_corrupt", "File handoff action identity is required for an attachment manifest.");
852
+ }
853
+ const sendBaseline = state.ownershipBaselines?.[recordedSend?.actionId ?? ""] ?? state.ownershipBaseline;
854
+ if (state.ownershipBaseline !== undefined && (recordedSend === undefined
855
+ || state.ownershipBaseline.actionId !== recordedSend.actionId
856
+ || state.ownershipBaseline.targetBindingDigest !== recordedSend.targetDigest)) {
857
+ throw new SubmissionInputError("operation_state_corrupt", "Durable ownership baseline does not name the original Send action.");
858
+ }
859
+ if (state.submissionWitness !== undefined) {
860
+ validateDurableSubmissionWitness(state.submissionWitness);
861
+ if (recordedSend === undefined
862
+ || state.submissionWitness.actionId !== recordedSend.actionId
863
+ || state.submissionWitness.actionKind !== recordedSend.kind
864
+ || state.submissionWitness.targetBindingDigest !== recordedSend.targetDigest
865
+ || sendBaseline === undefined
866
+ || state.submissionWitness.baselineSnapshotDigest !== sendBaseline.baseline.snapshotDigest) {
867
+ throw new SubmissionInputError("operation_state_corrupt", "Durable submission witness is not bound to the original Send action and pre-Send baseline.");
868
+ }
869
+ }
870
+ if (state.receipt !== undefined)
871
+ validateDurableReceipt(state.receipt);
872
+ if (options.signal !== undefined && !isAbortSignalLike(options.signal)) {
873
+ throw new SubmissionInputError("port_protocol_violation", "Cancellation signal is invalid.");
874
+ }
875
+ if (options.deadlineAt !== undefined &&
876
+ (!Number.isFinite(options.deadlineAt) || !Number.isSafeInteger(options.deadlineAt) || options.deadlineAt < 0 || options.deadlineAt > MAX_DEADLINE_AT)) {
877
+ throw new SubmissionInputError("port_protocol_violation", "Deadline is invalid.");
878
+ }
879
+ if (!ports || typeof ports !== "object" || Array.isArray(ports) || typeof ports.observeStaging !== "function" || typeof ports.persistActionIntent !== "function" || typeof ports.executeFileHandoffOnce !== "function" || typeof ports.observeAttachments !== "function" || typeof ports.persistReceiptEvidence !== "function") {
880
+ throw new SubmissionInputError("port_protocol_violation", "Submission ports are incomplete.");
881
+ }
882
+ return envelope;
883
+ }
884
+ function validateActionRecord(action) {
885
+ const policies = {
886
+ status_read: "read_only",
887
+ configuration_set: "reconcile_set_to_value",
888
+ tool_set: "reconcile_set_to_value",
889
+ composer_set: "reconcile_set_to_value",
890
+ power_discovery: "read_only",
891
+ power_select: "reconcile_set_to_value",
892
+ file_handoff: "observe_only_after_intent",
893
+ send: "observe_only_after_intent",
894
+ work_steer: "observe_only_after_intent",
895
+ stop: "observe_only_after_intent",
896
+ download: "reconcile_local_effect",
897
+ local_output_commit: "reconcile_local_effect",
898
+ clipboard_capture_restore: "reconcile_local_effect"
899
+ };
900
+ if (typeof action.kind !== "string" || policies[action.kind] === undefined || action.repeatPolicy !== policies[action.kind]) {
901
+ throw new SubmissionInputError("operation_state_corrupt", "Operation action kind or repeat policy is invalid.");
902
+ }
903
+ if (!Number.isSafeInteger(action.intentRevision) || action.intentRevision < 1 || !isIsoInstant(action.intentAt)) {
904
+ throw new SubmissionInputError("operation_state_corrupt", "Operation action intent evidence is invalid.");
905
+ }
906
+ if (action.parentActionId !== undefined && !isUuid(action.parentActionId)) {
907
+ throw new SubmissionInputError("operation_state_corrupt", "Operation parent action identity is invalid.");
908
+ }
909
+ if (action.targetDigest !== undefined && !isDigest(action.targetDigest)) {
910
+ throw new SubmissionInputError("operation_state_corrupt", "Operation action target evidence is invalid.");
911
+ }
912
+ if ((action.kind === "file_handoff" || action.kind === "send") && action.targetDigest === undefined) {
913
+ throw new SubmissionInputError("operation_state_corrupt", "Non-repeatable action target evidence is missing.");
914
+ }
915
+ if (action.outcome !== undefined && action.outcome !== "satisfied" && action.outcome !== "not_satisfied" && action.outcome !== "uncertain") {
916
+ throw new SubmissionInputError("operation_state_corrupt", "Operation action outcome is invalid.");
917
+ }
918
+ if (action.receiptRevision !== undefined && (!Number.isSafeInteger(action.receiptRevision) || action.receiptRevision < 1)) {
919
+ throw new SubmissionInputError("operation_state_corrupt", "Operation action receipt revision is invalid.");
920
+ }
921
+ if (action.receiptAt !== undefined && !isIsoInstant(action.receiptAt)) {
922
+ throw new SubmissionInputError("operation_state_corrupt", "Operation action receipt time is invalid.");
923
+ }
924
+ if (action.receiptAt !== undefined && Date.parse(action.receiptAt) < Date.parse(action.intentAt)) {
925
+ throw new SubmissionInputError("operation_state_corrupt", "Operation action receipt precedes its intent.");
926
+ }
927
+ if (action.evidenceDigest !== undefined && !isDigest(action.evidenceDigest)) {
928
+ throw new SubmissionInputError("operation_state_corrupt", "Operation action evidence is invalid.");
929
+ }
930
+ if (action.blockerCode !== undefined && (typeof action.blockerCode !== "string" || !CODE_PATTERN.test(action.blockerCode))) {
931
+ throw new SubmissionInputError("operation_state_corrupt", "Operation action blocker is invalid.");
932
+ }
933
+ const hasReceiptEvidence = action.receiptRevision !== undefined || action.receiptAt !== undefined || action.evidenceDigest !== undefined || action.blockerCode !== undefined;
934
+ if (action.outcome === undefined && hasReceiptEvidence) {
935
+ throw new SubmissionInputError("operation_state_corrupt", "Unreceipted operation action contains receipt evidence.");
936
+ }
937
+ if (action.outcome !== undefined && (action.receiptRevision === undefined || action.receiptAt === undefined)) {
938
+ throw new SubmissionInputError("operation_state_corrupt", "Receipted operation action is missing receipt evidence.");
939
+ }
940
+ if (action.outcome === "satisfied" && action.evidenceDigest === undefined) {
941
+ throw new SubmissionInputError("operation_state_corrupt", "Satisfied operation action is missing evidence.");
942
+ }
943
+ }
944
+ function validateManifest(manifest) {
945
+ if (!manifest || manifest.orderPolicy !== "exact" || !Number.isSafeInteger(manifest.count) || manifest.count < 0 || manifest.count > MAX_ATTACHMENTS || !Array.isArray(manifest.identities) || manifest.identities.length !== manifest.count) {
946
+ throw new SubmissionInputError("attachment_manifest_mismatch", "Attachment manifest is invalid.");
947
+ }
948
+ manifest.identities.forEach((entry, index) => {
949
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
950
+ throw new SubmissionInputError("attachment_manifest_mismatch", "Attachment manifest identity is invalid.");
951
+ }
952
+ assertExactRecord(entry, ["identityDigest", "ordinal"], ["identityDigest", "ordinal"]);
953
+ if (entry.ordinal !== index || !isDigest(entry.identityDigest)) {
954
+ throw new SubmissionInputError("attachment_manifest_mismatch", "Attachment manifest order is invalid.");
955
+ }
956
+ });
957
+ const seen = new Set();
958
+ for (const entry of manifest.identities) {
959
+ if (seen.has(entry.identityDigest)) {
960
+ throw new SubmissionInputError("attachment_manifest_mismatch", "Attachment manifest contains duplicate identities.");
961
+ }
962
+ seen.add(entry.identityDigest);
963
+ }
964
+ }
965
+ function sanitizeExpectedEnvelope(value) {
966
+ assertExactRecord(value, ["surface", "targetBindingDigest", "configurationReceiptDigest", "composerReceiptDigest", "attachmentManifest"], ["surface", "targetBindingDigest", "configurationReceiptDigest", "composerReceiptDigest", "attachmentManifest"]);
967
+ if (value.surface !== "chat" && value.surface !== "work") {
968
+ throw new SubmissionInputError("port_protocol_violation", "Expected surface is invalid.");
969
+ }
970
+ if (!isDigest(value.targetBindingDigest) || !isDigest(value.configurationReceiptDigest) || !isDigest(value.composerReceiptDigest)) {
971
+ throw new SubmissionInputError("port_protocol_violation", "Expected receipt digests are invalid.");
972
+ }
973
+ const manifest = value.attachmentManifest;
974
+ if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
975
+ throw new SubmissionInputError("attachment_manifest_mismatch", "Attachment manifest is invalid.");
976
+ }
977
+ assertExactRecord(manifest, ["count", "orderPolicy", "identities"], ["count", "orderPolicy", "identities"]);
978
+ validateManifest(manifest);
979
+ return {
980
+ surface: value.surface,
981
+ targetBindingDigest: value.targetBindingDigest,
982
+ configurationReceiptDigest: value.configurationReceiptDigest,
983
+ composerReceiptDigest: value.composerReceiptDigest,
984
+ attachmentManifest: {
985
+ count: manifest.count,
986
+ orderPolicy: "exact",
987
+ identities: manifest.identities.map(entry => ({ identityDigest: entry.identityDigest, ordinal: entry.ordinal }))
988
+ }
989
+ };
990
+ }
991
+ function validateTargetBinding(value) {
992
+ assertExactRecord(value, ["providerId", "browserId", "tabId", "coordinationScope", "tabClaimEvidenceDigest", "canonicalThreadUrl", "conversationId", "userTurnBaselineDigest", "assistantTurnBaselineDigest", "configurationReceiptDigest", "evidenceProfile", "targetLifecycle", "newTargetAnchorDigest", "blankTaskEvidenceDigest", "targetEstablishment"], ["providerId", "browserId", "tabId", "coordinationScope", "evidenceProfile"]);
993
+ if (!isSafeId(value.providerId) || !isSafeId(value.browserId) || !isSafeId(value.tabId) || (value.coordinationScope !== "process" && value.coordinationScope !== "provider")) {
994
+ throw new SubmissionInputError("operation_state_corrupt", "Operation target binding identity is invalid.");
995
+ }
996
+ if (value.conversationId !== undefined && !isSafeId(value.conversationId)) {
997
+ throw new SubmissionInputError("operation_state_corrupt", "Operation conversation identity is invalid.");
998
+ }
999
+ if (value.canonicalThreadUrl !== undefined && !isCanonicalThreadUrl(value.canonicalThreadUrl)) {
1000
+ throw new SubmissionInputError("operation_state_corrupt", "Operation canonical thread URL is invalid.");
1001
+ }
1002
+ for (const digest of [value.tabClaimEvidenceDigest, value.userTurnBaselineDigest, value.assistantTurnBaselineDigest, value.configurationReceiptDigest]) {
1003
+ if (digest !== undefined && !isDigest(digest))
1004
+ throw new SubmissionInputError("operation_state_corrupt", "Operation target evidence digest is invalid.");
1005
+ }
1006
+ const profile = value.evidenceProfile;
1007
+ if (!profile || typeof profile !== "object" || Array.isArray(profile))
1008
+ throw new SubmissionInputError("operation_state_corrupt", "Operation target evidence profile is invalid.");
1009
+ assertExactRecord(profile, ["providerIdentity", "stableTabId", "stableConversationId", "stableUserTurnId", "authoritativeTabClaim", "replacementTabRecovery"], ["providerIdentity", "stableTabId", "stableConversationId", "stableUserTurnId", "authoritativeTabClaim", "replacementTabRecovery"]);
1010
+ if (typeof profile.replacementTabRecovery !== "boolean")
1011
+ throw new SubmissionInputError("operation_state_corrupt", "Operation target recovery profile is invalid.");
1012
+ for (const value of [profile.providerIdentity, profile.stableTabId, profile.stableConversationId, profile.stableUserTurnId, profile.authoritativeTabClaim]) {
1013
+ if (value !== "required" && value !== "unavailable")
1014
+ throw new SubmissionInputError("operation_state_corrupt", "Operation target evidence profile is invalid.");
1015
+ }
1016
+ if (profile.stableConversationId === "required" && value.conversationId === undefined) {
1017
+ throw new SubmissionInputError("operation_state_corrupt", "Required stable conversation evidence is absent.");
1018
+ }
1019
+ if (value.coordinationScope === "provider" &&
1020
+ (profile.authoritativeTabClaim !== "required" || value.tabClaimEvidenceDigest === undefined)) {
1021
+ throw new SubmissionInputError("operation_state_corrupt", "Provider-scoped coordination requires authoritative tab-claim evidence.");
1022
+ }
1023
+ const lifecycle = value.targetLifecycle ?? "fixed";
1024
+ if (lifecycle !== "fixed" && lifecycle !== "new_pending" && lifecycle !== "new_established") {
1025
+ throw new SubmissionInputError("operation_state_corrupt", "Operation target lifecycle is invalid.");
1026
+ }
1027
+ if (value.newTargetAnchorDigest !== undefined && !isDigest(value.newTargetAnchorDigest)) {
1028
+ throw new SubmissionInputError("operation_state_corrupt", "Operation new-target anchor digest is invalid.");
1029
+ }
1030
+ if (value.blankTaskEvidenceDigest !== undefined && !isDigest(value.blankTaskEvidenceDigest)) {
1031
+ throw new SubmissionInputError("operation_state_corrupt", "Operation blank-task evidence digest is invalid.");
1032
+ }
1033
+ if (lifecycle === "fixed" && (value.newTargetAnchorDigest !== undefined || value.blankTaskEvidenceDigest !== undefined || value.targetEstablishment !== undefined)) {
1034
+ throw new SubmissionInputError("operation_state_corrupt", "Fixed target contains new-target identity fields.");
1035
+ }
1036
+ if (lifecycle === "new_pending" && (value.newTargetAnchorDigest === undefined
1037
+ || value.blankTaskEvidenceDigest === undefined
1038
+ || value.conversationId !== undefined
1039
+ || value.canonicalThreadUrl !== undefined
1040
+ || value.targetEstablishment !== undefined
1041
+ || profile.stableConversationId !== "unavailable"
1042
+ || profile.stableUserTurnId !== "unavailable")) {
1043
+ throw new SubmissionInputError("operation_state_corrupt", "Pending new target identity is invalid.");
1044
+ }
1045
+ if (value.targetEstablishment !== undefined) {
1046
+ try {
1047
+ validateSubmissionTargetEstablishment(value.targetEstablishment, value.targetEstablishment.targetBindingDigest);
1048
+ }
1049
+ catch {
1050
+ throw new SubmissionInputError("operation_state_corrupt", "Target establishment evidence is invalid.");
1051
+ }
1052
+ }
1053
+ }
1054
+ function cloneOwnershipBaseline(value) {
1055
+ return {
1056
+ schemaVersion: value.schemaVersion,
1057
+ snapshotDigest: value.snapshotDigest,
1058
+ target: {
1059
+ provider: cloneOwnershipIdentity(value.target.provider),
1060
+ browser: cloneOwnershipIdentity(value.target.browser),
1061
+ tab: cloneOwnershipIdentity(value.target.tab),
1062
+ thread: cloneOwnershipIdentity(value.target.thread),
1063
+ conversation: cloneOwnershipIdentity(value.target.conversation),
1064
+ canonicalThreadUrl: cloneOwnershipIdentity(value.target.canonicalThreadUrl),
1065
+ authoritativeTabClaim: cloneOwnershipIdentity(value.target.authoritativeTabClaim),
1066
+ coordinationScope: value.target.coordinationScope
1067
+ },
1068
+ userTurns: value.userTurns.map(cloneOwnershipTurn),
1069
+ assistantTurns: value.assistantTurns.map(cloneOwnershipTurn),
1070
+ completeness: "complete"
1071
+ };
1072
+ }
1073
+ function cloneOwnershipIdentity(value) {
1074
+ if (typeof value !== "object" || value === null || !("status" in value))
1075
+ return value;
1076
+ return value.status === "available"
1077
+ ? { status: "available", value: value.value }
1078
+ : { status: "unavailable", reason: value.reason };
1079
+ }
1080
+ function cloneOwnershipTurn(value) {
1081
+ return {
1082
+ ...(value.stableId === undefined ? {} : { stableId: value.stableId }),
1083
+ evidenceDigest: value.evidenceDigest,
1084
+ structureDigest: value.structureDigest,
1085
+ ordinal: value.ordinal,
1086
+ ...(value.parentStableId === undefined ? {} : { parentStableId: value.parentStableId }),
1087
+ ...(value.branchStableId === undefined ? {} : { branchStableId: value.branchStableId }),
1088
+ ...(value.state === undefined ? {} : { state: value.state }),
1089
+ ...(value.artifactEvidenceDigests === undefined ? {} : { artifactEvidenceDigests: [...value.artifactEvidenceDigests] })
1090
+ };
1091
+ }
1092
+ function validateDurableSubmissionWitness(value) {
1093
+ assertExactRecord(value, ["schemaVersion", "actionId", "actionKind", "targetBindingDigest", "baselineSnapshotDigest", "postSendDeltaDigest", "operationUserEvidenceDigest", "userTurnId", "observedAt"], ["schemaVersion", "actionId", "actionKind", "targetBindingDigest", "baselineSnapshotDigest", "postSendDeltaDigest", "operationUserEvidenceDigest", "observedAt"]);
1094
+ if (value.schemaVersion !== OPERATION_SUBMISSION_WITNESS_SCHEMA_VERSION
1095
+ || !isUuid(value.actionId)
1096
+ || (value.actionKind !== "send" && value.actionKind !== "work_steer")
1097
+ || !isDigest(value.targetBindingDigest)
1098
+ || !isDigest(value.baselineSnapshotDigest)
1099
+ || !isDigest(value.postSendDeltaDigest)
1100
+ || !isDigest(value.operationUserEvidenceDigest)
1101
+ || (value.userTurnId !== undefined && !isSafeId(value.userTurnId))
1102
+ || !isIsoInstant(value.observedAt)) {
1103
+ throw new SubmissionInputError("operation_state_corrupt", "Durable submission witness is invalid.");
1104
+ }
1105
+ }
1106
+ function validateDurableReceipt(value) {
1107
+ assertExactRecord(value, ["schemaVersion", "operationId", "requestDigest", "targetBindingDigest", "userTurnId", "assistantTurnId", "ownershipEvidenceDigest", "userTurnEvidenceDigest", "responseDigest", "responseBytes", "responseFormat", "finishReason", "contentAvailable", "artifacts", "completedAt"], ["schemaVersion", "operationId", "requestDigest", "targetBindingDigest", "userTurnId", "assistantTurnId", "ownershipEvidenceDigest", "userTurnEvidenceDigest", "contentAvailable", "finishReason", "artifacts", "completedAt"]);
1108
+ if (value.schemaVersion !== OPERATION_RECEIPT_SCHEMA_VERSION ||
1109
+ !isUuid(value.operationId) ||
1110
+ !isDigest(value.requestDigest) ||
1111
+ !isDigest(value.targetBindingDigest) ||
1112
+ !isSafeId(value.userTurnId) ||
1113
+ !isSafeId(value.assistantTurnId) ||
1114
+ !isDigest(value.ownershipEvidenceDigest) ||
1115
+ !isDigest(value.userTurnEvidenceDigest) ||
1116
+ (value.responseFormat !== undefined && value.responseFormat !== "markdown" && value.responseFormat !== "text") ||
1117
+ typeof value.contentAvailable !== "boolean" ||
1118
+ typeof value.finishReason !== "string" ||
1119
+ !FINISH_REASON_PATTERN.test(value.finishReason) ||
1120
+ !isIsoInstant(value.completedAt) ||
1121
+ !Array.isArray(value.artifacts) ||
1122
+ value.artifacts.length > MAX_RECEIPT_ARTIFACTS) {
1123
+ throw new SubmissionInputError("operation_state_corrupt", "Durable operation receipt is invalid.");
1124
+ }
1125
+ const hasResponseDigest = value.responseDigest !== undefined;
1126
+ const hasResponseBytes = value.responseBytes !== undefined;
1127
+ const responseBytes = value.responseBytes;
1128
+ if (hasResponseDigest !== hasResponseBytes || value.contentAvailable !== hasResponseDigest) {
1129
+ throw new SubmissionInputError("operation_state_corrupt", "Durable response metadata must match content availability.");
1130
+ }
1131
+ if (hasResponseDigest && !isDigest(value.responseDigest))
1132
+ throw new SubmissionInputError("operation_state_corrupt", "Durable response digest is invalid.");
1133
+ if (hasResponseBytes && (typeof responseBytes !== "number" || !Number.isSafeInteger(responseBytes) || responseBytes < 0))
1134
+ throw new SubmissionInputError("operation_state_corrupt", "Durable response size is invalid.");
1135
+ const artifactKeys = new Set();
1136
+ const artifactOrdinals = new Set();
1137
+ for (const artifact of value.artifacts) {
1138
+ if (!artifact || typeof artifact !== "object" || Array.isArray(artifact))
1139
+ throw new SubmissionInputError("operation_state_corrupt", "Durable artifact receipt is invalid.");
1140
+ assertExactRecord(artifact, ["schemaVersion", "operationId", "artifactKey", "assistantTurnId", "sourceIdentityDigest", "kind", "ordinal", "outputKey", "mimeType", "bytes", "sha256", "status", "blockerCode"], ["schemaVersion", "operationId", "artifactKey", "assistantTurnId", "sourceIdentityDigest", "kind", "ordinal", "status"]);
1141
+ if (artifact.schemaVersion !== OPERATION_ARTIFACT_RECEIPT_SCHEMA_VERSION ||
1142
+ !isUuid(artifact.operationId) ||
1143
+ artifact.operationId !== value.operationId ||
1144
+ !isOpaqueKey(artifact.artifactKey) ||
1145
+ artifactKeys.has(artifact.artifactKey) ||
1146
+ !isSafeId(artifact.assistantTurnId) ||
1147
+ artifact.assistantTurnId !== value.assistantTurnId ||
1148
+ !isDigest(artifact.sourceIdentityDigest) ||
1149
+ !Number.isSafeInteger(artifact.ordinal) ||
1150
+ artifact.ordinal < 0 ||
1151
+ artifactOrdinals.has(artifact.ordinal) ||
1152
+ (artifact.kind !== "file" && artifact.kind !== "image" && artifact.kind !== "other") ||
1153
+ (artifact.status !== "available" && artifact.status !== "transferred" && artifact.status !== "partial" && artifact.status !== "blocked")) {
1154
+ throw new SubmissionInputError("operation_state_corrupt", "Durable artifact receipt is invalid.");
1155
+ }
1156
+ artifactKeys.add(artifact.artifactKey);
1157
+ artifactOrdinals.add(artifact.ordinal);
1158
+ if (artifact.outputKey !== undefined && (typeof artifact.outputKey !== "string" || !OUTPUT_KEY_PATTERN.test(artifact.outputKey)))
1159
+ throw new SubmissionInputError("operation_state_corrupt", "Durable artifact output key is invalid.");
1160
+ if (artifact.mimeType !== undefined && (typeof artifact.mimeType !== "string" || !MIME_PATTERN.test(artifact.mimeType)))
1161
+ throw new SubmissionInputError("operation_state_corrupt", "Durable artifact MIME type is invalid.");
1162
+ if (artifact.bytes !== undefined && (!Number.isSafeInteger(artifact.bytes) || artifact.bytes < 0))
1163
+ throw new SubmissionInputError("operation_state_corrupt", "Durable artifact size is invalid.");
1164
+ if (artifact.sha256 !== undefined && (typeof artifact.sha256 !== "string" || !SHA256_PATTERN.test(artifact.sha256)))
1165
+ throw new SubmissionInputError("operation_state_corrupt", "Durable artifact digest is invalid.");
1166
+ if (artifact.blockerCode !== undefined && (typeof artifact.blockerCode !== "string" || !CODE_PATTERN.test(artifact.blockerCode)))
1167
+ throw new SubmissionInputError("operation_state_corrupt", "Durable artifact blocker is invalid.");
1168
+ if (artifact.status === "transferred" && (artifact.outputKey === undefined || artifact.bytes === undefined || artifact.sha256 === undefined || artifact.blockerCode !== undefined)) {
1169
+ throw new SubmissionInputError("operation_state_corrupt", "Transferred artifact receipt is incomplete or contradictory.");
1170
+ }
1171
+ if ((artifact.status === "partial" || artifact.status === "blocked") && artifact.blockerCode === undefined) {
1172
+ throw new SubmissionInputError("operation_state_corrupt", "Partial or blocked artifact receipt requires a blocker.");
1173
+ }
1174
+ if (artifact.status === "available" && artifact.blockerCode !== undefined) {
1175
+ throw new SubmissionInputError("operation_state_corrupt", "Available artifact receipt cannot carry a blocker.");
1176
+ }
1177
+ }
1178
+ }
1179
+ function cloneExpectedEnvelope(value) {
1180
+ return {
1181
+ surface: value.surface,
1182
+ targetBindingDigest: value.targetBindingDigest,
1183
+ configurationReceiptDigest: value.configurationReceiptDigest,
1184
+ composerReceiptDigest: value.composerReceiptDigest,
1185
+ attachmentManifest: cloneManifest(value.attachmentManifest)
1186
+ };
1187
+ }
1188
+ function cloneManifest(value) {
1189
+ return {
1190
+ count: value.count,
1191
+ orderPolicy: "exact",
1192
+ identities: value.identities.map(entry => ({ identityDigest: entry.identityDigest, ordinal: entry.ordinal }))
1193
+ };
1194
+ }
1195
+ function assertExactRecord(value, allowed, required) {
1196
+ if (!isPlainRecord(value))
1197
+ throw new SubmissionInputError("port_protocol_violation", "Boundary value must be a record.");
1198
+ const record = value;
1199
+ const allowedSet = new Set(allowed);
1200
+ const requiredSet = new Set(required);
1201
+ for (const key of Object.keys(record)) {
1202
+ if (!allowedSet.has(key))
1203
+ throw new SubmissionInputError("port_protocol_violation", "Boundary value contains an unsupported field.");
1204
+ }
1205
+ for (const key of requiredSet) {
1206
+ if (!Object.prototype.hasOwnProperty.call(record, key))
1207
+ throw new SubmissionInputError("port_protocol_violation", "Boundary value is missing a required field.");
1208
+ }
1209
+ }
1210
+ function validateStageObservation(value) {
1211
+ if (!value || typeof value !== "object" || typeof value.status !== "string")
1212
+ throw new Error("invalid stage observation");
1213
+ if (value.status === "exact") {
1214
+ assertExactRecord(value, ["status", "evidenceDigest"], ["status", "evidenceDigest"]);
1215
+ assertDigest(value.evidenceDigest);
1216
+ }
1217
+ else if (value.status === "mismatch" || value.status === "unavailable") {
1218
+ assertExactRecord(value, ["status", "reason", "evidenceDigest"], ["status", "reason"]);
1219
+ if (!["target", "configuration", "composer", "unknown"].includes(value.reason))
1220
+ throw new Error("invalid stage reason");
1221
+ if (value.evidenceDigest !== undefined)
1222
+ assertDigest(value.evidenceDigest);
1223
+ }
1224
+ else {
1225
+ throw new Error("invalid stage status");
1226
+ }
1227
+ }
1228
+ function validateAttachmentObservation(value, manifest) {
1229
+ if (!value || typeof value !== "object" || typeof value.status !== "string")
1230
+ throw new Error("invalid attachment observation");
1231
+ if (value.status === "absent") {
1232
+ assertExactRecord(value, ["status", "evidenceDigest", "count", "orderPolicy", "identityDigests"], ["status", "evidenceDigest", "count", "orderPolicy", "identityDigests"]);
1233
+ assertDigest(value.evidenceDigest);
1234
+ if (manifest.count === 0 || value.count !== 0 || value.orderPolicy !== "exact" || !Array.isArray(value.identityDigests) || value.identityDigests.length !== 0)
1235
+ throw new Error("invalid absent attachment postcondition");
1236
+ }
1237
+ else if (value.status === "exact") {
1238
+ assertExactRecord(value, ["status", "evidenceDigest", "count", "orderPolicy", "identityDigests"], ["status", "evidenceDigest", "count", "orderPolicy", "identityDigests"]);
1239
+ assertDigest(value.evidenceDigest);
1240
+ if (value.count !== manifest.count || value.orderPolicy !== "exact" || !Array.isArray(value.identityDigests) || value.identityDigests.length !== manifest.count)
1241
+ throw new Error("attachment postcondition mismatch");
1242
+ value.identityDigests.forEach((identity, index) => {
1243
+ if (!isDigest(identity) || identity !== manifest.identities[index]?.identityDigest)
1244
+ throw new Error("attachment identity mismatch");
1245
+ });
1246
+ }
1247
+ else if (!["mismatch", "delayed", "ambiguous", "unavailable"].includes(value.status)) {
1248
+ throw new Error("invalid attachment status");
1249
+ }
1250
+ else if (value.evidenceDigest !== undefined) {
1251
+ assertExactRecord(value, ["status", "evidenceDigest"], ["status"]);
1252
+ assertDigest(value.evidenceDigest);
1253
+ }
1254
+ else {
1255
+ assertExactRecord(value, ["status", "evidenceDigest"], ["status"]);
1256
+ }
1257
+ }
1258
+ function validateHandoffResult(value) {
1259
+ if (!value || typeof value !== "object" || typeof value.status !== "string")
1260
+ throw new Error("invalid handoff result");
1261
+ if (value.status === "satisfied") {
1262
+ assertExactRecord(value, ["status", "evidenceDigest"], ["status", "evidenceDigest"]);
1263
+ assertDigest(value.evidenceDigest);
1264
+ }
1265
+ else if (value.status === "not_satisfied") {
1266
+ assertExactRecord(value, ["status", "blockerCode", "evidenceDigest"], ["status"]);
1267
+ if (value.blockerCode !== undefined && !isBlockerCode(value.blockerCode))
1268
+ throw new Error("invalid handoff blocker");
1269
+ if (value.evidenceDigest !== undefined)
1270
+ assertDigest(value.evidenceDigest);
1271
+ }
1272
+ else if (value.status === "uncertain") {
1273
+ assertExactRecord(value, ["status", "evidenceDigest", "quarantine"], ["status", "quarantine"]);
1274
+ if (value.evidenceDigest !== undefined)
1275
+ assertDigest(value.evidenceDigest);
1276
+ if (value.quarantine !== "provider" && value.quarantine !== "caller")
1277
+ throw new Error("invalid handoff quarantine");
1278
+ }
1279
+ else
1280
+ throw new Error("invalid handoff status");
1281
+ }
1282
+ function validateFinalResult(value, expected, mode) {
1283
+ if (!value || typeof value !== "object" || typeof value.status !== "string")
1284
+ throw new Error("invalid final result");
1285
+ if (value.status === "submitted" || value.status === "already_submitted") {
1286
+ assertExactRecord(value, ["status", "targetBindingDigest", "evidenceDigest", "userTurnId", "userTurnEvidenceDigest", "postSendDeltaDigest", "assistantTurnId", "targetEstablishment"], ["status", "targetBindingDigest", "evidenceDigest", "userTurnId", "userTurnEvidenceDigest", "postSendDeltaDigest"]);
1287
+ if (value.targetBindingDigest !== expected.targetBindingDigest || !isDigest(value.targetBindingDigest) || !isDigest(value.evidenceDigest) || !isDigest(value.userTurnEvidenceDigest) || !isDigest(value.postSendDeltaDigest) || !isSafeId(value.userTurnId))
1288
+ throw new Error("invalid submission evidence");
1289
+ if (value.assistantTurnId !== undefined && !isSafeId(value.assistantTurnId))
1290
+ throw new Error("invalid assistant identity");
1291
+ if (value.targetEstablishment !== undefined) {
1292
+ validateSubmissionTargetEstablishment(value.targetEstablishment, expected.targetBindingDigest);
1293
+ if (value.targetEstablishment.userTurnId !== value.userTurnId || value.targetEstablishment.userTurnEvidenceDigest !== value.userTurnEvidenceDigest) {
1294
+ throw new Error("target establishment turn mismatch");
1295
+ }
1296
+ if (value.targetEstablishment.postSendDeltaDigest !== value.postSendDeltaDigest) {
1297
+ throw new Error("target establishment delta mismatch");
1298
+ }
1299
+ }
1300
+ if (mode === "observe_only" && value.status === "submitted")
1301
+ throw new Error("observation cannot claim a new submission");
1302
+ }
1303
+ else if (value.status === "blocked") {
1304
+ assertExactRecord(value, ["status", "blockerCode", "evidenceDigest"], ["status", "blockerCode"]);
1305
+ if (!isBlockerCode(value.blockerCode))
1306
+ throw new Error("invalid final blocker");
1307
+ if (value.evidenceDigest !== undefined)
1308
+ assertDigest(value.evidenceDigest);
1309
+ }
1310
+ else if (value.status === "uncertain") {
1311
+ assertExactRecord(value, ["status", "evidenceDigest", "quarantine"], ["status", "quarantine"]);
1312
+ if (value.evidenceDigest !== undefined)
1313
+ assertDigest(value.evidenceDigest);
1314
+ if (value.quarantine !== "provider" && value.quarantine !== "caller")
1315
+ throw new Error("invalid final quarantine");
1316
+ }
1317
+ else
1318
+ throw new Error("invalid final status");
1319
+ }
1320
+ /** Validate a redacted provider identity proof at an adapter boundary. */
1321
+ export function validateSubmissionTargetEstablishment(value, expectedTargetBindingDigest, expectedActionId) {
1322
+ assertExactRecord(value, ["targetBindingDigest", "anchorDigest", "causalSendActionId", "conversationId", "canonicalThreadUrl", "userTurnId", "userTurnEvidenceDigest", "postSendDeltaDigest", "evidenceDigest", "observedAt"], ["targetBindingDigest", "anchorDigest", "causalSendActionId", "conversationId", "canonicalThreadUrl", "userTurnId", "userTurnEvidenceDigest", "postSendDeltaDigest", "evidenceDigest"]);
1323
+ if (value.targetBindingDigest !== expectedTargetBindingDigest
1324
+ || !isDigest(value.targetBindingDigest)
1325
+ || !isDigest(value.anchorDigest)
1326
+ || !isUuid(value.causalSendActionId)
1327
+ || (expectedActionId !== undefined && value.causalSendActionId !== expectedActionId)
1328
+ || !isSafeId(value.conversationId)
1329
+ || !isCanonicalThreadUrl(value.canonicalThreadUrl)
1330
+ || !isSafeId(value.userTurnId)
1331
+ || !isDigest(value.userTurnEvidenceDigest)
1332
+ || !isDigest(value.postSendDeltaDigest)
1333
+ || !isDigest(value.evidenceDigest)
1334
+ || (value.observedAt !== undefined && !isIsoInstant(value.observedAt))) {
1335
+ throw new Error("invalid target establishment evidence");
1336
+ }
1337
+ }
1338
+ /**
1339
+ * The establishment callback is a durability boundary, not a fire-and-forget
1340
+ * notification. Before a submitted receipt is allowed, its return value must
1341
+ * prove that the journal now exposes the same one-way identity refinement.
1342
+ * This intentionally reads only bounded own data properties and does not
1343
+ * retain or stringify the callback's state object.
1344
+ */
1345
+ function validateDurableTargetEstablishmentResult(value, expectedTargetBindingDigest, expected) {
1346
+ if (!isPlainRecord(value))
1347
+ throw new Error("invalid durable target establishment result");
1348
+ assertExactRecord(value, ["state", "handle"], ["state", "handle"]);
1349
+ const state = value.state;
1350
+ const handle = value.handle;
1351
+ if (!isPlainRecord(state) || !isPlainRecord(handle))
1352
+ throw new Error("invalid durable target establishment result");
1353
+ const handleTargetDigest = readOwnData(handle, "targetBindingDigest");
1354
+ const target = readOwnData(state, "target");
1355
+ if (handleTargetDigest !== expectedTargetBindingDigest || !isPlainRecord(target)) {
1356
+ throw new Error("durable target establishment is not authenticated");
1357
+ }
1358
+ if (readOwnData(target, "targetLifecycle") !== "new_established"
1359
+ || readOwnData(target, "conversationId") !== expected.conversationId
1360
+ || readOwnData(target, "canonicalThreadUrl") !== expected.canonicalThreadUrl) {
1361
+ throw new Error("durable target establishment identity does not match");
1362
+ }
1363
+ const durable = readOwnData(target, "targetEstablishment");
1364
+ if (!isPlainRecord(durable))
1365
+ throw new Error("durable target establishment evidence is missing");
1366
+ for (const key of [
1367
+ "targetBindingDigest",
1368
+ "anchorDigest",
1369
+ "causalSendActionId",
1370
+ "conversationId",
1371
+ "canonicalThreadUrl",
1372
+ "userTurnId",
1373
+ "userTurnEvidenceDigest",
1374
+ "evidenceDigest"
1375
+ ]) {
1376
+ if (readOwnData(durable, key) !== expected[key])
1377
+ throw new Error("durable target establishment evidence does not match");
1378
+ }
1379
+ }
1380
+ function findUniqueAction(state, kind) {
1381
+ const actions = Object.values(state.actions).filter(action => action.kind === kind);
1382
+ if (actions.length > 1)
1383
+ return "corrupt";
1384
+ return actions[0];
1385
+ }
1386
+ function coherentSubmissionState(state, sendAction, handoffAction, targetBindingDigest, manifestCount) {
1387
+ if (sendAction === "corrupt" || handoffAction === "corrupt")
1388
+ return false;
1389
+ if (sendAction !== undefined && (sendAction.kind !== "send" ||
1390
+ sendAction.targetDigest !== targetBindingDigest ||
1391
+ sendAction.requestDigest !== state.requestDigest ||
1392
+ sendAction.repeatPolicy !== "observe_only_after_intent" ||
1393
+ !isUuid(sendAction.actionId)))
1394
+ return false;
1395
+ if (handoffAction !== undefined && (handoffAction.kind !== "file_handoff" ||
1396
+ handoffAction.targetDigest !== targetBindingDigest ||
1397
+ handoffAction.requestDigest !== state.requestDigest ||
1398
+ handoffAction.repeatPolicy !== "observe_only_after_intent" ||
1399
+ !isUuid(handoffAction.actionId)))
1400
+ return false;
1401
+ if (handoffAction !== undefined && state.mutationBoundary === "none")
1402
+ return false;
1403
+ if (sendAction !== undefined && BOUNDARY_RANK[state.mutationBoundary] < BOUNDARY_RANK.send_may_have_occurred)
1404
+ return false;
1405
+ if (manifestCount === 0 && handoffAction !== undefined)
1406
+ return false;
1407
+ if (manifestCount > 0 && state.phase !== "prepared" && handoffAction === undefined)
1408
+ return false;
1409
+ // These are the only legal durable snapshots at each non-repeatable
1410
+ // boundary. A record in an earlier phase is not a harmless hint: it can
1411
+ // authorize a mutation that the journal never causally admitted.
1412
+ if (state.phase === "prepared") {
1413
+ if (sendAction !== undefined)
1414
+ return false;
1415
+ // A process can stop after fsyncing a handoff intent (or its satisfied
1416
+ // receipt) and before appending handoff_pending/ready. That committed
1417
+ // prefix is valid and must resume through observation only; rejecting it
1418
+ // here would strand the exact crash boundary the journal is designed to
1419
+ // recover.
1420
+ if (handoffAction === undefined) {
1421
+ if (state.mutationBoundary !== "none")
1422
+ return false;
1423
+ }
1424
+ else if (manifestCount === 0
1425
+ || state.mutationBoundary !== "handoff_may_have_occurred"
1426
+ || (handoffAction.outcome !== undefined && handoffAction.outcome !== "satisfied")) {
1427
+ return false;
1428
+ }
1429
+ }
1430
+ else if (state.phase === "handoff_pending") {
1431
+ if (sendAction !== undefined || handoffAction === undefined || handoffAction.outcome !== undefined || state.mutationBoundary !== "handoff_may_have_occurred")
1432
+ return false;
1433
+ }
1434
+ else if (state.phase === "ready") {
1435
+ if (sendAction !== undefined && sendAction.outcome !== undefined)
1436
+ return false;
1437
+ if (manifestCount > 0 && (handoffAction === undefined || handoffAction.outcome !== "satisfied"))
1438
+ return false;
1439
+ if (sendAction === undefined && BOUNDARY_RANK[state.mutationBoundary] >= BOUNDARY_RANK.send_may_have_occurred)
1440
+ return false;
1441
+ if (sendAction !== undefined && state.mutationBoundary !== "send_may_have_occurred")
1442
+ return false;
1443
+ }
1444
+ else if (state.phase === "send_pending") {
1445
+ if (sendAction === undefined || sendAction.outcome !== undefined || state.mutationBoundary !== "send_may_have_occurred")
1446
+ return false;
1447
+ }
1448
+ else if (state.phase === "submitted" || state.phase === "generating" || state.phase === "capturing") {
1449
+ if (sendAction === undefined || sendAction.outcome !== "satisfied" || BOUNDARY_RANK[state.mutationBoundary] < BOUNDARY_RANK.send_may_have_occurred)
1450
+ return false;
1451
+ }
1452
+ else if (state.phase === "completed") {
1453
+ if (sendAction === undefined || sendAction.outcome !== "satisfied" || BOUNDARY_RANK[state.mutationBoundary] < BOUNDARY_RANK.send_may_have_occurred)
1454
+ return false;
1455
+ }
1456
+ else if (state.phase === "uncertain") {
1457
+ if (state.mutationBoundary === "handoff_may_have_occurred" && (handoffAction === undefined || (handoffAction.outcome !== undefined && handoffAction.outcome !== "satisfied")))
1458
+ return false;
1459
+ if (BOUNDARY_RANK[state.mutationBoundary] >= BOUNDARY_RANK.send_may_have_occurred && sendAction === undefined)
1460
+ return false;
1461
+ if (state.mutationBoundary === "none")
1462
+ return false;
1463
+ }
1464
+ if (state.receipt !== undefined) {
1465
+ if (state.phase !== "completed" || sendAction === undefined || sendAction.outcome !== "satisfied")
1466
+ return false;
1467
+ }
1468
+ if (state.phase === "completed" && state.receipt === undefined)
1469
+ return false;
1470
+ if (state.mutationBoundary === "handoff_may_have_occurred" && handoffAction === undefined)
1471
+ return false;
1472
+ if (BOUNDARY_RANK[state.mutationBoundary] >= BOUNDARY_RANK.send_may_have_occurred && sendAction === undefined)
1473
+ return false;
1474
+ return true;
1475
+ }
1476
+ function targetBindingMatches(operation, expected) {
1477
+ return operation.handle.targetBindingDigest === expected.targetBindingDigest && operation.state.target !== undefined;
1478
+ }
1479
+ function stageReasonToBlocker(reason) {
1480
+ if (reason === "target")
1481
+ return "target_binding_mismatch";
1482
+ if (reason === "configuration")
1483
+ return "configuration_drift";
1484
+ if (reason === "composer")
1485
+ return "composer_drift";
1486
+ return "port_protocol_violation";
1487
+ }
1488
+ function cancellationCode(options) {
1489
+ if (options.signal?.aborted)
1490
+ return "operation_cancelled";
1491
+ if (options.deadlineAt !== undefined && Date.now() >= options.deadlineAt)
1492
+ return "operation_timeout";
1493
+ return undefined;
1494
+ }
1495
+ function safeIdentity(operation) {
1496
+ const operationRecord = safeDataRecord(operation);
1497
+ const state = safeDataRecord(readOwnData(operationRecord, "state"));
1498
+ const handle = safeDataRecord(readOwnData(operationRecord, "handle"));
1499
+ const operationId = readOwnData(state, "operationId");
1500
+ const requestDigest = readOwnData(state, "requestDigest");
1501
+ const surface = readOwnData(state, "surface");
1502
+ const targetBindingDigest = state === undefined ? undefined : readOwnData(handle, "targetBindingDigest");
1503
+ return {
1504
+ operationId: isUuid(operationId) ? operationId : INVALID_OPERATION_ID,
1505
+ requestDigest: isDigest(requestDigest) ? requestDigest : INVALID_DIGEST,
1506
+ surface: surface === "work" ? "work" : "chat",
1507
+ ...(isDigest(targetBindingDigest) ? { targetBindingDigest } : {})
1508
+ };
1509
+ }
1510
+ function blockedResult(identity, expected, code, observationRequired) {
1511
+ // Only the durable operation snapshot can authenticate a target identity.
1512
+ // `expected` is caller input and must never replace that identity on an
1513
+ // invalid-input path, even when it happens to be digest-shaped.
1514
+ void expected;
1515
+ return blockedBase(identity, code, observationRequired, "none");
1516
+ }
1517
+ function blockedBase(base, code, observationRequired, boundary, evidenceDigest) {
1518
+ return { ...base, kind: "blocked", blocker: blocker(code, observationRequired, boundary, evidenceDigest) };
1519
+ }
1520
+ function uncertainBase(base, code, boundary, evidenceDigest) {
1521
+ return { ...base, kind: "uncertain", blocker: blocker(code, true, boundary, evidenceDigest) };
1522
+ }
1523
+ function cancelledResult(base, code, boundary) {
1524
+ return {
1525
+ ...base,
1526
+ kind: "cancelled",
1527
+ blocker: {
1528
+ code,
1529
+ observationRequired: boundary !== "none",
1530
+ mutationBoundary: boundary
1531
+ }
1532
+ };
1533
+ }
1534
+ function blocker(code, observationRequired, mutationBoundary, evidenceDigest) {
1535
+ return {
1536
+ code,
1537
+ observationRequired,
1538
+ mutationBoundary,
1539
+ ...(evidenceDigest === undefined ? {} : { evidenceDigest })
1540
+ };
1541
+ }
1542
+ function blockerEvidence(base, expected, code, evidenceDigest) {
1543
+ return {
1544
+ kind: "blocker",
1545
+ operationId: base.operationId,
1546
+ requestDigest: base.requestDigest,
1547
+ surface: base.surface,
1548
+ phase: "uncertain",
1549
+ mutationBoundary: "send_may_have_occurred",
1550
+ targetBindingDigest: expected.targetBindingDigest,
1551
+ blocker: blocker(code, true, "send_may_have_occurred", evidenceDigest)
1552
+ };
1553
+ }
1554
+ async function persistBlockerBestEffort(ports, evidence) {
1555
+ try {
1556
+ await ports.persistReceiptEvidence(evidence);
1557
+ }
1558
+ catch {
1559
+ // The caller receives the original redacted blocker. No mutation retry is
1560
+ // ever authorized by a failed persistence call.
1561
+ }
1562
+ }
1563
+ function assertDigest(value) {
1564
+ if (!isDigest(value))
1565
+ throw new Error("invalid digest");
1566
+ }
1567
+ function isDigest(value) {
1568
+ return typeof value === "string" && DIGEST_PATTERN.test(value);
1569
+ }
1570
+ function isIsoInstant(value) {
1571
+ if (typeof value !== "string" || !INSTANT_PATTERN.test(value))
1572
+ return false;
1573
+ const parsed = Date.parse(value);
1574
+ return Number.isFinite(parsed) && new Date(parsed).toISOString() === value;
1575
+ }
1576
+ function isCanonicalThreadUrl(value) {
1577
+ if (typeof value !== "string" || value.length === 0 || value.length > 4096 || value.includes("?") || value.includes("#"))
1578
+ return false;
1579
+ try {
1580
+ const parsed = new URL(value);
1581
+ return parsed.protocol === "https:"
1582
+ && parsed.username === ""
1583
+ && parsed.password === ""
1584
+ && parsed.search === ""
1585
+ && parsed.hash === ""
1586
+ && parsed.hostname.length > 0;
1587
+ }
1588
+ catch {
1589
+ return false;
1590
+ }
1591
+ }
1592
+ function isAbortSignalLike(value) {
1593
+ if (value === null || typeof value !== "object" || Array.isArray(value))
1594
+ return false;
1595
+ const candidate = value;
1596
+ return typeof candidate.aborted === "boolean"
1597
+ && typeof candidate.addEventListener === "function"
1598
+ && typeof candidate.removeEventListener === "function";
1599
+ }
1600
+ function isPlainRecord(value) {
1601
+ if (value === null || typeof value !== "object" || Array.isArray(value))
1602
+ return false;
1603
+ try {
1604
+ const prototype = Object.getPrototypeOf(value);
1605
+ if (prototype !== Object.prototype && prototype !== null)
1606
+ return false;
1607
+ for (const descriptor of Object.values(Object.getOwnPropertyDescriptors(value))) {
1608
+ if (!("value" in descriptor) || descriptor.get !== undefined || descriptor.set !== undefined)
1609
+ return false;
1610
+ }
1611
+ return true;
1612
+ }
1613
+ catch {
1614
+ return false;
1615
+ }
1616
+ }
1617
+ function readOwnData(value, key) {
1618
+ if (!isPlainRecord(value))
1619
+ return undefined;
1620
+ try {
1621
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
1622
+ return descriptor !== undefined && "value" in descriptor ? descriptor.value : undefined;
1623
+ }
1624
+ catch {
1625
+ return undefined;
1626
+ }
1627
+ }
1628
+ function safeDataRecord(value) {
1629
+ return isPlainRecord(value) ? value : undefined;
1630
+ }
1631
+ function hasAccessorInPlainData(value, seen = new Set(), depth = 0) {
1632
+ if (value === null || typeof value !== "object")
1633
+ return false;
1634
+ let prototype;
1635
+ try {
1636
+ prototype = Object.getPrototypeOf(value);
1637
+ }
1638
+ catch {
1639
+ return true;
1640
+ }
1641
+ if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null)
1642
+ return false;
1643
+ if (seen.has(value))
1644
+ return false;
1645
+ if (depth > 32)
1646
+ return true;
1647
+ seen.add(value);
1648
+ let descriptors;
1649
+ try {
1650
+ descriptors = Object.getOwnPropertyDescriptors(value);
1651
+ }
1652
+ catch {
1653
+ return true;
1654
+ }
1655
+ for (const descriptor of Object.values(descriptors)) {
1656
+ if (!("value" in descriptor))
1657
+ return true;
1658
+ if (hasAccessorInPlainData(descriptor.value, seen, depth + 1))
1659
+ return true;
1660
+ }
1661
+ return false;
1662
+ }
1663
+ function isSafeId(value) {
1664
+ return typeof value === "string" && OPAQUE_ID_PATTERN.test(value) && !/[\u0000-\u001f\u007f]/.test(value);
1665
+ }
1666
+ function isOpaqueKey(value) {
1667
+ return typeof value === "string" && OPAQUE_KEY_PATTERN.test(value);
1668
+ }
1669
+ function isUuid(value) {
1670
+ return typeof value === "string" && UUID_PATTERN.test(value);
1671
+ }
1672
+ function isBlockerCode(value) {
1673
+ return typeof value === "string" && [
1674
+ "operation_cancelled", "operation_timeout", "stale_handle", "operation_state_corrupt", "target_binding_mismatch", "target_evidence_unavailable", "configuration_drift", "composer_drift", "attachment_manifest_mismatch", "input_file_changed", "ambiguous_file_handoff", "ambiguous_submit", "concurrent_user_turn", "send_control_unavailable", "tab_ownership_conflict", "runtime_incompatible", "backend_unavailable", "browser_bridge_unavailable", "login_required", "captcha", "rate_limited", "permission_required", "needs_confirmation", "selector_drift", "journal_unavailable", "port_protocol_violation", "already_completed"
1675
+ ].includes(value);
1676
+ }