From 6295e79288cb849338589e98ff6d9e6640ed04fa Mon Sep 17 00:00:00 2001 From: Kairos Ops <186839917+KairosOps@users.noreply.github.com> Date: Sun, 21 Jun 2026 05:43:16 +0800 Subject: [PATCH] fix: sanitize derived project ids --- packages/cli/__tests__/commands/start.test.ts | 83 ++++++++++++++++++- packages/cli/src/commands/start.ts | 11 ++- 2 files changed, 90 insertions(+), 4 deletions(-) diff --git a/packages/cli/__tests__/commands/start.test.ts b/packages/cli/__tests__/commands/start.test.ts index 72be6811d7..f77269363e 100644 --- a/packages/cli/__tests__/commands/start.test.ts +++ b/packages/cli/__tests__/commands/start.test.ts @@ -23,6 +23,7 @@ import { generateExternalId, getDefaultRuntime, recordActivityEvent, + sanitizeProjectId, type SessionManager, } from "@aoagents/ao-core"; @@ -2462,7 +2463,7 @@ describe("start command — autoCreateConfig", () => { process.env["AO_GLOBAL_CONFIG"]!, [ "projects:", - ` ${basename(tmpDir)}:`, + ` ${sanitizeProjectId(basename(tmpDir))}:`, ` path: ${join(tmpDir, "other-repo")}`, "", ].join("\n"), @@ -3085,6 +3086,86 @@ describe("start command — path-based deduplication in addProjectToConfig", () else process.env["AO_CONFIG_PATH"] = origEnv; } }); + + it("sanitizes dotted directory names before adding a local project key", async () => { + const repoDir = join(tmpDir, "llama.cpp"); + createFakeRepo(repoDir, "https://github.com/org/llama.cpp.git"); + + const configPath = join(tmpDir, "agent-orchestrator.yaml"); + const { stringify: yamlStringify } = await import("yaml"); + writeFileSync( + configPath, + yamlStringify( + { + defaults: { + runtime: "process", + agent: "claude-code", + workspace: "worktree", + notifiers: [], + }, + projects: { + "my-app": { + name: "My App", + repo: "org/my-app", + path: join(tmpDir, "my-app"), + defaultBranch: "main", + sessionPrefix: "app", + }, + }, + }, + { indent: 2 }, + ), + ); + + const shell = await import("../../src/lib/shell.js"); + vi.mocked(shell.git).mockImplementation(async (args: string[], workingDir?: string) => { + if (args[0] === "rev-parse" && args[1] === "--git-dir" && workingDir === repoDir) { + return ".git"; + } + if ( + args[0] === "remote" && + args[1] === "get-url" && + args[2] === "origin" && + workingDir === repoDir + ) { + return "https://github.com/org/llama.cpp.git"; + } + if (args[0] === "symbolic-ref" && workingDir === repoDir) { + return "refs/remotes/origin/main"; + } + if (args[0] === "rev-parse" && args[1] === "--verify" && workingDir === repoDir) { + return "abc"; + } + return null; + }); + + const origEnv = process.env["AO_CONFIG_PATH"]; + process.env["AO_CONFIG_PATH"] = configPath; + + try { + await program.parseAsync([ + "node", + "test", + "start", + repoDir, + "--no-dashboard", + "--no-orchestrator", + ]); + + const content = readFileSync(configPath, "utf-8"); + const parsed = parseYaml(content) as { projects: Record> }; + expect(parsed.projects["llama.cpp"]).toBeUndefined(); + expect(parsed.projects["llama-cpp"]).toMatchObject({ + name: "llama-cpp", + path: repoDir, + defaultBranch: "main", + sessionPrefix: "lc", + }); + } finally { + if (origEnv === undefined) delete process.env["AO_CONFIG_PATH"]; + else process.env["AO_CONFIG_PATH"] = origEnv; + } + }); }); describe("start command — global registry mutations", () => { diff --git a/packages/cli/src/commands/start.ts b/packages/cli/src/commands/start.ts index fb27adce8a..40d45b8685 100644 --- a/packages/cli/src/commands/start.ts +++ b/packages/cli/src/commands/start.ts @@ -34,6 +34,7 @@ import { loadLocalProjectConfigDetailed, recordActivityEvent, registerProjectInGlobalConfig, + sanitizeProjectId, getGlobalConfigPath, type OrchestratorConfig, type LocalProjectConfig, @@ -141,6 +142,10 @@ function writeProjectBehaviorConfig(projectPath: string, config: LocalProjectCon writeLocalProjectConfig(projectPath, config); } +function deriveProjectIdFromPath(projectPath: string): string { + return sanitizeProjectId(basename(projectPath)) || "project"; +} + /** * Register a flat local config (agent-orchestrator.yaml without `projects:`) * into the global config so loadConfig can resolve it. @@ -148,7 +153,7 @@ function writeProjectBehaviorConfig(projectPath: string, config: LocalProjectCon */ async function registerFlatConfig(configPath: string): Promise { const projectPath = resolve(dirname(configPath)); - const projectId = basename(projectPath); + const projectId = deriveProjectIdFromPath(projectPath); // Read flat config fields const raw = readFileSync(configPath, "utf-8"); @@ -537,7 +542,7 @@ export async function autoCreateConfig(workingDir: string): Promise