@inetafrica/open-claudia 2.14.8 → 3.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/.env.example +30 -4
- package/CHANGELOG.md +21 -0
- package/README.md +87 -66
- package/bin/agent.js +44 -18
- package/bin/cli.js +30 -28
- package/bin/dream.js +5 -11
- package/bin/loopback-client.js +29 -5
- package/bin/schedule.js +19 -5
- package/bin/tool.js +23 -5
- package/bot-agent.js +5 -2350
- package/bot.js +105 -7
- package/channels/telegram/adapter.js +6 -1
- package/channels/voice/adapter.js +1 -0
- package/channels/voice/manage.js +43 -5
- package/config-dir.js +3 -11
- package/core/actions.js +149 -40
- package/core/approvals.js +136 -29
- package/core/auth-flow.js +103 -19
- package/core/config-dir.js +19 -0
- package/core/config.js +127 -69
- package/core/doctor.js +111 -57
- package/core/dream.js +219 -62
- package/core/enforcer.js +0 -0
- package/core/entities.js +2 -1
- package/core/fsutil.js +55 -0
- package/core/handlers.js +503 -217
- package/core/ideas.js +2 -1
- package/core/identity.js +8 -11
- package/core/io.js +24 -3
- package/core/jobs.js +589 -72
- package/core/lessons.js +3 -2
- package/core/loopback.js +45 -8
- package/core/memory-mutation-queue.js +241 -0
- package/core/migration-backup.js +1060 -0
- package/core/pack-review.js +295 -88
- package/core/packs.js +4 -3
- package/core/persona.js +2 -1
- package/core/process-tree.js +19 -2
- package/core/provider-migration.js +39 -0
- package/core/provider-status.js +80 -0
- package/core/providers/claude-events.js +117 -0
- package/core/providers/claude-hook.js +114 -0
- package/core/providers/claude.js +296 -0
- package/core/providers/codex-events.js +121 -0
- package/core/providers/codex-hook.js +280 -0
- package/core/providers/codex.js +286 -0
- package/core/providers/contract.js +153 -0
- package/core/providers/env.js +148 -0
- package/core/providers/events.js +170 -0
- package/core/providers/hook-command.js +22 -0
- package/core/providers/index.js +240 -0
- package/core/providers/utility-policy.js +269 -0
- package/core/recall/classic.js +2 -2
- package/core/recall/discoverer.js +71 -22
- package/core/recall/metrics.js +27 -1
- package/core/recall/tuning.js +4 -1
- package/core/recall/warm-walker.js +151 -108
- package/core/recall-filter.js +86 -61
- package/core/router.js +79 -7
- package/core/run-context.js +185 -0
- package/core/runner.js +1415 -1282
- package/core/scheduler.js +515 -210
- package/core/side-chat.js +346 -0
- package/core/state.js +1096 -95
- package/core/subagent.js +72 -98
- package/core/system-prompt.js +462 -279
- package/core/tool-guard.js +73 -39
- package/core/tools.js +22 -5
- package/core/transcripts.js +30 -1
- package/core/usage-log.js +19 -4
- package/core/utility-agent.js +654 -0
- package/core/web-auth.js +29 -13
- package/core/web-sessions.js +78 -0
- package/docs/CHANNEL_DESIGN.md +181 -0
- package/docs/MULTI_CHANNEL_PLAN.md +254 -0
- package/docs/PROVIDER_MIGRATION.md +67 -0
- package/health.js +50 -39
- package/package.json +51 -4
- package/setup.js +198 -38
- package/soul.md +39 -0
- package/test-ability-extraction.js +5 -0
- package/test-approval-async.js +63 -4
- package/test-cursor-removal.js +334 -0
- package/test-delivery-contract.js +88 -0
- package/test-enforcer.js +70 -27
- package/test-fixtures/current-provider-parity.json +132 -0
- package/test-fixtures/fake-agent-cli.js +477 -0
- package/test-fixtures/migrations/claude-only/jobs.json +3 -0
- package/test-fixtures/migrations/claude-only/sessions.json +5 -0
- package/test-fixtures/migrations/claude-only/state.json +5 -0
- package/test-fixtures/migrations/cursor-selected/crons.json +3 -0
- package/test-fixtures/migrations/cursor-selected/jobs.json +3 -0
- package/test-fixtures/migrations/cursor-selected/sessions.json +5 -0
- package/test-fixtures/migrations/cursor-selected/state.json +6 -0
- package/test-fixtures/migrations/dual-provider/jobs.json +3 -0
- package/test-fixtures/migrations/dual-provider/sessions.json +7 -0
- package/test-fixtures/migrations/dual-provider/state.json +10 -0
- package/test-fixtures/migrations/malformed-partial/jobs.json +1 -0
- package/test-fixtures/migrations/malformed-partial/sessions.json +1 -0
- package/test-fixtures/migrations/malformed-partial/sessions.json.bak +3 -0
- package/test-fixtures/migrations/malformed-partial/state.json +1 -0
- package/test-fixtures/migrations/malformed-partial/state.json.bak +5 -0
- package/test-fixtures/migrations/missing-project/jobs.json +3 -0
- package/test-fixtures/migrations/missing-project/sessions.json +3 -0
- package/test-fixtures/migrations/missing-project/state.json +4 -0
- package/test-fixtures/migrations/multi-user/jobs.json +4 -0
- package/test-fixtures/migrations/multi-user/sessions.json +4 -0
- package/test-fixtures/migrations/multi-user/state.json +6 -0
- package/test-fixtures/provider-event-samples.json +17 -0
- package/test-learning-e2e.js +5 -0
- package/test-memory-mutation-queue.js +191 -0
- package/test-provider-boot-matrix.js +399 -0
- package/test-provider-bootstrap.js +158 -0
- package/test-provider-capabilities.js +232 -0
- package/test-provider-claude.js +206 -0
- package/test-provider-codex.js +228 -0
- package/test-provider-compaction.js +371 -0
- package/test-provider-core-prompt.js +264 -0
- package/test-provider-coupling-audit.js +90 -0
- package/test-provider-dream.js +312 -0
- package/test-provider-enforcer.js +252 -0
- package/test-provider-env-isolation.js +150 -0
- package/test-provider-events.js +141 -0
- package/test-provider-fixture.js +332 -0
- package/test-provider-gateway.js +508 -0
- package/test-provider-language.js +89 -0
- package/test-provider-migration-backup.js +424 -0
- package/test-provider-pack-review.js +436 -0
- package/test-provider-parity-e2e.js +537 -0
- package/test-provider-prompt-parity.js +89 -0
- package/test-provider-recall.js +271 -0
- package/test-provider-registry.js +251 -0
- package/test-provider-resume-parity.js +41 -0
- package/test-provider-run-lifecycle.js +349 -0
- package/test-provider-runner.js +271 -0
- package/test-provider-scheduler.js +689 -0
- package/test-provider-session-commands.js +185 -0
- package/test-provider-session-history.js +205 -0
- package/test-provider-side-chat.js +337 -0
- package/test-provider-state-migration.js +316 -0
- package/test-provider-stream-decoder.js +69 -0
- package/test-provider-subagent.js +228 -0
- package/test-provider-tool-hooks.js +360 -0
- package/test-recall-discoverer.js +1 -0
- package/test-recall-engine.js +3 -3
- package/test-recall-evolution.js +18 -0
- package/test-run-lock.js +63 -0
- package/test-runner-watchdog-static.js +16 -8
- package/test-single-runtime.js +35 -0
- package/test-telegram-poll-recovery.js +56 -0
- package/test-tool-guard.js +56 -0
- package/test-tools.js +19 -0
- package/test-unified-identity.js +3 -1
- package/test-usage-accounting.js +92 -20
- package/test-utility-provider-policy.js +486 -0
- package/test-web-sessions.js +74 -0
- package/web.js +159 -45
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const crypto = require("crypto");
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const { spawnSync } = require("child_process");
|
|
7
|
+
const { buildDenyGateCommand, shellQuote } = require("./hook-command");
|
|
8
|
+
|
|
9
|
+
const HOOK_TIMEOUT_SECONDS = 10;
|
|
10
|
+
const REQUIRED_EXEC_FLAGS = Object.freeze([
|
|
11
|
+
"--config",
|
|
12
|
+
"--strict-config",
|
|
13
|
+
"--ignore-user-config",
|
|
14
|
+
"--dangerously-bypass-hook-trust",
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
let compatibilityCaches = new WeakMap();
|
|
18
|
+
|
|
19
|
+
function hookError(code, message, details = {}) {
|
|
20
|
+
const error = new Error(message);
|
|
21
|
+
error.name = "ToolHookInitializationError";
|
|
22
|
+
error.code = code;
|
|
23
|
+
error.details = details;
|
|
24
|
+
return error;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function tomlString(value) {
|
|
28
|
+
return JSON.stringify(String(value));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function normalizedCwd(cwd) {
|
|
32
|
+
return path.resolve(String(cwd || process.cwd()));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function buildCodexHookOverrides({ cwd, nodePath, cliPath } = {}) {
|
|
36
|
+
const command = buildDenyGateCommand({ nodePath, cliPath });
|
|
37
|
+
const project = normalizedCwd(cwd);
|
|
38
|
+
return Object.freeze({
|
|
39
|
+
featuresHooks: "features.hooks=true",
|
|
40
|
+
featuresPlugins: "features.plugins=false",
|
|
41
|
+
featuresUnifiedExec: "features.unified_exec=false",
|
|
42
|
+
// A path is a dynamic TOML map key. Current Codex `-c` parsing rejects a
|
|
43
|
+
// quoted path inside dotted-key notation, so replace the isolated projects
|
|
44
|
+
// table with one explicit inline table (user config is ignored below).
|
|
45
|
+
projectTrust: `projects={${tomlString(project)}={ trust_level=${tomlString("untrusted")} }}`,
|
|
46
|
+
preToolUse: "hooks.PreToolUse=[{ matcher = \"^Bash$\", hooks = [{ type = \"command\", "
|
|
47
|
+
+ `command = ${tomlString(command)}, timeout = ${HOOK_TIMEOUT_SECONDS}, statusMessage = \"Checking Open Claudia tool policy\" }] }]`,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function hookArgs(overrides) {
|
|
52
|
+
return [
|
|
53
|
+
"--ignore-user-config",
|
|
54
|
+
"--strict-config",
|
|
55
|
+
"-c", overrides.featuresHooks,
|
|
56
|
+
"-c", overrides.featuresPlugins,
|
|
57
|
+
"-c", overrides.featuresUnifiedExec,
|
|
58
|
+
"-c", overrides.projectTrust,
|
|
59
|
+
"-c", overrides.preToolUse,
|
|
60
|
+
"--dangerously-bypass-hook-trust",
|
|
61
|
+
];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function fingerprint(args) {
|
|
65
|
+
return crypto.createHash("sha256").update(JSON.stringify(args)).digest("hex");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function prepareCodexHook(options = {}) {
|
|
69
|
+
const cwd = normalizedCwd(options.cwd);
|
|
70
|
+
const overrides = buildCodexHookOverrides({ ...options, cwd });
|
|
71
|
+
const args = Object.freeze(hookArgs(overrides));
|
|
72
|
+
return Object.freeze({
|
|
73
|
+
providerId: "codex",
|
|
74
|
+
cwd,
|
|
75
|
+
overrides,
|
|
76
|
+
args,
|
|
77
|
+
fingerprint: fingerprint(args),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function verifyPreparedCodexHook(prepared) {
|
|
82
|
+
if (!prepared || prepared.providerId !== "codex" || !Array.isArray(prepared.args)) return false;
|
|
83
|
+
let expected;
|
|
84
|
+
try { expected = prepareCodexHook({ cwd: prepared.cwd }); }
|
|
85
|
+
catch (_) { return false; }
|
|
86
|
+
return prepared.fingerprint === expected.fingerprint
|
|
87
|
+
&& prepared.args.length === expected.args.length
|
|
88
|
+
&& prepared.args.every((value, index) => value === expected.args[index]);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function commandResult(runner, binary, args, options) {
|
|
92
|
+
try {
|
|
93
|
+
const result = runner(binary, args, options);
|
|
94
|
+
return result && typeof result === "object"
|
|
95
|
+
? result
|
|
96
|
+
: { ok: false, output: "", code: 1 };
|
|
97
|
+
} catch (error) {
|
|
98
|
+
return { ok: false, output: "", code: 1, error };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function featureState(output, name, expected) {
|
|
103
|
+
const line = String(output || "").split(/\r?\n/).find((entry) => entry.trim().startsWith(`${name} `));
|
|
104
|
+
if (!line) return false;
|
|
105
|
+
return line.trim().split(/\s+/).at(-1) === String(expected);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function compatibilityCache(runner) {
|
|
109
|
+
let cache = compatibilityCaches.get(runner);
|
|
110
|
+
if (!cache) {
|
|
111
|
+
cache = new Map();
|
|
112
|
+
compatibilityCaches.set(runner, cache);
|
|
113
|
+
}
|
|
114
|
+
return cache;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function checkCodexHookCompatibility({ binary, runCommand, env, cwd, prepared = prepareCodexHook({ cwd }), cache = true } = {}) {
|
|
118
|
+
if (typeof binary !== "string" || !binary || typeof runCommand !== "function") {
|
|
119
|
+
return { ok: false, code: "MANDATORY_TOOL_HOOK_UNAVAILABLE", reason: "Codex hook compatibility could not be probed" };
|
|
120
|
+
}
|
|
121
|
+
if (!verifyPreparedCodexHook(prepared)) {
|
|
122
|
+
return { ok: false, code: "TOOL_HOOK_CONFIG_UNVERIFIED", reason: "Open Claudia's generated Codex hook configuration failed verification" };
|
|
123
|
+
}
|
|
124
|
+
const key = `${binary}\0${prepared.fingerprint}`;
|
|
125
|
+
const values = compatibilityCache(runCommand);
|
|
126
|
+
if (cache && values.has(key)) return values.get(key);
|
|
127
|
+
|
|
128
|
+
const opts = { env, cwd: prepared.cwd, timeout: 10000 };
|
|
129
|
+
const execHelp = commandResult(runCommand, binary, ["exec", "--help"], opts);
|
|
130
|
+
const resumeHelp = commandResult(runCommand, binary, ["exec", "resume", "--help"], opts);
|
|
131
|
+
const missing = [];
|
|
132
|
+
for (const flag of REQUIRED_EXEC_FLAGS) {
|
|
133
|
+
if (!execHelp.ok || !String(execHelp.output || "").includes(flag)) missing.push(`exec ${flag}`);
|
|
134
|
+
if (!resumeHelp.ok || !String(resumeHelp.output || "").includes(flag)) missing.push(`resume ${flag}`);
|
|
135
|
+
}
|
|
136
|
+
let result;
|
|
137
|
+
if (missing.length) {
|
|
138
|
+
result = {
|
|
139
|
+
ok: false,
|
|
140
|
+
code: "MANDATORY_TOOL_HOOK_UNAVAILABLE",
|
|
141
|
+
reason: `Codex CLI is missing mandatory tool-hook options: ${missing.join(", ")}`,
|
|
142
|
+
};
|
|
143
|
+
} else {
|
|
144
|
+
const o = prepared.overrides;
|
|
145
|
+
const features = commandResult(runCommand, binary, [
|
|
146
|
+
"features",
|
|
147
|
+
"-c", o.featuresHooks,
|
|
148
|
+
"-c", o.featuresPlugins,
|
|
149
|
+
"-c", o.featuresUnifiedExec,
|
|
150
|
+
"-c", o.projectTrust,
|
|
151
|
+
"-c", o.preToolUse,
|
|
152
|
+
"list",
|
|
153
|
+
], opts);
|
|
154
|
+
const configured = features.ok
|
|
155
|
+
&& featureState(features.output, "hooks", true)
|
|
156
|
+
&& featureState(features.output, "plugins", false)
|
|
157
|
+
&& featureState(features.output, "unified_exec", false);
|
|
158
|
+
result = configured
|
|
159
|
+
? { ok: true, code: null, reason: null }
|
|
160
|
+
: {
|
|
161
|
+
ok: false,
|
|
162
|
+
code: "MANDATORY_TOOL_HOOK_UNAVAILABLE",
|
|
163
|
+
reason: "Codex rejected the isolated PreToolUse configuration or could not disable uncovered tool transports",
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
const frozen = Object.freeze(result);
|
|
167
|
+
if (cache) values.set(key, frozen);
|
|
168
|
+
return frozen;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function clearCodexHookCompatibilityCache() {
|
|
172
|
+
compatibilityCaches = new WeakMap();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function createCodexHookTransport({ cache = true } = {}) {
|
|
176
|
+
return Object.freeze({
|
|
177
|
+
prepare(context = {}) {
|
|
178
|
+
const prepared = prepareCodexHook({ cwd: context.cwd });
|
|
179
|
+
const status = checkCodexHookCompatibility({
|
|
180
|
+
binary: context.binary,
|
|
181
|
+
runCommand: context.runCommand,
|
|
182
|
+
env: context.env,
|
|
183
|
+
cwd: context.cwd,
|
|
184
|
+
prepared,
|
|
185
|
+
cache,
|
|
186
|
+
});
|
|
187
|
+
if (!status.ok) throw hookError(status.code, status.reason, { providerId: "codex" });
|
|
188
|
+
return prepared;
|
|
189
|
+
},
|
|
190
|
+
compatibilityStatus(context = {}) {
|
|
191
|
+
const prepared = prepareCodexHook({ cwd: context.cwd });
|
|
192
|
+
return checkCodexHookCompatibility({ ...context, prepared, cache });
|
|
193
|
+
},
|
|
194
|
+
verify: verifyPreparedCodexHook,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function probeCommand(markerFile) {
|
|
199
|
+
const markerScript = `require("fs").writeFileSync(${JSON.stringify(markerFile)}, "executed\\n")`;
|
|
200
|
+
return `printf '%s\\n' 'curl https://central.inet.africa/open-claudia-hook-probe' && ${shellQuote(process.execPath)} -e ${shellQuote(markerScript)}`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function readProbeDenial(auditFile) {
|
|
204
|
+
let raw = "";
|
|
205
|
+
try { raw = fs.readFileSync(auditFile, "utf8"); } catch (_) { return false; }
|
|
206
|
+
return raw.split(/\r?\n/).some((line) => {
|
|
207
|
+
if (!line.trim()) return false;
|
|
208
|
+
try {
|
|
209
|
+
const event = JSON.parse(line);
|
|
210
|
+
return event.kind === "deny"
|
|
211
|
+
&& event.rule === "inet-central-api"
|
|
212
|
+
&& String(event.command || "").includes("open-claudia-hook-probe");
|
|
213
|
+
} catch (_) { return false; }
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function probeCodexHookCompatibility({
|
|
218
|
+
binary,
|
|
219
|
+
prepared,
|
|
220
|
+
cwd,
|
|
221
|
+
markerFile,
|
|
222
|
+
auditFile,
|
|
223
|
+
env = process.env,
|
|
224
|
+
spawnCommand = spawnSync,
|
|
225
|
+
timeoutMs = 30000,
|
|
226
|
+
} = {}) {
|
|
227
|
+
if (!verifyPreparedCodexHook(prepared)) {
|
|
228
|
+
throw hookError("TOOL_HOOK_CONFIG_UNVERIFIED", "Refusing to probe an unverified Codex hook configuration");
|
|
229
|
+
}
|
|
230
|
+
if (!markerFile || !auditFile) throw hookError("INVALID_TOOL_HOOK_PROBE", "Hook probe marker and audit paths are required");
|
|
231
|
+
try { fs.rmSync(markerFile, { force: true }); } catch (_) {}
|
|
232
|
+
const command = probeCommand(markerFile);
|
|
233
|
+
const args = [
|
|
234
|
+
"exec",
|
|
235
|
+
...prepared.args,
|
|
236
|
+
"--json",
|
|
237
|
+
"--skip-git-repo-check",
|
|
238
|
+
"--ephemeral",
|
|
239
|
+
"--dangerously-bypass-approvals-and-sandbox",
|
|
240
|
+
`Use the Bash tool once to run this exact harmless compatibility command, then stop:\n${command}`,
|
|
241
|
+
];
|
|
242
|
+
const result = spawnCommand(binary, args, {
|
|
243
|
+
cwd: normalizedCwd(cwd || prepared.cwd),
|
|
244
|
+
env: {
|
|
245
|
+
...env,
|
|
246
|
+
FAKE_AGENT_SCENARIO: env.FAKE_AGENT_SCENARIO || "hook-probe",
|
|
247
|
+
OPEN_CLAUDIA_HOOK_PROBE_COMMAND: command,
|
|
248
|
+
OPEN_CLAUDIA_HOOK_PROBE_MARKER: markerFile,
|
|
249
|
+
TOOL_GUARD_LOG: auditFile,
|
|
250
|
+
},
|
|
251
|
+
input: "",
|
|
252
|
+
encoding: "utf8",
|
|
253
|
+
timeout: timeoutMs,
|
|
254
|
+
});
|
|
255
|
+
const markerExecuted = fs.existsSync(markerFile);
|
|
256
|
+
const denyObserved = readProbeDenial(auditFile);
|
|
257
|
+
const status = Number.isInteger(result?.status) ? result.status : 1;
|
|
258
|
+
const ok = status === 0 && denyObserved && !markerExecuted;
|
|
259
|
+
return {
|
|
260
|
+
ok,
|
|
261
|
+
status,
|
|
262
|
+
denyObserved,
|
|
263
|
+
markerExecuted,
|
|
264
|
+
diagnostic: ok
|
|
265
|
+
? null
|
|
266
|
+
: `Codex tool-hook probe failed (status=${status}, denyObserved=${denyObserved}, markerExecuted=${markerExecuted})`,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
module.exports = {
|
|
271
|
+
HOOK_TIMEOUT_SECONDS,
|
|
272
|
+
REQUIRED_EXEC_FLAGS,
|
|
273
|
+
buildCodexHookOverrides,
|
|
274
|
+
checkCodexHookCompatibility,
|
|
275
|
+
clearCodexHookCompatibilityCache,
|
|
276
|
+
createCodexHookTransport,
|
|
277
|
+
prepareCodexHook,
|
|
278
|
+
probeCodexHookCompatibility,
|
|
279
|
+
verifyPreparedCodexHook,
|
|
280
|
+
};
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const config = require("../config");
|
|
4
|
+
const auth = require("../auth-flow");
|
|
5
|
+
const { createJsonlDecoder } = require("./events");
|
|
6
|
+
const { createCodexParserState, normalizeCodexEvent } = require("./codex-events");
|
|
7
|
+
const { createCodexHookTransport } = require("./codex-hook");
|
|
8
|
+
|
|
9
|
+
const CODEX_MODELS = Object.freeze(["gpt-5.5", "gpt-5.4", "gpt-5.4-mini", "gpt-5.3-codex"]);
|
|
10
|
+
const CODEX_EFFORT_VALUES = Object.freeze(["minimal", "low", "medium", "high", "xhigh"]);
|
|
11
|
+
const REQUIRED_EXEC_HELP_FLAGS = Object.freeze(["--config", "--json", "--sandbox", "--image", "--model", "--skip-git-repo-check"]);
|
|
12
|
+
const REQUIRED_RESUME_HELP_FLAGS = Object.freeze(["--config", "--json", "--image", "--model", "--skip-git-repo-check"]);
|
|
13
|
+
|
|
14
|
+
const capabilities = Object.freeze({
|
|
15
|
+
resume: { support: "native", reason: null },
|
|
16
|
+
readOnlyMode: { support: "native", reason: null },
|
|
17
|
+
effort: { support: "native", values: [...CODEX_EFFORT_VALUES], reason: null },
|
|
18
|
+
budget: { support: "unsupported", reason: "Codex exec has no per-run monetary budget flag" },
|
|
19
|
+
worktree: { support: "unsupported", reason: "Codex exec does not create provider-managed worktrees" },
|
|
20
|
+
partialStreaming: { support: "unsupported", reason: "Codex JSONL does not expose the Claude-style partial message stream" },
|
|
21
|
+
maxTurns: { support: "unsupported", reason: "Codex exec has no max-turns option" },
|
|
22
|
+
imageFlag: { support: "native", reason: null },
|
|
23
|
+
persistentUtilityStream: { support: "unsupported", reason: "Codex utility calls run as bounded ephemeral exec processes" },
|
|
24
|
+
preToolHook: { support: "native", reason: null },
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
function providerError(code, message, details = {}) {
|
|
28
|
+
const error = new Error(message);
|
|
29
|
+
error.name = "ProviderInvocationError";
|
|
30
|
+
error.code = code;
|
|
31
|
+
error.details = details;
|
|
32
|
+
return error;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function settingsOf(context) {
|
|
36
|
+
return context.providerSettings || context.settings || {};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function preparedPrompt(context) {
|
|
40
|
+
const userPrompt = typeof context.userPrompt === "string"
|
|
41
|
+
? context.userPrompt
|
|
42
|
+
: (typeof context.prompt === "string" ? context.prompt : null);
|
|
43
|
+
if (userPrompt === null) throw providerError("INVALID_PROVIDER_CONTEXT", "userPrompt must be a string");
|
|
44
|
+
const dynamic = typeof context.dynamicContext === "string" ? context.dynamicContext : "";
|
|
45
|
+
return dynamic ? `${dynamic}\n\n${userPrompt}` : userPrompt;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function coreInstructionsOf(context) {
|
|
49
|
+
if (typeof context.coreInstructions !== "string" || !context.coreInstructions.trim()) {
|
|
50
|
+
throw providerError("MISSING_CORE_INSTRUCTIONS", "coreInstructions are required");
|
|
51
|
+
}
|
|
52
|
+
return context.coreInstructions;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function sessionFor(context) {
|
|
56
|
+
if (context.fresh) return null;
|
|
57
|
+
if (context.resumeSessionId) return String(context.resumeSessionId);
|
|
58
|
+
if (context.continueSession) {
|
|
59
|
+
if (!context.sessionId) throw providerError("INVALID_PROVIDER_CONTEXT", "continueSession requires a captured Codex sessionId");
|
|
60
|
+
return String(context.sessionId);
|
|
61
|
+
}
|
|
62
|
+
return context.sessionId ? String(context.sessionId) : null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function imagePathsOf(context) {
|
|
66
|
+
const paths = context.imagePaths || context.images || [];
|
|
67
|
+
if (!Array.isArray(paths) || paths.some((item) => typeof item !== "string" || !item.trim())) {
|
|
68
|
+
throw providerError("INVALID_PROVIDER_CONTEXT", "imagePaths must be an array of non-empty strings");
|
|
69
|
+
}
|
|
70
|
+
return paths;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function createCodexProvider(options = {}) {
|
|
74
|
+
const resolveExecutable = options.resolveExecutable || (() => config.discoverProviderExecutable("codex"));
|
|
75
|
+
const defaultModel = options.defaultModel ?? config.config.CODEX_MODEL ?? null;
|
|
76
|
+
const buildEnv = options.buildEnv || auth.codexSubprocessEnv;
|
|
77
|
+
const getAuthStatus = options.getAuthStatus || ((binary) => auth.codexProviderAuthStatus({ binary }));
|
|
78
|
+
const runCommand = options.runCommand || auth.runCommand;
|
|
79
|
+
// Factories are safe by default. `false` is reserved for hermetic argv tests
|
|
80
|
+
// that do not launch a real provider process.
|
|
81
|
+
const hookTransport = options.hookTransport === false
|
|
82
|
+
? null
|
|
83
|
+
: (options.hookTransport || createCodexHookTransport());
|
|
84
|
+
|
|
85
|
+
function executable() {
|
|
86
|
+
return resolveExecutable() || null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function requireExecutable() {
|
|
90
|
+
const binary = executable();
|
|
91
|
+
if (!binary) throw providerError("NO_AVAILABLE_PROVIDER", "OpenAI Codex is not configured or installed", { providerId: "codex" });
|
|
92
|
+
return binary;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function assertSupportedContext(context) {
|
|
96
|
+
const settings = settingsOf(context);
|
|
97
|
+
const budget = context.budget ?? settings.budget ?? null;
|
|
98
|
+
if (budget !== null) throw providerError("UNSUPPORTED_PROVIDER_SETTING", capabilities.budget.reason, { setting: "budget" });
|
|
99
|
+
if (context.worktree ?? settings.worktree ?? false) {
|
|
100
|
+
throw providerError("UNSUPPORTED_PROVIDER_SETTING", capabilities.worktree.reason, { setting: "worktree" });
|
|
101
|
+
}
|
|
102
|
+
if (context.maxTurns !== undefined && context.maxTurns !== null) {
|
|
103
|
+
throw providerError("UNSUPPORTED_PROVIDER_SETTING", capabilities.maxTurns.reason, { setting: "maxTurns" });
|
|
104
|
+
}
|
|
105
|
+
if (context.voiceStream) {
|
|
106
|
+
throw providerError("UNSUPPORTED_PROVIDER_CAPABILITY", capabilities.partialStreaming.reason, { capability: "partialStreaming" });
|
|
107
|
+
}
|
|
108
|
+
if (context.toolHookSettings) {
|
|
109
|
+
throw providerError("INVALID_PROVIDER_CONTEXT", "Codex tool hooks must use Open Claudia's generated transient configuration", { capability: "preToolHook" });
|
|
110
|
+
}
|
|
111
|
+
if (context.allowedTools !== undefined || context.disallowedTools !== undefined) {
|
|
112
|
+
throw providerError("UNSUPPORTED_PROVIDER_CAPABILITY", "Codex tool allow/deny lists are not available through this adapter", { capability: "toolLists" });
|
|
113
|
+
}
|
|
114
|
+
if (context.outputSchema && !context.outputSchemaPath) {
|
|
115
|
+
throw providerError("INVALID_PROVIDER_CONTEXT", "Codex outputSchema requires a prepared outputSchemaPath");
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function buildInvocation(context, utility) {
|
|
120
|
+
if (!context || typeof context !== "object") throw providerError("INVALID_PROVIDER_CONTEXT", "provider context must be an object");
|
|
121
|
+
assertSupportedContext(context);
|
|
122
|
+
const coreInstructions = coreInstructionsOf(context);
|
|
123
|
+
const resumeId = utility ? null : sessionFor(context);
|
|
124
|
+
const settings = settingsOf(context);
|
|
125
|
+
const binary = requireExecutable();
|
|
126
|
+
const env = { ...buildEnv() };
|
|
127
|
+
const readOnlyModes = new Set(["plan", "ask"]);
|
|
128
|
+
const readOnly = !!context.readOnly
|
|
129
|
+
|| readOnlyModes.has(settings.permissionMode)
|
|
130
|
+
|| readOnlyModes.has(context.permissionMode);
|
|
131
|
+
let preparedHook = null;
|
|
132
|
+
let warnings = null;
|
|
133
|
+
if (hookTransport) {
|
|
134
|
+
try {
|
|
135
|
+
preparedHook = hookTransport.prepare({
|
|
136
|
+
binary,
|
|
137
|
+
env,
|
|
138
|
+
cwd: context.cwd || process.cwd(),
|
|
139
|
+
runCommand,
|
|
140
|
+
readOnly,
|
|
141
|
+
});
|
|
142
|
+
if (!hookTransport.verify(preparedHook)) {
|
|
143
|
+
throw providerError("TOOL_HOOK_CONFIG_UNVERIFIED", "Open Claudia's generated Codex hook configuration failed verification");
|
|
144
|
+
}
|
|
145
|
+
} catch (error) {
|
|
146
|
+
if (readOnly && context.allowReadOnlyWithoutToolHook === true) {
|
|
147
|
+
warnings = [{
|
|
148
|
+
code: "READ_ONLY_TOOL_HOOK_UNAVAILABLE",
|
|
149
|
+
message: "Codex is continuing in read-only sandbox mode because the mandatory tool hook is unavailable; unrestricted execution remains blocked.",
|
|
150
|
+
}];
|
|
151
|
+
} else {
|
|
152
|
+
throw providerError(
|
|
153
|
+
"MANDATORY_TOOL_HOOK_UNAVAILABLE",
|
|
154
|
+
error?.message || "Codex's mandatory tool hook could not be initialized",
|
|
155
|
+
{ providerId: "codex", causeCode: error?.code || null },
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const args = ["exec"];
|
|
161
|
+
if (resumeId) args.push("resume");
|
|
162
|
+
if (preparedHook) args.push(...preparedHook.args);
|
|
163
|
+
|
|
164
|
+
args.push("-c", `developer_instructions=${JSON.stringify(coreInstructions)}`);
|
|
165
|
+
|
|
166
|
+
const effort = context.effort || settings.effort || null;
|
|
167
|
+
if (effort) {
|
|
168
|
+
if (!CODEX_EFFORT_VALUES.includes(effort)) {
|
|
169
|
+
throw providerError("UNSUPPORTED_PROVIDER_SETTING", `Unsupported Codex effort: ${effort}`, { setting: "effort", value: effort });
|
|
170
|
+
}
|
|
171
|
+
args.push("-c", `model_reasoning_effort=${JSON.stringify(effort)}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (resumeId && readOnly) args.push("-c", `sandbox_mode=${JSON.stringify("read-only")}`);
|
|
175
|
+
|
|
176
|
+
const imagePaths = imagePathsOf(context);
|
|
177
|
+
if (imagePaths.length) {
|
|
178
|
+
if (resumeId) {
|
|
179
|
+
for (const imagePath of imagePaths) args.push("--image", imagePath);
|
|
180
|
+
} else {
|
|
181
|
+
args.push("--image", ...imagePaths);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
args.push("--json", "--skip-git-repo-check");
|
|
186
|
+
if (!resumeId && readOnly) args.push("--sandbox", "read-only");
|
|
187
|
+
else if (!readOnly) args.push("--dangerously-bypass-approvals-and-sandbox");
|
|
188
|
+
|
|
189
|
+
const model = context.model || settings.model || (utility && context.tier ? provider.tierModel(context.tier) : defaultModel);
|
|
190
|
+
if (model) args.push("--model", String(model));
|
|
191
|
+
if (context.outputSchemaPath) args.push("--output-schema", String(context.outputSchemaPath));
|
|
192
|
+
if (utility) args.push("--ephemeral");
|
|
193
|
+
|
|
194
|
+
if (resumeId) args.push(resumeId);
|
|
195
|
+
const prompt = preparedPrompt(context);
|
|
196
|
+
// `codex exec -` consumes the prompt from stdin. Utility payloads often
|
|
197
|
+
// contain private memory or guardrail mandates and must not be exposed in
|
|
198
|
+
// argv/process listings.
|
|
199
|
+
args.push(utility ? "-" : prompt);
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
binary,
|
|
203
|
+
args,
|
|
204
|
+
env,
|
|
205
|
+
stdin: utility ? prompt : null,
|
|
206
|
+
parserState: createCodexParserState(),
|
|
207
|
+
...(warnings ? { warnings } : {}),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const provider = {
|
|
212
|
+
id: "codex",
|
|
213
|
+
label: "OpenAI Codex",
|
|
214
|
+
capabilities,
|
|
215
|
+
|
|
216
|
+
isAvailable: () => !!executable(),
|
|
217
|
+
executable,
|
|
218
|
+
defaultModel: () => defaultModel,
|
|
219
|
+
modelChoices: () => [...CODEX_MODELS],
|
|
220
|
+
|
|
221
|
+
compatibilityStatus() {
|
|
222
|
+
const binary = executable();
|
|
223
|
+
if (!binary) return { state: "not_checked", version: null, reason: "Codex CLI not found" };
|
|
224
|
+
const versionResult = runCommand(binary, ["--version"], { env: buildEnv(), timeout: 10000 });
|
|
225
|
+
if (!versionResult.ok) return { state: "unknown", version: null, reason: "Codex version probe failed" };
|
|
226
|
+
const version = (String(versionResult.output).match(/\b(\d+\.\d+\.\d+)\b/) || [null, null])[1];
|
|
227
|
+
const execHelp = runCommand(binary, ["exec", "--help"], { env: buildEnv(), timeout: 10000 });
|
|
228
|
+
const resumeHelp = runCommand(binary, ["exec", "resume", "--help"], { env: buildEnv(), timeout: 10000 });
|
|
229
|
+
if (!execHelp.ok || !resumeHelp.ok) return { state: "unknown", version, reason: "Codex compatibility probe failed" };
|
|
230
|
+
const missing = [
|
|
231
|
+
...REQUIRED_EXEC_HELP_FLAGS.filter((flag) => !String(execHelp.output).includes(flag)).map((flag) => `exec ${flag}`),
|
|
232
|
+
...REQUIRED_RESUME_HELP_FLAGS.filter((flag) => !String(resumeHelp.output).includes(flag)).map((flag) => `resume ${flag}`),
|
|
233
|
+
];
|
|
234
|
+
if (missing.length) return { state: "incompatible", version, reason: `Codex CLI is missing required options: ${missing.join(", ")}` };
|
|
235
|
+
if (hookTransport) {
|
|
236
|
+
const hookStatus = hookTransport.compatibilityStatus({ binary, runCommand, env: buildEnv(), cwd: process.cwd() });
|
|
237
|
+
if (!hookStatus.ok) return { state: "incompatible", version, reason: hookStatus.reason };
|
|
238
|
+
}
|
|
239
|
+
return { state: "compatible", version, reason: null };
|
|
240
|
+
},
|
|
241
|
+
|
|
242
|
+
buildMainInvocation: (context) => buildInvocation(context, false),
|
|
243
|
+
buildCompactionInvocation(context) {
|
|
244
|
+
if (!context?.fresh || !context.readOnly || !context.outputSchemaPath || context.maxTurns != null) {
|
|
245
|
+
throw providerError("INVALID_PROVIDER_CONTEXT", "Codex compaction seed requires a fresh read-only output contract without maxTurns");
|
|
246
|
+
}
|
|
247
|
+
// Deliberately persistent: buildUtilityInvocation adds --ephemeral, which
|
|
248
|
+
// would make the successful seed impossible to resume.
|
|
249
|
+
return buildInvocation({ ...context, permissionMode: "plan" }, false);
|
|
250
|
+
},
|
|
251
|
+
buildUtilityInvocation: (context) => buildInvocation({ ...context, fresh: true }, true),
|
|
252
|
+
createEventDecoder: (decoderOptions) => createJsonlDecoder(decoderOptions),
|
|
253
|
+
normalizeEvent: normalizeCodexEvent,
|
|
254
|
+
authStatus: () => getAuthStatus(executable()),
|
|
255
|
+
authHelp: () => auth.codexAuthHelp(),
|
|
256
|
+
preflightAuth() {
|
|
257
|
+
const status = provider.authStatus();
|
|
258
|
+
if (status.state !== "unauthenticated") return null;
|
|
259
|
+
return {
|
|
260
|
+
code: "PROVIDER_UNAUTHENTICATED",
|
|
261
|
+
providerId: "codex",
|
|
262
|
+
message: auth.codexAuthHelp(status.reason || "Codex authentication failed"),
|
|
263
|
+
};
|
|
264
|
+
},
|
|
265
|
+
nativeToolsNote: "Codex exposes its native exec tools; Open Claudia capability checks and approval policy remain authoritative.",
|
|
266
|
+
tierModel(tier) {
|
|
267
|
+
if (tier === "low") return "gpt-5.4-mini";
|
|
268
|
+
if (tier === "medium") return "gpt-5.3-codex";
|
|
269
|
+
return defaultModel || "gpt-5.5";
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
return provider;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const codexProvider = createCodexProvider();
|
|
277
|
+
|
|
278
|
+
module.exports = {
|
|
279
|
+
CODEX_EFFORT_VALUES,
|
|
280
|
+
CODEX_MODELS,
|
|
281
|
+
REQUIRED_EXEC_HELP_FLAGS,
|
|
282
|
+
REQUIRED_RESUME_HELP_FLAGS,
|
|
283
|
+
capabilities,
|
|
284
|
+
codexProvider,
|
|
285
|
+
createCodexProvider,
|
|
286
|
+
};
|