codex-octopus 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/lib.js ADDED
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Pure, testable logic extracted from index.ts.
3
+ */
4
+ import { normalize, resolve, sep } from "node:path";
5
+ // ── Env helpers ────────────────────────────────────────────────────
6
+ export function envStr(key, env = process.env) {
7
+ return env[key] || undefined;
8
+ }
9
+ export function envList(key, env = process.env) {
10
+ const val = env[key];
11
+ if (!val)
12
+ return undefined;
13
+ if (val.startsWith("[")) {
14
+ try {
15
+ const parsed = JSON.parse(val);
16
+ if (Array.isArray(parsed))
17
+ return parsed.map(String).filter(Boolean);
18
+ }
19
+ catch {
20
+ // fall through to comma-split
21
+ }
22
+ }
23
+ return val
24
+ .split(",")
25
+ .map((s) => s.trim())
26
+ .filter(Boolean);
27
+ }
28
+ export function envNum(key, env = process.env) {
29
+ const val = env[key];
30
+ if (!val)
31
+ return undefined;
32
+ const n = Number(val);
33
+ return Number.isNaN(n) ? undefined : n;
34
+ }
35
+ export function envBool(key, fallback, env = process.env) {
36
+ const val = env[key];
37
+ if (val === undefined)
38
+ return fallback;
39
+ return val === "true" || val === "1";
40
+ }
41
+ // ── Tool name sanitization ─────────────────────────────────────────
42
+ export const MAX_TOOL_NAME_LEN = 64 - "_reply".length;
43
+ export function sanitizeToolName(raw) {
44
+ const sanitized = raw
45
+ .replace(/[^a-zA-Z0-9_]/g, "_")
46
+ .slice(0, MAX_TOOL_NAME_LEN);
47
+ return sanitized || "codex";
48
+ }
49
+ // ── cwd security check ────────────────────────────────────────────
50
+ export function isDescendantPath(requested, baseCwd) {
51
+ const normalBase = normalize(baseCwd);
52
+ const normalReq = normalize(resolve(normalBase, requested));
53
+ if (normalReq === normalBase)
54
+ return true;
55
+ const baseWithSep = normalBase.endsWith(sep)
56
+ ? normalBase
57
+ : normalBase + sep;
58
+ return normalReq.startsWith(baseWithSep);
59
+ }
60
+ // ── Sandbox mode validation ───────────────────────────────────────
61
+ export const VALID_SANDBOX_MODES = new Set([
62
+ "read-only",
63
+ "workspace-write",
64
+ "danger-full-access",
65
+ ]);
66
+ export function validateSandboxMode(mode) {
67
+ return VALID_SANDBOX_MODES.has(mode) ? mode : "read-only";
68
+ }
69
+ // Strictness order: most permissive → most restrictive
70
+ const SANDBOX_STRICTNESS = {
71
+ "danger-full-access": 0,
72
+ "workspace-write": 1,
73
+ "read-only": 2,
74
+ };
75
+ /**
76
+ * Narrow sandbox mode: returns the stricter of base and override.
77
+ * Callers can tighten but never loosen.
78
+ */
79
+ export function narrowSandboxMode(base, override) {
80
+ if (!VALID_SANDBOX_MODES.has(override))
81
+ return base;
82
+ const baseLevel = SANDBOX_STRICTNESS[base] ?? 2;
83
+ const overrideLevel = SANDBOX_STRICTNESS[override] ?? 2;
84
+ return overrideLevel >= baseLevel ? override : base;
85
+ }
86
+ // ── Approval policy validation ────────────────────────────────────
87
+ export const VALID_APPROVAL_POLICIES = new Set([
88
+ "never",
89
+ "on-request",
90
+ "on-failure",
91
+ "untrusted",
92
+ ]);
93
+ export function validateApprovalPolicy(policy) {
94
+ return VALID_APPROVAL_POLICIES.has(policy) ? policy : "on-failure";
95
+ }
96
+ const APPROVAL_STRICTNESS = {
97
+ never: 0,
98
+ "on-failure": 1,
99
+ "on-request": 2,
100
+ untrusted: 3,
101
+ };
102
+ /**
103
+ * Narrow approval policy: returns the stricter of base and override.
104
+ * Callers can tighten but never loosen.
105
+ */
106
+ export function narrowApprovalPolicy(base, override) {
107
+ if (!VALID_APPROVAL_POLICIES.has(override))
108
+ return base;
109
+ const baseLevel = APPROVAL_STRICTNESS[base] ?? 1;
110
+ const overrideLevel = APPROVAL_STRICTNESS[override] ?? 1;
111
+ return overrideLevel >= baseLevel ? override : base;
112
+ }
113
+ // ── Effort validation ─────────────────────────────────────────────
114
+ export const VALID_EFFORTS = new Set([
115
+ "minimal",
116
+ "low",
117
+ "medium",
118
+ "high",
119
+ "xhigh",
120
+ ]);
121
+ export function validateEffort(effort) {
122
+ return VALID_EFFORTS.has(effort) ? effort : "medium";
123
+ }
124
+ // ── Factory name derivation ────────────────────────────────────────
125
+ export function deriveServerName(description) {
126
+ const slug = description
127
+ .toLowerCase()
128
+ .replace(/[^a-z0-9]+/g, "-")
129
+ .replace(/^-|-$/g, "")
130
+ .slice(0, 30);
131
+ return slug || `agent-${Date.now()}`;
132
+ }
133
+ export function deriveToolName(name) {
134
+ const slug = name
135
+ .replace(/[^a-zA-Z0-9]+/g, "_")
136
+ .replace(/^_|_$/g, "")
137
+ .slice(0, MAX_TOOL_NAME_LEN);
138
+ return slug || "agent";
139
+ }
140
+ // ── Factory env serialization ──────────────────────────────────────
141
+ export function serializeArrayEnv(val) {
142
+ const hasComma = val.some((v) => String(v).includes(","));
143
+ return hasComma ? JSON.stringify(val) : val.join(",");
144
+ }
145
+ // ── Formatters ─────────────────────────────────────────────────────
146
+ export function formatErrorMessage(error) {
147
+ return error instanceof Error ? error.message : String(error);
148
+ }
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerFactoryTool(server: McpServer): void;
@@ -0,0 +1,110 @@
1
+ import { z } from "zod/v4";
2
+ import { OPTION_CATALOG } from "../constants.js";
3
+ import { sanitizeToolName, deriveServerName, deriveToolName, serializeArrayEnv, } from "../lib.js";
4
+ function buildEnvFromParams(params) {
5
+ const env = {};
6
+ for (const opt of OPTION_CATALOG) {
7
+ const val = params[opt.key];
8
+ if (val === undefined || val === null)
9
+ continue;
10
+ if (Array.isArray(val)) {
11
+ if (val.length > 0)
12
+ env[opt.envVar] = serializeArrayEnv(val);
13
+ }
14
+ else if (typeof val === "object") {
15
+ env[opt.envVar] = JSON.stringify(val);
16
+ }
17
+ else if (typeof val === "boolean") {
18
+ env[opt.envVar] = String(val);
19
+ }
20
+ else if (val !== "") {
21
+ env[opt.envVar] = String(val);
22
+ }
23
+ }
24
+ return env;
25
+ }
26
+ export function registerFactoryTool(server) {
27
+ server.registerTool("create_codex_mcp", {
28
+ description: [
29
+ "Generate a .mcp.json config entry for a new Codex Octopus MCP server instance.",
30
+ "WHEN TO USE: user says 'codex octopus agent', 'codex octopus mcp',",
31
+ "'new codex octopus', 'add codex octopus', 'create codex octopus',",
32
+ "'codex octopus instance', 'codex octopus config', 'codex octopus server',",
33
+ "or any phrase combining 'codex octopus' with agent/mcp/new/add/create/config/server/setup.",
34
+ "This is a wizard: only a description is required.",
35
+ "Returns a ready-to-use .mcp.json config and lists all customization options.",
36
+ "Call again with more parameters to refine.",
37
+ ].join(" "),
38
+ inputSchema: z.object({
39
+ description: z.string().describe("What this agent should do, in plain language. Example: 'a code reviewer with read-only sandbox'"),
40
+ name: z.string().optional().describe("Server name / alias (derived from description if omitted)"),
41
+ toolName: z.string().optional().describe("Custom tool name prefix"),
42
+ model: z.string().optional(),
43
+ instructions: z.string().optional(),
44
+ appendInstructions: z.string().optional(),
45
+ sandboxMode: z.enum(["read-only", "workspace-write", "danger-full-access"]).optional(),
46
+ approvalPolicy: z.enum(["never", "on-request", "on-failure", "untrusted"]).optional(),
47
+ cwd: z.string().optional(),
48
+ additionalDirs: z.array(z.string()).optional(),
49
+ effort: z.enum(["minimal", "low", "medium", "high", "xhigh"]).optional(),
50
+ networkAccess: z.boolean().optional(),
51
+ webSearchMode: z.enum(["disabled", "cached", "live"]).optional(),
52
+ persistSession: z.boolean().optional(),
53
+ apiKey: z.string().optional().describe("OpenAI API key for this agent (leave unset to inherit)"),
54
+ }),
55
+ }, async (params) => {
56
+ const { description, name: nameParam, toolName: toolNameParam } = params;
57
+ const name = nameParam || deriveServerName(description);
58
+ const derivedToolName = toolNameParam
59
+ ? sanitizeToolName(toolNameParam)
60
+ : deriveToolName(name);
61
+ const env = {
62
+ CODEX_TOOL_NAME: derivedToolName,
63
+ CODEX_SERVER_NAME: name,
64
+ CODEX_DESCRIPTION: description,
65
+ };
66
+ const optionEnv = buildEnvFromParams(params);
67
+ Object.assign(env, optionEnv);
68
+ const configured = Object.keys(optionEnv);
69
+ const notConfigured = OPTION_CATALOG.filter((o) => !configured.includes(o.envVar));
70
+ const SENSITIVE_KEYS = new Set(["CODEX_API_KEY"]);
71
+ const displayEnv = {};
72
+ for (const [k, v] of Object.entries(env)) {
73
+ displayEnv[k] = SENSITIVE_KEYS.has(k) ? "<REDACTED>" : v;
74
+ }
75
+ const mcpEntry = {
76
+ [name]: {
77
+ command: "npx",
78
+ args: ["codex-octopus"],
79
+ env: displayEnv,
80
+ },
81
+ };
82
+ const sections = [];
83
+ sections.push("## Generated config", "", "Add to `mcpServers` in your `.mcp.json`:", "", "```json", JSON.stringify(mcpEntry, null, 2), "```");
84
+ sections.push("", "## What's configured");
85
+ sections.push("", `| Setting | Value |`, `|---|---|`);
86
+ sections.push(`| Name | \`${name}\` |`);
87
+ sections.push(`| Tool names | \`${derivedToolName}\`, \`${derivedToolName}_reply\` |`);
88
+ for (const key of configured) {
89
+ const opt = OPTION_CATALOG.find((o) => o.envVar === key);
90
+ if (opt) {
91
+ const val = SENSITIVE_KEYS.has(key) ? "<REDACTED>" : env[key];
92
+ sections.push(`| ${opt.label} | \`${val}\` |`);
93
+ }
94
+ }
95
+ if (configured.length === 0) {
96
+ sections.push(`| _(all defaults)_ | — |`);
97
+ }
98
+ if (notConfigured.length > 0) {
99
+ sections.push("", "## Available customizations", "", "These options are not yet set. Ask the user if they'd like to configure any:", "");
100
+ for (const opt of notConfigured) {
101
+ sections.push(`- **${opt.label}** — ${opt.hint}`);
102
+ sections.push(` Example: ${opt.example}`);
103
+ }
104
+ sections.push("", "_Call this tool again with additional parameters to refine the config._");
105
+ }
106
+ return {
107
+ content: [{ type: "text", text: sections.join("\n") }],
108
+ };
109
+ });
110
+ }
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { ThreadConfig } from "../types.js";
3
+ export declare function registerQueryTools(server: McpServer, baseConfig: ThreadConfig, toolName: string, toolDescription: string, apiKey?: string): void;
@@ -0,0 +1,167 @@
1
+ import { z } from "zod/v4";
2
+ import { resolve } from "node:path";
3
+ import { Codex } from "@openai/codex-sdk";
4
+ import { narrowSandboxMode, narrowApprovalPolicy, formatErrorMessage, } from "../lib.js";
5
+ async function runQuery(prompt, overrides, baseConfig, apiKey) {
6
+ const codex = new Codex({
7
+ ...(apiKey ? { apiKey } : {}),
8
+ });
9
+ const threadOptions = {};
10
+ // Model
11
+ const model = overrides.model || baseConfig.model;
12
+ if (model)
13
+ threadOptions.model = model;
14
+ // Working directory — accept any path, preserve agent's base access
15
+ let cwd = baseConfig.cwd || process.cwd();
16
+ if (overrides.cwd) {
17
+ const resolvedCwd = resolve(cwd, overrides.cwd);
18
+ if (resolvedCwd !== cwd) {
19
+ const dirs = new Set(baseConfig.additionalDirectories || []);
20
+ dirs.add(cwd);
21
+ threadOptions.additionalDirectories = [...dirs];
22
+ cwd = resolvedCwd;
23
+ }
24
+ }
25
+ threadOptions.workingDirectory = cwd;
26
+ // Per-invocation additionalDirs — unions with server-level + auto-added dirs
27
+ if (overrides.additionalDirs?.length) {
28
+ const existing = threadOptions.additionalDirectories || baseConfig.additionalDirectories || [];
29
+ const dirs = new Set(existing);
30
+ for (const dir of overrides.additionalDirs) {
31
+ dirs.add(dir);
32
+ }
33
+ threadOptions.additionalDirectories = [...dirs];
34
+ }
35
+ else if (baseConfig.additionalDirectories && !threadOptions.additionalDirectories) {
36
+ threadOptions.additionalDirectories = baseConfig.additionalDirectories;
37
+ }
38
+ // Sandbox mode — can only tighten
39
+ const baseSandbox = baseConfig.sandboxMode || "read-only";
40
+ if (overrides.sandboxMode) {
41
+ threadOptions.sandboxMode = narrowSandboxMode(baseSandbox, overrides.sandboxMode);
42
+ }
43
+ else {
44
+ threadOptions.sandboxMode = baseSandbox;
45
+ }
46
+ // Approval policy — can only tighten
47
+ const baseApproval = baseConfig.approvalPolicy || "on-failure";
48
+ if (overrides.approvalPolicy) {
49
+ threadOptions.approvalPolicy = narrowApprovalPolicy(baseApproval, overrides.approvalPolicy);
50
+ }
51
+ else {
52
+ threadOptions.approvalPolicy = baseApproval;
53
+ }
54
+ // Effort
55
+ const effort = overrides.effort || baseConfig.effort;
56
+ if (effort)
57
+ threadOptions.modelReasoningEffort = effort;
58
+ // Network access
59
+ if (overrides.networkAccess !== undefined) {
60
+ threadOptions.networkAccessEnabled = overrides.networkAccess;
61
+ }
62
+ else if (baseConfig.networkAccess !== undefined) {
63
+ threadOptions.networkAccessEnabled = baseConfig.networkAccess;
64
+ }
65
+ // Web search
66
+ const webSearch = overrides.webSearchMode || baseConfig.webSearchMode;
67
+ if (webSearch)
68
+ threadOptions.webSearchMode = webSearch;
69
+ // Start or resume thread
70
+ const thread = overrides.resumeThreadId
71
+ ? codex.resumeThread(overrides.resumeThreadId, threadOptions)
72
+ : codex.startThread(threadOptions);
73
+ // Build prompt with instructions
74
+ let fullPrompt = prompt;
75
+ if (!overrides.resumeThreadId) {
76
+ const instructions = overrides.instructions || baseConfig.instructions;
77
+ const appendInstructions = baseConfig.appendInstructions;
78
+ if (instructions) {
79
+ fullPrompt = `${instructions}\n\n${prompt}`;
80
+ }
81
+ else if (appendInstructions) {
82
+ fullPrompt = `${appendInstructions}\n\n${prompt}`;
83
+ }
84
+ }
85
+ const turn = await thread.run(fullPrompt);
86
+ return {
87
+ threadId: thread.id || "",
88
+ response: turn.finalResponse || "",
89
+ usage: turn.usage || { input_tokens: 0, cached_input_tokens: 0, output_tokens: 0 },
90
+ isError: false,
91
+ };
92
+ }
93
+ function formatResult(result) {
94
+ const payload = {
95
+ thread_id: result.threadId,
96
+ result: result.response,
97
+ usage: result.usage,
98
+ is_error: result.isError,
99
+ };
100
+ return {
101
+ content: [
102
+ { type: "text", text: JSON.stringify(payload, null, 2) },
103
+ ],
104
+ isError: result.isError,
105
+ };
106
+ }
107
+ function formatError(error) {
108
+ return {
109
+ content: [
110
+ { type: "text", text: `Error: ${formatErrorMessage(error)}` },
111
+ ],
112
+ isError: true,
113
+ };
114
+ }
115
+ export function registerQueryTools(server, baseConfig, toolName, toolDescription, apiKey) {
116
+ const replyToolName = `${toolName}_reply`;
117
+ server.registerTool(toolName, {
118
+ description: toolDescription,
119
+ inputSchema: z.object({
120
+ prompt: z.string().describe("Task or question for Codex"),
121
+ cwd: z.string().optional().describe("Working directory (overrides CODEX_CWD)"),
122
+ model: z.string().optional().describe('Model override (e.g. "gpt-5-codex", "o3", "codex-1")'),
123
+ additionalDirs: z.array(z.string()).optional().describe("Extra directories the agent can access for this invocation"),
124
+ effort: z.enum(["minimal", "low", "medium", "high", "xhigh"]).optional().describe("Reasoning effort override"),
125
+ sandboxMode: z.enum(["read-only", "workspace-write"]).optional().describe("Sandbox mode override (can only tighten, never loosen)"),
126
+ approvalPolicy: z.enum(["on-failure", "on-request", "untrusted"]).optional().describe("Approval policy override (can only tighten, never loosen)"),
127
+ networkAccess: z.boolean().optional().describe("Enable network access from sandbox"),
128
+ webSearchMode: z.enum(["disabled", "cached", "live"]).optional().describe("Web search mode"),
129
+ instructions: z.string().optional().describe("Additional instructions (prepended to prompt)"),
130
+ }),
131
+ }, async ({ prompt, cwd, model, additionalDirs, effort, sandboxMode, approvalPolicy, networkAccess, webSearchMode, instructions }) => {
132
+ try {
133
+ const result = await runQuery(prompt, {
134
+ cwd, model, additionalDirs, effort, sandboxMode, approvalPolicy, networkAccess, webSearchMode, instructions,
135
+ }, baseConfig, apiKey);
136
+ return formatResult(result);
137
+ }
138
+ catch (error) {
139
+ return formatError(error);
140
+ }
141
+ });
142
+ if (baseConfig.persistSession !== false) {
143
+ server.registerTool(replyToolName, {
144
+ description: [
145
+ `Continue a previous ${toolName} conversation by thread ID.`,
146
+ "Use this for follow-up questions, iterative refinement,",
147
+ "or multi-step workflows that build on prior context.",
148
+ ].join(" "),
149
+ inputSchema: z.object({
150
+ thread_id: z.string().describe(`Thread ID from a prior ${toolName} response`),
151
+ prompt: z.string().describe("Follow-up instruction or question"),
152
+ cwd: z.string().optional().describe("Working directory override"),
153
+ model: z.string().optional().describe("Model override"),
154
+ }),
155
+ }, async ({ thread_id, prompt, cwd, model }) => {
156
+ try {
157
+ const result = await runQuery(prompt, {
158
+ cwd, model, resumeThreadId: thread_id,
159
+ }, baseConfig, apiKey);
160
+ return formatResult(result);
161
+ }
162
+ catch (error) {
163
+ return formatError(error);
164
+ }
165
+ });
166
+ }
167
+ }
@@ -0,0 +1,32 @@
1
+ export interface ThreadConfig {
2
+ cwd?: string;
3
+ model?: string;
4
+ sandboxMode?: "read-only" | "workspace-write" | "danger-full-access";
5
+ approvalPolicy?: "never" | "on-request" | "on-failure" | "untrusted";
6
+ effort?: "minimal" | "low" | "medium" | "high" | "xhigh";
7
+ additionalDirectories?: string[];
8
+ networkAccess?: boolean;
9
+ webSearchMode?: "disabled" | "cached" | "live";
10
+ persistSession?: boolean;
11
+ instructions?: string;
12
+ appendInstructions?: string;
13
+ }
14
+ export interface InvocationOverrides {
15
+ cwd?: string;
16
+ model?: string;
17
+ sandboxMode?: string;
18
+ approvalPolicy?: string;
19
+ effort?: string;
20
+ additionalDirs?: string[];
21
+ networkAccess?: boolean;
22
+ webSearchMode?: string;
23
+ instructions?: string;
24
+ resumeThreadId?: string;
25
+ }
26
+ export interface OptionCatalogEntry {
27
+ key: string;
28
+ envVar: string;
29
+ label: string;
30
+ hint: string;
31
+ example: string;
32
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "codex-octopus",
3
+ "version": "1.0.0",
4
+ "description": "One brain, many arms — spawn multiple specialized Codex agents as MCP servers",
5
+ "type": "module",
6
+ "bin": {
7
+ "codex-octopus": "dist/index.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "start": "node dist/index.js",
12
+ "dev": "tsc --watch",
13
+ "test": "vitest run",
14
+ "test:watch": "vitest",
15
+ "test:coverage": "vitest run --coverage"
16
+ },
17
+ "dependencies": {
18
+ "@openai/codex-sdk": "^0.118.0",
19
+ "@modelcontextprotocol/sdk": "^1.29.0",
20
+ "zod": "^4.3.6"
21
+ },
22
+ "devDependencies": {
23
+ "@types/node": "^22.0.0",
24
+ "@vitest/coverage-v8": "^4.1.2",
25
+ "typescript": "^5.8.0",
26
+ "vitest": "^4.1.2"
27
+ },
28
+ "license": "ISC",
29
+ "engines": {
30
+ "node": ">=18.0.0"
31
+ }
32
+ }
package/src/config.ts ADDED
@@ -0,0 +1,67 @@
1
+ import type { ThreadConfig } from "./types.js";
2
+ import {
3
+ envStr,
4
+ envList,
5
+ envBool,
6
+ validateSandboxMode,
7
+ validateApprovalPolicy,
8
+ validateEffort,
9
+ } from "./lib.js";
10
+
11
+ export function buildBaseConfig(): ThreadConfig {
12
+ const config: ThreadConfig = {
13
+ cwd: envStr("CODEX_CWD") || process.cwd(),
14
+ persistSession: envBool("CODEX_PERSIST_SESSION", true),
15
+ };
16
+
17
+ const model = envStr("CODEX_MODEL");
18
+ if (model) config.model = model;
19
+
20
+ const rawSandbox = envStr("CODEX_SANDBOX_MODE") || "read-only";
21
+ const sandbox = validateSandboxMode(rawSandbox);
22
+ if (rawSandbox !== sandbox) {
23
+ console.error(
24
+ `codex-octopus: invalid CODEX_SANDBOX_MODE "${rawSandbox}", using "read-only"`
25
+ );
26
+ }
27
+ config.sandboxMode = sandbox as ThreadConfig["sandboxMode"];
28
+
29
+ const rawApproval = envStr("CODEX_APPROVAL_POLICY") || "on-failure";
30
+ const approval = validateApprovalPolicy(rawApproval);
31
+ if (rawApproval !== approval) {
32
+ console.error(
33
+ `codex-octopus: invalid CODEX_APPROVAL_POLICY "${rawApproval}", using "on-failure"`
34
+ );
35
+ }
36
+ config.approvalPolicy = approval as ThreadConfig["approvalPolicy"];
37
+
38
+ const effort = envStr("CODEX_EFFORT");
39
+ if (effort) {
40
+ const validated = validateEffort(effort);
41
+ if (effort !== validated) {
42
+ console.error(
43
+ `codex-octopus: invalid CODEX_EFFORT "${effort}", using "medium"`
44
+ );
45
+ }
46
+ config.effort = validated as ThreadConfig["effort"];
47
+ }
48
+
49
+ const dirs = envList("CODEX_ADDITIONAL_DIRS");
50
+ if (dirs) config.additionalDirectories = dirs;
51
+
52
+ const networkAccess = envStr("CODEX_NETWORK_ACCESS");
53
+ if (networkAccess !== undefined) {
54
+ config.networkAccess = networkAccess === "true" || networkAccess === "1";
55
+ }
56
+
57
+ const webSearch = envStr("CODEX_WEB_SEARCH");
58
+ if (webSearch) config.webSearchMode = webSearch as ThreadConfig["webSearchMode"];
59
+
60
+ const instructions = envStr("CODEX_INSTRUCTIONS");
61
+ if (instructions) config.instructions = instructions;
62
+
63
+ const appendInstructions = envStr("CODEX_APPEND_INSTRUCTIONS");
64
+ if (appendInstructions) config.appendInstructions = appendInstructions;
65
+
66
+ return config;
67
+ }
@@ -0,0 +1,88 @@
1
+ import type { OptionCatalogEntry } from "./types.js";
2
+
3
+ export const OPTION_CATALOG: OptionCatalogEntry[] = [
4
+ {
5
+ key: "model",
6
+ envVar: "CODEX_MODEL",
7
+ label: "Model",
8
+ hint: "Which model to use",
9
+ example: '"gpt-5-codex", "o3", "codex-1"',
10
+ },
11
+ {
12
+ key: "appendInstructions",
13
+ envVar: "CODEX_APPEND_INSTRUCTIONS",
14
+ label: "Behavior instructions",
15
+ hint: "Instructions appended to the default system prompt",
16
+ example: '"Always explain your reasoning step by step."',
17
+ },
18
+ {
19
+ key: "instructions",
20
+ envVar: "CODEX_INSTRUCTIONS",
21
+ label: "Full instructions",
22
+ hint: "Completely replaces the default instructions",
23
+ example: '"You are a security auditor..."',
24
+ },
25
+ {
26
+ key: "sandboxMode",
27
+ envVar: "CODEX_SANDBOX_MODE",
28
+ label: "Sandbox mode",
29
+ hint: "How file system access is restricted",
30
+ example: '"read-only", "workspace-write", "danger-full-access"',
31
+ },
32
+ {
33
+ key: "approvalPolicy",
34
+ envVar: "CODEX_APPROVAL_POLICY",
35
+ label: "Approval policy",
36
+ hint: "When to prompt for command approval",
37
+ example: '"never" (auto-approve), "on-failure", "on-request", "untrusted"',
38
+ },
39
+ {
40
+ key: "cwd",
41
+ envVar: "CODEX_CWD",
42
+ label: "Working directory",
43
+ hint: "Default directory the agent operates in",
44
+ example: '"/home/user/project"',
45
+ },
46
+ {
47
+ key: "additionalDirs",
48
+ envVar: "CODEX_ADDITIONAL_DIRS",
49
+ label: "Additional directories",
50
+ hint: "Extra directories the agent can access (comma-separated)",
51
+ example: '"/shared/libs,/data"',
52
+ },
53
+ {
54
+ key: "effort",
55
+ envVar: "CODEX_EFFORT",
56
+ label: "Reasoning effort",
57
+ hint: "How hard the agent thinks",
58
+ example: '"minimal" (fastest), "low", "medium", "high", "xhigh" (deepest)',
59
+ },
60
+ {
61
+ key: "networkAccess",
62
+ envVar: "CODEX_NETWORK_ACCESS",
63
+ label: "Network access",
64
+ hint: "Allow network access from sandbox",
65
+ example: '"true" or "false" (default)',
66
+ },
67
+ {
68
+ key: "webSearchMode",
69
+ envVar: "CODEX_WEB_SEARCH",
70
+ label: "Web search",
71
+ hint: "Web search mode",
72
+ example: '"disabled" (default), "cached", "live"',
73
+ },
74
+ {
75
+ key: "persistSession",
76
+ envVar: "CODEX_PERSIST_SESSION",
77
+ label: "Persist sessions",
78
+ hint: "Save sessions for later resume via _reply tool",
79
+ example: '"true" (default) or "false"',
80
+ },
81
+ {
82
+ key: "apiKey",
83
+ envVar: "CODEX_API_KEY",
84
+ label: "API key",
85
+ hint: "OpenAI API key for this agent (overrides inherited auth)",
86
+ example: '"sk-..." — leave unset to inherit from parent',
87
+ },
88
+ ];