@right-link/paperclip-plugin-codex-remote 0.3.1
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/LICENSE +21 -0
- package/README.md +77 -0
- package/dist/cli/format-event.d.ts +2 -0
- package/dist/cli/format-event.d.ts.map +1 -0
- package/dist/cli/format-event.js +213 -0
- package/dist/cli/format-event.js.map +1 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/quota-probe.d.ts +3 -0
- package/dist/cli/quota-probe.d.ts.map +1 -0
- package/dist/cli/quota-probe.js +97 -0
- package/dist/cli/quota-probe.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +84 -0
- package/dist/index.js.map +1 -0
- package/dist/server/adapter.d.ts +16 -0
- package/dist/server/adapter.d.ts.map +1 -0
- package/dist/server/adapter.js +157 -0
- package/dist/server/adapter.js.map +1 -0
- package/dist/server/adapter.test.d.ts +2 -0
- package/dist/server/adapter.test.d.ts.map +1 -0
- package/dist/server/adapter.test.js +84 -0
- package/dist/server/adapter.test.js.map +1 -0
- package/dist/server/codex-args.d.ts +13 -0
- package/dist/server/codex-args.d.ts.map +1 -0
- package/dist/server/codex-args.js +60 -0
- package/dist/server/codex-args.js.map +1 -0
- package/dist/server/codex-args.test.d.ts +2 -0
- package/dist/server/codex-args.test.d.ts.map +1 -0
- package/dist/server/codex-args.test.js +94 -0
- package/dist/server/codex-args.test.js.map +1 -0
- package/dist/server/codex-home.d.ts +47 -0
- package/dist/server/codex-home.d.ts.map +1 -0
- package/dist/server/codex-home.js +378 -0
- package/dist/server/codex-home.js.map +1 -0
- package/dist/server/codex-home.test.d.ts +2 -0
- package/dist/server/codex-home.test.d.ts.map +1 -0
- package/dist/server/codex-home.test.js +244 -0
- package/dist/server/codex-home.test.js.map +1 -0
- package/dist/server/execute.d.ts +16 -0
- package/dist/server/execute.d.ts.map +1 -0
- package/dist/server/execute.js +906 -0
- package/dist/server/execute.js.map +1 -0
- package/dist/server/execute.remote.test.d.ts +2 -0
- package/dist/server/execute.remote.test.d.ts.map +1 -0
- package/dist/server/execute.remote.test.js +487 -0
- package/dist/server/execute.remote.test.js.map +1 -0
- package/dist/server/index.d.ts +8 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +57 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/parse.d.ts +22 -0
- package/dist/server/parse.d.ts.map +1 -0
- package/dist/server/parse.js +213 -0
- package/dist/server/parse.js.map +1 -0
- package/dist/server/parse.test.d.ts +2 -0
- package/dist/server/parse.test.d.ts.map +1 -0
- package/dist/server/parse.test.js +107 -0
- package/dist/server/parse.test.js.map +1 -0
- package/dist/server/quota-spawn-error.test.d.ts +2 -0
- package/dist/server/quota-spawn-error.test.d.ts.map +1 -0
- package/dist/server/quota-spawn-error.test.js +77 -0
- package/dist/server/quota-spawn-error.test.js.map +1 -0
- package/dist/server/quota.d.ts +64 -0
- package/dist/server/quota.d.ts.map +1 -0
- package/dist/server/quota.js +432 -0
- package/dist/server/quota.js.map +1 -0
- package/dist/server/sandbox-env.d.ts +4 -0
- package/dist/server/sandbox-env.d.ts.map +1 -0
- package/dist/server/sandbox-env.js +23 -0
- package/dist/server/sandbox-env.js.map +1 -0
- package/dist/server/skills.d.ts +8 -0
- package/dist/server/skills.d.ts.map +1 -0
- package/dist/server/skills.js +24 -0
- package/dist/server/skills.js.map +1 -0
- package/dist/server/tailscale.d.ts +24 -0
- package/dist/server/tailscale.d.ts.map +1 -0
- package/dist/server/tailscale.js +95 -0
- package/dist/server/tailscale.js.map +1 -0
- package/dist/server/test.d.ts +3 -0
- package/dist/server/test.d.ts.map +1 -0
- package/dist/server/test.js +811 -0
- package/dist/server/test.js.map +1 -0
- package/dist/server/test.remote.test.d.ts +2 -0
- package/dist/server/test.remote.test.d.ts.map +1 -0
- package/dist/server/test.remote.test.js +257 -0
- package/dist/server/test.remote.test.js.map +1 -0
- package/dist/ui/build-config.d.ts +3 -0
- package/dist/ui/build-config.d.ts.map +1 -0
- package/dist/ui/build-config.js +113 -0
- package/dist/ui/build-config.js.map +1 -0
- package/dist/ui/build-config.test.d.ts +2 -0
- package/dist/ui/build-config.test.d.ts.map +1 -0
- package/dist/ui/build-config.test.js +49 -0
- package/dist/ui/build-config.test.js.map +1 -0
- package/dist/ui/index.d.ts +3 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +3 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/parse-stdout.d.ts +3 -0
- package/dist/ui/parse-stdout.d.ts.map +1 -0
- package/dist/ui/parse-stdout.js +261 -0
- package/dist/ui/parse-stdout.js.map +1 -0
- package/dist/ui/parse-stdout.test.d.ts +2 -0
- package/dist/ui/parse-stdout.test.d.ts.map +1 -0
- package/dist/ui/parse-stdout.test.js +77 -0
- package/dist/ui/parse-stdout.test.js.map +1 -0
- package/dist/ui-parser.d.ts +2 -0
- package/dist/ui-parser.d.ts.map +1 -0
- package/dist/ui-parser.js +245 -0
- package/dist/ui-parser.js.map +1 -0
- package/package.json +69 -0
|
@@ -0,0 +1,906 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { inferOpenAiCompatibleBiller } from "@paperclipai/adapter-utils";
|
|
5
|
+
import { adapterExecutionTargetIsRemote, adapterExecutionTargetRemoteCwd, overrideAdapterExecutionTargetRemoteCwd, adapterExecutionTargetSessionIdentity, adapterExecutionTargetSessionMatches, adapterExecutionTargetUsesPaperclipBridge, describeAdapterExecutionTarget, ensureAdapterExecutionTargetCommandResolvable, ensureAdapterExecutionTargetRuntimeCommandInstalled, prepareAdapterExecutionTargetRuntime, readAdapterExecutionTarget, resolveAdapterExecutionTargetTimeoutSec, resolveAdapterExecutionTargetCommandForLogs, runAdapterExecutionTargetShellCommand, runAdapterExecutionTargetProcess, startAdapterExecutionTargetPaperclipBridge, } from "@paperclipai/adapter-utils/execution-target";
|
|
6
|
+
import { asString, asNumber, parseObject, buildPaperclipEnv, buildInvocationEnvForLogs, ensureAbsoluteDirectory, ensurePaperclipSkillSymlink, ensurePathInEnv, refreshPaperclipWorkspaceEnvForExecution, readPaperclipRuntimeSkillEntries, readPaperclipIssueWorkModeFromContext, renderTemplate, renderPaperclipWakePrompt, stringifyPaperclipWakePayload, DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE, joinPromptSections, } from "@paperclipai/adapter-utils/server-utils";
|
|
7
|
+
import { parseCodexJsonl, extractCodexRetryNotBefore, isCodexTransientUpstreamError, isCodexUnknownSessionError, } from "./parse.js";
|
|
8
|
+
import { pathExists, prepareManagedCodexHome, prepareRemoteCodexHomeAsset, resolveManagedCodexHomeDir, resolveSharedCodexHomeDir } from "./codex-home.js";
|
|
9
|
+
import { resolveCodexDesiredSkillNames } from "./skills.js";
|
|
10
|
+
import { buildCodexExecArgs } from "./codex-args.js";
|
|
11
|
+
import { applyTailscaleProxyEnv, ensureSandboxTailscaleUp, readTailscaleAuthKey } from "./tailscale.js";
|
|
12
|
+
import { stripNonPosixSandboxEnvKeys } from "./sandbox-env.js";
|
|
13
|
+
import { SANDBOX_INSTALL_COMMAND } from "../index.js";
|
|
14
|
+
const __moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const SANDBOX_GLOBAL_CODEX_HOME = "/root/.codex";
|
|
16
|
+
const CODEX_ROLLOUT_NOISE_RE = /^\d{4}-\d{2}-\d{2}T[^\s]+\s+ERROR\s+codex_core::rollout::list:\s+state db missing rollout path for thread\s+[a-z0-9-]+$/i;
|
|
17
|
+
function stripCodexRolloutNoise(text) {
|
|
18
|
+
const parts = text.split(/\r?\n/);
|
|
19
|
+
const kept = [];
|
|
20
|
+
for (const part of parts) {
|
|
21
|
+
const trimmed = part.trim();
|
|
22
|
+
if (!trimmed) {
|
|
23
|
+
kept.push(part);
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (CODEX_ROLLOUT_NOISE_RE.test(trimmed))
|
|
27
|
+
continue;
|
|
28
|
+
kept.push(part);
|
|
29
|
+
}
|
|
30
|
+
return kept.join("\n");
|
|
31
|
+
}
|
|
32
|
+
function firstNonEmptyLine(text) {
|
|
33
|
+
return (text
|
|
34
|
+
.split(/\r?\n/)
|
|
35
|
+
.map((line) => line.trim())
|
|
36
|
+
.find(Boolean) ?? "");
|
|
37
|
+
}
|
|
38
|
+
function hasNonEmptyEnvValue(env, key) {
|
|
39
|
+
const raw = env[key];
|
|
40
|
+
return typeof raw === "string" && raw.trim().length > 0;
|
|
41
|
+
}
|
|
42
|
+
async function runSandboxPaperclipPreflight(input) {
|
|
43
|
+
if (!input.target || input.target.kind !== "remote" || input.target.transport !== "sandbox") {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
const script = [
|
|
47
|
+
"set -eu",
|
|
48
|
+
"mkdir -p .paperclip-runtime/codex",
|
|
49
|
+
'if [ -n "${CODEX_HOME:-}" ]; then',
|
|
50
|
+
' test -f "$CODEX_HOME/config.toml"',
|
|
51
|
+
' test ! -f "$CODEX_HOME/auth.json"',
|
|
52
|
+
'fi',
|
|
53
|
+
"probe_file=.paperclip-runtime/codex/preflight-write-check",
|
|
54
|
+
"printf '%s\\n' paperclip-preflight > \"$probe_file\"",
|
|
55
|
+
"rm -f \"$probe_file\"",
|
|
56
|
+
"if [ -n \"${PAPERCLIP_API_URL:-}\" ] && [ -n \"${PAPERCLIP_API_KEY:-}\" ] && [ -n \"${PAPERCLIP_TASK_ID:-}\" ]; then",
|
|
57
|
+
" curl -fsS -H \"Authorization: Bearer $PAPERCLIP_API_KEY\" \"$PAPERCLIP_API_URL/api/issues/$PAPERCLIP_TASK_ID/heartbeat-context\" >/dev/null",
|
|
58
|
+
"fi",
|
|
59
|
+
].join("\n");
|
|
60
|
+
const result = await runAdapterExecutionTargetShellCommand(input.runId, input.target, script, {
|
|
61
|
+
cwd: input.cwd,
|
|
62
|
+
env: input.env,
|
|
63
|
+
timeoutSec: Math.min(Math.max(input.timeoutSec, 1), 30),
|
|
64
|
+
onLog: input.onLog,
|
|
65
|
+
});
|
|
66
|
+
if (!result.timedOut && result.exitCode === 0)
|
|
67
|
+
return null;
|
|
68
|
+
const detail = [result.stderr.trim(), result.stdout.trim()].filter(Boolean).join("\n");
|
|
69
|
+
const message = result.timedOut
|
|
70
|
+
? "Codex sandbox preflight timed out before starting Codex."
|
|
71
|
+
: `Codex sandbox preflight failed before starting Codex${detail ? `: ${detail}` : "."}`;
|
|
72
|
+
return {
|
|
73
|
+
exitCode: result.exitCode ?? 1,
|
|
74
|
+
signal: result.signal,
|
|
75
|
+
timedOut: result.timedOut,
|
|
76
|
+
errorMessage: message,
|
|
77
|
+
errorCode: "codex_sandbox_preflight_failed",
|
|
78
|
+
resultJson: {
|
|
79
|
+
phase: "codex_sandbox_preflight",
|
|
80
|
+
stdout: result.stdout,
|
|
81
|
+
stderr: result.stderr,
|
|
82
|
+
timedOut: result.timedOut,
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function resolveCodexBillingType(env) {
|
|
87
|
+
// Codex uses API-key auth when OPENAI_API_KEY is present; otherwise rely on local login/session auth.
|
|
88
|
+
return hasNonEmptyEnvValue(env, "OPENAI_API_KEY") ? "api" : "subscription";
|
|
89
|
+
}
|
|
90
|
+
async function installSandboxGlobalCodexHome(input) {
|
|
91
|
+
if (!input.target || input.target.kind !== "remote" || input.target.transport !== "sandbox" || !input.sourceRemoteHome) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
const script = [
|
|
95
|
+
"set -eu",
|
|
96
|
+
`src=${JSON.stringify(input.sourceRemoteHome)}`,
|
|
97
|
+
`dst=${JSON.stringify(SANDBOX_GLOBAL_CODEX_HOME)}`,
|
|
98
|
+
'test -d "$src"',
|
|
99
|
+
'rm -rf "$dst"',
|
|
100
|
+
'mkdir -p "$dst"',
|
|
101
|
+
'cp -a "$src"/. "$dst"/',
|
|
102
|
+
'rm -f "$dst/auth.json"',
|
|
103
|
+
'chmod 700 "$dst"',
|
|
104
|
+
'find "$dst" -type f -exec chmod 600 {} \\;',
|
|
105
|
+
'test -f "$dst/config.toml"',
|
|
106
|
+
'test ! -f "$dst/auth.json"',
|
|
107
|
+
].join("\n");
|
|
108
|
+
const result = await runAdapterExecutionTargetShellCommand(input.runId, input.target, script, {
|
|
109
|
+
cwd: "/",
|
|
110
|
+
env: {},
|
|
111
|
+
timeoutSec: Math.min(Math.max(input.timeoutSec, 1), 30),
|
|
112
|
+
onLog: input.onLog,
|
|
113
|
+
});
|
|
114
|
+
if (!result.timedOut && result.exitCode === 0) {
|
|
115
|
+
return SANDBOX_GLOBAL_CODEX_HOME;
|
|
116
|
+
}
|
|
117
|
+
const detail = [result.stderr.trim(), result.stdout.trim()].filter(Boolean).join("\n");
|
|
118
|
+
throw new Error(result.timedOut
|
|
119
|
+
? "Timed out while installing sandbox global Codex home."
|
|
120
|
+
: `Failed to install sandbox global Codex home${detail ? `: ${detail}` : "."}`);
|
|
121
|
+
}
|
|
122
|
+
function resolveCodexBiller(env, billingType) {
|
|
123
|
+
const openAiCompatibleBiller = inferOpenAiCompatibleBiller(env, "openai");
|
|
124
|
+
if (openAiCompatibleBiller === "openrouter")
|
|
125
|
+
return "openrouter";
|
|
126
|
+
return billingType === "subscription" ? "chatgpt" : openAiCompatibleBiller ?? "openai";
|
|
127
|
+
}
|
|
128
|
+
async function isLikelyPaperclipRepoRoot(candidate) {
|
|
129
|
+
const [hasWorkspace, hasPackageJson, hasServerDir, hasAdapterUtilsDir] = await Promise.all([
|
|
130
|
+
pathExists(path.join(candidate, "pnpm-workspace.yaml")),
|
|
131
|
+
pathExists(path.join(candidate, "package.json")),
|
|
132
|
+
pathExists(path.join(candidate, "server")),
|
|
133
|
+
pathExists(path.join(candidate, "packages", "adapter-utils")),
|
|
134
|
+
]);
|
|
135
|
+
return hasWorkspace && hasPackageJson && hasServerDir && hasAdapterUtilsDir;
|
|
136
|
+
}
|
|
137
|
+
async function isLikelyPaperclipRuntimeSkillPath(candidate, skillName, options = {}) {
|
|
138
|
+
if (path.basename(candidate) !== skillName)
|
|
139
|
+
return false;
|
|
140
|
+
const skillsRoot = path.dirname(candidate);
|
|
141
|
+
if (path.basename(skillsRoot) !== "skills")
|
|
142
|
+
return false;
|
|
143
|
+
if (options.requireSkillMarkdown !== false && !(await pathExists(path.join(candidate, "SKILL.md")))) {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
let cursor = path.dirname(skillsRoot);
|
|
147
|
+
for (let depth = 0; depth < 6; depth += 1) {
|
|
148
|
+
if (await isLikelyPaperclipRepoRoot(cursor))
|
|
149
|
+
return true;
|
|
150
|
+
const parent = path.dirname(cursor);
|
|
151
|
+
if (parent === cursor)
|
|
152
|
+
break;
|
|
153
|
+
cursor = parent;
|
|
154
|
+
}
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
async function pruneBrokenUnavailablePaperclipSkillSymlinks(skillsHome, allowedSkillNames, onLog) {
|
|
158
|
+
const allowed = new Set(Array.from(allowedSkillNames));
|
|
159
|
+
const entries = await fs.readdir(skillsHome, { withFileTypes: true }).catch(() => []);
|
|
160
|
+
for (const entry of entries) {
|
|
161
|
+
if (allowed.has(entry.name) || !entry.isSymbolicLink())
|
|
162
|
+
continue;
|
|
163
|
+
const target = path.join(skillsHome, entry.name);
|
|
164
|
+
const linkedPath = await fs.readlink(target).catch(() => null);
|
|
165
|
+
if (!linkedPath)
|
|
166
|
+
continue;
|
|
167
|
+
const resolvedLinkedPath = path.resolve(path.dirname(target), linkedPath);
|
|
168
|
+
if (await pathExists(resolvedLinkedPath))
|
|
169
|
+
continue;
|
|
170
|
+
if (!(await isLikelyPaperclipRuntimeSkillPath(resolvedLinkedPath, entry.name, {
|
|
171
|
+
requireSkillMarkdown: false,
|
|
172
|
+
}))) {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
await fs.unlink(target).catch(() => { });
|
|
176
|
+
await onLog("stdout", `[paperclip] Removed stale Codex skill "${entry.name}" from ${skillsHome}\n`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function resolveCodexSkillsDir(codexHome) {
|
|
180
|
+
return path.join(codexHome, "skills");
|
|
181
|
+
}
|
|
182
|
+
function isFilesystemPermissionError(error) {
|
|
183
|
+
if (!error || typeof error !== "object")
|
|
184
|
+
return false;
|
|
185
|
+
const code = error.code;
|
|
186
|
+
return code === "EPERM" || code === "EACCES";
|
|
187
|
+
}
|
|
188
|
+
async function copyCodexSkillFallback(source, target, options = {}) {
|
|
189
|
+
const existing = await fs.lstat(target).catch(() => null);
|
|
190
|
+
if (existing?.isSymbolicLink()) {
|
|
191
|
+
await fs.unlink(target);
|
|
192
|
+
}
|
|
193
|
+
else if (existing?.isDirectory() && options.repairExisting === true) {
|
|
194
|
+
await fs.rm(target, { recursive: true, force: true });
|
|
195
|
+
}
|
|
196
|
+
else if (existing) {
|
|
197
|
+
return "skipped";
|
|
198
|
+
}
|
|
199
|
+
await fs.cp(source, target, { recursive: true, dereference: true });
|
|
200
|
+
return existing ? "repaired" : "created";
|
|
201
|
+
}
|
|
202
|
+
function readCodexTransientFallbackMode(context) {
|
|
203
|
+
const value = asString(context.codexTransientFallbackMode, "").trim();
|
|
204
|
+
switch (value) {
|
|
205
|
+
case "same_session":
|
|
206
|
+
case "safer_invocation":
|
|
207
|
+
case "fresh_session":
|
|
208
|
+
case "fresh_session_safer_invocation":
|
|
209
|
+
return value;
|
|
210
|
+
default:
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function fallbackModeUsesSaferInvocation(mode) {
|
|
215
|
+
return mode === "safer_invocation" || mode === "fresh_session_safer_invocation";
|
|
216
|
+
}
|
|
217
|
+
function fallbackModeUsesFreshSession(mode) {
|
|
218
|
+
return mode === "fresh_session" || mode === "fresh_session_safer_invocation";
|
|
219
|
+
}
|
|
220
|
+
function buildCodexTransientHandoffNote(input) {
|
|
221
|
+
return [
|
|
222
|
+
"Paperclip session handoff:",
|
|
223
|
+
input.previousSessionId ? `- Previous session: ${input.previousSessionId}` : "",
|
|
224
|
+
"- Rotation reason: repeated Codex transient remote-compaction failures",
|
|
225
|
+
`- Fallback mode: ${input.fallbackMode}`,
|
|
226
|
+
input.continuationSummaryBody
|
|
227
|
+
? `- Issue continuation summary: ${input.continuationSummaryBody.slice(0, 1_500)}`
|
|
228
|
+
: "",
|
|
229
|
+
"Continue from the current task state. Rebuild only the minimum context you need.",
|
|
230
|
+
]
|
|
231
|
+
.filter(Boolean)
|
|
232
|
+
.join("\n");
|
|
233
|
+
}
|
|
234
|
+
export async function ensureCodexSkillsInjected(onLog, options = {}) {
|
|
235
|
+
const allSkillsEntries = options.skillsEntries ?? await readPaperclipRuntimeSkillEntries({}, __moduleDir);
|
|
236
|
+
const desiredSkillNames = options.desiredSkillNames ?? allSkillsEntries.map((entry) => entry.key);
|
|
237
|
+
const desiredSet = new Set(desiredSkillNames);
|
|
238
|
+
const skillsEntries = allSkillsEntries.filter((entry) => desiredSet.has(entry.key));
|
|
239
|
+
if (skillsEntries.length === 0)
|
|
240
|
+
return;
|
|
241
|
+
const skillsHome = options.skillsHome ?? resolveCodexSkillsDir(resolveSharedCodexHomeDir());
|
|
242
|
+
await fs.mkdir(skillsHome, { recursive: true });
|
|
243
|
+
const linkSkill = options.linkSkill;
|
|
244
|
+
const injectionMode = options.injectionMode ?? "symlink";
|
|
245
|
+
for (const entry of skillsEntries) {
|
|
246
|
+
const target = path.join(skillsHome, entry.runtimeName);
|
|
247
|
+
try {
|
|
248
|
+
if (injectionMode === "copy") {
|
|
249
|
+
const result = await copyCodexSkillFallback(entry.source, target, { repairExisting: true });
|
|
250
|
+
if (result === "skipped")
|
|
251
|
+
continue;
|
|
252
|
+
await onLog("stdout", `[paperclip] ${result === "repaired" ? "Repaired" : "Copied"} Codex skill "${entry.runtimeName}" into ${skillsHome}\n`);
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
const existing = await fs.lstat(target).catch(() => null);
|
|
256
|
+
if (existing?.isSymbolicLink()) {
|
|
257
|
+
const linkedPath = await fs.readlink(target).catch(() => null);
|
|
258
|
+
const resolvedLinkedPath = linkedPath
|
|
259
|
+
? path.resolve(path.dirname(target), linkedPath)
|
|
260
|
+
: null;
|
|
261
|
+
if (resolvedLinkedPath &&
|
|
262
|
+
resolvedLinkedPath !== entry.source &&
|
|
263
|
+
(await isLikelyPaperclipRuntimeSkillPath(resolvedLinkedPath, entry.runtimeName))) {
|
|
264
|
+
await fs.unlink(target);
|
|
265
|
+
if (linkSkill) {
|
|
266
|
+
await linkSkill(entry.source, target);
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
await fs.symlink(entry.source, target);
|
|
270
|
+
}
|
|
271
|
+
await onLog("stdout", `[paperclip] Repaired Codex skill "${entry.runtimeName}" into ${skillsHome}\n`);
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
const result = await ensurePaperclipSkillSymlink(entry.source, target, linkSkill);
|
|
276
|
+
if (result === "skipped")
|
|
277
|
+
continue;
|
|
278
|
+
await onLog("stdout", `[paperclip] ${result === "repaired" ? "Repaired" : "Injected"} Codex skill "${entry.runtimeName}" into ${skillsHome}\n`);
|
|
279
|
+
}
|
|
280
|
+
catch (err) {
|
|
281
|
+
if (!linkSkill && isFilesystemPermissionError(err)) {
|
|
282
|
+
try {
|
|
283
|
+
const result = await copyCodexSkillFallback(entry.source, target);
|
|
284
|
+
if (result !== "skipped") {
|
|
285
|
+
await onLog("stdout", `[paperclip] ${result === "repaired" ? "Repaired" : "Copied"} Codex skill "${entry.runtimeName}" into ${skillsHome} because symlinks are unavailable\n`);
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
// Fall through to the original diagnostic below.
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
await onLog("stderr", `[paperclip] Failed to inject Codex skill "${entry.key}" into ${skillsHome}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
await pruneBrokenUnavailablePaperclipSkillSymlinks(skillsHome, skillsEntries.map((entry) => entry.runtimeName), onLog);
|
|
297
|
+
}
|
|
298
|
+
export async function execute(ctx) {
|
|
299
|
+
const { runId, agent, runtime, config, context, onLog, onMeta, onSpawn, authToken } = ctx;
|
|
300
|
+
const promptTemplate = asString(config.promptTemplate, DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE);
|
|
301
|
+
const command = asString(config.command, "codex");
|
|
302
|
+
const model = asString(config.model, "");
|
|
303
|
+
const workspaceContext = parseObject(context.paperclipWorkspace);
|
|
304
|
+
const workspaceCwd = asString(workspaceContext.cwd, "");
|
|
305
|
+
const workspaceSource = asString(workspaceContext.source, "");
|
|
306
|
+
const workspaceStrategy = asString(workspaceContext.strategy, "");
|
|
307
|
+
const workspaceId = asString(workspaceContext.workspaceId, "");
|
|
308
|
+
const workspaceRepoUrl = asString(workspaceContext.repoUrl, "");
|
|
309
|
+
const workspaceRepoRef = asString(workspaceContext.repoRef, "");
|
|
310
|
+
const workspaceBranch = asString(workspaceContext.branchName, "");
|
|
311
|
+
const workspaceWorktreePath = asString(workspaceContext.worktreePath, "");
|
|
312
|
+
const agentHome = asString(workspaceContext.agentHome, "");
|
|
313
|
+
const workspaceHints = Array.isArray(context.paperclipWorkspaces)
|
|
314
|
+
? context.paperclipWorkspaces.filter((value) => typeof value === "object" && value !== null)
|
|
315
|
+
: [];
|
|
316
|
+
const runtimeServiceIntents = Array.isArray(context.paperclipRuntimeServiceIntents)
|
|
317
|
+
? context.paperclipRuntimeServiceIntents.filter((value) => typeof value === "object" && value !== null)
|
|
318
|
+
: [];
|
|
319
|
+
const runtimeServices = Array.isArray(context.paperclipRuntimeServices)
|
|
320
|
+
? context.paperclipRuntimeServices.filter((value) => typeof value === "object" && value !== null)
|
|
321
|
+
: [];
|
|
322
|
+
const runtimePrimaryUrl = asString(context.paperclipRuntimePrimaryUrl, "");
|
|
323
|
+
const configuredCwd = asString(config.cwd, "");
|
|
324
|
+
const useConfiguredInsteadOfAgentHome = workspaceSource === "agent_home" && configuredCwd.length > 0;
|
|
325
|
+
const effectiveWorkspaceCwd = useConfiguredInsteadOfAgentHome ? "" : workspaceCwd;
|
|
326
|
+
const cwd = effectiveWorkspaceCwd || configuredCwd || process.cwd();
|
|
327
|
+
const envConfig = parseObject(config.env);
|
|
328
|
+
const executionTarget = readAdapterExecutionTarget({
|
|
329
|
+
executionTarget: ctx.executionTarget,
|
|
330
|
+
legacyRemoteExecution: ctx.executionTransport?.remoteExecution,
|
|
331
|
+
});
|
|
332
|
+
const executionTargetIsRemote = adapterExecutionTargetIsRemote(executionTarget);
|
|
333
|
+
const executionTargetIsSandbox = executionTarget?.kind === "remote" && executionTarget.transport === "sandbox";
|
|
334
|
+
const remoteTimingStartedAt = Date.now();
|
|
335
|
+
const logRemoteTiming = async (message) => {
|
|
336
|
+
if (!executionTargetIsRemote)
|
|
337
|
+
return;
|
|
338
|
+
await onLog("stdout", `[paperclip] codex_remote timing +${Date.now() - remoteTimingStartedAt}ms: ${message}\n`);
|
|
339
|
+
};
|
|
340
|
+
await logRemoteTiming(`execution target resolved (${describeAdapterExecutionTarget(executionTarget)})`);
|
|
341
|
+
const remoteGitSandboxConfig = parseObject(config.remoteGitSandbox);
|
|
342
|
+
const skipRemoteWorkspaceSync = executionTargetIsRemote &&
|
|
343
|
+
(config.remoteWorkspaceSync === false || remoteGitSandboxConfig.enabled === true);
|
|
344
|
+
const configuredCodexHome = typeof envConfig.CODEX_HOME === "string" && envConfig.CODEX_HOME.trim().length > 0
|
|
345
|
+
? path.resolve(envConfig.CODEX_HOME.trim())
|
|
346
|
+
: null;
|
|
347
|
+
const codexSkillEntries = await readPaperclipRuntimeSkillEntries(config, __moduleDir);
|
|
348
|
+
const desiredSkillNames = resolveCodexDesiredSkillNames(config, codexSkillEntries);
|
|
349
|
+
await ensureAbsoluteDirectory(cwd, { createIfMissing: true });
|
|
350
|
+
const configuredOpenAiApiKey = typeof envConfig.OPENAI_API_KEY === "string" && envConfig.OPENAI_API_KEY.trim().length > 0
|
|
351
|
+
? envConfig.OPENAI_API_KEY.trim()
|
|
352
|
+
: null;
|
|
353
|
+
const preparedManagedCodexHome = configuredCodexHome
|
|
354
|
+
? null
|
|
355
|
+
: await prepareManagedCodexHome(process.env, onLog, agent.companyId, {
|
|
356
|
+
apiKey: configuredOpenAiApiKey,
|
|
357
|
+
});
|
|
358
|
+
const defaultCodexHome = resolveManagedCodexHomeDir(process.env, agent.companyId);
|
|
359
|
+
const effectiveCodexHome = configuredCodexHome ?? preparedManagedCodexHome ?? defaultCodexHome;
|
|
360
|
+
await fs.mkdir(effectiveCodexHome, { recursive: true });
|
|
361
|
+
// Inject skills into the same CODEX_HOME that Codex will actually run with
|
|
362
|
+
// (managed home in the default case, or an explicit override from adapter config).
|
|
363
|
+
const codexSkillsDir = resolveCodexSkillsDir(effectiveCodexHome);
|
|
364
|
+
await ensureCodexSkillsInjected(onLog, {
|
|
365
|
+
skillsHome: codexSkillsDir,
|
|
366
|
+
skillsEntries: codexSkillEntries,
|
|
367
|
+
desiredSkillNames,
|
|
368
|
+
injectionMode: executionTargetIsSandbox ? "copy" : "symlink",
|
|
369
|
+
});
|
|
370
|
+
const timeoutSec = resolveAdapterExecutionTargetTimeoutSec(executionTarget, asNumber(config.timeoutSec, 0));
|
|
371
|
+
const graceSec = asNumber(config.graceSec, 20);
|
|
372
|
+
let effectiveExecutionCwd = adapterExecutionTargetRemoteCwd(executionTarget, cwd);
|
|
373
|
+
// For remote runs, sync a sandbox-sanitized copy of the Codex home so the
|
|
374
|
+
// sandbox does not inherit host-only config.toml sections (notably
|
|
375
|
+
// `mcp_servers`, which point at host binaries and stall Codex for each
|
|
376
|
+
// server's startup_timeout_sec on every run).
|
|
377
|
+
const remoteCodexHomeAsset = executionTargetIsRemote
|
|
378
|
+
? await prepareRemoteCodexHomeAsset(effectiveCodexHome)
|
|
379
|
+
: null;
|
|
380
|
+
await logRemoteTiming(remoteCodexHomeAsset
|
|
381
|
+
? "prepared sanitized remote CODEX_HOME asset without auth.json"
|
|
382
|
+
: "using local CODEX_HOME");
|
|
383
|
+
const remoteCodexHomeAssetDir = remoteCodexHomeAsset?.dir ?? effectiveCodexHome;
|
|
384
|
+
const preparedExecutionTargetRuntime = executionTargetIsRemote
|
|
385
|
+
? await (async () => {
|
|
386
|
+
await onLog("stdout", skipRemoteWorkspaceSync
|
|
387
|
+
? `[paperclip] Syncing CODEX_HOME to ${describeAdapterExecutionTarget(executionTarget)}; using remote workspace at ${effectiveExecutionCwd} (skipping workspace archive sync).\n`
|
|
388
|
+
: `[paperclip] Syncing workspace and CODEX_HOME to ${describeAdapterExecutionTarget(executionTarget)}.\n`);
|
|
389
|
+
const prepared = await prepareAdapterExecutionTargetRuntime({
|
|
390
|
+
runId,
|
|
391
|
+
target: executionTarget,
|
|
392
|
+
adapterKey: "codex",
|
|
393
|
+
timeoutSec,
|
|
394
|
+
workspaceLocalDir: cwd,
|
|
395
|
+
// For runtime-owned remote workspaces, anchor the runtime (and thus
|
|
396
|
+
// the synced CODEX_HOME asset) at the target cwd without uploading
|
|
397
|
+
// or overwriting a host workspace archive.
|
|
398
|
+
...(skipRemoteWorkspaceSync
|
|
399
|
+
? { workspaceRemoteDir: effectiveExecutionCwd, syncWorkspace: false }
|
|
400
|
+
: {}),
|
|
401
|
+
installCommand: SANDBOX_INSTALL_COMMAND,
|
|
402
|
+
detectCommand: command,
|
|
403
|
+
assets: [
|
|
404
|
+
{
|
|
405
|
+
key: "home",
|
|
406
|
+
localDir: remoteCodexHomeAssetDir,
|
|
407
|
+
followSymlinks: true,
|
|
408
|
+
},
|
|
409
|
+
],
|
|
410
|
+
});
|
|
411
|
+
await logRemoteTiming(`synced remote runtime assets to ${prepared.workspaceRemoteDir}`);
|
|
412
|
+
return prepared;
|
|
413
|
+
})().catch(async (error) => {
|
|
414
|
+
await remoteCodexHomeAsset?.cleanup();
|
|
415
|
+
throw error;
|
|
416
|
+
})
|
|
417
|
+
: null;
|
|
418
|
+
if (preparedExecutionTargetRuntime?.workspaceRemoteDir) {
|
|
419
|
+
effectiveExecutionCwd = preparedExecutionTargetRuntime.workspaceRemoteDir;
|
|
420
|
+
}
|
|
421
|
+
const runtimeExecutionTarget = overrideAdapterExecutionTargetRemoteCwd(executionTarget, effectiveExecutionCwd);
|
|
422
|
+
// Only pull workspace changes back to the host when we also uploaded the
|
|
423
|
+
// workspace. With skipRemoteWorkspaceSync the sandbox owns the workspace
|
|
424
|
+
// (runtime-owned), so there is nothing to restore — the agent persists its
|
|
425
|
+
// own changes from inside the sandbox (git push / MCP / API). This also keeps
|
|
426
|
+
// codex_remote from doing a host-side download of the run's changes.
|
|
427
|
+
const restoreRemoteWorkspace = preparedExecutionTargetRuntime && !skipRemoteWorkspaceSync
|
|
428
|
+
? () => preparedExecutionTargetRuntime.restoreWorkspace()
|
|
429
|
+
: null;
|
|
430
|
+
let paperclipBridge = null;
|
|
431
|
+
const remoteCodexHome = executionTargetIsRemote
|
|
432
|
+
? preparedExecutionTargetRuntime?.assetDirs.home ??
|
|
433
|
+
path.posix.join(effectiveExecutionCwd, ".paperclip-runtime", "codex", "home")
|
|
434
|
+
: null;
|
|
435
|
+
const sandboxGlobalCodexHome = await installSandboxGlobalCodexHome({
|
|
436
|
+
runId,
|
|
437
|
+
target: runtimeExecutionTarget,
|
|
438
|
+
sourceRemoteHome: remoteCodexHome,
|
|
439
|
+
timeoutSec,
|
|
440
|
+
onLog,
|
|
441
|
+
});
|
|
442
|
+
await logRemoteTiming(sandboxGlobalCodexHome
|
|
443
|
+
? `installed sandbox global Codex home at ${sandboxGlobalCodexHome}`
|
|
444
|
+
: "using synced remote CODEX_HOME");
|
|
445
|
+
const hasExplicitApiKey = typeof envConfig.PAPERCLIP_API_KEY === "string" && envConfig.PAPERCLIP_API_KEY.trim().length > 0;
|
|
446
|
+
const env = { ...buildPaperclipEnv(agent) };
|
|
447
|
+
env.PAPERCLIP_RUN_ID = runId;
|
|
448
|
+
const wakeTaskId = (typeof context.taskId === "string" && context.taskId.trim().length > 0 && context.taskId.trim()) ||
|
|
449
|
+
(typeof context.issueId === "string" && context.issueId.trim().length > 0 && context.issueId.trim()) ||
|
|
450
|
+
null;
|
|
451
|
+
const wakeReason = typeof context.wakeReason === "string" && context.wakeReason.trim().length > 0
|
|
452
|
+
? context.wakeReason.trim()
|
|
453
|
+
: null;
|
|
454
|
+
const wakeCommentId = (typeof context.wakeCommentId === "string" && context.wakeCommentId.trim().length > 0 && context.wakeCommentId.trim()) ||
|
|
455
|
+
(typeof context.commentId === "string" && context.commentId.trim().length > 0 && context.commentId.trim()) ||
|
|
456
|
+
null;
|
|
457
|
+
const approvalId = typeof context.approvalId === "string" && context.approvalId.trim().length > 0
|
|
458
|
+
? context.approvalId.trim()
|
|
459
|
+
: null;
|
|
460
|
+
const approvalStatus = typeof context.approvalStatus === "string" && context.approvalStatus.trim().length > 0
|
|
461
|
+
? context.approvalStatus.trim()
|
|
462
|
+
: null;
|
|
463
|
+
const linkedIssueIds = Array.isArray(context.issueIds)
|
|
464
|
+
? context.issueIds.filter((value) => typeof value === "string" && value.trim().length > 0)
|
|
465
|
+
: [];
|
|
466
|
+
const wakePayloadJson = stringifyPaperclipWakePayload(context.paperclipWake);
|
|
467
|
+
const issueWorkMode = readPaperclipIssueWorkModeFromContext(context);
|
|
468
|
+
if (wakeTaskId) {
|
|
469
|
+
env.PAPERCLIP_TASK_ID = wakeTaskId;
|
|
470
|
+
}
|
|
471
|
+
if (issueWorkMode) {
|
|
472
|
+
env.PAPERCLIP_ISSUE_WORK_MODE = issueWorkMode;
|
|
473
|
+
}
|
|
474
|
+
if (wakeReason) {
|
|
475
|
+
env.PAPERCLIP_WAKE_REASON = wakeReason;
|
|
476
|
+
}
|
|
477
|
+
if (wakeCommentId) {
|
|
478
|
+
env.PAPERCLIP_WAKE_COMMENT_ID = wakeCommentId;
|
|
479
|
+
}
|
|
480
|
+
if (approvalId) {
|
|
481
|
+
env.PAPERCLIP_APPROVAL_ID = approvalId;
|
|
482
|
+
}
|
|
483
|
+
if (approvalStatus) {
|
|
484
|
+
env.PAPERCLIP_APPROVAL_STATUS = approvalStatus;
|
|
485
|
+
}
|
|
486
|
+
if (linkedIssueIds.length > 0) {
|
|
487
|
+
env.PAPERCLIP_LINKED_ISSUE_IDS = linkedIssueIds.join(",");
|
|
488
|
+
}
|
|
489
|
+
if (wakePayloadJson) {
|
|
490
|
+
env.PAPERCLIP_WAKE_PAYLOAD_JSON = wakePayloadJson;
|
|
491
|
+
}
|
|
492
|
+
refreshPaperclipWorkspaceEnvForExecution({
|
|
493
|
+
env,
|
|
494
|
+
envConfig,
|
|
495
|
+
workspaceCwd: effectiveWorkspaceCwd,
|
|
496
|
+
workspaceSource,
|
|
497
|
+
workspaceStrategy,
|
|
498
|
+
workspaceId,
|
|
499
|
+
workspaceRepoUrl,
|
|
500
|
+
workspaceRepoRef,
|
|
501
|
+
workspaceBranch,
|
|
502
|
+
workspaceWorktreePath,
|
|
503
|
+
workspaceHints,
|
|
504
|
+
agentHome,
|
|
505
|
+
executionTargetIsRemote,
|
|
506
|
+
executionCwd: effectiveExecutionCwd,
|
|
507
|
+
});
|
|
508
|
+
if (runtimeServiceIntents.length > 0) {
|
|
509
|
+
env.PAPERCLIP_RUNTIME_SERVICE_INTENTS_JSON = JSON.stringify(runtimeServiceIntents);
|
|
510
|
+
}
|
|
511
|
+
if (runtimeServices.length > 0) {
|
|
512
|
+
env.PAPERCLIP_RUNTIME_SERVICES_JSON = JSON.stringify(runtimeServices);
|
|
513
|
+
}
|
|
514
|
+
if (runtimePrimaryUrl) {
|
|
515
|
+
env.PAPERCLIP_RUNTIME_PRIMARY_URL = runtimePrimaryUrl;
|
|
516
|
+
}
|
|
517
|
+
// Point Codex at the synced remote home (which carries config.toml — including
|
|
518
|
+
// the model_provider/base_url and any bearer token — plus auth.json) so a
|
|
519
|
+
// provider-realized remote sandbox uses the configured provider instead of
|
|
520
|
+
// falling back to the default api.openai.com endpoint. Sandbox runs use the
|
|
521
|
+
// user-level /root/.codex home because Codex treats that as the trusted global
|
|
522
|
+
// provider config; SSH remotes keep the synced runtime home.
|
|
523
|
+
env.CODEX_HOME = sandboxGlobalCodexHome ?? remoteCodexHome ?? effectiveCodexHome;
|
|
524
|
+
if (sandboxGlobalCodexHome) {
|
|
525
|
+
env.HOME = path.posix.dirname(sandboxGlobalCodexHome);
|
|
526
|
+
}
|
|
527
|
+
if (!hasExplicitApiKey && authToken) {
|
|
528
|
+
env.PAPERCLIP_API_KEY = authToken;
|
|
529
|
+
}
|
|
530
|
+
const shouldUsePaperclipBridge = executionTargetIsSandbox && adapterExecutionTargetUsesPaperclipBridge(runtimeExecutionTarget);
|
|
531
|
+
if (shouldUsePaperclipBridge) {
|
|
532
|
+
paperclipBridge = await startAdapterExecutionTargetPaperclipBridge({
|
|
533
|
+
runId,
|
|
534
|
+
target: runtimeExecutionTarget,
|
|
535
|
+
runtimeRootDir: preparedExecutionTargetRuntime?.runtimeRootDir,
|
|
536
|
+
adapterKey: "codex",
|
|
537
|
+
timeoutSec,
|
|
538
|
+
hostApiToken: env.PAPERCLIP_API_KEY,
|
|
539
|
+
onLog,
|
|
540
|
+
});
|
|
541
|
+
if (paperclipBridge) {
|
|
542
|
+
Object.assign(env, paperclipBridge.env);
|
|
543
|
+
}
|
|
544
|
+
await logRemoteTiming("sandbox callback bridge started");
|
|
545
|
+
}
|
|
546
|
+
const effectiveEnv = Object.fromEntries(Object.entries({ ...process.env, ...env }).filter((entry) => typeof entry[1] === "string"));
|
|
547
|
+
const billingType = resolveCodexBillingType(effectiveEnv);
|
|
548
|
+
const runtimeEnv = Object.fromEntries(Object.entries(ensurePathInEnv(effectiveEnv)).filter((entry) => typeof entry[1] === "string"));
|
|
549
|
+
// Remote sandboxes run a POSIX shell: drop host env keys that aren't valid
|
|
550
|
+
// shell identifiers (e.g. Windows `ProgramFiles(x86)`) before they reach the
|
|
551
|
+
// sandbox, which otherwise rejects the whole exec.
|
|
552
|
+
if (executionTargetIsRemote) {
|
|
553
|
+
stripNonPosixSandboxEnvKeys(env);
|
|
554
|
+
stripNonPosixSandboxEnvKeys(runtimeEnv);
|
|
555
|
+
}
|
|
556
|
+
// When the sandbox runs behind Tailscale (auth key supplied via the
|
|
557
|
+
// environment's env vars), route outbound traffic through the userspace proxy
|
|
558
|
+
// so Codex can reach a private/tailnet model provider. The tailnet itself is
|
|
559
|
+
// brought up below, before the preflight. localhost (the callback bridge)
|
|
560
|
+
// stays direct via NO_PROXY.
|
|
561
|
+
if (executionTargetIsSandbox && readTailscaleAuthKey(envConfig)) {
|
|
562
|
+
applyTailscaleProxyEnv(env);
|
|
563
|
+
applyTailscaleProxyEnv(runtimeEnv);
|
|
564
|
+
}
|
|
565
|
+
await ensureAdapterExecutionTargetRuntimeCommandInstalled({
|
|
566
|
+
runId,
|
|
567
|
+
target: executionTarget,
|
|
568
|
+
installCommand: ctx.runtimeCommandSpec?.installCommand,
|
|
569
|
+
detectCommand: ctx.runtimeCommandSpec?.detectCommand,
|
|
570
|
+
cwd,
|
|
571
|
+
env: runtimeEnv,
|
|
572
|
+
timeoutSec,
|
|
573
|
+
graceSec,
|
|
574
|
+
onLog,
|
|
575
|
+
});
|
|
576
|
+
await logRemoteTiming("runtime command install/probe completed");
|
|
577
|
+
await ensureAdapterExecutionTargetCommandResolvable(command, executionTarget, cwd, runtimeEnv);
|
|
578
|
+
const resolvedCommand = await resolveAdapterExecutionTargetCommandForLogs(command, executionTarget, cwd, runtimeEnv);
|
|
579
|
+
await logRemoteTiming(`resolved command ${resolvedCommand}`);
|
|
580
|
+
const loggedEnv = buildInvocationEnvForLogs(env, {
|
|
581
|
+
runtimeEnv,
|
|
582
|
+
includeRuntimeKeys: ["HOME"],
|
|
583
|
+
resolvedCommand,
|
|
584
|
+
});
|
|
585
|
+
const runtimeSessionParams = parseObject(runtime.sessionParams);
|
|
586
|
+
const runtimeSessionId = asString(runtimeSessionParams.sessionId, runtime.sessionId ?? "");
|
|
587
|
+
const runtimeSessionCwd = asString(runtimeSessionParams.cwd, "");
|
|
588
|
+
const runtimeRemoteExecution = parseObject(runtimeSessionParams.remoteExecution);
|
|
589
|
+
const canResumeSession = runtimeSessionId.length > 0 &&
|
|
590
|
+
(runtimeSessionCwd.length === 0 || path.resolve(runtimeSessionCwd) === path.resolve(effectiveExecutionCwd)) &&
|
|
591
|
+
adapterExecutionTargetSessionMatches(runtimeRemoteExecution, runtimeExecutionTarget);
|
|
592
|
+
const codexTransientFallbackMode = readCodexTransientFallbackMode(context);
|
|
593
|
+
const forceSaferInvocation = fallbackModeUsesSaferInvocation(codexTransientFallbackMode);
|
|
594
|
+
const forceFreshSession = fallbackModeUsesFreshSession(codexTransientFallbackMode);
|
|
595
|
+
const sessionId = canResumeSession && !forceFreshSession ? runtimeSessionId : null;
|
|
596
|
+
if (executionTargetIsRemote && runtimeSessionId && !canResumeSession) {
|
|
597
|
+
await onLog("stdout", `[paperclip] Codex session "${runtimeSessionId}" does not match the current remote execution identity and will not be resumed in "${effectiveExecutionCwd}". Starting a fresh remote session.\n`);
|
|
598
|
+
}
|
|
599
|
+
else if (runtimeSessionId && !canResumeSession) {
|
|
600
|
+
await onLog("stdout", `[paperclip] Codex session "${runtimeSessionId}" was saved for cwd "${runtimeSessionCwd}" and will not be resumed in "${effectiveExecutionCwd}".\n`);
|
|
601
|
+
}
|
|
602
|
+
const instructionsFilePath = asString(config.instructionsFilePath, "").trim();
|
|
603
|
+
const instructionsDir = instructionsFilePath ? `${path.dirname(instructionsFilePath)}/` : "";
|
|
604
|
+
let instructionsPrefix = "";
|
|
605
|
+
let instructionsChars = 0;
|
|
606
|
+
if (instructionsFilePath) {
|
|
607
|
+
try {
|
|
608
|
+
const instructionsContents = await fs.readFile(instructionsFilePath, "utf8");
|
|
609
|
+
instructionsPrefix =
|
|
610
|
+
`${instructionsContents}\n\n` +
|
|
611
|
+
`The above agent instructions were loaded from ${instructionsFilePath}. ` +
|
|
612
|
+
`Resolve any relative file references from ${instructionsDir}.\n\n`;
|
|
613
|
+
instructionsChars = instructionsPrefix.length;
|
|
614
|
+
}
|
|
615
|
+
catch (err) {
|
|
616
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
617
|
+
await onLog("stdout", `[paperclip] Warning: could not read agent instructions file "${instructionsFilePath}": ${reason}\n`);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
const repoAgentsNote = "Codex exec automatically applies repo-scoped AGENTS.md instructions from the current workspace; Paperclip does not currently suppress that discovery.";
|
|
621
|
+
const bootstrapPromptTemplate = asString(config.bootstrapPromptTemplate, "");
|
|
622
|
+
const templateData = {
|
|
623
|
+
agentId: agent.id,
|
|
624
|
+
companyId: agent.companyId,
|
|
625
|
+
runId,
|
|
626
|
+
company: { id: agent.companyId },
|
|
627
|
+
agent,
|
|
628
|
+
run: { id: runId, source: "on_demand" },
|
|
629
|
+
context,
|
|
630
|
+
};
|
|
631
|
+
const renderedBootstrapPrompt = !sessionId && bootstrapPromptTemplate.trim().length > 0
|
|
632
|
+
? renderTemplate(bootstrapPromptTemplate, templateData).trim()
|
|
633
|
+
: "";
|
|
634
|
+
const wakePrompt = renderPaperclipWakePrompt(context.paperclipWake, { resumedSession: Boolean(sessionId) });
|
|
635
|
+
const shouldUseResumeDeltaPrompt = Boolean(sessionId) && wakePrompt.length > 0;
|
|
636
|
+
const promptInstructionsPrefix = shouldUseResumeDeltaPrompt ? "" : instructionsPrefix;
|
|
637
|
+
instructionsChars = promptInstructionsPrefix.length;
|
|
638
|
+
const continuationSummary = parseObject(context.paperclipContinuationSummary);
|
|
639
|
+
const continuationSummaryBody = asString(continuationSummary.body, "").trim() || null;
|
|
640
|
+
const codexFallbackHandoffNote = forceFreshSession
|
|
641
|
+
? buildCodexTransientHandoffNote({
|
|
642
|
+
previousSessionId: runtimeSessionId || runtime.sessionId || null,
|
|
643
|
+
fallbackMode: codexTransientFallbackMode ?? "fresh_session",
|
|
644
|
+
continuationSummaryBody,
|
|
645
|
+
})
|
|
646
|
+
: "";
|
|
647
|
+
const commandNotes = (() => {
|
|
648
|
+
if (!instructionsFilePath) {
|
|
649
|
+
const notes = [repoAgentsNote];
|
|
650
|
+
if (forceSaferInvocation) {
|
|
651
|
+
notes.push("Codex transient fallback requested safer invocation settings for this retry.");
|
|
652
|
+
}
|
|
653
|
+
if (forceFreshSession) {
|
|
654
|
+
notes.push("Codex transient fallback forced a fresh session with a continuation handoff.");
|
|
655
|
+
}
|
|
656
|
+
return notes;
|
|
657
|
+
}
|
|
658
|
+
if (instructionsPrefix.length > 0) {
|
|
659
|
+
if (shouldUseResumeDeltaPrompt) {
|
|
660
|
+
const notes = [
|
|
661
|
+
`Loaded agent instructions from ${instructionsFilePath}`,
|
|
662
|
+
"Skipped stdin instruction reinjection because an existing Codex session is being resumed with a wake delta.",
|
|
663
|
+
repoAgentsNote,
|
|
664
|
+
];
|
|
665
|
+
if (forceSaferInvocation) {
|
|
666
|
+
notes.push("Codex transient fallback requested safer invocation settings for this retry.");
|
|
667
|
+
}
|
|
668
|
+
if (forceFreshSession) {
|
|
669
|
+
notes.push("Codex transient fallback forced a fresh session with a continuation handoff.");
|
|
670
|
+
}
|
|
671
|
+
return notes;
|
|
672
|
+
}
|
|
673
|
+
const notes = [
|
|
674
|
+
`Loaded agent instructions from ${instructionsFilePath}`,
|
|
675
|
+
`Prepended instructions + path directive to stdin prompt (relative references from ${instructionsDir}).`,
|
|
676
|
+
repoAgentsNote,
|
|
677
|
+
];
|
|
678
|
+
if (forceSaferInvocation) {
|
|
679
|
+
notes.push("Codex transient fallback requested safer invocation settings for this retry.");
|
|
680
|
+
}
|
|
681
|
+
if (forceFreshSession) {
|
|
682
|
+
notes.push("Codex transient fallback forced a fresh session with a continuation handoff.");
|
|
683
|
+
}
|
|
684
|
+
return notes;
|
|
685
|
+
}
|
|
686
|
+
const notes = [
|
|
687
|
+
`Configured instructionsFilePath ${instructionsFilePath}, but file could not be read; continuing without injected instructions.`,
|
|
688
|
+
repoAgentsNote,
|
|
689
|
+
];
|
|
690
|
+
if (forceSaferInvocation) {
|
|
691
|
+
notes.push("Codex transient fallback requested safer invocation settings for this retry.");
|
|
692
|
+
}
|
|
693
|
+
if (forceFreshSession) {
|
|
694
|
+
notes.push("Codex transient fallback forced a fresh session with a continuation handoff.");
|
|
695
|
+
}
|
|
696
|
+
return notes;
|
|
697
|
+
})();
|
|
698
|
+
if (executionTargetIsSandbox) {
|
|
699
|
+
commandNotes.push("Added --skip-git-repo-check for sandbox execution because Codex requires an explicit trust bypass in headless remote workspaces.");
|
|
700
|
+
}
|
|
701
|
+
const renderedPrompt = shouldUseResumeDeltaPrompt ? "" : renderTemplate(promptTemplate, templateData);
|
|
702
|
+
const sessionHandoffNote = asString(context.paperclipSessionHandoffMarkdown, "").trim();
|
|
703
|
+
const prompt = joinPromptSections([
|
|
704
|
+
promptInstructionsPrefix,
|
|
705
|
+
renderedBootstrapPrompt,
|
|
706
|
+
wakePrompt,
|
|
707
|
+
codexFallbackHandoffNote,
|
|
708
|
+
sessionHandoffNote,
|
|
709
|
+
renderedPrompt,
|
|
710
|
+
]);
|
|
711
|
+
const promptMetrics = {
|
|
712
|
+
promptChars: prompt.length,
|
|
713
|
+
instructionsChars,
|
|
714
|
+
bootstrapPromptChars: renderedBootstrapPrompt.length,
|
|
715
|
+
wakePromptChars: wakePrompt.length,
|
|
716
|
+
sessionHandoffChars: sessionHandoffNote.length,
|
|
717
|
+
heartbeatPromptChars: renderedPrompt.length,
|
|
718
|
+
};
|
|
719
|
+
const runAttempt = async (resumeSessionId) => {
|
|
720
|
+
await logRemoteTiming(resumeSessionId ? `starting Codex resume ${resumeSessionId}` : "starting fresh Codex exec");
|
|
721
|
+
const execArgs = buildCodexExecArgs(forceSaferInvocation ? { ...config, fastMode: false } : config, {
|
|
722
|
+
resumeSessionId,
|
|
723
|
+
skipGitRepoCheck: executionTargetIsSandbox,
|
|
724
|
+
isolatePaperclipTaskSystem: executionTargetIsSandbox,
|
|
725
|
+
});
|
|
726
|
+
const args = execArgs.args;
|
|
727
|
+
const commandNotesWithFastMode = execArgs.fastModeIgnoredReason == null
|
|
728
|
+
? commandNotes
|
|
729
|
+
: [...commandNotes, execArgs.fastModeIgnoredReason];
|
|
730
|
+
if (onMeta) {
|
|
731
|
+
await onMeta({
|
|
732
|
+
adapterType: "codex_remote",
|
|
733
|
+
command: resolvedCommand,
|
|
734
|
+
cwd: effectiveExecutionCwd,
|
|
735
|
+
commandNotes: commandNotesWithFastMode,
|
|
736
|
+
commandArgs: args.map((value, idx) => {
|
|
737
|
+
if (idx === args.length - 1 && value !== "-")
|
|
738
|
+
return `<prompt ${prompt.length} chars>`;
|
|
739
|
+
return value;
|
|
740
|
+
}),
|
|
741
|
+
env: loggedEnv,
|
|
742
|
+
prompt,
|
|
743
|
+
promptMetrics,
|
|
744
|
+
context,
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
let firstOutputLogged = false;
|
|
748
|
+
const proc = await runAdapterExecutionTargetProcess(runId, runtimeExecutionTarget, command, args, {
|
|
749
|
+
cwd,
|
|
750
|
+
env,
|
|
751
|
+
stdin: prompt,
|
|
752
|
+
timeoutSec,
|
|
753
|
+
graceSec,
|
|
754
|
+
onSpawn,
|
|
755
|
+
onLog: async (stream, chunk) => {
|
|
756
|
+
if (!firstOutputLogged && chunk.length > 0) {
|
|
757
|
+
firstOutputLogged = true;
|
|
758
|
+
await logRemoteTiming(`first Codex ${stream} chunk received (${Buffer.byteLength(chunk, "utf8")} bytes)`);
|
|
759
|
+
}
|
|
760
|
+
if (stream !== "stderr") {
|
|
761
|
+
await onLog(stream, chunk);
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
const cleaned = stripCodexRolloutNoise(chunk);
|
|
765
|
+
if (!cleaned.trim())
|
|
766
|
+
return;
|
|
767
|
+
await onLog(stream, cleaned);
|
|
768
|
+
},
|
|
769
|
+
});
|
|
770
|
+
await logRemoteTiming(`Codex process completed exit=${proc.exitCode ?? "null"} timedOut=${proc.timedOut}`);
|
|
771
|
+
const cleanedStderr = stripCodexRolloutNoise(proc.stderr);
|
|
772
|
+
return {
|
|
773
|
+
proc: {
|
|
774
|
+
...proc,
|
|
775
|
+
stderr: cleanedStderr,
|
|
776
|
+
},
|
|
777
|
+
rawStderr: proc.stderr,
|
|
778
|
+
parsed: parseCodexJsonl(proc.stdout),
|
|
779
|
+
};
|
|
780
|
+
};
|
|
781
|
+
const toResult = (attempt, clearSessionOnMissingSession = false, isRetry = false) => {
|
|
782
|
+
if (attempt.proc.timedOut) {
|
|
783
|
+
return {
|
|
784
|
+
exitCode: attempt.proc.exitCode,
|
|
785
|
+
signal: attempt.proc.signal,
|
|
786
|
+
timedOut: true,
|
|
787
|
+
errorMessage: `Timed out after ${timeoutSec}s`,
|
|
788
|
+
clearSession: clearSessionOnMissingSession,
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
const canFallbackToRuntimeSession = !isRetry && !forceFreshSession;
|
|
792
|
+
const resolvedSessionId = attempt.parsed.sessionId ??
|
|
793
|
+
(canFallbackToRuntimeSession ? (runtimeSessionId ?? runtime.sessionId ?? null) : null);
|
|
794
|
+
const resolvedSessionParams = resolvedSessionId
|
|
795
|
+
? {
|
|
796
|
+
sessionId: resolvedSessionId,
|
|
797
|
+
cwd: effectiveExecutionCwd,
|
|
798
|
+
...(executionTargetIsRemote
|
|
799
|
+
? {
|
|
800
|
+
remoteExecution: adapterExecutionTargetSessionIdentity(runtimeExecutionTarget),
|
|
801
|
+
}
|
|
802
|
+
: {}),
|
|
803
|
+
...(workspaceId ? { workspaceId } : {}),
|
|
804
|
+
...(workspaceRepoUrl ? { repoUrl: workspaceRepoUrl } : {}),
|
|
805
|
+
...(workspaceRepoRef ? { repoRef: workspaceRepoRef } : {}),
|
|
806
|
+
}
|
|
807
|
+
: null;
|
|
808
|
+
const parsedError = typeof attempt.parsed.errorMessage === "string" ? attempt.parsed.errorMessage.trim() : "";
|
|
809
|
+
const stderrLine = firstNonEmptyLine(attempt.proc.stderr);
|
|
810
|
+
const fallbackErrorMessage = parsedError ||
|
|
811
|
+
stderrLine ||
|
|
812
|
+
`Codex exited with code ${attempt.proc.exitCode ?? -1}`;
|
|
813
|
+
const transientRetryNotBefore = (attempt.proc.exitCode ?? 0) !== 0
|
|
814
|
+
? extractCodexRetryNotBefore({
|
|
815
|
+
stdout: attempt.proc.stdout,
|
|
816
|
+
stderr: attempt.proc.stderr,
|
|
817
|
+
errorMessage: fallbackErrorMessage,
|
|
818
|
+
})
|
|
819
|
+
: null;
|
|
820
|
+
const transientUpstream = (attempt.proc.exitCode ?? 0) !== 0 &&
|
|
821
|
+
isCodexTransientUpstreamError({
|
|
822
|
+
stdout: attempt.proc.stdout,
|
|
823
|
+
stderr: attempt.proc.stderr,
|
|
824
|
+
errorMessage: fallbackErrorMessage,
|
|
825
|
+
});
|
|
826
|
+
return {
|
|
827
|
+
exitCode: attempt.proc.exitCode,
|
|
828
|
+
signal: attempt.proc.signal,
|
|
829
|
+
timedOut: false,
|
|
830
|
+
errorMessage: (attempt.proc.exitCode ?? 0) === 0
|
|
831
|
+
? null
|
|
832
|
+
: fallbackErrorMessage,
|
|
833
|
+
errorCode: transientUpstream
|
|
834
|
+
? "codex_transient_upstream"
|
|
835
|
+
: null,
|
|
836
|
+
errorFamily: transientUpstream ? "transient_upstream" : null,
|
|
837
|
+
retryNotBefore: transientRetryNotBefore ? transientRetryNotBefore.toISOString() : null,
|
|
838
|
+
usage: attempt.parsed.usage,
|
|
839
|
+
sessionId: resolvedSessionId,
|
|
840
|
+
sessionParams: resolvedSessionParams,
|
|
841
|
+
sessionDisplayId: resolvedSessionId,
|
|
842
|
+
provider: "openai",
|
|
843
|
+
biller: resolveCodexBiller(effectiveEnv, billingType),
|
|
844
|
+
model,
|
|
845
|
+
billingType,
|
|
846
|
+
costUsd: null,
|
|
847
|
+
resultJson: {
|
|
848
|
+
stdout: attempt.proc.stdout,
|
|
849
|
+
stderr: attempt.proc.stderr,
|
|
850
|
+
...(transientUpstream ? { errorFamily: "transient_upstream" } : {}),
|
|
851
|
+
...(transientRetryNotBefore ? { retryNotBefore: transientRetryNotBefore.toISOString() } : {}),
|
|
852
|
+
...(transientRetryNotBefore ? { transientRetryNotBefore: transientRetryNotBefore.toISOString() } : {}),
|
|
853
|
+
},
|
|
854
|
+
summary: attempt.parsed.summary,
|
|
855
|
+
clearSession: Boolean((clearSessionOnMissingSession || forceFreshSession) && !resolvedSessionId),
|
|
856
|
+
};
|
|
857
|
+
};
|
|
858
|
+
try {
|
|
859
|
+
// Bring up Tailscale before anything talks to the model provider. No-op
|
|
860
|
+
// unless this is a sandbox with TAILSCALE_AUTHKEY set; throws on failure so
|
|
861
|
+
// a broken tailnet stops the run with a clear error instead of Codex later
|
|
862
|
+
// failing to reach the provider.
|
|
863
|
+
if (await ensureSandboxTailscaleUp({
|
|
864
|
+
runId,
|
|
865
|
+
target: runtimeExecutionTarget ?? null,
|
|
866
|
+
envConfig,
|
|
867
|
+
cwd: effectiveExecutionCwd,
|
|
868
|
+
timeoutSec,
|
|
869
|
+
onLog,
|
|
870
|
+
})) {
|
|
871
|
+
await logRemoteTiming("sandbox Tailscale ready");
|
|
872
|
+
}
|
|
873
|
+
const preflightFailure = await runSandboxPaperclipPreflight({
|
|
874
|
+
runId,
|
|
875
|
+
target: runtimeExecutionTarget ?? null,
|
|
876
|
+
cwd: effectiveExecutionCwd,
|
|
877
|
+
env: runtimeEnv,
|
|
878
|
+
timeoutSec,
|
|
879
|
+
onLog,
|
|
880
|
+
});
|
|
881
|
+
await logRemoteTiming(preflightFailure ? "sandbox Paperclip preflight failed" : "sandbox Paperclip preflight passed");
|
|
882
|
+
if (preflightFailure)
|
|
883
|
+
return preflightFailure;
|
|
884
|
+
const initial = await runAttempt(sessionId);
|
|
885
|
+
if (sessionId &&
|
|
886
|
+
!initial.proc.timedOut &&
|
|
887
|
+
(initial.proc.exitCode ?? 0) !== 0 &&
|
|
888
|
+
isCodexUnknownSessionError(initial.proc.stdout, initial.rawStderr)) {
|
|
889
|
+
await onLog("stdout", `[paperclip] Codex resume session "${sessionId}" is unavailable; retrying with a fresh session.\n`);
|
|
890
|
+
const retry = await runAttempt(null);
|
|
891
|
+
return toResult(retry, true, true);
|
|
892
|
+
}
|
|
893
|
+
return toResult(initial, false, false);
|
|
894
|
+
}
|
|
895
|
+
finally {
|
|
896
|
+
if (paperclipBridge) {
|
|
897
|
+
await paperclipBridge.stop();
|
|
898
|
+
}
|
|
899
|
+
if (restoreRemoteWorkspace) {
|
|
900
|
+
await onLog("stdout", `[paperclip] Restoring workspace changes from ${describeAdapterExecutionTarget(executionTarget)}.\n`);
|
|
901
|
+
await restoreRemoteWorkspace();
|
|
902
|
+
}
|
|
903
|
+
await remoteCodexHomeAsset?.cleanup();
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
//# sourceMappingURL=execute.js.map
|