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.
- package/CHANGELOG.md +42 -0
- package/README.md +43 -0
- package/contracts/v1/fixtures/backend-capabilities.json +40 -1
- package/contracts/v1/fixtures/backend-compatibility.json +21 -0
- package/contracts/v1/fixtures/backend-version.json +6 -1
- package/contracts/v1/fixtures/download-blocked-by-browser.json +22 -0
- package/contracts/v1/fixtures/download-receipt-failed.json +22 -0
- package/contracts/v1/fixtures/download-receipt-timeout.json +22 -0
- package/contracts/v1/fixtures/journal-rpc-indeterminate.json +32 -0
- package/contracts/v1/fixtures/journal-rpc-unsupported-platform.json +32 -0
- package/contracts/v1/fixtures/journal-runtime-unavailable.json +32 -0
- package/contracts/v1/fixtures/operation-action-prepared-event.json +42 -0
- package/contracts/v1/fixtures/operation-action.json +13 -0
- package/contracts/v1/fixtures/operation-artifact-receipt.json +14 -0
- package/contracts/v1/fixtures/operation-artifact-transfer-intent-event.json +24 -0
- package/contracts/v1/fixtures/operation-artifact-transfer-receipt-event.json +26 -0
- package/contracts/v1/fixtures/operation-artifact-transfer-state.json +178 -0
- package/contracts/v1/fixtures/operation-blocker.json +10 -0
- package/contracts/v1/fixtures/operation-collect-request.json +17 -0
- package/contracts/v1/fixtures/operation-collect-result.json +42 -0
- package/contracts/v1/fixtures/operation-control-receipt.json +13 -0
- package/contracts/v1/fixtures/operation-control-request.json +18 -0
- package/contracts/v1/fixtures/operation-control-result.json +31 -0
- package/contracts/v1/fixtures/operation-event.json +18 -0
- package/contracts/v1/fixtures/operation-handle.json +10 -0
- package/contracts/v1/fixtures/operation-inspect-request.json +13 -0
- package/contracts/v1/fixtures/operation-inspect-result.json +205 -0
- package/contracts/v1/fixtures/operation-ownership-baseline-event.json +34 -0
- package/contracts/v1/fixtures/operation-receipt.json +32 -0
- package/contracts/v1/fixtures/operation-recovery-decision.json +7 -0
- package/contracts/v1/fixtures/operation-recovery-observation.json +12 -0
- package/contracts/v1/fixtures/operation-request.json +33 -0
- package/contracts/v1/fixtures/operation-state.json +144 -0
- package/contracts/v1/fixtures/operation-submission-witness-event.json +20 -0
- package/contracts/v1/fixtures/operation-submission-witness.json +11 -0
- package/contracts/v1/fixtures/operation-submit-result.json +16 -0
- package/contracts/v1/fixtures/operation-target-established-event.json +21 -0
- package/contracts/v1/manifest.json +185 -1
- package/contracts/v1/parity-suite.json +106 -8
- package/contracts/v1/schemas/backend-compatibility.schema.json +65 -0
- package/contracts/v1/schemas/backend-request.schema.json +8 -2
- package/contracts/v1/schemas/capabilities.schema.json +81 -1
- package/contracts/v1/schemas/manifest.schema.json +57 -0
- package/contracts/v1/schemas/operation-action.schema.json +155 -0
- package/contracts/v1/schemas/operation-artifact-receipt.schema.json +89 -0
- package/contracts/v1/schemas/operation-blocker.schema.json +70 -0
- package/contracts/v1/schemas/operation-collect-request.schema.json +46 -0
- package/contracts/v1/schemas/operation-collect-result.schema.json +74 -0
- package/contracts/v1/schemas/operation-control-receipt.schema.json +108 -0
- package/contracts/v1/schemas/operation-control-request.schema.json +111 -0
- package/contracts/v1/schemas/operation-control-result.schema.json +112 -0
- package/contracts/v1/schemas/operation-event.schema.json +756 -0
- package/contracts/v1/schemas/operation-handle.schema.json +55 -0
- package/contracts/v1/schemas/operation-inspect-request.schema.json +42 -0
- package/contracts/v1/schemas/operation-inspect-result.schema.json +1662 -0
- package/contracts/v1/schemas/operation-receipt.schema.json +153 -0
- package/contracts/v1/schemas/operation-recovery.schema.json +314 -0
- package/contracts/v1/schemas/operation-request.schema.json +158 -0
- package/contracts/v1/schemas/operation-state.schema.json +691 -0
- package/contracts/v1/schemas/operation-submission-witness.schema.json +48 -0
- package/contracts/v1/schemas/operation-submit-result.schema.json +94 -0
- package/contracts/v1/surface-drift-policy.json +50 -0
- package/contracts/v1/vectors/operation-request-digest-v1.json +43 -0
- package/dist/codex-chatgpt-control-backend.mjs +45882 -9361
- package/dist/codex-chatgpt-control-journal.mjs +5547 -0
- package/dist/codex-chatgpt-control-live-smoke.bundle.mjs +51677 -0
- package/dist/codex-chatgpt-control-release-canary.bundle.mjs +52104 -0
- package/dist/codex-chatgpt-control.bundle.mjs +48392 -10260
- package/dist/src/backend/client.d.ts +103 -2
- package/dist/src/backend/client.js +1436 -97
- package/dist/src/backend/compatibility.d.ts +12 -0
- package/dist/src/backend/compatibility.js +208 -0
- package/dist/src/backend/protocol.d.ts +84 -1
- package/dist/src/backend/protocol.js +31 -5
- package/dist/src/backend/runtime-identity.d.ts +10 -0
- package/dist/src/backend/runtime-identity.js +141 -0
- package/dist/src/backend/session.d.ts +10 -2
- package/dist/src/backend/session.js +478 -7
- package/dist/src/backend/stdio-server.d.ts +4 -1
- package/dist/src/backend/stdio-server.js +240 -28
- package/dist/src/browser/active-composer-file-input.d.ts +9 -0
- package/dist/src/browser/active-composer-file-input.js +33 -0
- package/dist/src/browser/attach.d.ts +4 -1
- package/dist/src/browser/attach.js +136 -28
- package/dist/src/browser/chatgpt-url.d.ts +2 -0
- package/dist/src/browser/chatgpt-url.js +9 -0
- package/dist/src/browser/downloads.d.ts +17 -1
- package/dist/src/browser/downloads.js +150 -28
- package/dist/src/browser/page-state.js +6 -0
- package/dist/src/client.d.ts +43 -0
- package/dist/src/client.js +1279 -59
- package/dist/src/commands/artifacts.js +6 -4
- package/dist/src/commands/chat-popover.d.ts +37 -0
- package/dist/src/commands/chat-popover.js +537 -0
- package/dist/src/commands/configuration.d.ts +5 -0
- package/dist/src/commands/configuration.js +134 -21
- package/dist/src/commands/context.js +10 -3
- package/dist/src/commands/doctor.d.ts +1 -1
- package/dist/src/commands/doctor.js +27 -4
- package/dist/src/commands/experience.d.ts +5 -0
- package/dist/src/commands/experience.js +104 -21
- package/dist/src/commands/files.js +14 -33
- package/dist/src/commands/modes.js +85 -21
- package/dist/src/commands/power-discovery.d.ts +130 -0
- package/dist/src/commands/power-discovery.js +669 -0
- package/dist/src/commands/project-sources.js +325 -31
- package/dist/src/commands/sequence.js +59 -32
- package/dist/src/commands/session.js +19 -6
- package/dist/src/commands/work.js +31 -0
- package/dist/src/dom/composer-text.d.ts +6 -0
- package/dist/src/dom/composer-text.js +85 -0
- package/dist/src/errors.d.ts +9 -0
- package/dist/src/errors.js +22 -0
- package/dist/src/index.d.ts +6 -0
- package/dist/src/index.js +9 -0
- package/dist/src/operations/artifact-output.d.ts +69 -0
- package/dist/src/operations/artifact-output.js +1748 -0
- package/dist/src/operations/artifact-stream.d.ts +19 -0
- package/dist/src/operations/artifact-stream.js +35 -0
- package/dist/src/operations/artifact-transfer.d.ts +149 -0
- package/dist/src/operations/artifact-transfer.js +1312 -0
- package/dist/src/operations/browser-adapter.d.ts +167 -0
- package/dist/src/operations/browser-adapter.js +2103 -0
- package/dist/src/operations/browser-observation.d.ts +110 -0
- package/dist/src/operations/browser-observation.js +1256 -0
- package/dist/src/operations/browser-target.d.ts +105 -0
- package/dist/src/operations/browser-target.js +531 -0
- package/dist/src/operations/canonical.d.ts +4 -0
- package/dist/src/operations/canonical.js +412 -0
- package/dist/src/operations/chatgpt-runtime.d.ts +72 -0
- package/dist/src/operations/chatgpt-runtime.js +1232 -0
- package/dist/src/operations/client.d.ts +138 -0
- package/dist/src/operations/client.js +1145 -0
- package/dist/src/operations/collector.d.ts +195 -0
- package/dist/src/operations/collector.js +1039 -0
- package/dist/src/operations/configuration-routing.d.ts +4 -0
- package/dist/src/operations/configuration-routing.js +14 -0
- package/dist/src/operations/control.d.ts +372 -0
- package/dist/src/operations/control.js +1498 -0
- package/dist/src/operations/file-identity.d.ts +33 -0
- package/dist/src/operations/file-identity.js +153 -0
- package/dist/src/operations/handle.d.ts +19 -0
- package/dist/src/operations/handle.js +727 -0
- package/dist/src/operations/index.d.ts +25 -0
- package/dist/src/operations/index.js +25 -0
- package/dist/src/operations/journal-authority.d.ts +12 -0
- package/dist/src/operations/journal-authority.js +1 -0
- package/dist/src/operations/journal-rpc-client.d.ts +12 -0
- package/dist/src/operations/journal-rpc-client.js +225 -0
- package/dist/src/operations/journal-rpc-protocol.d.ts +32 -0
- package/dist/src/operations/journal-rpc-protocol.js +214 -0
- package/dist/src/operations/journal-rpc-server.d.ts +15 -0
- package/dist/src/operations/journal-rpc-server.js +272 -0
- package/dist/src/operations/journal.d.ts +143 -0
- package/dist/src/operations/journal.js +1957 -0
- package/dist/src/operations/production-attachments.d.ts +118 -0
- package/dist/src/operations/production-attachments.js +1035 -0
- package/dist/src/operations/production-chatgpt-artifacts.d.ts +74 -0
- package/dist/src/operations/production-chatgpt-artifacts.js +1146 -0
- package/dist/src/operations/production-chatgpt-attachments.d.ts +80 -0
- package/dist/src/operations/production-chatgpt-attachments.js +1639 -0
- package/dist/src/operations/production-configuration.d.ts +42 -0
- package/dist/src/operations/production-configuration.js +1544 -0
- package/dist/src/operations/production-primitives.d.ts +53 -0
- package/dist/src/operations/production-primitives.js +856 -0
- package/dist/src/operations/production-work-steer.d.ts +203 -0
- package/dist/src/operations/production-work-steer.js +1218 -0
- package/dist/src/operations/recovery.d.ts +80 -0
- package/dist/src/operations/recovery.js +174 -0
- package/dist/src/operations/runtime-adapter.d.ts +126 -0
- package/dist/src/operations/runtime-adapter.js +802 -0
- package/dist/src/operations/send-once.d.ts +224 -0
- package/dist/src/operations/send-once.js +1081 -0
- package/dist/src/operations/service.d.ts +286 -0
- package/dist/src/operations/service.js +2831 -0
- package/dist/src/operations/staging.d.ts +136 -0
- package/dist/src/operations/staging.js +630 -0
- package/dist/src/operations/state-machine.d.ts +29 -0
- package/dist/src/operations/state-machine.js +2047 -0
- package/dist/src/operations/submission.d.ts +423 -0
- package/dist/src/operations/submission.js +1676 -0
- package/dist/src/operations/transactional-chat-power.d.ts +17 -0
- package/dist/src/operations/transactional-chat-power.js +212 -0
- package/dist/src/operations/turn-ownership.d.ts +202 -0
- package/dist/src/operations/turn-ownership.js +700 -0
- package/dist/src/operations/types.d.ts +454 -0
- package/dist/src/operations/types.js +20 -0
- package/dist/src/operations/wire-requests.d.ts +21 -0
- package/dist/src/operations/wire-requests.js +396 -0
- package/dist/src/operations/wire-results.d.ts +97 -0
- package/dist/src/operations/wire-results.js +818 -0
- package/dist/src/runner/responses.d.ts +3 -1
- package/dist/src/runner/responses.js +19 -1
- package/dist/src/runner/result.js +227 -55
- package/dist/src/runner/types.d.ts +12 -0
- package/dist/src/runtime/command-routing.d.ts +149 -0
- package/dist/src/runtime/command-routing.js +431 -0
- package/dist/src/runtime/coordinated-browser.d.ts +24 -0
- package/dist/src/runtime/coordinated-browser.js +315 -0
- package/dist/src/runtime/coordinated-page.d.ts +41 -0
- package/dist/src/runtime/coordinated-page.js +651 -0
- package/dist/src/runtime/operation-context.d.ts +142 -0
- package/dist/src/runtime/operation-context.js +410 -0
- package/dist/src/runtime/runtime-session.d.ts +95 -0
- package/dist/src/runtime/runtime-session.js +314 -0
- package/dist/src/runtime/tab-coordinator.d.ts +200 -0
- package/dist/src/runtime/tab-coordinator.js +1200 -0
- package/dist/src/runtime/value-boundaries.d.ts +18 -0
- package/dist/src/runtime/value-boundaries.js +49 -0
- package/dist/src/safety/blockers.js +8 -1
- package/dist/src/safety/untrusted-output.js +2 -1
- package/dist/src/scripts/backend-server.js +4 -1
- package/dist/src/scripts/capture-surface-profile.js +7 -2
- package/dist/src/scripts/journal-server.d.ts +2 -0
- package/dist/src/scripts/journal-server.js +37 -0
- package/dist/src/scripts/live-smoke/harness.js +79 -7
- package/dist/src/scripts/live-smoke/scenarios.d.ts +42 -1
- package/dist/src/scripts/live-smoke/scenarios.js +147 -30
- package/dist/src/scripts/live-smoke/transactional.d.ts +17 -0
- package/dist/src/scripts/live-smoke/transactional.js +254 -0
- package/dist/src/scripts/live-smoke/types.d.ts +3 -0
- package/dist/src/scripts/release-canary-module.js +17 -4
- package/dist/src/types.d.ts +38 -1
- package/package.json +8 -3
- package/references/2026-08-16-transactional-operations.md +459 -0
- package/references/2026-09-06-journal-service.md +140 -0
- package/references/agents-runner.md +34 -0
- package/references/backend-protocol.md +176 -0
- package/references/python-parity.md +137 -0
- package/references/responses-adapter.md +19 -0
- package/references/streaming.md +6 -0
|
@@ -1,31 +1,111 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|
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
|
-
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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",
|
|
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) &&
|
|
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
|
-
|
|
68
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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?: (
|
|
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>;
|