@4xeoz/re-entry 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/README.md +337 -0
  2. package/node_modules/@webmcp-challenge/reentry-core/README.md +112 -0
  3. package/node_modules/@webmcp-challenge/reentry-core/package.json +38 -0
  4. package/node_modules/@webmcp-challenge/reentry-core/protocol/test-vectors/v0.1.json +47 -0
  5. package/node_modules/@webmcp-challenge/reentry-core/src/agent-adapter.mjs +438 -0
  6. package/node_modules/@webmcp-challenge/reentry-core/src/cloud-receiver-http.mjs +268 -0
  7. package/node_modules/@webmcp-challenge/reentry-core/src/host-sdk.mjs +278 -0
  8. package/node_modules/@webmcp-challenge/reentry-core/src/index.mjs +3 -0
  9. package/node_modules/@webmcp-challenge/reentry-core/src/local-connector-client.mjs +530 -0
  10. package/node_modules/@webmcp-challenge/reentry-core/src/managed-context-adapter.mjs +275 -0
  11. package/node_modules/@webmcp-challenge/reentry-core/src/protocol.mjs +845 -0
  12. package/node_modules/@webmcp-challenge/reentry-core/src/receiver-core.mjs +867 -0
  13. package/node_modules/@webmcp-challenge/reentry-core/src/receiver-delivery.mjs +613 -0
  14. package/node_modules/@webmcp-challenge/reentry-core/src/receiver-http-contract.mjs +24 -0
  15. package/node_modules/@webmcp-challenge/reentry-core/src/receiver-support.mjs +151 -0
  16. package/node_modules/@webmcp-challenge/reentry-core/src/sqlite-receiver-schema.mjs +184 -0
  17. package/node_modules/@webmcp-challenge/reentry-core/src/sqlite-receiver-store.mjs +573 -0
  18. package/package.json +44 -0
  19. package/src/browser-prompt.mjs +18 -0
  20. package/src/codex-discovery.mjs +246 -0
  21. package/src/codex-exec-adapter.mjs +197 -0
  22. package/src/codex-queue-adapter.mjs +214 -0
  23. package/src/credentials.mjs +87 -0
  24. package/src/index.mjs +6 -0
  25. package/src/local-connector.mjs +82 -0
  26. package/src/macos-service.mjs +184 -0
  27. package/src/main.mjs +733 -0
  28. package/src/pairing-client.mjs +545 -0
  29. package/src/terminal-ui.mjs +99 -0
@@ -0,0 +1,246 @@
1
+ import { accessSync, constants, statSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { delimiter, isAbsolute, join, normalize, resolve } from "node:path";
4
+ import process from "node:process";
5
+ import { spawnSync } from "node:child_process";
6
+
7
+ export const CODEX_BINARY_ENVIRONMENT_VARIABLE = "CODEX_BINARY";
8
+ export const MINIMUM_NODE_MAJOR = 24;
9
+
10
+ const CODEX_COMMAND = "codex";
11
+ const CODEX_APP_BUNDLE_NAMES = Object.freeze(["ChatGPT.app", "Codex.app"]);
12
+ const DEFAULT_COMMAND_DIRECTORIES = Object.freeze([
13
+ "/opt/homebrew/bin",
14
+ "/usr/local/bin",
15
+ "/usr/bin",
16
+ "/bin",
17
+ ]);
18
+ const MAX_REFERENCE_BYTES = 4 * 1_024;
19
+ const MAX_VERSION_BYTES = 512;
20
+ const CONTROL_CHARACTER_PATTERN = /[\u0000-\u001f\u007f]/;
21
+
22
+ export class CodexDiscoveryError extends Error {
23
+ constructor(code, message, options = {}) {
24
+ super(message, options.cause === undefined ? undefined : { cause: options.cause });
25
+ this.name = "CodexDiscoveryError";
26
+ this.code = code;
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Find the Codex CLI without relying on one machine's ChatGPT installation path.
32
+ * Explicit CLI configuration wins, then CODEX_BINARY, PATH, and known macOS app bundles.
33
+ */
34
+ export function discoverCodexExecutable(options = {}) {
35
+ const environment = options.environment ?? process.env;
36
+ const homeDirectory = options.homeDirectory ?? homedir();
37
+ const platform = options.platform ?? process.platform;
38
+ const applicationDirectories = options.applicationDirectories ?? [
39
+ "/Applications",
40
+ join(homeDirectory, "Applications"),
41
+ ];
42
+ const requested = options.requested !== undefined
43
+ ? options.requested
44
+ : environment[CODEX_BINARY_ENVIRONMENT_VARIABLE];
45
+
46
+ if (requested !== undefined) {
47
+ const reference = requireReference(requested, "Codex executable");
48
+ const candidates = reference.includes("/")
49
+ ? [resolveReference(reference, homeDirectory)]
50
+ : [
51
+ ...pathCandidates(reference, environment.PATH),
52
+ ...(reference === CODEX_COMMAND
53
+ ? DEFAULT_COMMAND_DIRECTORIES.map((directory) => join(directory, reference))
54
+ : []),
55
+ ];
56
+ const executable = firstExecutable(candidates);
57
+ if (executable) return executable;
58
+ throw codexFailure(
59
+ "connector_codex_binary_not_found",
60
+ "The configured Codex executable could not be found or is not executable.",
61
+ );
62
+ }
63
+
64
+ const candidates = [
65
+ ...pathCandidates(CODEX_COMMAND, environment.PATH),
66
+ ...DEFAULT_COMMAND_DIRECTORIES.map((directory) => join(directory, CODEX_COMMAND)),
67
+ ...(platform === "darwin" ? macOSBundleCandidates(applicationDirectories) : []),
68
+ ];
69
+ const executable = firstExecutable(candidates);
70
+ if (executable) return executable;
71
+
72
+ throw codexFailure(
73
+ "connector_codex_not_found",
74
+ "Codex CLI was not found. Install Codex, add it to PATH, or set CODEX_BINARY.",
75
+ );
76
+ }
77
+
78
+ /**
79
+ * Check the selected Codex binary before the Connector claims a delivery.
80
+ */
81
+ export function verifyCodexExecutable(executable, options = {}) {
82
+ const normalizedExecutable = requireReference(executable, "Codex executable");
83
+ const timeoutMs = options.timeoutMs ?? 10_000;
84
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 100 || timeoutMs > 60_000) {
85
+ throw new TypeError("Codex version check timeout is invalid");
86
+ }
87
+ const spawnSyncCommand = options.spawnSyncCommand ?? spawnSync;
88
+ if (typeof spawnSyncCommand !== "function") {
89
+ throw new TypeError("Codex version check command must be a function");
90
+ }
91
+
92
+ let result;
93
+ try {
94
+ result = spawnSyncCommand(normalizedExecutable, ["--version"], {
95
+ encoding: "utf8",
96
+ env: options.environment ?? process.env,
97
+ stdio: ["ignore", "pipe", "pipe"],
98
+ timeout: timeoutMs,
99
+ });
100
+ } catch (error) {
101
+ throw codexFailure(
102
+ "connector_codex_unusable",
103
+ "The Codex CLI could not be started.",
104
+ error,
105
+ );
106
+ }
107
+
108
+ if (!result || result.error || result.status !== 0) {
109
+ throw codexFailure(
110
+ "connector_codex_unusable",
111
+ "The selected Codex CLI did not pass its version check.",
112
+ result?.error,
113
+ );
114
+ }
115
+
116
+ return Object.freeze({
117
+ executable: normalizedExecutable,
118
+ version: readVersion(result.stdout, result.stderr),
119
+ });
120
+ }
121
+
122
+ /**
123
+ * Validate the host project directory before a delivery is claimed.
124
+ */
125
+ export function validateCodexWorkingDirectory(value, options = {}) {
126
+ const homeDirectory = options.homeDirectory ?? homedir();
127
+ const reference = requireReference(value, "Codex working directory");
128
+ const expanded = resolveReference(reference, homeDirectory);
129
+ if (!isAbsolute(expanded)) {
130
+ throw codexFailure(
131
+ "connector_codex_cd_absolute",
132
+ "The Codex working directory must be an absolute path.",
133
+ );
134
+ }
135
+ const workingDirectory = normalize(expanded);
136
+ const statFile = options.statSync ?? statSync;
137
+ const accessFile = options.accessSync ?? accessSync;
138
+ let stats;
139
+ try {
140
+ stats = statFile(workingDirectory);
141
+ } catch (error) {
142
+ throw codexFailure(
143
+ "connector_codex_cd_missing",
144
+ "The Codex working directory does not exist.",
145
+ error,
146
+ );
147
+ }
148
+ if (!stats?.isDirectory?.()) {
149
+ throw codexFailure(
150
+ "connector_codex_cd_invalid",
151
+ "The Codex working directory is not a directory.",
152
+ );
153
+ }
154
+ try {
155
+ accessFile(workingDirectory, constants.R_OK | constants.W_OK | constants.X_OK);
156
+ } catch (error) {
157
+ throw codexFailure(
158
+ "connector_codex_cd_unusable",
159
+ "The Codex working directory is not readable and writable.",
160
+ error,
161
+ );
162
+ }
163
+ return workingDirectory;
164
+ }
165
+
166
+ export function requireSupportedNode(version = process.versions.node) {
167
+ const major = Number.parseInt(String(version).split(".")[0], 10);
168
+ if (!Number.isSafeInteger(major) || major < MINIMUM_NODE_MAJOR) {
169
+ throw codexFailure(
170
+ "connector_node_unsupported",
171
+ `The Local Connector requires Node.js ${MINIMUM_NODE_MAJOR} or newer.`,
172
+ );
173
+ }
174
+ return String(version);
175
+ }
176
+
177
+ function macOSBundleCandidates(applicationDirectories) {
178
+ return CODEX_APP_BUNDLE_NAMES.flatMap((bundleName) => applicationDirectories.map((directory) => (
179
+ join(directory, bundleName, "Contents", "Resources", "codex")
180
+ )));
181
+ }
182
+
183
+ function pathCandidates(command, pathValue) {
184
+ const directories = [
185
+ ...(typeof pathValue === "string" ? pathValue.split(delimiter) : []),
186
+ ];
187
+ return directories
188
+ .filter((directory) => directory.length > 0)
189
+ .map((directory) => join(directory, command));
190
+ }
191
+
192
+ function firstExecutable(candidates) {
193
+ const seen = new Set();
194
+ for (const candidate of candidates) {
195
+ const normalized = normalize(candidate);
196
+ if (seen.has(normalized)) continue;
197
+ seen.add(normalized);
198
+ if (isExecutableFile(normalized)) return normalized;
199
+ }
200
+ return null;
201
+ }
202
+
203
+ function isExecutableFile(path) {
204
+ try {
205
+ accessSync(path, constants.F_OK | constants.X_OK);
206
+ return statSync(path).isFile();
207
+ } catch {
208
+ return false;
209
+ }
210
+ }
211
+
212
+ function resolveReference(value, homeDirectory) {
213
+ if (value === "~") return homeDirectory;
214
+ if (value.startsWith("~/")) return join(homeDirectory, value.slice(2));
215
+ return value;
216
+ }
217
+
218
+ function readVersion(stdout, stderr) {
219
+ const value = typeof stdout === "string" && stdout.trim().length > 0
220
+ ? stdout
221
+ : typeof stderr === "string"
222
+ ? stderr
223
+ : "";
224
+ const firstLine = value.split(/\r?\n/, 1)[0].trim();
225
+ if (Buffer.byteLength(firstLine, "utf8") > MAX_VERSION_BYTES) {
226
+ return "available";
227
+ }
228
+ return firstLine || "available";
229
+ }
230
+
231
+ function requireReference(value, label) {
232
+ if (
233
+ typeof value !== "string" ||
234
+ value.length === 0 ||
235
+ value.trim() !== value ||
236
+ Buffer.byteLength(value, "utf8") > MAX_REFERENCE_BYTES ||
237
+ CONTROL_CHARACTER_PATTERN.test(value)
238
+ ) {
239
+ throw codexFailure("connector_configuration_invalid", `${label} is invalid.`);
240
+ }
241
+ return value;
242
+ }
243
+
244
+ function codexFailure(code, message, cause) {
245
+ return new CodexDiscoveryError(code, message, cause === undefined ? {} : { cause });
246
+ }
@@ -0,0 +1,197 @@
1
+ import { spawn } from "node:child_process";
2
+
3
+ import {
4
+ AGENT_ACTIVATION_RESULT_TYPE,
5
+ validateAgentActivation,
6
+ } from "@webmcp-challenge/reentry-core/agent-adapter";
7
+ import {
8
+ discoverCodexExecutable,
9
+ validateCodexWorkingDirectory,
10
+ } from "./codex-discovery.mjs";
11
+
12
+ export const CODEX_EXEC_ADAPTER_ID = "codex_exec_local";
13
+
14
+ const OPTION_FIELDS = Object.freeze([
15
+ "workingDirectory",
16
+ "executable",
17
+ "commandTimeoutMs",
18
+ "clock",
19
+ "spawnCommand",
20
+ ]);
21
+ const MIN_COMMAND_TIMEOUT_MS = 100;
22
+ const MAX_COMMAND_TIMEOUT_MS = 60_000;
23
+ const MAX_REFERENCE_BYTES = 4 * 1_024;
24
+ const CONTROL_CHARACTER_PATTERN = /[\u0000-\u001f\u007f]/;
25
+
26
+ /**
27
+ * Create the fresh-session Codex adapter that lives inside the Local Connector process.
28
+ *
29
+ * Each activation starts a new `codex exec` process. The activation's validated page context
30
+ * becomes a fixed prompt; no existing Codex thread or session is looked up or resumed.
31
+ */
32
+ export function createCodexExecAdapter(options) {
33
+ requireExactRecord(options, OPTION_FIELDS, ["workingDirectory"], "Codex exec adapter options");
34
+ const workingDirectory = validateCodexWorkingDirectory(options.workingDirectory);
35
+ const executable = options.executable === undefined
36
+ ? discoverCodexExecutable()
37
+ : requireReference(options.executable, "Codex executable");
38
+ const commandTimeoutMs = requireTimeout(options.commandTimeoutMs ?? 60_000);
39
+ const clock = options.clock ?? (() => new Date());
40
+ const spawnCommand = options.spawnCommand ?? spawn;
41
+ if (typeof clock !== "function") throw new TypeError("Codex exec adapter clock must be a function");
42
+ if (typeof spawnCommand !== "function") {
43
+ throw new TypeError("Codex exec adapter spawnCommand must be a function");
44
+ }
45
+
46
+ return Object.freeze({
47
+ async activate(rawActivation) {
48
+ const activation = validateAgentActivation(rawActivation);
49
+ const now = readClock(clock);
50
+ if (
51
+ Date.parse(activation.lease_expires_at) <= now.getTime() ||
52
+ Date.parse(activation.receipt.expires_at) <= now.getTime()
53
+ ) {
54
+ return activationResult(activation, "rejected", "activation_rejected", null);
55
+ }
56
+
57
+ await runCodexExec({
58
+ executable,
59
+ workingDirectory,
60
+ prompt: buildContinuationPrompt(activation),
61
+ timeoutMs: commandTimeoutMs,
62
+ spawnCommand,
63
+ });
64
+ return activationResult(
65
+ activation,
66
+ "accepted",
67
+ "activation_dispatch_accepted",
68
+ null,
69
+ );
70
+ },
71
+ });
72
+ }
73
+
74
+ function buildContinuationPrompt(activation) {
75
+ return [
76
+ "You are a Re-entry continuation agent.",
77
+ "This is a new session. Do not look for or resume another session.",
78
+ "Open the exact canonical page below and read its current state.",
79
+ "Continue the task using only the page's currently available WebMCP tools.",
80
+ "Prepare the next safe step, then stop before the human decision boundary.",
81
+ "Do not submit or perform the final consequential action.",
82
+ "",
83
+ "Re-entry context:",
84
+ `Workflow: ${activation.continuation.workflow_id}`,
85
+ `Event: ${activation.continuation.event_type}`,
86
+ `State version: ${activation.continuation.state_version}`,
87
+ `Canonical page: ${activation.continuation.canonical_url}`,
88
+ `Human boundary: ${activation.receipt.human_boundary}`,
89
+ ].join("\n");
90
+ }
91
+
92
+ function runCodexExec({ executable, workingDirectory, prompt, timeoutMs, spawnCommand }) {
93
+ return new Promise((resolve, reject) => {
94
+ let child;
95
+ let settled = false;
96
+ let timer;
97
+
98
+ const finish = (callback, value) => {
99
+ if (settled) return;
100
+ settled = true;
101
+ clearTimeout(timer);
102
+ callback(value);
103
+ };
104
+
105
+ try {
106
+ child = spawnCommand(
107
+ executable,
108
+ ["exec", "--cd", workingDirectory, prompt],
109
+ { stdio: ["ignore", "ignore", "ignore"] },
110
+ );
111
+ } catch (error) {
112
+ finish(reject, error);
113
+ return;
114
+ }
115
+
116
+ if (!child || typeof child.once !== "function") {
117
+ finish(reject, new Error("Codex exec process is invalid"));
118
+ return;
119
+ }
120
+
121
+ timer = setTimeout(() => {
122
+ try {
123
+ child.kill?.("SIGTERM");
124
+ } catch {
125
+ // The activation remains unknown even if the process cannot be terminated.
126
+ }
127
+ finish(reject, new Error("Codex exec process timed out"));
128
+ }, timeoutMs);
129
+
130
+ child.once("error", (error) => finish(reject, error));
131
+ child.once("close", (code, signal) => {
132
+ if (code === 0 && signal === null) {
133
+ finish(resolve);
134
+ return;
135
+ }
136
+ finish(reject, new Error("Codex exec process did not complete successfully"));
137
+ });
138
+ });
139
+ }
140
+
141
+ function activationResult(activation, outcome, code, unavailableCapability) {
142
+ return Object.freeze({
143
+ type: AGENT_ACTIVATION_RESULT_TYPE,
144
+ protocol_version: "0.1",
145
+ delivery_id: activation.delivery_id,
146
+ event_id: activation.event_id,
147
+ attempt: activation.attempt,
148
+ outcome,
149
+ code,
150
+ unavailable_capability: unavailableCapability,
151
+ });
152
+ }
153
+
154
+ function readClock(clock) {
155
+ const value = clock();
156
+ if (!(value instanceof Date) || !Number.isFinite(value.getTime())) {
157
+ throw new TypeError("Codex exec adapter clock must return a valid Date");
158
+ }
159
+ return new Date(value.getTime());
160
+ }
161
+
162
+ function requireTimeout(value) {
163
+ if (
164
+ !Number.isSafeInteger(value) ||
165
+ value < MIN_COMMAND_TIMEOUT_MS ||
166
+ value > MAX_COMMAND_TIMEOUT_MS
167
+ ) {
168
+ throw new TypeError("Codex exec adapter commandTimeoutMs is invalid");
169
+ }
170
+ return value;
171
+ }
172
+
173
+ function requireReference(value, label) {
174
+ if (
175
+ typeof value !== "string" ||
176
+ value.length === 0 ||
177
+ value.trim() !== value ||
178
+ Buffer.byteLength(value, "utf8") > MAX_REFERENCE_BYTES ||
179
+ CONTROL_CHARACTER_PATTERN.test(value)
180
+ ) {
181
+ throw new TypeError(`${label} is invalid`);
182
+ }
183
+ return value;
184
+ }
185
+
186
+ function requireExactRecord(value, allowedFields, requiredFields, label) {
187
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
188
+ throw new TypeError(`${label} must be an object`);
189
+ }
190
+ const fields = Object.keys(value);
191
+ if (
192
+ fields.some((field) => !allowedFields.includes(field)) ||
193
+ requiredFields.some((field) => !fields.includes(field))
194
+ ) {
195
+ throw new TypeError(`${label} fields are invalid`);
196
+ }
197
+ }
@@ -0,0 +1,214 @@
1
+ import { spawn } from "node:child_process";
2
+
3
+ import {
4
+ AGENT_ACTIVATION_RESULT_TYPE,
5
+ validateAgentActivation,
6
+ } from "@webmcp-challenge/reentry-core/agent-adapter";
7
+ import { createManagedContextAdapter } from "@webmcp-challenge/reentry-core/managed-context-adapter";
8
+
9
+ export const CODEX_QUEUE_ADAPTER_ID = "codex_queue_local";
10
+ export const DEFAULT_CODEX_EXECUTABLE = "/Applications/ChatGPT.app/Contents/Resources/codex";
11
+
12
+ const OPTION_FIELDS = Object.freeze([
13
+ "threadId",
14
+ "executable",
15
+ "commandTimeoutMs",
16
+ "clock",
17
+ "spawnCommand",
18
+ ]);
19
+ const MIN_COMMAND_TIMEOUT_MS = 100;
20
+ const MAX_COMMAND_TIMEOUT_MS = 60_000;
21
+ const MAX_REFERENCE_BYTES = 4 * 1_024;
22
+ const CONTROL_CHARACTER_PATTERN = /[\u0000-\u001f\u007f]/;
23
+
24
+ /**
25
+ * Create the Codex adapter that lives inside the Local Connector process.
26
+ *
27
+ * This is a deliberately small local preview. The configured thread is private local binding
28
+ * state and never comes from the Receiver event or the Agent activation value. A future
29
+ * production adapter must replace the single-session binding with a Grant-scoped binding
30
+ * authority and prove Browser/WebMCP acquisition separately.
31
+ */
32
+ export function createCodexQueueAdapter(options) {
33
+ requireExactRecord(options, OPTION_FIELDS, ["threadId"], "Codex queue adapter options");
34
+ const threadId = requireReference(options.threadId, "Codex thread");
35
+ const executable = requireReference(
36
+ options.executable ?? DEFAULT_CODEX_EXECUTABLE,
37
+ "Codex executable",
38
+ );
39
+ const commandTimeoutMs = requireTimeout(options.commandTimeoutMs ?? 5_000);
40
+ const clock = options.clock ?? (() => new Date());
41
+ const spawnCommand = options.spawnCommand ?? spawn;
42
+ if (typeof clock !== "function") throw new TypeError("Codex queue adapter clock must be a function");
43
+ if (typeof spawnCommand !== "function") {
44
+ throw new TypeError("Codex queue adapter spawnCommand must be a function");
45
+ }
46
+
47
+ const bindingsByGrant = new Map();
48
+ const managedAdapter = createManagedContextAdapter({
49
+ adapterId: CODEX_QUEUE_ADAPTER_ID,
50
+ bindingAuthority: {
51
+ resolveBinding({ grantId }) {
52
+ return bindingsByGrant.get(grantId) ?? null;
53
+ },
54
+ },
55
+ activateBoundContext({ activation, bindingRef }) {
56
+ return queueCodexMessage({
57
+ executable,
58
+ threadId: bindingRef,
59
+ message: buildContinuationMessage(activation),
60
+ timeoutMs: commandTimeoutMs,
61
+ spawnCommand,
62
+ }).then(() => activationResult(
63
+ activation,
64
+ "accepted",
65
+ "activation_dispatch_accepted",
66
+ null,
67
+ ));
68
+ },
69
+ clock,
70
+ });
71
+
72
+ return Object.freeze({
73
+ async activate(rawActivation) {
74
+ const activation = validateAgentActivation(rawActivation);
75
+ const now = readClock(clock);
76
+ if (
77
+ Date.parse(activation.lease_expires_at) <= now.getTime() ||
78
+ Date.parse(activation.receipt.expires_at) <= now.getTime()
79
+ ) {
80
+ return activationResult(activation, "rejected", "activation_rejected", null);
81
+ }
82
+
83
+ const grantId = activation.receipt.grant_id;
84
+ if (!bindingsByGrant.has(grantId)) {
85
+ bindingsByGrant.set(grantId, {
86
+ type: "webmcp.managed_context_binding",
87
+ protocol_version: "0.1",
88
+ grant_id: grantId,
89
+ adapter_id: CODEX_QUEUE_ADAPTER_ID,
90
+ binding_ref: threadId,
91
+ bound_at: now.toISOString(),
92
+ expires_at: activation.receipt.expires_at,
93
+ });
94
+ }
95
+ return managedAdapter.activate(activation);
96
+ },
97
+ });
98
+ }
99
+
100
+ function buildContinuationMessage(activation) {
101
+ return [
102
+ "Re-entry continuation is ready.",
103
+ "Open the exact canonical page below, read its current state, and continue the existing task.",
104
+ "Use only the page's current WebMCP tools and stop before the human decision boundary.",
105
+ `Canonical page: ${activation.continuation.canonical_url}`,
106
+ ].join("\n");
107
+ }
108
+
109
+ function queueCodexMessage({ executable, threadId, message, timeoutMs, spawnCommand }) {
110
+ return new Promise((resolve, reject) => {
111
+ let child;
112
+ let settled = false;
113
+ let timer;
114
+
115
+ const finish = (callback, value) => {
116
+ if (settled) return;
117
+ settled = true;
118
+ clearTimeout(timer);
119
+ callback(value);
120
+ };
121
+
122
+ try {
123
+ child = spawnCommand(
124
+ executable,
125
+ ["queue", "--thread", threadId, "--message", message],
126
+ { stdio: ["ignore", "ignore", "ignore"] },
127
+ );
128
+ } catch (error) {
129
+ finish(reject, error);
130
+ return;
131
+ }
132
+
133
+ if (!child || typeof child.once !== "function") {
134
+ finish(reject, new Error("Codex queue process is invalid"));
135
+ return;
136
+ }
137
+
138
+ timer = setTimeout(() => {
139
+ try {
140
+ child.kill?.("SIGTERM");
141
+ } catch {
142
+ // The activation remains unknown even if the process cannot be terminated.
143
+ }
144
+ finish(reject, new Error("Codex queue process timed out"));
145
+ }, timeoutMs);
146
+
147
+ child.once("error", (error) => finish(reject, error));
148
+ child.once("close", (code, signal) => {
149
+ if (code === 0 && signal === null) {
150
+ finish(resolve);
151
+ return;
152
+ }
153
+ finish(reject, new Error("Codex queue process did not complete successfully"));
154
+ });
155
+ });
156
+ }
157
+
158
+ function activationResult(activation, outcome, code, unavailableCapability) {
159
+ return Object.freeze({
160
+ type: AGENT_ACTIVATION_RESULT_TYPE,
161
+ protocol_version: "0.1",
162
+ delivery_id: activation.delivery_id,
163
+ event_id: activation.event_id,
164
+ attempt: activation.attempt,
165
+ outcome,
166
+ code,
167
+ unavailable_capability: unavailableCapability,
168
+ });
169
+ }
170
+
171
+ function readClock(clock) {
172
+ const value = clock();
173
+ if (!(value instanceof Date) || !Number.isFinite(value.getTime())) {
174
+ throw new TypeError("Codex queue adapter clock must return a valid Date");
175
+ }
176
+ return new Date(value.getTime());
177
+ }
178
+
179
+ function requireTimeout(value) {
180
+ if (
181
+ !Number.isSafeInteger(value) ||
182
+ value < MIN_COMMAND_TIMEOUT_MS ||
183
+ value > MAX_COMMAND_TIMEOUT_MS
184
+ ) {
185
+ throw new TypeError("Codex queue adapter commandTimeoutMs is invalid");
186
+ }
187
+ return value;
188
+ }
189
+
190
+ function requireReference(value, label) {
191
+ if (
192
+ typeof value !== "string" ||
193
+ value.length === 0 ||
194
+ value.trim() !== value ||
195
+ Buffer.byteLength(value, "utf8") > MAX_REFERENCE_BYTES ||
196
+ CONTROL_CHARACTER_PATTERN.test(value)
197
+ ) {
198
+ throw new TypeError(`${label} is invalid`);
199
+ }
200
+ return value;
201
+ }
202
+
203
+ function requireExactRecord(value, allowedFields, requiredFields, label) {
204
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
205
+ throw new TypeError(`${label} must be an object`);
206
+ }
207
+ const fields = Object.keys(value);
208
+ if (
209
+ fields.some((field) => !allowedFields.includes(field)) ||
210
+ requiredFields.some((field) => !fields.includes(field))
211
+ ) {
212
+ throw new TypeError(`${label} fields are invalid`);
213
+ }
214
+ }