@nowcrew/daemon 0.6.68 → 0.6.69
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/agent-ability/materializer.js +30 -1
- package/dist/agent-ability/resolver.js +27 -16
- package/dist/agent-ability/runtime.js +3 -0
- package/dist/agent-ability/types.js +6 -11
- package/dist/automatic-install-target.js +15 -10
- package/dist/background-job.js +50 -0
- package/dist/computer-cli.js +10 -1
- package/dist/computer-service.js +41 -1
- package/dist/config.js +44 -0
- package/dist/console-formatter.js +76 -1
- package/dist/console.js +23 -4
- package/dist/daemon-migration-controller.js +16 -18
- package/dist/execution-protocol.js +0 -1
- package/dist/execution-runner.js +60 -11
- package/dist/i18n.js +2 -0
- package/dist/local-execution-prompt.js +32 -13
- package/dist/local-executor.js +107 -55
- package/dist/local-memory/index-budget.js +74 -0
- package/dist/local-memory/pending.js +111 -0
- package/dist/local-memory/runtime.js +87 -0
- package/dist/local-memory/types.js +1 -0
- package/dist/local-memory/write.js +448 -0
- package/dist/machine-info.js +3 -4
- package/dist/main.js +83 -0
- package/dist/managed-service-lifecycle.js +47 -2
- package/dist/managed-service-startup.js +2 -0
- package/dist/profile-layout.js +10 -4
- package/dist/project-dependencies.js +139 -124
- package/dist/project-dependency-lock.js +87 -0
- package/dist/project-dependency-manifest.js +2 -12
- package/dist/project-registry-wiring.js +1 -4
- package/dist/project-skills/controller.js +2 -0
- package/dist/project-skills/runtime-launch.js +10 -7
- package/dist/prompt.js +19 -1
- package/dist/provider-env.js +3 -21
- package/dist/repository-identity.js +45 -0
- package/dist/runner.js +4 -2
- package/dist/runtimes/codex-app-server-runner.js +55 -23
- package/dist/runtimes/codex-command-safety.js +75 -0
- package/dist/runtimes/codex-contract.js +20 -5
- package/dist/runtimes/codex-deepseek-config.js +7 -17
- package/dist/runtimes/codex-turn-retry.js +69 -0
- package/dist/runtimes/codex.js +4 -3
- package/dist/serve.js +68 -3
- package/dist/storage-config.js +175 -0
- package/dist/storage-control.js +193 -0
- package/dist/storage-copy.js +628 -0
- package/dist/storage-credential-guard.js +21 -0
- package/dist/storage-doctor.js +228 -0
- package/dist/storage-identity.js +29 -0
- package/dist/storage-layout.js +143 -0
- package/dist/storage-manifest.js +855 -0
- package/dist/storage-migration-controller.js +420 -0
- package/dist/storage-migration-manifest.js +105 -0
- package/dist/storage-migration-policy.js +100 -0
- package/dist/storage-runtime-controller.js +339 -0
- package/dist/storage-runtime-readiness.js +184 -0
- package/dist/storage-runtime.js +39 -0
- package/dist/storage-safe-fs.js +302 -0
- package/dist/storage-serve-adapter.js +65 -0
- package/dist/storage-status-delivery.js +209 -0
- package/dist/storage-status-replay.js +125 -0
- package/dist/storage-target-confirmation.js +129 -0
- package/dist/storage-worker-command.js +37 -0
- package/dist/storage-worker-executor.js +430 -0
- package/dist/storage-worker-handoff.js +151 -0
- package/dist/storage-worker-operation.js +284 -0
- package/dist/storage-worker-status.js +259 -0
- package/dist/supervised-runtime.js +3 -1
- package/dist/workspace.js +34 -14
- package/package.json +1 -1
- package/dist/self-training-workspace.js +0 -15
package/dist/prompt.js
CHANGED
|
@@ -305,7 +305,7 @@ ${memoryTaskReadRule}
|
|
|
305
305
|
- **跨任务协调看实时清单**:用 \`crew task list --mine\` 查你当前所有任务及其状态(谁在排队、谁在评审)。例:你一次只能测一个页面时,据此判断"正在测 #1、#2 排队",在 #2 的工作日志里标注"等 #1 完成"。
|
|
306
306
|
- 需要更多背景时再 \`Read $CREW_HOME/notes/<topic>.md\`;不要一次把所有笔记读进上下文。
|
|
307
307
|
|
|
308
|
-
${communicationSection}
|
|
308
|
+
${communicationSection}${ctx.collaborationToolsEnabled === true ? buildCollaborationToolsPrompt() : ""}
|
|
309
309
|
|
|
310
310
|
CLI 成功时打印人类可读文本到 stdout;失败时 stderr 给出错误,并用**退出码**告诉你下一步:
|
|
311
311
|
- \`4\` = 任务已被他人认领 / 不可认领 → 停手,别抢,转做别的。
|
|
@@ -385,3 +385,21 @@ export function buildOriginDecisionRetryPrompt(channelId, threadId) {
|
|
|
385
385
|
"普通内部消息不算外部回复决策;--send-draft 也不是外部回复开关。",
|
|
386
386
|
].join("\n");
|
|
387
387
|
}
|
|
388
|
+
/**
|
|
389
|
+
* Codex 原生 collaboration.* 工具的用法与边界(spec 2d;2026-08-28 对 Codex 0.149.1/0.150.1 实测)。
|
|
390
|
+
* 只在 daemon 开关打开且 runtime 为 codex 时注入;是本机 runtime 能力事实,不是产品策略。
|
|
391
|
+
* 契约来自实测,不编造 Codex 未声明的返回字段。
|
|
392
|
+
*/
|
|
393
|
+
export function buildCollaborationToolsPrompt() {
|
|
394
|
+
return `
|
|
395
|
+
|
|
396
|
+
## Codex 原生协作工具
|
|
397
|
+
本机 Codex 已开启 \`collaboration.*\` 子 agent 工具。它们完全在 Codex 进程内运行:子 agent 的工作对 NowWork 的频道、线程、任务、execution 记录和按 agent 的 token 统计**都不可见**,token 并入你本轮的用量。只用于你自己会汇总的内部交叉核对、并行调研;绝不用于人类或其它 NowWork 成员需要观察、审计或接手的事。
|
|
398
|
+
- **\`collaboration.spawn_agent\`** —— 必填 \`task_name\`(仅小写字母/数字/下划线)、\`message\`;可选 \`fork_turns\`(\`"none"\`|\`"all"\`|\`"3"\` 这样的轮数,默认 \`"all"\`)、\`model\`、\`reasoning_effort\`。立即启动独立 turn,你不阻塞;子 agent 跑完后 idle 而非销毁。新 agent 用 **canonical 任务名路径**引用,如 \`/root/api_review\`。含 root 最多 4 个活跃 agent。
|
|
399
|
+
- **\`collaboration.followup_task\`** —— 必填 \`target\`(canonical 路径,不是 UUID)、\`message\`。异步投递并把 idle 的目标唤醒进新 turn,可重复。
|
|
400
|
+
- **\`collaboration.send_message\`** —— 必填 \`target\`、\`message\`。只入队,**不**启动 turn;要触发工作用 followup_task。
|
|
401
|
+
- **\`collaboration.list_agents\`** —— 可选 \`path_prefix\`。只读,列出 live agents。
|
|
402
|
+
- **\`collaboration.wait_agent\`** —— 可选 \`timeout_ms\`(10000–3600000,默认 30000)。阻塞你直到任一子 agent 有更新、有新输入或超时。
|
|
403
|
+
- **没有任何返回值有保证的 schema**:不要去解析返回值里的 \`agent_id\`/\`id\`/\`accepted\` 之类字段;用 \`list_agents\` 和 \`wait_agent\` 获取状态。
|
|
404
|
+
- **不要调用 \`collaboration.interrupt_agent\`**:它会把你自己这轮 turn 一起中断(2026-08-10 线上事故),中断后本轮结果全部丢失且不会重跑。让子 agent 自然跑完或直接忽略其结果。`;
|
|
405
|
+
}
|
package/dist/provider-env.js
CHANGED
|
@@ -11,24 +11,6 @@
|
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
import { DEEPSEEK_CODEX_KEY_ENV, deepSeekCodexHome, } from "./runtimes/codex-deepseek-config.js";
|
|
13
13
|
import { DEEPSEEK_HARNESS_CONFIG_ENV } from "./runtimes/deepseek-harness.js";
|
|
14
|
-
const ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
15
|
-
const RESERVED_ENV = new Set([
|
|
16
|
-
"PATH",
|
|
17
|
-
"XDG_CONFIG_HOME",
|
|
18
|
-
"XDG_DATA_HOME",
|
|
19
|
-
"XDG_CACHE_HOME",
|
|
20
|
-
"GH_CONFIG_DIR",
|
|
21
|
-
"CLOUDSDK_CONFIG",
|
|
22
|
-
]);
|
|
23
|
-
/** Keep user-provided variables away from daemon-owned runtime and credential fields. */
|
|
24
|
-
export function sanitizeEnvVars(raw) {
|
|
25
|
-
if (!raw)
|
|
26
|
-
return {};
|
|
27
|
-
return Object.fromEntries(Object.entries(raw).filter(([key, value]) => typeof value === "string"
|
|
28
|
-
&& ENV_KEY_RE.test(key)
|
|
29
|
-
&& !key.toUpperCase().startsWith("CREW_")
|
|
30
|
-
&& !RESERVED_ENV.has(key.toUpperCase())));
|
|
31
|
-
}
|
|
32
14
|
/** custom 模式下必须从继承 env 中剔除的全局路由/凭证变量(避免与注入值叠加或抢优先级)。 */
|
|
33
15
|
const CONFLICTING_ENV = [
|
|
34
16
|
"ANTHROPIC_BASE_URL",
|
|
@@ -57,12 +39,12 @@ const CONFLICTING_CODEX_ENV = [
|
|
|
57
39
|
DEEPSEEK_CODEX_KEY_ENV,
|
|
58
40
|
];
|
|
59
41
|
/** 返回叠加了 provider 隔离的新 env(不改入参);不支持的 runtime/provider 组合原样返回。 */
|
|
60
|
-
export function applyProviderEnv(base, runtime, cfg, homeDir
|
|
42
|
+
export function applyProviderEnv(base, runtime, cfg, homeDir) {
|
|
61
43
|
if (runtime === "codex" && cfg.provider === "deepseek") {
|
|
62
44
|
const env = { ...base };
|
|
63
45
|
for (const key of CONFLICTING_CODEX_ENV)
|
|
64
46
|
delete env[key];
|
|
65
|
-
env.CODEX_HOME =
|
|
47
|
+
env.CODEX_HOME = deepSeekCodexHome(homeDir);
|
|
66
48
|
if (cfg.providerApiKey)
|
|
67
49
|
env[DEEPSEEK_CODEX_KEY_ENV] = cfg.providerApiKey;
|
|
68
50
|
return env;
|
|
@@ -118,7 +100,7 @@ export function applyDeepSeekHarnessMachineEnv(base, machineEnv) {
|
|
|
118
100
|
*/
|
|
119
101
|
export function providerFingerprint(runtime, cfg) {
|
|
120
102
|
if (runtime === "codex" && cfg.provider === "deepseek") {
|
|
121
|
-
return `deepseek|${cfg.
|
|
103
|
+
return `deepseek|${cfg.providerApiKey ? "key" : "nokey"}`;
|
|
122
104
|
}
|
|
123
105
|
if (runtime !== "claude" || cfg.provider !== "custom")
|
|
124
106
|
return null;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { AbilityRepositoryUrlSchema } from "./agent-ability/types.js";
|
|
3
|
+
const SCP_REPOSITORY = /^git@([^:]+):(.+)$/u;
|
|
4
|
+
const COMMIT = /^[a-f0-9]{40}$/u;
|
|
5
|
+
function normalizePath(value) {
|
|
6
|
+
const path = value.replace(/^\/+|\/+$/gu, "").replace(/\.git$/iu, "");
|
|
7
|
+
if (!path || path.split("/").some((part) => part.length === 0 || part === "." || part === ".." || part.includes("\\"))) {
|
|
8
|
+
throw new Error("project_dependency_repository_path_invalid");
|
|
9
|
+
}
|
|
10
|
+
return path;
|
|
11
|
+
}
|
|
12
|
+
function repositoryParts(repository) {
|
|
13
|
+
const value = repository.trim();
|
|
14
|
+
AbilityRepositoryUrlSchema.parse(value);
|
|
15
|
+
const scp = value.match(SCP_REPOSITORY);
|
|
16
|
+
if (scp !== null)
|
|
17
|
+
return { host: scp[1].toLowerCase(), path: normalizePath(scp[2]) };
|
|
18
|
+
const url = new URL(value);
|
|
19
|
+
if (url.search || url.hash || url.username || url.password) {
|
|
20
|
+
throw new Error("project_dependency_repository_url_invalid");
|
|
21
|
+
}
|
|
22
|
+
return { host: url.hostname.toLowerCase(), path: normalizePath(url.pathname) };
|
|
23
|
+
}
|
|
24
|
+
export function normalizeRepositoryIdentity(repository) {
|
|
25
|
+
const parts = repositoryParts(repository);
|
|
26
|
+
const canonical = `${parts.host}/${parts.path}`;
|
|
27
|
+
const hash = createHash("sha256").update(canonical, "utf8").digest("hex");
|
|
28
|
+
const basename = parts.path.split("/").at(-1) ?? "repo";
|
|
29
|
+
const displayName = basename.replace(/[^A-Za-z0-9._-]/gu, "_") || "repo";
|
|
30
|
+
return Object.freeze({
|
|
31
|
+
canonical,
|
|
32
|
+
repositoryKey: `repo-${hash}`,
|
|
33
|
+
displayName,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
export function worktreeDirectoryName(branch, resolvedCommit) {
|
|
37
|
+
if (!COMMIT.test(resolvedCommit))
|
|
38
|
+
throw new Error("project_dependency_commit_invalid");
|
|
39
|
+
const safeBranch = branch
|
|
40
|
+
.replaceAll("/", "__")
|
|
41
|
+
.replace(/[^A-Za-z0-9._-]/gu, "_")
|
|
42
|
+
.replace(/^[-.]+/u, "_")
|
|
43
|
+
.slice(0, 128) || "default";
|
|
44
|
+
return `${safeBranch}--${resolvedCommit}`;
|
|
45
|
+
}
|
package/dist/runner.js
CHANGED
|
@@ -10,8 +10,7 @@ import { readOriginDecisionFile, resetOriginDecisionFile } from "./origin-decisi
|
|
|
10
10
|
import { readBoundImDecisionFile, resetBoundImDecisionFile } from "./bound-im-decision.js";
|
|
11
11
|
import { launchSupervisedRuntime } from "./supervised-runtime.js";
|
|
12
12
|
import { awaitWithCancellation, } from "./runtime-cancellation.js";
|
|
13
|
-
export { awaitExit, exitActivity } from "./local-executor.js";
|
|
14
|
-
export { sanitizeEnvVars } from "./provider-env.js";
|
|
13
|
+
export { awaitExit, exitActivity, sanitizeEnvVars } from "./local-executor.js";
|
|
15
14
|
const ICON = {
|
|
16
15
|
init: "🟢", text: "💬", reading: "📖", sending: "📨", checking: "🔎",
|
|
17
16
|
claiming: "📌", crew: "⚙️", tool: "🛠️", tool_result: "↩️", done: "✅", error: "❌",
|
|
@@ -59,6 +58,7 @@ export async function runAgent(config, input, onActivity = defaultPrint, onConso
|
|
|
59
58
|
...(input.wakeOrigin ? { wakeOrigin: input.wakeOrigin } : {}),
|
|
60
59
|
memory: capMemoryForInject(workspace.memory),
|
|
61
60
|
...(resuming ? {} : { workLog: capWorkLogForInject(workspace.workLog) }),
|
|
61
|
+
collaborationToolsEnabled: config.codexCollaborationTools && runtime === "codex",
|
|
62
62
|
}),
|
|
63
63
|
wakePrompt: ({ resuming, rotatedForBudget, nearBudget }) => {
|
|
64
64
|
const distillHint = nearBudget
|
|
@@ -75,6 +75,7 @@ export async function runAgent(config, input, onActivity = defaultPrint, onConso
|
|
|
75
75
|
name: runtime,
|
|
76
76
|
...(providerConfig.model ? { model: providerConfig.model } : {}),
|
|
77
77
|
...(reasoning.success ? { reasoning: reasoning.data } : {}),
|
|
78
|
+
codexCollaborationTools: config.codexCollaborationTools,
|
|
78
79
|
},
|
|
79
80
|
effectivePermission: config.dangerous ? "full_access" : "workspace_write",
|
|
80
81
|
captureFinal: true,
|
|
@@ -106,6 +107,7 @@ export async function runAgent(config, input, onActivity = defaultPrint, onConso
|
|
|
106
107
|
softTokens: config.sessionSoftTokens,
|
|
107
108
|
maxTurns: config.sessionMaxTurns,
|
|
108
109
|
},
|
|
110
|
+
localMemoryV2Enabled: config.localMemoryV2Handles?.has(input.handle) === true,
|
|
109
111
|
}, {
|
|
110
112
|
onActivity,
|
|
111
113
|
onConsole,
|
|
@@ -6,7 +6,8 @@ import { pathToFileURL } from "node:url";
|
|
|
6
6
|
import spawn from "cross-spawn";
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
import { signalSupervisorTree } from "../execution-supervisor.js";
|
|
9
|
-
import { assertCodexNativeWorkspaceVersion, assertCodexProjectWorkspaceResponse, assertCodexSkillsListResponse, boundedCodexPlan, codexProjectWorkspaceUnsupported, } from "./codex-contract.js";
|
|
9
|
+
import { assertCodexCollaborationVersion, assertCodexNativeWorkspaceVersion, assertCodexProjectWorkspaceResponse, assertCodexSkillsListResponse, boundedCodexPlan, codexProjectWorkspaceUnsupported, } from "./codex-contract.js";
|
|
10
|
+
import { decideTurnRetry, itemMakesTurnUnsafe } from "./codex-turn-retry.js";
|
|
10
11
|
import { startFirstProgressWatchdog } from "./progress-watchdog.js";
|
|
11
12
|
const RunnerInputSchema = z.object({
|
|
12
13
|
systemPrompt: z.string().min(1),
|
|
@@ -20,13 +21,9 @@ const RunnerInputSchema = z.object({
|
|
|
20
21
|
cwd: z.string().min(1).optional(),
|
|
21
22
|
workspaceRoots: z.array(z.string().min(1)).optional(),
|
|
22
23
|
skillRoots: z.array(z.string().min(1)).optional(),
|
|
23
|
-
|
|
24
|
+
codexCollaborationTools: z.boolean().optional(),
|
|
24
25
|
resume: z.boolean(),
|
|
25
|
-
}).strict()
|
|
26
|
-
if (input.networkAccess === true && input.effectivePermission !== "workspace_write") {
|
|
27
|
-
ctx.addIssue({ code: "custom", message: "network_access_requires_workspace_write" });
|
|
28
|
-
}
|
|
29
|
-
});
|
|
26
|
+
}).strict();
|
|
30
27
|
const RPC_TIMEOUT_MS = 30_000;
|
|
31
28
|
const INITIALIZE_RPC_TIMEOUT_MS = 60_000;
|
|
32
29
|
const ERROR_MESSAGE_CAP = 2_000;
|
|
@@ -42,12 +39,17 @@ const MAX_TRANSIENT_TURN_RETRIES = 2;
|
|
|
42
39
|
const TRANSIENT_TURN_RETRY_DELAY_MS = 15_000;
|
|
43
40
|
const TRANSIENT_TURN_RETRY_BACKOFF_FACTOR = 3;
|
|
44
41
|
const PROCESS_TREE_STOP_TIMEOUT_MS = 1_000;
|
|
42
|
+
/** 进程级:一旦某个 collabAgentToolCall 的 item/completed 没有对应 item/started,本执行后续永不重试(spec R4-3)。 */
|
|
43
|
+
let collabStartedMissing = false;
|
|
45
44
|
const USER_INPUT_UNAVAILABLE_MESSAGE = "NowWork cannot collect interactive user input in this headless run; ask the user in the final response instead of assuming an answer.";
|
|
46
|
-
export function codexAppServerArgs(
|
|
45
|
+
export function codexAppServerArgs(input) {
|
|
47
46
|
return [
|
|
48
|
-
...(projectRootMarkers === undefined
|
|
47
|
+
...(input.projectRootMarkers === undefined
|
|
49
48
|
? []
|
|
50
|
-
: ["-c", `project_root_markers=${JSON.stringify(projectRootMarkers)}`]),
|
|
49
|
+
: ["-c", `project_root_markers=${JSON.stringify(input.projectRootMarkers)}`]),
|
|
50
|
+
// multi_agent 按开关;multi_agent_v2 无条件关,钉死已验证的 v1 契约(spec 2b)。
|
|
51
|
+
"-c", `features.multi_agent=${input.collaborationTools === true}`,
|
|
52
|
+
"-c", "features.multi_agent_v2=false",
|
|
51
53
|
"app-server", "--listen", "stdio://",
|
|
52
54
|
];
|
|
53
55
|
}
|
|
@@ -58,7 +60,8 @@ const TRANSIENT_TURN_ERROR_PATTERNS = [
|
|
|
58
60
|
/too many requests/i,
|
|
59
61
|
];
|
|
60
62
|
/** 模型侧瞬态失败(网关过载/限流)→ 换个时间整轮重试大概率成功。
|
|
61
|
-
* interrupted
|
|
63
|
+
* interrupted 单独归为 collab_interrupted:2026-08-10 实锤是模型自己调 collaboration.interrupt_agent
|
|
64
|
+
* 连带把父 turn 置为 interrupted。两类原因都只在本 turn 无白名单外工具活动时重试(codex-turn-retry.ts)。 */
|
|
62
65
|
export function isTransientTurnFailure(detail) {
|
|
63
66
|
return TRANSIENT_TURN_ERROR_PATTERNS.some((pattern) => pattern.test(detail));
|
|
64
67
|
}
|
|
@@ -418,7 +421,10 @@ async function stopChildTree(child) {
|
|
|
418
421
|
}
|
|
419
422
|
async function runCodexAppServerAttempt(bin, input, attempt, initializeTimeoutMs) {
|
|
420
423
|
const attemptStartedAt = Date.now();
|
|
421
|
-
const child = spawn(bin, codexAppServerArgs(
|
|
424
|
+
const child = spawn(bin, codexAppServerArgs({
|
|
425
|
+
...(input.projectRootMarkers === undefined ? {} : { projectRootMarkers: input.projectRootMarkers }),
|
|
426
|
+
...(input.codexCollaborationTools === undefined ? {} : { collaborationTools: input.codexCollaborationTools }),
|
|
427
|
+
}), {
|
|
422
428
|
cwd: process.cwd(),
|
|
423
429
|
env: process.env,
|
|
424
430
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -439,6 +445,8 @@ async function runCodexAppServerAttempt(bin, input, attempt, initializeTimeoutMs
|
|
|
439
445
|
let threadId = null;
|
|
440
446
|
let announcedThreadId = null;
|
|
441
447
|
let turnId = null;
|
|
448
|
+
let turnHasUnsafeSideEffect = false;
|
|
449
|
+
const startedItemIds = new Set();
|
|
442
450
|
let lastUsage;
|
|
443
451
|
let completionResolve;
|
|
444
452
|
let completionReject;
|
|
@@ -457,6 +465,20 @@ async function runCodexAppServerAttempt(bin, input, attempt, initializeTimeoutMs
|
|
|
457
465
|
|| method === "item/agentMessage/delta" || method === "item/completed") {
|
|
458
466
|
firstProgress.observe();
|
|
459
467
|
}
|
|
468
|
+
if (method === "item/started" || method === "item/completed") {
|
|
469
|
+
const item = params !== null && typeof params === "object"
|
|
470
|
+
? params.item
|
|
471
|
+
: undefined;
|
|
472
|
+
if (method === "item/started" && typeof item?.id === "string")
|
|
473
|
+
startedItemIds.add(item.id);
|
|
474
|
+
if (method === "item/completed" && item?.type === "collabAgentToolCall"
|
|
475
|
+
&& (typeof item.id !== "string" || !startedItemIds.has(item.id)) && !collabStartedMissing) {
|
|
476
|
+
collabStartedMissing = true;
|
|
477
|
+
process.stderr.write("[codex-app-server] collab_item_started_missing\n");
|
|
478
|
+
}
|
|
479
|
+
if (itemMakesTurnUnsafe(item))
|
|
480
|
+
turnHasUnsafeSideEffect = true;
|
|
481
|
+
}
|
|
460
482
|
for (const event of mapCodexNotification(method, params)) {
|
|
461
483
|
const eventThreadId = typeof event.thread_id === "string" ? event.thread_id : null;
|
|
462
484
|
if (event.type === "thread.started" && eventThreadId === announcedThreadId)
|
|
@@ -501,6 +523,8 @@ async function runCodexAppServerAttempt(bin, input, attempt, initializeTimeoutMs
|
|
|
501
523
|
if (input.workspaceRoots !== undefined || input.skillRoots !== undefined) {
|
|
502
524
|
assertCodexNativeWorkspaceVersion(initialized);
|
|
503
525
|
}
|
|
526
|
+
if (input.codexCollaborationTools === true)
|
|
527
|
+
assertCodexCollaborationVersion(initialized);
|
|
504
528
|
logStage(activeStage, "ok", attempt, stageStartedAt);
|
|
505
529
|
rpc.notify("initialized");
|
|
506
530
|
activeStage = input.resume ? "thread_resume" : "thread_start";
|
|
@@ -565,9 +589,6 @@ async function runCodexAppServerAttempt(bin, input, attempt, initializeTimeoutMs
|
|
|
565
589
|
...(input.reasoning === undefined || input.reasoning === "default"
|
|
566
590
|
? {}
|
|
567
591
|
: { effort: input.reasoning }),
|
|
568
|
-
...(input.networkAccess === true ? {
|
|
569
|
-
sandboxPolicy: { type: "workspaceWrite", networkAccess: true },
|
|
570
|
-
} : {}),
|
|
571
592
|
});
|
|
572
593
|
logStage(activeStage, "ok", attempt, stageStartedAt);
|
|
573
594
|
turnId = started.turn.id;
|
|
@@ -588,15 +609,19 @@ async function runCodexAppServerAttempt(bin, input, attempt, initializeTimeoutMs
|
|
|
588
609
|
}),
|
|
589
610
|
});
|
|
590
611
|
if (completed.turn?.status === "completed")
|
|
591
|
-
return { code: 0, initializeTimedOut: false };
|
|
612
|
+
return { code: 0, initializeTimedOut: false, unsafeToRetry: false };
|
|
592
613
|
const detail = completed.turn?.error?.message ?? `turn status ${completed.turn?.status ?? "unknown"}`;
|
|
593
614
|
process.stderr.write(sanitizeCodexDiagnostic(`Codex turn failed: ${detail.slice(0, ERROR_MESSAGE_CAP)}\n`, secrets));
|
|
615
|
+
const status = completed.turn?.status;
|
|
616
|
+
const retryReason = cancelling ? undefined
|
|
617
|
+
: status === "interrupted" ? "collab_interrupted"
|
|
618
|
+
: status === "failed" && isTransientTurnFailure(detail) ? "transient_turn_failure"
|
|
619
|
+
: undefined;
|
|
594
620
|
return {
|
|
595
|
-
code:
|
|
621
|
+
code: status === "interrupted" ? 130 : 1,
|
|
596
622
|
initializeTimedOut: false,
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
&& isTransientTurnFailure(detail),
|
|
623
|
+
...(retryReason === undefined ? {} : { retryReason }),
|
|
624
|
+
unsafeToRetry: turnHasUnsafeSideEffect || collabStartedMissing,
|
|
600
625
|
};
|
|
601
626
|
}
|
|
602
627
|
catch (error) {
|
|
@@ -609,7 +634,7 @@ async function runCodexAppServerAttempt(bin, input, attempt, initializeTimeoutMs
|
|
|
609
634
|
logStage(activeStage, "error", attempt, stageStartedAt);
|
|
610
635
|
}
|
|
611
636
|
process.stderr.write(`Codex app-server execution failed: ${safeErrorMessage(error, secrets)}\n`);
|
|
612
|
-
return { code: 1, initializeTimedOut };
|
|
637
|
+
return { code: 1, initializeTimedOut, unsafeToRetry: false };
|
|
613
638
|
}
|
|
614
639
|
finally {
|
|
615
640
|
firstProgress.stop();
|
|
@@ -641,13 +666,20 @@ export async function runCodexAppServer(bin, options = {}) {
|
|
|
641
666
|
process.stderr.write(`[codex-app-server] stage=retry status=start attempt=${attempt + 1} elapsed_ms=0 reason=initialize_timeout\n`);
|
|
642
667
|
continue;
|
|
643
668
|
}
|
|
644
|
-
|
|
669
|
+
const decision = decideTurnRetry(result, turnRetries, MAX_TRANSIENT_TURN_RETRIES);
|
|
670
|
+
if (decision.kind === "retry") {
|
|
645
671
|
const delayMs = turnRetryDelayMs * TRANSIENT_TURN_RETRY_BACKOFF_FACTOR ** turnRetries;
|
|
646
672
|
turnRetries += 1;
|
|
647
|
-
process.stderr.write(`[codex-app-server] stage=retry status=start attempt=${attempt + 1} elapsed_ms=0 reason
|
|
673
|
+
process.stderr.write(`[codex-app-server] stage=retry status=start attempt=${attempt + 1} elapsed_ms=0 reason=${decision.reason}\n`);
|
|
648
674
|
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
649
675
|
continue;
|
|
650
676
|
}
|
|
677
|
+
if (decision.kind === "skipped") {
|
|
678
|
+
process.stderr.write(`[codex-app-server] stage=retry status=skipped attempt=${attempt} reason=${decision.reason}_unsafe\n`);
|
|
679
|
+
}
|
|
680
|
+
else if (decision.kind === "exhausted") {
|
|
681
|
+
process.stderr.write(`[codex-app-server] stage=retry status=exhausted attempt=${attempt} reason=${decision.reason}\n`);
|
|
682
|
+
}
|
|
651
683
|
return result.code;
|
|
652
684
|
}
|
|
653
685
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex app-server 重试门控用的「只读 crew 命令」判定(spec Part 1,R3-1/R4-1)。
|
|
3
|
+
*
|
|
4
|
+
* 白名单 + 封闭文法,不是黑名单:任何解析不确定都返回 false。只回答"这是不是白名单里某条
|
|
5
|
+
* crew 只读命令的一次干净调用",不做"读/写"语义判断。任何非 crew 命令(sed/rg/git/python…)
|
|
6
|
+
* 一律 false——白名单之外的一切都视为不可安全重试。零 IO,可独立单测。
|
|
7
|
+
*/
|
|
8
|
+
/** 封闭列表,由 cli/src/run.ts 命令面反推;新增 CLI 只读命令时同步维护。漏加只会更保守。 */
|
|
9
|
+
export const READ_ONLY_CREW_COMMANDS = new Set([
|
|
10
|
+
"whoami",
|
|
11
|
+
"server info",
|
|
12
|
+
"message read",
|
|
13
|
+
"message check",
|
|
14
|
+
"thread read",
|
|
15
|
+
"task list",
|
|
16
|
+
"schedule list",
|
|
17
|
+
"schedule runs",
|
|
18
|
+
"reminder list",
|
|
19
|
+
"reminder log",
|
|
20
|
+
"resolve",
|
|
21
|
+
"channel members",
|
|
22
|
+
"integration env",
|
|
23
|
+
"integration list",
|
|
24
|
+
]);
|
|
25
|
+
const SINGLE_VERB = new Set(["whoami", "resolve"]);
|
|
26
|
+
// 一层 shell 包装:bash/sh/zsh -c / -lc / -cl,后跟被单引号或双引号整体包裹的正文。
|
|
27
|
+
const WRAPPER = /^(?:\/usr\/bin\/|\/bin\/)?(?:bash|sh|zsh)\s+-(?:l?c|cl)\s+(.+)$/su;
|
|
28
|
+
// 整串禁字符:出现任一即拒绝(含单个 &、引号、$、括号、换行、制表、反斜杠)。
|
|
29
|
+
const FORBIDDEN = /[&;|<>$`(){}\\'"\n\r\t]/u;
|
|
30
|
+
const OPTION_TOKEN = /^--?[A-Za-z][A-Za-z0-9-]*(?:=[A-Za-z0-9._:/@#%+-]+)?$/u;
|
|
31
|
+
const VALUE_TOKEN = /^[A-Za-z0-9._:/@#%+][A-Za-z0-9._:/@#%+-]+$/u;
|
|
32
|
+
function unwrap(command) {
|
|
33
|
+
const match = WRAPPER.exec(command);
|
|
34
|
+
if (match === null)
|
|
35
|
+
return command;
|
|
36
|
+
const quoted = match[1] ?? "";
|
|
37
|
+
const quote = quoted[0];
|
|
38
|
+
if (quote !== "'" && quote !== "\"")
|
|
39
|
+
return null;
|
|
40
|
+
if (quoted.length < 2 || quoted[quoted.length - 1] !== quote)
|
|
41
|
+
return null;
|
|
42
|
+
const body = quoted.slice(1, -1);
|
|
43
|
+
if (body.includes(quote))
|
|
44
|
+
return null;
|
|
45
|
+
if (quote === "\"" && /[$\\]/u.test(body))
|
|
46
|
+
return null;
|
|
47
|
+
return body;
|
|
48
|
+
}
|
|
49
|
+
export function isReadOnlyCrewCommand(command) {
|
|
50
|
+
const body = unwrap(command);
|
|
51
|
+
if (body === null)
|
|
52
|
+
return false;
|
|
53
|
+
if (FORBIDDEN.test(body))
|
|
54
|
+
return false;
|
|
55
|
+
const trimmed = body.trim();
|
|
56
|
+
if (trimmed === "" || /\s{2,}/u.test(trimmed))
|
|
57
|
+
return false;
|
|
58
|
+
const tokens = trimmed.split(" ");
|
|
59
|
+
if (tokens[0] !== "crew")
|
|
60
|
+
return false;
|
|
61
|
+
const verb = tokens[1];
|
|
62
|
+
if (verb === undefined)
|
|
63
|
+
return false;
|
|
64
|
+
let consumed;
|
|
65
|
+
if (SINGLE_VERB.has(verb)) {
|
|
66
|
+
consumed = 2;
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
const pair = `${verb} ${tokens[2] ?? ""}`;
|
|
70
|
+
if (!READ_ONLY_CREW_COMMANDS.has(pair))
|
|
71
|
+
return false;
|
|
72
|
+
consumed = 3;
|
|
73
|
+
}
|
|
74
|
+
return tokens.slice(consumed).every((token) => OPTION_TOKEN.test(token) || VALUE_TOKEN.test(token));
|
|
75
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { CODEX_NATIVE_WORKSPACE_MIN_VERSION } from "./codex.js";
|
|
1
|
+
import { CODEX_COLLABORATION_MIN_VERSION, CODEX_NATIVE_WORKSPACE_MIN_VERSION } from "./codex.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
export const CODEX_PROJECT_WORKSPACE_UNSUPPORTED = "codex_project_workspace_unsupported";
|
|
4
|
+
export const CODEX_COLLABORATION_UNSUPPORTED = "codex_collaboration_unsupported";
|
|
4
5
|
export const CODEX_PLAN_ENTRY_LIMIT = 16;
|
|
5
6
|
// 16 steps remain inside the server's 4,000 UTF-16-unit plan payload cap even
|
|
6
7
|
// when every retained code point is a surrogate pair (for example, emoji).
|
|
@@ -51,18 +52,32 @@ const versionTuple = (version) => {
|
|
|
51
52
|
const versionAtLeast = (actual, minimum) => actual[0] > minimum[0]
|
|
52
53
|
|| (actual[0] === minimum[0] && (actual[1] > minimum[1]
|
|
53
54
|
|| (actual[1] === minimum[1] && actual[2] >= minimum[2])));
|
|
54
|
-
|
|
55
|
-
export function assertCodexNativeWorkspaceVersion(response) {
|
|
55
|
+
function initializeVersion(response) {
|
|
56
56
|
const parsed = NativeInitializeResponseSchema.safeParse(response);
|
|
57
57
|
if (!parsed.success)
|
|
58
|
-
|
|
58
|
+
return null;
|
|
59
59
|
const match = /(?:^|[/\s])(\d+\.\d+\.\d+)(?=$|[\s(])/u.exec(parsed.data.userAgent);
|
|
60
|
-
|
|
60
|
+
return match === null ? null : versionTuple(match[1] ?? "");
|
|
61
|
+
}
|
|
62
|
+
/** Native roots require the app-server version returned by initialize, not a separate shell probe. */
|
|
63
|
+
export function assertCodexNativeWorkspaceVersion(response) {
|
|
64
|
+
const actual = initializeVersion(response);
|
|
61
65
|
const minimum = versionTuple(CODEX_NATIVE_WORKSPACE_MIN_VERSION);
|
|
62
66
|
if (actual === null || minimum === null || !versionAtLeast(actual, minimum)) {
|
|
63
67
|
throw codexProjectWorkspaceUnsupported();
|
|
64
68
|
}
|
|
65
69
|
}
|
|
70
|
+
/** Collaboration tools are exposed only on an app-server whose contract was verified (spec 2b). */
|
|
71
|
+
export function assertCodexCollaborationVersion(response) {
|
|
72
|
+
const actual = initializeVersion(response);
|
|
73
|
+
const minimum = versionTuple(CODEX_COLLABORATION_MIN_VERSION);
|
|
74
|
+
if (actual === null || minimum === null || !versionAtLeast(actual, minimum)) {
|
|
75
|
+
throw codexCollaborationUnsupported();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export function codexCollaborationUnsupported() {
|
|
79
|
+
return new Error(CODEX_COLLABORATION_UNSUPPORTED);
|
|
80
|
+
}
|
|
66
81
|
const PLAN_STATUSES = new Set(["pending", "inProgress", "completed"]);
|
|
67
82
|
const clipCharacters = (value, limit) => [...value.trim().replace(/\s+/gu, " ")].slice(0, limit).join("");
|
|
68
83
|
/** Treat plan updates as bounded user-visible data; any malformed retained step rejects the event. */
|
|
@@ -7,20 +7,10 @@ export const DEEPSEEK_CODEX_BASE_URL = "https://api.deepseek.com/";
|
|
|
7
7
|
export const DEEPSEEK_CODEX_KEY_ENV = "NOWCREW_DEEPSEEK_API_KEY";
|
|
8
8
|
export const DEEPSEEK_CODEX_REASONING_LEVELS = ["low", "high", "max"];
|
|
9
9
|
const PRIVATE_CODEX_HOME = ".codex-deepseek";
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
return resolve(homeDir, selfTraining ? SELF_TRAINING_PRIVATE_CODEX_HOME : PRIVATE_CODEX_HOME);
|
|
10
|
+
export function deepSeekCodexHome(homeDir) {
|
|
11
|
+
return resolve(homeDir, PRIVATE_CODEX_HOME);
|
|
13
12
|
}
|
|
14
|
-
function
|
|
15
|
-
const url = new URL(value);
|
|
16
|
-
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
17
|
-
throw new Error("DeepSeek provider base URL must use HTTP(S)");
|
|
18
|
-
}
|
|
19
|
-
if (!url.pathname.endsWith("/"))
|
|
20
|
-
url.pathname = `${url.pathname}/`;
|
|
21
|
-
return url.toString();
|
|
22
|
-
}
|
|
23
|
-
export function renderDeepSeekConfig(codexHome, baseUrl = DEEPSEEK_CODEX_BASE_URL) {
|
|
13
|
+
export function renderDeepSeekConfig(codexHome) {
|
|
24
14
|
const catalogPath = join(codexHome, "models.json");
|
|
25
15
|
return [
|
|
26
16
|
`model = ${JSON.stringify(DEEPSEEK_CODEX_MODEL)}`,
|
|
@@ -30,7 +20,7 @@ export function renderDeepSeekConfig(codexHome, baseUrl = DEEPSEEK_CODEX_BASE_UR
|
|
|
30
20
|
"",
|
|
31
21
|
"[model_providers.deepseek]",
|
|
32
22
|
'name = "deepseek"',
|
|
33
|
-
`base_url = ${JSON.stringify(
|
|
23
|
+
`base_url = ${JSON.stringify(DEEPSEEK_CODEX_BASE_URL)}`,
|
|
34
24
|
'wire_api = "responses"',
|
|
35
25
|
`env_key = ${JSON.stringify(DEEPSEEK_CODEX_KEY_ENV)}`,
|
|
36
26
|
"",
|
|
@@ -50,11 +40,11 @@ async function writePrivateAtomic(path, content) {
|
|
|
50
40
|
await rm(temporary, { force: true });
|
|
51
41
|
}
|
|
52
42
|
}
|
|
53
|
-
export async function materializeDeepSeekCodexHome(homeDir
|
|
54
|
-
const codexHome = deepSeekCodexHome(homeDir
|
|
43
|
+
export async function materializeDeepSeekCodexHome(homeDir) {
|
|
44
|
+
const codexHome = deepSeekCodexHome(homeDir);
|
|
55
45
|
await mkdir(codexHome, { recursive: true, mode: 0o700 });
|
|
56
46
|
await chmod(codexHome, 0o700);
|
|
57
47
|
await writePrivateAtomic(join(codexHome, "models.json"), DEEPSEEK_CODEX_CATALOG);
|
|
58
|
-
await writePrivateAtomic(join(codexHome, "config.toml"), renderDeepSeekConfig(codexHome
|
|
48
|
+
await writePrivateAtomic(join(codexHome, "config.toml"), renderDeepSeekConfig(codexHome));
|
|
59
49
|
return codexHome;
|
|
60
50
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex app-server turn 级重试决策(spec Part 1)。
|
|
3
|
+
*
|
|
4
|
+
* attempt 只上报两个正交事实:retryReason(为什么值得重试)与 unsafeToRetry(本 turn 是否已有
|
|
5
|
+
* 白名单之外的工具活动)。外层循环通过 decideTurnRetry 单一出口判断,所有原因共用一个预算。
|
|
6
|
+
* 2026-08-10 事故根因:模型自己调 collaboration.interrupt_agent,Codex 把父 turn 也置为
|
|
7
|
+
* interrupted;旧代码直接 exit 130。同时旧的网关过载重试是无条件的,会重放 crew message send。
|
|
8
|
+
*/
|
|
9
|
+
import { isReadOnlyCrewCommand } from "./codex-command-safety.js";
|
|
10
|
+
/**
|
|
11
|
+
* Codex app-server v2 `codex app-server generate-json-schema` ThreadItem 枚举的全部 18 种类型,
|
|
12
|
+
* 逐一裁定安全性(2026-08-28 最终评审 F1:实测真实 app-server 0.150.1 发现 userMessage 缺失导致
|
|
13
|
+
* 所有 turn 被判不安全,回归影响过载重试与 interrupted 重试两条路径)。新增协议类型时,必须在此
|
|
14
|
+
* 表格里显式落座,不允许静默归类。
|
|
15
|
+
*
|
|
16
|
+
* safe(协议/叙述类,无副作用,共 9 种):
|
|
17
|
+
* - userMessage Codex 对我们自己发送的 prompt 的回显,每个 turn 必有
|
|
18
|
+
* - hookPrompt hook 触发的系统提示回显
|
|
19
|
+
* - agentMessage 模型最终回复文本
|
|
20
|
+
* - reasoning 模型推理过程(不透出给下游)
|
|
21
|
+
* - plan 模型计划更新
|
|
22
|
+
* - contextCompaction 上下文压缩通知
|
|
23
|
+
* - sleep 模型主动等待
|
|
24
|
+
* - enteredReviewMode 进入 review 模式通知
|
|
25
|
+
* - exitedReviewMode 退出 review 模式通知
|
|
26
|
+
* conditional(1 种,视具体内容而定):
|
|
27
|
+
* - commandExecution 仅当 isReadOnlyCrewCommand(command) 为 true 时安全
|
|
28
|
+
* unsafe(有副作用或无法判断,共 8 种 + 未知类型):
|
|
29
|
+
* - fileChange 写文件
|
|
30
|
+
* - mcpToolCall 调用外部 MCP 工具
|
|
31
|
+
* - dynamicToolCall 调用动态工具
|
|
32
|
+
* - collabAgentToolCall 协作工具(spawnAgent/interrupt_agent 等)
|
|
33
|
+
* - subAgentActivity 子 agent 活动
|
|
34
|
+
* - webSearch 联网搜索
|
|
35
|
+
* - imageView 查看图片
|
|
36
|
+
* - imageGeneration 生成图片
|
|
37
|
+
* - (任何未知/缺失 type) 一律视为不安全
|
|
38
|
+
*/
|
|
39
|
+
const NARRATIVE_ITEM_TYPES = new Set([
|
|
40
|
+
"userMessage",
|
|
41
|
+
"hookPrompt",
|
|
42
|
+
"agentMessage",
|
|
43
|
+
"reasoning",
|
|
44
|
+
"plan",
|
|
45
|
+
"contextCompaction",
|
|
46
|
+
"sleep",
|
|
47
|
+
"enteredReviewMode",
|
|
48
|
+
"exitedReviewMode",
|
|
49
|
+
]);
|
|
50
|
+
/** 白名单判定:只有叙述类 item 和白名单内的 crew 只读命令是安全的;其它一切(含未知类型)不安全。 */
|
|
51
|
+
export function itemMakesTurnUnsafe(item) {
|
|
52
|
+
if (item?.type === undefined)
|
|
53
|
+
return true;
|
|
54
|
+
if (NARRATIVE_ITEM_TYPES.has(item.type))
|
|
55
|
+
return false;
|
|
56
|
+
if (item.type === "commandExecution") {
|
|
57
|
+
return typeof item.command !== "string" || !isReadOnlyCrewCommand(item.command);
|
|
58
|
+
}
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
export function decideTurnRetry(result, turnRetries, maxRetries) {
|
|
62
|
+
if (result.retryReason === undefined)
|
|
63
|
+
return { kind: "return" };
|
|
64
|
+
if (result.unsafeToRetry)
|
|
65
|
+
return { kind: "skipped", reason: result.retryReason };
|
|
66
|
+
if (turnRetries < maxRetries)
|
|
67
|
+
return { kind: "retry", reason: result.retryReason };
|
|
68
|
+
return { kind: "exhausted", reason: result.retryReason };
|
|
69
|
+
}
|
package/dist/runtimes/codex.js
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
import spawn from "cross-spawn";
|
|
6
6
|
/** Minimum CLI whose real app-server contract supports NowCrew native project workspaces. */
|
|
7
7
|
export const CODEX_NATIVE_WORKSPACE_MIN_VERSION = "0.148.0";
|
|
8
|
+
/** Minimum CLI whose `multi_agent` collaboration tool contract was empirically verified (2026-08-28). */
|
|
9
|
+
export const CODEX_COLLABORATION_MIN_VERSION = "0.149.1";
|
|
8
10
|
// Codex CLI 原生 model_reasoning_effort 档位(codex 0.135.0 实测:非法值时 config 解析报错枚举这六档)。
|
|
9
11
|
// 注意:codex 对非法值是硬失败(进程直接退出),所以必须白名单过滤;白名单外(含 "default"、
|
|
10
12
|
// claude 专属的 "max")回落 CODEX_DEFAULT_EFFORT。
|
|
@@ -22,6 +24,8 @@ export function buildCodexArgs(input) {
|
|
|
22
24
|
? input.reasoning
|
|
23
25
|
: CODEX_DEFAULT_EFFORT;
|
|
24
26
|
args.push("-c", `model_reasoning_effort=${effort}`);
|
|
27
|
+
// 纵深防御:exec 启动器没有 initialize 握手、无法断言版本,协作工具恒关(spec 2b)。
|
|
28
|
+
args.push("-c", "features.multi_agent=false", "-c", "features.multi_agent_v2=false");
|
|
25
29
|
if (input.projectRootMarkers) {
|
|
26
30
|
args.push("-c", `project_root_markers=${JSON.stringify(input.projectRootMarkers)}`);
|
|
27
31
|
}
|
|
@@ -32,9 +36,6 @@ export function buildCodexArgs(input) {
|
|
|
32
36
|
else if (input.effectivePermission === "full_access" || (input.effectivePermission === undefined && input.dangerous)) {
|
|
33
37
|
args.push("--dangerously-bypass-approvals-and-sandbox");
|
|
34
38
|
}
|
|
35
|
-
if (input.effectivePermission === "workspace_write" && input.networkAccess === true) {
|
|
36
|
-
args.push("-c", "sandbox_workspace_write.network_access=true");
|
|
37
|
-
}
|
|
38
39
|
for (const imagePath of input.imagePaths ?? [])
|
|
39
40
|
args.push("--image", imagePath);
|
|
40
41
|
// `-` instructs codex exec to read the prompt from stdin. Keeping the complete prompt out of argv
|