@bakers-build/addon-codex 0.3.0 → 0.3.1-next.308
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/src/index.d.ts +16 -2
- package/dist/src/index.js +149 -7
- package/package.json +3 -3
package/dist/src/index.d.ts
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
|
-
import { type AgentFootprint, type AgentPlugin, type AgentRunIdentity, type BbAgent } from '@bakers-build/conductor/contract';
|
|
1
|
+
import { type AgentFootprint, type AgentPlugin, type AgentRunIdentity, type BbAgent, type RunEnvPolicy, type SecurityOptions } from '@bakers-build/conductor/contract';
|
|
2
2
|
export { parseCodexTranscript } from './footprint.js';
|
|
3
|
-
export interface CodexAgentOptions {
|
|
3
|
+
export interface CodexAgentOptions extends SecurityOptions {
|
|
4
4
|
model?: string;
|
|
5
5
|
effort?: string;
|
|
6
6
|
args?: string[];
|
|
7
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* The server ids a Codex `config.toml` declares under `mcp_servers`, in table,
|
|
10
|
+
* dotted-key and inline-table form. Throws on a declaration it cannot read or
|
|
11
|
+
* that `-c mcp_servers.<id>.enabled=false` cannot address (Codex splits `-c`
|
|
12
|
+
* keys on every dot), so a server never stays enabled unnoticed.
|
|
13
|
+
* shortcut: line-based, so a multi-line string holding a header-like line is
|
|
14
|
+
* read as one; switch to a TOML parser if that ever matters.
|
|
15
|
+
*/
|
|
16
|
+
export declare function codexMcpServerIds(toml: string): string[];
|
|
8
17
|
export declare class CodexAgent implements BbAgent {
|
|
9
18
|
private readonly home;
|
|
10
19
|
readonly id = "codex";
|
|
@@ -12,7 +21,12 @@ export declare class CodexAgent implements BbAgent {
|
|
|
12
21
|
private readonly model;
|
|
13
22
|
private readonly effort;
|
|
14
23
|
private readonly overrides;
|
|
24
|
+
private readonly security;
|
|
25
|
+
readonly runEnvPolicy: RunEnvPolicy;
|
|
15
26
|
constructor(options?: CodexAgentOptions, home?: string);
|
|
27
|
+
/** The launch flags a security mode owns (`docs/addons/codex.md`). */
|
|
28
|
+
private securityFlags;
|
|
29
|
+
private userMcpServers;
|
|
16
30
|
launchCommand(prompt: string, _run?: AgentRunIdentity): string;
|
|
17
31
|
getRunLog(worktreePath: string, run?: AgentRunIdentity): Promise<string>;
|
|
18
32
|
parseFootprint(log: string): AgentFootprint;
|
package/dist/src/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
1
2
|
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
2
3
|
import { homedir } from 'node:os';
|
|
3
4
|
import { join } from 'node:path';
|
|
4
|
-
import { resolveAgentCliOptions, } from '@bakers-build/conductor/contract';
|
|
5
|
+
import { resolveAgentCliOptions, resolveSecurityOptions, } from '@bakers-build/conductor/contract';
|
|
5
6
|
import { shellQuote } from '@bakers-build/core';
|
|
6
7
|
import { parseCodexTranscript } from './footprint.js';
|
|
7
8
|
export { parseCodexTranscript } from './footprint.js';
|
|
@@ -10,7 +11,84 @@ const CODEX_RESERVED = [
|
|
|
10
11
|
'-s',
|
|
11
12
|
'--dangerously-bypass-approvals-and-sandbox',
|
|
12
13
|
'--dangerously-bypass-hook-trust',
|
|
14
|
+
'-a',
|
|
15
|
+
'--ask-for-approval',
|
|
13
16
|
];
|
|
17
|
+
const SANDBOX_DEFAULTS = {
|
|
18
|
+
trusted_commands: [],
|
|
19
|
+
deny_read: [],
|
|
20
|
+
allow_write: [],
|
|
21
|
+
network: ['*'],
|
|
22
|
+
mcp_servers: {},
|
|
23
|
+
};
|
|
24
|
+
/** One dotted-key segment: bare, "basic" or 'literal'. */
|
|
25
|
+
const KEY_SEGMENT = /\s*(?:([A-Za-z0-9_-]+)|"([^"\\]*)"|'([^']*)')\s*/y;
|
|
26
|
+
function keyPath(text) {
|
|
27
|
+
const path = [];
|
|
28
|
+
KEY_SEGMENT.lastIndex = 0;
|
|
29
|
+
for (;;) {
|
|
30
|
+
const m = KEY_SEGMENT.exec(text);
|
|
31
|
+
if (!m)
|
|
32
|
+
return null;
|
|
33
|
+
path.push((m[1] ?? m[2] ?? m[3]));
|
|
34
|
+
if (KEY_SEGMENT.lastIndex === text.length)
|
|
35
|
+
return path;
|
|
36
|
+
if (text[KEY_SEGMENT.lastIndex] !== '.')
|
|
37
|
+
return null;
|
|
38
|
+
KEY_SEGMENT.lastIndex += 1;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The server ids a Codex `config.toml` declares under `mcp_servers`, in table,
|
|
43
|
+
* dotted-key and inline-table form. Throws on a declaration it cannot read or
|
|
44
|
+
* that `-c mcp_servers.<id>.enabled=false` cannot address (Codex splits `-c`
|
|
45
|
+
* keys on every dot), so a server never stays enabled unnoticed.
|
|
46
|
+
* shortcut: line-based, so a multi-line string holding a header-like line is
|
|
47
|
+
* read as one; switch to a TOML parser if that ever matters.
|
|
48
|
+
*/
|
|
49
|
+
export function codexMcpServerIds(toml) {
|
|
50
|
+
const ids = new Set();
|
|
51
|
+
const unreadable = (line) => new Error(`Codex config: cannot read the mcp_servers declaration ${line}`);
|
|
52
|
+
let table = [];
|
|
53
|
+
for (const raw of toml.split('\n')) {
|
|
54
|
+
const line = raw.trim();
|
|
55
|
+
if (line === '' || line.startsWith('#'))
|
|
56
|
+
continue;
|
|
57
|
+
const header = /^\[(\[?)(.*?)\]\]?\s*(?:#.*)?$/.exec(line);
|
|
58
|
+
const key = header
|
|
59
|
+
? header[2]
|
|
60
|
+
: line.slice(0, Math.max(line.indexOf('='), 0));
|
|
61
|
+
if (!header && key === '')
|
|
62
|
+
continue; // a value continued from an earlier line
|
|
63
|
+
const path = keyPath(key);
|
|
64
|
+
if (path === null || (header && header[1] !== '')) {
|
|
65
|
+
// Only keys directly in `[mcp_servers]` are server ids; deeper ones
|
|
66
|
+
// belong to a server already found.
|
|
67
|
+
const inRoot = !header && table.length === 1 && table[0] === 'mcp_servers';
|
|
68
|
+
if (key?.includes('mcp_servers') || inRoot) {
|
|
69
|
+
throw unreadable(line);
|
|
70
|
+
}
|
|
71
|
+
table = header ? ['<unread>'] : table;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const full = header ? path : [...table, ...path];
|
|
75
|
+
if (header)
|
|
76
|
+
table = path;
|
|
77
|
+
if (full[0] !== 'mcp_servers')
|
|
78
|
+
continue;
|
|
79
|
+
if (full.length === 1) {
|
|
80
|
+
if (header)
|
|
81
|
+
continue;
|
|
82
|
+
throw unreadable(line);
|
|
83
|
+
}
|
|
84
|
+
const id = full[1];
|
|
85
|
+
if (!/^[A-Za-z0-9_-]+$/.test(id)) {
|
|
86
|
+
throw new Error(`Codex config: mcp_servers id "${id}" cannot be disabled with -c`);
|
|
87
|
+
}
|
|
88
|
+
ids.add(id);
|
|
89
|
+
}
|
|
90
|
+
return [...ids];
|
|
91
|
+
}
|
|
14
92
|
const CODEX_KNOWN = [
|
|
15
93
|
{ kind: 'model', flags: ['--model', '-m'], configKeys: ['model'] },
|
|
16
94
|
{ kind: 'effort', flags: [], configKeys: ['model_reasoning_effort'] },
|
|
@@ -22,6 +100,8 @@ export class CodexAgent {
|
|
|
22
100
|
model;
|
|
23
101
|
effort;
|
|
24
102
|
overrides;
|
|
103
|
+
security;
|
|
104
|
+
runEnvPolicy;
|
|
25
105
|
constructor(options = {}, home = homedir()) {
|
|
26
106
|
this.home = home;
|
|
27
107
|
const cli = resolveAgentCliOptions(options, CODEX_RESERVED, CODEX_KNOWN);
|
|
@@ -29,14 +109,76 @@ export class CodexAgent {
|
|
|
29
109
|
this.model = cli.model;
|
|
30
110
|
this.effort = cli.effort;
|
|
31
111
|
this.overrides = cli.overrides;
|
|
112
|
+
this.security = resolveSecurityOptions(options, SANDBOX_DEFAULTS);
|
|
113
|
+
this.runEnvPolicy = this.security.runEnvPolicy;
|
|
114
|
+
// Codex has no read deny or unsandboxed-command list to map these onto.
|
|
115
|
+
for (const key of ['trusted_commands', 'deny_read']) {
|
|
116
|
+
if (this.security.sandbox[key].length > 0) {
|
|
117
|
+
throw new Error(`Codex does not support sandbox.${key}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// `-c` takes a TOML value; a JSON object is not one.
|
|
121
|
+
for (const [key, value] of Object.entries(this.security.config ?? {})) {
|
|
122
|
+
if (value !== null &&
|
|
123
|
+
typeof value === 'object' &&
|
|
124
|
+
!Array.isArray(value)) {
|
|
125
|
+
throw new Error(`Codex config.${key} is an object; use one dotted key per value`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
32
128
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
129
|
+
/** The launch flags a security mode owns (`docs/addons/codex.md`). */
|
|
130
|
+
securityFlags() {
|
|
131
|
+
const { mode, sandbox, config } = this.security;
|
|
132
|
+
if (mode === 'system')
|
|
133
|
+
return [];
|
|
134
|
+
if (mode === 'yolo') {
|
|
135
|
+
return [
|
|
136
|
+
'--sandbox',
|
|
137
|
+
'danger-full-access',
|
|
138
|
+
'--dangerously-bypass-approvals-and-sandbox',
|
|
139
|
+
'--dangerously-bypass-hook-trust',
|
|
140
|
+
];
|
|
141
|
+
}
|
|
142
|
+
const pairs = (entries) => entries.flatMap(([key, value]) => [
|
|
143
|
+
'-c',
|
|
144
|
+
`${key}=${typeof value === 'string' ? value : JSON.stringify(value)}`,
|
|
145
|
+
]);
|
|
146
|
+
if (mode === 'custom')
|
|
147
|
+
return pairs(Object.entries(config ?? {}));
|
|
148
|
+
const flags = [
|
|
149
|
+
'--sandbox',
|
|
150
|
+
'workspace-write',
|
|
151
|
+
'-a',
|
|
152
|
+
'never',
|
|
153
|
+
...pairs([
|
|
154
|
+
['sandbox_workspace_write.network_access', sandbox.network.length > 0],
|
|
155
|
+
]),
|
|
39
156
|
];
|
|
157
|
+
for (const dir of sandbox.allow_write) {
|
|
158
|
+
flags.push('--add-dir', dir.startsWith('~/') ? join(this.home, dir.slice(2)) : dir);
|
|
159
|
+
}
|
|
160
|
+
for (const id of this.userMcpServers()) {
|
|
161
|
+
if (!Object.hasOwn(sandbox.mcp_servers, id)) {
|
|
162
|
+
flags.push(...pairs([[`mcp_servers.${id}.enabled`, false]]));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return flags;
|
|
166
|
+
}
|
|
167
|
+
userMcpServers() {
|
|
168
|
+
const codexHome = process.env.CODEX_HOME || join(this.home, '.codex');
|
|
169
|
+
let toml;
|
|
170
|
+
try {
|
|
171
|
+
toml = readFileSync(join(codexHome, 'config.toml'), 'utf8');
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return [];
|
|
175
|
+
}
|
|
176
|
+
return codexMcpServerIds(toml);
|
|
177
|
+
}
|
|
178
|
+
launchCommand(prompt, _run) {
|
|
179
|
+
// Quote at assembly; a plain word has nothing for the shell to expand.
|
|
180
|
+
const word = (t) => (/^[\w./=-]+$/.test(t) ? t : shellQuote(t));
|
|
181
|
+
const parts = ['codex', ...this.securityFlags().map(word)];
|
|
40
182
|
if (this.model !== undefined && !this.overrides.model) {
|
|
41
183
|
parts.push('--model', shellQuote(this.model));
|
|
42
184
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bakers-build/addon-codex",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1-next.308",
|
|
4
4
|
"description": "bakers.build agent plugin for the Codex CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"directory": "bakers-cli/addons/codex"
|
|
21
21
|
},
|
|
22
22
|
"peerDependencies": {
|
|
23
|
-
"@bakers-build/conductor": "^0.3.
|
|
24
|
-
"@bakers-build/core": "^0.3.
|
|
23
|
+
"@bakers-build/conductor": "^0.3.1-next.308",
|
|
24
|
+
"@bakers-build/core": "^0.3.1-next.308"
|
|
25
25
|
}
|
|
26
26
|
}
|