bosun 0.41.2 → 0.41.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/.env.example +1 -1
- package/agent/agent-pool.mjs +9 -2
- package/agent/agent-prompt-catalog.mjs +971 -0
- package/agent/agent-prompts.mjs +2 -970
- package/agent/agent-supervisor.mjs +119 -6
- package/agent/autofix-git.mjs +33 -0
- package/agent/autofix-prompts.mjs +151 -0
- package/agent/autofix.mjs +11 -175
- package/agent/bosun-skills.mjs +3 -2
- package/bosun.config.example.json +17 -0
- package/bosun.schema.json +87 -188
- package/cli.mjs +34 -1
- package/config/config-doctor.mjs +5 -250
- package/config/config-file-names.mjs +5 -0
- package/config/config.mjs +89 -493
- package/config/executor-config.mjs +493 -0
- package/config/repo-root.mjs +1 -2
- package/config/workspace-health.mjs +242 -0
- package/git/git-safety.mjs +15 -0
- package/github/github-oauth-portal.mjs +46 -0
- package/infra/library-manager-utils.mjs +22 -0
- package/infra/library-manager-well-known-sources.mjs +578 -0
- package/infra/library-manager.mjs +512 -1030
- package/infra/monitor.mjs +35 -9
- package/infra/session-tracker.mjs +10 -7
- package/kanban/kanban-adapter.mjs +17 -1
- package/lib/codebase-audit-manifests.mjs +117 -0
- package/lib/codebase-audit.mjs +18 -115
- package/package.json +18 -3
- package/server/setup-web-server.mjs +58 -5
- package/server/ui-server.mjs +1394 -79
- package/shell/codex-config-file.mjs +178 -0
- package/shell/codex-config.mjs +538 -575
- package/task/task-cli.mjs +54 -3
- package/task/task-executor.mjs +143 -13
- package/task/task-store.mjs +409 -1
- package/telegram/telegram-bot.mjs +127 -0
- package/tools/apply-pr-suggestions.mjs +401 -0
- package/tools/syntax-check.mjs +28 -9
- package/ui/app.js +3 -14
- package/ui/components/kanban-board.js +227 -4
- package/ui/components/session-list.js +85 -5
- package/ui/demo-defaults.js +338 -84
- package/ui/demo.html +155 -0
- package/ui/modules/session-api.js +96 -0
- package/ui/modules/settings-schema.js +1 -2
- package/ui/modules/state.js +43 -3
- package/ui/setup.html +4 -5
- package/ui/styles/components.css +58 -4
- package/ui/tabs/agents.js +12 -15
- package/ui/tabs/control.js +1 -0
- package/ui/tabs/library.js +484 -22
- package/ui/tabs/manual-flows.js +105 -29
- package/ui/tabs/tasks.js +848 -141
- package/ui/tabs/telemetry.js +129 -11
- package/ui/tabs/workflow-canvas-utils.mjs +130 -0
- package/ui/tabs/workflows.js +293 -23
- package/voice/voice-tool-definitions.mjs +757 -0
- package/voice/voice-tools.mjs +34 -778
- package/workflow/manual-flow-audit.mjs +165 -0
- package/workflow/manual-flows.mjs +164 -259
- package/workflow/workflow-engine.mjs +147 -58
- package/workflow/workflow-nodes/definitions.mjs +1207 -0
- package/workflow/workflow-nodes/transforms.mjs +612 -0
- package/workflow/workflow-nodes.mjs +358 -63
- package/workflow/workflow-templates.mjs +313 -191
- package/workflow-templates/_helpers.mjs +154 -0
- package/workflow-templates/agents.mjs +61 -4
- package/workflow-templates/code-quality.mjs +7 -7
- package/workflow-templates/github.mjs +20 -10
- package/workflow-templates/task-batch.mjs +44 -11
- package/workflow-templates/task-lifecycle.mjs +31 -6
- package/workspace/worktree-manager.mjs +277 -3
package/shell/codex-config.mjs
CHANGED
|
@@ -26,11 +26,17 @@
|
|
|
26
26
|
* append or patch well-known sections rather than rewriting the whole file.
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
|
-
import { existsSync, readFileSync,
|
|
29
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
30
30
|
import { resolve, dirname, parse, isAbsolute } from "node:path";
|
|
31
|
-
import { homedir } from "node:os";
|
|
32
31
|
import { fileURLToPath } from "node:url";
|
|
33
32
|
import { resolveCodexProfileRuntime } from "./codex-model-profiles.mjs";
|
|
33
|
+
import {
|
|
34
|
+
CONFIG_PATH,
|
|
35
|
+
ensureTrustedProjects,
|
|
36
|
+
getConfigPath,
|
|
37
|
+
readCodexConfig,
|
|
38
|
+
writeCodexConfig,
|
|
39
|
+
} from "./codex-config-file.mjs";
|
|
34
40
|
|
|
35
41
|
const __filename = fileURLToPath(import.meta.url);
|
|
36
42
|
const __dirname = dirname(__filename);
|
|
@@ -51,9 +57,6 @@ function getVibeKanbanVersion() {
|
|
|
51
57
|
|
|
52
58
|
// ── Constants ────────────────────────────────────────────────────────────────
|
|
53
59
|
|
|
54
|
-
const CODEX_DIR = resolve(homedir(), ".codex");
|
|
55
|
-
const CONFIG_PATH = resolve(CODEX_DIR, "config.toml");
|
|
56
|
-
|
|
57
60
|
/** Minimum recommended stream idle timeout (ms) for complex agentic tasks. */
|
|
58
61
|
const MIN_STREAM_IDLE_TIMEOUT_MS = 300_000; // 5 minutes
|
|
59
62
|
|
|
@@ -101,20 +104,6 @@ function buildDefaultAgentSdkBlock(primary = "codex") {
|
|
|
101
104
|
].join("\n");
|
|
102
105
|
}
|
|
103
106
|
|
|
104
|
-
/**
|
|
105
|
-
* @deprecated No longer used — max_threads is now managed under [agent_sdk].
|
|
106
|
-
* The [agents] section in Codex CLI uses serde(flatten) to parse all keys
|
|
107
|
-
* as agent role names, so bare scalar keys like max_threads = 12 cause:
|
|
108
|
-
* "invalid length 1, expected struct AgentRoleToml with 2 elements"
|
|
109
|
-
* Kept only for migration removal of stale [agents] sections.
|
|
110
|
-
*/
|
|
111
|
-
const buildAgentsBlock = (_maxThreads) =>
|
|
112
|
-
[
|
|
113
|
-
"",
|
|
114
|
-
"# ── Agent roles (added by bosun) ──",
|
|
115
|
-
AGENTS_HEADER,
|
|
116
|
-
"",
|
|
117
|
-
].join("\n");
|
|
118
107
|
|
|
119
108
|
// ── Feature Flags ────────────────────────────────────────────────────────────
|
|
120
109
|
|
|
@@ -205,109 +194,166 @@ function resolveAgentMaxThreads(envOverrides = process.env) {
|
|
|
205
194
|
*
|
|
206
195
|
* Also migrates any stale max_threads from [agents] if found.
|
|
207
196
|
*/
|
|
208
|
-
|
|
209
|
-
toml
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
skipped: false,
|
|
197
|
+
function findSectionRange(toml, header) {
|
|
198
|
+
const headerIdx = toml.indexOf(header);
|
|
199
|
+
if (headerIdx === -1) return null;
|
|
200
|
+
const afterHeader = headerIdx + header.length;
|
|
201
|
+
const nextSection = toml.indexOf("\n[", afterHeader);
|
|
202
|
+
const sectionEnd = nextSection === -1 ? toml.length : nextSection;
|
|
203
|
+
return {
|
|
204
|
+
headerIdx,
|
|
205
|
+
afterHeader,
|
|
206
|
+
sectionEnd,
|
|
207
|
+
section: toml.substring(afterHeader, sectionEnd),
|
|
220
208
|
};
|
|
209
|
+
}
|
|
221
210
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
return
|
|
211
|
+
function stripStaleAgentsMaxThreads(section) {
|
|
212
|
+
const staleRegex = /^[ \t]*#[^\n]*max.*threads[^\n]*\n?|^[ \t]*max_threads\s*=\s*\d+[^\n]*\n?/gm;
|
|
213
|
+
if (!staleRegex.test(section)) {
|
|
214
|
+
return { section, changed: false };
|
|
226
215
|
}
|
|
227
|
-
|
|
216
|
+
return {
|
|
217
|
+
section: section.replace(staleRegex, ""),
|
|
218
|
+
changed: true,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
228
221
|
|
|
229
|
-
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
}
|
|
242
|
-
// If [agents] section is now empty (only whitespace/comments about agents),
|
|
243
|
-
// remove the whole section to avoid confusing Codex CLI
|
|
244
|
-
const updatedAgentsIdx = toml.indexOf(AGENTS_HEADER);
|
|
245
|
-
if (updatedAgentsIdx !== -1) {
|
|
246
|
-
const afterUpdated = updatedAgentsIdx + AGENTS_HEADER.length;
|
|
247
|
-
const nextUpdated = toml.indexOf("\n[", afterUpdated);
|
|
248
|
-
const endUpdated = nextUpdated === -1 ? toml.length : nextUpdated;
|
|
249
|
-
const remaining = toml.substring(afterUpdated, endUpdated).trim();
|
|
250
|
-
// If only whitespace or the bosun comment header remains, remove entire section
|
|
251
|
-
if (!remaining || remaining.split(/\r?\n/).every((line) => {
|
|
252
|
-
const trimmed = String(line || "").trim();
|
|
253
|
-
return !trimmed || trimmed.startsWith("#");
|
|
254
|
-
})) {
|
|
255
|
-
// Remove from the line before [agents] header to section end
|
|
256
|
-
const lineStart = toml.lastIndexOf("\n", updatedAgentsIdx);
|
|
257
|
-
const removeFrom = lineStart === -1 ? updatedAgentsIdx : lineStart;
|
|
258
|
-
toml = toml.substring(0, removeFrom) + toml.substring(endUpdated);
|
|
259
|
-
result.changed = true;
|
|
260
|
-
}
|
|
261
|
-
}
|
|
222
|
+
function sectionHasOnlyComments(section) {
|
|
223
|
+
const remaining = String(section || "").trim();
|
|
224
|
+
return !remaining || remaining.split(/\r?\n/).every((line) => {
|
|
225
|
+
const trimmed = String(line || "").trim();
|
|
226
|
+
return !trimmed || trimmed.startsWith("#");
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function removeStaleAgentsMaxThreads(toml) {
|
|
231
|
+
let nextToml = toml;
|
|
232
|
+
const agentsSection = findSectionRange(nextToml, AGENTS_HEADER);
|
|
233
|
+
if (!agentsSection) {
|
|
234
|
+
return { toml: nextToml, changed: false };
|
|
262
235
|
}
|
|
263
236
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
237
|
+
const strippedSection = stripStaleAgentsMaxThreads(agentsSection.section);
|
|
238
|
+
if (!strippedSection.changed) {
|
|
239
|
+
return { toml: nextToml, changed: false };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
nextToml =
|
|
243
|
+
nextToml.substring(0, agentsSection.afterHeader) +
|
|
244
|
+
strippedSection.section +
|
|
245
|
+
nextToml.substring(agentsSection.sectionEnd);
|
|
246
|
+
|
|
247
|
+
const updatedAgentsSection = findSectionRange(nextToml, AGENTS_HEADER);
|
|
248
|
+
if (!updatedAgentsSection) {
|
|
249
|
+
return { toml: nextToml, changed: true };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (!sectionHasOnlyComments(updatedAgentsSection.section)) {
|
|
253
|
+
return { toml: nextToml, changed: true };
|
|
272
254
|
}
|
|
273
255
|
|
|
256
|
+
const lineStart = nextToml.lastIndexOf("\n", updatedAgentsSection.headerIdx);
|
|
257
|
+
const removeFrom = lineStart === -1 ? updatedAgentsSection.headerIdx : lineStart;
|
|
258
|
+
return {
|
|
259
|
+
toml: nextToml.substring(0, removeFrom) + nextToml.substring(updatedAgentsSection.sectionEnd),
|
|
260
|
+
changed: true,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
function resolveAgentSdkSectionRange(toml) {
|
|
264
|
+
const sdkIdx = toml.indexOf(AGENT_SDK_HEADER);
|
|
265
|
+
if (sdkIdx === -1) return null;
|
|
274
266
|
const afterSdkHeader = sdkIdx + AGENT_SDK_HEADER.length;
|
|
275
|
-
// Find the end of [agent_sdk] — either [agent_sdk.capabilities] or the next section
|
|
276
267
|
const capsIdx = toml.indexOf(AGENT_SDK_CAPS_HEADER, afterSdkHeader);
|
|
277
268
|
const nextSectionIdx = toml.indexOf("\n[", afterSdkHeader);
|
|
278
|
-
// Use the capabilities sub-section boundary or next top-level section
|
|
279
269
|
let sdkSectionEnd;
|
|
280
270
|
if (capsIdx !== -1 && (nextSectionIdx === -1 || capsIdx <= nextSectionIdx)) {
|
|
281
271
|
sdkSectionEnd = capsIdx;
|
|
282
272
|
} else {
|
|
283
273
|
sdkSectionEnd = nextSectionIdx === -1 ? toml.length : nextSectionIdx;
|
|
284
274
|
}
|
|
275
|
+
return { afterSdkHeader, sdkSectionEnd };
|
|
276
|
+
}
|
|
285
277
|
|
|
286
|
-
|
|
278
|
+
function upsertAgentSdkMaxThreads(toml, desired, overwrite) {
|
|
279
|
+
const sdkRange = resolveAgentSdkSectionRange(toml);
|
|
280
|
+
if (!sdkRange) {
|
|
281
|
+
return {
|
|
282
|
+
toml,
|
|
283
|
+
changed: true,
|
|
284
|
+
existing: null,
|
|
285
|
+
added: true,
|
|
286
|
+
updated: false,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
287
289
|
|
|
290
|
+
let sdkSection = toml.substring(sdkRange.afterSdkHeader, sdkRange.sdkSectionEnd);
|
|
288
291
|
const maxThreadsRegex = /^max_threads\s*=\s*(\d+)/m;
|
|
289
292
|
const match = sdkSection.match(maxThreadsRegex);
|
|
290
293
|
if (match) {
|
|
291
|
-
|
|
292
|
-
if (overwrite
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
294
|
+
const existing = parsePositiveInt(match[1]);
|
|
295
|
+
if (!overwrite || existing === desired) {
|
|
296
|
+
return {
|
|
297
|
+
toml,
|
|
298
|
+
changed: false,
|
|
299
|
+
existing,
|
|
300
|
+
added: false,
|
|
301
|
+
updated: false,
|
|
302
|
+
};
|
|
296
303
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
304
|
+
sdkSection = sdkSection.replace(maxThreadsRegex, `max_threads = ${desired}`);
|
|
305
|
+
return {
|
|
306
|
+
toml: toml.substring(0, sdkRange.afterSdkHeader) + sdkSection + toml.substring(sdkRange.sdkSectionEnd),
|
|
307
|
+
changed: true,
|
|
308
|
+
existing,
|
|
309
|
+
added: false,
|
|
310
|
+
updated: true,
|
|
311
|
+
};
|
|
302
312
|
}
|
|
303
313
|
|
|
304
|
-
|
|
305
|
-
|
|
314
|
+
sdkSection = sdkSection.trimEnd() + `\nmax_threads = ${desired}\n`;
|
|
315
|
+
return {
|
|
316
|
+
toml: toml.substring(0, sdkRange.afterSdkHeader) + sdkSection + toml.substring(sdkRange.sdkSectionEnd),
|
|
317
|
+
changed: true,
|
|
318
|
+
existing: null,
|
|
319
|
+
added: true,
|
|
320
|
+
updated: false,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export function ensureAgentMaxThreads(
|
|
325
|
+
toml,
|
|
326
|
+
{ maxThreads, overwrite = false } = {},
|
|
327
|
+
) {
|
|
328
|
+
const result = {
|
|
329
|
+
toml,
|
|
330
|
+
changed: false,
|
|
331
|
+
existing: null,
|
|
332
|
+
applied: null,
|
|
333
|
+
added: false,
|
|
334
|
+
updated: false,
|
|
335
|
+
skipped: false,
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const desired = parsePositiveInt(maxThreads);
|
|
339
|
+
if (!desired) {
|
|
340
|
+
result.skipped = true;
|
|
341
|
+
return result;
|
|
306
342
|
}
|
|
343
|
+
result.applied = desired;
|
|
344
|
+
|
|
345
|
+
const migratedAgents = removeStaleAgentsMaxThreads(toml);
|
|
346
|
+
toml = migratedAgents.toml;
|
|
347
|
+
result.changed = migratedAgents.changed;
|
|
307
348
|
|
|
349
|
+
const agentSdkUpdate = upsertAgentSdkMaxThreads(toml, desired, overwrite);
|
|
350
|
+
result.toml = agentSdkUpdate.toml;
|
|
351
|
+
result.existing = agentSdkUpdate.existing;
|
|
352
|
+
result.added = agentSdkUpdate.added;
|
|
353
|
+
result.updated = agentSdkUpdate.updated;
|
|
354
|
+
result.changed = result.changed || agentSdkUpdate.changed;
|
|
308
355
|
return result;
|
|
309
356
|
}
|
|
310
|
-
|
|
311
357
|
/**
|
|
312
358
|
* Check whether config has a [features] section.
|
|
313
359
|
*/
|
|
@@ -711,6 +757,20 @@ export function buildSandboxWorkspaceWrite(options = {}) {
|
|
|
711
757
|
if (desiredRoots.length === 0) {
|
|
712
758
|
return "";
|
|
713
759
|
}
|
|
760
|
+
return buildSandboxWorkspaceWriteBlock({
|
|
761
|
+
desiredRoots,
|
|
762
|
+
networkAccess,
|
|
763
|
+
excludeTmpdirEnvVar,
|
|
764
|
+
excludeSlashTmp,
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function buildSandboxWorkspaceWriteBlock({
|
|
769
|
+
desiredRoots,
|
|
770
|
+
networkAccess,
|
|
771
|
+
excludeTmpdirEnvVar,
|
|
772
|
+
excludeSlashTmp,
|
|
773
|
+
}) {
|
|
714
774
|
return [
|
|
715
775
|
"",
|
|
716
776
|
"# ── Workspace-write sandbox defaults (added by bosun) ──",
|
|
@@ -723,6 +783,66 @@ export function buildSandboxWorkspaceWrite(options = {}) {
|
|
|
723
783
|
].join("\n");
|
|
724
784
|
}
|
|
725
785
|
|
|
786
|
+
function findTomlSection(toml, header) {
|
|
787
|
+
const headerIdx = toml.indexOf(header);
|
|
788
|
+
if (headerIdx === -1) return null;
|
|
789
|
+
const afterHeader = headerIdx + header.length;
|
|
790
|
+
const nextSection = toml.indexOf("\n[", afterHeader);
|
|
791
|
+
const sectionEnd = nextSection === -1 ? toml.length : nextSection;
|
|
792
|
+
return {
|
|
793
|
+
headerIdx,
|
|
794
|
+
afterHeader,
|
|
795
|
+
sectionEnd,
|
|
796
|
+
section: toml.substring(afterHeader, sectionEnd),
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function ensureSandboxWorkspaceFlags(section, flags) {
|
|
801
|
+
let nextSection = section;
|
|
802
|
+
let changed = false;
|
|
803
|
+
for (const [key, value] of Object.entries(flags)) {
|
|
804
|
+
const keyRegex = new RegExp(`^${escapeRegex(key)}\\s*=`, "m");
|
|
805
|
+
if (keyRegex.test(nextSection)) continue;
|
|
806
|
+
nextSection = nextSection.trimEnd() + `\n${key} = ${value}\n`;
|
|
807
|
+
changed = true;
|
|
808
|
+
}
|
|
809
|
+
return { section: nextSection, changed };
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function mergeSandboxWorkspaceRoots(section, desiredRoots, repoRoot) {
|
|
813
|
+
const rootsRegex = /^writable_roots\s*=\s*(\[[^\]]*\])\s*$/m;
|
|
814
|
+
const match = section.match(rootsRegex);
|
|
815
|
+
if (!match) {
|
|
816
|
+
if (desiredRoots.length === 0) {
|
|
817
|
+
return { section, changed: false, rootsAdded: [] };
|
|
818
|
+
}
|
|
819
|
+
return {
|
|
820
|
+
section: section.trimEnd() + `\nwritable_roots = ${formatTomlArray(desiredRoots)}\n`,
|
|
821
|
+
changed: true,
|
|
822
|
+
rootsAdded: desiredRoots,
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
const existingRoots = parseTomlArrayLiteral(match[1]);
|
|
827
|
+
const validExisting = existingRoots.filter((root) => root === "/tmp" || existsSync(root));
|
|
828
|
+
const merged = normalizeWritableRoots(validExisting, { repoRoot, validateExistence: true });
|
|
829
|
+
const rootsAdded = [];
|
|
830
|
+
for (const root of desiredRoots) {
|
|
831
|
+
if (merged.includes(root)) continue;
|
|
832
|
+
merged.push(root);
|
|
833
|
+
rootsAdded.push(root);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
let changed = existingRoots.some((root) => root !== "/tmp" && !existsSync(root));
|
|
837
|
+
const formatted = formatTomlArray(merged);
|
|
838
|
+
if (formatted !== match[1]) {
|
|
839
|
+
section = section.replace(rootsRegex, `writable_roots = ${formatted}`);
|
|
840
|
+
changed = true;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
return { section, changed, rootsAdded };
|
|
844
|
+
}
|
|
845
|
+
|
|
726
846
|
export function ensureSandboxWorkspaceWrite(toml, options = {}) {
|
|
727
847
|
const {
|
|
728
848
|
writableRoots = [],
|
|
@@ -738,124 +858,86 @@ export function ensureSandboxWorkspaceWrite(toml, options = {}) {
|
|
|
738
858
|
if (desiredRoots.length === 0) {
|
|
739
859
|
return { toml, changed: false, added: false, rootsAdded: [] };
|
|
740
860
|
}
|
|
741
|
-
const block = [
|
|
742
|
-
"",
|
|
743
|
-
"# ── Workspace-write sandbox defaults (added by bosun) ──",
|
|
744
|
-
"[sandbox_workspace_write]",
|
|
745
|
-
`network_access = ${networkAccess}`,
|
|
746
|
-
`exclude_tmpdir_env_var = ${excludeTmpdirEnvVar}`,
|
|
747
|
-
`exclude_slash_tmp = ${excludeSlashTmp}`,
|
|
748
|
-
`writable_roots = ${formatTomlArray(desiredRoots)}`,
|
|
749
|
-
"",
|
|
750
|
-
].join("\n");
|
|
751
861
|
return {
|
|
752
|
-
toml: toml.trimEnd() + "\n" +
|
|
862
|
+
toml: toml.trimEnd() + "\n" + buildSandboxWorkspaceWriteBlock({
|
|
863
|
+
desiredRoots,
|
|
864
|
+
networkAccess,
|
|
865
|
+
excludeTmpdirEnvVar,
|
|
866
|
+
excludeSlashTmp,
|
|
867
|
+
}),
|
|
753
868
|
changed: true,
|
|
754
869
|
added: true,
|
|
755
870
|
rootsAdded: desiredRoots,
|
|
756
871
|
};
|
|
757
872
|
}
|
|
758
873
|
|
|
759
|
-
const
|
|
760
|
-
|
|
761
|
-
if (headerIdx === -1) {
|
|
874
|
+
const sectionInfo = findTomlSection(toml, "[sandbox_workspace_write]");
|
|
875
|
+
if (!sectionInfo) {
|
|
762
876
|
return { toml, changed: false, added: false, rootsAdded: [] };
|
|
763
877
|
}
|
|
764
878
|
|
|
765
|
-
const
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
const ensureFlag = (key, value) => {
|
|
773
|
-
const keyRegex = new RegExp(`^${escapeRegex(key)}\\s*=`, "m");
|
|
774
|
-
if (!keyRegex.test(section)) {
|
|
775
|
-
section = section.trimEnd() + `\n${key} = ${value}\n`;
|
|
776
|
-
changed = true;
|
|
777
|
-
}
|
|
778
|
-
};
|
|
779
|
-
|
|
780
|
-
ensureFlag("network_access", networkAccess);
|
|
781
|
-
ensureFlag("exclude_tmpdir_env_var", excludeTmpdirEnvVar);
|
|
782
|
-
ensureFlag("exclude_slash_tmp", excludeSlashTmp);
|
|
783
|
-
|
|
784
|
-
const rootsRegex = /^writable_roots\s*=\s*(\[[^\]]*\])\s*$/m;
|
|
785
|
-
const match = section.match(rootsRegex);
|
|
786
|
-
if (match) {
|
|
787
|
-
const existingRoots = parseTomlArrayLiteral(match[1]);
|
|
788
|
-
// Filter out stale roots that no longer exist on disk
|
|
789
|
-
const validExisting = existingRoots.filter((r) => r === "/tmp" || existsSync(r));
|
|
790
|
-
const merged = normalizeWritableRoots(validExisting, { repoRoot, validateExistence: true });
|
|
791
|
-
for (const root of desiredRoots) {
|
|
792
|
-
if (!merged.includes(root)) {
|
|
793
|
-
merged.push(root);
|
|
794
|
-
rootsAdded.push(root);
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
// Track any roots that were removed due to non-existence
|
|
798
|
-
const staleRemoved = existingRoots.filter((r) => r !== "/tmp" && !existsSync(r));
|
|
799
|
-
if (staleRemoved.length > 0) changed = true;
|
|
800
|
-
const formatted = formatTomlArray(merged);
|
|
801
|
-
if (formatted !== match[1]) {
|
|
802
|
-
section = section.replace(rootsRegex, `writable_roots = ${formatted}`);
|
|
803
|
-
changed = true;
|
|
804
|
-
}
|
|
805
|
-
} else if (desiredRoots.length > 0) {
|
|
806
|
-
section = section.trimEnd() + `\nwritable_roots = ${formatTomlArray(desiredRoots)}\n`;
|
|
807
|
-
rootsAdded = desiredRoots;
|
|
808
|
-
changed = true;
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
if (!changed) {
|
|
879
|
+
const flagsResult = ensureSandboxWorkspaceFlags(sectionInfo.section, {
|
|
880
|
+
network_access: networkAccess,
|
|
881
|
+
exclude_tmpdir_env_var: excludeTmpdirEnvVar,
|
|
882
|
+
exclude_slash_tmp: excludeSlashTmp,
|
|
883
|
+
});
|
|
884
|
+
const rootsResult = mergeSandboxWorkspaceRoots(flagsResult.section, desiredRoots, repoRoot);
|
|
885
|
+
if (!flagsResult.changed && !rootsResult.changed) {
|
|
812
886
|
return { toml, changed: false, added: false, rootsAdded: [] };
|
|
813
887
|
}
|
|
814
888
|
|
|
815
|
-
const updatedToml =
|
|
816
|
-
toml.substring(0, afterHeader) + section + toml.substring(sectionEnd);
|
|
817
|
-
|
|
818
889
|
return {
|
|
819
|
-
toml:
|
|
890
|
+
toml:
|
|
891
|
+
toml.substring(0, sectionInfo.afterHeader) +
|
|
892
|
+
rootsResult.section +
|
|
893
|
+
toml.substring(sectionInfo.sectionEnd),
|
|
820
894
|
changed: true,
|
|
821
895
|
added: false,
|
|
822
|
-
rootsAdded,
|
|
896
|
+
rootsAdded: rootsResult.rootsAdded,
|
|
823
897
|
};
|
|
824
898
|
}
|
|
825
899
|
|
|
826
900
|
/**
|
|
827
|
-
*
|
|
828
|
-
*
|
|
829
|
-
*
|
|
830
|
-
*
|
|
901
|
+
* Prunes non-existent entries from the `[sandbox_workspace_write]` writable_roots list.
|
|
902
|
+
*
|
|
903
|
+
* Looks up the `writable_roots` array in the `[sandbox_workspace_write]` section,
|
|
904
|
+
* checks each path on disk, and removes any roots that no longer exist. The `/tmp`
|
|
905
|
+
* root is always preserved, even if it cannot be checked reliably. Returns the
|
|
906
|
+
* updated TOML (if any change was made), a `changed` flag, and the list of roots
|
|
907
|
+
* that were removed.
|
|
908
|
+
*
|
|
909
|
+
* @param {string} toml - The full Codex TOML configuration contents.
|
|
910
|
+
* @returns {{ toml: string, changed: boolean, removed: string[] }} Result of pruning.
|
|
831
911
|
*/
|
|
832
912
|
export function pruneStaleSandboxRoots(toml) {
|
|
833
913
|
if (!hasSandboxWorkspaceWrite(toml)) {
|
|
834
914
|
return { toml, changed: false, removed: [] };
|
|
835
915
|
}
|
|
836
|
-
const
|
|
837
|
-
|
|
838
|
-
if (headerIdx === -1) return { toml, changed: false, removed: [] };
|
|
839
|
-
const afterHeader = headerIdx + header.length;
|
|
840
|
-
const nextSection = toml.indexOf("\n[", afterHeader);
|
|
841
|
-
const sectionEnd = nextSection === -1 ? toml.length : nextSection;
|
|
842
|
-
let section = toml.substring(afterHeader, sectionEnd);
|
|
916
|
+
const sectionInfo = findTomlSection(toml, "[sandbox_workspace_write]");
|
|
917
|
+
if (!sectionInfo) return { toml, changed: false, removed: [] };
|
|
843
918
|
|
|
844
919
|
const rootsRegex = /^writable_roots\s*=\s*(\[[^\]]*\])\s*$/m;
|
|
845
|
-
const match = section.match(rootsRegex);
|
|
920
|
+
const match = sectionInfo.section.match(rootsRegex);
|
|
846
921
|
if (!match) return { toml, changed: false, removed: [] };
|
|
847
922
|
|
|
848
923
|
const existing = parseTomlArrayLiteral(match[1]);
|
|
849
|
-
const valid = existing.filter((
|
|
850
|
-
const removed = existing.filter((
|
|
924
|
+
const valid = existing.filter((root) => root === "/tmp" || existsSync(root));
|
|
925
|
+
const removed = existing.filter((root) => root !== "/tmp" && !existsSync(root));
|
|
851
926
|
if (removed.length === 0) return { toml, changed: false, removed: [] };
|
|
852
927
|
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
928
|
+
const nextSection = sectionInfo.section.replace(
|
|
929
|
+
rootsRegex,
|
|
930
|
+
`writable_roots = ${formatTomlArray(valid)}`,
|
|
931
|
+
);
|
|
932
|
+
return {
|
|
933
|
+
toml:
|
|
934
|
+
toml.substring(0, sectionInfo.afterHeader) +
|
|
935
|
+
nextSection +
|
|
936
|
+
toml.substring(sectionInfo.sectionEnd),
|
|
937
|
+
changed: true,
|
|
938
|
+
removed,
|
|
939
|
+
};
|
|
857
940
|
}
|
|
858
|
-
|
|
859
941
|
/**
|
|
860
942
|
* Build the [shell_environment_policy] section.
|
|
861
943
|
* Default: inherit = "all" so .NET, Go, Node etc. env vars are visible.
|
|
@@ -888,38 +970,67 @@ export function hasMicrosoftDocsMcp(toml) {
|
|
|
888
970
|
}
|
|
889
971
|
|
|
890
972
|
/**
|
|
891
|
-
* Build MCP server blocks for context7
|
|
892
|
-
* These are universally useful for
|
|
973
|
+
* Build MCP server blocks for common servers: context7, sequential-thinking,
|
|
974
|
+
* playwright, and microsoft-docs. These are universally useful for
|
|
975
|
+
* documentation lookups and related tasks.
|
|
893
976
|
*/
|
|
894
|
-
|
|
977
|
+
const COMMON_MCP_SERVER_DEFS = [
|
|
978
|
+
{
|
|
979
|
+
name: "context7",
|
|
980
|
+
headerComment: "# ── Common MCP servers (added by bosun) ──",
|
|
981
|
+
lines: [
|
|
982
|
+
"[mcp_servers.context7]",
|
|
983
|
+
"startup_timeout_sec = 120",
|
|
984
|
+
'command = "npx"',
|
|
985
|
+
'args = ["-y", "@upstash/context7-mcp"]',
|
|
986
|
+
],
|
|
987
|
+
isPresent: hasContext7Mcp,
|
|
988
|
+
},
|
|
989
|
+
{
|
|
990
|
+
name: "sequential-thinking",
|
|
991
|
+
lines: [
|
|
992
|
+
"[mcp_servers.sequential-thinking]",
|
|
993
|
+
"startup_timeout_sec = 120",
|
|
994
|
+
'command = "npx"',
|
|
995
|
+
'args = ["-y", "@modelcontextprotocol/server-sequential-thinking"]',
|
|
996
|
+
],
|
|
997
|
+
isPresent: (toml) => hasNamedMcpServer(toml, "sequential-thinking"),
|
|
998
|
+
},
|
|
999
|
+
{
|
|
1000
|
+
name: "playwright",
|
|
1001
|
+
lines: [
|
|
1002
|
+
"[mcp_servers.playwright]",
|
|
1003
|
+
"startup_timeout_sec = 120",
|
|
1004
|
+
'command = "npx"',
|
|
1005
|
+
'args = ["-y", "@playwright/mcp@latest"]',
|
|
1006
|
+
],
|
|
1007
|
+
isPresent: (toml) => hasNamedMcpServer(toml, "playwright"),
|
|
1008
|
+
},
|
|
1009
|
+
{
|
|
1010
|
+
name: "microsoft-docs",
|
|
1011
|
+
lines: [
|
|
1012
|
+
"[mcp_servers.microsoft-docs]",
|
|
1013
|
+
'url = "https://learn.microsoft.com/api/mcp"',
|
|
1014
|
+
'# NOTE: Tool list intentionally limited to avoid Azure Responses API schema-size/parser issues.',
|
|
1015
|
+
'tools = ["microsoft_docs_search", "microsoft_code_sample_search"]',
|
|
1016
|
+
],
|
|
1017
|
+
isPresent: hasMicrosoftDocsMcp,
|
|
1018
|
+
},
|
|
1019
|
+
];
|
|
1020
|
+
|
|
1021
|
+
function buildCommonMcpBlock(definition) {
|
|
895
1022
|
return [
|
|
896
1023
|
"",
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
"startup_timeout_sec = 120",
|
|
900
|
-
'command = "npx"',
|
|
901
|
-
'args = ["-y", "@upstash/context7-mcp"]',
|
|
902
|
-
"",
|
|
903
|
-
"[mcp_servers.sequential-thinking]",
|
|
904
|
-
"startup_timeout_sec = 120",
|
|
905
|
-
'command = "npx"',
|
|
906
|
-
'args = ["-y", "@modelcontextprotocol/server-sequential-thinking"]',
|
|
907
|
-
"",
|
|
908
|
-
"[mcp_servers.playwright]",
|
|
909
|
-
"startup_timeout_sec = 120",
|
|
910
|
-
'command = "npx"',
|
|
911
|
-
'args = ["-y", "@playwright/mcp@latest"]',
|
|
912
|
-
"",
|
|
913
|
-
"[mcp_servers.microsoft-docs]",
|
|
914
|
-
'url = "https://learn.microsoft.com/api/mcp"',
|
|
915
|
-
// microsoft_docs_fetch description alone is ~2KB and breaks the Azure
|
|
916
|
-
// Responses API JSON parser when combined with other MCP tool schemas.
|
|
917
|
-
// Keep only the two search tools which are sufficient for most use cases.
|
|
918
|
-
'tools = ["microsoft_docs_search", "microsoft_code_sample_search"]',
|
|
1024
|
+
...(definition.headerComment ? [definition.headerComment] : []),
|
|
1025
|
+
...definition.lines,
|
|
919
1026
|
"",
|
|
920
1027
|
].join("\n");
|
|
921
1028
|
}
|
|
922
1029
|
|
|
1030
|
+
export function buildCommonMcpBlocks() {
|
|
1031
|
+
return COMMON_MCP_SERVER_DEFS.map(buildCommonMcpBlock).join("");
|
|
1032
|
+
}
|
|
1033
|
+
|
|
923
1034
|
function hasNamedMcpServer(toml, name) {
|
|
924
1035
|
return new RegExp(`^\\[mcp_servers\\.${escapeRegex(name)}\\]`, "m").test(
|
|
925
1036
|
toml,
|
|
@@ -966,28 +1077,11 @@ function stripDeprecatedSandboxPermissions(toml) {
|
|
|
966
1077
|
|
|
967
1078
|
// ── Public API ───────────────────────────────────────────────────────────────
|
|
968
1079
|
|
|
969
|
-
|
|
970
|
-
* Read the current config.toml (or return empty string if it doesn't exist).
|
|
971
|
-
*/
|
|
972
|
-
export function readCodexConfig() {
|
|
973
|
-
if (!existsSync(CONFIG_PATH)) return "";
|
|
974
|
-
return readFileSync(CONFIG_PATH, "utf8");
|
|
975
|
-
}
|
|
1080
|
+
export { readCodexConfig };
|
|
976
1081
|
|
|
977
|
-
|
|
978
|
-
* Write the config.toml, creating ~/.codex/ if needed.
|
|
979
|
-
*/
|
|
980
|
-
export function writeCodexConfig(content) {
|
|
981
|
-
mkdirSync(CODEX_DIR, { recursive: true });
|
|
982
|
-
writeFileSync(CONFIG_PATH, content, "utf8");
|
|
983
|
-
}
|
|
1082
|
+
export { writeCodexConfig };
|
|
984
1083
|
|
|
985
|
-
|
|
986
|
-
* Get the path to the Codex config file.
|
|
987
|
-
*/
|
|
988
|
-
export function getConfigPath() {
|
|
989
|
-
return CONFIG_PATH;
|
|
990
|
-
}
|
|
1084
|
+
export { getConfigPath };
|
|
991
1085
|
|
|
992
1086
|
/**
|
|
993
1087
|
* Check whether the config already has a [mcp_servers.vibe_kanban] section.
|
|
@@ -1327,55 +1421,7 @@ export function ensureRetrySettings(toml, providerName) {
|
|
|
1327
1421
|
* @param {object} [opts.env] Environment overrides (defaults to process.env)
|
|
1328
1422
|
* @param {string} [opts.primarySdk] Primary agent SDK: "codex", "copilot", or "claude"
|
|
1329
1423
|
*/
|
|
1330
|
-
|
|
1331
|
-
vkBaseUrl = "http://127.0.0.1:54089",
|
|
1332
|
-
skipVk = true,
|
|
1333
|
-
manageVkMcp = false,
|
|
1334
|
-
dryRun = false,
|
|
1335
|
-
env = process.env,
|
|
1336
|
-
primarySdk,
|
|
1337
|
-
} = {}) {
|
|
1338
|
-
const result = {
|
|
1339
|
-
path: CONFIG_PATH,
|
|
1340
|
-
created: false,
|
|
1341
|
-
vkAdded: false,
|
|
1342
|
-
vkRemoved: false,
|
|
1343
|
-
vkEnvUpdated: false,
|
|
1344
|
-
agentSdkAdded: false,
|
|
1345
|
-
featuresAdded: [],
|
|
1346
|
-
agentMaxThreads: null,
|
|
1347
|
-
agentMaxThreadsSkipped: null,
|
|
1348
|
-
sandboxAdded: false,
|
|
1349
|
-
sandboxWorkspaceAdded: false,
|
|
1350
|
-
sandboxWorkspaceUpdated: false,
|
|
1351
|
-
sandboxWorkspaceRootsAdded: [],
|
|
1352
|
-
sandboxStaleRootsRemoved: [],
|
|
1353
|
-
shellEnvAdded: false,
|
|
1354
|
-
commonMcpAdded: false,
|
|
1355
|
-
profileProvidersAdded: [],
|
|
1356
|
-
timeoutsFixed: [],
|
|
1357
|
-
retriesAdded: [],
|
|
1358
|
-
trustedProjectsAdded: [],
|
|
1359
|
-
noChanges: true,
|
|
1360
|
-
};
|
|
1361
|
-
|
|
1362
|
-
const configExisted = existsSync(CONFIG_PATH);
|
|
1363
|
-
const originalToml = readCodexConfig();
|
|
1364
|
-
let toml = stripDeprecatedSandboxPermissions(originalToml);
|
|
1365
|
-
if (!configExisted) {
|
|
1366
|
-
result.created = true;
|
|
1367
|
-
toml = "";
|
|
1368
|
-
}
|
|
1369
|
-
|
|
1370
|
-
const sandboxModeResult = ensureTopLevelSandboxMode(
|
|
1371
|
-
toml,
|
|
1372
|
-
env.CODEX_SANDBOX_MODE,
|
|
1373
|
-
);
|
|
1374
|
-
toml = sandboxModeResult.toml;
|
|
1375
|
-
if (sandboxModeResult.changed) {
|
|
1376
|
-
result.sandboxAdded = true;
|
|
1377
|
-
}
|
|
1378
|
-
|
|
1424
|
+
function resolveSandboxWorkspaceOptions(env) {
|
|
1379
1425
|
const repoRoot =
|
|
1380
1426
|
env.BOSUN_AGENT_REPO_ROOT ||
|
|
1381
1427
|
env.REPO_ROOT ||
|
|
@@ -1384,160 +1430,149 @@ export function ensureCodexConfig({
|
|
|
1384
1430
|
const additionalRoots = env.BOSUN_WORKSPACES_DIR
|
|
1385
1431
|
? [env.BOSUN_WORKSPACES_DIR]
|
|
1386
1432
|
: [];
|
|
1387
|
-
|
|
1433
|
+
return {
|
|
1388
1434
|
repoRoot,
|
|
1389
1435
|
additionalRoots,
|
|
1390
1436
|
writableRoots: env.CODEX_SANDBOX_WRITABLE_ROOTS,
|
|
1391
|
-
}
|
|
1392
|
-
|
|
1437
|
+
};
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
function applySandboxDefaults(toml, env, result) {
|
|
1441
|
+
const sandboxModeResult = ensureTopLevelSandboxMode(
|
|
1442
|
+
toml,
|
|
1443
|
+
env.CODEX_SANDBOX_MODE,
|
|
1444
|
+
);
|
|
1445
|
+
let nextToml = sandboxModeResult.toml;
|
|
1446
|
+
if (sandboxModeResult.changed) {
|
|
1447
|
+
result.sandboxAdded = true;
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
const sandboxOptions = resolveSandboxWorkspaceOptions(env);
|
|
1451
|
+
const sandboxWorkspaceResult = ensureSandboxWorkspaceWrite(nextToml, sandboxOptions);
|
|
1452
|
+
nextToml = sandboxWorkspaceResult.toml;
|
|
1393
1453
|
result.sandboxWorkspaceAdded = sandboxWorkspaceResult.added;
|
|
1394
1454
|
result.sandboxWorkspaceUpdated =
|
|
1395
1455
|
sandboxWorkspaceResult.changed && !sandboxWorkspaceResult.added;
|
|
1396
1456
|
result.sandboxWorkspaceRootsAdded = sandboxWorkspaceResult.rootsAdded;
|
|
1397
1457
|
|
|
1398
|
-
const pruneResult = pruneStaleSandboxRoots(
|
|
1399
|
-
|
|
1458
|
+
const pruneResult = pruneStaleSandboxRoots(nextToml);
|
|
1459
|
+
nextToml = pruneResult.toml;
|
|
1400
1460
|
result.sandboxStaleRootsRemoved = pruneResult.removed;
|
|
1401
1461
|
|
|
1402
|
-
if (!hasShellEnvPolicy(
|
|
1403
|
-
|
|
1462
|
+
if (!hasShellEnvPolicy(nextToml)) {
|
|
1463
|
+
nextToml += buildShellEnvPolicy(env.CODEX_SHELL_ENV_POLICY || "all");
|
|
1404
1464
|
result.shellEnvAdded = true;
|
|
1405
1465
|
}
|
|
1406
1466
|
|
|
1467
|
+
return { toml: nextToml, ...sandboxOptions };
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
function normalizePrimarySdkName(primarySdk, env) {
|
|
1407
1471
|
const rawPrimary = String(primarySdk || env.PRIMARY_AGENT || "codex")
|
|
1408
1472
|
.trim()
|
|
1409
1473
|
.toLowerCase();
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1474
|
+
if (rawPrimary === "copilot" || rawPrimary.includes("copilot")) return "copilot";
|
|
1475
|
+
if (rawPrimary === "claude" || rawPrimary.includes("claude")) return "claude";
|
|
1476
|
+
if (rawPrimary === "codex" || rawPrimary.includes("codex")) return "codex";
|
|
1477
|
+
return "codex";
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
function applyAgentSdkDefaults(toml, env, primarySdk, result) {
|
|
1481
|
+
let nextToml = toml;
|
|
1482
|
+
const normalizedPrimary = normalizePrimarySdkName(primarySdk, env);
|
|
1483
|
+
if (!hasAgentSdkConfig(nextToml)) {
|
|
1484
|
+
nextToml += buildAgentSdkBlock({ primary: normalizedPrimary });
|
|
1420
1485
|
result.agentSdkAdded = true;
|
|
1421
1486
|
}
|
|
1422
1487
|
|
|
1423
1488
|
const maxThreads = resolveAgentMaxThreads(env);
|
|
1424
1489
|
if (maxThreads.explicit && !maxThreads.value) {
|
|
1425
1490
|
result.agentMaxThreadsSkipped = String(maxThreads.raw);
|
|
1426
|
-
|
|
1427
|
-
const maxThreadsResult = ensureAgentMaxThreads(toml, {
|
|
1428
|
-
maxThreads: maxThreads.value,
|
|
1429
|
-
overwrite: maxThreads.explicit,
|
|
1430
|
-
});
|
|
1431
|
-
toml = maxThreadsResult.toml;
|
|
1432
|
-
if (maxThreadsResult.changed && !maxThreadsResult.skipped) {
|
|
1433
|
-
result.agentMaxThreads = {
|
|
1434
|
-
from: maxThreadsResult.existing,
|
|
1435
|
-
to: maxThreadsResult.applied,
|
|
1436
|
-
explicit: maxThreads.explicit,
|
|
1437
|
-
};
|
|
1438
|
-
} else if (maxThreadsResult.skipped && maxThreads.explicit) {
|
|
1439
|
-
result.agentMaxThreadsSkipped = String(maxThreads.raw);
|
|
1440
|
-
}
|
|
1491
|
+
return nextToml;
|
|
1441
1492
|
}
|
|
1442
1493
|
|
|
1443
|
-
const
|
|
1444
|
-
|
|
1445
|
-
|
|
1494
|
+
const maxThreadsResult = ensureAgentMaxThreads(nextToml, {
|
|
1495
|
+
maxThreads: maxThreads.value,
|
|
1496
|
+
overwrite: maxThreads.explicit,
|
|
1497
|
+
});
|
|
1498
|
+
nextToml = maxThreadsResult.toml;
|
|
1499
|
+
if (maxThreadsResult.changed && !maxThreadsResult.skipped) {
|
|
1500
|
+
result.agentMaxThreads = {
|
|
1501
|
+
from: maxThreadsResult.existing,
|
|
1502
|
+
to: maxThreadsResult.applied,
|
|
1503
|
+
explicit: maxThreads.explicit,
|
|
1504
|
+
};
|
|
1505
|
+
} else if (maxThreadsResult.skipped && maxThreads.explicit) {
|
|
1506
|
+
result.agentMaxThreadsSkipped = String(maxThreads.raw);
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
return nextToml;
|
|
1510
|
+
}
|
|
1446
1511
|
|
|
1512
|
+
function applyVibeKanbanDefaults(toml, { manageVkMcp, skipVk, vkBaseUrl }, result) {
|
|
1513
|
+
let nextToml = toml;
|
|
1447
1514
|
const shouldManageGlobalVkMcp = Boolean(manageVkMcp) && !skipVk;
|
|
1448
1515
|
if (!shouldManageGlobalVkMcp) {
|
|
1449
|
-
if (hasVibeKanbanMcp(
|
|
1450
|
-
|
|
1516
|
+
if (hasVibeKanbanMcp(nextToml)) {
|
|
1517
|
+
nextToml = removeVibeKanbanMcp(nextToml);
|
|
1451
1518
|
result.vkRemoved = true;
|
|
1452
1519
|
}
|
|
1453
|
-
|
|
1454
|
-
|
|
1520
|
+
return nextToml;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
if (!hasVibeKanbanMcp(nextToml)) {
|
|
1524
|
+
nextToml += buildVibeKanbanBlock({ vkBaseUrl });
|
|
1455
1525
|
result.vkAdded = true;
|
|
1526
|
+
return nextToml;
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
const vkEnvValues = {
|
|
1530
|
+
VK_BASE_URL: vkBaseUrl,
|
|
1531
|
+
VK_ENDPOINT_URL: vkBaseUrl,
|
|
1532
|
+
};
|
|
1533
|
+
const beforeVkEnv = nextToml;
|
|
1534
|
+
if (!hasVibeKanbanEnv(nextToml)) {
|
|
1535
|
+
nextToml =
|
|
1536
|
+
nextToml.trimEnd() +
|
|
1537
|
+
"\n\n[mcp_servers.vibe_kanban.env]\n" +
|
|
1538
|
+
'VK_BASE_URL = "' + vkBaseUrl + '"\n' +
|
|
1539
|
+
'VK_ENDPOINT_URL = "' + vkBaseUrl + '"\n';
|
|
1456
1540
|
} else {
|
|
1457
|
-
|
|
1458
|
-
VK_BASE_URL: vkBaseUrl,
|
|
1459
|
-
VK_ENDPOINT_URL: vkBaseUrl,
|
|
1460
|
-
};
|
|
1461
|
-
const beforeVkEnv = toml;
|
|
1462
|
-
if (!hasVibeKanbanEnv(toml)) {
|
|
1463
|
-
toml =
|
|
1464
|
-
toml.trimEnd() +
|
|
1465
|
-
"\n\n[mcp_servers.vibe_kanban.env]\n" +
|
|
1466
|
-
`VK_BASE_URL = "${vkBaseUrl}"\n` +
|
|
1467
|
-
`VK_ENDPOINT_URL = "${vkBaseUrl}"\n`;
|
|
1468
|
-
} else {
|
|
1469
|
-
toml = updateVibeKanbanEnv(toml, vkEnvValues);
|
|
1470
|
-
}
|
|
1471
|
-
if (toml !== beforeVkEnv) {
|
|
1472
|
-
result.vkEnvUpdated = true;
|
|
1473
|
-
}
|
|
1541
|
+
nextToml = updateVibeKanbanEnv(nextToml, vkEnvValues);
|
|
1474
1542
|
}
|
|
1543
|
+
if (nextToml !== beforeVkEnv) {
|
|
1544
|
+
result.vkEnvUpdated = true;
|
|
1545
|
+
}
|
|
1546
|
+
return nextToml;
|
|
1547
|
+
}
|
|
1475
1548
|
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
"startup_timeout_sec = 120",
|
|
1484
|
-
'command = "npx"',
|
|
1485
|
-
'args = ["-y", "@upstash/context7-mcp"]',
|
|
1486
|
-
"",
|
|
1487
|
-
].join("\n"),
|
|
1488
|
-
},
|
|
1489
|
-
{
|
|
1490
|
-
present: hasNamedMcpServer(toml, "sequential-thinking"),
|
|
1491
|
-
block: [
|
|
1492
|
-
"",
|
|
1493
|
-
"[mcp_servers.sequential-thinking]",
|
|
1494
|
-
"startup_timeout_sec = 120",
|
|
1495
|
-
'command = "npx"',
|
|
1496
|
-
'args = ["-y", "@modelcontextprotocol/server-sequential-thinking"]',
|
|
1497
|
-
"",
|
|
1498
|
-
].join("\n"),
|
|
1499
|
-
},
|
|
1500
|
-
{
|
|
1501
|
-
present: hasNamedMcpServer(toml, "playwright"),
|
|
1502
|
-
block: [
|
|
1503
|
-
"",
|
|
1504
|
-
"[mcp_servers.playwright]",
|
|
1505
|
-
"startup_timeout_sec = 120",
|
|
1506
|
-
'command = "npx"',
|
|
1507
|
-
'args = ["-y", "@playwright/mcp@latest"]',
|
|
1508
|
-
"",
|
|
1509
|
-
].join("\n"),
|
|
1510
|
-
},
|
|
1511
|
-
{
|
|
1512
|
-
present: hasMicrosoftDocsMcp(toml),
|
|
1513
|
-
block: [
|
|
1514
|
-
"",
|
|
1515
|
-
"[mcp_servers.microsoft-docs]",
|
|
1516
|
-
'url = "https://learn.microsoft.com/api/mcp"',
|
|
1517
|
-
'tools = ["microsoft_docs_search", "microsoft_code_sample_search"]',
|
|
1518
|
-
"",
|
|
1519
|
-
].join("\n"),
|
|
1520
|
-
},
|
|
1521
|
-
];
|
|
1522
|
-
for (const item of commonMcpBlocks) {
|
|
1523
|
-
if (item.present) continue;
|
|
1524
|
-
toml += item.block;
|
|
1525
|
-
result.commonMcpAdded = true;
|
|
1549
|
+
function ensureCommonMcpDefaults(toml, result) {
|
|
1550
|
+
let nextToml = toml;
|
|
1551
|
+
for (const definition of COMMON_MCP_SERVER_DEFS) {
|
|
1552
|
+
if (!definition.isPresent(nextToml)) {
|
|
1553
|
+
nextToml += buildCommonMcpBlock(definition);
|
|
1554
|
+
result.commonMcpAdded = true;
|
|
1555
|
+
}
|
|
1526
1556
|
}
|
|
1527
1557
|
|
|
1528
1558
|
for (const serverName of ["context7", "sequential-thinking", "playwright"]) {
|
|
1529
|
-
const timeoutResult = ensureMcpStartupTimeout(
|
|
1530
|
-
|
|
1559
|
+
const timeoutResult = ensureMcpStartupTimeout(nextToml, serverName, 120);
|
|
1560
|
+
nextToml = timeoutResult.toml;
|
|
1531
1561
|
}
|
|
1532
1562
|
|
|
1533
|
-
|
|
1534
|
-
|
|
1563
|
+
return nextToml;
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
function applyModelProviderDefaults(toml, env, result) {
|
|
1567
|
+
let nextToml = toml;
|
|
1568
|
+
const providerResult = ensureModelProviderSectionsFromEnv(nextToml, env);
|
|
1569
|
+
nextToml = providerResult.toml;
|
|
1535
1570
|
result.profileProvidersAdded = providerResult.added;
|
|
1536
1571
|
|
|
1537
|
-
const timeoutAudit = auditStreamTimeouts(
|
|
1572
|
+
const timeoutAudit = auditStreamTimeouts(nextToml);
|
|
1538
1573
|
for (const item of timeoutAudit) {
|
|
1539
1574
|
if (!item.needsUpdate) continue;
|
|
1540
|
-
|
|
1575
|
+
nextToml = setStreamTimeout(nextToml, item.provider, RECOMMENDED_STREAM_IDLE_TIMEOUT_MS);
|
|
1541
1576
|
result.timeoutsFixed.push({
|
|
1542
1577
|
provider: item.provider,
|
|
1543
1578
|
from: item.currentValue,
|
|
@@ -1545,25 +1580,18 @@ export function ensureCodexConfig({
|
|
|
1545
1580
|
});
|
|
1546
1581
|
}
|
|
1547
1582
|
|
|
1548
|
-
const
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
if (toml !== beforeRetry) {
|
|
1583
|
+
for (const provider of auditStreamTimeouts(nextToml).map((item) => item.provider)) {
|
|
1584
|
+
const beforeRetry = nextToml;
|
|
1585
|
+
nextToml = ensureRetrySettings(nextToml, provider);
|
|
1586
|
+
if (nextToml !== beforeRetry) {
|
|
1553
1587
|
result.retriesAdded.push(provider);
|
|
1554
1588
|
}
|
|
1555
1589
|
}
|
|
1556
1590
|
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
if (!dryRun && (result.created || changed)) {
|
|
1561
|
-
writeCodexConfig(toml);
|
|
1562
|
-
}
|
|
1591
|
+
return nextToml;
|
|
1592
|
+
}
|
|
1563
1593
|
|
|
1564
|
-
|
|
1565
|
-
// current execution roots in the global user config. Without this, Codex CLI
|
|
1566
|
-
// warns that project config is disabled and ignores repo-scoped settings.
|
|
1594
|
+
function applyTrustedProjectDefaults(repoRoot, additionalRoots, dryRun, result) {
|
|
1567
1595
|
const trustPaths = [repoRoot, ...additionalRoots]
|
|
1568
1596
|
.map((p) => String(p || "").trim())
|
|
1569
1597
|
.filter(Boolean)
|
|
@@ -1572,22 +1600,101 @@ export function ensureCodexConfig({
|
|
|
1572
1600
|
const trustResult = ensureTrustedProjects(trustPaths, { dryRun });
|
|
1573
1601
|
result.trustedProjectsAdded = trustResult.added;
|
|
1574
1602
|
}
|
|
1603
|
+
}
|
|
1575
1604
|
|
|
1576
|
-
|
|
1605
|
+
function createEnsureCodexConfigResult() {
|
|
1606
|
+
return {
|
|
1607
|
+
path: CONFIG_PATH,
|
|
1608
|
+
created: false,
|
|
1609
|
+
vkAdded: false,
|
|
1610
|
+
vkRemoved: false,
|
|
1611
|
+
vkEnvUpdated: false,
|
|
1612
|
+
agentSdkAdded: false,
|
|
1613
|
+
featuresAdded: [],
|
|
1614
|
+
agentMaxThreads: null,
|
|
1615
|
+
agentMaxThreadsSkipped: null,
|
|
1616
|
+
sandboxAdded: false,
|
|
1617
|
+
sandboxWorkspaceAdded: false,
|
|
1618
|
+
sandboxWorkspaceUpdated: false,
|
|
1619
|
+
sandboxWorkspaceRootsAdded: [],
|
|
1620
|
+
sandboxStaleRootsRemoved: [],
|
|
1621
|
+
shellEnvAdded: false,
|
|
1622
|
+
commonMcpAdded: false,
|
|
1623
|
+
profileProvidersAdded: [],
|
|
1624
|
+
timeoutsFixed: [],
|
|
1625
|
+
retriesAdded: [],
|
|
1626
|
+
trustedProjectsAdded: [],
|
|
1627
|
+
noChanges: true,
|
|
1628
|
+
};
|
|
1577
1629
|
}
|
|
1578
1630
|
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
if (result.noChanges) {
|
|
1586
|
-
log(" :check: Codex CLI config is already up to date");
|
|
1587
|
-
log(` ${result.path}`);
|
|
1588
|
-
return;
|
|
1631
|
+
function initializeCodexConfigState(result) {
|
|
1632
|
+
const configExisted = existsSync(CONFIG_PATH);
|
|
1633
|
+
const originalToml = readCodexConfig();
|
|
1634
|
+
if (!configExisted) {
|
|
1635
|
+
result.created = true;
|
|
1636
|
+
return { originalToml, toml: "" };
|
|
1589
1637
|
}
|
|
1638
|
+
return {
|
|
1639
|
+
originalToml,
|
|
1640
|
+
toml: stripDeprecatedSandboxPermissions(originalToml),
|
|
1641
|
+
};
|
|
1642
|
+
}
|
|
1590
1643
|
|
|
1644
|
+
function applyEnsureCodexConfigDefaults(toml, env, primarySdk, vibeKanbanOptions, result) {
|
|
1645
|
+
const sandboxState = applySandboxDefaults(toml, env, result);
|
|
1646
|
+
let nextToml = sandboxState.toml;
|
|
1647
|
+
|
|
1648
|
+
nextToml = applyAgentSdkDefaults(nextToml, env, primarySdk, result);
|
|
1649
|
+
|
|
1650
|
+
const featureResult = ensureFeatureFlags(nextToml, env);
|
|
1651
|
+
result.featuresAdded = featureResult.added;
|
|
1652
|
+
nextToml = featureResult.toml;
|
|
1653
|
+
|
|
1654
|
+
nextToml = applyVibeKanbanDefaults(nextToml, vibeKanbanOptions, result);
|
|
1655
|
+
nextToml = ensureCommonMcpDefaults(nextToml, result);
|
|
1656
|
+
nextToml = applyModelProviderDefaults(nextToml, env, result);
|
|
1657
|
+
|
|
1658
|
+
return { sandboxState, toml: nextToml };
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
function persistCodexConfigIfChanged(toml, originalToml, dryRun, result) {
|
|
1662
|
+
const changed = toml !== originalToml;
|
|
1663
|
+
result.noChanges = !result.created && !changed;
|
|
1664
|
+
if (!dryRun && (result.created || changed)) {
|
|
1665
|
+
writeCodexConfig(toml);
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
export function ensureCodexConfig({
|
|
1670
|
+
vkBaseUrl = "http://127.0.0.1:54089",
|
|
1671
|
+
skipVk = true,
|
|
1672
|
+
manageVkMcp = false,
|
|
1673
|
+
dryRun = false,
|
|
1674
|
+
env = process.env,
|
|
1675
|
+
primarySdk,
|
|
1676
|
+
} = {}) {
|
|
1677
|
+
const result = createEnsureCodexConfigResult();
|
|
1678
|
+
const { originalToml, toml: initialToml } = initializeCodexConfigState(result);
|
|
1679
|
+
const { sandboxState, toml } = applyEnsureCodexConfigDefaults(
|
|
1680
|
+
initialToml,
|
|
1681
|
+
env,
|
|
1682
|
+
primarySdk,
|
|
1683
|
+
{ manageVkMcp, skipVk, vkBaseUrl },
|
|
1684
|
+
result,
|
|
1685
|
+
);
|
|
1686
|
+
|
|
1687
|
+
persistCodexConfigIfChanged(toml, originalToml, dryRun, result);
|
|
1688
|
+
applyTrustedProjectDefaults(
|
|
1689
|
+
sandboxState.repoRoot,
|
|
1690
|
+
sandboxState.additionalRoots,
|
|
1691
|
+
dryRun,
|
|
1692
|
+
result,
|
|
1693
|
+
);
|
|
1694
|
+
return result;
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
function logConfigSummaryHeader(result, log) {
|
|
1591
1698
|
if (result.created) {
|
|
1592
1699
|
log(" :edit: Created new Codex CLI config");
|
|
1593
1700
|
}
|
|
@@ -1614,7 +1721,9 @@ export function printConfigSummary(result, log = console.log) {
|
|
|
1614
1721
|
: `${result.featuresAdded.length} feature flags`;
|
|
1615
1722
|
log(` :check: Added feature flags: ${key}`);
|
|
1616
1723
|
}
|
|
1724
|
+
}
|
|
1617
1725
|
|
|
1726
|
+
function logSandboxSummary(result, log) {
|
|
1618
1727
|
if (result.sandboxAdded) {
|
|
1619
1728
|
log(" :check: Added sandbox permissions (disk-full-write-access)");
|
|
1620
1729
|
}
|
|
@@ -1643,7 +1752,9 @@ export function printConfigSummary(result, log = console.log) {
|
|
|
1643
1752
|
if (result.shellEnvAdded) {
|
|
1644
1753
|
log(" :check: Added shell environment policy (inherit=all)");
|
|
1645
1754
|
}
|
|
1755
|
+
}
|
|
1646
1756
|
|
|
1757
|
+
function logAgentSdkSummary(result, log) {
|
|
1647
1758
|
if (result.agentMaxThreads) {
|
|
1648
1759
|
const fromLabel =
|
|
1649
1760
|
result.agentMaxThreads.from === null
|
|
@@ -1657,7 +1768,9 @@ export function printConfigSummary(result, log = console.log) {
|
|
|
1657
1768
|
` :alert: Skipped agents.max_threads (invalid value: ${result.agentMaxThreadsSkipped})`,
|
|
1658
1769
|
);
|
|
1659
1770
|
}
|
|
1771
|
+
}
|
|
1660
1772
|
|
|
1773
|
+
function logProviderSummary(result, log) {
|
|
1661
1774
|
if (result.commonMcpAdded) {
|
|
1662
1775
|
log(
|
|
1663
1776
|
" :check: Added common MCP servers (context7, sequential-thinking, playwright, microsoft-docs)",
|
|
@@ -1682,179 +1795,27 @@ export function printConfigSummary(result, log = console.log) {
|
|
|
1682
1795
|
for (const p of result.retriesAdded) {
|
|
1683
1796
|
log(` :check: Added retry settings to [${p}]`);
|
|
1684
1797
|
}
|
|
1685
|
-
|
|
1686
|
-
log(` Config: ${result.path}`);
|
|
1687
|
-
}
|
|
1688
|
-
|
|
1689
|
-
// ── Trusted Projects ─────────────────────────────────────────────────────────
|
|
1690
|
-
|
|
1691
|
-
/**
|
|
1692
|
-
* Escape a string for use inside a double-quoted TOML basic string.
|
|
1693
|
-
* Handles backslashes (Windows paths) and double-quote characters.
|
|
1694
|
-
*/
|
|
1695
|
-
function tomlEscapeStr(s) {
|
|
1696
|
-
return String(s).replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
1697
|
-
}
|
|
1698
|
-
|
|
1699
|
-
/**
|
|
1700
|
-
* Format an array of strings as a TOML array literal, correctly escaping
|
|
1701
|
-
* backslashes so Windows paths are stored faithfully.
|
|
1702
|
-
*
|
|
1703
|
-
* Example output: ["C:\\Users\\jon\\bosun", "/home/jon/bosun"]
|
|
1704
|
-
*/
|
|
1705
|
-
function formatTomlArrayEscaped(values) {
|
|
1706
|
-
return `[${values.map((v) => `"${tomlEscapeStr(v)}"`).join(", ")}]`;
|
|
1707
|
-
}
|
|
1708
|
-
|
|
1709
|
-
function toWindowsNamespacePath(pathValue) {
|
|
1710
|
-
const value = String(pathValue || "").trim();
|
|
1711
|
-
if (!value) return null;
|
|
1712
|
-
if (value.startsWith("\\\\?\\")) return value;
|
|
1713
|
-
const drivePath = toWindowsDrivePath(value);
|
|
1714
|
-
if (drivePath) return `\\\\?\\${drivePath}`;
|
|
1715
|
-
return null;
|
|
1716
|
-
}
|
|
1717
|
-
|
|
1718
|
-
function toWindowsDrivePath(pathValue) {
|
|
1719
|
-
const raw = String(pathValue || "").trim();
|
|
1720
|
-
if (!raw) return null;
|
|
1721
|
-
let value = raw.replace(/\//g, "\\");
|
|
1722
|
-
if (value.startsWith("\\\\?\\")) value = value.slice(4);
|
|
1723
|
-
if (/^[a-zA-Z]:\\/.test(value)) return value;
|
|
1724
|
-
const wslMatch = raw.match(/^\/mnt\/([a-zA-Z])\/(.+)$/);
|
|
1725
|
-
if (wslMatch) {
|
|
1726
|
-
const drive = wslMatch[1].toUpperCase();
|
|
1727
|
-
const rest = wslMatch[2].replace(/\//g, "\\");
|
|
1728
|
-
return `${drive}:\\${rest}`;
|
|
1729
|
-
}
|
|
1730
|
-
return null;
|
|
1731
|
-
}
|
|
1732
|
-
|
|
1733
|
-
function normalizeTrustedPathForCompare(pathValue) {
|
|
1734
|
-
const trimTrailingPathSeparators = (value) => {
|
|
1735
|
-
let out = String(value || "");
|
|
1736
|
-
while (out.endsWith("/") || out.endsWith("\\")) out = out.slice(0, -1);
|
|
1737
|
-
return out;
|
|
1738
|
-
};
|
|
1739
|
-
const raw = String(pathValue || "").trim();
|
|
1740
|
-
if (!raw) return "";
|
|
1741
|
-
const windowsDrivePath = toWindowsDrivePath(raw);
|
|
1742
|
-
if (windowsDrivePath) {
|
|
1743
|
-
return trimTrailingPathSeparators(windowsDrivePath).toLowerCase();
|
|
1744
|
-
}
|
|
1745
|
-
if (process.platform === "win32") {
|
|
1746
|
-
let normalized = raw.replace(/\//g, "\\");
|
|
1747
|
-
if (normalized.startsWith("\\\\?\\UNC\\")) {
|
|
1748
|
-
normalized = "\\\\" + normalized.slice(8);
|
|
1749
|
-
} else if (normalized.startsWith("\\\\?\\")) {
|
|
1750
|
-
normalized = normalized.slice(4);
|
|
1751
|
-
}
|
|
1752
|
-
normalized = trimTrailingPathSeparators(normalized);
|
|
1753
|
-
return normalized.toLowerCase();
|
|
1754
|
-
}
|
|
1755
|
-
return trimTrailingPathSeparators(resolve(raw));
|
|
1756
|
-
}
|
|
1757
|
-
|
|
1758
|
-
function buildTrustedPathVariants(pathValue) {
|
|
1759
|
-
const base = resolve(pathValue);
|
|
1760
|
-
const variants = [base];
|
|
1761
|
-
const namespaced = toWindowsNamespacePath(base);
|
|
1762
|
-
if (namespaced && namespaced !== base) variants.push(namespaced);
|
|
1763
|
-
return variants;
|
|
1764
1798
|
}
|
|
1765
1799
|
|
|
1766
1800
|
/**
|
|
1767
|
-
*
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
if (!raw) return [];
|
|
1771
|
-
const inner = raw.trim().replace(/^\[/, "").replace(/\]$/, "");
|
|
1772
|
-
if (!inner.trim()) return [];
|
|
1773
|
-
// Split on commas that are NOT inside quotes
|
|
1774
|
-
const items = [];
|
|
1775
|
-
let buf = "";
|
|
1776
|
-
let inStr = false;
|
|
1777
|
-
for (let i = 0; i < inner.length; i++) {
|
|
1778
|
-
const ch = inner[i];
|
|
1779
|
-
if (ch === "\\" && inStr) { buf += ch + (inner[++i] || ""); continue; }
|
|
1780
|
-
if (ch === '"') { inStr = !inStr; buf += ch; continue; }
|
|
1781
|
-
if (ch === "," && !inStr) { items.push(buf.trim()); buf = ""; continue; }
|
|
1782
|
-
buf += ch;
|
|
1783
|
-
}
|
|
1784
|
-
if (buf.trim()) items.push(buf.trim());
|
|
1785
|
-
return items
|
|
1786
|
-
.map((item) => item.replace(/^"(.*)"$/s, "$1")) // strip outer quotes
|
|
1787
|
-
.map((item) => item.replace(/\\(["\\])/g, "$1")) // unescape \" and \\
|
|
1788
|
-
.filter(Boolean);
|
|
1789
|
-
}
|
|
1790
|
-
|
|
1791
|
-
/**
|
|
1792
|
-
* Ensure the given directory paths are listed in the `trusted_projects`
|
|
1793
|
-
* top-level key in ~/.codex/config.toml.
|
|
1794
|
-
*
|
|
1795
|
-
* Codex refuses to load a per-project .codex/config.toml unless the project
|
|
1796
|
-
* directory appears in this list — producing warnings like:
|
|
1797
|
-
* ":alert: Project config.toml files are disabled … add <dir> as a trusted project"
|
|
1798
|
-
*
|
|
1799
|
-
* Paths are stored as-is (forward or back slashes preserved) with proper TOML
|
|
1800
|
-
* escaping so Windows paths survive round-trips through the file.
|
|
1801
|
-
*
|
|
1802
|
-
* @param {string[]} paths Absolute directories to trust (e.g. [bosunHome])
|
|
1803
|
-
* @param {{ dryRun?: boolean }} [opts]
|
|
1804
|
-
* @returns {{ added: string[], already: string[], path: string }}
|
|
1801
|
+
* Print a human-friendly summary of what ensureCodexConfig() did.
|
|
1802
|
+
* @param {object} result Return value from ensureCodexConfig()
|
|
1803
|
+
* @param {(msg: string) => void} [log] Logger (default: console.log)
|
|
1805
1804
|
*/
|
|
1806
|
-
export function
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
.
|
|
1810
|
-
|
|
1811
|
-
if (desired.length === 0) return result;
|
|
1812
|
-
|
|
1813
|
-
let toml = readCodexConfig() || "";
|
|
1814
|
-
|
|
1815
|
-
// Parse existing trusted_projects (multi-line arrays may span lines)
|
|
1816
|
-
const existingMatch = toml.match(/^trusted_projects\s*=\s*(\[[^\]]*\])/m);
|
|
1817
|
-
const existing = existingMatch ? parseTomlArrayLiteralEscaped(existingMatch[1]) : [];
|
|
1818
|
-
const existingNormalized = new Set(
|
|
1819
|
-
existing.map((p) => normalizeTrustedPathForCompare(p)).filter(Boolean),
|
|
1820
|
-
);
|
|
1821
|
-
|
|
1822
|
-
let changed = false;
|
|
1823
|
-
for (const p of desired) {
|
|
1824
|
-
const normalized = normalizeTrustedPathForCompare(p);
|
|
1825
|
-
if (!normalized) continue;
|
|
1826
|
-
if (existingNormalized.has(normalized)) {
|
|
1827
|
-
result.already.push(p);
|
|
1828
|
-
} else {
|
|
1829
|
-
existing.push(p);
|
|
1830
|
-
existingNormalized.add(normalized);
|
|
1831
|
-
result.added.push(p);
|
|
1832
|
-
changed = true;
|
|
1833
|
-
}
|
|
1834
|
-
}
|
|
1835
|
-
|
|
1836
|
-
if (!changed) return result;
|
|
1837
|
-
if (dryRun) return result;
|
|
1838
|
-
|
|
1839
|
-
const newLine = `trusted_projects = ${formatTomlArrayEscaped(existing)}`;
|
|
1840
|
-
|
|
1841
|
-
if (existingMatch) {
|
|
1842
|
-
toml = toml.replace(/^trusted_projects\s*=\s*\[[^\]]*\]/m, newLine);
|
|
1843
|
-
} else {
|
|
1844
|
-
// Insert before the first section header (or at top if no sections)
|
|
1845
|
-
const firstSection = toml.search(/^\[/m);
|
|
1846
|
-
if (firstSection === -1) {
|
|
1847
|
-
toml = `${newLine}\n${toml}`;
|
|
1848
|
-
} else {
|
|
1849
|
-
toml = `${toml.slice(0, firstSection)}${newLine}\n\n${toml.slice(firstSection)}`;
|
|
1850
|
-
}
|
|
1805
|
+
export function printConfigSummary(result, log = console.log) {
|
|
1806
|
+
if (result.noChanges) {
|
|
1807
|
+
log(" :check: Codex CLI config is already up to date");
|
|
1808
|
+
log(` ${result.path}`);
|
|
1809
|
+
return;
|
|
1851
1810
|
}
|
|
1852
1811
|
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1812
|
+
logConfigSummaryHeader(result, log);
|
|
1813
|
+
logSandboxSummary(result, log);
|
|
1814
|
+
logAgentSdkSummary(result, log);
|
|
1815
|
+
logProviderSummary(result, log);
|
|
1816
|
+
log(` Config: ${result.path}`);
|
|
1856
1817
|
}
|
|
1857
|
-
|
|
1818
|
+
export { ensureTrustedProjects };
|
|
1858
1819
|
// ── Internal Helpers ─────────────────────────────────────────────────────────
|
|
1859
1820
|
|
|
1860
1821
|
function escapeRegex(str) {
|
|
@@ -1866,3 +1827,5 @@ function parseBoolEnv(value) {
|
|
|
1866
1827
|
if (["0", "false", "no", "off", "n"].includes(raw)) return false;
|
|
1867
1828
|
return true;
|
|
1868
1829
|
}
|
|
1830
|
+
|
|
1831
|
+
|