Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 50 additions & 2 deletions plugins/omo/components/bootstrap/dist/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -3543,7 +3543,15 @@ async function linkBundledAgentsStep(options) {
const agentsTarget = join21(options.codexHome, "agents");
try {
const stageRoot = join21(options.pluginData, "bootstrap", "agents-stage");
await stageBundledAgents(options.pluginRoot, stageRoot);
const previouslyInstalledAgents = await readInstalledAgentPaths(stageRoot);
const previouslyStagedAgentContents = await readStagedAgentContents(stageRoot);
const existingConfig = await readConfigIfPresent(join21(options.codexHome, "config.toml"));
const foreignAgentFiles = await stageBundledAgents(options.pluginRoot, stageRoot, existingConfig);
for (const agentFile of foreignAgentFiles) {
const agentPath = join21(agentsTarget, agentFile);
if (previouslyInstalledAgents.has(agentPath) && await matchesAgentContent(agentPath, previouslyStagedAgentContents.get(agentFile)))
await rm10(agentPath, { force: true });
}
const preservedReasoning = await capturePreservedAgentReasoning({ codexHome: options.codexHome });
const preservedServiceTier = await capturePreservedAgentServiceTier({ codexHome: options.codexHome });
const linked = await linkCachedPluginAgents({
Expand All @@ -3567,9 +3575,43 @@ async function linkBundledAgentsStep(options) {
};
}
}
async function stageBundledAgents(pluginRoot, stageRoot) {
async function readInstalledAgentPaths(stageRoot) {
try {
const parsed = JSON.parse(await readFile(join21(stageRoot, ".installed-agents.json"), "utf8"));
if (!isRecord(parsed) || !Array.isArray(parsed["agents"]))
return new Set();
return new Set(parsed["agents"].filter((path) => typeof path === "string"));
} catch (error) {
if (error instanceof Error && "code" in error && error.code === "ENOENT")
return new Set();
throw error;
}
}
async function readStagedAgentContents(stageRoot) {
const contents = new Map();
const componentsRoot = join21(stageRoot, "components");
for (const componentName of await directoryNames(componentsRoot)) {
const agentsDir = join21(componentsRoot, componentName, "agents");
for (const agentFile of await fileNames(agentsDir))
contents.set(agentFile, await readFile(join21(agentsDir, agentFile), "utf8"));
}
return contents;
}
async function matchesAgentContent(path, expectedContent) {
if (expectedContent === void 0)
return false;
try {
return await readFile(path, "utf8") === expectedContent;
} catch (error) {
if (error instanceof Error && "code" in error && error.code === "ENOENT")
return false;
throw error;
}
}
async function stageBundledAgents(pluginRoot, stageRoot, existingConfig) {
await rm10(stageRoot, { force: true, recursive: true });
await mkdir7(stageRoot, { recursive: true });
const foreignAgentFiles = [];
const componentsRoot = join21(pluginRoot, "components");
for (const componentName of await directoryNames(componentsRoot)) {
const agentsDir = join21(componentsRoot, componentName, "agents");
Expand All @@ -3579,9 +3621,15 @@ async function stageBundledAgents(pluginRoot, stageRoot) {
const stagedAgentsDir = join21(stageRoot, "components", componentName, "agents");
await mkdir7(stagedAgentsDir, { recursive: true });
for (const agentFile of agentFiles) {
const agentConfig = { configFile: `./agents/${agentFile}`, name: agentNameFromToml3(agentFile) };
if (hasForeignAgentRegistration(existingConfig, agentConfig)) {
foreignAgentFiles.push(agentFile);
continue;
}
await copyFile2(join21(agentsDir, agentFile), join21(stagedAgentsDir, agentFile));
}
}
return foreignAgentFiles;
}
async function updateConfigStep(options, inputs, degraded) {
const configPath = join21(options.codexHome, "config.toml");
Expand Down
97 changes: 97 additions & 0 deletions plugins/omo/components/bootstrap/src/agent-staging.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { copyFile, mkdir, readFile, readdir, rm } from "node:fs/promises";
import { join } from "node:path";

import { hasForeignAgentRegistration } from "../../../../src/install/codex-config-agents.ts";

const AGENT_MANIFEST = ".installed-agents.json";

export async function readInstalledAgentPaths(stageRoot: string): Promise<ReadonlySet<string>> {
try {
const parsed: unknown = JSON.parse(await readFile(join(stageRoot, AGENT_MANIFEST), "utf8"));
if (!isRecord(parsed) || !Array.isArray(parsed["agents"])) return new Set();
return new Set(parsed["agents"].filter((path): path is string => typeof path === "string"));
} catch (error) {
if (error instanceof Error && "code" in error && error.code === "ENOENT") return new Set();
throw error;
}
}

export async function readStagedAgentContents(stageRoot: string): Promise<ReadonlyMap<string, string>> {
const contents = new Map<string, string>();
const componentsRoot = join(stageRoot, "components");
for (const componentName of await directoryNames(componentsRoot)) {
const agentsDir = join(componentsRoot, componentName, "agents");
for (const agentFile of await fileNames(agentsDir)) {
contents.set(agentFile, await readFile(join(agentsDir, agentFile), "utf8"));
}
}
return contents;
}

export async function matchesAgentContent(path: string, expectedContent: string | undefined): Promise<boolean> {
if (expectedContent === undefined) return false;
try {
return (await readFile(path, "utf8")) === expectedContent;
} catch (error) {
if (error instanceof Error && "code" in error && error.code === "ENOENT") return false;
throw error;
}
}

export async function stageBundledAgents(
pluginRoot: string,
stageRoot: string,
existingConfig: string,
): Promise<readonly string[]> {
await rm(stageRoot, { force: true, recursive: true });
await mkdir(stageRoot, { recursive: true });
const foreignAgentFiles: string[] = [];
const componentsRoot = join(pluginRoot, "components");
for (const componentName of await directoryNames(componentsRoot)) {
const agentsDir = join(componentsRoot, componentName, "agents");
const agentFiles = (await fileNames(agentsDir)).filter((name) => name.endsWith(".toml"));
if (agentFiles.length === 0) continue;
const stagedAgentsDir = join(stageRoot, "components", componentName, "agents");
await mkdir(stagedAgentsDir, { recursive: true });
for (const agentFile of agentFiles) {
const agentConfig = { configFile: `./agents/${agentFile}`, name: agentNameFromToml(agentFile) };
if (hasForeignAgentRegistration(existingConfig, agentConfig)) {
foreignAgentFiles.push(agentFile);
continue;
}
await copyFile(join(agentsDir, agentFile), join(stagedAgentsDir, agentFile));
}
}
return foreignAgentFiles;
}

export function agentNameFromToml(fileName: string): string {
return fileName.endsWith(".toml") ? fileName.slice(0, -".toml".length) : fileName;
}

async function directoryNames(root: string): Promise<string[]> {
return entryNames(root, (entry) => entry.isDirectory());
}

async function fileNames(root: string): Promise<string[]> {
return entryNames(root, (entry) => entry.isFile());
}

async function entryNames(root: string, keep: (entry: { isDirectory(): boolean; isFile(): boolean }) => boolean): Promise<string[]> {
try {
const entries: readonly { isDirectory(): boolean; isFile(): boolean; name: string }[] = await readdir(root, {
withFileTypes: true,
});
return entries
.filter((entry) => keep(entry))
.map((entry) => entry.name)
.sort();
} catch (error) {
if (error instanceof Error && "code" in error && error.code === "ENOENT") return [];
throw error;
}
}

function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
66 changes: 22 additions & 44 deletions plugins/omo/components/bootstrap/src/setup.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { copyFile, mkdir, readFile, readdir, rm, stat } from "node:fs/promises";
import { readFile, rm, stat } from "node:fs/promises";
import { join } from "node:path";

// These relative imports resolve at BUILD time in the monorepo; esbuild
Expand All @@ -17,6 +17,13 @@ import { trustedHookStatesForPlugin } from "../../../../src/install/codex-hook-t
import { resolveCodexInstallerBinDir } from "../../../../src/install/codex-installer-bin-dir.ts";
import { prepareGitBashForInstall } from "../../../../src/install/git-bash.ts";
import type { CodexAgentConfig, GitBashResolution } from "../../../../src/install/types.ts";
import {
agentNameFromToml,
matchesAgentContent,
readInstalledAgentPaths,
readStagedAgentContents,
stageBundledAgents,
} from "./agent-staging.ts";
import { appendBootstrapLog, BOOTSTRAP_DOCTOR_HINT } from "./worker.ts";
import type { BootstrapDegradedEntry, BootstrapStepOutcome } from "./worker.ts";

Expand Down Expand Up @@ -91,7 +98,19 @@ async function linkBundledAgentsStep(options: WorkerSetupOptions): Promise<Agent
// first: bootstrap must never persist anything under PLUGIN_ROOT (the
// Codex-managed marketplace cache).
const stageRoot = join(options.pluginData, "bootstrap", "agents-stage");
await stageBundledAgents(options.pluginRoot, stageRoot);
const previouslyInstalledAgents = await readInstalledAgentPaths(stageRoot);
const previouslyStagedAgentContents = await readStagedAgentContents(stageRoot);
const existingConfig = await readConfigIfPresent(join(options.codexHome, "config.toml"));
const foreignAgentFiles = await stageBundledAgents(options.pluginRoot, stageRoot, existingConfig);
for (const agentFile of foreignAgentFiles) {
const agentPath = join(agentsTarget, agentFile);
if (
previouslyInstalledAgents.has(agentPath) &&
(await matchesAgentContent(agentPath, previouslyStagedAgentContents.get(agentFile)))
) {
await rm(agentPath, { force: true });
}
}
const preservedReasoning = await capturePreservedAgentReasoning({ codexHome: options.codexHome });
const preservedServiceTier = await capturePreservedAgentServiceTier({ codexHome: options.codexHome });
const linked = await linkCachedPluginAgents({
Expand All @@ -118,22 +137,6 @@ async function linkBundledAgentsStep(options: WorkerSetupOptions): Promise<Agent
}
}

async function stageBundledAgents(pluginRoot: string, stageRoot: string): Promise<void> {
await rm(stageRoot, { force: true, recursive: true });
await mkdir(stageRoot, { recursive: true });
const componentsRoot = join(pluginRoot, "components");
for (const componentName of await directoryNames(componentsRoot)) {
const agentsDir = join(componentsRoot, componentName, "agents");
const agentFiles = (await fileNames(agentsDir)).filter((name) => name.endsWith(".toml"));
if (agentFiles.length === 0) continue;
const stagedAgentsDir = join(stageRoot, "components", componentName, "agents");
await mkdir(stagedAgentsDir, { recursive: true });
for (const agentFile of agentFiles) {
await copyFile(join(agentsDir, agentFile), join(stagedAgentsDir, agentFile));
}
}
}

async function updateConfigStep(
options: WorkerSetupOptions,
inputs: { agentConfigs: readonly CodexAgentConfig[]; gitBashEnabled: boolean },
Expand All @@ -147,7 +150,7 @@ async function updateConfigStep(
// for such a role would collide with the mirrored entry once Codex
// discovers <codexHome>/agents/<name>.toml (two different file paths for
// one role name -> upstream warning), so foreign pre-existing blocks are
// left untouched; directory discovery still loads the linked toml.
// left untouched and their runtime-local copies are excluded from staging.
const existingConfig = await readConfigIfPresent(configPath);
const agentConfigs = inputs.agentConfigs.filter(
(agentConfig) => !hasForeignAgentRegistration(existingConfig, agentConfig),
Expand Down Expand Up @@ -268,31 +271,6 @@ async function stampGitBashEnvStep(options: WorkerSetupOptions, degraded: Bootst
}
}

async function directoryNames(root: string): Promise<string[]> {
return entryNames(root, (entry) => entry.isDirectory());
}

async function fileNames(root: string): Promise<string[]> {
return entryNames(root, (entry) => entry.isFile());
}

async function entryNames(root: string, keep: (entry: { isDirectory(): boolean; isFile(): boolean }) => boolean): Promise<string[]> {
try {
const entries = await readdir(root, { withFileTypes: true });
return entries
.filter((entry) => keep(entry))
.map((entry) => entry.name)
.sort();
} catch (error) {
if (error instanceof Error && "code" in error && error.code === "ENOENT") return [];
throw error;
}
}

function agentNameFromToml(fileName: string): string {
return fileName.endsWith(".toml") ? fileName.slice(0, -".toml".length) : fileName;
}

function errorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
32 changes: 27 additions & 5 deletions plugins/omo/test/bootstrap-setup.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,10 @@ test("#given a completed first run #when the worker setup runs again #then confi
});
});

test("#given a config.toml that already declares [agents.explorer] at a different path (Orca mirror) #when the worker setup runs #then no second colliding registration is added for that role", async () => {
test("#given a config.toml that already declares [agents.explorer] at a different path (Orca mirror) #when the worker setup runs #then the mirrored role is not also linked into the runtime agents directory", async () => {
await withSetupFixture(async (fixture) => {
const orcaMirrorPath = "/orca-mirrored-home/.codex/agents/explorer.toml";
await runWorkerSetup(setupOptions(fixture));
await writeFile(
join(fixture.codexHome, "config.toml"),
`[marketplaces.sisyphuslabs]\n${MARKETPLACE_SOURCE_LINE}\n\n[agents.explorer]\nconfig_file = "${orcaMirrorPath}"\n`,
Expand All @@ -149,11 +150,15 @@ test("#given a config.toml that already declares [agents.explorer] at a differen
"no colliding ./agents registration for the mirrored role",
);
assert.match(config, /\[agents\.metis\]\nconfig_file = "\.\/agents\/metis\.toml"/);
assert.equal(
await readFile(join(fixture.codexHome, "agents", "explorer.toml"), "utf8"),
BUNDLED_EXPLORER_TOML,
"the linked toml is still staged for Codex directory discovery",
await assert.rejects(() => stat(join(fixture.codexHome, "agents", "explorer.toml")), { code: "ENOENT" });
assert.equal(await readFile(join(fixture.codexHome, "agents", "metis.toml"), "utf8"), BUNDLED_METIS_TOML);
const manifest = JSON.parse(
await readFile(join(fixture.pluginData, "bootstrap", "agents-stage", ".installed-agents.json"), "utf8"),
);
assert.deepEqual(manifest.agents, [join(fixture.codexHome, "agents", "metis.toml")]);
await runWorkerSetup(setupOptions(fixture));
assert.equal(await readConfig(fixture), config);
await assert.rejects(() => stat(join(fixture.codexHome, "agents", "explorer.toml")), { code: "ENOENT" });
});
});

Expand All @@ -168,6 +173,23 @@ test("#given a config.toml with no pre-existing agent entries #when the worker s
});
});

test("#given an unmanaged local role with the same name as an Orca registration #when the worker setup runs #then the local role is preserved", async () => {
await withSetupFixture(async (fixture) => {
const userAgent = 'description = "User-owned explorer"\nmodel = "gpt-5.6"\n';
await runWorkerSetup(setupOptions(fixture));
await mkdir(join(fixture.codexHome, "agents"), { recursive: true });
await writeFile(join(fixture.codexHome, "agents", "explorer.toml"), userAgent);
await writeFile(
join(fixture.codexHome, "config.toml"),
`[marketplaces.sisyphuslabs]\n${MARKETPLACE_SOURCE_LINE}\n\n[agents.explorer]\nconfig_file = "/orca-mirrored-home/.codex/agents/explorer.toml"\n`,
);

await runWorkerSetup(setupOptions(fixture));

assert.equal(await readFile(join(fixture.codexHome, "agents", "explorer.toml"), "utf8"), userAgent);
});
});

test("#given a package-relative CodeGraph MCP path #when worker setup runs #then the path is stamped absolute", async () => {
await withSetupFixture(async (fixture) => {
await writeFile(
Expand Down