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
@@ -1,31 +1,111 @@
1
- import { createInterface } from "node:readline";
1
+ import { TextDecoder } from "node:util";
2
2
  import { BackendSession } from "./session.js";
3
- import { backendResponseError, parseBackendRequest, ProtocolError } from "./protocol.js";
3
+ import { backendResponseError, BACKEND_NDJSON_FRAME_LIMIT_BYTES, isValidBackendRequestId, parseBackendRequest, ProtocolError } from "./protocol.js";
4
+ const DEFAULT_BACKEND_SERVER_MAX_IN_FLIGHT = 256;
5
+ const MAX_BACKEND_SERVER_IN_FLIGHT = 10_000;
4
6
  export async function runBackendStdioServer(options) {
5
- const session = options.session ?? new BackendSession();
6
- const lines = createInterface({
7
- input: options.input,
8
- crlfDelay: Infinity
7
+ validateServerOptions(options);
8
+ const session = options.session ?? new BackendSession(options.backendIdentity === undefined ? {} : { backendIdentity: options.backendIdentity });
9
+ const frameLimitBytes = options.frameLimitBytes ?? BACKEND_NDJSON_FRAME_LIMIT_BYTES;
10
+ const maxInFlight = options.maxInFlight ?? DEFAULT_BACKEND_SERVER_MAX_IN_FLIGHT;
11
+ let outputUnavailable = false;
12
+ let correlationViolation = false;
13
+ const writeJson = createJsonLineWriter(options.output, frameLimitBytes, () => {
14
+ outputUnavailable = true;
15
+ // A failed output route cannot safely correlate any later response. Stop
16
+ // reading as soon as the writer detects the failure; the final cleanup
17
+ // below closes the output after already-admitted routes settle.
18
+ options.input.destroy();
9
19
  });
10
- const writeJson = createJsonLineWriter(options.output);
20
+ const failClosedOnCorrelationViolation = () => {
21
+ if (correlationViolation)
22
+ return;
23
+ correlationViolation = true;
24
+ // A duplicate active requestId has no safe response route: either caller
25
+ // could observe a frame carrying that id. Stop admitting new input, but
26
+ // leave the already-admitted route alive so its sole terminal response
27
+ // can still settle without being preempted by the duplicate.
28
+ options.input.destroy();
29
+ };
30
+ const activeRequestIds = new Set();
11
31
  const tasks = new Set();
12
- for await (const line of lines) {
13
- const trimmed = line.trim();
14
- if (trimmed.length === 0)
15
- continue;
16
- const task = handleLine(session, trimmed, writeJson, options.error);
17
- tasks.add(task);
18
- void task.finally(() => {
19
- tasks.delete(task);
20
- }).catch(() => { });
32
+ try {
33
+ for await (const line of readBoundedNdjsonLines(options.input, frameLimitBytes)) {
34
+ if (outputUnavailable || correlationViolation || !writeJson.isUsable())
35
+ break;
36
+ const trimmed = line.trim();
37
+ if (trimmed.length === 0)
38
+ continue;
39
+ // Check the bounded, already-framed requestId before waiting for an
40
+ // in-flight slot. Otherwise maxInFlight=1 can turn an active duplicate
41
+ // into an apparently valid sequential reuse after the first route
42
+ // settles.
43
+ const preflightRequestId = requestIdFromLine(trimmed);
44
+ if (preflightRequestId !== undefined && activeRequestIds.has(preflightRequestId)) {
45
+ failClosedOnCorrelationViolation();
46
+ break;
47
+ }
48
+ while (tasks.size >= maxInFlight)
49
+ await waitForTaskSlot(tasks);
50
+ if (outputUnavailable || correlationViolation || !writeJson.isUsable())
51
+ break;
52
+ const task = handleLine(session, trimmed, writeJson, options.error, activeRequestIds, failClosedOnCorrelationViolation);
53
+ tasks.add(task);
54
+ void task.finally(() => {
55
+ tasks.delete(task);
56
+ }).catch(() => { });
57
+ }
58
+ }
59
+ catch (caught) {
60
+ if (caught instanceof BackendFrameError) {
61
+ // Once framing is untrusted, no later request can be correlated safely.
62
+ // Close the owned input/output routes so an embedding cannot continue
63
+ // using a partially consumed sidecar.
64
+ options.input.destroy();
65
+ options.output.end();
66
+ }
67
+ if (!correlationViolation) {
68
+ await writeDiagnostic(options.error, caught instanceof BackendFrameError
69
+ ? new Error(caught.message)
70
+ : caught);
71
+ }
72
+ }
73
+ const settled = await Promise.allSettled(tasks);
74
+ const fatalOutput = settled.find(result => result.status === "rejected"
75
+ && result.reason instanceof BackendFrameError);
76
+ if (correlationViolation || fatalOutput?.status === "rejected") {
77
+ options.input.destroy();
78
+ options.output.end();
21
79
  }
22
- await Promise.allSettled(tasks);
23
80
  }
24
- async function handleLine(session, line, writeJson, error) {
81
+ async function handleLine(session, line, writeJson, error, activeRequestIds, failClosedOnCorrelationViolation) {
25
82
  let request;
83
+ let registeredRequestId;
26
84
  try {
27
85
  const raw = JSON.parse(line);
86
+ const rawRequestId = isRecord(raw) && isValidBackendRequestId(raw.requestId)
87
+ ? raw.requestId
88
+ : undefined;
89
+ if (rawRequestId !== undefined && activeRequestIds.has(rawRequestId)) {
90
+ // Check before protocol parsing too: a malformed duplicate must not
91
+ // fall through to the generic error path and reuse the active id.
92
+ failClosedOnCorrelationViolation();
93
+ return;
94
+ }
28
95
  request = parseBackendRequest(raw);
96
+ if (!writeJson.isUsable())
97
+ return;
98
+ if (request.requestId !== undefined) {
99
+ if (activeRequestIds.has(request.requestId)) {
100
+ // Never send an error carrying this id: it belongs to the route that
101
+ // was admitted first, and a duplicate response would settle that
102
+ // original caller as if its own route had failed.
103
+ failClosedOnCorrelationViolation();
104
+ return;
105
+ }
106
+ activeRequestIds.add(request.requestId);
107
+ registeredRequestId = request.requestId;
108
+ }
29
109
  if (request.command === "runner.stream") {
30
110
  for await (const event of session.stream(request)) {
31
111
  await writeJson(event);
@@ -35,16 +115,22 @@ async function handleLine(session, line, writeJson, error) {
35
115
  await writeJson(await session.dispatch(request));
36
116
  }
37
117
  catch (caught) {
118
+ if (caught instanceof BackendFrameError)
119
+ throw caught;
38
120
  const response = backendResponseError(request?.requestId ?? requestIdFromLine(line), normalizeError(caught));
39
121
  await writeJson(response);
40
122
  if (!(caught instanceof ProtocolError)) {
41
123
  await writeDiagnostic(error, caught);
42
124
  }
43
125
  }
126
+ finally {
127
+ if (registeredRequestId !== undefined)
128
+ activeRequestIds.delete(registeredRequestId);
129
+ }
44
130
  }
45
131
  function normalizeError(error) {
46
132
  if (error instanceof SyntaxError) {
47
- return new ProtocolError("invalid_request", `Invalid JSON backend request line: ${error.message}`, false);
133
+ return new ProtocolError("invalid_request", "Invalid JSON backend request line.", false);
48
134
  }
49
135
  if (error instanceof Error)
50
136
  return error;
@@ -53,7 +139,7 @@ function normalizeError(error) {
53
139
  function requestIdFromLine(line) {
54
140
  try {
55
141
  const parsed = JSON.parse(line);
56
- if (isRecord(parsed) && typeof parsed.requestId === "string" && parsed.requestId.length > 0) {
142
+ if (isRecord(parsed) && isValidBackendRequestId(parsed.requestId)) {
57
143
  return parsed.requestId;
58
144
  }
59
145
  }
@@ -62,27 +148,153 @@ function requestIdFromLine(line) {
62
148
  }
63
149
  return undefined;
64
150
  }
65
- function createJsonLineWriter(output) {
151
+ function createJsonLineWriter(output, frameLimitBytes, onFatal) {
66
152
  let tail = Promise.resolve();
67
- return value => {
68
- const next = tail.then(() => writeLine(output, JSON.stringify(value)));
153
+ let usable = true;
154
+ let fatalNotified = false;
155
+ const markFatal = () => {
156
+ usable = false;
157
+ if (fatalNotified)
158
+ return;
159
+ fatalNotified = true;
160
+ onFatal();
161
+ };
162
+ const writeJson = (value) => {
163
+ const next = tail.then(() => {
164
+ if (!usable) {
165
+ throw new BackendFrameError("backend_invalid_output", "Backend output is unavailable.");
166
+ }
167
+ let line;
168
+ try {
169
+ const encoded = JSON.stringify(value);
170
+ if (typeof encoded !== "string") {
171
+ throw new Error("JSON.stringify returned no value.");
172
+ }
173
+ line = encoded;
174
+ }
175
+ catch {
176
+ const frameError = new BackendFrameError("backend_invalid_output", "Backend response could not be encoded as JSON.");
177
+ markFatal();
178
+ throw frameError;
179
+ }
180
+ // The shared limit is the encoded JSON frame, excluding its NDJSON
181
+ // delimiter. Keep server output symmetric with both readers.
182
+ if (Buffer.byteLength(line, "utf8") > frameLimitBytes) {
183
+ const frameError = new BackendFrameError("backend_frame_too_large", `Backend output frame exceeds the ${frameLimitBytes} byte limit.`);
184
+ markFatal();
185
+ throw frameError;
186
+ }
187
+ return writeLine(output, `${line}\n`).catch(() => {
188
+ const frameError = new BackendFrameError("backend_invalid_output", "Backend output could not be written.");
189
+ markFatal();
190
+ throw frameError;
191
+ });
192
+ });
69
193
  tail = next.catch(() => { });
70
194
  return next;
71
195
  };
196
+ writeJson.isUsable = () => usable;
197
+ return writeJson;
72
198
  }
73
199
  async function writeDiagnostic(error, value) {
74
200
  if (error === undefined)
75
201
  return;
76
202
  const message = value instanceof Error ? `${value.name}: ${value.message}` : String(value);
77
- await writeLine(error, message);
203
+ await writeLine(error, `${message}\n`);
78
204
  }
79
205
  async function writeLine(output, line) {
80
- if (output.write(`${line}\n`))
81
- return;
82
- await new Promise(resolve => {
83
- output.once("drain", resolve);
206
+ await new Promise((resolve, reject) => {
207
+ let settled = false;
208
+ const finish = (error) => {
209
+ if (settled)
210
+ return;
211
+ settled = true;
212
+ output.off("error", onError);
213
+ if (error !== undefined && error !== null)
214
+ reject(error);
215
+ else
216
+ resolve();
217
+ };
218
+ const onError = (error) => finish(error);
219
+ output.once("error", onError);
220
+ try {
221
+ output.write(line, error => finish(error));
222
+ }
223
+ catch (error) {
224
+ finish(error instanceof Error ? error : new Error(String(error)));
225
+ }
84
226
  });
85
227
  }
86
228
  function isRecord(value) {
87
229
  return value !== null && typeof value === "object" && !Array.isArray(value);
88
230
  }
231
+ async function waitForTaskSlot(tasks) {
232
+ await Promise.race([...tasks].map(task => task.then(() => undefined, () => undefined)));
233
+ }
234
+ class BackendFrameError extends Error {
235
+ code;
236
+ constructor(code, message) {
237
+ super(message);
238
+ this.code = code;
239
+ this.name = "BackendFrameError";
240
+ }
241
+ }
242
+ async function* readBoundedNdjsonLines(input, limitBytes) {
243
+ let frameBuffer = Buffer.alloc(0);
244
+ let frameBytes = 0;
245
+ const decoder = new TextDecoder("utf-8", { fatal: true });
246
+ for await (const chunk of input) {
247
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk), "utf8");
248
+ let offset = 0;
249
+ while (offset < bytes.length) {
250
+ const newlineIndex = bytes.indexOf(0x0a, offset);
251
+ const segmentEnd = newlineIndex >= 0 ? newlineIndex : bytes.length;
252
+ const segment = bytes.subarray(offset, segmentEnd);
253
+ if (segment.length > 0) {
254
+ const nextFrameBytes = frameBytes + segment.length;
255
+ if (nextFrameBytes > limitBytes) {
256
+ throw new BackendFrameError("backend_frame_too_large", `Backend input frame exceeds the ${limitBytes} byte limit.`);
257
+ }
258
+ if (nextFrameBytes > frameBuffer.length) {
259
+ let nextCapacity = Math.max(frameBuffer.length, 1);
260
+ while (nextCapacity < nextFrameBytes) {
261
+ nextCapacity = Math.min(limitBytes, nextCapacity * 2);
262
+ }
263
+ const grown = Buffer.allocUnsafe(nextCapacity);
264
+ frameBuffer.copy(grown, 0, 0, frameBytes);
265
+ frameBuffer = grown;
266
+ }
267
+ segment.copy(frameBuffer, frameBytes);
268
+ frameBytes = nextFrameBytes;
269
+ }
270
+ if (newlineIndex < 0)
271
+ break;
272
+ const frame = frameBuffer.subarray(0, frameBytes);
273
+ const body = frame.length > 0 && frame[frame.length - 1] === 0x0d
274
+ ? frame.subarray(0, frame.length - 1)
275
+ : frame;
276
+ try {
277
+ yield decoder.decode(body);
278
+ }
279
+ catch {
280
+ throw new BackendFrameError("backend_invalid_encoding", "Backend input contained invalid UTF-8.");
281
+ }
282
+ frameBytes = 0;
283
+ offset = newlineIndex + 1;
284
+ }
285
+ }
286
+ if (frameBytes > 0) {
287
+ throw new BackendFrameError("backend_unterminated_frame", "Backend input ended with an unterminated NDJSON frame.");
288
+ }
289
+ }
290
+ function validateServerOptions(options) {
291
+ for (const [name, value] of [
292
+ ["maxInFlight", options.maxInFlight],
293
+ ["frameLimitBytes", options.frameLimitBytes]
294
+ ]) {
295
+ const max = name === "frameLimitBytes" ? BACKEND_NDJSON_FRAME_LIMIT_BYTES : MAX_BACKEND_SERVER_IN_FLIGHT;
296
+ if (value !== undefined && (!Number.isSafeInteger(value) || value <= 0 || value > max)) {
297
+ throw new TypeError(`Backend stdio option ${name} must be a positive safe integer.`);
298
+ }
299
+ }
300
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Fixed CDP expression for one trusted user gesture on the unique file input
3
+ * owned by the unique visible ChatGPT composer. Local paths never enter this
4
+ * expression; the sanctioned file-chooser capability performs the handoff.
5
+ *
6
+ * Keep this shared by the ordinary files command and the transactional
7
+ * attachment provider so their target-selection safety contract cannot drift.
8
+ */
9
+ export declare const ACTIVE_COMPOSER_FILE_INPUT_CLICK_EXPRESSION = "(() => {\n const visible = element => {\n if (element.hidden || element.closest(\"[hidden], [inert], [aria-hidden='true']\")) return false;\n const style = getComputedStyle(element);\n const rect = element.getBoundingClientRect();\n return style.display !== \"none\" && style.visibility !== \"hidden\" && style.opacity !== \"0\"\n && (rect.width > 0 || rect.height > 0);\n };\n const textboxes = [...document.querySelectorAll(\"textarea, [contenteditable='true'], [role='textbox']\")].filter(visible);\n const composers = [...new Set(textboxes.map(textbox =>\n textbox.closest(\"form\")\n ?? textbox.closest(\"[data-testid*='composer' i]\")\n ?? textbox.closest(\"[aria-label*='composer' i]\")\n ?? textbox.closest(\"[class*='composer' i]\")\n ).filter(Boolean))];\n if (composers.length !== 1) return { ok: false, reason: \"active composer was not unique\" };\n const all = [...composers[0].querySelectorAll(\"input[type='file']\")]\n .filter(input => !input.disabled && input.getAttribute(\"aria-disabled\") !== \"true\");\n const preferred = all.filter(input => input.id === \"upload-files\");\n const nonImage = all.filter(input => input.getAttribute(\"accept\") !== \"image/*\");\n const candidates = preferred.length ? preferred : nonImage.length ? nonImage : all;\n if (candidates.length !== 1) return { ok: false, reason: \"active composer file input was not unique\" };\n candidates[0].click();\n return { ok: true };\n})()";
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Fixed CDP expression for one trusted user gesture on the unique file input
3
+ * owned by the unique visible ChatGPT composer. Local paths never enter this
4
+ * expression; the sanctioned file-chooser capability performs the handoff.
5
+ *
6
+ * Keep this shared by the ordinary files command and the transactional
7
+ * attachment provider so their target-selection safety contract cannot drift.
8
+ */
9
+ export const ACTIVE_COMPOSER_FILE_INPUT_CLICK_EXPRESSION = `(() => {
10
+ const visible = element => {
11
+ if (element.hidden || element.closest("[hidden], [inert], [aria-hidden='true']")) return false;
12
+ const style = getComputedStyle(element);
13
+ const rect = element.getBoundingClientRect();
14
+ return style.display !== "none" && style.visibility !== "hidden" && style.opacity !== "0"
15
+ && (rect.width > 0 || rect.height > 0);
16
+ };
17
+ const textboxes = [...document.querySelectorAll("textarea, [contenteditable='true'], [role='textbox']")].filter(visible);
18
+ const composers = [...new Set(textboxes.map(textbox =>
19
+ textbox.closest("form")
20
+ ?? textbox.closest("[data-testid*='composer' i]")
21
+ ?? textbox.closest("[aria-label*='composer' i]")
22
+ ?? textbox.closest("[class*='composer' i]")
23
+ ).filter(Boolean))];
24
+ if (composers.length !== 1) return { ok: false, reason: "active composer was not unique" };
25
+ const all = [...composers[0].querySelectorAll("input[type='file']")]
26
+ .filter(input => !input.disabled && input.getAttribute("aria-disabled") !== "true");
27
+ const preferred = all.filter(input => input.id === "upload-files");
28
+ const nonImage = all.filter(input => input.getAttribute("accept") !== "image/*");
29
+ const candidates = preferred.length ? preferred : nonImage.length ? nonImage : all;
30
+ if (candidates.length !== 1) return { ok: false, reason: "active composer file input was not unique" };
31
+ candidates[0].click();
32
+ return { ok: true };
33
+ })()`;
@@ -1,10 +1,13 @@
1
1
  import type { BootstrapArgs, BrowserLike, PageLike, RuntimeEnv } from "../types.js";
2
+ import { type CoordinatedBrowserOptions } from "../runtime/coordinated-browser.js";
2
3
  export type AttachedBrowser = {
3
4
  browser: BrowserLike;
4
5
  page: PageLike;
5
6
  browserName: string;
6
7
  tabId?: string;
7
8
  };
8
- export declare function attachChatGPTBrowser(env: RuntimeEnv, args?: BootstrapArgs): Promise<AttachedBrowser>;
9
+ export declare function attachChatGPTBrowser(env: RuntimeEnv, args?: BootstrapArgs, coordination?: CoordinatedBrowserOptions): Promise<AttachedBrowser>;
10
+ /** Resolve the configured provider browser without selecting, claiming, or creating a tab. */
11
+ export declare function resolveChatGPTBrowser(env: RuntimeEnv, coordination?: CoordinatedBrowserOptions): Promise<BrowserLike>;
9
12
  export { isChatGPTUrl } from "./chatgpt-url.js";
10
13
  export declare function tabIdFromPage(page: PageLike): string | undefined;
@@ -1,11 +1,13 @@
1
1
  import { BrowserBridgeUnavailableError, ChatGPTControlError, LoginRequiredError } from "../errors.js";
2
2
  import { CHATGPT_HOME, isChatGPTUrl } from "./chatgpt-url.js";
3
3
  import { parseConversationId, readPageState } from "./page-state.js";
4
+ import { createCoordinatedBrowser, createCoordinatedPageForBrowser } from "../runtime/coordinated-browser.js";
5
+ import { unwrapCoordinatedPage } from "../runtime/coordinated-page.js";
4
6
  const MAX_EXISTING_TAB_DIAGNOSTIC_CANDIDATES = 10;
5
7
  const MAX_EXISTING_TAB_DIAGNOSTIC_FIELD_LENGTH = 240;
6
- export async function attachChatGPTBrowser(env, args = {}) {
7
- const browser = await getBrowser(env);
8
- const page = await getOrCreateChatGPTPage(browser, env, args);
8
+ export async function attachChatGPTBrowser(env, args = {}, coordination) {
9
+ const browser = await getBrowser(env, coordination);
10
+ const page = await getOrCreateChatGPTPage(browser, env, args, coordination);
9
11
  await assertPageOnChatGPTOrigin(page);
10
12
  const state = await readPageState(page);
11
13
  if (!isChatGPTUrl(state.url))
@@ -24,9 +26,13 @@ export async function attachChatGPTBrowser(env, args = {}) {
24
26
  }
25
27
  return attached;
26
28
  }
27
- async function getBrowser(env) {
29
+ /** Resolve the configured provider browser without selecting, claiming, or creating a tab. */
30
+ export async function resolveChatGPTBrowser(env, coordination) {
31
+ return await getBrowser(env, coordination);
32
+ }
33
+ async function getBrowser(env, coordination) {
28
34
  if (env.browser !== undefined) {
29
- return env.browser;
35
+ return createCoordinatedBrowser(env.browser, coordination);
30
36
  }
31
37
  const anyEnv = env;
32
38
  const agent = env.agent ?? anyEnv.agent ?? globalThis.agent;
@@ -36,7 +42,7 @@ async function getBrowser(env) {
36
42
  ?? await tryBrowserGet(browsers, "extension")
37
43
  ?? await tryBrowserGet(browsers, "chrome");
38
44
  if (maybeBrowser !== undefined) {
39
- return maybeBrowser;
45
+ return createCoordinatedBrowser(maybeBrowser, coordination);
40
46
  }
41
47
  }
42
48
  throw new BrowserBridgeUnavailableError();
@@ -95,12 +101,15 @@ async function tryBrowserGetPreferredListed(browsers) {
95
101
  return undefined;
96
102
  }
97
103
  }
98
- async function getOrCreateChatGPTPage(browser, env, args) {
104
+ async function getOrCreateChatGPTPage(browser, env, args, coordination) {
99
105
  const targetUrl = args.url ?? CHATGPT_HOME;
100
106
  assertSafeChatGPTNavigation(targetUrl);
101
107
  const explicitExistingPolicy = normalizeExplicitExistingTabPolicy(args);
102
108
  if (env.page !== undefined) {
103
- const cached = normalizePage(env.page);
109
+ // An invocation can begin with a page captured before browser discovery.
110
+ // Rebind it to the discovered browser-wide actor, unwrapping only through
111
+ // the explicit seam so a page is never nested under two coordinators.
112
+ const cached = createCoordinatedPageForBrowser(normalizePage(env.page), browser, coordination);
104
113
  if (await cachedPageMatchesBootstrapArgs(cached, args, explicitExistingPolicy)) {
105
114
  return cached;
106
115
  }
@@ -553,7 +562,69 @@ function normalizeBrowser(browser) {
553
562
  if (browser === undefined || browser === null || typeof browser !== "object") {
554
563
  return undefined;
555
564
  }
556
- return browser;
565
+ // Browsers returned by the Codex bridge are capability proxies. Reading a
566
+ // method normally returns a receiver-safe callable, while extracting the
567
+ // same function from its prototype loses the proxy's private-field binding.
568
+ // Normalize that trusted bridge result into a plain BrowserLike before the
569
+ // descriptor-only coordination facade inspects it.
570
+ const rawBrowser = browser;
571
+ const normalized = {};
572
+ const name = providerValue(rawBrowser, "name");
573
+ if (typeof name === "string")
574
+ normalized.name = name;
575
+ const rawUser = providerValue(rawBrowser, "user");
576
+ if (isProviderRecord(rawUser)) {
577
+ const openTabs = providerCallable(rawUser, "openTabs");
578
+ const claimTab = providerCallable(rawUser, "claimTab");
579
+ normalized.user = {
580
+ ...(openTabs === undefined ? {} : {
581
+ openTabs: async () => await openTabs()
582
+ }),
583
+ ...(claimTab === undefined ? {} : {
584
+ claimTab: async (tab) => normalizePage(await claimTab(tab))
585
+ })
586
+ };
587
+ }
588
+ const rawTabs = providerValue(rawBrowser, "tabs");
589
+ if (isProviderRecord(rawTabs)) {
590
+ const create = providerCallable(rawTabs, "create");
591
+ const newer = providerCallable(rawTabs, "new");
592
+ const selected = providerCallable(rawTabs, "selected");
593
+ const list = providerCallable(rawTabs, "list");
594
+ const get = providerCallable(rawTabs, "get");
595
+ const finalize = providerCallable(rawTabs, "finalize");
596
+ normalized.tabs = {
597
+ ...(create === undefined ? {} : {
598
+ create: async (url) => normalizePage(await create(url))
599
+ }),
600
+ ...(newer === undefined ? {} : {
601
+ new: async (url) => normalizePage(await newer(...(url === undefined ? [] : [url])))
602
+ }),
603
+ ...(selected === undefined ? {} : {
604
+ selected: async () => {
605
+ const page = await selected();
606
+ return page === undefined ? undefined : normalizePage(page);
607
+ }
608
+ }),
609
+ ...(list === undefined ? {} : {
610
+ list: async () => {
611
+ const pages = await list();
612
+ return Array.isArray(pages) ? pages.map(normalizePage) : pages;
613
+ }
614
+ }),
615
+ ...(get === undefined ? {} : {
616
+ get: async (id) => normalizePage(await get(id))
617
+ }),
618
+ ...(finalize === undefined ? {} : {
619
+ finalize: async (options) => { await finalize(options); }
620
+ })
621
+ };
622
+ }
623
+ const newPage = providerCallable(rawBrowser, "newPage");
624
+ if (newPage !== undefined) {
625
+ normalized.newPage = async () => normalizePage(await newPage());
626
+ }
627
+ return normalized;
557
628
  }
558
629
  async function hydrateTab(browser, pageOrTab) {
559
630
  const maybe = pageOrTab;
@@ -568,28 +639,65 @@ async function hydrateTab(browser, pageOrTab) {
568
639
  return normalizePage(pageOrTab);
569
640
  }
570
641
  function normalizePage(pageOrTab) {
642
+ if (isPageWrapper(pageOrTab))
643
+ return pageOrTab;
644
+ if (!isProviderRecord(pageOrTab))
645
+ return pageOrTab;
571
646
  const maybe = pageOrTab;
572
- const playwright = maybe.playwright ?? maybe.page;
573
- if (playwright !== undefined && typeof playwright === "object") {
574
- return new Proxy(playwright, {
575
- get(target, prop) {
576
- if (prop in target) {
577
- const value = target[prop];
578
- return typeof value === "function" ? value.bind(target) : value;
579
- }
580
- const value = maybe[prop];
581
- return typeof value === "function" ? value.bind(maybe) : value;
582
- }
583
- });
647
+ const embedded = providerValue(maybe, "playwright") ?? providerValue(maybe, "page");
648
+ const primary = isProviderRecord(embedded) ? embedded : maybe;
649
+ const normalized = {};
650
+ for (const property of ["id", "tabId"]) {
651
+ const value = providerValue(maybe, property) ?? providerValue(primary, property);
652
+ if (typeof value === "string")
653
+ normalized[property] = value;
654
+ }
655
+ for (const property of ["keyboard", "mouse", "cua", "capabilities"]) {
656
+ const value = providerValue(primary, property) ?? providerValue(maybe, property);
657
+ if (isProviderRecord(value))
658
+ normalized[property] = value;
659
+ }
660
+ if (isProviderRecord(embedded))
661
+ normalized.playwright = embedded;
662
+ for (const method of [
663
+ "url", "goto", "title", "locator", "getByRole", "getByPlaceholder",
664
+ "getByText", "waitForTimeout", "waitForEvent", "evaluate", "content", "close"
665
+ ]) {
666
+ const callable = providerCallable(primary, method) ?? providerCallable(maybe, method);
667
+ if (callable !== undefined)
668
+ normalized[method] = (...args) => callable(...args);
669
+ }
670
+ const stringUrl = providerValue(maybe, "url");
671
+ if (normalized.url === undefined && typeof stringUrl === "string") {
672
+ normalized.url = () => stringUrl;
673
+ }
674
+ const stringTitle = providerValue(maybe, "title");
675
+ if (normalized.title === undefined && typeof stringTitle === "string") {
676
+ normalized.title = async () => stringTitle;
677
+ }
678
+ return normalized;
679
+ }
680
+ function isProviderRecord(value) {
681
+ return (typeof value === "object" && value !== null) || typeof value === "function";
682
+ }
683
+ function providerValue(value, key) {
684
+ try {
685
+ return Reflect.get(value, key, value);
584
686
  }
585
- if (typeof maybe.url === "string") {
586
- return {
587
- ...maybe,
588
- url: () => maybe.url,
589
- title: async () => typeof maybe.title === "string" ? maybe.title : ""
590
- };
687
+ catch {
688
+ return undefined;
591
689
  }
592
- return pageOrTab;
690
+ }
691
+ function providerCallable(value, key) {
692
+ const candidate = providerValue(value, key);
693
+ if (typeof candidate !== "function")
694
+ return undefined;
695
+ return (...args) => Reflect.apply(candidate, value, args);
696
+ }
697
+ function isPageWrapper(value) {
698
+ if (value === null || (typeof value !== "object" && typeof value !== "function"))
699
+ return false;
700
+ return unwrapCoordinatedPage(value) !== value;
593
701
  }
594
702
  export function tabIdFromPage(page) {
595
703
  const maybe = page;
@@ -6,3 +6,5 @@ export declare const CHATGPT_HOME = "https://chatgpt.com/";
6
6
  */
7
7
  export declare function isChatGPTUrl(value: string | undefined): boolean;
8
8
  export declare function requireChatGPTUrl(value: string, label: string): string;
9
+ /** Public context for ChatGPT's signed attachment endpoint, never its token. */
10
+ export declare function chatGPTAttachmentContextUrl(value: string | undefined): string | undefined;
@@ -26,3 +26,12 @@ export function requireChatGPTUrl(value, label) {
26
26
  }
27
27
  return value;
28
28
  }
29
+ /** Public context for ChatGPT's signed attachment endpoint, never its token. */
30
+ export function chatGPTAttachmentContextUrl(value) {
31
+ if (!isChatGPTUrl(value))
32
+ return undefined;
33
+ const url = new URL(value);
34
+ if (url.pathname !== "/backend-api/estuary/content")
35
+ return undefined;
36
+ return `${url.origin}${url.pathname}`;
37
+ }
@@ -1,7 +1,23 @@
1
1
  import type { DownloadedFile, PageLike } from "../types.js";
2
+ import { ChatGPTControlError } from "../errors.js";
2
3
  export type DownloadLike = {
3
4
  suggestedFilename?: () => string;
5
+ /** Capability-based stream exposed by Playwright's Download implementation. */
6
+ createReadStream?: () => Promise<AsyncIterable<Uint8Array>>;
4
7
  saveAs?: (path: string) => Promise<void>;
5
- path?: () => Promise<string | null>;
8
+ path?: (options?: {
9
+ timeoutMs?: number;
10
+ }) => Promise<string | null>;
6
11
  };
12
+ export declare class DownloadReceiptTimeoutError extends ChatGPTControlError {
13
+ constructor(stage: string);
14
+ }
15
+ export declare class DownloadBrowserBlockedError extends ChatGPTControlError {
16
+ constructor();
17
+ }
18
+ export declare class DownloadReceiptFailedError extends ChatGPTControlError {
19
+ constructor();
20
+ }
21
+ export declare function isTerminalDownloadError(error: unknown): error is DownloadReceiptTimeoutError | DownloadBrowserBlockedError | DownloadReceiptFailedError;
22
+ export declare function isTerminalDownloadCode(code: string | undefined): boolean;
7
23
  export declare function waitForDownloadFromClick(page: PageLike, click: () => Promise<void>, destDir: string, timeoutMs: number, filenameHint?: string): Promise<DownloadedFile>;