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,727 @@
|
|
|
1
|
+
import { hmacDigest, operationRequestDigest } from "./canonical.js";
|
|
2
|
+
import { assertOperationId } from "./state-machine.js";
|
|
3
|
+
import { basename } from "node:path";
|
|
4
|
+
import { OPERATION_CONTROL_REQUEST_SCHEMA_VERSION, OPERATION_HANDLE_SCHEMA_VERSION, OPERATION_REQUEST_SCHEMA_VERSION } from "./types.js";
|
|
5
|
+
const DIGEST_PATTERN = /^hmac-sha256:[0-9a-f]{64}$/;
|
|
6
|
+
const SHA256_PATTERN = /^[0-9a-f]{64}$/;
|
|
7
|
+
const MAX_INPUT_FILES = 256;
|
|
8
|
+
const MAX_TOOLS = 256;
|
|
9
|
+
const MAX_PROMPT_BYTES = 8 * 1024 * 1024;
|
|
10
|
+
const MAX_JSON_DEPTH = 16;
|
|
11
|
+
const MAX_JSON_NODES = 10_000;
|
|
12
|
+
const MAX_JSON_KEYS = 10_000;
|
|
13
|
+
const MAX_JSON_STRING_BYTES = 8 * 1024 * 1024;
|
|
14
|
+
const MAX_JSON_KEY_BYTES = 4096;
|
|
15
|
+
const RESERVED_CANONICAL_KEYS = new Set(["$undefined", "$date", "$bytes"]);
|
|
16
|
+
// Snapshots are authority-boundary material. Keep them only for the duration
|
|
17
|
+
// of one synchronous top-level validation/digest call; retaining them globally
|
|
18
|
+
// would make a later mutation of a caller-owned object invisible. A fresh store
|
|
19
|
+
// is installed for every exported entrypoint and restored for reentrant calls.
|
|
20
|
+
let activeSnapshots;
|
|
21
|
+
function withSnapshotContext(callback) {
|
|
22
|
+
const previous = activeSnapshots;
|
|
23
|
+
activeSnapshots = new WeakMap();
|
|
24
|
+
try {
|
|
25
|
+
return callback();
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
activeSnapshots = previous;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export class OperationHandleError extends Error {
|
|
32
|
+
code;
|
|
33
|
+
constructor(code, message) {
|
|
34
|
+
super(message);
|
|
35
|
+
this.code = code;
|
|
36
|
+
this.name = "OperationHandleError";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export function operationSubmitRequestDigest(key, request, files) {
|
|
40
|
+
return withSnapshotContext(() => operationSubmitRequestDigestImpl(key, request, files));
|
|
41
|
+
}
|
|
42
|
+
function operationSubmitRequestDigestImpl(key, request, files) {
|
|
43
|
+
if (!request || typeof request !== "object") {
|
|
44
|
+
throw new OperationHandleError("invalid_operation_request", "Operation request must be an object.");
|
|
45
|
+
}
|
|
46
|
+
assertExactKeys(request, "operation request", [
|
|
47
|
+
"schemaVersion", "operationId", "surface", "prompt", "target", "configuration", "files", "capture", "timeoutMs"
|
|
48
|
+
]);
|
|
49
|
+
const operationId = readData(request, "operationId");
|
|
50
|
+
const schemaVersion = readData(request, "schemaVersion");
|
|
51
|
+
const surface = readData(request, "surface");
|
|
52
|
+
const prompt = readData(request, "prompt");
|
|
53
|
+
const target = readData(request, "target");
|
|
54
|
+
const configuration = readData(request, "configuration");
|
|
55
|
+
const capture = readData(request, "capture");
|
|
56
|
+
const timeoutMs = readData(request, "timeoutMs");
|
|
57
|
+
const requestedFilesValue = readData(request, "files");
|
|
58
|
+
assertId(operationId, "operationId");
|
|
59
|
+
if (schemaVersion !== OPERATION_REQUEST_SCHEMA_VERSION) {
|
|
60
|
+
throw new OperationHandleError("unsupported_operation_request", "Operation request schemaVersion is unsupported.");
|
|
61
|
+
}
|
|
62
|
+
if (surface !== "chat" && surface !== "work") {
|
|
63
|
+
throw new OperationHandleError("invalid_operation_surface", "Operation surface must be chat or work.");
|
|
64
|
+
}
|
|
65
|
+
if (typeof prompt !== "string") {
|
|
66
|
+
throw new OperationHandleError("invalid_operation_prompt", "Operation prompt must be a string.");
|
|
67
|
+
}
|
|
68
|
+
if (Buffer.byteLength(prompt, "utf8") > MAX_PROMPT_BYTES) {
|
|
69
|
+
throw new OperationHandleError("invalid_operation_prompt", "Operation prompt exceeds the bounded request limit.");
|
|
70
|
+
}
|
|
71
|
+
validateTargetRequest(target);
|
|
72
|
+
validateConfigurationRequest(configuration);
|
|
73
|
+
validateCapturePolicy(capture);
|
|
74
|
+
validateTimeout(timeoutMs, "timeoutMs");
|
|
75
|
+
const manifestEntries = readArrayEntries(files, "invalid_operation_file_manifest", "Operation file manifest", MAX_INPUT_FILES);
|
|
76
|
+
if (manifestEntries.length > MAX_INPUT_FILES) {
|
|
77
|
+
throw new OperationHandleError("invalid_operation_file_manifest", "Operation file manifest exceeds the bounded input-file limit.");
|
|
78
|
+
}
|
|
79
|
+
const requestedFiles = requestedFilesValue === undefined
|
|
80
|
+
? []
|
|
81
|
+
: readArrayEntries(requestedFilesValue, "operation_file_manifest_mismatch", "Operation file request list", MAX_INPUT_FILES);
|
|
82
|
+
if (requestedFiles.length > MAX_INPUT_FILES || requestedFiles.length !== manifestEntries.length) {
|
|
83
|
+
throw new OperationHandleError("operation_file_manifest_mismatch", "Operation file manifest must match the request file list exactly.");
|
|
84
|
+
}
|
|
85
|
+
for (const [index, file] of manifestEntries.entries()) {
|
|
86
|
+
assertExactKeys(file, "operation file manifest entry", ["displayName", "bytes", "contentSha256"], "invalid_operation_file_manifest");
|
|
87
|
+
const displayName = readData(file, "displayName");
|
|
88
|
+
const byteCount = readData(file, "bytes");
|
|
89
|
+
const contentSha256 = readData(file, "contentSha256");
|
|
90
|
+
if (typeof displayName !== "string" ||
|
|
91
|
+
displayName.length === 0 ||
|
|
92
|
+
displayName.length > 512 ||
|
|
93
|
+
/[\\/\u0000-\u001f\u007f]/u.test(displayName) ||
|
|
94
|
+
!Number.isSafeInteger(byteCount) ||
|
|
95
|
+
byteCount < 0 ||
|
|
96
|
+
typeof contentSha256 !== "string" ||
|
|
97
|
+
!SHA256_PATTERN.test(contentSha256)) {
|
|
98
|
+
throw new OperationHandleError("invalid_operation_file_manifest", "Operation file manifest contains invalid size or SHA-256 data.");
|
|
99
|
+
}
|
|
100
|
+
const requested = requestedFiles[index];
|
|
101
|
+
assertExactKeys(requested, "operation file request", ["path", "displayName"], "invalid_operation_file_request");
|
|
102
|
+
const requestedPath = readData(requested, "path");
|
|
103
|
+
const requestedDisplayName = readData(requested, "displayName");
|
|
104
|
+
if (typeof requestedPath !== "string" || requestedPath.length === 0 || requestedPath.length > 4096 || /[\u0000-\u001f\u007f]/u.test(requestedPath)) {
|
|
105
|
+
throw new OperationHandleError("invalid_operation_file_request", "Operation file request requires a non-empty local path.");
|
|
106
|
+
}
|
|
107
|
+
if (requestedDisplayName !== undefined)
|
|
108
|
+
validateBoundedString(requestedDisplayName, "files[].displayName");
|
|
109
|
+
const expectedDisplayName = (requestedDisplayName ?? basename(requestedPath)).normalize("NFC");
|
|
110
|
+
if (displayName !== expectedDisplayName) {
|
|
111
|
+
throw new OperationHandleError("operation_file_manifest_mismatch", "Operation file manifest order or display name does not match the request.");
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return operationRequestDigest(key, {
|
|
115
|
+
operationId,
|
|
116
|
+
surface,
|
|
117
|
+
target,
|
|
118
|
+
prompt,
|
|
119
|
+
configuration,
|
|
120
|
+
tools: configuration === undefined ? undefined : readData(configuration, "tools"),
|
|
121
|
+
files: manifestEntries,
|
|
122
|
+
capturePolicy: capture,
|
|
123
|
+
behavior: {
|
|
124
|
+
// Timeout is deliberately absent: changing a local wait budget must not
|
|
125
|
+
// turn the same user intent into a different durable operation.
|
|
126
|
+
operationRequestSchemaVersion: schemaVersion
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
export function operationControlRequestDigest(key, request) {
|
|
131
|
+
return withSnapshotContext(() => operationControlRequestDigestImpl(key, request));
|
|
132
|
+
}
|
|
133
|
+
function operationControlRequestDigestImpl(key, request) {
|
|
134
|
+
if (!request || typeof request !== "object") {
|
|
135
|
+
throw new OperationHandleError("invalid_operation_control_request", "Operation control request must be an object.");
|
|
136
|
+
}
|
|
137
|
+
assertExactKeys(request, "operation control request", [
|
|
138
|
+
"schemaVersion", "controlActionId", "parent", "action", "expectedAssistantTurnId", "steerPrompt", "timeoutMs"
|
|
139
|
+
]);
|
|
140
|
+
const controlActionId = readData(request, "controlActionId");
|
|
141
|
+
const schemaVersion = readData(request, "schemaVersion");
|
|
142
|
+
const parent = readData(request, "parent");
|
|
143
|
+
const action = readData(request, "action");
|
|
144
|
+
const expectedAssistantTurnId = readData(request, "expectedAssistantTurnId");
|
|
145
|
+
const steerPromptValue = readData(request, "steerPrompt");
|
|
146
|
+
const timeoutMs = readData(request, "timeoutMs");
|
|
147
|
+
assertId(controlActionId, "controlActionId");
|
|
148
|
+
if (schemaVersion !== OPERATION_CONTROL_REQUEST_SCHEMA_VERSION) {
|
|
149
|
+
throw new OperationHandleError("unsupported_operation_control_request", "Operation control schemaVersion is unsupported.");
|
|
150
|
+
}
|
|
151
|
+
validateHandleShape(parent);
|
|
152
|
+
const parentRecord = parent;
|
|
153
|
+
const parentPhase = readData(parentRecord, "phase");
|
|
154
|
+
const parentTargetBindingDigest = readData(parentRecord, "targetBindingDigest");
|
|
155
|
+
if (parentPhase !== "generating" || parentTargetBindingDigest === undefined) {
|
|
156
|
+
throw new OperationHandleError("invalid_operation_control_target", "Operation control requires a generating parent handle with an exact target binding.");
|
|
157
|
+
}
|
|
158
|
+
if (typeof expectedAssistantTurnId !== "string" ||
|
|
159
|
+
expectedAssistantTurnId.trim().length === 0 ||
|
|
160
|
+
expectedAssistantTurnId.length > 512) {
|
|
161
|
+
throw new OperationHandleError("invalid_operation_control_target", "Operation control requires an exact assistant turn ID.");
|
|
162
|
+
}
|
|
163
|
+
if (action !== "stop" && action !== "steer") {
|
|
164
|
+
throw new OperationHandleError("invalid_operation_control_request", "Operation control action must be stop or steer.");
|
|
165
|
+
}
|
|
166
|
+
if (action === "steer" && (typeof steerPromptValue !== "string" || steerPromptValue.length === 0)) {
|
|
167
|
+
throw new OperationHandleError("invalid_operation_control_request", "A steer control requires steerPrompt.");
|
|
168
|
+
}
|
|
169
|
+
if (steerPromptValue !== undefined && (typeof steerPromptValue !== "string" || Buffer.byteLength(steerPromptValue, "utf8") > MAX_PROMPT_BYTES)) {
|
|
170
|
+
throw new OperationHandleError("invalid_operation_control_request", "steerPrompt exceeds the bounded request limit.");
|
|
171
|
+
}
|
|
172
|
+
if (action === "stop" && steerPromptValue !== undefined) {
|
|
173
|
+
throw new OperationHandleError("invalid_operation_control_request", "A stop control must not include steerPrompt.");
|
|
174
|
+
}
|
|
175
|
+
validateTimeout(timeoutMs, "timeoutMs");
|
|
176
|
+
const steerPrompt = steerPromptValue === undefined
|
|
177
|
+
? undefined
|
|
178
|
+
: {
|
|
179
|
+
digest: hmacDigest(key, "codex-chatgpt-control/control-steer-prompt/v1", steerPromptValue),
|
|
180
|
+
bytes: Buffer.byteLength(steerPromptValue, "utf8")
|
|
181
|
+
};
|
|
182
|
+
const parentOperationId = readData(parentRecord, "operationId");
|
|
183
|
+
const parentRequestDigest = readData(parentRecord, "requestDigest");
|
|
184
|
+
return hmacDigest(key, "codex-chatgpt-control/operation-control-request/v1", {
|
|
185
|
+
schemaVersion: OPERATION_CONTROL_REQUEST_SCHEMA_VERSION,
|
|
186
|
+
controlActionId,
|
|
187
|
+
parentOperationId,
|
|
188
|
+
parentRequestDigest,
|
|
189
|
+
parentTargetBindingDigest,
|
|
190
|
+
expectedAssistantTurnId,
|
|
191
|
+
action,
|
|
192
|
+
steerPrompt
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
export function operationHandleFromState(key, state) {
|
|
196
|
+
return withSnapshotContext(() => operationHandleFromStateImpl(key, state));
|
|
197
|
+
}
|
|
198
|
+
function operationHandleFromStateImpl(key, state) {
|
|
199
|
+
const stateRecord = state;
|
|
200
|
+
snapshotRecord(stateRecord, "operation state", "invalid_operation_handle");
|
|
201
|
+
const operationId = readData(stateRecord, "operationId");
|
|
202
|
+
const requestDigest = readData(stateRecord, "requestDigest");
|
|
203
|
+
const surface = readData(stateRecord, "surface");
|
|
204
|
+
const revision = readData(stateRecord, "revision");
|
|
205
|
+
const phase = readData(stateRecord, "phase");
|
|
206
|
+
const mutationBoundary = readData(stateRecord, "mutationBoundary");
|
|
207
|
+
const target = readData(stateRecord, "target");
|
|
208
|
+
const handle = {
|
|
209
|
+
schemaVersion: OPERATION_HANDLE_SCHEMA_VERSION,
|
|
210
|
+
operationId: operationId,
|
|
211
|
+
requestDigest: requestDigest,
|
|
212
|
+
surface: surface,
|
|
213
|
+
revision: revision,
|
|
214
|
+
phase: phase,
|
|
215
|
+
mutationBoundary: mutationBoundary
|
|
216
|
+
};
|
|
217
|
+
if (target !== undefined) {
|
|
218
|
+
handle.targetBindingDigest = hmacDigest(key, "codex-chatgpt-control/operation-target-binding/v1", operationTargetBindingProjectionImpl(target));
|
|
219
|
+
}
|
|
220
|
+
return handle;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Return only the immutable target material used for action/handle binding.
|
|
224
|
+
* A provider-assigned identity may be added to a new target after Send, but
|
|
225
|
+
* it must never change the digest that authorizes the original action.
|
|
226
|
+
*/
|
|
227
|
+
export function operationTargetBindingProjection(target) {
|
|
228
|
+
return withSnapshotContext(() => operationTargetBindingProjectionImpl(target));
|
|
229
|
+
}
|
|
230
|
+
function operationTargetBindingProjectionImpl(target) {
|
|
231
|
+
const targetRecord = target;
|
|
232
|
+
const values = snapshotRecord(targetRecord, "operation target", "invalid_operation_handle");
|
|
233
|
+
const lifecycle = values.get("targetLifecycle") ?? "fixed";
|
|
234
|
+
if (lifecycle !== "new_pending" && lifecycle !== "new_established")
|
|
235
|
+
return target;
|
|
236
|
+
const immutableAnchor = Object.create(null);
|
|
237
|
+
for (const [key, value] of values) {
|
|
238
|
+
if (key === "targetLifecycle" || key === "canonicalThreadUrl" || key === "conversationId" || key === "targetEstablishment")
|
|
239
|
+
continue;
|
|
240
|
+
Object.defineProperty(immutableAnchor, key, {
|
|
241
|
+
value,
|
|
242
|
+
enumerable: true,
|
|
243
|
+
writable: true,
|
|
244
|
+
configurable: true
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
const evidenceProfile = values.get("evidenceProfile");
|
|
248
|
+
const evidenceValues = snapshotRecord(evidenceProfile, "operation target evidence profile", "invalid_operation_handle");
|
|
249
|
+
const evidence = Object.create(null);
|
|
250
|
+
for (const [key, value] of evidenceValues) {
|
|
251
|
+
Object.defineProperty(evidence, key, {
|
|
252
|
+
value,
|
|
253
|
+
enumerable: true,
|
|
254
|
+
writable: true,
|
|
255
|
+
configurable: true
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
targetLifecycle: "new",
|
|
260
|
+
...immutableAnchor,
|
|
261
|
+
evidenceProfile: {
|
|
262
|
+
...evidence,
|
|
263
|
+
// Provider conversation/user-turn availability is established after
|
|
264
|
+
// Send and therefore is not part of the pre-Send action anchor.
|
|
265
|
+
stableConversationId: "unavailable",
|
|
266
|
+
stableUserTurnId: "unavailable"
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
export function validateOperationHandle(key, handle, state) {
|
|
271
|
+
return withSnapshotContext(() => validateOperationHandleImpl(key, handle, state));
|
|
272
|
+
}
|
|
273
|
+
function validateOperationHandleImpl(key, handle, state) {
|
|
274
|
+
const stateRecord = state;
|
|
275
|
+
snapshotRecord(stateRecord, "operation state", "invalid_operation_handle");
|
|
276
|
+
const stateOperationId = readData(stateRecord, "operationId");
|
|
277
|
+
const stateRequestDigest = readData(stateRecord, "requestDigest");
|
|
278
|
+
const stateSurface = readData(stateRecord, "surface");
|
|
279
|
+
const stateRevision = readData(stateRecord, "revision");
|
|
280
|
+
const statePhase = readData(stateRecord, "phase");
|
|
281
|
+
const stateMutationBoundary = readData(stateRecord, "mutationBoundary");
|
|
282
|
+
validateHandleShape(handle);
|
|
283
|
+
const handleOperationId = readData(handle, "operationId");
|
|
284
|
+
const handleRequestDigest = readData(handle, "requestDigest");
|
|
285
|
+
const handleSurface = readData(handle, "surface");
|
|
286
|
+
const handleRevision = readData(handle, "revision");
|
|
287
|
+
const handleBoundary = readData(handle, "mutationBoundary");
|
|
288
|
+
const handlePhase = readData(handle, "phase");
|
|
289
|
+
const handleTargetBindingDigest = readData(handle, "targetBindingDigest");
|
|
290
|
+
if (handleOperationId !== stateOperationId || handleRequestDigest !== stateRequestDigest || handleSurface !== stateSurface) {
|
|
291
|
+
throw new OperationHandleError("operation_handle_mismatch", "Operation handle does not match the durable operation binding.");
|
|
292
|
+
}
|
|
293
|
+
if (!Number.isSafeInteger(handleRevision) || handleRevision < 1) {
|
|
294
|
+
throw new OperationHandleError("invalid_operation_handle", "Operation handle revision must be a positive safe integer.");
|
|
295
|
+
}
|
|
296
|
+
if (handleRevision > stateRevision) {
|
|
297
|
+
throw new OperationHandleError("operation_handle_ahead", "Operation handle claims a revision newer than durable state.");
|
|
298
|
+
}
|
|
299
|
+
if (BOUNDARY_RANK[handleBoundary] > BOUNDARY_RANK[stateMutationBoundary]) {
|
|
300
|
+
throw new OperationHandleError("operation_handle_state_mismatch", "Operation handle claims a mutation boundary ahead of durable state.");
|
|
301
|
+
}
|
|
302
|
+
if (handleRevision < stateRevision && !phaseCanReach(handlePhase, statePhase)) {
|
|
303
|
+
throw new OperationHandleError("operation_handle_state_mismatch", "Operation handle phase cannot precede the current durable phase.");
|
|
304
|
+
}
|
|
305
|
+
const current = operationHandleFromStateImpl(key, state);
|
|
306
|
+
if (handleTargetBindingDigest !== current.targetBindingDigest
|
|
307
|
+
&& !(handleTargetBindingDigest === undefined && handleRevision < stateRevision)) {
|
|
308
|
+
throw new OperationHandleError("operation_handle_target_mismatch", "Operation handle target binding does not match durable state.");
|
|
309
|
+
}
|
|
310
|
+
if (handleRevision === stateRevision &&
|
|
311
|
+
(handlePhase !== statePhase || handleBoundary !== stateMutationBoundary)) {
|
|
312
|
+
throw new OperationHandleError("operation_handle_state_mismatch", "Operation handle state fields disagree at the same revision.");
|
|
313
|
+
}
|
|
314
|
+
return { stale: handleRevision < stateRevision, current };
|
|
315
|
+
}
|
|
316
|
+
const OPERATION_PHASES = new Set([
|
|
317
|
+
"prepared", "handoff_pending", "ready", "send_pending", "submitted", "generating", "capturing", "completed", "uncertain"
|
|
318
|
+
]);
|
|
319
|
+
const BOUNDARY_RANK = {
|
|
320
|
+
none: 0,
|
|
321
|
+
handoff_may_have_occurred: 1,
|
|
322
|
+
send_may_have_occurred: 2,
|
|
323
|
+
control_may_have_occurred: 3
|
|
324
|
+
};
|
|
325
|
+
const PHASE_EDGES = {
|
|
326
|
+
prepared: ["handoff_pending", "ready", "uncertain"],
|
|
327
|
+
handoff_pending: ["ready", "uncertain"],
|
|
328
|
+
ready: ["send_pending", "uncertain"],
|
|
329
|
+
send_pending: ["submitted", "uncertain"],
|
|
330
|
+
submitted: ["generating", "capturing", "uncertain"],
|
|
331
|
+
generating: ["capturing", "uncertain"],
|
|
332
|
+
capturing: ["completed", "uncertain"],
|
|
333
|
+
completed: [],
|
|
334
|
+
uncertain: ["ready", "submitted", "generating", "capturing", "completed"]
|
|
335
|
+
};
|
|
336
|
+
function validateHandleShape(handle) {
|
|
337
|
+
if (!handle || typeof handle !== "object") {
|
|
338
|
+
throw new OperationHandleError("invalid_operation_handle", "Operation handle must be an object.");
|
|
339
|
+
}
|
|
340
|
+
assertExactKeys(handle, "operation handle", [
|
|
341
|
+
"schemaVersion", "operationId", "requestDigest", "surface", "revision", "phase", "mutationBoundary", "targetBindingDigest"
|
|
342
|
+
]);
|
|
343
|
+
const schemaVersion = readData(handle, "schemaVersion");
|
|
344
|
+
const operationId = readData(handle, "operationId");
|
|
345
|
+
const requestDigest = readData(handle, "requestDigest");
|
|
346
|
+
const surface = readData(handle, "surface");
|
|
347
|
+
const revision = readData(handle, "revision");
|
|
348
|
+
const phase = readData(handle, "phase");
|
|
349
|
+
const mutationBoundary = readData(handle, "mutationBoundary");
|
|
350
|
+
const targetBindingDigest = readData(handle, "targetBindingDigest");
|
|
351
|
+
if (schemaVersion !== OPERATION_HANDLE_SCHEMA_VERSION) {
|
|
352
|
+
throw new OperationHandleError("unsupported_operation_handle", "Operation handle schemaVersion is unsupported.");
|
|
353
|
+
}
|
|
354
|
+
assertId(operationId, "operationId");
|
|
355
|
+
if (typeof requestDigest !== "string" || !DIGEST_PATTERN.test(requestDigest)) {
|
|
356
|
+
throw new OperationHandleError("invalid_operation_handle", "Operation handle requestDigest is invalid.");
|
|
357
|
+
}
|
|
358
|
+
if (surface !== "chat" && surface !== "work") {
|
|
359
|
+
throw new OperationHandleError("invalid_operation_handle", "Operation handle surface is invalid.");
|
|
360
|
+
}
|
|
361
|
+
if (!Number.isSafeInteger(revision) || revision < 1) {
|
|
362
|
+
throw new OperationHandleError("invalid_operation_handle", "Operation handle revision must be a positive safe integer.");
|
|
363
|
+
}
|
|
364
|
+
if (typeof phase !== "string"
|
|
365
|
+
|| !OPERATION_PHASES.has(phase)
|
|
366
|
+
|| typeof mutationBoundary !== "string"
|
|
367
|
+
|| !(mutationBoundary in BOUNDARY_RANK)) {
|
|
368
|
+
throw new OperationHandleError("invalid_operation_handle", "Operation handle progress fields are invalid.");
|
|
369
|
+
}
|
|
370
|
+
if (targetBindingDigest !== undefined && (typeof targetBindingDigest !== "string" || !DIGEST_PATTERN.test(targetBindingDigest))) {
|
|
371
|
+
throw new OperationHandleError("invalid_operation_handle", "Operation handle targetBindingDigest is invalid.");
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
function phaseCanReach(from, to) {
|
|
375
|
+
if (from === to)
|
|
376
|
+
return true;
|
|
377
|
+
const seen = new Set([from]);
|
|
378
|
+
const queue = [from];
|
|
379
|
+
while (queue.length > 0) {
|
|
380
|
+
const current = queue.shift();
|
|
381
|
+
for (const next of PHASE_EDGES[current]) {
|
|
382
|
+
if (next === to)
|
|
383
|
+
return true;
|
|
384
|
+
if (!seen.has(next)) {
|
|
385
|
+
seen.add(next);
|
|
386
|
+
queue.push(next);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
function validateTargetRequest(target) {
|
|
393
|
+
snapshotRecord(target, "operation target", "invalid_operation_target");
|
|
394
|
+
const type = readData(target, "type");
|
|
395
|
+
if (typeof type !== "string") {
|
|
396
|
+
throw new OperationHandleError("invalid_operation_target", "Operation target is invalid.");
|
|
397
|
+
}
|
|
398
|
+
if (type === "new" || type === "selected_tab") {
|
|
399
|
+
assertExactKeys(target, "operation target", ["type"]);
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
if (type === "tab_id") {
|
|
403
|
+
assertExactKeys(target, "operation target", ["type", "tabId"]);
|
|
404
|
+
return validateBoundedString(readData(target, "tabId"), "target.tabId");
|
|
405
|
+
}
|
|
406
|
+
if (type === "conversation_id") {
|
|
407
|
+
assertExactKeys(target, "operation target", ["type", "conversationId"]);
|
|
408
|
+
return validateBoundedString(readData(target, "conversationId"), "target.conversationId");
|
|
409
|
+
}
|
|
410
|
+
if (type === "url") {
|
|
411
|
+
assertExactKeys(target, "operation target", ["type", "url"]);
|
|
412
|
+
const url = readData(target, "url");
|
|
413
|
+
validateBoundedString(url, "target.url", 4096);
|
|
414
|
+
let parsed;
|
|
415
|
+
try {
|
|
416
|
+
parsed = new URL(url);
|
|
417
|
+
}
|
|
418
|
+
catch {
|
|
419
|
+
throw new OperationHandleError("invalid_operation_target", "Operation URL target must be an absolute HTTP(S) URL.");
|
|
420
|
+
}
|
|
421
|
+
if ((parsed.protocol !== "https:" && parsed.protocol !== "http:") || parsed.username !== "" || parsed.password !== "") {
|
|
422
|
+
throw new OperationHandleError("invalid_operation_target", "Operation URL target must be HTTP(S) and contain no credentials.");
|
|
423
|
+
}
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
throw new OperationHandleError("invalid_operation_target", "Unsupported operation target type.");
|
|
427
|
+
}
|
|
428
|
+
function validateConfigurationRequest(configuration) {
|
|
429
|
+
if (configuration === undefined)
|
|
430
|
+
return;
|
|
431
|
+
assertExactKeys(configuration, "operation configuration", ["experience", "model", "modelVersion", "reasoning", "mode", "tools", "additional"], "invalid_operation_configuration");
|
|
432
|
+
const experience = readData(configuration, "experience");
|
|
433
|
+
const model = readData(configuration, "model");
|
|
434
|
+
const modelVersion = readData(configuration, "modelVersion");
|
|
435
|
+
const reasoning = readData(configuration, "reasoning");
|
|
436
|
+
const mode = readData(configuration, "mode");
|
|
437
|
+
const tools = readData(configuration, "tools");
|
|
438
|
+
const additional = readData(configuration, "additional");
|
|
439
|
+
if (experience !== undefined && experience !== "chat" && experience !== "work") {
|
|
440
|
+
throw new OperationHandleError("invalid_operation_configuration", "Configuration experience must be chat or work.");
|
|
441
|
+
}
|
|
442
|
+
for (const [label, value] of [
|
|
443
|
+
["model", model],
|
|
444
|
+
["modelVersion", modelVersion],
|
|
445
|
+
["reasoning", reasoning],
|
|
446
|
+
["mode", mode]
|
|
447
|
+
]) {
|
|
448
|
+
if (value !== undefined)
|
|
449
|
+
validateBoundedString(value, `configuration.${label}`);
|
|
450
|
+
}
|
|
451
|
+
if (tools !== undefined) {
|
|
452
|
+
const entries = readArrayEntries(tools, "invalid_operation_configuration", "Configuration tools", MAX_TOOLS);
|
|
453
|
+
if (entries.length > MAX_TOOLS) {
|
|
454
|
+
throw new OperationHandleError("invalid_operation_configuration", "Configuration tools must be an array.");
|
|
455
|
+
}
|
|
456
|
+
for (const tool of entries)
|
|
457
|
+
validateBoundedString(tool, "configuration.tools[]");
|
|
458
|
+
}
|
|
459
|
+
if (additional !== undefined) {
|
|
460
|
+
validateJsonValue(additional, "configuration.additional");
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
function validateCapturePolicy(capture) {
|
|
464
|
+
if (capture === undefined)
|
|
465
|
+
return;
|
|
466
|
+
assertExactKeys(capture, "operation capture policy", ["responseContent", "responseFormat", "artifacts", "outputDirectory"], "invalid_operation_capture");
|
|
467
|
+
const responseContent = readData(capture, "responseContent");
|
|
468
|
+
const responseFormat = readData(capture, "responseFormat");
|
|
469
|
+
const artifacts = readData(capture, "artifacts");
|
|
470
|
+
const outputDirectory = readData(capture, "outputDirectory");
|
|
471
|
+
if (responseContent !== "include" && responseContent !== "metadata") {
|
|
472
|
+
throw new OperationHandleError("invalid_operation_capture", "Capture responseContent is invalid.");
|
|
473
|
+
}
|
|
474
|
+
if (responseFormat !== undefined && responseFormat !== "markdown" && responseFormat !== "text") {
|
|
475
|
+
throw new OperationHandleError("invalid_operation_capture", "Capture responseFormat is invalid.");
|
|
476
|
+
}
|
|
477
|
+
if (artifacts !== "receipt_only" && artifacts !== "transfer") {
|
|
478
|
+
throw new OperationHandleError("invalid_operation_capture", "Capture artifacts policy is invalid.");
|
|
479
|
+
}
|
|
480
|
+
if (outputDirectory !== undefined && (typeof outputDirectory !== "string"
|
|
481
|
+
|| outputDirectory.length === 0
|
|
482
|
+
|| outputDirectory.length > 4096
|
|
483
|
+
|| /[\u0000-\u001f\u007f]/u.test(outputDirectory))) {
|
|
484
|
+
throw new OperationHandleError("invalid_operation_capture", "Capture outputDirectory must be a non-empty path.");
|
|
485
|
+
}
|
|
486
|
+
if (artifacts === "transfer" && outputDirectory === undefined) {
|
|
487
|
+
throw new OperationHandleError("invalid_operation_capture", "Artifact transfer requires outputDirectory.");
|
|
488
|
+
}
|
|
489
|
+
if (artifacts === "receipt_only" && outputDirectory !== undefined) {
|
|
490
|
+
throw new OperationHandleError("invalid_operation_capture", "outputDirectory is only valid when artifacts are transferred.");
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
function validateTimeout(value, label) {
|
|
494
|
+
if (value !== undefined && (!Number.isSafeInteger(value) || value < 0)) {
|
|
495
|
+
throw new OperationHandleError("invalid_operation_timeout", `${label} must be a non-negative safe integer.`);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
function validateBoundedString(value, label, maxLength = 512) {
|
|
499
|
+
if (typeof value !== "string" || value.trim().length === 0 || value.length > maxLength || /[\u0000-\u001f\u007f]/u.test(value)) {
|
|
500
|
+
throw new OperationHandleError("invalid_operation_request", `${label} must be a bounded non-empty string.`);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
function assertId(value, label) {
|
|
504
|
+
if (typeof value !== "string") {
|
|
505
|
+
throw new OperationHandleError("invalid_operation_id", `${label} is invalid.`);
|
|
506
|
+
}
|
|
507
|
+
try {
|
|
508
|
+
assertOperationId(value, label);
|
|
509
|
+
}
|
|
510
|
+
catch (error) {
|
|
511
|
+
const message = error instanceof Error ? error.message : `${label} is invalid.`;
|
|
512
|
+
throw new OperationHandleError("invalid_operation_id", message);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
function assertExactKeys(value, label, allowed, code = "invalid_operation_request") {
|
|
516
|
+
const values = snapshotRecord(value, label, code);
|
|
517
|
+
const allowedSet = new Set(allowed);
|
|
518
|
+
for (const key of values.keys()) {
|
|
519
|
+
if (!allowedSet.has(key)) {
|
|
520
|
+
throw new OperationHandleError("invalid_operation_request", `${label} contains an unsupported field.`);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
function validateJsonValue(value, label) {
|
|
525
|
+
const budget = { nodes: 0, bytes: 0 };
|
|
526
|
+
const active = new WeakSet();
|
|
527
|
+
const visit = (candidate, depth) => {
|
|
528
|
+
budget.nodes += 1;
|
|
529
|
+
if (budget.nodes > MAX_JSON_NODES)
|
|
530
|
+
throw new OperationHandleError("invalid_operation_configuration", `${label} exceeds the bounded node limit.`);
|
|
531
|
+
if (depth > MAX_JSON_DEPTH)
|
|
532
|
+
throw new OperationHandleError("invalid_operation_configuration", `${label} exceeds the bounded nesting limit.`);
|
|
533
|
+
if (candidate === null || typeof candidate === "boolean")
|
|
534
|
+
return;
|
|
535
|
+
if (typeof candidate === "string") {
|
|
536
|
+
budget.bytes += Buffer.byteLength(candidate, "utf8");
|
|
537
|
+
if (budget.bytes > MAX_JSON_STRING_BYTES)
|
|
538
|
+
throw new OperationHandleError("invalid_operation_configuration", `${label} exceeds the bounded byte limit.`);
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
if (typeof candidate === "number") {
|
|
542
|
+
if (!Number.isFinite(candidate))
|
|
543
|
+
throw new OperationHandleError("invalid_operation_configuration", `${label} contains a non-finite number.`);
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
if (candidate === undefined || typeof candidate !== "object") {
|
|
547
|
+
throw new OperationHandleError("invalid_operation_configuration", `${label} contains an unsupported value.`);
|
|
548
|
+
}
|
|
549
|
+
let isArray = false;
|
|
550
|
+
try {
|
|
551
|
+
isArray = Array.isArray(candidate);
|
|
552
|
+
}
|
|
553
|
+
catch {
|
|
554
|
+
throw new OperationHandleError("invalid_operation_configuration", `${label} contains an inaccessible value.`);
|
|
555
|
+
}
|
|
556
|
+
const object = candidate;
|
|
557
|
+
if (active.has(object))
|
|
558
|
+
throw new OperationHandleError("invalid_operation_configuration", `${label} contains a cyclic value.`);
|
|
559
|
+
active.add(object);
|
|
560
|
+
try {
|
|
561
|
+
if (isArray) {
|
|
562
|
+
const entries = readArrayEntries(candidate, "invalid_operation_configuration", label, MAX_JSON_NODES);
|
|
563
|
+
for (const entry of entries)
|
|
564
|
+
visit(entry, depth + 1);
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
try {
|
|
568
|
+
const values = snapshotRecord(candidate, label, "invalid_operation_configuration");
|
|
569
|
+
for (const [key, entry] of values) {
|
|
570
|
+
if (key.length > 256
|
|
571
|
+
|| Buffer.byteLength(key, "utf8") > MAX_JSON_KEY_BYTES
|
|
572
|
+
|| /[\u0000-\u001f\u007f]/u.test(key)
|
|
573
|
+
|| RESERVED_CANONICAL_KEYS.has(key)) {
|
|
574
|
+
throw new OperationHandleError("invalid_operation_configuration", `${label} contains an invalid object key.`);
|
|
575
|
+
}
|
|
576
|
+
budget.bytes += Buffer.byteLength(key, "utf8");
|
|
577
|
+
if (budget.bytes > MAX_JSON_STRING_BYTES)
|
|
578
|
+
throw new OperationHandleError("invalid_operation_configuration", `${label} exceeds the bounded byte limit.`);
|
|
579
|
+
visit(entry, depth + 1);
|
|
580
|
+
}
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
catch (error) {
|
|
584
|
+
if (error instanceof OperationHandleError && error.code === "invalid_operation_configuration")
|
|
585
|
+
throw error;
|
|
586
|
+
throw new OperationHandleError("invalid_operation_configuration", `${label} contains an unsupported value.`);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
finally {
|
|
590
|
+
active.delete(object);
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
visit(value, 0);
|
|
594
|
+
}
|
|
595
|
+
function snapshotRecord(value, label, code = "invalid_operation_request") {
|
|
596
|
+
if (value === null || typeof value !== "object") {
|
|
597
|
+
throw new OperationHandleError(code, `${label} must be a plain object.`);
|
|
598
|
+
}
|
|
599
|
+
const store = activeSnapshots;
|
|
600
|
+
if (store === undefined)
|
|
601
|
+
throw new OperationHandleError(code, "Operation data context is unavailable.");
|
|
602
|
+
const cached = store.get(value);
|
|
603
|
+
if (cached !== undefined)
|
|
604
|
+
return cached;
|
|
605
|
+
let prototype;
|
|
606
|
+
let descriptors;
|
|
607
|
+
let keys;
|
|
608
|
+
try {
|
|
609
|
+
if (Array.isArray(value))
|
|
610
|
+
throw new Error("array");
|
|
611
|
+
prototype = Object.getPrototypeOf(value);
|
|
612
|
+
descriptors = Object.getOwnPropertyDescriptors(value);
|
|
613
|
+
keys = Reflect.ownKeys(descriptors);
|
|
614
|
+
}
|
|
615
|
+
catch {
|
|
616
|
+
throw new OperationHandleError(code, `${label} could not be inspected safely.`);
|
|
617
|
+
}
|
|
618
|
+
if (keys.length > MAX_JSON_KEYS)
|
|
619
|
+
throw new OperationHandleError(code, `${label} exceeds the bounded key limit.`);
|
|
620
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
621
|
+
throw new OperationHandleError(code, `${label} must be a plain object.`);
|
|
622
|
+
}
|
|
623
|
+
const values = new Map();
|
|
624
|
+
for (const key of keys) {
|
|
625
|
+
if (typeof key !== "string") {
|
|
626
|
+
throw new OperationHandleError(code, `${label} contains an unsupported symbol field.`);
|
|
627
|
+
}
|
|
628
|
+
const descriptor = descriptorFromMap(descriptors, key, label, code);
|
|
629
|
+
if (descriptor.get !== undefined || descriptor.set !== undefined || !("value" in descriptor) || descriptor.enumerable !== true) {
|
|
630
|
+
throw new OperationHandleError(code, `${label} contains an unsafe property.`);
|
|
631
|
+
}
|
|
632
|
+
values.set(key, descriptor.value);
|
|
633
|
+
}
|
|
634
|
+
store.set(value, values);
|
|
635
|
+
return values;
|
|
636
|
+
}
|
|
637
|
+
function readData(value, key) {
|
|
638
|
+
const store = activeSnapshots;
|
|
639
|
+
if (store === undefined)
|
|
640
|
+
throw new OperationHandleError("invalid_operation_request", "Operation data context is unavailable.");
|
|
641
|
+
const snapshot = store.get(value);
|
|
642
|
+
if (snapshot !== undefined)
|
|
643
|
+
return snapshot.get(key);
|
|
644
|
+
let descriptor;
|
|
645
|
+
try {
|
|
646
|
+
descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
647
|
+
}
|
|
648
|
+
catch {
|
|
649
|
+
throw new OperationHandleError("invalid_operation_request", "Operation data could not be read safely.");
|
|
650
|
+
}
|
|
651
|
+
if (descriptor === undefined)
|
|
652
|
+
return undefined;
|
|
653
|
+
if (!("value" in descriptor) || descriptor.get !== undefined || descriptor.set !== undefined) {
|
|
654
|
+
throw new OperationHandleError("invalid_operation_request", "Operation data contains an unsafe property.");
|
|
655
|
+
}
|
|
656
|
+
return descriptor.value;
|
|
657
|
+
}
|
|
658
|
+
function readArrayEntries(value, code, label, maxLength) {
|
|
659
|
+
if (value === null || typeof value !== "object") {
|
|
660
|
+
throw new OperationHandleError(code, `${label} must be an array.`);
|
|
661
|
+
}
|
|
662
|
+
let isArray = false;
|
|
663
|
+
let prototype;
|
|
664
|
+
let descriptors;
|
|
665
|
+
let keys;
|
|
666
|
+
try {
|
|
667
|
+
isArray = Array.isArray(value);
|
|
668
|
+
prototype = Object.getPrototypeOf(value);
|
|
669
|
+
descriptors = Object.getOwnPropertyDescriptors(value);
|
|
670
|
+
keys = Reflect.ownKeys(descriptors);
|
|
671
|
+
}
|
|
672
|
+
catch {
|
|
673
|
+
throw new OperationHandleError(code, `${label} could not be inspected safely.`);
|
|
674
|
+
}
|
|
675
|
+
if (!isArray || prototype !== Array.prototype)
|
|
676
|
+
throw new OperationHandleError(code, `${label} must be a standard array.`);
|
|
677
|
+
const lengthDescriptor = descriptorFromMap(descriptors, "length", label, code);
|
|
678
|
+
if (!("value" in lengthDescriptor)
|
|
679
|
+
|| lengthDescriptor.get !== undefined
|
|
680
|
+
|| lengthDescriptor.set !== undefined
|
|
681
|
+
|| typeof lengthDescriptor.value !== "number"
|
|
682
|
+
|| !Number.isSafeInteger(lengthDescriptor.value)
|
|
683
|
+
|| lengthDescriptor.value < 0
|
|
684
|
+
|| lengthDescriptor.value > maxLength
|
|
685
|
+
|| lengthDescriptor.enumerable !== false
|
|
686
|
+
|| lengthDescriptor.configurable !== false) {
|
|
687
|
+
throw new OperationHandleError(code, `${label} contains an invalid length.`);
|
|
688
|
+
}
|
|
689
|
+
const length = lengthDescriptor.value;
|
|
690
|
+
if (keys.length !== length + 1)
|
|
691
|
+
throw new OperationHandleError(code, `${label} must not be sparse or custom.`);
|
|
692
|
+
for (const key of keys) {
|
|
693
|
+
if (typeof key !== "string" || (key !== "length" && parseArrayIndex(key) === undefined)) {
|
|
694
|
+
throw new OperationHandleError(code, `${label} must not contain custom fields.`);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
const entries = [];
|
|
698
|
+
for (let index = 0; index < length; index += 1) {
|
|
699
|
+
const descriptor = descriptorFromMap(descriptors, String(index), label, code);
|
|
700
|
+
if (!("value" in descriptor) || descriptor.get !== undefined || descriptor.set !== undefined || descriptor.enumerable !== true) {
|
|
701
|
+
throw new OperationHandleError(code, `${label} contains an unsafe entry.`);
|
|
702
|
+
}
|
|
703
|
+
entries.push(descriptor.value);
|
|
704
|
+
}
|
|
705
|
+
return entries;
|
|
706
|
+
}
|
|
707
|
+
function descriptorFromMap(descriptors, key, label, code = "invalid_operation_request") {
|
|
708
|
+
let descriptor;
|
|
709
|
+
try {
|
|
710
|
+
descriptor = Object.getOwnPropertyDescriptor(descriptors, key);
|
|
711
|
+
}
|
|
712
|
+
catch {
|
|
713
|
+
throw new OperationHandleError(code, `${label} could not be inspected safely.`);
|
|
714
|
+
}
|
|
715
|
+
if (descriptor === undefined || !("value" in descriptor)) {
|
|
716
|
+
throw new OperationHandleError(code, `${label} contains an invalid property.`);
|
|
717
|
+
}
|
|
718
|
+
return descriptor.value;
|
|
719
|
+
}
|
|
720
|
+
function parseArrayIndex(key) {
|
|
721
|
+
if (!/^(?:0|[1-9][0-9]*)$/u.test(key))
|
|
722
|
+
return undefined;
|
|
723
|
+
const index = Number(key);
|
|
724
|
+
if (!Number.isSafeInteger(index) || index < 0 || index > 4_294_967_294 || String(index) !== key)
|
|
725
|
+
return undefined;
|
|
726
|
+
return index;
|
|
727
|
+
}
|