@skelm/codex 0.3.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Scott Glover
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,131 @@
1
+ # @skelm/codex
2
+
3
+ > OpenAI Codex backend for [skelm](https://github.com/scottgl9/skelm) — wraps the official [`@openai/codex-sdk`](https://github.com/openai/codex/blob/main/sdk/typescript/README.md) with full skelm permission enforcement, MCP injection, skill loading, and streaming.
4
+
5
+ [![npm](https://img.shields.io/npm/v/@skelm/codex)](https://www.npmjs.com/package/@skelm/codex)
6
+
7
+ Part of [skelm](https://github.com/scottgl9/skelm).
8
+
9
+ Codex authenticates via the host `codex` CLI (`codex login`) or the `CODEX_API_KEY` env var. The SDK spawns codex under the hood and exchanges JSONL events — skelm enforces permissions at the boundary, pins the workspace, optionally routes egress through the gateway's CONNECT proxy, and emits audit events as Codex completes commands, file changes, and MCP tool calls.
10
+
11
+ | Capability | Value |
12
+ |--------------------|----------------------------------------------------------------|
13
+ | `prompt` | `false` (codex-sdk is agent-loop only) |
14
+ | `streaming` | `true` (`agent_message` text deltas flow to `onPartial`) |
15
+ | `sessionLifecycle` | `true` (`request.sessionId` → `Codex.resumeThread`) |
16
+ | `mcp` | `true` (skelm MCP servers injected via `config.mcp_servers`) |
17
+ | `skills` | `true` (skill bodies concatenated into the system prompt) |
18
+ | `toolPermissions` | `'native'` (Codex enforces sandbox / approval / network in its own process; skelm checks at the boundary) |
19
+
20
+ ## Prerequisites
21
+
22
+ - `codex` CLI on PATH (`codex --version` ≥ 0.130.0)
23
+ - Authenticated session — `codex login` once, or `CODEX_API_KEY` in env
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ npm install @skelm/codex
29
+ ```
30
+
31
+ ## Quick start
32
+
33
+ ```ts
34
+ // skelm.config.ts
35
+ import { defineConfig } from 'skelm'
36
+
37
+ export default defineConfig({
38
+ backends: {
39
+ agent: 'codex',
40
+ codex: {
41
+ model: 'gpt-5.3-codex',
42
+ modelReasoningEffort: 'medium',
43
+ skipGitRepoCheck: true,
44
+ },
45
+ },
46
+ defaults: {
47
+ permissions: {
48
+ networkEgress: 'deny',
49
+ allowedTools: [],
50
+ allowedExecutables: [],
51
+ allowedSkills: [],
52
+ allowedMcpServers: [],
53
+ fsRead: [],
54
+ fsWrite: [],
55
+ },
56
+ },
57
+ })
58
+ ```
59
+
60
+ ```ts
61
+ // codex-smoke.pipeline.ts
62
+ import { agent, pipeline } from 'skelm'
63
+ import { z } from 'zod'
64
+
65
+ export default pipeline({
66
+ id: 'codex-smoke',
67
+ input: z.object({ task: z.string() }),
68
+ output: z.object({ result: z.string() }),
69
+ steps: [
70
+ agent({
71
+ id: 'work',
72
+ backend: 'codex',
73
+ prompt: (ctx) => (ctx.input as { task: string }).task,
74
+ permissions: {
75
+ // For a real read-write workflow grant fsWrite roots + relevant tools.
76
+ // The mapper refuses fsWrite: ['*'] unless approval policy is empty.
77
+ fsRead: [],
78
+ fsWrite: [],
79
+ networkEgress: 'deny',
80
+ },
81
+ }),
82
+ ],
83
+ })
84
+ ```
85
+
86
+ ```bash
87
+ skelm run codex-smoke.pipeline.ts --input '{"task":"say ok"}'
88
+ ```
89
+
90
+ ## Permission mapping
91
+
92
+ The boundary-time mapper translates a resolved skelm policy into Codex SDK options. If the policy can't be honored safely, it throws `CodexPermissionError` before any Codex invocation.
93
+
94
+ | Skelm policy | Codex SDK option |
95
+ |---------------------------------------------|---------------------------------------------------------------|
96
+ | `fsWrite: []`, `fsRead: []` | `sandboxMode: 'read-only'` |
97
+ | `fsWrite: [<roots>]` | `sandboxMode: 'workspace-write'`, first root → `workingDirectory`, rest → `additionalDirectories` |
98
+ | `request.cwd` set | overrides `workingDirectory` |
99
+ | `fsWrite: ['*']` AND no approval policy | `sandboxMode: 'danger-full-access'` |
100
+ | `fsWrite: ['*']` AND approval policy set | **refused** — never silently escalate |
101
+ | `networkEgress: 'deny'` | `networkAccessEnabled: false` |
102
+ | `networkEgress: 'allow'` or `{ allowHosts }` | `networkAccessEnabled: true` (gateway proxy enforces hosts) |
103
+ | `approval.on` covers `tool` / `executable` | `approvalPolicy: 'untrusted'` |
104
+ | anything else | `approvalPolicy: 'on-request'` |
105
+
106
+ ## MCP, skills, streaming
107
+
108
+ - **MCP** — `request.mcpServers` is filtered by `policy.allowedMcpServers`, then passed to `Codex({ config: { mcp_servers: { … } } })`. Stdio transports are translated today; HTTP/SSE transports are dropped with `permission.denied` audit so the gap is visible.
109
+ - **Skills** — When `request.skills` is set and the policy permits, the backend calls `context.loadSkill(id)` for each id and concatenates the formatted skill blocks into the system prompt.
110
+ - **Streaming** — `agent_message.text` deltas flow to `BackendContext.onPartial`. `command_execution`, `file_change`, and `mcp_tool_call` items surface via `onItem` for audit emission.
111
+
112
+ ## API surface
113
+
114
+ - `createCodexBackend(options?: CodexBackendOptions): SkelmBackend` — the factory.
115
+ - `mapPermissionsToCodex({ policy, workingDirectory })` — boundary mapper; throws `CodexPermissionError` on unsafe widening.
116
+ - `buildAuditEntry(...)` — hash-chained-audit-ready record of the mapping decision.
117
+ - `filterIds(ids, allowlist)` — partition step-requested ids by an allowlist.
118
+ - Re-exports: `CodexPermissionError`, types `CodexBackendOptions`, `MappedCodexPolicy`, `CodexPermissionAuditEntry`.
119
+
120
+ ## Live integration test
121
+
122
+ ```bash
123
+ codex login
124
+ SKELM_CODEX_INTEGRATION=1 pnpm test packages/codex/test/integration.test.ts
125
+ ```
126
+
127
+ The skill-injection test registers a `magic-word` skill and asserts the agent surfaces the skill-provided answer — a real end-to-end verification.
128
+
129
+ ## License
130
+
131
+ MIT
@@ -0,0 +1,21 @@
1
+ import type { SkelmBackend } from '@skelm/core';
2
+ import type { CodexBackendOptions } from './types.js';
3
+ /**
4
+ * SkelmBackend for OpenAI Codex via the official `@openai/codex-sdk`.
5
+ *
6
+ * Surfaces the full skelm feature set against Codex:
7
+ *
8
+ * - MCP servers injected through `CodexOptions.config.mcp_servers`.
9
+ * - Skills concatenated into the system prompt before each turn.
10
+ * - Permissions translated to Codex sandbox + approval modes.
11
+ * - Streaming events relayed via `BackendContext.onPartial`.
12
+ * - Session resumption via `Codex.resumeThread`.
13
+ * - Cancellation via the SDK's per-turn `signal: AbortSignal`.
14
+ *
15
+ * Permission enforcement is `'native'`: Codex enforces sandbox / approval /
16
+ * network natively in its own process. Skelm validates at the boundary
17
+ * (pre-run refusal, workspace pinning, egress proxy envelope, post-event
18
+ * audit) — never widening what the policy permits.
19
+ */
20
+ export declare function createCodexBackend(options?: CodexBackendOptions): SkelmBackend;
21
+ //# sourceMappingURL=backend.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backend.d.ts","sourceRoot":"","sources":["../src/backend.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAOV,YAAY,EACb,MAAM,aAAa,CAAA;AAUpB,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAA;AAErD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,GAAE,mBAAwB,GAAG,YAAY,CA+HlF"}
@@ -0,0 +1,208 @@
1
+ import { buildSystemPromptFromRequest, loadSkillBodies, resolvePermissions } from '@skelm/core';
2
+ import { buildCodexOptions, buildMcpServerConfig, buildThreadOptions, consumeStream, makeCodexClient, } from './client.js';
3
+ import { mapPermissionsToCodex } from './permission-mapper.js';
4
+ /**
5
+ * SkelmBackend for OpenAI Codex via the official `@openai/codex-sdk`.
6
+ *
7
+ * Surfaces the full skelm feature set against Codex:
8
+ *
9
+ * - MCP servers injected through `CodexOptions.config.mcp_servers`.
10
+ * - Skills concatenated into the system prompt before each turn.
11
+ * - Permissions translated to Codex sandbox + approval modes.
12
+ * - Streaming events relayed via `BackendContext.onPartial`.
13
+ * - Session resumption via `Codex.resumeThread`.
14
+ * - Cancellation via the SDK's per-turn `signal: AbortSignal`.
15
+ *
16
+ * Permission enforcement is `'native'`: Codex enforces sandbox / approval /
17
+ * network natively in its own process. Skelm validates at the boundary
18
+ * (pre-run refusal, workspace pinning, egress proxy envelope, post-event
19
+ * audit) — never widening what the policy permits.
20
+ */
21
+ export function createCodexBackend(options = {}) {
22
+ const capabilities = {
23
+ prompt: false,
24
+ streaming: true,
25
+ sessionLifecycle: true,
26
+ mcp: true,
27
+ skills: true,
28
+ modelSelection: options.model !== undefined,
29
+ // Codex enforces sandbox / approval / network natively in its own process.
30
+ // Skelm checks at the boundary (refusing unsafe combinations before any
31
+ // Codex call); Codex enforces at runtime.
32
+ toolPermissions: 'native',
33
+ };
34
+ const backend = {
35
+ id: options.id ?? 'codex',
36
+ capabilities,
37
+ ...(options.label !== undefined && { label: options.label }),
38
+ async run(request, context) {
39
+ // When neither the request nor the context carries a resolved policy,
40
+ // synthesize an empty-deny ResolvedPolicy from `resolvePermissions(
41
+ // undefined, undefined)`. This matches the documented default-deny
42
+ // intent of skelm: an omitted policy must deny everything, not crash
43
+ // the run. Previously we threw "codex backend requires a resolved
44
+ // permission policy" here, which forced every codex agent() step to
45
+ // restate the same empty allowlists even when config-level defaults
46
+ // were set. The mapper below still translates this into Codex's
47
+ // strictest sandbox/approval/network settings.
48
+ const policy = request.permissions ?? context.permissions ?? resolvePermissions(undefined, undefined);
49
+ // Boundary check + sandbox/approval translation. Throws on refusal.
50
+ const mapped = mapPermissionsToCodex({
51
+ policy,
52
+ ...(request.cwd !== undefined && { workingDirectory: request.cwd }),
53
+ });
54
+ // Filter requested MCP servers through the allowlist.
55
+ const allowed = filterAllowedMcp(request.mcpServers, policy.allowedMcpServers);
56
+ const mcpConfig = buildMcpServerConfig(allowed.allowed);
57
+ const deniedMcp = allowed.denied.map((s) => s.id);
58
+ // Audit-only for now; the runner's audit writer is the durable record.
59
+ const logDenial = (dimension, ids, reason) => console.warn(JSON.stringify({ event: 'permission.denied', dimension, ids, reason, backend: 'codex' }));
60
+ if (deniedMcp.length > 0) {
61
+ logDenial('mcp', deniedMcp, 'not-in-allowlist');
62
+ }
63
+ if (mcpConfig !== null && mcpConfig.dropped.length > 0) {
64
+ logDenial('mcp', mcpConfig.dropped, 'transport-unsupported');
65
+ }
66
+ // Construct the SDK client with config + proxy env. Only forward
67
+ // `mcp_servers` to Codex — never leak the `dropped` bookkeeping field.
68
+ const codexOpts = buildCodexOptions(options, {
69
+ ...(context.proxyEnv !== undefined && { env: context.proxyEnv }),
70
+ ...(mcpConfig !== null && { config: { mcp_servers: mcpConfig.mcp_servers } }),
71
+ });
72
+ const codex = makeCodexClient(codexOpts);
73
+ // Compose the system prompt via @skelm/core's shared builder so
74
+ // systemPromptMode / systemPromptIncludeAgentDef take effect here.
75
+ const systemPrompt = await composeSystemPrompt(request, context, options.model);
76
+ const userPrompt = systemPrompt === undefined ? request.prompt : `${systemPrompt}\n\n---\n\n${request.prompt}`;
77
+ // Build the thread (resume vs fresh) honoring per-step sandbox/approval.
78
+ const threadOpts = buildThreadOptions(options, {
79
+ sandboxMode: mapped.sandboxMode,
80
+ approvalPolicy: mapped.approvalPolicy,
81
+ networkAccessEnabled: mapped.networkAccessEnabled,
82
+ webSearchEnabled: mapped.webSearchEnabled,
83
+ webSearchMode: mapped.webSearchMode,
84
+ ...(mapped.workingDirectory !== undefined && {
85
+ workingDirectory: mapped.workingDirectory,
86
+ }),
87
+ ...(mapped.additionalDirectories !== undefined && {
88
+ additionalDirectories: mapped.additionalDirectories,
89
+ }),
90
+ });
91
+ const sessionId = readSessionId(request);
92
+ const thread = sessionId !== undefined
93
+ ? codex.resumeThread(sessionId, threadOpts)
94
+ : codex.startThread(threadOpts);
95
+ // Compose abort: the runner-supplied `context.signal` AND the
96
+ // backend's `timeoutMs` (defensive ceiling). The SDK honors a single
97
+ // AbortSignal on TurnOptions natively.
98
+ const turnSignal = composeAbortSignal(context.signal, options.timeoutMs ?? 300_000);
99
+ const { events } = await thread.runStreamed(userPrompt, {
100
+ ...(request.outputSchema !== undefined && { outputSchema: request.outputSchema }),
101
+ signal: turnSignal.signal,
102
+ });
103
+ let result;
104
+ try {
105
+ result = await consumeStream(events, {
106
+ ...(context.onPartial !== undefined && { onText: context.onPartial }),
107
+ });
108
+ }
109
+ finally {
110
+ turnSignal.cancel();
111
+ }
112
+ const response = {
113
+ text: result.finalText,
114
+ stopReason: result.stopReason,
115
+ };
116
+ if (result.usage !== undefined) {
117
+ response.usage = {
118
+ ...(result.usage.inputTokens !== undefined && { inputTokens: result.usage.inputTokens }),
119
+ ...(result.usage.outputTokens !== undefined && {
120
+ outputTokens: result.usage.outputTokens,
121
+ }),
122
+ ...(result.usage.reasoningTokens !== undefined && {
123
+ reasoningTokens: result.usage.reasoningTokens,
124
+ }),
125
+ };
126
+ }
127
+ return response;
128
+ },
129
+ };
130
+ return backend;
131
+ }
132
+ function filterAllowedMcp(servers, allowlist) {
133
+ if (servers === undefined || servers.length === 0)
134
+ return { allowed: [], denied: [] };
135
+ const allowed = [];
136
+ const denied = [];
137
+ for (const s of servers) {
138
+ if (allowlist.has(s.id))
139
+ allowed.push(s);
140
+ else
141
+ denied.push(s);
142
+ }
143
+ return { allowed, denied };
144
+ }
145
+ async function composeSystemPrompt(req, ctx, model) {
146
+ // Route through @skelm/core's shared builder so `systemPromptMode` and
147
+ // `systemPromptIncludeAgentDef` actually take effect on codex. Without
148
+ // this, codex previously hand-rolled the prompt out of soul +
149
+ // instructions + system + skills, ignoring both flags — the same input
150
+ // produced identical token counts in extend vs replace mode.
151
+ //
152
+ // The builder owns the "extend" vs "replace" composition and the
153
+ // "include AGENTS.md/SOUL.md when replacing" carve-out. We pass an
154
+ // empty tool list because codex enforces its tool surface natively and
155
+ // skelm doesn't dispatch tools through this backend — there's no
156
+ // skelm-side tool inventory worth injecting.
157
+ const base = buildSystemPromptFromRequest(req, {
158
+ cwd: req.cwd ?? process.cwd(),
159
+ platform: process.platform,
160
+ date: new Date().toISOString().slice(0, 10),
161
+ ...(model !== undefined && { model }),
162
+ tools: [],
163
+ });
164
+ const skillBodies = await loadSkillBodies(req, ctx);
165
+ const parts = [];
166
+ if (base.length > 0)
167
+ parts.push(base);
168
+ parts.push(...skillBodies);
169
+ if (parts.length === 0)
170
+ return undefined;
171
+ return parts.join('\n\n---\n\n');
172
+ }
173
+ /**
174
+ * AgentRequest doesn't have a typed sessionId field at the moment, but
175
+ * runners may attach one through structural typing. Read defensively.
176
+ * TODO(@skelm/core): promote `sessionId?: string` to AgentRequest so this
177
+ * cast goes away.
178
+ */
179
+ function readSessionId(request) {
180
+ const sid = request.sessionId;
181
+ return typeof sid === 'string' && sid.length > 0 ? sid : undefined;
182
+ }
183
+ /**
184
+ * Compose the runner's signal with a backend-side timeout. The returned
185
+ * signal aborts when either fires; `cancel()` clears the timer so a
186
+ * successful run doesn't leak it.
187
+ */
188
+ function composeAbortSignal(upstream, timeoutMs) {
189
+ const controller = new AbortController();
190
+ if (upstream.aborted)
191
+ controller.abort(upstream.reason);
192
+ const onAbort = () => controller.abort(upstream.reason);
193
+ upstream.addEventListener('abort', onAbort, { once: true });
194
+ const timer = setTimeout(() => {
195
+ controller.abort(new Error(`codex backend timed out after ${timeoutMs}ms`));
196
+ }, timeoutMs);
197
+ // Don't keep the event loop alive solely for this timer.
198
+ if (typeof timer === 'object' && 'unref' in timer)
199
+ timer.unref();
200
+ return {
201
+ signal: controller.signal,
202
+ cancel: () => {
203
+ clearTimeout(timer);
204
+ upstream.removeEventListener('abort', onAbort);
205
+ },
206
+ };
207
+ }
208
+ //# sourceMappingURL=backend.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backend.js","sourceRoot":"","sources":["../src/backend.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Thin wrapper around `@openai/codex-sdk`'s `Codex` and `Thread`.
3
+ *
4
+ * - Owns a memoized `Codex` instance keyed by the constructed options. The
5
+ * SDK spawns the `codex` CLI per turn, but we want each backend instance
6
+ * to share auth/env/config across calls.
7
+ * - Translates `Codex` JSONL events into a normalized shape the backend
8
+ * can publish onto skelm's event bus and `onPartial` callback.
9
+ */
10
+ import { Codex, type CodexOptions, type ThreadEvent, type ThreadOptions } from '@openai/codex-sdk';
11
+ import type { CodexBackendOptions } from './types.js';
12
+ /** Build CodexOptions from CodexBackendOptions + per-run overrides. */
13
+ export declare function buildCodexOptions(opts: CodexBackendOptions, overrides?: {
14
+ env?: Record<string, string>;
15
+ config?: Record<string, unknown>;
16
+ }): CodexOptions;
17
+ /** Build ThreadOptions for `Codex.startThread`. */
18
+ export declare function buildThreadOptions(opts: CodexBackendOptions, extras: Partial<ThreadOptions>): ThreadOptions;
19
+ /**
20
+ * Translate a skelm `McpServerConfig` array into the JS shape that the
21
+ * Codex SDK's `config.mcp_servers` accepts. Today Codex's TOML config only
22
+ * supports stdio MCP servers via `command` + `args` + `env`; HTTP/SSE
23
+ * transports are dropped with a warning so the caller can audit the gap.
24
+ *
25
+ * The caller is expected to have already filtered by `policy.allowedMcpServers`.
26
+ */
27
+ export declare function buildMcpServerConfig(servers: ReadonlyArray<{
28
+ id: string;
29
+ transport: 'stdio' | 'http' | 'sse';
30
+ command?: string;
31
+ args?: readonly string[];
32
+ env?: Readonly<Record<string, string>>;
33
+ url?: string;
34
+ }>): {
35
+ mcp_servers: Record<string, unknown>;
36
+ dropped: string[];
37
+ } | null;
38
+ /**
39
+ * Iterate the Codex event stream and dispatch normalized events. The
40
+ * caller decides how to surface each event (onPartial, audit emission,
41
+ * etc.). Returns the aggregated final assistant text and usage.
42
+ */
43
+ export interface IterateResult {
44
+ finalText: string;
45
+ usage: {
46
+ inputTokens?: number;
47
+ outputTokens?: number;
48
+ reasoningTokens?: number;
49
+ } | undefined;
50
+ stopReason: string;
51
+ }
52
+ export declare function consumeStream(events: AsyncIterable<ThreadEvent>, callbacks: {
53
+ onText?: (delta: string) => void;
54
+ onItem?: (event: Extract<ThreadEvent, {
55
+ type: 'item.completed';
56
+ }>) => void;
57
+ onError?: (message: string) => void;
58
+ }): Promise<IterateResult>;
59
+ /** Construct a `Codex` instance. Exposed for testing. */
60
+ export declare function makeCodexClient(opts: CodexOptions): Codex;
61
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,KAAK,EAAE,KAAK,YAAY,EAAE,KAAK,WAAW,EAAE,KAAK,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAElG,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAA;AAErD,uEAAuE;AACvE,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,mBAAmB,EACzB,SAAS,GAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAO,GACjF,YAAY,CAuCd;AAED,mDAAmD;AACnD,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,mBAAmB,EACzB,MAAM,EAAE,OAAO,CAAC,aAAa,CAAC,GAC7B,aAAa,CAQf;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,aAAa,CAAC;IACrB,EAAE,EAAE,MAAM,CAAA;IACV,SAAS,EAAE,OAAO,GAAG,MAAM,GAAG,KAAK,CAAA;IACnC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IACxB,GAAG,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IACtC,GAAG,CAAC,EAAE,MAAM,CAAA;CACb,CAAC,GACD;IAAE,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG,IAAI,CAmBpE;AAED;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAC;QAAC,eAAe,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAA;IAC5F,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,wBAAsB,aAAa,CACjC,MAAM,EAAE,aAAa,CAAC,WAAW,CAAC,EAClC,SAAS,EAAE;IACT,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IAChC,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,WAAW,EAAE;QAAE,IAAI,EAAE,gBAAgB,CAAA;KAAE,CAAC,KAAK,IAAI,CAAA;IAC1E,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CACpC,GACA,OAAO,CAAC,aAAa,CAAC,CA6CxB;AAED,yDAAyD;AACzD,wBAAgB,eAAe,CAAC,IAAI,EAAE,YAAY,GAAG,KAAK,CAEzD"}
package/dist/client.js ADDED
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Thin wrapper around `@openai/codex-sdk`'s `Codex` and `Thread`.
3
+ *
4
+ * - Owns a memoized `Codex` instance keyed by the constructed options. The
5
+ * SDK spawns the `codex` CLI per turn, but we want each backend instance
6
+ * to share auth/env/config across calls.
7
+ * - Translates `Codex` JSONL events into a normalized shape the backend
8
+ * can publish onto skelm's event bus and `onPartial` callback.
9
+ */
10
+ import { Codex } from '@openai/codex-sdk';
11
+ /** Build CodexOptions from CodexBackendOptions + per-run overrides. */
12
+ export function buildCodexOptions(opts, overrides = {}) {
13
+ const out = {};
14
+ if (opts.codexPathOverride !== undefined)
15
+ out.codexPathOverride = opts.codexPathOverride;
16
+ if (opts.baseUrl !== undefined)
17
+ out.baseUrl = opts.baseUrl;
18
+ if (opts.apiKey !== undefined)
19
+ out.apiKey = opts.apiKey;
20
+ // env: merge skelm proxyEnv with the user's static env. The SDK does NOT
21
+ // inherit process.env when env is set, so we must replay any vars the
22
+ // codex CLI needs from the parent unless the caller opts out.
23
+ if (overrides.env !== undefined || opts.env !== undefined) {
24
+ const merged = {};
25
+ // Inherit common auth-relevant vars from process.env unless the user
26
+ // pinned env themselves.
27
+ if (opts.env === undefined) {
28
+ const passthrough = [
29
+ 'HOME',
30
+ 'PATH',
31
+ 'USER',
32
+ 'CODEX_API_KEY',
33
+ 'OPENAI_API_KEY',
34
+ 'OPENAI_BASE_URL',
35
+ ];
36
+ for (const k of passthrough) {
37
+ const v = process.env[k];
38
+ if (v !== undefined)
39
+ merged[k] = v;
40
+ }
41
+ }
42
+ else {
43
+ for (const [k, v] of Object.entries(opts.env))
44
+ merged[k] = v;
45
+ }
46
+ if (overrides.env !== undefined) {
47
+ for (const [k, v] of Object.entries(overrides.env))
48
+ merged[k] = v;
49
+ }
50
+ out.env = merged;
51
+ }
52
+ if (overrides.config !== undefined) {
53
+ out.config = overrides.config;
54
+ }
55
+ return out;
56
+ }
57
+ /** Build ThreadOptions for `Codex.startThread`. */
58
+ export function buildThreadOptions(opts, extras) {
59
+ const out = {};
60
+ if (opts.model !== undefined)
61
+ out.model = opts.model;
62
+ if (opts.modelReasoningEffort !== undefined)
63
+ out.modelReasoningEffort = opts.modelReasoningEffort;
64
+ if (opts.skipGitRepoCheck !== undefined)
65
+ out.skipGitRepoCheck = opts.skipGitRepoCheck;
66
+ else
67
+ out.skipGitRepoCheck = true;
68
+ Object.assign(out, extras);
69
+ return out;
70
+ }
71
+ /**
72
+ * Translate a skelm `McpServerConfig` array into the JS shape that the
73
+ * Codex SDK's `config.mcp_servers` accepts. Today Codex's TOML config only
74
+ * supports stdio MCP servers via `command` + `args` + `env`; HTTP/SSE
75
+ * transports are dropped with a warning so the caller can audit the gap.
76
+ *
77
+ * The caller is expected to have already filtered by `policy.allowedMcpServers`.
78
+ */
79
+ export function buildMcpServerConfig(servers) {
80
+ if (servers.length === 0)
81
+ return null;
82
+ const out = {};
83
+ const dropped = [];
84
+ for (const s of servers) {
85
+ if (s.transport === 'stdio' && s.command !== undefined) {
86
+ const entry = { command: s.command };
87
+ if (s.args !== undefined)
88
+ entry.args = [...s.args];
89
+ if (s.env !== undefined)
90
+ entry.env = { ...s.env };
91
+ out[s.id] = entry;
92
+ }
93
+ else {
94
+ // Codex CLI's stable config.toml schema doesn't expose HTTP/SSE MCP
95
+ // transports; users with remote MCP must wire ~/.codex/config.toml
96
+ // manually. Surface the drop in audit instead of silently swallowing.
97
+ dropped.push(s.id);
98
+ }
99
+ }
100
+ if (Object.keys(out).length === 0)
101
+ return null;
102
+ return { mcp_servers: out, dropped };
103
+ }
104
+ export async function consumeStream(events, callbacks) {
105
+ let finalText = '';
106
+ let usage = undefined;
107
+ let stopReason = 'turn.completed';
108
+ for await (const ev of events) {
109
+ switch (ev.type) {
110
+ case 'item.completed': {
111
+ // Aggregate assistant message text. `agent_message.text` is the
112
+ // full cumulative text up to this point — `onText` per skelm's
113
+ // BackendContext.onPartial contract should receive *deltas*, so we
114
+ // emit only the new suffix on each event.
115
+ if (ev.item.type === 'agent_message') {
116
+ const next = ev.item.text;
117
+ const delta = next.startsWith(finalText) ? next.slice(finalText.length) : next;
118
+ finalText = next;
119
+ if (delta.length > 0)
120
+ callbacks.onText?.(delta);
121
+ }
122
+ callbacks.onItem?.(ev);
123
+ break;
124
+ }
125
+ case 'turn.completed':
126
+ usage = {
127
+ inputTokens: ev.usage.input_tokens,
128
+ outputTokens: ev.usage.output_tokens,
129
+ reasoningTokens: ev.usage.reasoning_output_tokens,
130
+ };
131
+ stopReason = 'turn.completed';
132
+ break;
133
+ case 'turn.failed':
134
+ stopReason = 'turn.failed';
135
+ callbacks.onError?.(ev.error.message);
136
+ throw new Error(`codex turn failed: ${ev.error.message}`);
137
+ case 'error':
138
+ stopReason = 'error';
139
+ callbacks.onError?.(ev.message);
140
+ throw new Error(`codex stream error: ${ev.message}`);
141
+ // thread.started, turn.started, item.started, item.updated: not
142
+ // material to the final response; surface via onItem if the caller
143
+ // wants per-item audit (it doesn't, by default).
144
+ default:
145
+ break;
146
+ }
147
+ }
148
+ return { finalText, usage, stopReason };
149
+ }
150
+ /** Construct a `Codex` instance. Exposed for testing. */
151
+ export function makeCodexClient(opts) {
152
+ return new Codex(opts);
153
+ }
154
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @skelm/codex — OpenAI Codex backend for skelm via `@openai/codex-sdk`.
3
+ *
4
+ * Codex authenticates via the host `codex` CLI (`codex login`) or
5
+ * `CODEX_API_KEY`. The SDK spawns codex under the hood and exchanges JSONL
6
+ * events; skelm enforces permissions at the boundary and records audit
7
+ * events as Codex emits tool calls, file changes, and shell executions.
8
+ */
9
+ export { createCodexBackend } from './backend.js';
10
+ export { CodexPermissionError, buildAuditEntry, filterIds, mapPermissionsToCodex, } from './permission-mapper.js';
11
+ export type { CodexBackendOptions, CodexPermissionAuditEntry, MappedCodexPolicy, } from './types.js';
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAA;AACjD,OAAO,EACL,oBAAoB,EACpB,eAAe,EACf,SAAS,EACT,qBAAqB,GACtB,MAAM,wBAAwB,CAAA;AAC/B,YAAY,EACV,mBAAmB,EACnB,yBAAyB,EACzB,iBAAiB,GAClB,MAAM,YAAY,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @skelm/codex — OpenAI Codex backend for skelm via `@openai/codex-sdk`.
3
+ *
4
+ * Codex authenticates via the host `codex` CLI (`codex login`) or
5
+ * `CODEX_API_KEY`. The SDK spawns codex under the hood and exchanges JSONL
6
+ * events; skelm enforces permissions at the boundary and records audit
7
+ * events as Codex emits tool calls, file changes, and shell executions.
8
+ */
9
+ export { createCodexBackend } from './backend.js';
10
+ export { CodexPermissionError, buildAuditEntry, filterIds, mapPermissionsToCodex, } from './permission-mapper.js';
11
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAA;AACjD,OAAO,EACL,oBAAoB,EACpB,eAAe,EACf,SAAS,EACT,qBAAqB,GACtB,MAAM,wBAAwB,CAAA"}
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Translate a resolved skelm permission policy into Codex SDK options.
3
+ *
4
+ * Codex enforces its own sandbox in-process; the mapper is the boundary —
5
+ * if it can't translate the policy without widening, it refuses the run.
6
+ *
7
+ * Rules:
8
+ *
9
+ * fsWrite: ∅ and fsRead: ∅ → sandboxMode: 'read-only'
10
+ * fsWrite: ['*'] → 'danger-full-access' iff approval.mode === 'never'
11
+ * (otherwise refuse — never silently escalate)
12
+ * fsWrite: [<roots>] → 'workspace-write', primary root → workingDirectory,
13
+ * extras → additionalDirectories
14
+ * networkEgress: 'deny' → networkAccessEnabled: false
15
+ * networkEgress: anything else → networkAccessEnabled: true
16
+ * (host allowlists are enforced by skelm's
17
+ * gateway egress proxy, not by Codex)
18
+ *
19
+ * Approval:
20
+ *
21
+ * approval == null → 'on-request' (Codex's safest default)
22
+ * approval.on.includes('*') → 'untrusted'
23
+ * otherwise → 'on-request'
24
+ *
25
+ * Refusals throw `CodexPermissionError` and are emitted as audit entries.
26
+ */
27
+ import type { ResolvedPolicy } from '@skelm/core';
28
+ import type { CodexPermissionAuditEntry, MappedCodexPolicy } from './types.js';
29
+ export declare class CodexPermissionError extends Error {
30
+ readonly dimension: 'fs.write' | 'fs.read' | 'network' | 'approval';
31
+ readonly name = "CodexPermissionError";
32
+ constructor(message: string, dimension: 'fs.write' | 'fs.read' | 'network' | 'approval');
33
+ }
34
+ export interface MapInputs {
35
+ /** Resolved policy after profiles + step-level merging. */
36
+ policy: ResolvedPolicy;
37
+ /** Working directory hint from the runtime (`WorkspaceHandle.path`). */
38
+ workingDirectory?: string;
39
+ }
40
+ /**
41
+ * Produce the Codex `ThreadOptions`-compatible mapping. Throws
42
+ * `CodexPermissionError` when the policy cannot be honored safely.
43
+ */
44
+ export declare function mapPermissionsToCodex(input: MapInputs): MappedCodexPolicy;
45
+ /**
46
+ * Compute which skill / MCP ids the step requested but the resolved policy
47
+ * disallows. Caller fires `permission.denied` events for each.
48
+ */
49
+ export declare function filterByPolicy<T extends {
50
+ id: string;
51
+ }>(items: readonly T[] | undefined, allowlist: ReadonlySet<string>): {
52
+ allowed: T[];
53
+ denied: T[];
54
+ };
55
+ /** Same as filterByPolicy but for plain string ids (skills, secrets). */
56
+ export declare function filterIds(ids: readonly string[] | undefined, allowlist: ReadonlySet<string>): {
57
+ allowed: string[];
58
+ denied: string[];
59
+ };
60
+ /**
61
+ * Build an audit entry recording how a policy was translated. Useful for
62
+ * the gateway's hash-chained audit writer.
63
+ */
64
+ export declare function buildAuditEntry(runId: string, stepId: string, policy: ResolvedPolicy, mapped: MappedCodexPolicy, denied: readonly string[]): CodexPermissionAuditEntry;
65
+ //# sourceMappingURL=permission-mapper.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permission-mapper.d.ts","sourceRoot":"","sources":["../src/permission-mapper.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AACjD,OAAO,KAAK,EAAE,yBAAyB,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAI9E,qBAAa,oBAAqB,SAAQ,KAAK;IAI3C,QAAQ,CAAC,SAAS,EAAE,UAAU,GAAG,SAAS,GAAG,SAAS,GAAG,UAAU;IAHrE,SAAkB,IAAI,0BAAyB;gBAE7C,OAAO,EAAE,MAAM,EACN,SAAS,EAAE,UAAU,GAAG,SAAS,GAAG,SAAS,GAAG,UAAU;CAItE;AAED,MAAM,WAAW,SAAS;IACxB,2DAA2D;IAC3D,MAAM,EAAE,cAAc,CAAA;IACtB,wEAAwE;IACxE,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,SAAS,GAAG,iBAAiB,CAiEzE;AAkBD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,EACrD,KAAK,EAAE,SAAS,CAAC,EAAE,GAAG,SAAS,EAC/B,SAAS,EAAE,WAAW,CAAC,MAAM,CAAC,GAC7B;IAAE,OAAO,EAAE,CAAC,EAAE,CAAC;IAAC,MAAM,EAAE,CAAC,EAAE,CAAA;CAAE,CAS/B;AAED,yEAAyE;AACzE,wBAAgB,SAAS,CACvB,GAAG,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,EAClC,SAAS,EAAE,WAAW,CAAC,MAAM,CAAC,GAC7B;IAAE,OAAO,EAAE,MAAM,EAAE,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CASzC;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,cAAc,EACtB,MAAM,EAAE,iBAAiB,EACzB,MAAM,EAAE,SAAS,MAAM,EAAE,GACxB,yBAAyB,CAwB3B"}
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Translate a resolved skelm permission policy into Codex SDK options.
3
+ *
4
+ * Codex enforces its own sandbox in-process; the mapper is the boundary —
5
+ * if it can't translate the policy without widening, it refuses the run.
6
+ *
7
+ * Rules:
8
+ *
9
+ * fsWrite: ∅ and fsRead: ∅ → sandboxMode: 'read-only'
10
+ * fsWrite: ['*'] → 'danger-full-access' iff approval.mode === 'never'
11
+ * (otherwise refuse — never silently escalate)
12
+ * fsWrite: [<roots>] → 'workspace-write', primary root → workingDirectory,
13
+ * extras → additionalDirectories
14
+ * networkEgress: 'deny' → networkAccessEnabled: false
15
+ * networkEgress: anything else → networkAccessEnabled: true
16
+ * (host allowlists are enforced by skelm's
17
+ * gateway egress proxy, not by Codex)
18
+ *
19
+ * Approval:
20
+ *
21
+ * approval == null → 'on-request' (Codex's safest default)
22
+ * approval.on.includes('*') → 'untrusted'
23
+ * otherwise → 'on-request'
24
+ *
25
+ * Refusals throw `CodexPermissionError` and are emitted as audit entries.
26
+ */
27
+ const STAR = '*';
28
+ export class CodexPermissionError extends Error {
29
+ dimension;
30
+ name = 'CodexPermissionError';
31
+ constructor(message, dimension) {
32
+ super(message);
33
+ this.dimension = dimension;
34
+ }
35
+ }
36
+ /**
37
+ * Produce the Codex `ThreadOptions`-compatible mapping. Throws
38
+ * `CodexPermissionError` when the policy cannot be honored safely.
39
+ */
40
+ export function mapPermissionsToCodex(input) {
41
+ const { policy, workingDirectory } = input;
42
+ const fsWrite = Array.from(policy.fsWrite);
43
+ const fsRead = Array.from(policy.fsRead);
44
+ // Sandbox tier.
45
+ let sandboxMode;
46
+ let primary = workingDirectory;
47
+ const extras = [];
48
+ const hasStarWrite = fsWrite.includes(STAR);
49
+ if (hasStarWrite) {
50
+ // Broad write requires explicit no-approval acknowledgement.
51
+ if (policy.approval !== null) {
52
+ throw new CodexPermissionError('fsWrite includes "*" but an approval policy is set; refusing danger-full-access', 'fs.write');
53
+ }
54
+ sandboxMode = 'danger-full-access';
55
+ }
56
+ else if (fsWrite.length === 0) {
57
+ sandboxMode = 'read-only';
58
+ }
59
+ else {
60
+ sandboxMode = 'workspace-write';
61
+ if (primary === undefined)
62
+ primary = fsWrite[0];
63
+ for (const root of fsWrite) {
64
+ if (root !== primary && !extras.includes(root))
65
+ extras.push(root);
66
+ }
67
+ }
68
+ // Read-only sandbox doesn't need fsRead roots (codex's read-only mode is
69
+ // unrestricted by default; the read allowlist is informational here).
70
+ void fsRead;
71
+ // Network. Codex has two distinct network surfaces:
72
+ // 1. `networkAccessEnabled` — sandbox-shell egress (curl, wget, etc).
73
+ // The gateway egress proxy can enforce host allowlists for this
74
+ // surface when `networkEgress: { allowHosts }` is set.
75
+ // 2. `webSearchEnabled` / `webSearchMode` — the model's built-in web
76
+ // tool. This runs INSIDE the Codex process and does NOT go through
77
+ // the gateway proxy, so per-host allowlists cannot be enforced.
78
+ //
79
+ // Implication: a `{ allowHosts: ['api.example.com'] }` policy would, if
80
+ // we left web_search enabled, let Codex reach arbitrary URLs through its
81
+ // built-in tool — bypassing the allowlist the user just declared. To
82
+ // honor the policy faithfully, web_search is enabled ONLY when
83
+ // networkEgress === 'allow' (blanket allow). Anything else disables it.
84
+ const networkAccessEnabled = policy.networkEgress !== 'deny';
85
+ const webSearchAllowed = policy.networkEgress === 'allow';
86
+ const webSearchEnabled = webSearchAllowed;
87
+ const webSearchMode = webSearchAllowed ? 'live' : 'disabled';
88
+ // Approval mode.
89
+ const approvalPolicy = pickApprovalMode(policy);
90
+ const mapped = {
91
+ sandboxMode,
92
+ approvalPolicy,
93
+ networkAccessEnabled,
94
+ webSearchEnabled,
95
+ webSearchMode,
96
+ ...(primary !== undefined && { workingDirectory: primary }),
97
+ ...(extras.length > 0 && { additionalDirectories: extras }),
98
+ };
99
+ return mapped;
100
+ }
101
+ function pickApprovalMode(policy) {
102
+ // No explicit approval policy → don't escalate. Codex's sandbox is already
103
+ // enforcing what's allowed (sandboxMode + workingDirectory + network); the
104
+ // skelm boundary check has already refused unsafe combinations before we
105
+ // got here. Using 'on-request' here would deadlock every shell command in
106
+ // automated environments because skelm doesn't supply a Codex-side
107
+ // approval handler. Per project tenet "default-deny is structural": the
108
+ // sandbox is the deny, not approval prompts.
109
+ if (policy.approval === null)
110
+ return 'never';
111
+ // Explicit approval policy from the workflow author → respect it.
112
+ if (policy.approval.on.some((d) => d === 'tool' || d === 'executable')) {
113
+ return 'untrusted';
114
+ }
115
+ return 'on-request';
116
+ }
117
+ /**
118
+ * Compute which skill / MCP ids the step requested but the resolved policy
119
+ * disallows. Caller fires `permission.denied` events for each.
120
+ */
121
+ export function filterByPolicy(items, allowlist) {
122
+ if (items === undefined)
123
+ return { allowed: [], denied: [] };
124
+ const allowed = [];
125
+ const denied = [];
126
+ for (const item of items) {
127
+ if (allowlist.has(item.id))
128
+ allowed.push(item);
129
+ else
130
+ denied.push(item);
131
+ }
132
+ return { allowed, denied };
133
+ }
134
+ /** Same as filterByPolicy but for plain string ids (skills, secrets). */
135
+ export function filterIds(ids, allowlist) {
136
+ if (ids === undefined)
137
+ return { allowed: [], denied: [] };
138
+ const allowed = [];
139
+ const denied = [];
140
+ for (const id of ids) {
141
+ if (allowlist.has(id))
142
+ allowed.push(id);
143
+ else
144
+ denied.push(id);
145
+ }
146
+ return { allowed, denied };
147
+ }
148
+ /**
149
+ * Build an audit entry recording how a policy was translated. Useful for
150
+ * the gateway's hash-chained audit writer.
151
+ */
152
+ export function buildAuditEntry(runId, stepId, policy, mapped, denied) {
153
+ return {
154
+ runId,
155
+ stepId,
156
+ timestamp: new Date().toISOString(),
157
+ event: 'permission_check',
158
+ details: {
159
+ declaredPermissions: {
160
+ allowedExecutables: Array.from(policy.allowedExecutables),
161
+ allowedMcpServers: Array.from(policy.allowedMcpServers),
162
+ allowedSkills: Array.from(policy.allowedSkills),
163
+ fsRead: Array.from(policy.fsRead),
164
+ fsWrite: Array.from(policy.fsWrite),
165
+ networkEgress: typeof policy.networkEgress === 'string'
166
+ ? policy.networkEgress
167
+ : `allowHosts:${policy.networkEgress.allowHosts.join(',')}`,
168
+ },
169
+ mapped,
170
+ decision: denied.length === 0 ? 'allow' : 'deny',
171
+ deniedItems: Array.from(denied),
172
+ backend: 'codex',
173
+ },
174
+ };
175
+ }
176
+ //# sourceMappingURL=permission-mapper.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permission-mapper.js","sourceRoot":"","sources":["../src/permission-mapper.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,83 @@
1
+ /**
2
+ * `@skelm/codex` configuration types.
3
+ *
4
+ * The backend wraps OpenAI's official `@openai/codex-sdk`. The SDK spawns
5
+ * the `codex` CLI under the hood and exchanges JSONL events over stdio, so
6
+ * authentication piggybacks on `codex login` (`~/.codex/auth.json`) or the
7
+ * `CODEX_API_KEY` env var.
8
+ */
9
+ import type { ApprovalMode, ModelReasoningEffort, SandboxMode, WebSearchMode } from '@openai/codex-sdk';
10
+ export interface CodexBackendOptions {
11
+ /** Backend id. Defaults to 'codex' when only one codex backend is registered. */
12
+ id?: string;
13
+ /** Human-readable label for diagnostics. */
14
+ label?: string;
15
+ /** Path to the codex CLI. Default: 'codex' on PATH (the SDK resolves it). */
16
+ codexPathOverride?: string;
17
+ /** Override the Codex API base URL (self-hosted / proxied deployments). */
18
+ baseUrl?: string;
19
+ /** API key. Overrides the ambient `CODEX_API_KEY` env var. */
20
+ apiKey?: string;
21
+ /**
22
+ * Extra env vars passed to the spawned codex process. When set, the SDK
23
+ * does NOT inherit `process.env` — it uses these vars as the full env.
24
+ * Skelm always merges `BackendContext.proxyEnv` into this map at run time.
25
+ */
26
+ env?: Record<string, string>;
27
+ /** Default model id, e.g. 'gpt-5.2' or 'gpt-5.3-codex'. */
28
+ model?: string;
29
+ /** Default reasoning effort. */
30
+ modelReasoningEffort?: ModelReasoningEffort;
31
+ /**
32
+ * Skip the SDK's "is this a git repo" sanity check. Defaults to `true` for
33
+ * ephemeral skelm workspaces (which usually have no `.git`).
34
+ */
35
+ skipGitRepoCheck?: boolean;
36
+ /**
37
+ * Per-step abort timeout. The runtime's own abort signal already drives
38
+ * cancellation; this is a defensive ceiling. Default: 300_000 (5 min).
39
+ */
40
+ timeoutMs?: number;
41
+ }
42
+ /**
43
+ * Resolved Codex SDK options after skelm permissions are applied. Produced
44
+ * by `permission-mapper.ts` and fed into `Codex.startThread()`.
45
+ */
46
+ export interface MappedCodexPolicy {
47
+ sandboxMode: SandboxMode;
48
+ approvalPolicy: ApprovalMode;
49
+ networkAccessEnabled: boolean;
50
+ /**
51
+ * Codex's built-in web search bypasses `networkAccessEnabled` (that flag
52
+ * governs sandbox-shell egress only). When networkEgress is 'deny' we set
53
+ * webSearchMode to 'disabled' too so the agent cannot reach the public
54
+ * web through its built-in tool.
55
+ */
56
+ webSearchMode: WebSearchMode;
57
+ webSearchEnabled: boolean;
58
+ /** Primary working directory (== WorkspaceHandle.path when present). */
59
+ workingDirectory?: string;
60
+ /** Extra writable directories beyond `workingDirectory`. */
61
+ additionalDirectories?: string[];
62
+ }
63
+ export interface CodexPermissionAuditEntry {
64
+ runId: string;
65
+ stepId: string;
66
+ timestamp: string;
67
+ event: 'permission_check';
68
+ details: {
69
+ declaredPermissions: {
70
+ allowedExecutables: string[];
71
+ allowedMcpServers: string[];
72
+ allowedSkills: string[];
73
+ fsRead: string[];
74
+ fsWrite: string[];
75
+ networkEgress: string;
76
+ };
77
+ mapped: MappedCodexPolicy;
78
+ decision: 'allow' | 'deny';
79
+ deniedItems: string[];
80
+ backend: 'codex';
81
+ };
82
+ }
83
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EACV,YAAY,EACZ,oBAAoB,EACpB,WAAW,EACX,aAAa,EACd,MAAM,mBAAmB,CAAA;AAE1B,MAAM,WAAW,mBAAmB;IAClC,iFAAiF;IACjF,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,4CAA4C;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAA;IAId,6EAA6E;IAC7E,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,2EAA2E;IAC3E,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,8DAA8D;IAC9D,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;;OAIG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAI5B,2DAA2D;IAC3D,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,gCAAgC;IAChC,oBAAoB,CAAC,EAAE,oBAAoB,CAAA;IAI3C;;;OAGG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAA;IAC1B;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,WAAW,CAAA;IACxB,cAAc,EAAE,YAAY,CAAA;IAC5B,oBAAoB,EAAE,OAAO,CAAA;IAC7B;;;;;OAKG;IACH,aAAa,EAAE,aAAa,CAAA;IAC5B,gBAAgB,EAAE,OAAO,CAAA;IACzB,wEAAwE;IACxE,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,4DAA4D;IAC5D,qBAAqB,CAAC,EAAE,MAAM,EAAE,CAAA;CACjC;AAED,MAAM,WAAW,yBAAyB;IACxC,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,kBAAkB,CAAA;IACzB,OAAO,EAAE;QACP,mBAAmB,EAAE;YACnB,kBAAkB,EAAE,MAAM,EAAE,CAAA;YAC5B,iBAAiB,EAAE,MAAM,EAAE,CAAA;YAC3B,aAAa,EAAE,MAAM,EAAE,CAAA;YACvB,MAAM,EAAE,MAAM,EAAE,CAAA;YAChB,OAAO,EAAE,MAAM,EAAE,CAAA;YACjB,aAAa,EAAE,MAAM,CAAA;SACtB,CAAA;QACD,MAAM,EAAE,iBAAiB,CAAA;QACzB,QAAQ,EAAE,OAAO,GAAG,MAAM,CAAA;QAC1B,WAAW,EAAE,MAAM,EAAE,CAAA;QACrB,OAAO,EAAE,OAAO,CAAA;KACjB,CAAA;CACF"}
package/dist/types.js ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * `@skelm/codex` configuration types.
3
+ *
4
+ * The backend wraps OpenAI's official `@openai/codex-sdk`. The SDK spawns
5
+ * the `codex` CLI under the hood and exchanges JSONL events over stdio, so
6
+ * authentication piggybacks on `codex login` (`~/.codex/auth.json`) or the
7
+ * `CODEX_API_KEY` env var.
8
+ */
9
+ export {};
10
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG"}
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@skelm/codex",
3
+ "version": "0.3.9",
4
+ "description": "OpenAI Codex backend for skelm via the official @openai/codex-sdk",
5
+ "license": "MIT",
6
+ "author": "Scott Glover <scottgl@gmail.com>",
7
+ "homepage": "https://skelm.dev/",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/scottgl9/skelm.git",
11
+ "directory": "packages/codex"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/scottgl9/skelm/issues"
15
+ },
16
+ "keywords": [
17
+ "skelm",
18
+ "codex",
19
+ "openai",
20
+ "agent",
21
+ "backend",
22
+ "coding-agent"
23
+ ],
24
+ "type": "module",
25
+ "main": "./dist/index.js",
26
+ "types": "./dist/index.d.ts",
27
+ "exports": {
28
+ ".": {
29
+ "types": "./dist/index.d.ts",
30
+ "import": "./dist/index.js"
31
+ }
32
+ },
33
+ "files": [
34
+ "dist",
35
+ "README.md",
36
+ "LICENSE"
37
+ ],
38
+ "publishConfig": {
39
+ "access": "public",
40
+ "registry": "https://registry.npmjs.org/"
41
+ },
42
+ "scripts": {
43
+ "build": "tsc -p tsconfig.json",
44
+ "test": "vitest run",
45
+ "test:watch": "vitest",
46
+ "clean": "rm -rf dist tsconfig.tsbuildinfo"
47
+ },
48
+ "dependencies": {
49
+ "@openai/codex-sdk": "^0.130.0"
50
+ },
51
+ "peerDependencies": {
52
+ "@skelm/core": "^0.3.9"
53
+ },
54
+ "devDependencies": {
55
+ "@skelm/core": "^0.3.9",
56
+ "@types/node": "^20.10.0",
57
+ "typescript": "^5.3.0",
58
+ "vitest": "^2.1.5"
59
+ }
60
+ }