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