@kbediako/codex-orchestrator 0.1.3 → 0.1.4
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/README.md +6 -1
- package/dist/bin/codex-orchestrator.js +38 -0
- package/dist/orchestrator/src/cli/config/delegationConfig.js +485 -0
- package/dist/orchestrator/src/cli/control/confirmations.js +262 -0
- package/dist/orchestrator/src/cli/control/controlServer.js +1476 -0
- package/dist/orchestrator/src/cli/control/controlState.js +46 -0
- package/dist/orchestrator/src/cli/control/controlWatcher.js +222 -0
- package/dist/orchestrator/src/cli/control/delegationTokens.js +62 -0
- package/dist/orchestrator/src/cli/control/questions.js +106 -0
- package/dist/orchestrator/src/cli/delegationServer.js +1368 -0
- package/dist/orchestrator/src/cli/events/runEventStream.js +246 -0
- package/dist/orchestrator/src/cli/exec/context.js +4 -1
- package/dist/orchestrator/src/cli/exec/stageRunner.js +30 -5
- package/dist/orchestrator/src/cli/metrics/metricsAggregator.js +377 -147
- package/dist/orchestrator/src/cli/metrics/metricsRecorder.js +3 -5
- package/dist/orchestrator/src/cli/orchestrator.js +217 -40
- package/dist/orchestrator/src/cli/rlmRunner.js +26 -3
- package/dist/orchestrator/src/cli/run/manifestPersister.js +33 -3
- package/dist/orchestrator/src/cli/run/runPaths.js +14 -0
- package/dist/orchestrator/src/cli/services/commandRunner.js +1 -1
- package/dist/orchestrator/src/cli/utils/devtools.js +33 -2
- package/dist/orchestrator/src/persistence/ExperienceStore.js +113 -46
- package/dist/orchestrator/src/persistence/PersistenceCoordinator.js +8 -8
- package/dist/orchestrator/src/persistence/TaskStateStore.js +2 -1
- package/dist/orchestrator/src/persistence/lockFile.js +26 -1
- package/dist/orchestrator/src/sync/CloudSyncWorker.js +17 -4
- package/dist/packages/orchestrator/src/telemetry/otel-exporter.js +21 -0
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -130,6 +130,11 @@ Notes:
|
|
|
130
130
|
- `MCP_RUNNER_TASK_ID` is no longer coerced or lowercased silently. The CLI calls the shared `sanitizeTaskId` helper and fails fast when the value contains control characters, traversal attempts, or Windows-reserved characters (`<`, `>`, `:`, `"`, `/`, `\`, `|`, `?`, `*`). Set the correct task ID in your environment *before* invoking the CLI.
|
|
131
131
|
- Run IDs used for manifest or artifact storage must come from the CLI (or pass the shared `sanitizeRunId` helper). Strings with colons, control characters, or `../` are rejected to ensure every run directory lives under `.runs/<task-id>/cli/<run-id>` (and legacy `mcp` mirrors) without risking traversal.
|
|
132
132
|
|
|
133
|
+
### Delegation Guardrails
|
|
134
|
+
- `delegate.question.poll` clamps `wait_ms` to `MAX_QUESTION_POLL_WAIT_MS` (10s); each poll timeout is bounded by the remaining `wait_ms`.
|
|
135
|
+
- Confirm-to-act fallback only triggers on confirmation-specific errors (`error.code`), not generic tool failures.
|
|
136
|
+
- Tool profile entries used for MCP overrides are sanitized; only alphanumeric + `_`/`-` names are allowed (rejects `;`, `/`, `\n`, `=` and similar).
|
|
137
|
+
|
|
133
138
|
## Pipelines & Execution Plans
|
|
134
139
|
- Default pipelines live in `codex.orchestrator.json` (repository-specific) and `orchestrator/src/cli/pipelines/` (built-in defaults). Each stage is either a command (shell execution) or a nested pipeline.
|
|
135
140
|
- The `CommandPlanner` inspects the selected pipeline and target stage; you can pass `--target <stage-id>` (alias: `--target-stage`) or set `CODEX_ORCHESTRATOR_TARGET_STAGE` to focus on a specific step (e.g., rerun tests only).
|
|
@@ -149,7 +154,7 @@ Notes:
|
|
|
149
154
|
- `TaskStateStore` writes per-task snapshots with bounded lock retries; failures degrade gracefully while still writing the main manifest.
|
|
150
155
|
- `RunManifestWriter` generates the canonical manifest JSON for each run (mirrored under `.runs/`), while metrics appenders and summary writers keep `out/` up to date.
|
|
151
156
|
- Heartbeat files and timestamps guard against stalled runs. `orchestrator/src/cli/metrics/metricsRecorder.ts` aggregates command durations, exit codes, and guardrail stats for later review.
|
|
152
|
-
- Optional caps: `CODEX_ORCHESTRATOR_EXEC_EVENT_MAX_CHUNKS` limits captured exec chunk events per command (0
|
|
157
|
+
- Optional caps: `CODEX_ORCHESTRATOR_EXEC_EVENT_MAX_CHUNKS` limits captured exec chunk events per command (defaults to 500; set 0 for no cap), `CODEX_ORCHESTRATOR_TELEMETRY_MAX_EVENTS` caps in-memory telemetry events queued before flush (defaults to 1000; set 0 for no cap), and `CODEX_METRICS_PRIVACY_EVENTS_MAX` limits privacy decision events stored in `metrics.json` (-1 = no cap; `privacy_event_count` still reflects total).
|
|
153
158
|
|
|
154
159
|
## Customizing for New Projects
|
|
155
160
|
- Duplicate the templates under `/tasks`, `docs/`, and `.agent/` for your task ID and keep checklist status mirrored (`[ ]` → `[x]`) with links to the manifest that proves each outcome.
|
|
@@ -16,6 +16,8 @@ import { formatDevtoolsSetupSummary, runDevtoolsSetup } from '../orchestrator/sr
|
|
|
16
16
|
import { loadPackageInfo } from '../orchestrator/src/cli/utils/packageInfo.js';
|
|
17
17
|
import { slugify } from '../orchestrator/src/cli/utils/strings.js';
|
|
18
18
|
import { serveMcp } from '../orchestrator/src/cli/mcp.js';
|
|
19
|
+
import { startDelegationServer } from '../orchestrator/src/cli/delegationServer.js';
|
|
20
|
+
import { splitDelegationConfigOverrides } from '../orchestrator/src/cli/config/delegationConfig.js';
|
|
19
21
|
async function main() {
|
|
20
22
|
const args = process.argv.slice(2);
|
|
21
23
|
const command = args.shift();
|
|
@@ -66,6 +68,10 @@ async function main() {
|
|
|
66
68
|
case 'mcp':
|
|
67
69
|
await handleMcp(args);
|
|
68
70
|
break;
|
|
71
|
+
case 'delegate-server':
|
|
72
|
+
case 'delegation-server':
|
|
73
|
+
await handleDelegationServer(args);
|
|
74
|
+
break;
|
|
69
75
|
case 'version':
|
|
70
76
|
printVersion();
|
|
71
77
|
break;
|
|
@@ -506,6 +512,34 @@ async function handleMcp(rawArgs) {
|
|
|
506
512
|
const dryRun = Boolean(flags['dry-run']);
|
|
507
513
|
await serveMcp({ repoRoot, dryRun, extraArgs: positionals });
|
|
508
514
|
}
|
|
515
|
+
async function handleDelegationServer(rawArgs) {
|
|
516
|
+
const { flags } = parseArgs(rawArgs);
|
|
517
|
+
const repoRoot = typeof flags['repo'] === 'string' ? flags['repo'] : process.cwd();
|
|
518
|
+
const modeFlag = typeof flags['mode'] === 'string' ? flags['mode'] : undefined;
|
|
519
|
+
const overrideFlag = typeof flags['config'] === 'string'
|
|
520
|
+
? flags['config']
|
|
521
|
+
: typeof flags['config-override'] === 'string'
|
|
522
|
+
? flags['config-override']
|
|
523
|
+
: undefined;
|
|
524
|
+
const envMode = process.env.CODEX_DELEGATE_MODE?.trim();
|
|
525
|
+
const resolvedMode = modeFlag ?? envMode;
|
|
526
|
+
let mode;
|
|
527
|
+
if (resolvedMode) {
|
|
528
|
+
if (resolvedMode === 'full' || resolvedMode === 'question_only') {
|
|
529
|
+
mode = resolvedMode;
|
|
530
|
+
}
|
|
531
|
+
else {
|
|
532
|
+
console.warn(`Invalid delegate mode "${resolvedMode}". Falling back to config default.`);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
const configOverrides = overrideFlag
|
|
536
|
+
? splitDelegationConfigOverrides(overrideFlag).map((value) => ({
|
|
537
|
+
source: 'cli',
|
|
538
|
+
value
|
|
539
|
+
}))
|
|
540
|
+
: [];
|
|
541
|
+
await startDelegationServer({ repoRoot, mode, configOverrides });
|
|
542
|
+
}
|
|
509
543
|
function parseExecArgs(rawArgs) {
|
|
510
544
|
const notifyTargets = [];
|
|
511
545
|
let otelEndpoint = null;
|
|
@@ -687,6 +721,10 @@ Commands:
|
|
|
687
721
|
--yes Apply setup by running "codex mcp add ...".
|
|
688
722
|
--format json Emit machine-readable output (dry-run only).
|
|
689
723
|
mcp serve [--repo <path>] [--dry-run] [-- <extra args>]
|
|
724
|
+
delegate-server Run the delegation MCP server (stdio).
|
|
725
|
+
--repo <path> Repo root for config + manifests (default cwd).
|
|
726
|
+
--mode <full|question_only> Limit tool surface for child runs.
|
|
727
|
+
--config "<key>=<value>[;...]" Apply config overrides (repeat via separators).
|
|
690
728
|
version | --version
|
|
691
729
|
|
|
692
730
|
help Show this message.
|
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
import { realpathSync } from 'node:fs';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
6
|
+
import { logger } from '../../logger.js';
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
const toml = require('@iarna/toml');
|
|
9
|
+
const DEFAULT_ALLOWED_RUN_ROOTS = [];
|
|
10
|
+
const DEFAULT_CONFIG = {
|
|
11
|
+
delegate: {
|
|
12
|
+
allowNested: false,
|
|
13
|
+
toolProfile: [],
|
|
14
|
+
allowedToolServers: [],
|
|
15
|
+
mode: 'question_only',
|
|
16
|
+
expiryFallback: 'pause'
|
|
17
|
+
},
|
|
18
|
+
rlm: {
|
|
19
|
+
policy: 'always',
|
|
20
|
+
environment: 'docker',
|
|
21
|
+
allowedEnvironments: ['docker'],
|
|
22
|
+
maxIterations: 50,
|
|
23
|
+
maxSubcalls: 200,
|
|
24
|
+
maxSubcallDepth: 1,
|
|
25
|
+
wallClockTimeoutMs: 30 * 60 * 1000,
|
|
26
|
+
budgetUsd: 0,
|
|
27
|
+
budgetTokens: 0,
|
|
28
|
+
rootModel: '',
|
|
29
|
+
subModel: ''
|
|
30
|
+
},
|
|
31
|
+
runner: {
|
|
32
|
+
mode: 'prod',
|
|
33
|
+
allowedModes: ['prod']
|
|
34
|
+
},
|
|
35
|
+
ui: {
|
|
36
|
+
controlEnabled: false,
|
|
37
|
+
bindHost: '127.0.0.1',
|
|
38
|
+
allowedBindHosts: ['127.0.0.1'],
|
|
39
|
+
allowedRunRoots: DEFAULT_ALLOWED_RUN_ROOTS
|
|
40
|
+
},
|
|
41
|
+
github: {
|
|
42
|
+
enabled: false,
|
|
43
|
+
operations: []
|
|
44
|
+
},
|
|
45
|
+
paths: {
|
|
46
|
+
allowedRoots: []
|
|
47
|
+
},
|
|
48
|
+
confirm: {
|
|
49
|
+
autoPause: true,
|
|
50
|
+
maxPending: 3,
|
|
51
|
+
expiresInMs: 15 * 60 * 1000
|
|
52
|
+
},
|
|
53
|
+
sandbox: {
|
|
54
|
+
network: false
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
const SOURCE_PRIORITY = {
|
|
58
|
+
global: 1,
|
|
59
|
+
repo: 2,
|
|
60
|
+
env: 3,
|
|
61
|
+
cli: 4
|
|
62
|
+
};
|
|
63
|
+
export async function loadDelegationConfigFiles(options) {
|
|
64
|
+
const env = options.env ?? process.env;
|
|
65
|
+
const codexHome = options.codexHome ?? resolveCodexHome(env);
|
|
66
|
+
const globalPath = join(codexHome, 'config.toml');
|
|
67
|
+
const repoPath = join(options.repoRoot, '.codex', 'orchestrator.toml');
|
|
68
|
+
const globalRaw = await readTomlFile(globalPath);
|
|
69
|
+
const repoRaw = await readTomlFile(repoPath);
|
|
70
|
+
return {
|
|
71
|
+
global: globalRaw ? normalizeLayer(globalRaw, 'global') : null,
|
|
72
|
+
repo: repoRaw ? normalizeLayer(repoRaw, 'repo') : null
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export function computeEffectiveDelegationConfig(options) {
|
|
76
|
+
const sorted = [...options.layers].sort((a, b) => SOURCE_PRIORITY[a.source] - SOURCE_PRIORITY[b.source]);
|
|
77
|
+
const merged = sorted.reduce((acc, layer) => mergeLayer(acc, layer), {
|
|
78
|
+
source: 'global'
|
|
79
|
+
});
|
|
80
|
+
const repoLayer = sorted.find((layer) => layer.source === 'repo');
|
|
81
|
+
const defaults = structuredClone(DEFAULT_CONFIG);
|
|
82
|
+
const effective = {
|
|
83
|
+
delegate: {
|
|
84
|
+
...defaults.delegate,
|
|
85
|
+
...(merged.delegate ?? {})
|
|
86
|
+
},
|
|
87
|
+
rlm: {
|
|
88
|
+
...defaults.rlm,
|
|
89
|
+
...(merged.rlm ?? {})
|
|
90
|
+
},
|
|
91
|
+
runner: {
|
|
92
|
+
...defaults.runner,
|
|
93
|
+
...(merged.runner ?? {})
|
|
94
|
+
},
|
|
95
|
+
ui: {
|
|
96
|
+
...defaults.ui,
|
|
97
|
+
...(merged.ui ?? {})
|
|
98
|
+
},
|
|
99
|
+
github: {
|
|
100
|
+
...defaults.github
|
|
101
|
+
},
|
|
102
|
+
paths: {
|
|
103
|
+
...defaults.paths,
|
|
104
|
+
...(merged.paths ?? {})
|
|
105
|
+
},
|
|
106
|
+
confirm: {
|
|
107
|
+
...defaults.confirm,
|
|
108
|
+
...(merged.confirm ?? {})
|
|
109
|
+
},
|
|
110
|
+
sandbox: {
|
|
111
|
+
...defaults.sandbox,
|
|
112
|
+
...(merged.sandbox ?? {})
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
if (effective.delegate.mode !== 'full' && effective.delegate.mode !== 'question_only') {
|
|
116
|
+
effective.delegate.mode = defaults.delegate.mode;
|
|
117
|
+
}
|
|
118
|
+
const repoAllowedRoots = typeof repoLayer?.paths?.allowedRoots !== 'undefined' ? repoLayer.paths.allowedRoots : [options.repoRoot];
|
|
119
|
+
const repoCapRoots = normalizeRoots(repoAllowedRoots);
|
|
120
|
+
const requestedRoots = typeof merged.paths?.allowedRoots !== 'undefined' ? merged.paths.allowedRoots : repoCapRoots;
|
|
121
|
+
const candidateRoots = normalizeRoots(requestedRoots);
|
|
122
|
+
effective.paths.allowedRoots = intersectRoots(repoCapRoots, candidateRoots);
|
|
123
|
+
const repoToolCap = repoLayer?.delegate?.allowedToolServers ?? [];
|
|
124
|
+
const requestedToolProfile = merged.delegate?.toolProfile ?? repoToolCap;
|
|
125
|
+
effective.delegate.allowedToolServers = [...repoToolCap];
|
|
126
|
+
effective.delegate.toolProfile = intersectExact(repoToolCap, requestedToolProfile);
|
|
127
|
+
const repoAllowedModes = repoLayer?.runner?.allowedModes ?? defaults.runner.allowedModes;
|
|
128
|
+
effective.runner.allowedModes = repoAllowedModes;
|
|
129
|
+
const requestedMode = merged.runner?.mode ?? defaults.runner.mode;
|
|
130
|
+
effective.runner.mode = repoAllowedModes.includes(requestedMode) ? requestedMode : repoAllowedModes[0] ?? defaults.runner.mode;
|
|
131
|
+
const repoAllowedEnvs = repoLayer?.rlm?.allowedEnvironments ?? defaults.rlm.allowedEnvironments;
|
|
132
|
+
effective.rlm.allowedEnvironments = repoAllowedEnvs;
|
|
133
|
+
const requestedEnv = merged.rlm?.environment ?? defaults.rlm.environment;
|
|
134
|
+
effective.rlm.environment = repoAllowedEnvs.includes(requestedEnv) ? requestedEnv : repoAllowedEnvs[0] ?? defaults.rlm.environment;
|
|
135
|
+
const repoAllowedBindHosts = repoLayer?.ui?.allowedBindHosts ?? defaults.ui.allowedBindHosts;
|
|
136
|
+
effective.ui.allowedBindHosts = repoAllowedBindHosts;
|
|
137
|
+
const requestedBindHost = merged.ui?.bindHost ?? defaults.ui.bindHost;
|
|
138
|
+
effective.ui.bindHost = repoAllowedBindHosts.includes(requestedBindHost)
|
|
139
|
+
? requestedBindHost
|
|
140
|
+
: repoAllowedBindHosts[0] ?? defaults.ui.bindHost;
|
|
141
|
+
const repoAllowedRunRoots = typeof repoLayer?.ui?.allowedRunRoots !== 'undefined' ? repoLayer.ui.allowedRunRoots : [options.repoRoot];
|
|
142
|
+
const repoCapRunRoots = normalizeRoots(repoAllowedRunRoots);
|
|
143
|
+
const hasRunRootsOverride = typeof repoLayer?.ui?.allowedRunRoots !== 'undefined' || typeof merged.ui?.allowedRunRoots !== 'undefined';
|
|
144
|
+
const requestedRunRoots = typeof merged.ui?.allowedRunRoots !== 'undefined' ? merged.ui.allowedRunRoots : repoCapRunRoots;
|
|
145
|
+
const candidateRunRoots = normalizeRoots(requestedRunRoots);
|
|
146
|
+
effective.ui.allowedRunRoots = intersectRoots(repoCapRunRoots, candidateRunRoots);
|
|
147
|
+
if (hasRunRootsOverride && effective.ui.allowedRunRoots.length === 0 && repoCapRunRoots.length > 0) {
|
|
148
|
+
logger.warn('ui.allowedRunRoots override produced empty intersection with repo cap; UI run access disabled.');
|
|
149
|
+
}
|
|
150
|
+
const repoAllowNetwork = Boolean(repoLayer?.sandbox?.network ?? defaults.sandbox.network);
|
|
151
|
+
effective.sandbox.network = repoAllowNetwork && Boolean(merged.sandbox?.network ?? defaults.sandbox.network);
|
|
152
|
+
const githubEnabled = Boolean(repoLayer?.github?.enabled ?? false);
|
|
153
|
+
effective.github.enabled = githubEnabled;
|
|
154
|
+
effective.github.operations = githubEnabled ? [...(repoLayer?.github?.operations ?? [])] : [];
|
|
155
|
+
if (!hasRunRootsOverride && effective.ui.allowedRunRoots.length === 0) {
|
|
156
|
+
effective.ui.allowedRunRoots = [options.repoRoot];
|
|
157
|
+
}
|
|
158
|
+
return effective;
|
|
159
|
+
}
|
|
160
|
+
function mergeLayer(base, update) {
|
|
161
|
+
const merged = {
|
|
162
|
+
...base,
|
|
163
|
+
source: update.source
|
|
164
|
+
};
|
|
165
|
+
merged.delegate = mergeSection(base.delegate, update.delegate);
|
|
166
|
+
merged.rlm = mergeSection(base.rlm, update.rlm);
|
|
167
|
+
merged.runner = mergeSection(base.runner, update.runner);
|
|
168
|
+
merged.ui = mergeSection(base.ui, update.ui);
|
|
169
|
+
merged.github = mergeSection(base.github, update.github);
|
|
170
|
+
merged.paths = mergeSection(base.paths, update.paths);
|
|
171
|
+
merged.confirm = mergeSection(base.confirm, update.confirm);
|
|
172
|
+
merged.sandbox = mergeSection(base.sandbox, update.sandbox);
|
|
173
|
+
return merged;
|
|
174
|
+
}
|
|
175
|
+
function mergeSection(base, update) {
|
|
176
|
+
if (!update) {
|
|
177
|
+
return base;
|
|
178
|
+
}
|
|
179
|
+
if (!base) {
|
|
180
|
+
const cleaned = {};
|
|
181
|
+
for (const [key, value] of Object.entries(update)) {
|
|
182
|
+
if (typeof value === 'undefined') {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (Array.isArray(value)) {
|
|
186
|
+
cleaned[key] = [...value];
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
190
|
+
cleaned[key] = { ...value };
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
cleaned[key] = value;
|
|
194
|
+
}
|
|
195
|
+
return cleaned;
|
|
196
|
+
}
|
|
197
|
+
const merged = { ...base };
|
|
198
|
+
for (const [key, value] of Object.entries(update)) {
|
|
199
|
+
if (typeof value === 'undefined') {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (Array.isArray(value)) {
|
|
203
|
+
merged[key] = [...value];
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
207
|
+
merged[key] = { ...merged[key], ...value };
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
merged[key] = value;
|
|
211
|
+
}
|
|
212
|
+
return merged;
|
|
213
|
+
}
|
|
214
|
+
async function readTomlFile(path) {
|
|
215
|
+
try {
|
|
216
|
+
const raw = await readFile(path, 'utf8');
|
|
217
|
+
const parsed = toml.parse(raw);
|
|
218
|
+
return parsed;
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
if (error.code === 'ENOENT') {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
throw error;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
function normalizeLayer(raw, source) {
|
|
228
|
+
return {
|
|
229
|
+
source,
|
|
230
|
+
delegate: normalizeDelegate(raw.delegate),
|
|
231
|
+
rlm: normalizeRlm(raw.rlm),
|
|
232
|
+
runner: normalizeRunner(raw.runner),
|
|
233
|
+
ui: normalizeUi(raw.ui),
|
|
234
|
+
github: normalizeGithub(raw.github),
|
|
235
|
+
paths: normalizePaths(raw.paths),
|
|
236
|
+
confirm: normalizeConfirm(raw.confirm),
|
|
237
|
+
sandbox: normalizeSandbox(raw.sandbox)
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
function normalizeDelegate(value) {
|
|
241
|
+
const record = asRecord(value);
|
|
242
|
+
if (!record)
|
|
243
|
+
return undefined;
|
|
244
|
+
return {
|
|
245
|
+
allowNested: asBoolean(record.allow_nested ?? record.allowNested),
|
|
246
|
+
toolProfile: asStringArray(record.tool_profile ?? record.toolProfile),
|
|
247
|
+
allowedToolServers: asStringArray(record.allowed_tool_servers ?? record.allowedToolServers),
|
|
248
|
+
mode: asString(record.mode),
|
|
249
|
+
expiryFallback: asString(record.question_expiry_fallback ?? record.expiryFallback)
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function normalizeRlm(value) {
|
|
253
|
+
const record = asRecord(value);
|
|
254
|
+
if (!record)
|
|
255
|
+
return undefined;
|
|
256
|
+
return {
|
|
257
|
+
policy: asString(record.policy),
|
|
258
|
+
environment: asString(record.environment) ?? asString(record.env),
|
|
259
|
+
allowedEnvironments: asStringArray(record.allowed_environments ?? record.allowedEnvironments),
|
|
260
|
+
maxIterations: asNumber(record.max_iterations ?? record.maxIterations),
|
|
261
|
+
maxSubcalls: asNumber(record.max_subcalls ?? record.maxSubcalls),
|
|
262
|
+
maxSubcallDepth: asNumber(record.max_subcall_depth ?? record.maxSubcallDepth),
|
|
263
|
+
wallClockTimeoutMs: asNumber(record.wall_clock_timeout_ms ?? record.wallClockTimeoutMs),
|
|
264
|
+
budgetUsd: asNumber(record.budget_usd ?? record.budgetUsd),
|
|
265
|
+
budgetTokens: asNumber(record.budget_tokens ?? record.budgetTokens),
|
|
266
|
+
rootModel: asString(record.root_model ?? record.rootModel),
|
|
267
|
+
subModel: asString(record.sub_model ?? record.subModel)
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
function normalizeRunner(value) {
|
|
271
|
+
const record = asRecord(value);
|
|
272
|
+
if (!record)
|
|
273
|
+
return undefined;
|
|
274
|
+
return {
|
|
275
|
+
mode: asString(record.mode),
|
|
276
|
+
allowedModes: asStringArray(record.allowed_modes ?? record.allowedModes)
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
function normalizeUi(value) {
|
|
280
|
+
const record = asRecord(value);
|
|
281
|
+
if (!record)
|
|
282
|
+
return undefined;
|
|
283
|
+
return {
|
|
284
|
+
controlEnabled: asBoolean(record.control_enabled ?? record.controlEnabled),
|
|
285
|
+
bindHost: asString(record.bind_host ?? record.bindHost),
|
|
286
|
+
allowedBindHosts: asStringArray(record.allowed_bind_hosts ?? record.allowedBindHosts),
|
|
287
|
+
allowedRunRoots: asStringArray(record.allowed_run_roots ?? record.allowedRunRoots)
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
function normalizeGithub(value) {
|
|
291
|
+
const record = asRecord(value);
|
|
292
|
+
if (!record)
|
|
293
|
+
return undefined;
|
|
294
|
+
return {
|
|
295
|
+
enabled: asBoolean(record.enabled),
|
|
296
|
+
operations: asStringArray(record.operations)
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
function normalizePaths(value) {
|
|
300
|
+
const record = asRecord(value);
|
|
301
|
+
if (!record)
|
|
302
|
+
return undefined;
|
|
303
|
+
return {
|
|
304
|
+
allowedRoots: asStringArray(record.allowed_roots ?? record.allowedRoots)
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
function normalizeConfirm(value) {
|
|
308
|
+
const record = asRecord(value);
|
|
309
|
+
if (!record)
|
|
310
|
+
return undefined;
|
|
311
|
+
return {
|
|
312
|
+
autoPause: asBoolean(record.auto_pause ?? record.autoPause),
|
|
313
|
+
maxPending: asNumber(record.max_pending ?? record.maxPending),
|
|
314
|
+
expiresInMs: asNumber(record.expires_in_ms ?? record.expiresInMs)
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
function normalizeSandbox(value) {
|
|
318
|
+
const record = asRecord(value);
|
|
319
|
+
if (!record)
|
|
320
|
+
return undefined;
|
|
321
|
+
return {
|
|
322
|
+
network: asBoolean(record.network)
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
function asRecord(value) {
|
|
326
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
return value;
|
|
330
|
+
}
|
|
331
|
+
function asBoolean(value) {
|
|
332
|
+
if (typeof value === 'boolean') {
|
|
333
|
+
return value;
|
|
334
|
+
}
|
|
335
|
+
return undefined;
|
|
336
|
+
}
|
|
337
|
+
function asString(value) {
|
|
338
|
+
if (typeof value === 'string' && value.trim().length > 0) {
|
|
339
|
+
return value.trim();
|
|
340
|
+
}
|
|
341
|
+
return undefined;
|
|
342
|
+
}
|
|
343
|
+
function asStringArray(value) {
|
|
344
|
+
if (value === null || typeof value === 'undefined')
|
|
345
|
+
return undefined;
|
|
346
|
+
if (Array.isArray(value)) {
|
|
347
|
+
return value.filter((entry) => typeof entry === 'string' && entry.trim().length > 0).map((entry) => entry.trim());
|
|
348
|
+
}
|
|
349
|
+
if (typeof value === 'string') {
|
|
350
|
+
const trimmed = value.trim();
|
|
351
|
+
return trimmed.length > 0 ? [trimmed] : [];
|
|
352
|
+
}
|
|
353
|
+
return undefined;
|
|
354
|
+
}
|
|
355
|
+
function asNumber(value) {
|
|
356
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
357
|
+
return value;
|
|
358
|
+
}
|
|
359
|
+
return undefined;
|
|
360
|
+
}
|
|
361
|
+
function resolveCodexHome(env) {
|
|
362
|
+
const override = env.CODEX_HOME?.trim();
|
|
363
|
+
if (override) {
|
|
364
|
+
return isAbsolute(override) ? override : resolve(process.cwd(), override);
|
|
365
|
+
}
|
|
366
|
+
return join(homedir(), '.codex');
|
|
367
|
+
}
|
|
368
|
+
function normalizeRoots(roots) {
|
|
369
|
+
const normalized = roots
|
|
370
|
+
.filter((root) => typeof root === 'string')
|
|
371
|
+
.map((root) => realpathSafe(resolve(root)))
|
|
372
|
+
.filter((root) => root.length > 0);
|
|
373
|
+
return Array.from(new Set(normalized));
|
|
374
|
+
}
|
|
375
|
+
function intersectExact(cap, requested) {
|
|
376
|
+
if (cap.length === 0 || requested.length === 0) {
|
|
377
|
+
return [];
|
|
378
|
+
}
|
|
379
|
+
const set = new Set(cap);
|
|
380
|
+
return requested.filter((entry) => set.has(entry));
|
|
381
|
+
}
|
|
382
|
+
function intersectRoots(cap, requested) {
|
|
383
|
+
if (cap.length === 0 || requested.length === 0) {
|
|
384
|
+
return [];
|
|
385
|
+
}
|
|
386
|
+
const resolvedCap = cap.map((root) => realpathSafe(resolve(root)));
|
|
387
|
+
return requested
|
|
388
|
+
.map((root) => realpathSafe(resolve(root)))
|
|
389
|
+
.filter((candidate) => resolvedCap.some((allowed) => isWithinRoot(allowed, candidate)));
|
|
390
|
+
}
|
|
391
|
+
function isWithinRoot(root, candidate) {
|
|
392
|
+
const normalizedRoot = normalizePath(realpathSafe(resolve(root)));
|
|
393
|
+
const normalizedCandidate = normalizePath(realpathSafe(resolve(candidate)));
|
|
394
|
+
if (normalizedRoot === normalizedCandidate) {
|
|
395
|
+
return true;
|
|
396
|
+
}
|
|
397
|
+
const relativePath = relative(normalizedRoot, normalizedCandidate);
|
|
398
|
+
if (!relativePath) {
|
|
399
|
+
return true;
|
|
400
|
+
}
|
|
401
|
+
if (isAbsolute(relativePath)) {
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
return !relativePath.startsWith(`..${sep}`) && relativePath !== '..';
|
|
405
|
+
}
|
|
406
|
+
function realpathSafe(pathname) {
|
|
407
|
+
try {
|
|
408
|
+
return realpathSync(pathname);
|
|
409
|
+
}
|
|
410
|
+
catch {
|
|
411
|
+
return pathname;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
function normalizePath(pathname) {
|
|
415
|
+
return process.platform === 'win32' ? pathname.toLowerCase() : pathname;
|
|
416
|
+
}
|
|
417
|
+
export function resolveConfigDir(pathname) {
|
|
418
|
+
return dirname(pathname);
|
|
419
|
+
}
|
|
420
|
+
export function splitDelegationConfigOverrides(raw) {
|
|
421
|
+
if (!raw) {
|
|
422
|
+
return [];
|
|
423
|
+
}
|
|
424
|
+
const entries = [];
|
|
425
|
+
let current = '';
|
|
426
|
+
let bracketDepth = 0;
|
|
427
|
+
let inSingle = false;
|
|
428
|
+
let inDouble = false;
|
|
429
|
+
let escaping = false;
|
|
430
|
+
const flush = () => {
|
|
431
|
+
const trimmed = current.trim();
|
|
432
|
+
if (trimmed) {
|
|
433
|
+
entries.push(trimmed);
|
|
434
|
+
}
|
|
435
|
+
current = '';
|
|
436
|
+
};
|
|
437
|
+
for (const char of raw) {
|
|
438
|
+
if (escaping) {
|
|
439
|
+
current += char;
|
|
440
|
+
escaping = false;
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
if ((inSingle || inDouble) && char === '\\') {
|
|
444
|
+
current += char;
|
|
445
|
+
escaping = true;
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
if (!inDouble && char === '\'') {
|
|
449
|
+
inSingle = !inSingle;
|
|
450
|
+
current += char;
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
if (!inSingle && char === '"') {
|
|
454
|
+
inDouble = !inDouble;
|
|
455
|
+
current += char;
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
if (!inSingle && !inDouble) {
|
|
459
|
+
if (char === '[') {
|
|
460
|
+
bracketDepth += 1;
|
|
461
|
+
}
|
|
462
|
+
else if (char === ']') {
|
|
463
|
+
bracketDepth = Math.max(0, bracketDepth - 1);
|
|
464
|
+
}
|
|
465
|
+
if (bracketDepth === 0 && (char === ',' || char === ';' || char === '\n')) {
|
|
466
|
+
flush();
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
current += char;
|
|
471
|
+
}
|
|
472
|
+
flush();
|
|
473
|
+
return entries;
|
|
474
|
+
}
|
|
475
|
+
export function parseDelegationConfigOverride(value, source) {
|
|
476
|
+
const trimmed = value.trim();
|
|
477
|
+
if (!trimmed) {
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
480
|
+
const parsed = toml.parse(trimmed);
|
|
481
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
482
|
+
return null;
|
|
483
|
+
}
|
|
484
|
+
return normalizeLayer(parsed, source);
|
|
485
|
+
}
|