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
|
@@ -0,0 +1,1232 @@
|
|
|
1
|
+
import { attachChatGPTBrowser, isChatGPTUrl, resolveChatGPTBrowser, tabIdFromPage } from "../browser/attach.js";
|
|
2
|
+
import { CHATGPT_HOME } from "../browser/chatgpt-url.js";
|
|
3
|
+
import { composerTextbox, sendButton } from "../dom/selectors.js";
|
|
4
|
+
import { parseConversationId } from "../browser/page-state.js";
|
|
5
|
+
import { detectExperienceFromSnapshot, openExperience, readSurfaceSnapshot } from "../commands/experience.js";
|
|
6
|
+
import { coordinatedBrowserResource, unwrapCoordinatedBrowser, } from "../runtime/coordinated-browser.js";
|
|
7
|
+
import { unwrapCoordinatedPage } from "../runtime/coordinated-page.js";
|
|
8
|
+
import { getProcessTabCoordinator } from "../runtime/tab-coordinator.js";
|
|
9
|
+
import { createProductionConfigurationStaging } from "./production-configuration.js";
|
|
10
|
+
import { revalidateOperationFile } from "./file-identity.js";
|
|
11
|
+
import { createChatGPTAttachmentProviderAsync } from "./production-chatgpt-attachments.js";
|
|
12
|
+
import { createProductionOperationPrimitives } from "./production-primitives.js";
|
|
13
|
+
import { createRuntimeOperationBrowserAdapter } from "./runtime-adapter.js";
|
|
14
|
+
import { createProductionChatGPTArtifacts } from "./production-chatgpt-artifacts.js";
|
|
15
|
+
import { createProductionWorkSteerPrimitive } from "./production-work-steer.js";
|
|
16
|
+
import { assertAuthenticatedSendRecovery, createOperationBrowserAdapter, recoverAuthenticatedBrowserSend } from "./browser-adapter.js";
|
|
17
|
+
import { observeBrowserPage } from "./browser-observation.js";
|
|
18
|
+
/**
|
|
19
|
+
* Create a fresh, prompt-bearing control adapter. The prompt is accepted only
|
|
20
|
+
* for this invocation and is never cached or forwarded through durable phase
|
|
21
|
+
* records. The factory binds the authenticated parent target by tab ID.
|
|
22
|
+
*/
|
|
23
|
+
export function createChatGPTOperationControlAdapterFactory(options) {
|
|
24
|
+
const normalized = normalizeFactoryOptions(options);
|
|
25
|
+
return async (context) => {
|
|
26
|
+
const request = snapshotControlRequest(context.request);
|
|
27
|
+
const target = snapshotTargetBinding(context.target);
|
|
28
|
+
const targetRequest = Object.freeze({ type: "tab_id", tabId: target.tabId });
|
|
29
|
+
const parentOperationId = context.handle.operationId;
|
|
30
|
+
const parentRequestDigest = context.handle.requestDigest;
|
|
31
|
+
const targetBindingDigest = context.handle.targetBindingDigest;
|
|
32
|
+
if (targetBindingDigest === undefined)
|
|
33
|
+
throw new ChatGPTRuntimeFactoryError();
|
|
34
|
+
const adapterOptions = {
|
|
35
|
+
owner: normalized.owner,
|
|
36
|
+
evidenceDigest: normalized.evidenceDigest,
|
|
37
|
+
...(normalized.coordinator === undefined ? {} : { coordinator: normalized.coordinator }),
|
|
38
|
+
...(normalized.transactionTimeoutMs === undefined ? {} : { transactionTimeoutMs: normalized.transactionTimeoutMs }),
|
|
39
|
+
exposeStaging: false,
|
|
40
|
+
exposeControl: true,
|
|
41
|
+
recovery: Object.freeze({
|
|
42
|
+
operationId: parentOperationId,
|
|
43
|
+
requestDigest: parentRequestDigest,
|
|
44
|
+
surface: context.state.surface,
|
|
45
|
+
target,
|
|
46
|
+
targetRequest
|
|
47
|
+
}),
|
|
48
|
+
capture: async (captureRequest) => await captureChatGPTRequest({
|
|
49
|
+
...normalized,
|
|
50
|
+
request: undefined,
|
|
51
|
+
files: Object.freeze([]),
|
|
52
|
+
captureRequest,
|
|
53
|
+
recoveryTarget: target,
|
|
54
|
+
control: Object.freeze({
|
|
55
|
+
request,
|
|
56
|
+
targetBindingDigest
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
};
|
|
60
|
+
return createRuntimeOperationBrowserAdapter(adapterOptions);
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/** Alias for callers that use the shorter control-factory name. */
|
|
64
|
+
export const createChatGPTControlAdapterFactory = createChatGPTOperationControlAdapterFactory;
|
|
65
|
+
export class ChatGPTRuntimeFactoryError extends Error {
|
|
66
|
+
code = "chatgpt_runtime_unavailable";
|
|
67
|
+
constructor() {
|
|
68
|
+
super("The ChatGPT operation runtime could not prove the requested browser target safely.");
|
|
69
|
+
this.name = "ChatGPTRuntimeFactoryError";
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const DEFAULT_SURFACE_TIMEOUT_MS = 30_000;
|
|
73
|
+
const MAX_SURFACE_TIMEOUT_MS = 120_000;
|
|
74
|
+
const ID_PATTERN = /^[A-Za-z0-9._:-]{1,512}$/u;
|
|
75
|
+
/**
|
|
76
|
+
* Create the normal request-scoped adapter factory for `ChatGPTClient`.
|
|
77
|
+
*
|
|
78
|
+
* The returned factory performs no browser work until its capture callback is
|
|
79
|
+
* reached by `resolveTarget`. It is therefore safe to construct while a
|
|
80
|
+
* client is being initialized or while the browser bridge is unavailable.
|
|
81
|
+
*/
|
|
82
|
+
export function createChatGPTOperationAdapterFactory(options) {
|
|
83
|
+
const normalized = normalizeFactoryOptions(options);
|
|
84
|
+
return async (context) => {
|
|
85
|
+
const request = snapshotRequest(context.request);
|
|
86
|
+
const files = Object.freeze([...context.files]);
|
|
87
|
+
const adapterOptions = {
|
|
88
|
+
owner: normalized.owner,
|
|
89
|
+
evidenceDigest: normalized.evidenceDigest,
|
|
90
|
+
...(normalized.coordinator === undefined ? {} : { coordinator: normalized.coordinator }),
|
|
91
|
+
...(normalized.transactionTimeoutMs === undefined ? {} : { transactionTimeoutMs: normalized.transactionTimeoutMs }),
|
|
92
|
+
files,
|
|
93
|
+
fileManifestDigest: (ordinal, manifest) => normalized.evidenceDigest("file-manifest", { ordinal, ...manifest }),
|
|
94
|
+
exposeStaging: true,
|
|
95
|
+
exposeControl: true,
|
|
96
|
+
recoverAuthenticatedSend: async (recovery) => {
|
|
97
|
+
try {
|
|
98
|
+
assertAuthenticatedSendRecovery(recovery);
|
|
99
|
+
if (recovery.operationId !== request.operationId || recovery.surface !== request.surface
|
|
100
|
+
|| recovery.signal?.aborted || (recovery.deadlineAt !== undefined && Date.now() >= recovery.deadlineAt)) {
|
|
101
|
+
return { result: { status: "blocked", blockerCode: "target_binding_mismatch" } };
|
|
102
|
+
}
|
|
103
|
+
const expectedText = request.prompt.replace(/\s+/g, " ").trim().normalize("NFC");
|
|
104
|
+
const matchingTurns = new Set();
|
|
105
|
+
let matchingProbe = false;
|
|
106
|
+
const digest = async (domain, material) => {
|
|
107
|
+
const result = await normalized.evidenceDigest(domain, material);
|
|
108
|
+
if (matchingProbe && domain === "browser-observation-turn" && material !== null && typeof material === "object"
|
|
109
|
+
&& readDataProperty(material, "operationId") === request.operationId
|
|
110
|
+
&& readDataProperty(material, "role") === "user" && readDataProperty(material, "text") === expectedText) {
|
|
111
|
+
matchingTurns.add(result);
|
|
112
|
+
}
|
|
113
|
+
return result;
|
|
114
|
+
};
|
|
115
|
+
const captured = await captureChatGPTRequest({
|
|
116
|
+
...normalized, evidenceDigest: digest, request: undefined, files: Object.freeze([]),
|
|
117
|
+
captureRequest: { operationId: recovery.operationId, requestDigest: recovery.requestDigest, surface: recovery.surface,
|
|
118
|
+
target: { type: "tab_id", tabId: recovery.target.tabId }, signal: recovery.signal ?? new AbortController().signal },
|
|
119
|
+
recoveryTarget: recovery.target
|
|
120
|
+
});
|
|
121
|
+
const base = captured.primitives?.submission?.sendObservers;
|
|
122
|
+
if (base === undefined)
|
|
123
|
+
return { result: { status: "blocked", blockerCode: "target_evidence_unavailable" } };
|
|
124
|
+
const result = await recoverAuthenticatedBrowserSend(recovery, {
|
|
125
|
+
page: captured.page, owner: normalized.owner, coordinator: normalized.coordinator,
|
|
126
|
+
evidenceDigest: digest,
|
|
127
|
+
...(captured.capabilities === undefined ? {} : { capabilities: captured.capabilities }),
|
|
128
|
+
...(captured.authoritativeClaim === undefined ? {} : { authoritativeClaim: captured.authoritativeClaim }),
|
|
129
|
+
...(captured.observeCurrentTarget === undefined ? {} : { observeCurrentTarget: captured.observeCurrentTarget }),
|
|
130
|
+
...(normalized.transactionTimeoutMs === undefined ? {} : { transactionTimeoutMs: normalized.transactionTimeoutMs }),
|
|
131
|
+
sendObservers: { ...base, observePostcondition: async (probe) => {
|
|
132
|
+
matchingTurns.clear();
|
|
133
|
+
matchingProbe = true;
|
|
134
|
+
try {
|
|
135
|
+
const observed = await base.observePostcondition(probe);
|
|
136
|
+
const result = "result" in observed ? observed.result : observed;
|
|
137
|
+
if ((result.status === "submitted" || result.status === "already_submitted")
|
|
138
|
+
&& !matchingTurns.has(result.userTurnEvidenceDigest)) {
|
|
139
|
+
return { result: { status: "blocked", blockerCode: "ambiguous_submit" }, retryable: false };
|
|
140
|
+
}
|
|
141
|
+
return observed;
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
matchingProbe = false;
|
|
145
|
+
matchingTurns.clear();
|
|
146
|
+
}
|
|
147
|
+
} }
|
|
148
|
+
});
|
|
149
|
+
if (result.status !== "submitted" && result.status !== "already_submitted")
|
|
150
|
+
return { result };
|
|
151
|
+
const proof = result.targetEstablishment;
|
|
152
|
+
const target = recovery.target.targetLifecycle === "new_pending" && proof !== undefined
|
|
153
|
+
? { ...recovery.target, targetLifecycle: "new_established",
|
|
154
|
+
evidenceProfile: { ...recovery.target.evidenceProfile, stableConversationId: "required", stableUserTurnId: "required" },
|
|
155
|
+
conversationId: proof.conversationId,
|
|
156
|
+
canonicalThreadUrl: proof.canonicalThreadUrl,
|
|
157
|
+
targetEstablishment: { ...proof, observedAt: new Date().toISOString() } }
|
|
158
|
+
: recovery.target;
|
|
159
|
+
// The service calls collection only after durably accepting this
|
|
160
|
+
// proof. Retain only read ports, bound to the proved conversation.
|
|
161
|
+
const readAdapter = createOperationBrowserAdapter({
|
|
162
|
+
page: captured.page, owner: normalized.owner, evidenceDigest: normalized.evidenceDigest,
|
|
163
|
+
coordinator: normalized.coordinator,
|
|
164
|
+
...(captured.observeCurrentTarget === undefined ? {} : { observeCurrentTarget: captured.observeCurrentTarget }),
|
|
165
|
+
...(captured.capabilities === undefined ? {} : { capabilities: captured.capabilities }),
|
|
166
|
+
...(captured.authoritativeClaim === undefined ? {} : { authoritativeClaim: captured.authoritativeClaim }),
|
|
167
|
+
...(captured.primitives?.collector === undefined ? {} : { collector: captured.primitives.collector }),
|
|
168
|
+
recovery: { operationId: recovery.operationId, requestDigest: recovery.requestDigest,
|
|
169
|
+
surface: recovery.surface, target, signal: recovery.signal ?? new AbortController().signal }
|
|
170
|
+
});
|
|
171
|
+
return { result, collector: readAdapter.collector };
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return { result: { status: "blocked", blockerCode: "target_evidence_unavailable" } };
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
...(hasTransferDestination(request) ? { exposeArtifacts: true } : {}),
|
|
178
|
+
capture: async (captureRequest) => {
|
|
179
|
+
// `captureRequest` is reached only after OperationService has created
|
|
180
|
+
// the operation record. Never move this call into factory creation.
|
|
181
|
+
return await captureChatGPTRequest({
|
|
182
|
+
...normalized,
|
|
183
|
+
request,
|
|
184
|
+
files,
|
|
185
|
+
captureRequest,
|
|
186
|
+
recoveryTarget: undefined
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
return createRuntimeOperationBrowserAdapter(adapterOptions);
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
/** Alias for integrations that use the "runtime" naming first. */
|
|
194
|
+
export const createChatGPTOperationRuntimeFactory = createChatGPTOperationAdapterFactory;
|
|
195
|
+
/**
|
|
196
|
+
* Create the restart-safe handle factory. Recovery always attaches by the
|
|
197
|
+
* durable tab id; it never falls back to the selected tab or a replacement.
|
|
198
|
+
*/
|
|
199
|
+
export function createChatGPTOperationHandleAdapterFactory(options) {
|
|
200
|
+
const normalized = normalizeFactoryOptions(options);
|
|
201
|
+
return async (context) => {
|
|
202
|
+
const target = snapshotTargetBinding(context.target);
|
|
203
|
+
const targetRequest = Object.freeze({
|
|
204
|
+
type: "tab_id",
|
|
205
|
+
tabId: target.tabId
|
|
206
|
+
});
|
|
207
|
+
const adapterOptions = {
|
|
208
|
+
owner: normalized.owner,
|
|
209
|
+
evidenceDigest: normalized.evidenceDigest,
|
|
210
|
+
...(normalized.coordinator === undefined ? {} : { coordinator: normalized.coordinator }),
|
|
211
|
+
...(normalized.transactionTimeoutMs === undefined ? {} : { transactionTimeoutMs: normalized.transactionTimeoutMs }),
|
|
212
|
+
exposeStaging: false,
|
|
213
|
+
exposeControl: true,
|
|
214
|
+
recovery: Object.freeze({
|
|
215
|
+
operationId: context.operationId,
|
|
216
|
+
requestDigest: context.requestDigest,
|
|
217
|
+
surface: context.surface,
|
|
218
|
+
target,
|
|
219
|
+
targetRequest
|
|
220
|
+
}),
|
|
221
|
+
capture: async (captureRequest) => await captureChatGPTRequest({
|
|
222
|
+
...normalized,
|
|
223
|
+
request: undefined,
|
|
224
|
+
files: Object.freeze([]),
|
|
225
|
+
captureRequest,
|
|
226
|
+
recoveryTarget: target
|
|
227
|
+
})
|
|
228
|
+
};
|
|
229
|
+
return createRuntimeOperationBrowserAdapter(adapterOptions);
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
/** Alias retained for callers that call the restart path a recovery factory. */
|
|
233
|
+
export const createChatGPTOperationRecoveryFactory = createChatGPTOperationHandleAdapterFactory;
|
|
234
|
+
function normalizeFactoryOptions(value) {
|
|
235
|
+
if (value === null || typeof value !== "object")
|
|
236
|
+
throw new ChatGPTRuntimeFactoryError();
|
|
237
|
+
assertOwnDataKeys(value, [
|
|
238
|
+
"env",
|
|
239
|
+
"owner",
|
|
240
|
+
"evidenceDigest",
|
|
241
|
+
"coordinator",
|
|
242
|
+
"transactionTimeoutMs",
|
|
243
|
+
"surfaceTimeoutMs",
|
|
244
|
+
"capabilities",
|
|
245
|
+
"primitives"
|
|
246
|
+
]);
|
|
247
|
+
const env = snapshotRuntimeEnv(readDataProperty(value, "env"));
|
|
248
|
+
const owner = snapshotOwner(readDataProperty(value, "owner"));
|
|
249
|
+
const evidenceDigest = readDataProperty(value, "evidenceDigest");
|
|
250
|
+
if (typeof evidenceDigest !== "function")
|
|
251
|
+
throw new ChatGPTRuntimeFactoryError();
|
|
252
|
+
const coordinatorValue = readDataProperty(value, "coordinator");
|
|
253
|
+
const coordinator = coordinatorValue ?? getProcessTabCoordinator();
|
|
254
|
+
if (coordinator === null || typeof coordinator !== "object"
|
|
255
|
+
|| typeof coordinator.withBrowserAcquisition !== "function"
|
|
256
|
+
|| typeof coordinator.withTabTransaction !== "function") {
|
|
257
|
+
throw new ChatGPTRuntimeFactoryError();
|
|
258
|
+
}
|
|
259
|
+
const transactionTimeoutMs = readDataProperty(value, "transactionTimeoutMs");
|
|
260
|
+
if (transactionTimeoutMs !== undefined
|
|
261
|
+
&& (!Number.isSafeInteger(transactionTimeoutMs) || transactionTimeoutMs < 1 || transactionTimeoutMs > 120_000)) {
|
|
262
|
+
throw new ChatGPTRuntimeFactoryError();
|
|
263
|
+
}
|
|
264
|
+
const surfaceTimeoutMs = readDataProperty(value, "surfaceTimeoutMs") ?? DEFAULT_SURFACE_TIMEOUT_MS;
|
|
265
|
+
if (!Number.isSafeInteger(surfaceTimeoutMs) || surfaceTimeoutMs < 0 || surfaceTimeoutMs > MAX_SURFACE_TIMEOUT_MS) {
|
|
266
|
+
throw new ChatGPTRuntimeFactoryError();
|
|
267
|
+
}
|
|
268
|
+
const capabilities = snapshotCapabilities(readDataProperty(value, "capabilities"));
|
|
269
|
+
const primitives = readDataProperty(value, "primitives");
|
|
270
|
+
if (primitives !== undefined && typeof primitives !== "function") {
|
|
271
|
+
throw new ChatGPTRuntimeFactoryError();
|
|
272
|
+
}
|
|
273
|
+
return Object.freeze({
|
|
274
|
+
env,
|
|
275
|
+
owner,
|
|
276
|
+
evidenceDigest,
|
|
277
|
+
coordinator,
|
|
278
|
+
...(transactionTimeoutMs === undefined ? {} : { transactionTimeoutMs }),
|
|
279
|
+
surfaceTimeoutMs,
|
|
280
|
+
capabilities,
|
|
281
|
+
...(primitives === undefined ? {} : { primitives })
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
function snapshotOwner(value) {
|
|
285
|
+
if (value === null || typeof value !== "object")
|
|
286
|
+
throw new ChatGPTRuntimeFactoryError();
|
|
287
|
+
assertOwnDataKeys(value, ["backendSessionId", "ownerId", "operationId"]);
|
|
288
|
+
const backendSessionId = readDataProperty(value, "backendSessionId");
|
|
289
|
+
const ownerId = readDataProperty(value, "ownerId");
|
|
290
|
+
if (typeof backendSessionId !== "string" || !ID_PATTERN.test(backendSessionId)) {
|
|
291
|
+
throw new ChatGPTRuntimeFactoryError();
|
|
292
|
+
}
|
|
293
|
+
if (ownerId !== undefined && (typeof ownerId !== "string" || !ID_PATTERN.test(ownerId))) {
|
|
294
|
+
throw new ChatGPTRuntimeFactoryError();
|
|
295
|
+
}
|
|
296
|
+
return Object.freeze({
|
|
297
|
+
backendSessionId,
|
|
298
|
+
...(ownerId === undefined ? {} : { ownerId })
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
function snapshotRuntimeEnv(value) {
|
|
302
|
+
if (value === null || typeof value !== "object")
|
|
303
|
+
throw new ChatGPTRuntimeFactoryError();
|
|
304
|
+
assertOwnDataKeys(value, ["agent", "browser", "page", "clipboard", "now", "expectedTabId"]);
|
|
305
|
+
const snapshot = {};
|
|
306
|
+
const agent = readDataProperty(value, "agent");
|
|
307
|
+
const browser = readDataProperty(value, "browser");
|
|
308
|
+
const page = readDataProperty(value, "page");
|
|
309
|
+
const clipboard = readDataProperty(value, "clipboard");
|
|
310
|
+
const now = readDataProperty(value, "now");
|
|
311
|
+
const expectedTabId = readDataProperty(value, "expectedTabId");
|
|
312
|
+
if (now !== undefined && typeof now !== "function")
|
|
313
|
+
throw new ChatGPTRuntimeFactoryError();
|
|
314
|
+
if (expectedTabId !== undefined && (typeof expectedTabId !== "string" || !ID_PATTERN.test(expectedTabId))) {
|
|
315
|
+
throw new ChatGPTRuntimeFactoryError();
|
|
316
|
+
}
|
|
317
|
+
if (agent !== undefined)
|
|
318
|
+
snapshot.agent = agent;
|
|
319
|
+
if (browser !== undefined)
|
|
320
|
+
snapshot.browser = unwrapCoordinatedBrowser(browser);
|
|
321
|
+
if (page !== undefined)
|
|
322
|
+
snapshot.page = unwrapCoordinatedPage(page);
|
|
323
|
+
if (clipboard !== undefined && clipboard !== null && typeof clipboard === "object") {
|
|
324
|
+
snapshot.clipboard = clipboard;
|
|
325
|
+
}
|
|
326
|
+
if (now !== undefined)
|
|
327
|
+
snapshot.now = now;
|
|
328
|
+
if (expectedTabId !== undefined)
|
|
329
|
+
snapshot.expectedTabId = expectedTabId;
|
|
330
|
+
return Object.freeze(snapshot);
|
|
331
|
+
}
|
|
332
|
+
function snapshotRequest(request) {
|
|
333
|
+
if (request === null || typeof request !== "object")
|
|
334
|
+
throw new ChatGPTRuntimeFactoryError();
|
|
335
|
+
assertOwnDataKeys(request, [
|
|
336
|
+
"schemaVersion",
|
|
337
|
+
"operationId",
|
|
338
|
+
"surface",
|
|
339
|
+
"prompt",
|
|
340
|
+
"target",
|
|
341
|
+
"configuration",
|
|
342
|
+
"files",
|
|
343
|
+
"capture",
|
|
344
|
+
"timeoutMs"
|
|
345
|
+
]);
|
|
346
|
+
const operationId = readDataProperty(request, "operationId");
|
|
347
|
+
const surface = readDataProperty(request, "surface");
|
|
348
|
+
const prompt = readDataProperty(request, "prompt");
|
|
349
|
+
const target = readDataProperty(request, "target");
|
|
350
|
+
const configuration = readDataProperty(request, "configuration");
|
|
351
|
+
const capture = readDataProperty(request, "capture");
|
|
352
|
+
const files = readDataProperty(request, "files");
|
|
353
|
+
const schemaVersion = readDataProperty(request, "schemaVersion");
|
|
354
|
+
const timeoutMs = readDataProperty(request, "timeoutMs");
|
|
355
|
+
if (typeof operationId !== "string" || typeof surface !== "string" || typeof prompt !== "string"
|
|
356
|
+
|| !isOperationSurface(surface) || target === undefined || typeof target !== "object"
|
|
357
|
+
|| schemaVersion !== "chatgpt.browser_control.operation_request.v1") {
|
|
358
|
+
throw new ChatGPTRuntimeFactoryError();
|
|
359
|
+
}
|
|
360
|
+
const copy = {
|
|
361
|
+
schemaVersion,
|
|
362
|
+
operationId,
|
|
363
|
+
surface,
|
|
364
|
+
prompt,
|
|
365
|
+
target: snapshotTargetRequest(target)
|
|
366
|
+
};
|
|
367
|
+
if (configuration !== undefined)
|
|
368
|
+
copy.configuration = cloneSafeData(configuration);
|
|
369
|
+
if (capture !== undefined)
|
|
370
|
+
copy.capture = cloneSafeData(capture);
|
|
371
|
+
if (files !== undefined)
|
|
372
|
+
copy.files = cloneSafeData(files);
|
|
373
|
+
if (timeoutMs !== undefined)
|
|
374
|
+
copy.timeoutMs = timeoutMs;
|
|
375
|
+
return Object.freeze(copy);
|
|
376
|
+
}
|
|
377
|
+
function snapshotControlRequest(request) {
|
|
378
|
+
if (request === null || typeof request !== "object")
|
|
379
|
+
throw new ChatGPTRuntimeFactoryError();
|
|
380
|
+
assertOwnDataKeys(request, [
|
|
381
|
+
"schemaVersion",
|
|
382
|
+
"controlActionId",
|
|
383
|
+
"parent",
|
|
384
|
+
"action",
|
|
385
|
+
"expectedAssistantTurnId",
|
|
386
|
+
"steerPrompt",
|
|
387
|
+
"timeoutMs"
|
|
388
|
+
]);
|
|
389
|
+
const schemaVersion = readDataProperty(request, "schemaVersion");
|
|
390
|
+
const controlActionId = readDataProperty(request, "controlActionId");
|
|
391
|
+
const parent = readDataProperty(request, "parent");
|
|
392
|
+
const action = readDataProperty(request, "action");
|
|
393
|
+
const expectedAssistantTurnId = readDataProperty(request, "expectedAssistantTurnId");
|
|
394
|
+
const steerPrompt = readDataProperty(request, "steerPrompt");
|
|
395
|
+
const timeoutMs = readDataProperty(request, "timeoutMs");
|
|
396
|
+
if (schemaVersion !== "chatgpt.browser_control.operation_control_request.v1"
|
|
397
|
+
|| typeof controlActionId !== "string"
|
|
398
|
+
|| !ID_PATTERN.test(controlActionId)
|
|
399
|
+
|| parent === undefined
|
|
400
|
+
|| typeof parent !== "object"
|
|
401
|
+
|| Array.isArray(parent)
|
|
402
|
+
|| (action !== "stop" && action !== "steer")
|
|
403
|
+
|| typeof expectedAssistantTurnId !== "string"
|
|
404
|
+
|| !ID_PATTERN.test(expectedAssistantTurnId)
|
|
405
|
+
|| (steerPrompt !== undefined && typeof steerPrompt !== "string")
|
|
406
|
+
|| (action === "steer" && typeof steerPrompt !== "string")
|
|
407
|
+
|| (action === "stop" && steerPrompt !== undefined)
|
|
408
|
+
|| (timeoutMs !== undefined && (typeof timeoutMs !== "number" || !Number.isSafeInteger(timeoutMs) || timeoutMs < 0))) {
|
|
409
|
+
throw new ChatGPTRuntimeFactoryError();
|
|
410
|
+
}
|
|
411
|
+
return Object.freeze({
|
|
412
|
+
schemaVersion,
|
|
413
|
+
controlActionId,
|
|
414
|
+
parent: cloneSafeData(parent),
|
|
415
|
+
action,
|
|
416
|
+
expectedAssistantTurnId,
|
|
417
|
+
...(steerPrompt === undefined ? {} : { steerPrompt }),
|
|
418
|
+
...(timeoutMs === undefined ? {} : { timeoutMs: timeoutMs })
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
function snapshotTargetRequest(value) {
|
|
422
|
+
if (value === null || typeof value !== "object")
|
|
423
|
+
throw new ChatGPTRuntimeFactoryError();
|
|
424
|
+
const type = readDataProperty(value, "type");
|
|
425
|
+
switch (type) {
|
|
426
|
+
case "new":
|
|
427
|
+
case "selected_tab":
|
|
428
|
+
assertOwnDataKeys(value, ["type"]);
|
|
429
|
+
return Object.freeze({ type });
|
|
430
|
+
case "tab_id": {
|
|
431
|
+
assertOwnDataKeys(value, ["type", "tabId"]);
|
|
432
|
+
const tabId = readDataProperty(value, "tabId");
|
|
433
|
+
if (typeof tabId !== "string" || !ID_PATTERN.test(tabId))
|
|
434
|
+
throw new ChatGPTRuntimeFactoryError();
|
|
435
|
+
return Object.freeze({ type, tabId });
|
|
436
|
+
}
|
|
437
|
+
case "conversation_id": {
|
|
438
|
+
assertOwnDataKeys(value, ["type", "conversationId"]);
|
|
439
|
+
const conversationId = readDataProperty(value, "conversationId");
|
|
440
|
+
if (typeof conversationId !== "string" || !ID_PATTERN.test(conversationId))
|
|
441
|
+
throw new ChatGPTRuntimeFactoryError();
|
|
442
|
+
return Object.freeze({ type, conversationId });
|
|
443
|
+
}
|
|
444
|
+
case "url": {
|
|
445
|
+
assertOwnDataKeys(value, ["type", "url"]);
|
|
446
|
+
const url = readDataProperty(value, "url");
|
|
447
|
+
if (typeof url !== "string")
|
|
448
|
+
throw new ChatGPTRuntimeFactoryError();
|
|
449
|
+
const canonical = canonicalChatGPTUrl(url);
|
|
450
|
+
if (canonical === undefined)
|
|
451
|
+
throw new ChatGPTRuntimeFactoryError();
|
|
452
|
+
return Object.freeze({ type, url: canonical });
|
|
453
|
+
}
|
|
454
|
+
default:
|
|
455
|
+
throw new ChatGPTRuntimeFactoryError();
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
function snapshotTargetBinding(value) {
|
|
459
|
+
const copy = cloneSafeData(value);
|
|
460
|
+
if (copy === null || typeof copy !== "object" || Array.isArray(copy))
|
|
461
|
+
throw new ChatGPTRuntimeFactoryError();
|
|
462
|
+
const target = copy;
|
|
463
|
+
if (typeof target.providerId !== "string" || typeof target.browserId !== "string"
|
|
464
|
+
|| typeof target.tabId !== "string" || typeof target.coordinationScope !== "string"
|
|
465
|
+
|| !ID_PATTERN.test(target.providerId) || !ID_PATTERN.test(target.browserId) || !ID_PATTERN.test(target.tabId)
|
|
466
|
+
|| (target.coordinationScope !== "process" && target.coordinationScope !== "provider")) {
|
|
467
|
+
throw new ChatGPTRuntimeFactoryError();
|
|
468
|
+
}
|
|
469
|
+
if (target.targetLifecycle === "new_pending")
|
|
470
|
+
throw new ChatGPTRuntimeFactoryError();
|
|
471
|
+
return Object.freeze(target);
|
|
472
|
+
}
|
|
473
|
+
async function captureChatGPTRequest(options) {
|
|
474
|
+
const targetRequest = options.recoveryTarget === undefined
|
|
475
|
+
? options.request === undefined ? undefined : options.request.target
|
|
476
|
+
: Object.freeze({ type: "tab_id", tabId: options.recoveryTarget.tabId });
|
|
477
|
+
if (targetRequest === undefined)
|
|
478
|
+
throw new ChatGPTRuntimeFactoryError();
|
|
479
|
+
const targetSurface = options.captureRequest.surface;
|
|
480
|
+
const bootstrap = bootstrapArgsForTarget(targetRequest);
|
|
481
|
+
const bootstrapEnv = bootstrapEnvironment(options.env, targetRequest, options.recoveryTarget);
|
|
482
|
+
const acquisitionOwner = Object.freeze({
|
|
483
|
+
...options.owner,
|
|
484
|
+
operationId: options.captureRequest.operationId
|
|
485
|
+
});
|
|
486
|
+
const coordination = Object.freeze({
|
|
487
|
+
coordinator: options.coordinator,
|
|
488
|
+
owner: acquisitionOwner
|
|
489
|
+
});
|
|
490
|
+
try {
|
|
491
|
+
bootstrapEnv.browser = await resolveChatGPTBrowser(bootstrapEnv, coordination);
|
|
492
|
+
}
|
|
493
|
+
catch {
|
|
494
|
+
throw new ChatGPTRuntimeFactoryError();
|
|
495
|
+
}
|
|
496
|
+
let selectedTabId;
|
|
497
|
+
if (targetRequest.type === "selected_tab") {
|
|
498
|
+
// The browser wrapper performs this one read through the same process
|
|
499
|
+
// coordinator. Supplying the resulting exact page prevents the broader
|
|
500
|
+
// user-open-tab fallback from turning "selected" into "first ChatGPT tab".
|
|
501
|
+
const selected = await selectExactSelectedPage(bootstrapEnv.browser);
|
|
502
|
+
if (selected === undefined)
|
|
503
|
+
throw new ChatGPTRuntimeFactoryError();
|
|
504
|
+
selectedTabId = tabIdFromPage(selected);
|
|
505
|
+
if (selectedTabId === undefined || !ID_PATTERN.test(selectedTabId)) {
|
|
506
|
+
throw new ChatGPTRuntimeFactoryError();
|
|
507
|
+
}
|
|
508
|
+
bootstrapEnv.page = selected;
|
|
509
|
+
}
|
|
510
|
+
const env = Object.freeze(bootstrapEnv);
|
|
511
|
+
let attached;
|
|
512
|
+
try {
|
|
513
|
+
attached = await attachChatGPTBrowser(env, bootstrap, coordination);
|
|
514
|
+
}
|
|
515
|
+
catch {
|
|
516
|
+
throw new ChatGPTRuntimeFactoryError();
|
|
517
|
+
}
|
|
518
|
+
const rawPage = unwrapCoordinatedPage(attached.page);
|
|
519
|
+
const browserId = coordinatedBrowserResource(attached.browser).key;
|
|
520
|
+
const browserResource = coordinatedBrowserResource(attached.browser).key;
|
|
521
|
+
const tabId = tabIdFromPage(attached.page);
|
|
522
|
+
if (tabId === undefined || !ID_PATTERN.test(tabId))
|
|
523
|
+
throw new ChatGPTRuntimeFactoryError();
|
|
524
|
+
if (attached.tabId !== undefined && attached.tabId !== tabId)
|
|
525
|
+
throw new ChatGPTRuntimeFactoryError();
|
|
526
|
+
if (targetRequest.type === "tab_id" && tabId !== targetRequest.tabId) {
|
|
527
|
+
throw new ChatGPTRuntimeFactoryError();
|
|
528
|
+
}
|
|
529
|
+
if (targetRequest.type === "selected_tab" && (selectedTabId === undefined || tabId !== selectedTabId)) {
|
|
530
|
+
throw new ChatGPTRuntimeFactoryError();
|
|
531
|
+
}
|
|
532
|
+
if (options.recoveryTarget !== undefined) {
|
|
533
|
+
assertRecoveredBrowserIdentity(options.recoveryTarget, browserId, tabId);
|
|
534
|
+
}
|
|
535
|
+
await options.coordinator.withBrowserAcquisition(browserResource, {
|
|
536
|
+
owner: acquisitionOwner,
|
|
537
|
+
priority: options.recoveryTarget === undefined ? "mutation" : "read",
|
|
538
|
+
signal: options.captureRequest.signal,
|
|
539
|
+
...(options.transactionTimeoutMs === undefined ? {} : { timeoutMs: options.transactionTimeoutMs }),
|
|
540
|
+
label: "operation-target-prepare"
|
|
541
|
+
}, async () => {
|
|
542
|
+
await validateExactNavigation(rawPage, targetRequest);
|
|
543
|
+
await ensureSurface(rawPage, targetSurface, targetRequest.type === "new", options.surfaceTimeoutMs, tabId);
|
|
544
|
+
});
|
|
545
|
+
const capabilities = options.capabilities;
|
|
546
|
+
const observeCurrent = async (request) => {
|
|
547
|
+
const observationTarget = observationTargetForBinding(request.target);
|
|
548
|
+
const observed = await observePage(request.page, request.operationId, observationTarget, options.evidenceDigest);
|
|
549
|
+
return Object.freeze({ evidence: observed.snapshot.target });
|
|
550
|
+
};
|
|
551
|
+
const resolveTargetEvidence = async (request) => {
|
|
552
|
+
if (!sameTargetRequest(request.target, targetRequest)) {
|
|
553
|
+
throw new ChatGPTRuntimeFactoryError();
|
|
554
|
+
}
|
|
555
|
+
return await options.coordinator.withBrowserAcquisition(browserResource, {
|
|
556
|
+
owner: acquisitionOwner,
|
|
557
|
+
priority: "read",
|
|
558
|
+
signal: request.signal,
|
|
559
|
+
...(options.transactionTimeoutMs === undefined ? {} : { timeoutMs: options.transactionTimeoutMs }),
|
|
560
|
+
label: "operation-target-bind"
|
|
561
|
+
}, async () => {
|
|
562
|
+
await validateExactNavigation(rawPage, request.target);
|
|
563
|
+
const observationTarget = observationTargetForRequest(request, browserId, tabId, targetRequest);
|
|
564
|
+
const observed = await observePage(rawPage, request.operationId, observationTarget, options.evidenceDigest);
|
|
565
|
+
const anchor = observed.newTargetAnchor;
|
|
566
|
+
return Object.freeze({
|
|
567
|
+
page: rawPage,
|
|
568
|
+
evidence: observed.snapshot.target,
|
|
569
|
+
...(request.target.type === "new" ? { targetLifecycle: "new_pending" } : {}),
|
|
570
|
+
...(anchor === undefined ? {} : {
|
|
571
|
+
newTargetAnchorDigest: anchor.anchorDigest,
|
|
572
|
+
blankTaskEvidenceDigest: anchor.blankTaskEvidenceDigest
|
|
573
|
+
}),
|
|
574
|
+
capabilities
|
|
575
|
+
});
|
|
576
|
+
});
|
|
577
|
+
};
|
|
578
|
+
const request = options.request;
|
|
579
|
+
const productionOptions = {
|
|
580
|
+
evidenceDigest: options.evidenceDigest,
|
|
581
|
+
operationId: options.captureRequest.operationId,
|
|
582
|
+
requestDigest: options.captureRequest.requestDigest,
|
|
583
|
+
...(request === undefined ? {} : { desiredComposerText: request.prompt }),
|
|
584
|
+
...(options.recoveryTarget === undefined ? {} : { target: options.recoveryTarget })
|
|
585
|
+
};
|
|
586
|
+
const attachments = request === undefined || options.files.length === 0
|
|
587
|
+
? undefined
|
|
588
|
+
: await createChatGPTAttachmentProviderAsync({
|
|
589
|
+
evidenceDigest: options.evidenceDigest,
|
|
590
|
+
files: options.files,
|
|
591
|
+
identityDigest: (ordinal, manifest) => options.evidenceDigest("file-manifest", { ordinal, ...manifest }),
|
|
592
|
+
revalidateFile: identity => revalidateOperationFile(identity, {
|
|
593
|
+
signal: options.captureRequest.signal
|
|
594
|
+
}),
|
|
595
|
+
signal: options.captureRequest.signal
|
|
596
|
+
});
|
|
597
|
+
const production = createProductionOperationPrimitives({
|
|
598
|
+
...productionOptions,
|
|
599
|
+
...(attachments === undefined
|
|
600
|
+
? {}
|
|
601
|
+
: { observeAttachments: attachments.observeAttachments })
|
|
602
|
+
});
|
|
603
|
+
const providerPrimitives = attachments === undefined
|
|
604
|
+
? production
|
|
605
|
+
: mergePrimitivePorts(production, {
|
|
606
|
+
submission: {
|
|
607
|
+
handoffFiles: attachments.handoffFilesForAdapter,
|
|
608
|
+
observeAttachments: attachments.observeAttachments
|
|
609
|
+
}
|
|
610
|
+
});
|
|
611
|
+
const artifactSource = hasTransferDestination(request)
|
|
612
|
+
? createProductionChatGPTArtifacts({
|
|
613
|
+
page: rawPage,
|
|
614
|
+
evidenceDigest: options.evidenceDigest,
|
|
615
|
+
signal: options.captureRequest.signal,
|
|
616
|
+
...(options.transactionTimeoutMs === undefined ? {} : { timeoutMs: options.transactionTimeoutMs })
|
|
617
|
+
})
|
|
618
|
+
: undefined;
|
|
619
|
+
const artifactPrimitive = artifactSource === undefined
|
|
620
|
+
? undefined
|
|
621
|
+
: productionArtifactPrimitive(artifactSource);
|
|
622
|
+
let capturedPrimitives = artifactPrimitive === undefined
|
|
623
|
+
? providerPrimitives
|
|
624
|
+
: mergePrimitivePorts(providerPrimitives, { artifacts: artifactPrimitive });
|
|
625
|
+
if (options.control !== undefined && options.control.request.action === "steer") {
|
|
626
|
+
if (options.control.request.steerPrompt === undefined)
|
|
627
|
+
throw new ChatGPTRuntimeFactoryError();
|
|
628
|
+
const steerPrimitive = createProductionWorkSteerPrimitive({
|
|
629
|
+
evidenceDigest: options.evidenceDigest,
|
|
630
|
+
operationId: options.captureRequest.operationId,
|
|
631
|
+
parentRequestDigest: options.captureRequest.requestDigest,
|
|
632
|
+
targetBindingDigest: options.control.targetBindingDigest,
|
|
633
|
+
controlActionId: options.control.request.controlActionId,
|
|
634
|
+
expectedAssistantTurnId: options.control.request.expectedAssistantTurnId,
|
|
635
|
+
target: options.recoveryTarget ?? (() => { throw new ChatGPTRuntimeFactoryError(); })(),
|
|
636
|
+
prompt: options.control.request.steerPrompt,
|
|
637
|
+
observe: requestValue => observeWorkSteerPage(requestValue, options.evidenceDigest, options.recoveryTarget),
|
|
638
|
+
resolveComposer: requestValue => resolveWorkComposer(requestValue.page),
|
|
639
|
+
resolveSendControl: requestValue => resolveWorkSendControl(requestValue.page)
|
|
640
|
+
});
|
|
641
|
+
capturedPrimitives = mergePrimitivePorts(capturedPrimitives, {
|
|
642
|
+
control: workSteerControlPrimitive(steerPrimitive, capturedPrimitives.control)
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
const configuration = request?.configuration === undefined
|
|
646
|
+
? undefined
|
|
647
|
+
: createProductionConfigurationStaging({
|
|
648
|
+
evidenceDigest: options.evidenceDigest,
|
|
649
|
+
operationId: options.captureRequest.operationId,
|
|
650
|
+
requestDigest: options.captureRequest.requestDigest,
|
|
651
|
+
surface: targetSurface,
|
|
652
|
+
configuration: request.configuration
|
|
653
|
+
});
|
|
654
|
+
let primitives = composePrimitives(capturedPrimitives, configuration, options, {
|
|
655
|
+
operationId: options.captureRequest.operationId,
|
|
656
|
+
requestDigest: options.captureRequest.requestDigest,
|
|
657
|
+
surface: targetSurface,
|
|
658
|
+
...(request === undefined ? {} : {
|
|
659
|
+
prompt: request.prompt,
|
|
660
|
+
...(request.configuration === undefined ? {} : { configuration: request.configuration })
|
|
661
|
+
}),
|
|
662
|
+
files: options.files,
|
|
663
|
+
signal: options.captureRequest.signal,
|
|
664
|
+
page: rawPage,
|
|
665
|
+
...(options.recoveryTarget === undefined ? {} : { target: options.recoveryTarget })
|
|
666
|
+
});
|
|
667
|
+
if (artifactPrimitive === undefined && primitives.artifacts !== undefined) {
|
|
668
|
+
const { artifacts: _artifacts, ...withoutArtifacts } = primitives;
|
|
669
|
+
primitives = Object.freeze(withoutArtifacts);
|
|
670
|
+
}
|
|
671
|
+
return Object.freeze({
|
|
672
|
+
page: rawPage,
|
|
673
|
+
capabilities,
|
|
674
|
+
resolveTargetEvidence,
|
|
675
|
+
observeCurrentTarget: observeCurrent,
|
|
676
|
+
...(artifactPrimitive === undefined || !hasTransferDestination(request)
|
|
677
|
+
? {}
|
|
678
|
+
: { outputDirectory: request.capture.outputDirectory }),
|
|
679
|
+
primitives
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
function productionArtifactPrimitive(source) {
|
|
683
|
+
return Object.freeze({
|
|
684
|
+
acquireDownload: (request, page) => source.acquireDownload(request, page),
|
|
685
|
+
materializeDownload: source.materializeDownload
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
function workSteerControlPrimitive(primitive, existing) {
|
|
689
|
+
return Object.freeze({
|
|
690
|
+
...(existing ?? {}),
|
|
691
|
+
prepareSteer: async (request, page) => mapWorkSteerResult(await primitive.prepare({ page, signal: request.signal, deadlineAt: request.deadlineAt }), request, "prepare"),
|
|
692
|
+
executeSteerPrepared: async (request, page) => mapWorkSteerResult(await primitive.executePrepared({ page, prepared: productionPreparedFromControl(request.prepared), signal: request.signal, deadlineAt: request.deadlineAt }), request, "execute_prepared", request.prepared),
|
|
693
|
+
verifySteer: async (request, page) => mapWorkSteerResult(await primitive.verify({ page, prepared: productionPreparedFromControl(request.prepared), signal: request.signal, deadlineAt: request.deadlineAt }), request, "verify", request.prepared),
|
|
694
|
+
recoverSteer: async (request, page) => mapWorkSteerResult(await primitive.recover({ page, prepared: productionPreparedFromControl(request.prepared), baseline: request.baseline, signal: request.signal, deadlineAt: request.deadlineAt }), request, "recovery", request.prepared)
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
function productionPreparedFromControl(prepared) {
|
|
698
|
+
return Object.freeze({
|
|
699
|
+
schemaVersion: "chatgpt.browser_control.production_work_steer.v1",
|
|
700
|
+
operationId: prepared.parentOperationId,
|
|
701
|
+
parentRequestDigest: prepared.parentRequestDigest,
|
|
702
|
+
targetBindingDigest: prepared.parentTargetBindingDigest,
|
|
703
|
+
controlActionId: prepared.controlActionId,
|
|
704
|
+
action: "work_steer",
|
|
705
|
+
expectedAssistantTurnId: prepared.expectedAssistantTurnId,
|
|
706
|
+
assistantBranchId: prepared.assistantBranchId,
|
|
707
|
+
assistantParentTurnId: prepared.assistantParentTurnId,
|
|
708
|
+
baselineSnapshotDigest: prepared.baselineSnapshotDigest,
|
|
709
|
+
preparedDigest: prepared.preparedDigest,
|
|
710
|
+
baseline: prepared.baseline
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
function mapWorkSteerResult(result, request, phase, prepared) {
|
|
714
|
+
const identity = "prepared" in request
|
|
715
|
+
? request.prepared
|
|
716
|
+
: request;
|
|
717
|
+
const base = {
|
|
718
|
+
schemaVersion: "chatgpt.browser_control.operation_control_coordinator.v1",
|
|
719
|
+
phase,
|
|
720
|
+
parentOperationId: "prepared" in request ? identity.parentOperationId : identity.parentOperationId,
|
|
721
|
+
parentRequestDigest: "prepared" in request ? identity.parentRequestDigest : identity.parentRequestDigest,
|
|
722
|
+
parentTargetBindingDigest: "prepared" in request ? identity.parentTargetBindingDigest : identity.parentTargetBindingDigest,
|
|
723
|
+
controlActionId: "prepared" in request ? identity.controlActionId : identity.controlActionId,
|
|
724
|
+
action: "steer",
|
|
725
|
+
requestDigest: "prepared" in request ? identity.requestDigest : identity.requestDigest,
|
|
726
|
+
expectedAssistantTurnId: "prepared" in request ? identity.expectedAssistantTurnId : identity.expectedAssistantTurnId,
|
|
727
|
+
...(prepared === undefined ? {} : {
|
|
728
|
+
assistantBranchId: prepared.assistantBranchId,
|
|
729
|
+
assistantParentTurnId: prepared.assistantParentTurnId,
|
|
730
|
+
baselineSnapshotDigest: prepared.baselineSnapshotDigest,
|
|
731
|
+
preparedDigest: prepared.preparedDigest
|
|
732
|
+
})
|
|
733
|
+
};
|
|
734
|
+
if (result.status === "prepared") {
|
|
735
|
+
const productionPrepared = result.prepared;
|
|
736
|
+
return Object.freeze({
|
|
737
|
+
...base,
|
|
738
|
+
phase: "prepare",
|
|
739
|
+
status: "prepared",
|
|
740
|
+
observationRequired: false,
|
|
741
|
+
mutationBoundary: "none",
|
|
742
|
+
prepared: Object.freeze({
|
|
743
|
+
schemaVersion: base.schemaVersion,
|
|
744
|
+
parentOperationId: productionPrepared.operationId,
|
|
745
|
+
parentRequestDigest: productionPrepared.parentRequestDigest,
|
|
746
|
+
parentTargetBindingDigest: productionPrepared.targetBindingDigest,
|
|
747
|
+
controlActionId: productionPrepared.controlActionId,
|
|
748
|
+
action: "steer",
|
|
749
|
+
requestDigest: identity.requestDigest,
|
|
750
|
+
expectedAssistantTurnId: productionPrepared.expectedAssistantTurnId,
|
|
751
|
+
assistantBranchId: productionPrepared.assistantBranchId,
|
|
752
|
+
assistantParentTurnId: productionPrepared.assistantParentTurnId,
|
|
753
|
+
baselineSnapshotDigest: productionPrepared.baselineSnapshotDigest,
|
|
754
|
+
preparedDigest: productionPrepared.preparedDigest,
|
|
755
|
+
baseline: productionPrepared.baseline
|
|
756
|
+
})
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
if (result.status === "executed") {
|
|
760
|
+
return Object.freeze({
|
|
761
|
+
...base,
|
|
762
|
+
phase: "execute_prepared",
|
|
763
|
+
status: "executed",
|
|
764
|
+
observationRequired: true,
|
|
765
|
+
mutationBoundary: "control_may_have_occurred"
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
if (result.status === "satisfied") {
|
|
769
|
+
const receipt = result.receipt;
|
|
770
|
+
return Object.freeze({
|
|
771
|
+
...base,
|
|
772
|
+
phase: phase === "recovery" ? "recovery" : "verify",
|
|
773
|
+
status: "satisfied",
|
|
774
|
+
observationRequired: false,
|
|
775
|
+
mutationBoundary: "control_may_have_occurred",
|
|
776
|
+
assistantBranchId: result.assistantBranchId,
|
|
777
|
+
assistantParentTurnId: result.assistantParentTurnId,
|
|
778
|
+
baselineSnapshotDigest: result.baselineSnapshotDigest,
|
|
779
|
+
preparedDigest: result.preparedDigest,
|
|
780
|
+
receipt: Object.freeze({
|
|
781
|
+
schemaVersion: base.schemaVersion,
|
|
782
|
+
baselineSnapshotDigest: receipt.baselineSnapshotDigest,
|
|
783
|
+
preparedDigest: receipt.preparedDigest,
|
|
784
|
+
assistantTurnId: receipt.assistantTurnId,
|
|
785
|
+
assistantBranchId: receipt.assistantBranchId,
|
|
786
|
+
assistantParentTurnId: receipt.assistantParentTurnId,
|
|
787
|
+
userTurnId: receipt.userTurnId,
|
|
788
|
+
userTurnEvidenceDigest: receipt.userTurnEvidenceDigest,
|
|
789
|
+
postSendDeltaDigest: receipt.postSendDeltaDigest,
|
|
790
|
+
evidenceDigest: receipt.evidenceDigest
|
|
791
|
+
})
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
if (result.status === "blocked") {
|
|
795
|
+
return Object.freeze({
|
|
796
|
+
...base,
|
|
797
|
+
status: "blocked",
|
|
798
|
+
blockerCode: result.blockerCode,
|
|
799
|
+
observationRequired: result.observationRequired,
|
|
800
|
+
mutationBoundary: result.mutationBoundary,
|
|
801
|
+
...(result.evidenceDigest === undefined ? {} : { evidenceDigest: result.evidenceDigest })
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
return Object.freeze({
|
|
805
|
+
...base,
|
|
806
|
+
status: "uncertain",
|
|
807
|
+
blockerCode: result.blockerCode,
|
|
808
|
+
observationRequired: true,
|
|
809
|
+
mutationBoundary: "control_may_have_occurred",
|
|
810
|
+
quarantine: result.quarantine,
|
|
811
|
+
...(result.evidenceDigest === undefined ? {} : { evidenceDigest: result.evidenceDigest })
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
async function observeWorkSteerPage(request, evidenceDigest, targetBinding) {
|
|
815
|
+
return await observeBrowserPage(request.page, {
|
|
816
|
+
operationId: request.operationId,
|
|
817
|
+
target: observationTargetForBinding(targetBinding),
|
|
818
|
+
evidenceDigest,
|
|
819
|
+
responseContent: "metadata",
|
|
820
|
+
...(request.baseline === undefined ? {} : { baseline: request.baseline })
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
async function resolveWorkComposer(page) {
|
|
824
|
+
try {
|
|
825
|
+
const locator = composerTextbox(page);
|
|
826
|
+
const count = typeof locator.count === "function" ? await locator.count() : 0;
|
|
827
|
+
const visible = count === 1 && typeof locator.isVisible === "function" && await locator.isVisible();
|
|
828
|
+
return visible ? { locator, capabilityKey: "chatgpt.composer", candidateCount: 1 } : undefined;
|
|
829
|
+
}
|
|
830
|
+
catch {
|
|
831
|
+
return undefined;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
async function resolveWorkSendControl(page) {
|
|
835
|
+
try {
|
|
836
|
+
const locator = sendButton(page);
|
|
837
|
+
const count = typeof locator.count === "function" ? await locator.count() : 0;
|
|
838
|
+
const visible = count === 1 && typeof locator.isVisible === "function" && await locator.isVisible();
|
|
839
|
+
return visible ? { locator, capabilityKey: "chatgpt.send", localeKey: "locale.registry", candidateCount: 1 } : undefined;
|
|
840
|
+
}
|
|
841
|
+
catch {
|
|
842
|
+
return undefined;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
function composePrimitives(production, configuration, options, context) {
|
|
846
|
+
const productionStaging = production.staging;
|
|
847
|
+
const productionReadCurrent = productionStaging?.readCurrent;
|
|
848
|
+
const productionMutateOnce = productionStaging?.mutateOnce;
|
|
849
|
+
const productionObserve = productionStaging?.observe;
|
|
850
|
+
const configurationReadCurrent = configuration?.readCurrent;
|
|
851
|
+
const configurationMutateOnce = configuration?.mutateOnce;
|
|
852
|
+
const configurationObserve = configuration?.observe;
|
|
853
|
+
const configured = configurationReadCurrent === undefined
|
|
854
|
+
|| configurationMutateOnce === undefined
|
|
855
|
+
|| configurationObserve === undefined
|
|
856
|
+
|| productionReadCurrent === undefined
|
|
857
|
+
|| productionMutateOnce === undefined
|
|
858
|
+
|| productionObserve === undefined
|
|
859
|
+
? productionStaging
|
|
860
|
+
: Object.freeze({
|
|
861
|
+
readCurrent: request => request.kind === "composer_set"
|
|
862
|
+
? productionReadCurrent(request)
|
|
863
|
+
: configurationReadCurrent(request),
|
|
864
|
+
mutateOnce: request => request.kind === "composer_set"
|
|
865
|
+
? productionMutateOnce(request)
|
|
866
|
+
: configurationMutateOnce(request),
|
|
867
|
+
observe: request => request.kind === "composer_set"
|
|
868
|
+
? productionObserve(request)
|
|
869
|
+
: configurationObserve(request)
|
|
870
|
+
});
|
|
871
|
+
let result = Object.freeze({
|
|
872
|
+
...production,
|
|
873
|
+
...(configured === undefined ? {} : { staging: configured })
|
|
874
|
+
});
|
|
875
|
+
const augment = options.primitives?.(context);
|
|
876
|
+
if (augment !== undefined) {
|
|
877
|
+
// Provider integrations normally add one narrow method (for example the
|
|
878
|
+
// chooser handoff) to a production port. Replacing the whole nested port
|
|
879
|
+
// would silently discard the production Send observers or collector
|
|
880
|
+
// ownership logic, so merge at the method boundary instead.
|
|
881
|
+
result = mergePrimitivePorts(result, augment);
|
|
882
|
+
}
|
|
883
|
+
return result;
|
|
884
|
+
}
|
|
885
|
+
function mergePrimitivePorts(base, augment) {
|
|
886
|
+
const merge = (left, right) => {
|
|
887
|
+
if (left === undefined)
|
|
888
|
+
return right === undefined ? undefined : Object.freeze({ ...right });
|
|
889
|
+
if (right === undefined)
|
|
890
|
+
return left;
|
|
891
|
+
return Object.freeze({ ...left, ...right });
|
|
892
|
+
};
|
|
893
|
+
const staging = merge(base.staging, augment.staging);
|
|
894
|
+
const submission = merge(base.submission, augment.submission);
|
|
895
|
+
const collector = merge(base.collector, augment.collector);
|
|
896
|
+
const control = merge(base.control, augment.control);
|
|
897
|
+
const artifacts = merge(base.artifacts, augment.artifacts);
|
|
898
|
+
return Object.freeze({
|
|
899
|
+
...(staging === undefined ? {} : { staging }),
|
|
900
|
+
...(submission === undefined ? {} : { submission }),
|
|
901
|
+
...(collector === undefined ? {} : { collector }),
|
|
902
|
+
...(control === undefined ? {} : { control }),
|
|
903
|
+
...(artifacts === undefined ? {} : { artifacts })
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
function bootstrapArgsForTarget(target) {
|
|
907
|
+
switch (target.type) {
|
|
908
|
+
case "new":
|
|
909
|
+
return Object.freeze({ url: CHATGPT_HOME, preferExistingTab: false });
|
|
910
|
+
case "selected_tab":
|
|
911
|
+
return Object.freeze({
|
|
912
|
+
existingTab: {
|
|
913
|
+
target: { type: "selected", host: "chatgpt" },
|
|
914
|
+
ifMissing: "block",
|
|
915
|
+
ifMultiple: "block",
|
|
916
|
+
requireChatGPT: true
|
|
917
|
+
}
|
|
918
|
+
});
|
|
919
|
+
case "tab_id":
|
|
920
|
+
return Object.freeze({
|
|
921
|
+
existingTab: {
|
|
922
|
+
target: { type: "tabId", tabId: target.tabId },
|
|
923
|
+
ifMissing: "block",
|
|
924
|
+
ifMultiple: "block",
|
|
925
|
+
requireChatGPT: true
|
|
926
|
+
}
|
|
927
|
+
});
|
|
928
|
+
case "conversation_id":
|
|
929
|
+
return Object.freeze({
|
|
930
|
+
url: new URL(`/c/${target.conversationId}`, CHATGPT_HOME).toString(),
|
|
931
|
+
existingTab: {
|
|
932
|
+
target: { type: "conversationId", conversationId: target.conversationId },
|
|
933
|
+
ifMissing: "open",
|
|
934
|
+
ifMultiple: "block",
|
|
935
|
+
requireChatGPT: true
|
|
936
|
+
}
|
|
937
|
+
});
|
|
938
|
+
case "url":
|
|
939
|
+
return Object.freeze({
|
|
940
|
+
url: target.url,
|
|
941
|
+
existingTab: {
|
|
942
|
+
target: { type: "url", url: target.url },
|
|
943
|
+
ifMissing: "open",
|
|
944
|
+
ifMultiple: "block",
|
|
945
|
+
requireChatGPT: true
|
|
946
|
+
}
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
function bootstrapEnvironment(env, target, recoveryTarget) {
|
|
951
|
+
const copy = { ...env };
|
|
952
|
+
// A selected-tab request must be proven through browser.tabs.selected. A
|
|
953
|
+
// cached arbitrary page would make `existingTab: selected` non-exact.
|
|
954
|
+
if (target.type === "new" || target.type === "selected_tab") {
|
|
955
|
+
delete copy.page;
|
|
956
|
+
}
|
|
957
|
+
if (recoveryTarget !== undefined) {
|
|
958
|
+
copy.expectedTabId = recoveryTarget.tabId;
|
|
959
|
+
if (copy.page !== undefined && tabIdFromPage(copy.page) !== recoveryTarget.tabId) {
|
|
960
|
+
delete copy.page;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
return copy;
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* `attachChatGPTBrowser` also supports user-open-tab discovery. That is
|
|
967
|
+
* intentionally broader than an exact selected-tab request, so selected-tab
|
|
968
|
+
* capture is preflighted through the browser's selected-tab primitive and the
|
|
969
|
+
* resulting page is then handed back to attach as its cached exact page.
|
|
970
|
+
*/
|
|
971
|
+
async function selectExactSelectedPage(browser) {
|
|
972
|
+
const tabs = browser?.tabs;
|
|
973
|
+
const selected = tabs?.selected;
|
|
974
|
+
if (typeof selected !== "function")
|
|
975
|
+
return undefined;
|
|
976
|
+
let page;
|
|
977
|
+
try {
|
|
978
|
+
const candidate = await Promise.resolve(selected.call(tabs));
|
|
979
|
+
if (candidate === undefined)
|
|
980
|
+
return undefined;
|
|
981
|
+
// `candidate` is a coordinated page returned by the coordinated browser.
|
|
982
|
+
// Validate its URL while retaining that wrapper so the read stays inside
|
|
983
|
+
// the browser actor; unwrap only after the exact selected-tab proof.
|
|
984
|
+
const url = await Promise.resolve(candidate.url?.()).catch(() => undefined);
|
|
985
|
+
if (!isChatGPTUrl(url))
|
|
986
|
+
return undefined;
|
|
987
|
+
page = unwrapCoordinatedPage(candidate);
|
|
988
|
+
}
|
|
989
|
+
catch {
|
|
990
|
+
return undefined;
|
|
991
|
+
}
|
|
992
|
+
if (page === undefined)
|
|
993
|
+
return undefined;
|
|
994
|
+
return page;
|
|
995
|
+
}
|
|
996
|
+
async function validateExactNavigation(page, target) {
|
|
997
|
+
if (target.type !== "conversation_id" && target.type !== "url")
|
|
998
|
+
return;
|
|
999
|
+
const actual = await Promise.resolve(page.url?.()).catch(() => undefined);
|
|
1000
|
+
const actualCanonical = canonicalChatGPTUrl(actual);
|
|
1001
|
+
if (actualCanonical === undefined)
|
|
1002
|
+
throw new ChatGPTRuntimeFactoryError();
|
|
1003
|
+
if (target.type === "url") {
|
|
1004
|
+
const expected = canonicalChatGPTUrl(target.url);
|
|
1005
|
+
if (expected === undefined || actualCanonical !== expected)
|
|
1006
|
+
throw new ChatGPTRuntimeFactoryError();
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
if (parseConversationId(actualCanonical) !== target.conversationId) {
|
|
1010
|
+
throw new ChatGPTRuntimeFactoryError();
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
async function ensureSurface(page, surface, allowSwitch, timeoutMs, tabId) {
|
|
1014
|
+
const before = detectExperienceFromSnapshot(await readSurfaceSnapshot(page));
|
|
1015
|
+
if (before.experience === surface)
|
|
1016
|
+
return;
|
|
1017
|
+
// A fixed conversation/URL target must never be navigated to a surface home
|
|
1018
|
+
// as a side effect of attachment. The exact target is more important than
|
|
1019
|
+
// a best-effort surface guess; callers can explicitly switch it first.
|
|
1020
|
+
if (!allowSwitch || before.experience === "unknown")
|
|
1021
|
+
throw new ChatGPTRuntimeFactoryError();
|
|
1022
|
+
const result = await openExperience(Object.freeze({ page, expectedTabId: tabId }), { experience: surface, timeoutMs });
|
|
1023
|
+
if (!result.ok || result.data?.experience !== surface)
|
|
1024
|
+
throw new ChatGPTRuntimeFactoryError();
|
|
1025
|
+
const after = detectExperienceFromSnapshot(await readSurfaceSnapshot(page));
|
|
1026
|
+
if (after.experience !== surface)
|
|
1027
|
+
throw new ChatGPTRuntimeFactoryError();
|
|
1028
|
+
}
|
|
1029
|
+
async function observePage(page, operationId, target, evidenceDigest) {
|
|
1030
|
+
return await observeBrowserPage(page, {
|
|
1031
|
+
operationId,
|
|
1032
|
+
target,
|
|
1033
|
+
evidenceDigest,
|
|
1034
|
+
responseContent: "metadata"
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
function observationTargetForRequest(request, browserId, tabId, target) {
|
|
1038
|
+
const expectedConversationId = request.target.type === "conversation_id"
|
|
1039
|
+
? request.target.conversationId
|
|
1040
|
+
: request.target.type === "url" ? parseConversationId(request.target.url) : undefined;
|
|
1041
|
+
return {
|
|
1042
|
+
providerId: "chatgpt",
|
|
1043
|
+
browserId,
|
|
1044
|
+
tabId,
|
|
1045
|
+
coordinationScope: "process",
|
|
1046
|
+
...(target.type === "new" ? { targetLifecycle: "new_pending" } : {}),
|
|
1047
|
+
...(expectedConversationId === undefined ? {} : { expectedConversationId })
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
function observationTargetForBinding(target) {
|
|
1051
|
+
return {
|
|
1052
|
+
providerId: target.providerId,
|
|
1053
|
+
browserId: target.browserId,
|
|
1054
|
+
tabId: target.tabId,
|
|
1055
|
+
coordinationScope: target.coordinationScope,
|
|
1056
|
+
...(target.targetLifecycle === undefined ? {} : { targetLifecycle: target.targetLifecycle }),
|
|
1057
|
+
...(target.conversationId === undefined ? {} : { expectedConversationId: target.conversationId })
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
function assertRecoveredBrowserIdentity(target, browserId, tabId) {
|
|
1061
|
+
if (target.providerId !== "chatgpt" || target.browserId !== browserId || target.tabId !== tabId) {
|
|
1062
|
+
throw new ChatGPTRuntimeFactoryError();
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
function canonicalChatGPTUrl(value) {
|
|
1066
|
+
if (typeof value !== "string" || !isChatGPTUrl(value))
|
|
1067
|
+
return undefined;
|
|
1068
|
+
try {
|
|
1069
|
+
const url = new URL(value);
|
|
1070
|
+
if (url.search !== "" || url.hash !== "")
|
|
1071
|
+
return undefined;
|
|
1072
|
+
url.pathname = url.pathname.replace(/\/{2,}/gu, "/").replace(/\/$/u, "") || "/";
|
|
1073
|
+
return url.toString();
|
|
1074
|
+
}
|
|
1075
|
+
catch {
|
|
1076
|
+
return undefined;
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
function isAbsolutePath(value) {
|
|
1080
|
+
return value.startsWith("/")
|
|
1081
|
+
|| /^[A-Za-z]:[\\/]/u.test(value)
|
|
1082
|
+
|| value.startsWith("\\\\");
|
|
1083
|
+
}
|
|
1084
|
+
function hasTransferDestination(request) {
|
|
1085
|
+
const capture = request?.capture;
|
|
1086
|
+
if (capture === undefined || capture.artifacts !== "transfer" || typeof capture.outputDirectory !== "string") {
|
|
1087
|
+
return false;
|
|
1088
|
+
}
|
|
1089
|
+
return capture.outputDirectory.length > 0
|
|
1090
|
+
&& capture.outputDirectory.length <= 4096
|
|
1091
|
+
&& isAbsolutePath(capture.outputDirectory)
|
|
1092
|
+
&& !/[\u0000-\u001f\u007f]/u.test(capture.outputDirectory);
|
|
1093
|
+
}
|
|
1094
|
+
function isOperationSurface(value) {
|
|
1095
|
+
return value === "chat" || value === "work";
|
|
1096
|
+
}
|
|
1097
|
+
function sameTargetRequest(left, right) {
|
|
1098
|
+
if (left.type !== right.type)
|
|
1099
|
+
return false;
|
|
1100
|
+
switch (left.type) {
|
|
1101
|
+
case "new":
|
|
1102
|
+
case "selected_tab":
|
|
1103
|
+
return true;
|
|
1104
|
+
case "tab_id":
|
|
1105
|
+
return right.type === "tab_id" && left.tabId === right.tabId;
|
|
1106
|
+
case "conversation_id":
|
|
1107
|
+
return right.type === "conversation_id" && left.conversationId === right.conversationId;
|
|
1108
|
+
case "url":
|
|
1109
|
+
return right.type === "url" && left.url === right.url;
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
function readDataProperty(value, key) {
|
|
1113
|
+
try {
|
|
1114
|
+
let current = value;
|
|
1115
|
+
for (let depth = 0; current !== null && depth < 12; depth += 1) {
|
|
1116
|
+
const descriptor = Object.getOwnPropertyDescriptor(current, key);
|
|
1117
|
+
if (descriptor !== undefined) {
|
|
1118
|
+
if (!("value" in descriptor) || descriptor.get !== undefined || descriptor.set !== undefined) {
|
|
1119
|
+
throw new ChatGPTRuntimeFactoryError();
|
|
1120
|
+
}
|
|
1121
|
+
return descriptor.value;
|
|
1122
|
+
}
|
|
1123
|
+
const prototype = Object.getPrototypeOf(current);
|
|
1124
|
+
current = prototype !== null && (typeof prototype === "object" || typeof prototype === "function")
|
|
1125
|
+
? prototype
|
|
1126
|
+
: null;
|
|
1127
|
+
}
|
|
1128
|
+
return undefined;
|
|
1129
|
+
}
|
|
1130
|
+
catch (error) {
|
|
1131
|
+
if (error instanceof ChatGPTRuntimeFactoryError)
|
|
1132
|
+
throw error;
|
|
1133
|
+
throw new ChatGPTRuntimeFactoryError();
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
function snapshotCapabilities(value) {
|
|
1137
|
+
if (value === undefined) {
|
|
1138
|
+
return Object.freeze({
|
|
1139
|
+
stableProviderId: true,
|
|
1140
|
+
stableBrowserId: true,
|
|
1141
|
+
stableTabId: true,
|
|
1142
|
+
authoritativeTabClaim: false,
|
|
1143
|
+
concurrentTabs: false
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
1147
|
+
throw new ChatGPTRuntimeFactoryError();
|
|
1148
|
+
}
|
|
1149
|
+
const keys = [
|
|
1150
|
+
"stableProviderId",
|
|
1151
|
+
"stableBrowserId",
|
|
1152
|
+
"stableTabId",
|
|
1153
|
+
"authoritativeTabClaim",
|
|
1154
|
+
"concurrentTabs"
|
|
1155
|
+
];
|
|
1156
|
+
assertOwnDataKeys(value, keys);
|
|
1157
|
+
const result = {
|
|
1158
|
+
stableProviderId: readDataProperty(value, "stableProviderId") ?? false,
|
|
1159
|
+
stableBrowserId: readDataProperty(value, "stableBrowserId") ?? false,
|
|
1160
|
+
stableTabId: readDataProperty(value, "stableTabId") ?? false,
|
|
1161
|
+
authoritativeTabClaim: readDataProperty(value, "authoritativeTabClaim") ?? false,
|
|
1162
|
+
concurrentTabs: readDataProperty(value, "concurrentTabs") ?? false
|
|
1163
|
+
};
|
|
1164
|
+
if (Object.values(result).some(item => typeof item !== "boolean")) {
|
|
1165
|
+
throw new ChatGPTRuntimeFactoryError();
|
|
1166
|
+
}
|
|
1167
|
+
return Object.freeze(result);
|
|
1168
|
+
}
|
|
1169
|
+
function assertOwnDataKeys(value, allowed) {
|
|
1170
|
+
let descriptors;
|
|
1171
|
+
try {
|
|
1172
|
+
descriptors = Object.getOwnPropertyDescriptors(value);
|
|
1173
|
+
}
|
|
1174
|
+
catch {
|
|
1175
|
+
throw new ChatGPTRuntimeFactoryError();
|
|
1176
|
+
}
|
|
1177
|
+
const allowedSet = new Set(allowed);
|
|
1178
|
+
for (const key of Reflect.ownKeys(descriptors)) {
|
|
1179
|
+
if (typeof key !== "string" || !allowedSet.has(key))
|
|
1180
|
+
throw new ChatGPTRuntimeFactoryError();
|
|
1181
|
+
const descriptor = descriptors[key];
|
|
1182
|
+
if (descriptor === undefined || !("value" in descriptor) || descriptor.get !== undefined || descriptor.set !== undefined) {
|
|
1183
|
+
throw new ChatGPTRuntimeFactoryError();
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
function cloneSafeData(value, seen = new Set(), depth = 0) {
|
|
1188
|
+
if (value === null || typeof value !== "object") {
|
|
1189
|
+
if (typeof value === "function" || typeof value === "symbol" || typeof value === "bigint") {
|
|
1190
|
+
throw new ChatGPTRuntimeFactoryError();
|
|
1191
|
+
}
|
|
1192
|
+
return value;
|
|
1193
|
+
}
|
|
1194
|
+
if (depth > 16 || seen.has(value))
|
|
1195
|
+
throw new ChatGPTRuntimeFactoryError();
|
|
1196
|
+
seen.add(value);
|
|
1197
|
+
try {
|
|
1198
|
+
// Durable operation snapshots are string-keyed wire data. Silently
|
|
1199
|
+
// dropping a symbol would make the snapshot differ from the caller's
|
|
1200
|
+
// object while still accepting an unreviewed property.
|
|
1201
|
+
if (Reflect.ownKeys(value).some(key => typeof key !== "string")) {
|
|
1202
|
+
throw new ChatGPTRuntimeFactoryError();
|
|
1203
|
+
}
|
|
1204
|
+
if (Array.isArray(value)) {
|
|
1205
|
+
const result = value.map(item => cloneSafeData(item, seen, depth + 1));
|
|
1206
|
+
seen.delete(value);
|
|
1207
|
+
return Object.freeze(result);
|
|
1208
|
+
}
|
|
1209
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1210
|
+
if (prototype !== Object.prototype && prototype !== null)
|
|
1211
|
+
throw new ChatGPTRuntimeFactoryError();
|
|
1212
|
+
const result = {};
|
|
1213
|
+
for (const key of Object.keys(value)) {
|
|
1214
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
1215
|
+
if (descriptor === undefined || !("value" in descriptor) || descriptor.get !== undefined || descriptor.set !== undefined) {
|
|
1216
|
+
throw new ChatGPTRuntimeFactoryError();
|
|
1217
|
+
}
|
|
1218
|
+
Object.defineProperty(result, key, {
|
|
1219
|
+
configurable: true,
|
|
1220
|
+
enumerable: true,
|
|
1221
|
+
value: cloneSafeData(descriptor.value, seen, depth + 1),
|
|
1222
|
+
writable: true
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
seen.delete(value);
|
|
1226
|
+
return Object.freeze(result);
|
|
1227
|
+
}
|
|
1228
|
+
catch (error) {
|
|
1229
|
+
seen.delete(value);
|
|
1230
|
+
throw error;
|
|
1231
|
+
}
|
|
1232
|
+
}
|