@right-link/paperclip-plugin-codex-remote 0.3.1 → 0.3.2
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/dist/chunk-FULSN5VN.js +5348 -0
- package/dist/chunk-YJYVA7CY.js +99 -0
- package/dist/chunk-ZLN6QQMX.js +3267 -0
- package/dist/cli/index.js +190 -2
- package/dist/index.js +28 -84
- package/dist/server/adapter.js +140 -135
- package/dist/server/index.js +41 -56
- package/dist/server-utils-C4H4WJOG.js +104 -0
- package/dist/ui/index.js +318 -3
- package/package.json +7 -5
- package/dist/cli/format-event.js +0 -213
- package/dist/cli/format-event.js.map +0 -1
- package/dist/cli/index.js.map +0 -1
- package/dist/cli/quota-probe.js +0 -97
- package/dist/cli/quota-probe.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/server/adapter.js.map +0 -1
- package/dist/server/adapter.test.js +0 -84
- package/dist/server/adapter.test.js.map +0 -1
- package/dist/server/codex-args.js +0 -60
- package/dist/server/codex-args.js.map +0 -1
- package/dist/server/codex-args.test.js +0 -94
- package/dist/server/codex-args.test.js.map +0 -1
- package/dist/server/codex-home.js +0 -378
- package/dist/server/codex-home.js.map +0 -1
- package/dist/server/codex-home.test.js +0 -244
- package/dist/server/codex-home.test.js.map +0 -1
- package/dist/server/execute.js +0 -906
- package/dist/server/execute.js.map +0 -1
- package/dist/server/execute.remote.test.js +0 -487
- package/dist/server/execute.remote.test.js.map +0 -1
- package/dist/server/index.js.map +0 -1
- package/dist/server/parse.js +0 -213
- package/dist/server/parse.js.map +0 -1
- package/dist/server/parse.test.js +0 -107
- package/dist/server/parse.test.js.map +0 -1
- package/dist/server/quota-spawn-error.test.js +0 -77
- package/dist/server/quota-spawn-error.test.js.map +0 -1
- package/dist/server/quota.js +0 -432
- package/dist/server/quota.js.map +0 -1
- package/dist/server/sandbox-env.js +0 -23
- package/dist/server/sandbox-env.js.map +0 -1
- package/dist/server/skills.js +0 -24
- package/dist/server/skills.js.map +0 -1
- package/dist/server/tailscale.js +0 -95
- package/dist/server/tailscale.js.map +0 -1
- package/dist/server/test.js +0 -811
- package/dist/server/test.js.map +0 -1
- package/dist/server/test.remote.test.js +0 -257
- package/dist/server/test.remote.test.js.map +0 -1
- package/dist/ui/build-config.js +0 -113
- package/dist/ui/build-config.js.map +0 -1
- package/dist/ui/build-config.test.js +0 -49
- package/dist/ui/build-config.test.js.map +0 -1
- package/dist/ui/index.js.map +0 -1
- package/dist/ui/parse-stdout.js +0 -261
- package/dist/ui/parse-stdout.js.map +0 -1
- package/dist/ui/parse-stdout.test.js +0 -77
- package/dist/ui/parse-stdout.test.js.map +0 -1
- package/dist/ui-parser.js.map +0 -1
package/dist/server/test.js
DELETED
|
@@ -1,811 +0,0 @@
|
|
|
1
|
-
import { asString, parseObject, ensurePathInEnv, } from "@paperclipai/adapter-utils/server-utils";
|
|
2
|
-
import { ensureAdapterExecutionTargetCommandResolvable, ensureAdapterExecutionTargetDirectory, maybeRunSandboxInstallCommand, runAdapterExecutionTargetProcess, runAdapterExecutionTargetShellCommand, describeAdapterExecutionTarget, resolveAdapterExecutionTargetCwd, prepareAdapterExecutionTargetRuntime, } from "@paperclipai/adapter-utils/execution-target";
|
|
3
|
-
import fs from "node:fs/promises";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import os from "node:os";
|
|
6
|
-
import { parseCodexJsonl } from "./parse.js";
|
|
7
|
-
import { SANDBOX_INSTALL_COMMAND } from "../index.js";
|
|
8
|
-
import { applyTailscaleProxyEnv, ensureSandboxTailscaleUp, readTailscaleAuthKey } from "./tailscale.js";
|
|
9
|
-
import { stripNonPosixSandboxEnvKeys } from "./sandbox-env.js";
|
|
10
|
-
import { codexHomeDir, readCodexAuthInfo } from "./quota.js";
|
|
11
|
-
import { buildCodexExecArgs } from "./codex-args.js";
|
|
12
|
-
import { prepareManagedCodexHome, prepareRemoteCodexHomeAsset } from "./codex-home.js";
|
|
13
|
-
function summarizeStatus(checks) {
|
|
14
|
-
if (checks.some((check) => check.level === "error"))
|
|
15
|
-
return "fail";
|
|
16
|
-
if (checks.some((check) => check.level === "warn"))
|
|
17
|
-
return "warn";
|
|
18
|
-
return "pass";
|
|
19
|
-
}
|
|
20
|
-
function isNonEmpty(value) {
|
|
21
|
-
return typeof value === "string" && value.trim().length > 0;
|
|
22
|
-
}
|
|
23
|
-
function firstNonEmptyLine(text) {
|
|
24
|
-
return (text
|
|
25
|
-
.split(/\r?\n/)
|
|
26
|
-
.map((line) => line.trim())
|
|
27
|
-
.find(Boolean) ?? "");
|
|
28
|
-
}
|
|
29
|
-
function commandLooksLike(command, expected) {
|
|
30
|
-
const base = path.basename(command).toLowerCase();
|
|
31
|
-
return base === expected || base === `${expected}.cmd` || base === `${expected}.exe`;
|
|
32
|
-
}
|
|
33
|
-
function summarizeProbeDetail(stdout, stderr, parsedError) {
|
|
34
|
-
const raw = parsedError?.trim() || firstNonEmptyLine(stderr) || firstNonEmptyLine(stdout);
|
|
35
|
-
if (!raw)
|
|
36
|
-
return null;
|
|
37
|
-
const clean = raw.replace(/\s+/g, " ").trim();
|
|
38
|
-
const max = 240;
|
|
39
|
-
return clean.length > max ? `${clean.slice(0, max - 1)}…` : clean;
|
|
40
|
-
}
|
|
41
|
-
const CODEX_AUTH_REQUIRED_RE = /(?:not\s+logged\s+in|login\s+required|authentication\s+required|authentication\s+token\s+has\s+been\s+invalidated|token_invalidated|unauthorized|invalid(?:\s+or\s+missing)?\s+api(?:[_\s-]?key)?|openai[_\s-]?api[_\s-]?key|api[_\s-]?key.*required|please\s+run\s+`?codex\s+login`?)/i;
|
|
42
|
-
// A cold Cloudflare sandbox needs ~60-90s for the first `codex exec`: the CLI
|
|
43
|
-
// cold-starts, performs a model-refresh that can itself time out, then runs the
|
|
44
|
-
// turn. The host RPC budget is this value + a 30s overhead buffer (see
|
|
45
|
-
// resolvePluginExecuteRpcTimeoutMs), capped at 15 min, so 150s leaves the RPC
|
|
46
|
-
// ~180s — enough to actually observe the probe result instead of the RPC
|
|
47
|
-
// timing out and surfacing an opaque 500.
|
|
48
|
-
const SANDBOX_HELLO_PROBE_TIMEOUT_SEC = 150;
|
|
49
|
-
function parseKeyValueLines(text) {
|
|
50
|
-
const out = {};
|
|
51
|
-
for (const line of text.split(/\r?\n/)) {
|
|
52
|
-
const match = /^([^=]+)=(.*)$/.exec(line.trim());
|
|
53
|
-
if (!match)
|
|
54
|
-
continue;
|
|
55
|
-
out[match[1].trim()] = match[2].trim();
|
|
56
|
-
}
|
|
57
|
-
return out;
|
|
58
|
-
}
|
|
59
|
-
async function addRemoteCodexHomeDiagnostics(input) {
|
|
60
|
-
const codexHome = input.env.CODEX_HOME?.trim();
|
|
61
|
-
if (!codexHome || !input.target || input.target.kind !== "remote")
|
|
62
|
-
return;
|
|
63
|
-
const probe = await runAdapterExecutionTargetShellCommand(input.runId, input.target, [
|
|
64
|
-
'printf "home=%s\\n" "$CODEX_HOME"',
|
|
65
|
-
"for name in auth.json config.toml config.json instructions.md; do",
|
|
66
|
-
' file="$CODEX_HOME/$name"',
|
|
67
|
-
' if [ -f "$file" ]; then',
|
|
68
|
-
' bytes=$(wc -c < "$file" | tr -d "[:space:]")',
|
|
69
|
-
' printf "%s=%s\\n" "$name" "$bytes"',
|
|
70
|
-
" else",
|
|
71
|
-
' printf "%s=missing\\n" "$name"',
|
|
72
|
-
" fi",
|
|
73
|
-
"done",
|
|
74
|
-
].join("\n"), {
|
|
75
|
-
cwd: input.cwd,
|
|
76
|
-
env: input.env,
|
|
77
|
-
timeoutSec: 15,
|
|
78
|
-
graceSec: 5,
|
|
79
|
-
onLog: async () => { },
|
|
80
|
-
}).catch((error) => ({
|
|
81
|
-
exitCode: 1,
|
|
82
|
-
signal: null,
|
|
83
|
-
timedOut: false,
|
|
84
|
-
stdout: "",
|
|
85
|
-
stderr: error instanceof Error ? error.message : String(error),
|
|
86
|
-
pid: null,
|
|
87
|
-
startedAt: new Date().toISOString(),
|
|
88
|
-
}));
|
|
89
|
-
if (probe.timedOut) {
|
|
90
|
-
input.checks.push({
|
|
91
|
-
code: "codex_remote_home_diagnostic_timed_out",
|
|
92
|
-
level: "warn",
|
|
93
|
-
message: "Timed out checking remote Codex home.",
|
|
94
|
-
hint: "The sandbox command runner is slow before Codex starts; retry the environment test.",
|
|
95
|
-
});
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
if ((probe.exitCode ?? 1) !== 0) {
|
|
99
|
-
input.checks.push({
|
|
100
|
-
code: "codex_remote_home_diagnostic_failed",
|
|
101
|
-
level: "warn",
|
|
102
|
-
message: "Could not inspect remote Codex home.",
|
|
103
|
-
detail: summarizeProbeDetail(probe.stdout, probe.stderr, null) ?? undefined,
|
|
104
|
-
});
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
const facts = parseKeyValueLines(probe.stdout);
|
|
108
|
-
const authBytes = facts["auth.json"];
|
|
109
|
-
if (authBytes && authBytes !== "missing") {
|
|
110
|
-
input.checks.push({
|
|
111
|
-
code: "codex_remote_auth_present",
|
|
112
|
-
level: "info",
|
|
113
|
-
message: "Remote Codex auth file is present.",
|
|
114
|
-
detail: `CODEX_HOME=${facts.home ?? codexHome}; auth.json=${authBytes} bytes`,
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
else {
|
|
118
|
-
input.checks.push({
|
|
119
|
-
code: "codex_remote_auth_not_shipped",
|
|
120
|
-
level: "info",
|
|
121
|
-
message: "Remote Codex auth.json is not shipped (by design).",
|
|
122
|
-
detail: `CODEX_HOME=${facts.home ?? codexHome}`,
|
|
123
|
-
hint: "Remote runs authenticate via the sanitized config.toml provider (e.g. requires_openai_auth = false + a bearer token), so no ChatGPT auth.json is copied into the sandbox.",
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
const configFacts = ["config.toml", "config.json", "instructions.md"]
|
|
127
|
-
.map((name) => `${name}=${facts[name] ?? "unknown"}`)
|
|
128
|
-
.join("; ");
|
|
129
|
-
input.checks.push({
|
|
130
|
-
code: "codex_remote_home_files",
|
|
131
|
-
level: "info",
|
|
132
|
-
message: "Remote Codex home file check completed.",
|
|
133
|
-
detail: configFacts,
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
async function addRemoteCodexCliDiagnostics(input) {
|
|
137
|
-
const codexHome = input.env.CODEX_HOME?.trim();
|
|
138
|
-
if (!codexHome || !input.target || input.target.kind !== "remote")
|
|
139
|
-
return null;
|
|
140
|
-
const probe = await runAdapterExecutionTargetShellCommand(input.runId, input.target, [
|
|
141
|
-
`codex_path=$(command -v ${JSON.stringify(input.command)} 2>/dev/null || true)`,
|
|
142
|
-
'printf "codex_path=%s\\n" "$codex_path"',
|
|
143
|
-
`version=$(${JSON.stringify(input.command)} --version 2>&1 | head -n 1 || true)`,
|
|
144
|
-
'printf "codex_version=%s\\n" "$version"',
|
|
145
|
-
"node -e \"const fs=require('fs'); const p=process.env.CODEX_HOME + '/auth.json'; const j=JSON.parse(fs.readFileSync(p,'utf8')); console.log('auth_mode=' + (j.auth_mode || '')); console.log('auth_keys=' + Object.keys(j).sort().join(','));\" 2>/dev/null || printf \"auth_mode=unreadable\\nauth_keys=unreadable\\n\"",
|
|
146
|
-
`help=$(${JSON.stringify(input.command)} exec --help 2>&1 | head -n 1 || true)`,
|
|
147
|
-
'printf "exec_help=%s\\n" "$help"',
|
|
148
|
-
].join("\n"), {
|
|
149
|
-
cwd: input.cwd,
|
|
150
|
-
env: input.env,
|
|
151
|
-
timeoutSec: 20,
|
|
152
|
-
graceSec: 5,
|
|
153
|
-
onLog: async () => { },
|
|
154
|
-
}).catch((error) => ({
|
|
155
|
-
exitCode: 1,
|
|
156
|
-
signal: null,
|
|
157
|
-
timedOut: false,
|
|
158
|
-
stdout: "",
|
|
159
|
-
stderr: error instanceof Error ? error.message : String(error),
|
|
160
|
-
pid: null,
|
|
161
|
-
startedAt: new Date().toISOString(),
|
|
162
|
-
}));
|
|
163
|
-
if (probe.timedOut) {
|
|
164
|
-
input.checks.push({
|
|
165
|
-
code: "codex_remote_cli_diagnostic_timed_out",
|
|
166
|
-
level: "warn",
|
|
167
|
-
message: "Timed out checking remote Codex CLI diagnostics.",
|
|
168
|
-
hint: "The CLI may be hanging before the hello probe. Check `codex --version` manually inside the sandbox.",
|
|
169
|
-
});
|
|
170
|
-
return null;
|
|
171
|
-
}
|
|
172
|
-
if ((probe.exitCode ?? 1) !== 0) {
|
|
173
|
-
input.checks.push({
|
|
174
|
-
code: "codex_remote_cli_diagnostic_failed",
|
|
175
|
-
level: "warn",
|
|
176
|
-
message: "Could not inspect remote Codex CLI diagnostics.",
|
|
177
|
-
detail: summarizeProbeDetail(probe.stdout, probe.stderr, null) ?? undefined,
|
|
178
|
-
});
|
|
179
|
-
return null;
|
|
180
|
-
}
|
|
181
|
-
const facts = parseKeyValueLines(probe.stdout);
|
|
182
|
-
input.checks.push({
|
|
183
|
-
code: "codex_remote_cli_version",
|
|
184
|
-
level: "info",
|
|
185
|
-
message: "Remote Codex CLI diagnostic completed.",
|
|
186
|
-
detail: `path=${facts.codex_path ?? "unknown"}; version=${facts.codex_version ?? "unknown"}`,
|
|
187
|
-
});
|
|
188
|
-
input.checks.push({
|
|
189
|
-
code: "codex_remote_auth_mode",
|
|
190
|
-
level: "info",
|
|
191
|
-
message: facts.auth_mode === "chatgpt"
|
|
192
|
-
? "Remote Codex auth is ChatGPT subscription mode."
|
|
193
|
-
: "Remote Codex auth mode was detected.",
|
|
194
|
-
detail: `auth_mode=${facts.auth_mode ?? "unknown"}; auth_keys=${facts.auth_keys ?? "unknown"}`,
|
|
195
|
-
...(facts.auth_mode === "chatgpt"
|
|
196
|
-
? {
|
|
197
|
-
hint: "ChatGPT-mode Codex auth copied from another machine may require an interactive refresh inside the sandbox.",
|
|
198
|
-
}
|
|
199
|
-
: {}),
|
|
200
|
-
});
|
|
201
|
-
input.checks.push({
|
|
202
|
-
code: "codex_remote_exec_help",
|
|
203
|
-
level: facts.exec_help ? "info" : "warn",
|
|
204
|
-
message: facts.exec_help ? "Remote `codex exec --help` returned." : "Remote `codex exec --help` returned no output.",
|
|
205
|
-
...(facts.exec_help ? { detail: facts.exec_help.slice(0, 240) } : {}),
|
|
206
|
-
});
|
|
207
|
-
return {
|
|
208
|
-
authMode: facts.auth_mode ?? null,
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
async function addRemoteCodexProviderDiagnostics(input) {
|
|
212
|
-
const codexHome = input.env.CODEX_HOME?.trim();
|
|
213
|
-
if (!codexHome || !input.target || input.target.kind !== "remote")
|
|
214
|
-
return null;
|
|
215
|
-
const probe = await runAdapterExecutionTargetShellCommand(input.runId, input.target, [
|
|
216
|
-
"if command -v tailscale >/dev/null 2>&1; then",
|
|
217
|
-
" tailscale_ip=$(tailscale ip -4 2>/dev/null | head -n 1 || true)",
|
|
218
|
-
" tailscale_status=$(tailscale status 2>&1 | head -n 1 || true)",
|
|
219
|
-
" printf \"tailscale_ip=%s\\n\" \"$tailscale_ip\"",
|
|
220
|
-
" printf \"tailscale_status=%s\\n\" \"$tailscale_status\"",
|
|
221
|
-
"else",
|
|
222
|
-
" printf \"tailscale_ip=missing_cli\\n\"",
|
|
223
|
-
" printf \"tailscale_status=missing_cli\\n\"",
|
|
224
|
-
"fi",
|
|
225
|
-
"node <<'NODE'",
|
|
226
|
-
"const { spawnSync } = require('child_process');",
|
|
227
|
-
"const fs = require('fs');",
|
|
228
|
-
"const net = require('net');",
|
|
229
|
-
"(async () => {",
|
|
230
|
-
"const cfg = fs.readFileSync(`${process.env.CODEX_HOME}/config.toml`, 'utf8');",
|
|
231
|
-
"const readTop = (key) => (cfg.match(new RegExp(`^${key}\\\\s*=\\\\s*\"([^\"]*)\"`, 'm')) || [])[1] || '';",
|
|
232
|
-
"const provider = readTop('model_provider');",
|
|
233
|
-
"const escapedProvider = provider.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');",
|
|
234
|
-
"const section = provider ? (cfg.match(new RegExp(`\\\\[model_providers\\\\.${escapedProvider}\\\\]([\\\\s\\\\S]*?)(?:\\\\n\\\\[|$)`)) || [])[1] || '' : '';",
|
|
235
|
-
"const readSection = (key) => (section.match(new RegExp(`^${key}\\\\s*=\\\\s*\"([^\"]*)\"`, 'm')) || [])[1] || '';",
|
|
236
|
-
"const base = readSection('base_url');",
|
|
237
|
-
"const wire = readSection('wire_api');",
|
|
238
|
-
"const requiresOpenaiAuth = /^\\s*requires_openai_auth\\s*=\\s*true/m.test(section);",
|
|
239
|
-
"const hasBearerToken = /^\\s*experimental_bearer_token\\s*=\\s*\"/m.test(section) || /^\\s*env_key\\s*=\\s*\"/m.test(section);",
|
|
240
|
-
"console.log(`model_provider=${provider}`);",
|
|
241
|
-
"console.log(`provider_base_url=${base}`);",
|
|
242
|
-
"console.log(`provider_wire_api=${wire}`);",
|
|
243
|
-
"console.log(`provider_requires_openai_auth=${requiresOpenaiAuth}`);",
|
|
244
|
-
"console.log(`provider_has_bearer_token=${hasBearerToken}`);",
|
|
245
|
-
"if (!base) { console.log('provider_tcp=skipped'); console.log('provider_http=skipped'); process.exit(0); }",
|
|
246
|
-
"let url;",
|
|
247
|
-
"try { url = new URL(base); } catch { console.log('provider_tcp=invalid_url'); console.log('provider_http=invalid_url'); process.exit(0); }",
|
|
248
|
-
"const port = Number(url.port || (url.protocol === 'https:' ? 443 : 80));",
|
|
249
|
-
"const tcp = await new Promise((resolve) => {",
|
|
250
|
-
" const socket = net.createConnection({ host: url.hostname, port, timeout: 5000 });",
|
|
251
|
-
" let done = false;",
|
|
252
|
-
" const finish = (value) => { if (done) return; done = true; socket.destroy(); resolve(value); };",
|
|
253
|
-
" socket.on('connect', () => finish('connect'));",
|
|
254
|
-
" socket.on('timeout', () => finish('timeout'));",
|
|
255
|
-
" socket.on('error', (error) => finish(`error:${error.code}`));",
|
|
256
|
-
"});",
|
|
257
|
-
"console.log(`provider_tcp=${tcp}`);",
|
|
258
|
-
"const modelsUrl = new URL(base);",
|
|
259
|
-
"modelsUrl.pathname = `${modelsUrl.pathname.replace(/\\/+$/, '')}/models`;",
|
|
260
|
-
"const curl = spawnSync('curl', ['-sS', '-o', '/dev/null', '-w', '%{http_code}', '--max-time', '8', modelsUrl.toString()], {",
|
|
261
|
-
" encoding: 'utf8',",
|
|
262
|
-
" env: process.env,",
|
|
263
|
-
"});",
|
|
264
|
-
"if (curl.error && curl.error.code === 'ENOENT') {",
|
|
265
|
-
" console.log('provider_http=missing_curl');",
|
|
266
|
-
"} else if (curl.status === 0) {",
|
|
267
|
-
" console.log(`provider_http=status:${String(curl.stdout || '').trim() || '000'}`);",
|
|
268
|
-
"} else {",
|
|
269
|
-
" const stderr = String(curl.stderr || '').replace(/\\s+/g, ' ').trim().slice(0, 120);",
|
|
270
|
-
" console.log(`provider_http=error:${curl.status}${stderr ? `:${stderr}` : ''}`);",
|
|
271
|
-
"}",
|
|
272
|
-
"})().catch((error) => {",
|
|
273
|
-
" console.log(`provider_diagnostic_error=${String(error && error.message ? error.message : error).replace(/\\s+/g, ' ').slice(0, 200)}`);",
|
|
274
|
-
" process.exitCode = 1;",
|
|
275
|
-
"});",
|
|
276
|
-
"NODE",
|
|
277
|
-
].join("\n"), {
|
|
278
|
-
cwd: input.cwd,
|
|
279
|
-
env: input.env,
|
|
280
|
-
timeoutSec: 20,
|
|
281
|
-
graceSec: 5,
|
|
282
|
-
onLog: async () => { },
|
|
283
|
-
}).catch((error) => ({
|
|
284
|
-
exitCode: 1,
|
|
285
|
-
signal: null,
|
|
286
|
-
timedOut: false,
|
|
287
|
-
stdout: "",
|
|
288
|
-
stderr: error instanceof Error ? error.message : String(error),
|
|
289
|
-
pid: null,
|
|
290
|
-
startedAt: new Date().toISOString(),
|
|
291
|
-
}));
|
|
292
|
-
if (probe.timedOut) {
|
|
293
|
-
input.checks.push({
|
|
294
|
-
code: "codex_remote_provider_diagnostic_timed_out",
|
|
295
|
-
level: "warn",
|
|
296
|
-
message: "Timed out checking remote Codex model provider connectivity.",
|
|
297
|
-
});
|
|
298
|
-
return null;
|
|
299
|
-
}
|
|
300
|
-
if ((probe.exitCode ?? 1) !== 0) {
|
|
301
|
-
input.checks.push({
|
|
302
|
-
code: "codex_remote_provider_diagnostic_failed",
|
|
303
|
-
level: "warn",
|
|
304
|
-
message: "Could not inspect remote Codex model provider configuration.",
|
|
305
|
-
detail: summarizeProbeDetail(probe.stdout, probe.stderr, null) ?? undefined,
|
|
306
|
-
});
|
|
307
|
-
return null;
|
|
308
|
-
}
|
|
309
|
-
const facts = parseKeyValueLines(probe.stdout);
|
|
310
|
-
const tcp = facts.provider_tcp ?? "unknown";
|
|
311
|
-
const http = facts.provider_http ?? "unknown";
|
|
312
|
-
const httpReachable = /^status:(?!000)/.test(http);
|
|
313
|
-
const providerReachable = tcp === "connect" || httpReachable;
|
|
314
|
-
const tailscaleIp = facts.tailscale_ip ?? "unknown";
|
|
315
|
-
const tailscaleStatus = facts.tailscale_status ?? "unknown";
|
|
316
|
-
input.checks.push({
|
|
317
|
-
code: "codex_remote_tailscale_status",
|
|
318
|
-
level: tailscaleIp && tailscaleIp !== "missing_cli" ? "info" : "warn",
|
|
319
|
-
message: tailscaleIp && tailscaleIp !== "missing_cli"
|
|
320
|
-
? "Remote sandbox Tailscale status was detected."
|
|
321
|
-
: "Remote sandbox Tailscale status could not be detected.",
|
|
322
|
-
detail: `tailscale_ip=${tailscaleIp}; status=${tailscaleStatus}`,
|
|
323
|
-
...(tailscaleIp && tailscaleIp !== "missing_cli"
|
|
324
|
-
? {}
|
|
325
|
-
: {
|
|
326
|
-
hint: "Install Tailscale in the sandbox image and set TAILSCALE_AUTHKEY on the Cloudflare Worker.",
|
|
327
|
-
}),
|
|
328
|
-
});
|
|
329
|
-
input.checks.push({
|
|
330
|
-
code: "codex_remote_model_provider",
|
|
331
|
-
level: "info",
|
|
332
|
-
message: "Remote Codex model provider configuration was detected.",
|
|
333
|
-
detail: `model_provider=${facts.model_provider ?? "unknown"}; base_url=${facts.provider_base_url ?? "unknown"}; wire_api=${facts.provider_wire_api ?? "unknown"}; requires_openai_auth=${facts.provider_requires_openai_auth ?? "unknown"}; bearer_token=${facts.provider_has_bearer_token === "true" ? "present" : facts.provider_has_bearer_token === "false" ? "absent" : "unknown"}`,
|
|
334
|
-
});
|
|
335
|
-
input.checks.push({
|
|
336
|
-
code: "codex_remote_model_provider_connectivity",
|
|
337
|
-
level: providerReachable ? "info" : "warn",
|
|
338
|
-
message: providerReachable
|
|
339
|
-
? "Remote sandbox can reach the configured Codex model provider."
|
|
340
|
-
: "Remote sandbox could not confirm connectivity to the configured Codex model provider.",
|
|
341
|
-
detail: `provider_tcp=${tcp}; provider_http=${http}`,
|
|
342
|
-
...(providerReachable
|
|
343
|
-
? {}
|
|
344
|
-
: {
|
|
345
|
-
hint: "If your provider base_url points at a local/Tailscale proxy, the remote sandbox must be able to reach that network address too.",
|
|
346
|
-
}),
|
|
347
|
-
});
|
|
348
|
-
return {
|
|
349
|
-
baseUrl: facts.provider_base_url ?? null,
|
|
350
|
-
tcp,
|
|
351
|
-
http,
|
|
352
|
-
requiresOpenaiAuth: facts.provider_requires_openai_auth === "true"
|
|
353
|
-
? true
|
|
354
|
-
: facts.provider_requires_openai_auth === "false"
|
|
355
|
-
? false
|
|
356
|
-
: null,
|
|
357
|
-
hasBearerToken: facts.provider_has_bearer_token === "true"
|
|
358
|
-
? true
|
|
359
|
-
: facts.provider_has_bearer_token === "false"
|
|
360
|
-
? false
|
|
361
|
-
: null,
|
|
362
|
-
};
|
|
363
|
-
}
|
|
364
|
-
async function prepareCodexHelloProbe(input) {
|
|
365
|
-
let preparedRuntime = null;
|
|
366
|
-
let preparedRuntimeWorkspaceLocalDir = null;
|
|
367
|
-
let remoteCodexHomeAssetCleanup = null;
|
|
368
|
-
const cleanup = async () => {
|
|
369
|
-
await preparedRuntime?.restoreWorkspace().catch(() => { });
|
|
370
|
-
if (preparedRuntimeWorkspaceLocalDir) {
|
|
371
|
-
await fs.rm(preparedRuntimeWorkspaceLocalDir, { recursive: true, force: true }).catch(() => { });
|
|
372
|
-
}
|
|
373
|
-
await remoteCodexHomeAssetCleanup?.().catch(() => { });
|
|
374
|
-
};
|
|
375
|
-
if (input.targetIsRemote && !input.probeApiKey) {
|
|
376
|
-
// Ship the same sanitized home real runs use (prepareRemoteCodexHomeAsset):
|
|
377
|
-
// sanitized config.toml and NO auth.json. This keeps the probe faithful to
|
|
378
|
-
// runs and respects `requires_openai_auth = false` (provider credentials
|
|
379
|
-
// come from config.toml, not a copied ChatGPT auth.json).
|
|
380
|
-
const managedHome = await prepareManagedCodexHome(process.env, async () => { }, input.companyId, {
|
|
381
|
-
apiKey: null,
|
|
382
|
-
});
|
|
383
|
-
const remoteCodexHomeAsset = await prepareRemoteCodexHomeAsset(managedHome);
|
|
384
|
-
remoteCodexHomeAssetCleanup = remoteCodexHomeAsset.cleanup;
|
|
385
|
-
preparedRuntimeWorkspaceLocalDir = await fs.mkdtemp(path.join(os.tmpdir(), `paperclip-codex-envtest-${input.runId}-`));
|
|
386
|
-
preparedRuntime = await prepareAdapterExecutionTargetRuntime({
|
|
387
|
-
runId: input.runId,
|
|
388
|
-
target: input.target,
|
|
389
|
-
adapterKey: "codex",
|
|
390
|
-
workspaceLocalDir: preparedRuntimeWorkspaceLocalDir,
|
|
391
|
-
// Pass `input.cwd` as the base (not a pre-built per-run subdir).
|
|
392
|
-
// `prepareRemoteManagedRuntime` itself appends
|
|
393
|
-
// `.paperclip-runtime/runs/<runId>/workspace` to whatever it gets, so
|
|
394
|
-
// pre-building a per-run path here would double-nest the run ID.
|
|
395
|
-
workspaceRemoteDir: input.cwd,
|
|
396
|
-
installCommand: SANDBOX_INSTALL_COMMAND,
|
|
397
|
-
detectCommand: input.command,
|
|
398
|
-
assets: [
|
|
399
|
-
{
|
|
400
|
-
key: "home",
|
|
401
|
-
localDir: remoteCodexHomeAsset.dir,
|
|
402
|
-
followSymlinks: true,
|
|
403
|
-
},
|
|
404
|
-
],
|
|
405
|
-
});
|
|
406
|
-
return {
|
|
407
|
-
command: input.command,
|
|
408
|
-
args: input.args,
|
|
409
|
-
env: preparedRuntime.assetDirs.home
|
|
410
|
-
? { ...input.env, CODEX_HOME: preparedRuntime.assetDirs.home }
|
|
411
|
-
: { ...input.env },
|
|
412
|
-
cleanup,
|
|
413
|
-
};
|
|
414
|
-
}
|
|
415
|
-
if (input.probeApiKey) {
|
|
416
|
-
const probeHome = input.targetIsRemote
|
|
417
|
-
? path.posix.join(input.cwd, ".paperclip-runtime", "codex", `probe-home-${input.runId}`)
|
|
418
|
-
: path.join(os.tmpdir(), `paperclip-codex-probe-${input.runId}`);
|
|
419
|
-
return {
|
|
420
|
-
command: "sh",
|
|
421
|
-
args: [
|
|
422
|
-
"-c",
|
|
423
|
-
'set -e; mkdir -p "$CODEX_HOME"; umask 077; printf "%s" "$_PAPERCLIP_CODEX_AUTH_JSON" > "$CODEX_HOME/auth.json"; unset _PAPERCLIP_CODEX_AUTH_JSON; trap \'rm -rf "$CODEX_HOME"\' EXIT INT TERM; "$0" "$@"',
|
|
424
|
-
input.command,
|
|
425
|
-
...input.args,
|
|
426
|
-
],
|
|
427
|
-
env: {
|
|
428
|
-
...input.env,
|
|
429
|
-
CODEX_HOME: probeHome,
|
|
430
|
-
_PAPERCLIP_CODEX_AUTH_JSON: JSON.stringify({ OPENAI_API_KEY: input.probeApiKey }),
|
|
431
|
-
},
|
|
432
|
-
cleanup,
|
|
433
|
-
};
|
|
434
|
-
}
|
|
435
|
-
return {
|
|
436
|
-
command: input.command,
|
|
437
|
-
args: input.args,
|
|
438
|
-
env: { ...input.env },
|
|
439
|
-
cleanup,
|
|
440
|
-
};
|
|
441
|
-
}
|
|
442
|
-
export async function testEnvironment(ctx) {
|
|
443
|
-
const checks = [];
|
|
444
|
-
const config = parseObject(ctx.config);
|
|
445
|
-
const command = asString(config.command, "codex");
|
|
446
|
-
const target = ctx.executionTarget ?? null;
|
|
447
|
-
const targetIsRemote = target?.kind === "remote";
|
|
448
|
-
const targetIsSandbox = target?.kind === "remote" && target.transport === "sandbox";
|
|
449
|
-
const cwd = resolveAdapterExecutionTargetCwd(target, asString(config.cwd, ""), process.cwd());
|
|
450
|
-
const targetLabel = targetIsRemote
|
|
451
|
-
? ctx.environmentName ?? describeAdapterExecutionTarget(target)
|
|
452
|
-
: null;
|
|
453
|
-
const runId = `codex-envtest-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
454
|
-
if (targetLabel) {
|
|
455
|
-
checks.push({
|
|
456
|
-
code: "codex_environment_target",
|
|
457
|
-
level: "info",
|
|
458
|
-
message: `Probing inside environment: ${targetLabel}`,
|
|
459
|
-
});
|
|
460
|
-
}
|
|
461
|
-
try {
|
|
462
|
-
await ensureAdapterExecutionTargetDirectory(runId, target, cwd, {
|
|
463
|
-
cwd,
|
|
464
|
-
env: {},
|
|
465
|
-
createIfMissing: true,
|
|
466
|
-
});
|
|
467
|
-
checks.push({
|
|
468
|
-
code: "codex_cwd_valid",
|
|
469
|
-
level: "info",
|
|
470
|
-
message: `Working directory is valid: ${cwd}`,
|
|
471
|
-
});
|
|
472
|
-
}
|
|
473
|
-
catch (err) {
|
|
474
|
-
checks.push({
|
|
475
|
-
code: "codex_cwd_invalid",
|
|
476
|
-
level: "error",
|
|
477
|
-
message: err instanceof Error ? err.message : "Invalid working directory",
|
|
478
|
-
detail: cwd,
|
|
479
|
-
});
|
|
480
|
-
}
|
|
481
|
-
const envConfig = parseObject(config.env);
|
|
482
|
-
const env = {};
|
|
483
|
-
for (const [key, value] of Object.entries(envConfig)) {
|
|
484
|
-
if (typeof value === "string")
|
|
485
|
-
env[key] = value;
|
|
486
|
-
}
|
|
487
|
-
const runtimeEnv = ensurePathInEnv({ ...process.env, ...env });
|
|
488
|
-
// Remote sandboxes run a POSIX shell: drop host env keys that aren't valid
|
|
489
|
-
// shell identifiers (e.g. Windows `ProgramFiles(x86)`) before they reach it.
|
|
490
|
-
if (targetIsRemote) {
|
|
491
|
-
stripNonPosixSandboxEnvKeys(env);
|
|
492
|
-
stripNonPosixSandboxEnvKeys(runtimeEnv);
|
|
493
|
-
}
|
|
494
|
-
// Bring up Tailscale for the probe so the provider-connectivity diagnostics
|
|
495
|
-
// and hello probe can reach a private/tailnet model provider. Surfaced as a
|
|
496
|
-
// check (never thrown) so the probe still returns its other diagnostics.
|
|
497
|
-
if (targetIsSandbox && readTailscaleAuthKey(envConfig)) {
|
|
498
|
-
try {
|
|
499
|
-
await ensureSandboxTailscaleUp({
|
|
500
|
-
runId,
|
|
501
|
-
target,
|
|
502
|
-
envConfig,
|
|
503
|
-
cwd,
|
|
504
|
-
timeoutSec: 60,
|
|
505
|
-
onLog: async () => { },
|
|
506
|
-
});
|
|
507
|
-
applyTailscaleProxyEnv(env);
|
|
508
|
-
checks.push({
|
|
509
|
-
code: "codex_tailscale_up",
|
|
510
|
-
level: "info",
|
|
511
|
-
message: "Brought up Tailscale in the sandbox for this probe.",
|
|
512
|
-
});
|
|
513
|
-
}
|
|
514
|
-
catch (err) {
|
|
515
|
-
checks.push({
|
|
516
|
-
code: "codex_tailscale_up_failed",
|
|
517
|
-
level: "error",
|
|
518
|
-
message: "Could not bring up Tailscale in the sandbox.",
|
|
519
|
-
detail: (err instanceof Error ? err.message : String(err)).slice(0, 240),
|
|
520
|
-
hint: "Verify TAILSCALE_AUTHKEY in the environment env vars and that the sandbox image ships tailscale + the tailscale-up helper.",
|
|
521
|
-
});
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
const installCheck = await maybeRunSandboxInstallCommand({
|
|
525
|
-
runId,
|
|
526
|
-
target,
|
|
527
|
-
adapterKey: "codex",
|
|
528
|
-
installCommand: SANDBOX_INSTALL_COMMAND,
|
|
529
|
-
detectCommand: command,
|
|
530
|
-
env,
|
|
531
|
-
});
|
|
532
|
-
if (installCheck)
|
|
533
|
-
checks.push(installCheck);
|
|
534
|
-
try {
|
|
535
|
-
await ensureAdapterExecutionTargetCommandResolvable(command, target, cwd, runtimeEnv);
|
|
536
|
-
checks.push({
|
|
537
|
-
code: "codex_command_resolvable",
|
|
538
|
-
level: "info",
|
|
539
|
-
message: `Command is executable: ${command}`,
|
|
540
|
-
});
|
|
541
|
-
}
|
|
542
|
-
catch (err) {
|
|
543
|
-
checks.push({
|
|
544
|
-
code: "codex_command_unresolvable",
|
|
545
|
-
level: "error",
|
|
546
|
-
message: err instanceof Error ? err.message : "Command is not executable",
|
|
547
|
-
detail: command,
|
|
548
|
-
});
|
|
549
|
-
}
|
|
550
|
-
const configOpenAiKey = env.OPENAI_API_KEY;
|
|
551
|
-
const hostOpenAiKey = targetIsRemote ? undefined : process.env.OPENAI_API_KEY;
|
|
552
|
-
if (isNonEmpty(configOpenAiKey) || isNonEmpty(hostOpenAiKey)) {
|
|
553
|
-
const source = isNonEmpty(configOpenAiKey) ? "adapter config env" : "server environment";
|
|
554
|
-
checks.push({
|
|
555
|
-
code: "codex_openai_api_key_present",
|
|
556
|
-
level: "info",
|
|
557
|
-
message: "OPENAI_API_KEY is set for Codex authentication.",
|
|
558
|
-
detail: `Detected in ${source}.`,
|
|
559
|
-
});
|
|
560
|
-
}
|
|
561
|
-
else if (!targetIsRemote) {
|
|
562
|
-
// Local-only auth file check. On remote targets, the probe will surface
|
|
563
|
-
// any missing-auth errors directly from the remote `codex` invocation.
|
|
564
|
-
const codexHome = isNonEmpty(env.CODEX_HOME) ? env.CODEX_HOME : undefined;
|
|
565
|
-
const codexAuth = await readCodexAuthInfo(codexHome).catch(() => null);
|
|
566
|
-
if (codexAuth) {
|
|
567
|
-
checks.push({
|
|
568
|
-
code: "codex_native_auth_present",
|
|
569
|
-
level: "info",
|
|
570
|
-
message: "Codex is authenticated via its own auth configuration.",
|
|
571
|
-
detail: codexAuth.email ? `Logged in as ${codexAuth.email}.` : `Credentials found in ${path.join(codexHome ?? codexHomeDir(), "auth.json")}.`,
|
|
572
|
-
});
|
|
573
|
-
}
|
|
574
|
-
else {
|
|
575
|
-
checks.push({
|
|
576
|
-
code: "codex_openai_api_key_missing",
|
|
577
|
-
level: "warn",
|
|
578
|
-
message: "OPENAI_API_KEY is not set. Codex runs may fail until authentication is configured.",
|
|
579
|
-
hint: "Set OPENAI_API_KEY in adapter env, shell environment, or run `codex auth` to log in.",
|
|
580
|
-
});
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
const canRunProbe = checks.every((check) => check.code !== "codex_cwd_invalid" && check.code !== "codex_command_unresolvable");
|
|
584
|
-
if (canRunProbe) {
|
|
585
|
-
if (!commandLooksLike(command, "codex")) {
|
|
586
|
-
checks.push({
|
|
587
|
-
code: "codex_hello_probe_skipped_custom_command",
|
|
588
|
-
level: "info",
|
|
589
|
-
message: "Skipped hello probe because command is not `codex`.",
|
|
590
|
-
detail: command,
|
|
591
|
-
hint: "Use the `codex` CLI command to run the automatic login and installation probe.",
|
|
592
|
-
});
|
|
593
|
-
}
|
|
594
|
-
else {
|
|
595
|
-
const execArgs = buildCodexExecArgs({ ...config, fastMode: false }, { skipGitRepoCheck: targetIsSandbox });
|
|
596
|
-
const args = execArgs.args;
|
|
597
|
-
if (execArgs.fastModeIgnoredReason) {
|
|
598
|
-
checks.push({
|
|
599
|
-
code: "codex_fast_mode_unsupported_model",
|
|
600
|
-
level: "warn",
|
|
601
|
-
message: execArgs.fastModeIgnoredReason,
|
|
602
|
-
hint: "Switch the agent model to GPT-5.4 or enter a manual model ID to enable Codex Fast mode.",
|
|
603
|
-
});
|
|
604
|
-
}
|
|
605
|
-
if (targetIsSandbox) {
|
|
606
|
-
checks.push({
|
|
607
|
-
code: "codex_git_repo_check_skipped",
|
|
608
|
-
level: "info",
|
|
609
|
-
message: "Added --skip-git-repo-check for sandbox hello probes.",
|
|
610
|
-
hint: "Codex requires an explicit trust bypass in headless remote sandbox workspaces.",
|
|
611
|
-
});
|
|
612
|
-
}
|
|
613
|
-
// Codex CLI (>= 0.122) ignores the OPENAI_API_KEY env var and only reads
|
|
614
|
-
// credentials from $CODEX_HOME/auth.json. When we have a key available,
|
|
615
|
-
// wrap the probe with a shell that materializes a per-run auth.json so
|
|
616
|
-
// the CLI can authenticate. The key content is passed via env (not on
|
|
617
|
-
// the command line) to avoid leaking it into process listings.
|
|
618
|
-
const probeApiKey = isNonEmpty(configOpenAiKey)
|
|
619
|
-
? configOpenAiKey
|
|
620
|
-
: isNonEmpty(hostOpenAiKey)
|
|
621
|
-
? hostOpenAiKey
|
|
622
|
-
: null;
|
|
623
|
-
const preparedProbe = await prepareCodexHelloProbe({
|
|
624
|
-
runId,
|
|
625
|
-
companyId: ctx.companyId,
|
|
626
|
-
target,
|
|
627
|
-
targetIsRemote,
|
|
628
|
-
cwd,
|
|
629
|
-
command,
|
|
630
|
-
args,
|
|
631
|
-
env,
|
|
632
|
-
probeApiKey,
|
|
633
|
-
});
|
|
634
|
-
try {
|
|
635
|
-
await addRemoteCodexHomeDiagnostics({
|
|
636
|
-
checks,
|
|
637
|
-
runId,
|
|
638
|
-
target,
|
|
639
|
-
cwd,
|
|
640
|
-
env: preparedProbe.env,
|
|
641
|
-
});
|
|
642
|
-
const cliDiagnostics = await addRemoteCodexCliDiagnostics({
|
|
643
|
-
checks,
|
|
644
|
-
runId,
|
|
645
|
-
target,
|
|
646
|
-
cwd,
|
|
647
|
-
command: preparedProbe.command,
|
|
648
|
-
env: preparedProbe.env,
|
|
649
|
-
});
|
|
650
|
-
const providerConnectivity = await addRemoteCodexProviderDiagnostics({
|
|
651
|
-
checks,
|
|
652
|
-
runId,
|
|
653
|
-
target,
|
|
654
|
-
cwd,
|
|
655
|
-
env: preparedProbe.env,
|
|
656
|
-
});
|
|
657
|
-
const providerBaseUrl = providerConnectivity?.baseUrl?.trim() ?? "";
|
|
658
|
-
const providerTcp = providerConnectivity?.tcp?.trim() ?? "";
|
|
659
|
-
const providerHttp = providerConnectivity?.http?.trim() ?? "";
|
|
660
|
-
const providerReachable = providerTcp === "connect" || /^status:(?!000)/.test(providerHttp);
|
|
661
|
-
// When the provider does not require OpenAI auth (e.g. a proxy like
|
|
662
|
-
// cliproxyapi authenticated by its own bearer token), the copied
|
|
663
|
-
// ChatGPT subscription token is irrelevant — Codex authenticates to the
|
|
664
|
-
// provider with the configured bearer token. In that case we want to
|
|
665
|
-
// actually run the hello probe to prove the end-to-end path, rather than
|
|
666
|
-
// skipping it just because auth.json happens to be in ChatGPT mode.
|
|
667
|
-
const providerRequiresOpenaiAuth = providerConnectivity?.requiresOpenaiAuth ?? true;
|
|
668
|
-
if (targetIsSandbox
|
|
669
|
-
&& providerBaseUrl.length > 0
|
|
670
|
-
&& providerReachable
|
|
671
|
-
&& cliDiagnostics?.authMode === "chatgpt"
|
|
672
|
-
&& providerRequiresOpenaiAuth
|
|
673
|
-
&& !probeApiKey) {
|
|
674
|
-
checks.push({
|
|
675
|
-
code: "codex_hello_probe_skipped_chatgpt_remote",
|
|
676
|
-
level: "warn",
|
|
677
|
-
message: "Skipped Codex hello probe for ChatGPT-mode auth in a remote sandbox.",
|
|
678
|
-
detail: `base_url=${providerBaseUrl}; provider_tcp=${providerTcp || "unknown"}; provider_http=${providerHttp || "unknown"}`,
|
|
679
|
-
hint: "The sandbox, Tailscale, Codex CLI, copied Codex home, and model provider are reachable. ChatGPT subscription tokens can require an interactive refresh, so the full conversation probe is left to an actual run instead of blocking this test.",
|
|
680
|
-
});
|
|
681
|
-
return {
|
|
682
|
-
adapterType: ctx.adapterType,
|
|
683
|
-
status: summarizeStatus(checks),
|
|
684
|
-
checks,
|
|
685
|
-
testedAt: new Date().toISOString(),
|
|
686
|
-
};
|
|
687
|
-
}
|
|
688
|
-
if (targetIsSandbox && providerBaseUrl.length > 0 && !providerReachable) {
|
|
689
|
-
checks.push({
|
|
690
|
-
code: "codex_hello_probe_skipped_provider_unreachable",
|
|
691
|
-
level: "warn",
|
|
692
|
-
message: "Skipped Codex hello probe because the sandbox cannot reach the configured model provider.",
|
|
693
|
-
detail: `base_url=${providerBaseUrl}; provider_tcp=${providerTcp || "unknown"}; provider_http=${providerHttp || "unknown"}`,
|
|
694
|
-
hint: "Fix Tailscale/ACL/firewall access from the sandbox to the Codex model provider, then retry the adapter test.",
|
|
695
|
-
});
|
|
696
|
-
return {
|
|
697
|
-
adapterType: ctx.adapterType,
|
|
698
|
-
status: summarizeStatus(checks),
|
|
699
|
-
checks,
|
|
700
|
-
testedAt: new Date().toISOString(),
|
|
701
|
-
};
|
|
702
|
-
}
|
|
703
|
-
checks.push({
|
|
704
|
-
code: "codex_hello_probe_invocation",
|
|
705
|
-
level: "info",
|
|
706
|
-
message: "Running Codex hello probe.",
|
|
707
|
-
detail: [preparedProbe.command, ...preparedProbe.args].join(" "),
|
|
708
|
-
...(targetIsSandbox
|
|
709
|
-
? {
|
|
710
|
-
hint: `Remote sandbox hello probes are capped at ${SANDBOX_HELLO_PROBE_TIMEOUT_SEC}s so the environment test can return diagnostics instead of timing out.`,
|
|
711
|
-
}
|
|
712
|
-
: {}),
|
|
713
|
-
});
|
|
714
|
-
let probe;
|
|
715
|
-
try {
|
|
716
|
-
probe = await runAdapterExecutionTargetProcess(runId, target, preparedProbe.command, preparedProbe.args, {
|
|
717
|
-
cwd,
|
|
718
|
-
env: preparedProbe.env,
|
|
719
|
-
timeoutSec: targetIsSandbox ? SANDBOX_HELLO_PROBE_TIMEOUT_SEC : 45,
|
|
720
|
-
graceSec: 5,
|
|
721
|
-
stdin: "Respond with hello.",
|
|
722
|
-
onLog: async () => { },
|
|
723
|
-
});
|
|
724
|
-
}
|
|
725
|
-
catch (err) {
|
|
726
|
-
// The probe transport (e.g. the plugin RPC) can fail or time out
|
|
727
|
-
// independently of the Codex process — surface it as a warning rather
|
|
728
|
-
// than letting it bubble up as an opaque 500 from the environment test.
|
|
729
|
-
checks.push({
|
|
730
|
-
code: "codex_hello_probe_transport_error",
|
|
731
|
-
level: "warn",
|
|
732
|
-
message: targetIsSandbox
|
|
733
|
-
? "Could not complete the Codex hello probe in the remote sandbox."
|
|
734
|
-
: "Could not complete the Codex hello probe.",
|
|
735
|
-
detail: (err instanceof Error ? err.message : String(err)).slice(0, 240),
|
|
736
|
-
hint: targetIsSandbox
|
|
737
|
-
? "The cold sandbox can take 1-2 minutes for the first `codex exec`. The diagnostics above already confirm the sandbox, Tailscale, Codex CLI, copied config, and model provider; retry the test once the sandbox is warm, or start a real run to confirm."
|
|
738
|
-
: "Retry the probe. If this persists, run `codex exec --json -` manually to debug.",
|
|
739
|
-
});
|
|
740
|
-
return {
|
|
741
|
-
adapterType: ctx.adapterType,
|
|
742
|
-
status: summarizeStatus(checks),
|
|
743
|
-
checks,
|
|
744
|
-
testedAt: new Date().toISOString(),
|
|
745
|
-
};
|
|
746
|
-
}
|
|
747
|
-
const parsed = parseCodexJsonl(probe.stdout);
|
|
748
|
-
const detail = summarizeProbeDetail(probe.stdout, probe.stderr, parsed.errorMessage);
|
|
749
|
-
const authEvidence = `${parsed.errorMessage ?? ""}\n${probe.stdout}\n${probe.stderr}`.trim();
|
|
750
|
-
if (CODEX_AUTH_REQUIRED_RE.test(authEvidence)) {
|
|
751
|
-
checks.push({
|
|
752
|
-
code: "codex_hello_probe_auth_required",
|
|
753
|
-
level: "warn",
|
|
754
|
-
message: "Codex CLI is installed, but authentication is not ready.",
|
|
755
|
-
...(detail ? { detail } : {}),
|
|
756
|
-
hint: probeApiKey
|
|
757
|
-
? "OPENAI_API_KEY was provided but Codex still rejected the request. Verify the key is valid for the OpenAI Responses API (e.g. `curl -H \"Authorization: Bearer $OPENAI_API_KEY\" https://api.openai.com/v1/models`), or run `codex login` and seed `~/.codex/auth.json`."
|
|
758
|
-
: "Refresh Codex login on the Paperclip host, then retry. ChatGPT-mode Codex auth copied into a remote sandbox can stop working when the token is invalidated.",
|
|
759
|
-
});
|
|
760
|
-
}
|
|
761
|
-
else if (probe.timedOut) {
|
|
762
|
-
checks.push({
|
|
763
|
-
code: "codex_hello_probe_timed_out",
|
|
764
|
-
level: "warn",
|
|
765
|
-
message: "Codex hello probe timed out.",
|
|
766
|
-
...(detail ? { detail } : {}),
|
|
767
|
-
hint: targetIsSandbox
|
|
768
|
-
? "The sandbox reached the model provider, but Codex did not finish the hello probe quickly enough. If this follows a login refresh, retry once; otherwise run `codex login` on the Paperclip host and retry."
|
|
769
|
-
: "Retry the probe. If this persists, verify Codex can run `Respond with hello` from this directory manually.",
|
|
770
|
-
});
|
|
771
|
-
}
|
|
772
|
-
else if ((probe.exitCode ?? 1) === 0) {
|
|
773
|
-
const summary = parsed.summary.trim();
|
|
774
|
-
const hasHello = /\bhello\b/i.test(summary);
|
|
775
|
-
checks.push({
|
|
776
|
-
code: hasHello ? "codex_hello_probe_passed" : "codex_hello_probe_unexpected_output",
|
|
777
|
-
level: hasHello ? "info" : "warn",
|
|
778
|
-
message: hasHello
|
|
779
|
-
? "Codex hello probe succeeded."
|
|
780
|
-
: "Codex probe ran but did not return `hello` as expected.",
|
|
781
|
-
...(summary ? { detail: summary.replace(/\s+/g, " ").trim().slice(0, 240) } : {}),
|
|
782
|
-
...(hasHello
|
|
783
|
-
? {}
|
|
784
|
-
: {
|
|
785
|
-
hint: "Try the probe manually (`codex exec --json -` then prompt: Respond with hello) to inspect full output.",
|
|
786
|
-
}),
|
|
787
|
-
});
|
|
788
|
-
}
|
|
789
|
-
else {
|
|
790
|
-
checks.push({
|
|
791
|
-
code: "codex_hello_probe_failed",
|
|
792
|
-
level: "error",
|
|
793
|
-
message: "Codex hello probe failed.",
|
|
794
|
-
...(detail ? { detail } : {}),
|
|
795
|
-
hint: "Run `codex exec --json -` manually in this working directory and prompt `Respond with hello` to debug.",
|
|
796
|
-
});
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
finally {
|
|
800
|
-
await preparedProbe.cleanup();
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
return {
|
|
805
|
-
adapterType: ctx.adapterType,
|
|
806
|
-
status: summarizeStatus(checks),
|
|
807
|
-
checks,
|
|
808
|
-
testedAt: new Date().toISOString(),
|
|
809
|
-
};
|
|
810
|
-
}
|
|
811
|
-
//# sourceMappingURL=test.js.map
|