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,1639 @@
1
+ import { types as nodeTypes } from "node:util";
2
+ import { ACTIVE_COMPOSER_FILE_INPUT_CLICK_EXPRESSION } from "../browser/active-composer-file-input.js";
3
+ import { localeLabels } from "../dom/locale-labels.js";
4
+ import { createProductionAttachmentPrimitive } from "./production-attachments.js";
5
+ import { isPlainDataRecord } from "../runtime/value-boundaries.js";
6
+ export const CHATGPT_ATTACHMENT_PROVIDER_SCHEMA_VERSION = "chatgpt.browser_control.production_chatgpt_attachments.v1";
7
+ const DIGEST_PATTERN = /^hmac-sha256:[0-9a-f]{64}$/u;
8
+ const ID_PATTERN = /^[A-Za-z0-9._:-]{1,512}$/u;
9
+ const SELECTOR_PATTERN = /^[A-Za-z0-9_#.:>\[\]="'()*^$|~+=\\ -]{1,4096}$/u;
10
+ const LOCALE_PATTERN = /^[A-Za-z]{2,8}(?:[-_][A-Za-z0-9]{1,8}){0,3}$/u;
11
+ const MAX_PROBE_ITEMS = 256;
12
+ const MAX_PROBE_TEXT = 512;
13
+ const MAX_TIMEOUT_MS = 30_000;
14
+ const CAPABILITY_KEY = "chatgpt.attachments.active-composer";
15
+ const ATTACHMENT_CONTROL_LABELS = Object.freeze([...new Set([
16
+ ...localeLabels.addFilesOpenerCandidates,
17
+ ...localeLabels.addPhotosFilesMenuItem,
18
+ ...localeLabels.projectSourcesUploadFiles
19
+ ].filter(label => typeof label === "string" && label.length > 0 && label.length <= MAX_PROBE_TEXT))]);
20
+ /**
21
+ * Build a request-scoped ChatGPT attachment capability.
22
+ *
23
+ * Important recovery property: a non-empty exact observation is impossible
24
+ * until this exact returned capability has completed its own chooser handoff.
25
+ * A fresh provider instance observing an existing/same-name attachment stays
26
+ * ambiguous, including after a process restart.
27
+ */
28
+ export function createChatGPTAttachmentProvider(options) {
29
+ const captured = captureOptions(options);
30
+ // Sync callers retain synchronous construction and validation. Attach a
31
+ // rejection handler immediately if a caller supplies an async authority.
32
+ const values = captured.files.map((file, ordinal) => {
33
+ let value;
34
+ try {
35
+ value = captured.identityDigest(ordinal, file.manifest);
36
+ }
37
+ catch {
38
+ throw new Error("invalid ChatGPT attachment provider options");
39
+ }
40
+ if (isNativePromise(value))
41
+ void value.catch(() => undefined);
42
+ return value;
43
+ });
44
+ return buildChatGPTAttachmentProvider(completeOptions(captured, values));
45
+ }
46
+ /** Resolve remote identity signatures before constructing browser capabilities. */
47
+ export async function createChatGPTAttachmentProviderAsync(options) {
48
+ const captured = captureOptions(options);
49
+ let values;
50
+ try {
51
+ values = await Promise.all(captured.files.map(async (file, ordinal) => await captured.identityDigest(ordinal, file.manifest)));
52
+ }
53
+ catch {
54
+ throw new Error("invalid ChatGPT attachment provider options");
55
+ }
56
+ return buildChatGPTAttachmentProvider(completeOptions(captured, values));
57
+ }
58
+ function buildChatGPTAttachmentProvider(normalized) {
59
+ let causalHandoff;
60
+ let menuOpened = false;
61
+ let hiddenInputActivation;
62
+ const expectedFactsForRequest = (request) => {
63
+ const facts = [];
64
+ for (const entry of request.manifest.identities) {
65
+ if (entry.ordinal < 0 || entry.ordinal >= normalized.manifestFacts.length
66
+ || normalized.identityDigests[entry.ordinal] !== entry.identityDigest)
67
+ return undefined;
68
+ const manifest = normalized.manifestFacts[entry.ordinal];
69
+ if (manifest === undefined)
70
+ return undefined;
71
+ facts.push(Object.freeze({
72
+ ordinal: entry.ordinal,
73
+ displayName: manifest.displayName,
74
+ bytes: manifest.bytes
75
+ }));
76
+ }
77
+ return Object.freeze(facts);
78
+ };
79
+ const recordCausalHandoff = (request, target) => {
80
+ causalHandoff = Object.freeze({
81
+ operationId: request.operationId,
82
+ requestDigest: request.requestDigest,
83
+ actionId: request.actionId,
84
+ targetBindingDigest: request.targetBindingDigest,
85
+ manifest: Object.freeze({
86
+ count: request.manifest.count,
87
+ identities: Object.freeze(request.manifest.identities.map(entry => Object.freeze({
88
+ identityDigest: entry.identityDigest,
89
+ ordinal: entry.ordinal
90
+ })))
91
+ }),
92
+ manifestFacts: normalized.manifestFacts,
93
+ target: Object.freeze({ ...target })
94
+ });
95
+ };
96
+ const probe = async (page, expected, requestSignal, requestDeadlineAt) => {
97
+ if (isAnyAbortRequested(normalized.signal, requestSignal, requestDeadlineAt))
98
+ return undefined;
99
+ const timeoutMs = boundedProbeTimeout(normalized.timeoutMs, requestDeadlineAt);
100
+ if (timeoutMs <= 0)
101
+ return undefined;
102
+ const result = await readComposerProbe(page, timeoutMs, normalized.labelCandidates, requestSignal ?? normalized.signal, expected);
103
+ return isAnyAbortRequested(normalized.signal, requestSignal, requestDeadlineAt) ? undefined : result;
104
+ };
105
+ const observeSurface = async (request, page, target) => {
106
+ if (normalized.signal?.aborted)
107
+ return { status: "unavailable", source: "live_surface" };
108
+ const current = await probe(page, expectedFactsForRequest(request));
109
+ if (current === undefined || current.status !== "ready" || !isSafeTarget(target)) {
110
+ return { status: "unavailable", source: "live_surface" };
111
+ }
112
+ const baseMaterial = surfaceEvidenceMaterial(request, target, current);
113
+ if (current.facts.length === 0 && current.attachmentRegionCount === 0
114
+ && current.inputFilesReadable && current.fileInputCount === 1) {
115
+ const evidence = await safeEvidence(normalized.evidenceDigest, "chatgpt-attachment-surface", {
116
+ ...baseMaterial,
117
+ status: "absent",
118
+ count: 0
119
+ });
120
+ return evidence === undefined
121
+ ? { status: "unavailable", source: "live_surface" }
122
+ : {
123
+ status: "absent",
124
+ source: "live_surface",
125
+ count: 0,
126
+ identityDigests: [],
127
+ providerEvidenceDigest: evidence
128
+ };
129
+ }
130
+ const causal = causalHandoff;
131
+ if (causal === undefined || !sameCausalRequest(causal, request, target)) {
132
+ return evidenceStatus(normalized.evidenceDigest, baseMaterial, "ambiguous", current.facts.length);
133
+ }
134
+ const match = compareCausalSurface(current, causal, request);
135
+ const sendReady = match.status === "exact"
136
+ ? await readComposerSendReadiness(page, normalized.timeoutMs, normalized.sendLabelCandidates, normalized.signal)
137
+ : undefined;
138
+ const observedStatus = match.status === "exact" && sendReady !== true
139
+ ? "delayed"
140
+ : match.status;
141
+ const evidence = await safeEvidence(normalized.evidenceDigest, "chatgpt-attachment-surface", {
142
+ ...baseMaterial,
143
+ status: observedStatus,
144
+ count: current.facts.length,
145
+ factsMatch: match.factsMatch,
146
+ multiplicityMatch: match.multiplicityMatch,
147
+ orderDeterministic: current.orderDeterministic,
148
+ duplicateNames: match.duplicateNames,
149
+ sendReady: sendReady === true
150
+ });
151
+ if (observedStatus !== "exact") {
152
+ return evidence === undefined
153
+ ? { status: observedStatus, source: "live_surface" }
154
+ : { status: observedStatus, source: "live_surface", providerEvidenceDigest: evidence };
155
+ }
156
+ return evidence === undefined
157
+ ? { status: "unavailable", source: "live_surface" }
158
+ : {
159
+ status: "exact",
160
+ source: "live_surface",
161
+ count: request.manifest.count,
162
+ identityDigests: request.manifest.identities.map(entry => entry.identityDigest),
163
+ providerEvidenceDigest: evidence
164
+ };
165
+ };
166
+ const prepareActivation = async (request, page, target, preparationOptions) => {
167
+ // The provider-level lifetime signal predates the one-shot request and
168
+ // retains its historical pre-mutation blocker. A caller/coordinator signal
169
+ // arrives after the durable intent and therefore must quarantine instead.
170
+ if (normalized.signal?.aborted)
171
+ return { status: "not_satisfied", blockerCode: "operation_timeout" };
172
+ if (request.signal?.aborted || request.deadlineAt !== undefined && Date.now() >= request.deadlineAt) {
173
+ return { status: "uncertain", quarantine: "caller" };
174
+ }
175
+ if (preparationOptions.timeoutMs <= 0)
176
+ return { status: "not_satisfied", blockerCode: "operation_timeout" };
177
+ const current = await probe(page, expectedFactsForRequest(request), request.signal, request.deadlineAt);
178
+ if (current === undefined || current.status !== "ready" || !isSafeTarget(target)) {
179
+ return { status: "not_satisfied", blockerCode: "selector_drift" };
180
+ }
181
+ // The only safe precondition for a first handoff is an unambiguously empty
182
+ // active composer. Existing same-name chips are not treated as success.
183
+ if (current.facts.length !== 0 || current.attachmentRegionCount !== 0
184
+ || !current.inputFilesReadable || current.fileInputCount !== 1) {
185
+ return { status: "not_satisfied", blockerCode: "ambiguous_file_handoff" };
186
+ }
187
+ const material = {
188
+ operationId: request.operationId,
189
+ requestDigest: request.requestDigest,
190
+ actionId: request.actionId,
191
+ targetBindingDigest: request.targetBindingDigest,
192
+ status: "prepared",
193
+ empty: true,
194
+ activationCandidateCount: current.activationCandidateCount,
195
+ menu: current.menuOpenerSelector !== undefined
196
+ };
197
+ const evidence = await safeEvidence(normalized.evidenceDigest, "chatgpt-attachment-precondition", material);
198
+ if (evidence === undefined)
199
+ return { status: "uncertain", quarantine: "provider" };
200
+ // Codex Chrome intentionally keeps ChatGPT's native file input hidden.
201
+ // Prefer its scoped CDP user-gesture capability when present: the fixed
202
+ // expression re-proves one visible composer and one owned file input, then
203
+ // the core primitive's already-registered chooser performs setFiles.
204
+ if (current.directActivationSelector === undefined && current.menuOpenerSelector !== undefined) {
205
+ const cdpSend = await resolveCdpSend(page, preparationOptions.timeoutMs);
206
+ if (cdpSend !== undefined) {
207
+ hiddenInputActivation = async ({ timeoutMs }) => {
208
+ const raw = cdpSend("Runtime.evaluate", {
209
+ expression: ACTIVE_COMPOSER_FILE_INPUT_CLICK_EXPRESSION,
210
+ userGesture: true,
211
+ awaitPromise: true,
212
+ returnByValue: true
213
+ }, { timeoutMs });
214
+ const evaluation = await awaitMutating(raw);
215
+ if (!cdpActivationAccepted(evaluation))
216
+ throw new Error("scoped file-input activation was refused");
217
+ };
218
+ if (isAnyAbortRequested(normalized.signal, request.signal, request.deadlineAt)) {
219
+ return { status: "uncertain", quarantine: "caller" };
220
+ }
221
+ return { status: "prepared", providerEvidenceDigest: evidence };
222
+ }
223
+ // Some non-CDP integrations expose only a localized plus-menu opener.
224
+ // The semantic probe has already proved it is in the active composer;
225
+ // click it once so the final resolver can identify the file row.
226
+ const opener = locatorFor(page, current.menuOpenerSelector);
227
+ if (opener === undefined)
228
+ return { status: "not_satisfied", blockerCode: "selector_drift" };
229
+ const click = safeMethod(opener, "click");
230
+ if (click === undefined)
231
+ return { status: "not_satisfied", blockerCode: "selector_drift" };
232
+ try {
233
+ const result = click.call(opener, {
234
+ timeout: preparationOptions.timeoutMs,
235
+ timeoutMs: preparationOptions.timeoutMs
236
+ });
237
+ await awaitMutating(result);
238
+ menuOpened = true;
239
+ }
240
+ catch {
241
+ // A click may have opened the menu before the bridge rejected. The
242
+ // final resolver may still prove a unique row; never retry the opener.
243
+ menuOpened = true;
244
+ return { status: "uncertain", quarantine: "provider" };
245
+ }
246
+ if (isAnyAbortRequested(normalized.signal, request.signal, request.deadlineAt)) {
247
+ return { status: "uncertain", quarantine: "caller" };
248
+ }
249
+ const afterMenu = await probe(page, expectedFactsForRequest(request), request.signal, request.deadlineAt);
250
+ if (afterMenu === undefined || afterMenu.menuUploadSelector === undefined
251
+ || afterMenu.activationCandidateCount !== 1) {
252
+ return { status: "uncertain", quarantine: "provider" };
253
+ }
254
+ }
255
+ return { status: "prepared", providerEvidenceDigest: evidence };
256
+ };
257
+ const resolveActivation = async (request, page, target) => {
258
+ if (isAnyAbortRequested(normalized.signal, request.signal, request.deadlineAt) || !isSafeTarget(target))
259
+ return undefined;
260
+ const current = await probe(page, expectedFactsForRequest(request), request.signal, request.deadlineAt);
261
+ if (current === undefined || current.status !== "ready")
262
+ return undefined;
263
+ if (hiddenInputActivation !== undefined) {
264
+ return {
265
+ activate: hiddenInputActivation,
266
+ candidateCount: 1,
267
+ capabilityKey: CAPABILITY_KEY
268
+ };
269
+ }
270
+ const selector = menuOpened
271
+ ? current.menuUploadSelector
272
+ : current.directActivationSelector;
273
+ if (selector === undefined || current.activationCandidateCount !== 1)
274
+ return undefined;
275
+ const locator = locatorFor(page, selector);
276
+ if (locator === undefined)
277
+ return undefined;
278
+ return { locator, candidateCount: 1, capabilityKey: CAPABILITY_KEY };
279
+ };
280
+ const primitiveOptions = {
281
+ evidenceDigest: normalized.evidenceDigest,
282
+ files: normalized.files,
283
+ identityDigest: normalized.identityDigest,
284
+ revalidateFile: normalized.revalidateFile,
285
+ observeSurface,
286
+ resolveActivation,
287
+ prepareActivation
288
+ };
289
+ const primitive = createProductionAttachmentPrimitive({
290
+ ...primitiveOptions,
291
+ ...(normalized.timeoutWasProvided ? { timeoutMs: normalized.timeoutMs } : {}),
292
+ ...(normalized.maxCandidatesWasProvided ? { maxCandidates: normalized.maxCandidates } : {})
293
+ });
294
+ const handoffFiles = async (request, page, target) => {
295
+ // Capture the request envelope before any provider callback can re-enter
296
+ // the caller and mutate its manifest while the native chooser operation is
297
+ // in flight. The core still performs its own validation; this snapshot is
298
+ // only the immutable causal record installed after a satisfied handoff.
299
+ const causalRequest = snapshotHandoffRequest(request);
300
+ const causalTarget = snapshotTargetBinding(target);
301
+ const result = await primitive.handoffFiles(request, page, target);
302
+ if (result.status === "satisfied" && causalRequest !== undefined && causalTarget !== undefined) {
303
+ recordCausalHandoff(causalRequest, causalTarget);
304
+ }
305
+ return result;
306
+ };
307
+ const handoffFilesForAdapter = async (request, files, page, target) => {
308
+ const causalRequest = snapshotHandoffRequest(request);
309
+ const causalTarget = snapshotTargetBinding(target);
310
+ const result = await primitive.handoffFilesForAdapter(request, files, page, target);
311
+ if (result.status === "satisfied" && causalRequest !== undefined && causalTarget !== undefined) {
312
+ recordCausalHandoff(causalRequest, causalTarget);
313
+ }
314
+ return result;
315
+ };
316
+ return Object.freeze({
317
+ observeAttachments: primitive.observeAttachments,
318
+ handoffFiles,
319
+ handoffFilesForAdapter
320
+ });
321
+ }
322
+ export const createProductionChatGPTAttachments = createChatGPTAttachmentProvider;
323
+ export const createChatGPTProductionAttachmentPrimitive = createChatGPTAttachmentProvider;
324
+ function captureOptions(value) {
325
+ if (value === null || typeof value !== "object" || Array.isArray(value))
326
+ throw new Error("invalid ChatGPT attachment provider options");
327
+ assertOwnDataKeys(value, ["evidenceDigest", "files", "identityDigest", "revalidateFile", "timeoutMs", "maxCandidates", "locale", "signal"]);
328
+ const evidenceDigest = readOwn(value, "evidenceDigest");
329
+ const files = readOwn(value, "files");
330
+ const identityDigest = readOwn(value, "identityDigest");
331
+ const revalidateFile = readOwn(value, "revalidateFile");
332
+ const timeoutValue = readOwn(value, "timeoutMs");
333
+ const maxCandidatesValue = readOwn(value, "maxCandidates");
334
+ const locale = readOwn(value, "locale");
335
+ const signal = readOwn(value, "signal");
336
+ if (typeof evidenceDigest !== "function" || !Array.isArray(files)
337
+ || typeof identityDigest !== "function" || typeof revalidateFile !== "function") {
338
+ throw new Error("invalid ChatGPT attachment provider options");
339
+ }
340
+ const timeoutMs = timeoutValue ?? 5_000;
341
+ const maxCandidates = maxCandidatesValue ?? 128;
342
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > MAX_TIMEOUT_MS
343
+ || !Number.isSafeInteger(maxCandidates) || maxCandidates < 1 || maxCandidates > 512) {
344
+ throw new Error("invalid ChatGPT attachment provider options");
345
+ }
346
+ if (locale !== undefined && (typeof locale !== "string" || !LOCALE_PATTERN.test(locale))) {
347
+ throw new Error("invalid ChatGPT attachment provider options");
348
+ }
349
+ if (signal !== undefined && !isAbortSignal(signal))
350
+ throw new Error("invalid ChatGPT attachment provider options");
351
+ const labels = ATTACHMENT_CONTROL_LABELS;
352
+ const sendLabels = Object.freeze([...new Set(localeLabels.sendButton.filter(label => typeof label === "string" && label.length > 0 && label.length <= MAX_PROBE_TEXT))]);
353
+ const snapshot = snapshotFileIdentities(files);
354
+ return Object.freeze({
355
+ evidenceDigest,
356
+ files: snapshot,
357
+ identityDigest,
358
+ revalidateFile,
359
+ timeoutMs,
360
+ maxCandidates,
361
+ timeoutWasProvided: timeoutValue !== undefined,
362
+ maxCandidatesWasProvided: maxCandidatesValue !== undefined,
363
+ manifestFacts: Object.freeze(snapshot.map(file => Object.freeze({ ...file.manifest }))),
364
+ ...(locale === undefined ? {} : { locale }),
365
+ ...(signal === undefined ? {} : { signal }),
366
+ labelCandidates: labels,
367
+ sendLabelCandidates: sendLabels
368
+ });
369
+ }
370
+ function completeOptions(captured, values) {
371
+ if (values.length !== captured.files.length || values.some(value => typeof value !== "string" || !DIGEST_PATTERN.test(value))
372
+ || new Set(values).size !== values.length)
373
+ throw new Error("invalid ChatGPT attachment provider options");
374
+ const identityDigests = Object.freeze([...values]);
375
+ const identityDigest = (ordinal, _manifest) => {
376
+ const value = identityDigests[ordinal];
377
+ if (value === undefined)
378
+ throw new Error("invalid ChatGPT attachment provider options");
379
+ return value;
380
+ };
381
+ return Object.freeze({ ...captured, identityDigests, identityDigest });
382
+ }
383
+ async function readComposerProbe(page, timeoutMs, labelCandidates, signal, expected, observationOnly = false) {
384
+ if (signal?.aborted)
385
+ return undefined;
386
+ const evaluate = safeMethod(page, "evaluate");
387
+ if (evaluate === undefined)
388
+ return undefined;
389
+ let raw;
390
+ try {
391
+ raw = evaluate.call(page, inspectChatGPTComposer, {
392
+ labels: [...labelCandidates],
393
+ ...(observationOnly ? { observationOnly: true } : {}),
394
+ ...(expected === undefined ? {} : {
395
+ expected: expected.map(fact => ({
396
+ ordinal: fact.ordinal,
397
+ displayName: fact.displayName,
398
+ bytes: fact.bytes
399
+ }))
400
+ })
401
+ }, { timeout: timeoutMs });
402
+ raw = await boundedNative(raw, timeoutMs);
403
+ }
404
+ catch {
405
+ return undefined;
406
+ }
407
+ return normalizeProbe(raw);
408
+ }
409
+ /** Share the provider's evidence rules with every empty-manifest checkpoint. */
410
+ export async function readChatGPTEmptyAttachmentState(page) {
411
+ const probe = await readComposerProbe(page, 10_000, ATTACHMENT_CONTROL_LABELS, undefined, undefined, true);
412
+ if (probe === undefined)
413
+ return undefined;
414
+ return {
415
+ supported: probe.status === "ready" && probe.inputFilesReadable && probe.fileInputCount === 1,
416
+ count: probe.facts.length,
417
+ visibleAttachmentCount: probe.attachmentRegionCount
418
+ };
419
+ }
420
+ async function readComposerSendReadiness(page, timeoutMs, labelCandidates, signal) {
421
+ if (signal?.aborted)
422
+ return undefined;
423
+ const evaluate = safeMethod(page, "evaluate");
424
+ if (evaluate === undefined)
425
+ return undefined;
426
+ try {
427
+ const pending = evaluate.call(page, inspectChatGPTSendReadiness, {
428
+ labels: [...labelCandidates]
429
+ }, { timeout: timeoutMs });
430
+ const raw = await boundedNative(pending, timeoutMs);
431
+ if (!isDataRecord(raw) || !hasExactKeys(raw, ["status"]))
432
+ return undefined;
433
+ const status = readOwn(raw, "status");
434
+ return status === "ready" ? true : status === "not_ready" ? false : undefined;
435
+ }
436
+ catch {
437
+ return undefined;
438
+ }
439
+ }
440
+ function inspectChatGPTSendReadiness(argument) {
441
+ const record = argument !== null && typeof argument === "object" && !Array.isArray(argument)
442
+ ? argument
443
+ : {};
444
+ const labels = Array.isArray(record.labels)
445
+ ? record.labels.filter((label) => typeof label === "string" && label.length <= 512)
446
+ .map(label => label.replace(/\s+/gu, " ").trim().toLocaleLowerCase())
447
+ : [];
448
+ const visible = (element) => {
449
+ if (element.hidden || element.closest("[hidden], [inert], [aria-hidden='true']"))
450
+ return false;
451
+ const style = getComputedStyle(element);
452
+ const rect = element.getBoundingClientRect();
453
+ return style.display !== "none" && style.visibility !== "hidden" && style.opacity !== "0"
454
+ && (rect.width > 0 || rect.height > 0);
455
+ };
456
+ const textboxes = [...document.querySelectorAll("textarea, [contenteditable='true'], [role='textbox']")].filter(visible);
457
+ const roots = [...new Set(textboxes.map(textbox => textbox.closest("form")
458
+ ?? textbox.closest("[data-testid*='composer' i]")
459
+ ?? textbox.closest("[aria-label*='composer' i]")
460
+ ?? textbox.closest("[class*='composer' i]")).filter((root) => root !== null))];
461
+ if (roots.length !== 1)
462
+ return { status: "ambiguous" };
463
+ const controls = [...roots[0].querySelectorAll("button, [role='button']")];
464
+ if (controls.length > 256)
465
+ return { status: "ambiguous" };
466
+ const candidates = controls.filter(control => {
467
+ if (!visible(control))
468
+ return false;
469
+ if (control.id === "composer-submit-button" || control.getAttribute("data-testid") === "send-button")
470
+ return true;
471
+ const accessible = [
472
+ control.getAttribute("aria-label"),
473
+ control.getAttribute("title"),
474
+ control.textContent
475
+ ].filter((value) => typeof value === "string")
476
+ .join(" ").replace(/\s+/gu, " ").trim().toLocaleLowerCase();
477
+ return labels.includes(accessible);
478
+ });
479
+ if (candidates.length !== 1)
480
+ return { status: "ambiguous" };
481
+ const control = candidates[0];
482
+ const enabled = control.disabled !== true
483
+ && !control.hasAttribute("disabled")
484
+ && control.getAttribute("aria-disabled") !== "true"
485
+ && !control.hasAttribute("inert")
486
+ && control.getAttribute("aria-hidden") !== "true";
487
+ return { status: enabled ? "ready" : "not_ready" };
488
+ }
489
+ /**
490
+ * This function is serialized into the page. It uses HTML/ARIA structure as
491
+ * the primary semantic contract and only uses the verified locale registry as
492
+ * a text fallback for localized menu rows. It returns no raw labels, URLs,
493
+ * prompts, account data, or file paths.
494
+ */
495
+ /** @internal Exact serialized evaluator, exported only for bridge-contract tests. */
496
+ export function inspectChatGPTComposer(argument) {
497
+ // Keep this evaluator self-contained. Browser bridges serialize only this
498
+ // function; module-scope helpers would be undefined in the page realm.
499
+ const MAX_PROBE_ITEMS = 256;
500
+ const MAX_PROBE_TEXT = 512;
501
+ const record = argument !== null && typeof argument === "object" && !Array.isArray(argument)
502
+ ? argument
503
+ : {};
504
+ const labels = Array.isArray(record.labels)
505
+ ? record.labels.filter((label) => typeof label === "string" && label.length <= 512)
506
+ : [];
507
+ const expected = [];
508
+ if (Array.isArray(record.expected)) {
509
+ if (record.expected.length > MAX_PROBE_ITEMS)
510
+ throw new Error("probe limit exceeded");
511
+ for (const item of record.expected) {
512
+ if (item === null || typeof item !== "object" || Array.isArray(item))
513
+ continue;
514
+ const entry = item;
515
+ if (typeof entry.ordinal !== "number" || !Number.isSafeInteger(entry.ordinal)
516
+ || typeof entry.displayName !== "string" || entry.displayName.length === 0 || entry.displayName.length > 512
517
+ || typeof entry.bytes !== "number" || !Number.isSafeInteger(entry.bytes) || entry.bytes < 0)
518
+ continue;
519
+ expected.push({ ordinal: entry.ordinal, displayName: entry.displayName.normalize("NFC"), bytes: entry.bytes });
520
+ }
521
+ expected.sort((left, right) => left.ordinal - right.ordinal);
522
+ }
523
+ const boundedQuery = (root, selector, maxMatched = MAX_PROBE_ITEMS, maxVisited = 4096) => {
524
+ const simpleMatch = (element, token) => {
525
+ let offset = 0;
526
+ const tag = /^[A-Za-z][A-Za-z0-9-]*/u.exec(token);
527
+ if (tag !== null) {
528
+ if (element.tagName.toLocaleLowerCase() !== tag[0].toLocaleLowerCase())
529
+ return false;
530
+ offset = tag[0].length;
531
+ }
532
+ while (offset < token.length) {
533
+ if (token[offset] !== "[")
534
+ return false;
535
+ const close = token.indexOf("]", offset + 1);
536
+ if (close < 0)
537
+ return false;
538
+ const expression = token.slice(offset + 1, close).trim();
539
+ const attribute = /^([A-Za-z0-9_:-]+)(?:(\*=|=)'([^']*)'(?:\s+(i))?)?$/u.exec(expression);
540
+ if (attribute === null)
541
+ return false;
542
+ const actual = element.getAttribute(attribute[1]);
543
+ if (attribute[2] === undefined) {
544
+ if (actual === null)
545
+ return false;
546
+ }
547
+ else {
548
+ if (actual === null)
549
+ return false;
550
+ const insensitive = attribute[4] === "i";
551
+ const left = insensitive ? actual.toLocaleLowerCase() : actual;
552
+ const rightValue = attribute[3] ?? "";
553
+ const right = insensitive ? rightValue.toLocaleLowerCase() : rightValue;
554
+ if (attribute[2] === "=" ? left !== right : !left.includes(right))
555
+ return false;
556
+ }
557
+ offset = close + 1;
558
+ }
559
+ return true;
560
+ };
561
+ const tokensFor = (branch) => {
562
+ const tokens = [];
563
+ let depth = 0;
564
+ let start = 0;
565
+ for (let index = 0; index <= branch.length; index += 1) {
566
+ const character = branch[index];
567
+ if (character === "[")
568
+ depth += 1;
569
+ if (character === "]")
570
+ depth -= 1;
571
+ if ((character === undefined || /\s/u.test(character)) && depth === 0) {
572
+ const token = branch.slice(start, index).trim();
573
+ if (token.length > 0)
574
+ tokens.push(token);
575
+ start = index + 1;
576
+ }
577
+ }
578
+ return tokens;
579
+ };
580
+ const selectorMatch = (element) => {
581
+ for (const rawBranch of selector.split(",")) {
582
+ const tokens = tokensFor(rawBranch.trim());
583
+ if (tokens.length === 0 || !simpleMatch(element, tokens[tokens.length - 1]))
584
+ continue;
585
+ let ancestor = element.parentNode;
586
+ let tokenIndex = tokens.length - 2;
587
+ while (tokenIndex >= 0) {
588
+ while (ancestor !== null
589
+ && (ancestor.nodeType !== 1 || !simpleMatch(ancestor, tokens[tokenIndex]))) {
590
+ ancestor = ancestor.parentNode;
591
+ }
592
+ if (ancestor === null)
593
+ break;
594
+ tokenIndex -= 1;
595
+ ancestor = ancestor.parentNode;
596
+ }
597
+ if (tokenIndex < 0)
598
+ return true;
599
+ }
600
+ return false;
601
+ };
602
+ let visited = 0;
603
+ const matches = [];
604
+ let current = root.firstChild;
605
+ while (current !== null) {
606
+ visited += 1;
607
+ if (visited > maxVisited)
608
+ throw new Error("probe limit exceeded");
609
+ if (current.nodeType === 1 && selectorMatch(current)) {
610
+ matches.push(current);
611
+ if (matches.length > maxMatched)
612
+ throw new Error("probe limit exceeded");
613
+ }
614
+ if (current.firstChild !== null) {
615
+ current = current.firstChild;
616
+ continue;
617
+ }
618
+ while (current !== null && current !== root && current.nextSibling === null)
619
+ current = current.parentNode;
620
+ current = current === null || current === root ? null : current.nextSibling;
621
+ }
622
+ return matches;
623
+ };
624
+ const boundedText = (node) => {
625
+ const chunks = [];
626
+ const ancestors = [];
627
+ let visited = 0;
628
+ let total = 0;
629
+ let current = node;
630
+ while (current !== null) {
631
+ visited += 1;
632
+ if (visited > 4096)
633
+ throw new Error("probe limit exceeded");
634
+ if (current.nodeType === 3) {
635
+ const value = current.nodeValue ?? "";
636
+ total += value.length;
637
+ if (total > MAX_PROBE_TEXT)
638
+ throw new Error("probe text limit exceeded");
639
+ if (value.length > 0)
640
+ chunks.push(value);
641
+ }
642
+ const child = current.firstChild;
643
+ if (child !== null) {
644
+ if (ancestors.length >= 4096)
645
+ throw new Error("probe limit exceeded");
646
+ ancestors.push(current);
647
+ current = child;
648
+ continue;
649
+ }
650
+ while (current !== null && current !== node && current.nextSibling === null)
651
+ current = ancestors.pop() ?? null;
652
+ if (current === node)
653
+ break;
654
+ if (current !== null)
655
+ current = current.nextSibling;
656
+ }
657
+ return chunks.join("").replace(/\s+/gu, " ").trim().normalize("NFC");
658
+ };
659
+ const boundedAttribute = (element, name) => {
660
+ const value = element.getAttribute(name) ?? "";
661
+ if (value.length > MAX_PROBE_TEXT)
662
+ throw new Error("probe text limit exceeded");
663
+ return value;
664
+ };
665
+ const unique = (values) => [...new Set(values)];
666
+ const visible = (element) => {
667
+ const html = element;
668
+ let ancestor = html;
669
+ for (let depth = 0; ancestor !== null && depth < 4096; depth += 1) {
670
+ if (ancestor.nodeType === 1) {
671
+ const candidate = ancestor;
672
+ if (candidate.hasAttribute("hidden") || candidate.hasAttribute("inert") || candidate.getAttribute("aria-hidden") === "true")
673
+ return false;
674
+ }
675
+ ancestor = ancestor.parentNode;
676
+ }
677
+ if (ancestor !== null)
678
+ throw new Error("probe limit exceeded");
679
+ const style = window.getComputedStyle(html);
680
+ if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0")
681
+ return false;
682
+ const rect = typeof html.getBoundingClientRect === "function" ? html.getBoundingClientRect() : undefined;
683
+ return rect === undefined || rect.width > 0 || rect.height > 0;
684
+ };
685
+ const semanticControl = (element) => {
686
+ const structural = [
687
+ boundedAttribute(element, "data-testid"),
688
+ boundedAttribute(element, "data-test-id"),
689
+ boundedAttribute(element, "data-action"),
690
+ boundedAttribute(element, "id"),
691
+ boundedAttribute(element, "class")
692
+ ].join(" ").toLocaleLowerCase();
693
+ if (/attach|upload|file|document|photo|image/u.test(structural))
694
+ return true;
695
+ const accessible = [
696
+ boundedAttribute(element, "aria-label"),
697
+ boundedAttribute(element, "title"),
698
+ boundedText(element)
699
+ ].join(" ").replace(/\s+/gu, " ").trim().toLocaleLowerCase();
700
+ return labels.some(label => accessible.includes(label.toLocaleLowerCase()));
701
+ };
702
+ const cssPath = (element) => {
703
+ const segments = [];
704
+ let current = element;
705
+ for (let depth = 0; current !== null && depth < 24; depth += 1) {
706
+ const id = current.getAttribute("id");
707
+ if (id !== null && /^[A-Za-z_][A-Za-z0-9_-]*$/u.test(id)) {
708
+ segments.unshift(`#${id}`);
709
+ break;
710
+ }
711
+ const parentNode = current.parentNode;
712
+ const parent = parentNode?.nodeType === 1 ? parentNode : null;
713
+ if (parent === null) {
714
+ segments.unshift(current.tagName.toLocaleLowerCase());
715
+ break;
716
+ }
717
+ let ordinal = 0;
718
+ let sibling = parent.firstChild;
719
+ while (sibling !== null) {
720
+ if (sibling.nodeType === 1 && sibling.tagName === current.tagName) {
721
+ ordinal += 1;
722
+ if (sibling === current)
723
+ break;
724
+ }
725
+ sibling = sibling.nextSibling;
726
+ }
727
+ if (ordinal === 0 || ordinal > MAX_PROBE_ITEMS)
728
+ throw new Error("probe limit exceeded");
729
+ segments.unshift(`${current.tagName.toLocaleLowerCase()}:nth-of-type(${ordinal})`);
730
+ current = parent;
731
+ }
732
+ return segments.join(" > ");
733
+ };
734
+ const textOf = (element) => [
735
+ boundedAttribute(element, "data-file-name"),
736
+ boundedAttribute(element, "data-filename"),
737
+ boundedAttribute(element, "aria-label"),
738
+ boundedAttribute(element, "title"),
739
+ boundedText(element)
740
+ ].join(" ").replace(/\s+/gu, " ").trim().normalize("NFC").slice(0, 512);
741
+ const parseBytes = (element, text) => {
742
+ const dataSize = element.getAttribute("data-file-size") ?? element.getAttribute("data-size");
743
+ if (dataSize !== null && /^\d+$/u.test(dataSize)) {
744
+ const value = Number(dataSize);
745
+ if (Number.isSafeInteger(value))
746
+ return value;
747
+ }
748
+ const match = /(?:^|[\s(])([0-9]+(?:\.[0-9]+)?)\s*(bytes?|B|KiB|KB|MiB|MB|GiB|GB)(?:$|[\s),])/iu.exec(text);
749
+ if (match === null)
750
+ return undefined;
751
+ const amount = Number(match[1]);
752
+ const unit = match[2]?.toLocaleLowerCase();
753
+ const multiplier = unit === "b" || unit === "byte" || unit === "bytes" ? 1
754
+ : unit === "kib" ? 1024
755
+ : unit === "kb" ? 1000
756
+ : unit === "mib" ? 1024 * 1024
757
+ : unit === "mb" ? 1000 * 1000
758
+ : unit === "gib" ? 1024 * 1024 * 1024
759
+ : unit === "gb" ? 1000 * 1000 * 1000
760
+ : undefined;
761
+ if (multiplier === undefined)
762
+ return undefined;
763
+ const value = amount * multiplier;
764
+ return Number.isSafeInteger(value) ? value : undefined;
765
+ };
766
+ const makeFact = (ordinal, name, bytes, orderKey) => {
767
+ const namePresent = name !== undefined && name.length > 0;
768
+ const sizePresent = bytes !== undefined;
769
+ if (expected.length === 0)
770
+ return { ordinal, namePresent, sizePresent, orderKey };
771
+ const nameMatches = namePresent
772
+ ? expected.filter(item => name.includes(item.displayName))
773
+ : [];
774
+ const nameMatch = nameMatches.length === 1;
775
+ const matched = nameMatch ? nameMatches[0] : undefined;
776
+ const bytesMatch = matched === undefined || !sizePresent ? undefined : bytes === matched.bytes;
777
+ const ambiguous = nameMatches.length > 1;
778
+ const matchOrdinal = matched === undefined || bytesMatch === false ? -1 : matched.ordinal;
779
+ return {
780
+ ordinal,
781
+ namePresent,
782
+ sizePresent,
783
+ nameMatch,
784
+ ...(bytesMatch === undefined ? {} : { bytesMatch }),
785
+ matchOrdinal,
786
+ ...(ambiguous ? { ambiguous: true } : {}),
787
+ orderKey
788
+ };
789
+ };
790
+ const inputFacts = (input) => {
791
+ const files = input.files;
792
+ if (files === null || files === undefined) {
793
+ // The Codex Chrome bridge deliberately omits FileList objects but keeps
794
+ // the standard path-redacted value surface. An exact empty string plus
795
+ // zero composer metadata is sufficient to prove the pre-handoff empty
796
+ // state; a non-empty or unreadable value remains ambiguous.
797
+ let value;
798
+ try {
799
+ value = input.value;
800
+ }
801
+ catch {
802
+ return { readable: false, facts: [] };
803
+ }
804
+ return { readable: typeof value === "string" && value.length === 0, facts: [] };
805
+ }
806
+ if (!Number.isSafeInteger(files.length) || files.length < 0 || files.length > MAX_PROBE_ITEMS) {
807
+ throw new Error("probe limit exceeded");
808
+ }
809
+ if (files.length === 0 && typeof input.value === "string" && input.value.length > 0) {
810
+ return { readable: false, facts: [] };
811
+ }
812
+ const facts = [];
813
+ for (let index = 0; index < files.length; index += 1) {
814
+ const file = files.item(index);
815
+ if (file === null)
816
+ return { readable: false, facts: [] };
817
+ const name = typeof file.name === "string" && file.name.length > 0 ? file.name.normalize("NFC") : undefined;
818
+ const bytes = Number.isSafeInteger(file.size) && file.size >= 0 ? file.size : undefined;
819
+ facts.push(makeFact(index, name, bytes, index));
820
+ }
821
+ return { readable: true, facts };
822
+ };
823
+ const metadataFacts = (root) => {
824
+ const selector = [
825
+ "[data-file-name]", "[data-filename]", "[data-file-size]", "[data-size]",
826
+ "[data-testid*='attachment' i]", "[data-testid*='file' i]",
827
+ "[aria-label*='attachment' i]", "[aria-label*='upload' i]", "[aria-label*='file' i]",
828
+ "[class*='attachment' i]", "[class*='upload' i]", "[class*='file' i]",
829
+ "[role='listitem']", "[role='progressbar']"
830
+ ].join(", ");
831
+ const explicitAttachment = (element) => element.hasAttribute("data-file-name") || element.hasAttribute("data-filename")
832
+ || element.hasAttribute("data-file-size") || element.hasAttribute("data-size")
833
+ || element.getAttribute("role") === "progressbar" || element.getAttribute("role") === "listitem"
834
+ || /(?:attachment|file)[-_]?(?:chip|tile|preview)/iu.test(boundedAttribute(element, "data-testid"));
835
+ const uploadControl = (element) => {
836
+ const control = element.tagName === "BUTTON" || element.tagName === "LABEL"
837
+ || element.getAttribute("role") === "button";
838
+ if (!control || explicitAttachment(element))
839
+ return false;
840
+ const label = boundedAttribute(element, "aria-label");
841
+ return element.tagName === "LABEL" || element.hasAttribute("aria-haspopup")
842
+ || element.getAttribute("id") === "composer-plus-btn"
843
+ || /^(?:add|upload|attach|choose|select|browse)\b/iu.test(label.trim())
844
+ || labels.some(candidate => label.toLocaleLowerCase() === candidate.toLocaleLowerCase());
845
+ };
846
+ const raw = unique(boundedQuery(root, selector)
847
+ .filter(visible)
848
+ .filter(element => {
849
+ if (element.tagName === "INPUT" || element.tagName === "TEXTAREA")
850
+ return false;
851
+ if (explicitAttachment(element))
852
+ return true;
853
+ // Menu openers and their icons are controls, not staged files. Real
854
+ // tiles/removal controls and progress indicators remain evidence.
855
+ let ancestor = element;
856
+ for (let depth = 0; ancestor !== null && ancestor !== root && depth < 4096; depth += 1) {
857
+ if (ancestor.nodeType === 1 && uploadControl(ancestor))
858
+ return false;
859
+ ancestor = ancestor.parentNode;
860
+ }
861
+ if (ancestor !== null && ancestor !== root)
862
+ throw new Error("probe limit exceeded");
863
+ return true;
864
+ }));
865
+ const rawSet = new Set(raw);
866
+ const nestedContainers = new Set();
867
+ for (const other of raw) {
868
+ let ancestorNode = other.parentNode;
869
+ let ancestor = ancestorNode?.nodeType === 1 ? ancestorNode : null;
870
+ let depth = 0;
871
+ while (ancestor !== null && depth < 4096) {
872
+ if (rawSet.has(ancestor)
873
+ && ancestor.getAttribute("data-file-name") === null
874
+ && ancestor.getAttribute("data-filename") === null) {
875
+ nestedContainers.add(ancestor);
876
+ }
877
+ ancestorNode = ancestor.parentNode;
878
+ ancestor = ancestorNode?.nodeType === 1 ? ancestorNode : null;
879
+ depth += 1;
880
+ }
881
+ if (ancestor !== null)
882
+ throw new Error("probe limit exceeded");
883
+ }
884
+ const nodes = raw.filter(candidate => !nestedContainers.has(candidate));
885
+ const facts = [];
886
+ for (let index = 0; index < nodes.length && index < 256; index += 1) {
887
+ const node = nodes[index];
888
+ const text = textOf(node);
889
+ const name = text.length > 0 ? text : undefined;
890
+ facts.push(makeFact(index, name, parseBytes(node, text), index));
891
+ }
892
+ return { facts, regionCount: nodes.length, orderDeterministic: nodes.length > 0 };
893
+ };
894
+ const textboxes = boundedQuery(document, "textarea, [contenteditable='true'], [role='textbox']").filter(visible);
895
+ const composerAncestor = (textbox) => {
896
+ let fallback = null;
897
+ let current = textbox;
898
+ for (let depth = 0; current !== null && depth < 4096; depth += 1) {
899
+ if (current.nodeType === 1) {
900
+ const element = current;
901
+ if (element.tagName === "FORM")
902
+ return element;
903
+ const testId = (element.getAttribute("data-testid") ?? "").toLocaleLowerCase();
904
+ const classTokens = (element.getAttribute("class") ?? "").toLocaleLowerCase().split(/\s+/u);
905
+ if (fallback === null && (testId.includes("composer")
906
+ || classTokens.includes("composer-parent")
907
+ || classTokens.includes("group/composer")))
908
+ fallback = element;
909
+ }
910
+ current = current.parentNode;
911
+ }
912
+ if (current !== null)
913
+ throw new Error("probe limit exceeded");
914
+ return fallback;
915
+ };
916
+ const roots = [...new Set(textboxes.map(composerAncestor).filter((root) => root !== null && visible(root)))];
917
+ if (roots.length !== 1) {
918
+ return {
919
+ status: roots.length === 0 ? "unavailable" : "ambiguous",
920
+ composerCount: roots.length,
921
+ fileInputCount: 0,
922
+ inputFilesReadable: false,
923
+ attachmentRegionCount: 0,
924
+ facts: [],
925
+ secondaryFacts: [],
926
+ factSource: "none",
927
+ orderDeterministic: false,
928
+ activationCandidateCount: 0
929
+ };
930
+ }
931
+ const root = roots[0];
932
+ const everyInput = boundedQuery(root, "input[type='file']");
933
+ const allInputs = everyInput.filter(input => !input.disabled && input.getAttribute("aria-disabled") !== "true");
934
+ const preferred = allInputs.filter(input => input.getAttribute("id") === "upload-files");
935
+ const nonImage = allInputs.filter(input => input.getAttribute("accept") !== "image/*");
936
+ const inputs = preferred.length > 0 ? preferred : nonImage.length > 0 ? nonImage : allInputs;
937
+ if (inputs.length !== 1) {
938
+ return {
939
+ status: "ambiguous",
940
+ composerCount: 1,
941
+ fileInputCount: allInputs.length,
942
+ inputFilesReadable: false,
943
+ attachmentRegionCount: 0,
944
+ facts: [],
945
+ secondaryFacts: [],
946
+ factSource: "none",
947
+ orderDeterministic: false,
948
+ activationCandidateCount: 0
949
+ };
950
+ }
951
+ const input = inputs[0];
952
+ const inputResult = inputFacts(input);
953
+ // ChatGPT may expose a general upload input alongside an image input. The
954
+ // selected input is unique, but absence also requires every alternate input
955
+ // to be readable and empty (including a disabled in-flight input).
956
+ const alternateInputsEmpty = everyInput.filter(candidate => candidate !== input).every(candidate => {
957
+ const result = inputFacts(candidate);
958
+ return result.readable && result.facts.length === 0;
959
+ });
960
+ const metadataResult = metadataFacts(root);
961
+ const inputPrimary = inputResult.readable && inputResult.facts.length > 0;
962
+ const facts = inputPrimary ? inputResult.facts : metadataResult.facts;
963
+ const secondaryFacts = inputPrimary ? metadataResult.facts : [];
964
+ const factSource = inputResult.readable
965
+ ? inputPrimary
966
+ ? metadataResult.facts.length > 0 ? "mixed" : "input"
967
+ : metadataResult.facts.length > 0 ? "metadata" : "none"
968
+ : metadataResult.facts.length > 0 ? "metadata" : "none";
969
+ const attachmentRegionCount = Math.max(metadataResult.regionCount, inputResult.facts.length);
970
+ const observation = {
971
+ status: alternateInputsEmpty ? "ready" : "ambiguous",
972
+ composerCount: 1,
973
+ fileInputCount: inputs.length,
974
+ inputFilesReadable: inputResult.readable,
975
+ attachmentRegionCount,
976
+ facts,
977
+ secondaryFacts,
978
+ factSource,
979
+ orderDeterministic: inputResult.readable || metadataResult.orderDeterministic,
980
+ activationCandidateCount: 0
981
+ };
982
+ if (!alternateInputsEmpty || record.observationOnly === true)
983
+ return observation;
984
+ const controls = boundedQuery(root, "label, button, [role='button'], [role='menuitem']").filter(visible);
985
+ const contains = (container, candidate) => {
986
+ let current = candidate;
987
+ for (let depth = 0; current !== null && depth < 4096; depth += 1) {
988
+ if (current === container)
989
+ return true;
990
+ current = current.parentNode;
991
+ }
992
+ if (current !== null)
993
+ throw new Error("probe limit exceeded");
994
+ return false;
995
+ };
996
+ const directCandidates = unique(controls.filter(control => {
997
+ const inputId = input.getAttribute("id") ?? "";
998
+ if (control.getAttribute("aria-haspopup") === "menu" && !contains(control, input))
999
+ return false;
1000
+ if (control === input || contains(control, input))
1001
+ return true;
1002
+ if (inputId.length > 0 && (control.getAttribute("for") === inputId || control.getAttribute("aria-controls") === inputId))
1003
+ return true;
1004
+ const inputRef = control.getAttribute("data-input-id") ?? control.getAttribute("data-file-input");
1005
+ return inputId.length > 0 && inputRef === inputId || semanticControl(control);
1006
+ }));
1007
+ const menuRootItems = boundedQuery(document, "[role='menu'] [role='menuitem']").filter(visible);
1008
+ const menuFallbackItems = unique(boundedQuery(document, "[role='menu'] div[tabindex='0'], [role='group'] div[tabindex='0'], [class*='popover' i] div[tabindex='0']")
1009
+ .filter(visible));
1010
+ const menuItems = (menuRootItems.length > 0 ? menuRootItems : menuFallbackItems).filter(item => {
1011
+ const inputId = input.getAttribute("id") ?? "";
1012
+ if (inputId.length > 0 && item.getAttribute("aria-controls") === inputId)
1013
+ return true;
1014
+ return semanticControl(item);
1015
+ });
1016
+ const menuOpeners = unique(boundedQuery(root, "button, [role='button']")
1017
+ .filter(visible)
1018
+ .filter(control => control.getAttribute("aria-haspopup") === "menu"
1019
+ && (semanticControl(control) || control.getAttribute("data-testid") !== null)));
1020
+ const directActivationSelector = directCandidates.length === 1 ? cssPath(directCandidates[0]) : undefined;
1021
+ const menuUploadSelector = menuItems.length === 1 ? cssPath(menuItems[0]) : undefined;
1022
+ const menuOpenerSelector = menuOpeners.length === 1 ? cssPath(menuOpeners[0]) : undefined;
1023
+ const candidateCount = directCandidates.length > 0
1024
+ ? directCandidates.length
1025
+ : menuItems.length > 0 ? menuItems.length
1026
+ : menuOpenerSelector !== undefined ? 1
1027
+ : 0;
1028
+ return {
1029
+ // Composer/input identity and activation identity are separate contracts.
1030
+ // Once a file is attached ChatGPT legitimately adds tile/remove controls,
1031
+ // so candidateCount can exceed one while the attachment surface remains
1032
+ // exact. Mutation paths validate activationCandidateCount independently.
1033
+ ...observation,
1034
+ ...(directActivationSelector === undefined ? {} : { directActivationSelector }),
1035
+ ...(menuOpenerSelector === undefined ? {} : { menuOpenerSelector }),
1036
+ ...(menuUploadSelector === undefined ? {} : { menuUploadSelector }),
1037
+ activationCandidateCount: candidateCount
1038
+ };
1039
+ }
1040
+ function normalizeProbe(raw) {
1041
+ if (!isDataRecord(raw))
1042
+ return undefined;
1043
+ if (!hasExactKeys(raw, [
1044
+ "status",
1045
+ "composerCount",
1046
+ "fileInputCount",
1047
+ "inputFilesReadable",
1048
+ "attachmentRegionCount",
1049
+ "facts",
1050
+ "secondaryFacts",
1051
+ "factSource",
1052
+ "orderDeterministic",
1053
+ "directActivationSelector",
1054
+ "menuOpenerSelector",
1055
+ "menuUploadSelector",
1056
+ "activationCandidateCount"
1057
+ ]))
1058
+ return undefined;
1059
+ const status = readOwn(raw, "status");
1060
+ const composerCount = readOwn(raw, "composerCount");
1061
+ const fileInputCount = readOwn(raw, "fileInputCount");
1062
+ const inputFilesReadable = readOwn(raw, "inputFilesReadable");
1063
+ const attachmentRegionCount = readOwn(raw, "attachmentRegionCount");
1064
+ const facts = readOwn(raw, "facts");
1065
+ const secondaryFacts = readOwn(raw, "secondaryFacts");
1066
+ const factSource = readOwn(raw, "factSource");
1067
+ const orderDeterministic = readOwn(raw, "orderDeterministic");
1068
+ const directActivationSelector = readOwn(raw, "directActivationSelector");
1069
+ const menuOpenerSelector = readOwn(raw, "menuOpenerSelector");
1070
+ const menuUploadSelector = readOwn(raw, "menuUploadSelector");
1071
+ const activationCandidateCount = readOwn(raw, "activationCandidateCount");
1072
+ if ((status !== "ready" && status !== "ambiguous" && status !== "unavailable")
1073
+ || !isBoundedCount(composerCount) || !isBoundedCount(fileInputCount)
1074
+ || typeof inputFilesReadable !== "boolean" || !isBoundedCount(attachmentRegionCount)
1075
+ || !Array.isArray(facts) || facts.length > MAX_PROBE_ITEMS || !hasSafeArrayDescriptors(facts)
1076
+ || !Array.isArray(secondaryFacts) || secondaryFacts.length > MAX_PROBE_ITEMS || !hasSafeArrayDescriptors(secondaryFacts)
1077
+ || (factSource !== "input" && factSource !== "metadata" && factSource !== "none" && factSource !== "mixed")
1078
+ || typeof orderDeterministic !== "boolean" || !isBoundedCount(activationCandidateCount))
1079
+ return undefined;
1080
+ const normalizeFacts = (rawFacts) => {
1081
+ const normalizedFacts = [];
1082
+ for (let index = 0; index < rawFacts.length; index += 1) {
1083
+ const fact = rawFacts[index];
1084
+ if (!isDataRecord(fact) || !hasExactKeys(fact, [
1085
+ "ordinal",
1086
+ "namePresent",
1087
+ "sizePresent",
1088
+ "nameMatch",
1089
+ "bytesMatch",
1090
+ "matchOrdinal",
1091
+ "ambiguous",
1092
+ "orderKey"
1093
+ ]))
1094
+ return undefined;
1095
+ const ordinal = readOwn(fact, "ordinal");
1096
+ const namePresent = readOwn(fact, "namePresent");
1097
+ const sizePresent = readOwn(fact, "sizePresent");
1098
+ const nameMatch = readOwn(fact, "nameMatch");
1099
+ const bytesMatch = readOwn(fact, "bytesMatch");
1100
+ const matchOrdinal = readOwn(fact, "matchOrdinal");
1101
+ const ambiguous = readOwn(fact, "ambiguous");
1102
+ const orderKey = readOwn(fact, "orderKey");
1103
+ if (ordinal !== index || typeof namePresent !== "boolean" || typeof sizePresent !== "boolean"
1104
+ || (nameMatch !== undefined && typeof nameMatch !== "boolean")
1105
+ || (bytesMatch !== undefined && typeof bytesMatch !== "boolean")
1106
+ || (matchOrdinal !== undefined && (!Number.isSafeInteger(matchOrdinal)
1107
+ || matchOrdinal < -1 || matchOrdinal > MAX_PROBE_ITEMS))
1108
+ || (ambiguous !== undefined && typeof ambiguous !== "boolean")
1109
+ || (orderKey !== undefined && !isBoundedCount(orderKey)))
1110
+ return undefined;
1111
+ normalizedFacts.push(Object.freeze({
1112
+ ordinal,
1113
+ namePresent,
1114
+ sizePresent,
1115
+ ...(nameMatch === undefined ? {} : { nameMatch }),
1116
+ ...(bytesMatch === undefined ? {} : { bytesMatch }),
1117
+ ...(matchOrdinal === undefined ? {} : { matchOrdinal }),
1118
+ ...(ambiguous === undefined ? {} : { ambiguous }),
1119
+ ...(orderKey === undefined ? {} : { orderKey })
1120
+ }));
1121
+ }
1122
+ return Object.freeze(normalizedFacts);
1123
+ };
1124
+ const normalizedFacts = normalizeFacts(facts);
1125
+ const normalizedSecondaryFacts = normalizeFacts(secondaryFacts);
1126
+ if (normalizedFacts === undefined || normalizedSecondaryFacts === undefined)
1127
+ return undefined;
1128
+ for (const selector of [directActivationSelector, menuOpenerSelector, menuUploadSelector]) {
1129
+ if (selector !== undefined && (!SELECTOR_PATTERN.test(selector) || selector.length > 4096))
1130
+ return undefined;
1131
+ }
1132
+ return Object.freeze({
1133
+ status,
1134
+ composerCount,
1135
+ fileInputCount,
1136
+ inputFilesReadable,
1137
+ attachmentRegionCount,
1138
+ facts: normalizedFacts,
1139
+ secondaryFacts: normalizedSecondaryFacts,
1140
+ factSource,
1141
+ orderDeterministic,
1142
+ ...(directActivationSelector === undefined ? {} : { directActivationSelector }),
1143
+ ...(menuOpenerSelector === undefined ? {} : { menuOpenerSelector }),
1144
+ ...(menuUploadSelector === undefined ? {} : { menuUploadSelector }),
1145
+ activationCandidateCount
1146
+ });
1147
+ }
1148
+ function compareCausalSurface(current, causal, request) {
1149
+ // A DOM filename/size is never a content-SHA proof. Exact identity is
1150
+ // justified only by this capability's own settled chooser handoff, the
1151
+ // frozen manifest selected for that handoff, and every exposed UI fact
1152
+ // matching that causal manifest in bounded ordinal order.
1153
+ const expectedCount = request.manifest.count;
1154
+ const multiplicityMatch = current.facts.length === expectedCount
1155
+ && current.attachmentRegionCount === expectedCount;
1156
+ const expectedFiles = causalManifestFiles(causal, request);
1157
+ if (expectedFiles === undefined || expectedFiles.length !== expectedCount) {
1158
+ return {
1159
+ status: "mismatch",
1160
+ factsMatch: false,
1161
+ multiplicityMatch,
1162
+ duplicateNames: duplicateNames(current.facts) || duplicateNames(current.secondaryFacts)
1163
+ };
1164
+ }
1165
+ const primary = compareFactList(current.facts, expectedCount);
1166
+ const secondary = current.secondaryFacts.length === 0
1167
+ ? { factsMatch: true, ambiguous: false, duplicateNames: false }
1168
+ : compareFactList(current.secondaryFacts, expectedCount);
1169
+ const duplicates = primary.duplicateNames || secondary.duplicateNames;
1170
+ if (!multiplicityMatch) {
1171
+ return { status: "mismatch", factsMatch: false, multiplicityMatch, duplicateNames: duplicates };
1172
+ }
1173
+ if (!current.orderDeterministic || primary.ambiguous || secondary.ambiguous) {
1174
+ return {
1175
+ status: "ambiguous",
1176
+ factsMatch: false,
1177
+ multiplicityMatch,
1178
+ duplicateNames: duplicates
1179
+ };
1180
+ }
1181
+ const factsMatch = primary.factsMatch && secondary.factsMatch;
1182
+ return {
1183
+ status: factsMatch ? "exact" : "mismatch",
1184
+ factsMatch,
1185
+ multiplicityMatch,
1186
+ duplicateNames: duplicates
1187
+ };
1188
+ }
1189
+ function compareFactList(facts, expectedCount) {
1190
+ if (facts.length !== expectedCount) {
1191
+ return { factsMatch: false, ambiguous: false, duplicateNames: duplicateNames(facts) };
1192
+ }
1193
+ const matchedOrdinals = [];
1194
+ let factsMatch = true;
1195
+ let ambiguous = false;
1196
+ for (let index = 0; index < facts.length; index += 1) {
1197
+ const fact = facts[index];
1198
+ if (fact.ambiguous === true)
1199
+ ambiguous = true;
1200
+ if (!fact.namePresent || fact.nameMatch !== true || fact.matchOrdinal !== index)
1201
+ factsMatch = false;
1202
+ if (fact.sizePresent && fact.bytesMatch !== true)
1203
+ factsMatch = false;
1204
+ if (fact.orderKey !== undefined && fact.orderKey !== index)
1205
+ factsMatch = false;
1206
+ if (fact.matchOrdinal !== undefined && fact.matchOrdinal >= 0)
1207
+ matchedOrdinals.push(fact.matchOrdinal);
1208
+ }
1209
+ const duplicate = duplicateNames(facts)
1210
+ || matchedOrdinals.length !== new Set(matchedOrdinals).size;
1211
+ if (duplicate)
1212
+ ambiguous = true;
1213
+ return { factsMatch, ambiguous, duplicateNames: duplicate };
1214
+ }
1215
+ function causalManifestFiles(causal, request) {
1216
+ if (causal.manifest.count !== request.manifest.count
1217
+ || causal.manifest.identities.length !== request.manifest.identities.length)
1218
+ return undefined;
1219
+ const files = causal.manifest.identities.map(entry => {
1220
+ const requestEntry = request.manifest.identities[entry.ordinal];
1221
+ return requestEntry?.identityDigest === entry.identityDigest
1222
+ ? causal.manifestFacts[entry.ordinal]
1223
+ : undefined;
1224
+ });
1225
+ if (files.some(entry => entry === undefined))
1226
+ return undefined;
1227
+ return files;
1228
+ }
1229
+ async function evidenceStatus(evidenceDigest, baseMaterial, status, count) {
1230
+ const evidence = await safeEvidence(evidenceDigest, "chatgpt-attachment-surface", {
1231
+ ...baseMaterial,
1232
+ status,
1233
+ count
1234
+ });
1235
+ return evidence === undefined
1236
+ ? { status, source: "live_surface" }
1237
+ : { status, source: "live_surface", providerEvidenceDigest: evidence };
1238
+ }
1239
+ function surfaceEvidenceMaterial(request, target, current) {
1240
+ return {
1241
+ schemaVersion: CHATGPT_ATTACHMENT_PROVIDER_SCHEMA_VERSION,
1242
+ operationId: request.operationId,
1243
+ requestDigest: request.requestDigest,
1244
+ targetBindingDigest: request.targetBindingDigest,
1245
+ providerId: target.providerId,
1246
+ browserId: target.browserId,
1247
+ tabId: target.tabId,
1248
+ composerCount: current.composerCount,
1249
+ fileInputCount: current.fileInputCount,
1250
+ factSource: current.factSource,
1251
+ orderDeterministic: current.orderDeterministic
1252
+ };
1253
+ }
1254
+ function sameCausalRequest(causal, request, target) {
1255
+ return causal.operationId === request.operationId
1256
+ && causal.requestDigest === request.requestDigest
1257
+ && causal.targetBindingDigest === request.targetBindingDigest
1258
+ && causal.manifest.count === request.manifest.count
1259
+ && causal.manifest.identities.every((entry, index) => {
1260
+ const current = request.manifest.identities[index];
1261
+ return current?.ordinal === entry.ordinal && current.identityDigest === entry.identityDigest;
1262
+ })
1263
+ && causal.target.providerId === target.providerId
1264
+ && causal.target.browserId === target.browserId
1265
+ && causal.target.tabId === target.tabId;
1266
+ }
1267
+ function duplicateNames(facts) {
1268
+ const matched = facts.flatMap(fact => fact.matchOrdinal === undefined || fact.matchOrdinal < 0
1269
+ ? []
1270
+ : [fact.matchOrdinal]);
1271
+ return facts.some(fact => fact.ambiguous === true)
1272
+ || matched.length !== new Set(matched).size;
1273
+ }
1274
+ function locatorFor(page, selector) {
1275
+ const locator = safeMethod(page, "locator");
1276
+ if (locator === undefined)
1277
+ return undefined;
1278
+ try {
1279
+ const value = locator.call(page, selector);
1280
+ return isSafeProviderObject(value) ? value : undefined;
1281
+ }
1282
+ catch {
1283
+ return undefined;
1284
+ }
1285
+ }
1286
+ async function resolveCdpSend(page, timeoutMs) {
1287
+ const capabilities = readOwn(page, "capabilities");
1288
+ if (!isSafeProviderObject(capabilities))
1289
+ return undefined;
1290
+ const get = providerCallable(capabilities, "get");
1291
+ if (get === undefined)
1292
+ return undefined;
1293
+ try {
1294
+ const pending = get("cdp");
1295
+ const capability = isNativePromise(pending) ? await boundedNative(pending, timeoutMs) : pending;
1296
+ if (!isSafeProviderObject(capability))
1297
+ return undefined;
1298
+ return providerCallable(capability, "send");
1299
+ }
1300
+ catch {
1301
+ return undefined;
1302
+ }
1303
+ }
1304
+ function cdpActivationAccepted(evaluation) {
1305
+ if (!isPlainDataRecord(evaluation))
1306
+ return false;
1307
+ const result = readOwn(evaluation, "result");
1308
+ const wrapped = isPlainDataRecord(result) ? readOwn(result, "value") : undefined;
1309
+ const value = wrapped ?? evaluation;
1310
+ return isPlainDataRecord(value) && readOwn(value, "ok") === true;
1311
+ }
1312
+ function providerCallable(value, key) {
1313
+ try {
1314
+ const candidate = Reflect.get(value, key, value);
1315
+ if (typeof candidate !== "function")
1316
+ return undefined;
1317
+ return (...args) => Reflect.apply(candidate, value, args);
1318
+ }
1319
+ catch {
1320
+ return undefined;
1321
+ }
1322
+ }
1323
+ async function safeEvidence(evidenceDigest, domain, material) {
1324
+ try {
1325
+ const value = await evidenceDigest(domain, material);
1326
+ return typeof value === "string" && DIGEST_PATTERN.test(value) ? value : undefined;
1327
+ }
1328
+ catch {
1329
+ return undefined;
1330
+ }
1331
+ }
1332
+ async function boundedNative(value, timeoutMs) {
1333
+ if (!isNativePromise(value)) {
1334
+ if (value !== null && typeof value === "object")
1335
+ throw new Error("provider callback promise is not native");
1336
+ return value;
1337
+ }
1338
+ return await new Promise((resolve, reject) => {
1339
+ const timer = setTimeout(() => reject(new Error("provider callback timed out")), timeoutMs);
1340
+ value.then(result => {
1341
+ clearTimeout(timer);
1342
+ resolve(result);
1343
+ }, error => {
1344
+ clearTimeout(timer);
1345
+ reject(error);
1346
+ });
1347
+ });
1348
+ }
1349
+ async function awaitMutating(value) {
1350
+ if (isNativePromise(value))
1351
+ return await value;
1352
+ if (value !== null && typeof value === "object")
1353
+ throw new Error("provider mutation promise is not native");
1354
+ return value;
1355
+ }
1356
+ function isNativePromise(value) {
1357
+ try {
1358
+ return nodeTypes.isPromise(value);
1359
+ }
1360
+ catch {
1361
+ return false;
1362
+ }
1363
+ }
1364
+ function safeMethod(value, key) {
1365
+ let current = value;
1366
+ for (let depth = 0; current !== null && depth < 12; depth += 1) {
1367
+ let descriptor;
1368
+ try {
1369
+ descriptor = Object.getOwnPropertyDescriptor(current, key);
1370
+ }
1371
+ catch {
1372
+ return undefined;
1373
+ }
1374
+ if (descriptor !== undefined) {
1375
+ return !descriptor.get && !descriptor.set && "value" in descriptor && typeof descriptor.value === "function"
1376
+ ? descriptor.value
1377
+ : undefined;
1378
+ }
1379
+ try {
1380
+ current = Object.getPrototypeOf(current);
1381
+ }
1382
+ catch {
1383
+ return undefined;
1384
+ }
1385
+ }
1386
+ return undefined;
1387
+ }
1388
+ function isSafeProviderObject(value) {
1389
+ if (value === null || (typeof value !== "object" && typeof value !== "function"))
1390
+ return false;
1391
+ try {
1392
+ const descriptors = Object.getOwnPropertyDescriptors(value);
1393
+ for (const key of Reflect.ownKeys(descriptors)) {
1394
+ if (typeof key !== "string")
1395
+ return false;
1396
+ const descriptor = descriptors[key];
1397
+ if (descriptor === undefined || descriptor.get !== undefined || descriptor.set !== undefined)
1398
+ return false;
1399
+ }
1400
+ return true;
1401
+ }
1402
+ catch {
1403
+ return false;
1404
+ }
1405
+ }
1406
+ function isDataRecord(value) {
1407
+ if (!isPlainDataRecord(value))
1408
+ return false;
1409
+ try {
1410
+ return Reflect.ownKeys(Object.getOwnPropertyDescriptors(value)).every(key => typeof key === "string");
1411
+ }
1412
+ catch {
1413
+ return false;
1414
+ }
1415
+ }
1416
+ function readOwn(value, key) {
1417
+ try {
1418
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
1419
+ return descriptor !== undefined && "value" in descriptor && descriptor.get === undefined && descriptor.set === undefined
1420
+ ? descriptor.value
1421
+ : undefined;
1422
+ }
1423
+ catch {
1424
+ return undefined;
1425
+ }
1426
+ }
1427
+ function assertOwnDataKeys(value, keys) {
1428
+ let descriptors;
1429
+ try {
1430
+ descriptors = Object.getOwnPropertyDescriptors(value);
1431
+ }
1432
+ catch {
1433
+ throw new Error("invalid ChatGPT attachment provider options");
1434
+ }
1435
+ const allowed = new Set(keys);
1436
+ for (const key of Reflect.ownKeys(descriptors)) {
1437
+ if (typeof key !== "string" || !allowed.has(key))
1438
+ throw new Error("invalid ChatGPT attachment provider options");
1439
+ const descriptor = descriptors[key];
1440
+ if (descriptor === undefined || !("value" in descriptor) || descriptor.get !== undefined || descriptor.set !== undefined) {
1441
+ throw new Error("invalid ChatGPT attachment provider options");
1442
+ }
1443
+ }
1444
+ }
1445
+ function hasExactKeys(value, keys) {
1446
+ try {
1447
+ const allowed = new Set(keys);
1448
+ const descriptors = Object.getOwnPropertyDescriptors(value);
1449
+ for (const key of Reflect.ownKeys(descriptors)) {
1450
+ if (typeof key !== "string" || !allowed.has(key))
1451
+ return false;
1452
+ const descriptor = descriptors[key];
1453
+ if (descriptor === undefined || !("value" in descriptor) || descriptor.get !== undefined || descriptor.set !== undefined)
1454
+ return false;
1455
+ }
1456
+ return true;
1457
+ }
1458
+ catch {
1459
+ return false;
1460
+ }
1461
+ }
1462
+ function isSafeTarget(value) {
1463
+ if (!isDataRecord(value))
1464
+ return false;
1465
+ const providerId = readOwn(value, "providerId");
1466
+ const browserId = readOwn(value, "browserId");
1467
+ const tabId = readOwn(value, "tabId");
1468
+ const scope = readOwn(value, "coordinationScope");
1469
+ return typeof providerId === "string" && ID_PATTERN.test(providerId)
1470
+ && typeof browserId === "string" && ID_PATTERN.test(browserId)
1471
+ && typeof tabId === "string" && ID_PATTERN.test(tabId)
1472
+ && (scope === "process" || scope === "provider");
1473
+ }
1474
+ function snapshotHandoffRequest(value) {
1475
+ if (!isDataRecord(value))
1476
+ return undefined;
1477
+ const operationId = readOwn(value, "operationId");
1478
+ const requestDigest = readOwn(value, "requestDigest");
1479
+ const surface = readOwn(value, "surface");
1480
+ const actionId = readOwn(value, "actionId");
1481
+ const targetBindingDigest = readOwn(value, "targetBindingDigest");
1482
+ const rawManifest = readOwn(value, "manifest");
1483
+ const manifest = snapshotHandoffManifest(rawManifest);
1484
+ if (typeof operationId !== "string" || !ID_PATTERN.test(operationId)
1485
+ || typeof requestDigest !== "string" || !DIGEST_PATTERN.test(requestDigest)
1486
+ || (surface !== "chat" && surface !== "work")
1487
+ || typeof actionId !== "string" || !ID_PATTERN.test(actionId)
1488
+ || typeof targetBindingDigest !== "string" || !DIGEST_PATTERN.test(targetBindingDigest)
1489
+ || manifest === undefined)
1490
+ return undefined;
1491
+ return Object.freeze({ operationId, requestDigest, surface, actionId, targetBindingDigest, manifest });
1492
+ }
1493
+ function snapshotHandoffManifest(value) {
1494
+ if (!isDataRecord(value) || !hasExactKeys(value, ["count", "orderPolicy", "identities"]))
1495
+ return undefined;
1496
+ const count = readOwn(value, "count");
1497
+ const orderPolicy = readOwn(value, "orderPolicy");
1498
+ const identities = readOwn(value, "identities");
1499
+ if (!isBoundedCount(count) || orderPolicy !== "exact" || !Array.isArray(identities)
1500
+ || identities.length !== count || !hasSafeArrayDescriptors(identities))
1501
+ return undefined;
1502
+ const result = [];
1503
+ const seen = new Set();
1504
+ for (let index = 0; index < identities.length; index += 1) {
1505
+ const entry = identities[index];
1506
+ if (!isDataRecord(entry) || !hasExactKeys(entry, ["identityDigest", "ordinal"]))
1507
+ return undefined;
1508
+ const identityDigest = readOwn(entry, "identityDigest");
1509
+ const ordinal = readOwn(entry, "ordinal");
1510
+ if (typeof identityDigest !== "string" || !DIGEST_PATTERN.test(identityDigest)
1511
+ || ordinal !== index || seen.has(identityDigest))
1512
+ return undefined;
1513
+ seen.add(identityDigest);
1514
+ result.push(Object.freeze({ identityDigest, ordinal }));
1515
+ }
1516
+ return Object.freeze({ count, orderPolicy: "exact", identities: Object.freeze(result) });
1517
+ }
1518
+ function snapshotTargetBinding(value) {
1519
+ if (!isSafeTarget(value))
1520
+ return undefined;
1521
+ try {
1522
+ return Object.freeze({ ...value });
1523
+ }
1524
+ catch {
1525
+ return undefined;
1526
+ }
1527
+ }
1528
+ function isAbortSignal(value) {
1529
+ if (typeof AbortSignal === "undefined" || !(value instanceof AbortSignal))
1530
+ return false;
1531
+ try {
1532
+ return typeof value.aborted === "boolean" && typeof value.addEventListener === "function";
1533
+ }
1534
+ catch {
1535
+ return false;
1536
+ }
1537
+ }
1538
+ function isAnyAbortRequested(providerSignal, requestSignal, requestDeadlineAt) {
1539
+ return providerSignal?.aborted === true
1540
+ || requestSignal?.aborted === true
1541
+ || requestDeadlineAt !== undefined && Date.now() >= requestDeadlineAt;
1542
+ }
1543
+ function boundedProbeTimeout(timeoutMs, requestDeadlineAt) {
1544
+ if (requestDeadlineAt === undefined)
1545
+ return timeoutMs;
1546
+ return Math.max(0, Math.min(timeoutMs, requestDeadlineAt - Date.now()));
1547
+ }
1548
+ /**
1549
+ * Snapshot the complete request-local file identity graph before any provider
1550
+ * callback is installed. A shallow array copy is not sufficient: callers
1551
+ * can otherwise mutate `sourcePath`, manifest facts, or inode proof after the
1552
+ * factory returns and make the later handoff/evidence refer to different
1553
+ * inputs than the journaled request.
1554
+ */
1555
+ function snapshotFileIdentities(value) {
1556
+ if (!Array.isArray(value) || value.length > MAX_PROBE_ITEMS || !hasSafeArrayDescriptors(value)) {
1557
+ throw new Error("invalid ChatGPT attachment provider options");
1558
+ }
1559
+ const result = [];
1560
+ for (let index = 0; index < value.length; index += 1) {
1561
+ const source = value[index];
1562
+ if (!isDataRecord(source) || !hasExactKeys(source, ["sourcePath", "manifest", "proof"])) {
1563
+ throw new Error("invalid ChatGPT attachment provider options");
1564
+ }
1565
+ const sourcePath = readOwn(source, "sourcePath");
1566
+ const manifest = readOwn(source, "manifest");
1567
+ const proof = readOwn(source, "proof");
1568
+ if (typeof sourcePath !== "string" || sourcePath.length === 0 || sourcePath.length > 4096
1569
+ || /[\u0000-\u001f\u007f]/u.test(sourcePath)) {
1570
+ throw new Error("invalid ChatGPT attachment provider options");
1571
+ }
1572
+ const manifestSnapshot = snapshotManifest(manifest);
1573
+ const proofSnapshot = snapshotProof(proof);
1574
+ if (manifestSnapshot === undefined || proofSnapshot === undefined
1575
+ || proofSnapshot.size !== String(manifestSnapshot.bytes)) {
1576
+ throw new Error("invalid ChatGPT attachment provider options");
1577
+ }
1578
+ result.push(Object.freeze({
1579
+ sourcePath,
1580
+ manifest: manifestSnapshot,
1581
+ proof: proofSnapshot
1582
+ }));
1583
+ }
1584
+ return Object.freeze(result);
1585
+ }
1586
+ function snapshotManifest(value) {
1587
+ if (!isDataRecord(value) || !hasExactKeys(value, ["displayName", "bytes", "contentSha256"]))
1588
+ return undefined;
1589
+ const displayName = readOwn(value, "displayName");
1590
+ const bytes = readOwn(value, "bytes");
1591
+ const contentSha256 = readOwn(value, "contentSha256");
1592
+ if (displayName === undefined || !safeDisplayName(displayName)
1593
+ || bytes === undefined || !isBoundedBytes(bytes)
1594
+ || contentSha256 === undefined || !/^[0-9a-f]{64}$/u.test(contentSha256))
1595
+ return undefined;
1596
+ return Object.freeze({ displayName, bytes, contentSha256 });
1597
+ }
1598
+ function snapshotProof(value) {
1599
+ if (!isDataRecord(value) || !hasExactKeys(value, ["device", "inode", "size", "modifiedNs", "changedNs"]))
1600
+ return undefined;
1601
+ const device = readOwn(value, "device");
1602
+ const inode = readOwn(value, "inode");
1603
+ const size = readOwn(value, "size");
1604
+ const modifiedNs = readOwn(value, "modifiedNs");
1605
+ const changedNs = readOwn(value, "changedNs");
1606
+ if (![device, inode, size, modifiedNs, changedNs].every(item => typeof item === "string"
1607
+ && item.length > 0 && item.length <= 256 && /^\d+$/u.test(item))) {
1608
+ return undefined;
1609
+ }
1610
+ return Object.freeze({ device, inode, size, modifiedNs, changedNs });
1611
+ }
1612
+ function hasSafeArrayDescriptors(value) {
1613
+ try {
1614
+ const descriptors = Object.getOwnPropertyDescriptors(value);
1615
+ for (const key of Reflect.ownKeys(descriptors)) {
1616
+ if (typeof key !== "string" || (key !== "length" && !/^\d+$/u.test(key)))
1617
+ return false;
1618
+ const descriptor = descriptors[key];
1619
+ if (descriptor === undefined || !("value" in descriptor) || descriptor.get !== undefined || descriptor.set !== undefined)
1620
+ return false;
1621
+ if (key !== "length" && Number(key) >= value.length)
1622
+ return false;
1623
+ }
1624
+ const length = Object.getOwnPropertyDescriptor(value, "length")?.value;
1625
+ return length === value.length && Number.isSafeInteger(length) && length >= 0;
1626
+ }
1627
+ catch {
1628
+ return false;
1629
+ }
1630
+ }
1631
+ function isBoundedCount(value) {
1632
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 && value <= MAX_PROBE_ITEMS;
1633
+ }
1634
+ function isBoundedBytes(value) {
1635
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 && value <= Number.MAX_SAFE_INTEGER;
1636
+ }
1637
+ function safeDisplayName(value) {
1638
+ return value.length > 0 && value.length <= MAX_PROBE_TEXT && !/[\\/\u0000-\u001f\u007f]/u.test(value);
1639
+ }