fix(codex): harden windows light install

This commit is contained in:
YeonGyu-Kim
2026-05-31 13:23:24 +09:00
parent 4e31d7df5d
commit 92bad87d84
42 changed files with 1675 additions and 23 deletions
@@ -17,6 +17,13 @@ const BUNDLED_MCP_RUNTIMES = [
destinationArg: "./components/ast-grep-mcp/dist/cli.js",
destinationDistFromPlugin: "components/ast-grep-mcp/dist",
},
{
label: "Git Bash MCP",
sourceArg: "../../git-bash-mcp/dist/cli.js",
sourceDistFromPlugin: "../../git-bash-mcp/dist",
destinationArg: "./components/git-bash-mcp/dist/cli.js",
destinationDistFromPlugin: "components/git-bash-mcp/dist",
},
{
label: "LSP MCP",
sourceArg: "../../lsp-tools-mcp/dist/cli.js",
+5 -1
View File
@@ -39,6 +39,7 @@ describe("codex-cache", () => {
mcpServers: {
ast_grep: { cwd: ".", args: ["../../ast-grep-mcp/dist/cli.js", "mcp"] },
custom: { args: ["/usr/local/bin/custom-mcp", "--stdio"] },
git_bash: { cwd: ".", args: ["../../git-bash-mcp/dist/cli.js", "mcp"] },
lsp: { cwd: ".", args: ["../../lsp-tools-mcp/dist/cli.js", "mcp"] },
},
}),
@@ -52,13 +53,16 @@ describe("codex-cache", () => {
mcpServers: {
ast_grep: { cwd?: string; args: string[] }
custom: { args: string[] }
git_bash: { cwd?: string; args: string[] }
lsp: { cwd?: string; args: string[] }
}
}
expect(Object.keys(rewritten.mcpServers).sort()).toEqual(["ast_grep", "custom", "lsp"])
expect(Object.keys(rewritten.mcpServers).sort()).toEqual(["ast_grep", "custom", "git_bash", "lsp"])
expect(rewritten.mcpServers.ast_grep.cwd).toBeUndefined()
expect(rewritten.mcpServers.ast_grep.args[0]).toBe(join(cacheRoot, "components", "ast-grep-mcp", "dist", "cli.js"))
expect(rewritten.mcpServers.custom.args).toEqual(["/usr/local/bin/custom-mcp", "--stdio"])
expect(rewritten.mcpServers.git_bash.cwd).toBeUndefined()
expect(rewritten.mcpServers.git_bash.args[0]).toBe(join(cacheRoot, "components", "git-bash-mcp", "dist", "cli.js"))
expect(rewritten.mcpServers.lsp.cwd).toBeUndefined()
expect(rewritten.mcpServers.lsp.args[0]).toBe(join(cacheRoot, "components", "lsp-tools-mcp", "dist", "cli.js"))
})
@@ -396,4 +396,67 @@ describe("codex-config-toml", () => {
expect(content).not.toContain("stale-explorer")
expect(content).not.toContain("ref = undefined")
})
test("#given windows platform #when updating sisyphuslabs plugin config #then enables git_bash plugin mcp policy", async () => {
// given
const root = await mkdtemp(join(tmpdir(), "omo-codex-config-git-bash-win32-"))
const configPath = join(root, "config.toml")
await writeFile(
configPath,
[
'[plugins."omo@sisyphuslabs"]',
"enabled = true",
"",
'[plugins."omo@sisyphuslabs".mcp_servers.lsp]',
"enabled = true",
"",
'[hooks.state."omo@sisyphuslabs:hooks/hooks.json:post_tool_use:0:0"]',
'trusted_hash = "sha256:keep"',
"",
].join("\n"),
)
// when
await updateCodexConfig({
configPath,
repoRoot: "/repo/packages/omo-codex",
marketplaceName: "sisyphuslabs",
marketplaceSource: { sourceType: "local", source: "/repo/packages/omo-codex/cache/sisyphuslabs" },
pluginNames: ["omo"],
platform: "win32",
trustedHookStates: [{ key: "omo@sisyphuslabs:hooks/hooks.json:post_tool_use:0:0", trustedHash: "sha256:keep" }],
})
// then
const content = await readFile(configPath, "utf8")
expect(content).toContain('[plugins."omo@sisyphuslabs".mcp_servers.lsp]')
expect(content).toContain('[plugins."omo@sisyphuslabs".mcp_servers.git_bash]')
expect(content).toContain("[hooks.state.\"omo@sisyphuslabs:hooks/hooks.json:post_tool_use:0:0\"]")
expect(content).toMatch(/\[plugins\."omo@sisyphuslabs"\.mcp_servers\.git_bash\][\s\S]*?enabled = true/)
})
test("#given non-windows platforms #when updating sisyphuslabs plugin config #then disables git_bash plugin mcp policy", async () => {
for (const platform of ["linux", "darwin"] as const) {
// given
const root = await mkdtemp(join(tmpdir(), `omo-codex-config-git-bash-${platform}-`))
const configPath = join(root, "config.toml")
// when
await updateCodexConfig({
configPath,
repoRoot: "/repo/packages/omo-codex",
marketplaceName: "sisyphuslabs",
marketplaceSource: { sourceType: "local", source: "/repo/packages/omo-codex/cache/sisyphuslabs" },
pluginNames: ["omo"],
platform,
})
// then
const content = await readFile(configPath, "utf8")
expect(content).toContain('[plugins."omo@sisyphuslabs".mcp_servers.git_bash]')
expect(content).toMatch(/\[plugins\."omo@sisyphuslabs"\.mcp_servers\.git_bash\][\s\S]*?enabled = false/)
expect(content).toContain('[plugins."omo@sisyphuslabs"]')
expect(content).toContain("enabled = true")
}
})
})
+21 -1
View File
@@ -4,7 +4,7 @@ import { ensureContext7McpServer } from "./codex-config-mcp"
import { ensureAutonomousPermissions } from "./codex-config-permissions"
import { ensureCodexMultiAgentV2Config } from "./codex-multi-agent-v2-config"
import { appendBlock, findTomlSection, replaceOrInsertSetting } from "./toml-section-editor"
import type { CodexAgentConfig, CodexMarketplaceSource, TrustedHookState } from "./types"
import type { CodexAgentConfig, CodexInstallPlatform, CodexMarketplaceSource, TrustedHookState } from "./types"
const SISYPHUS_LEGACY_MARKETPLACES = ["lazycodex", "code-yeongyu-codex-plugins"] as const
const MANAGED_CODEX_AGENT_NAMES = [
@@ -22,6 +22,7 @@ export async function updateCodexConfig(input: {
readonly marketplaceName: string
readonly marketplaceSource: CodexMarketplaceSource
readonly pluginNames: readonly string[]
readonly platform?: CodexInstallPlatform
readonly trustedHookStates?: readonly TrustedHookState[]
readonly agentConfigs?: readonly CodexAgentConfig[]
readonly autonomousPermissions?: boolean
@@ -51,6 +52,7 @@ export async function updateCodexConfig(input: {
for (const pluginName of input.pluginNames) {
config = ensurePluginEnabled(config, `${pluginName}@${input.marketplaceName}`)
}
config = ensureOmoGitBashMcpPolicy(config, input)
for (const state of input.trustedHookStates ?? []) {
config = ensureHookTrusted(config, state.key, state.trustedHash)
}
@@ -142,6 +144,24 @@ function ensurePluginEnabled(config: string, pluginKey: string): string {
return replaceOrInsertSetting(config, section, "enabled", "true")
}
function ensurePluginMcpEnabled(config: string, pluginKey: string, serverName: string, enabled: boolean): string {
const header = `plugins.${JSON.stringify(pluginKey)}.mcp_servers.${serverName}`
const section = findTomlSection(config, header)
const enabledValue = enabled ? "true" : "false"
if (!section) return appendBlock(config, `[${header}]\nenabled = ${enabledValue}\n`)
return replaceOrInsertSetting(config, section, "enabled", enabledValue)
}
function ensureOmoGitBashMcpPolicy(config: string, input: {
readonly marketplaceName: string
readonly pluginNames: readonly string[]
readonly platform?: CodexInstallPlatform
}): string {
if (input.marketplaceName !== "sisyphuslabs" || !input.pluginNames.includes("omo")) return config
const enabled = (input.platform ?? process.platform) === "win32"
return ensurePluginMcpEnabled(config, "omo@sisyphuslabs", "git_bash", enabled)
}
function ensureHookTrusted(config: string, key: string, trustedHash: string): string {
const header = `hooks.state.${JSON.stringify(key)}`
const section = findTomlSection(config, header)
+49
View File
@@ -185,4 +185,53 @@ describe("git-bash", () => {
expect(runCalls).toEqual([])
expect(result).toEqual({ found: true, path: null, source: "not-required" })
})
test("#given Windows without Git Bash and winget fails #when preparing #then original install hint is preserved", async () => {
// given
const missingResolution = {
found: false,
checkedPaths: [PROGRAM_FILES_GIT_BASH, PROGRAM_FILES_X86_GIT_BASH],
installHint: "install hint",
} as const
// when
const result = await prepareGitBashForInstall({
platform: "win32",
env: {},
cwd: "C:\\repo",
resolveGitBash: () => missingResolution,
runCommand: async () => {
throw new Error("winget unavailable")
},
})
// then
expect(result).toEqual(missingResolution)
})
test("#given Windows without Git Bash and winget exits successfully but bash is still missing #when preparing #then installer still fails with install hint", async () => {
// given
const missingResolution = {
found: false,
checkedPaths: [PROGRAM_FILES_GIT_BASH, PROGRAM_FILES_X86_GIT_BASH],
installHint: "install hint",
} as const
let resolveCallCount = 0
// when
const result = await prepareGitBashForInstall({
platform: "win32",
env: {},
cwd: "C:\\repo",
resolveGitBash: () => {
resolveCallCount += 1
return missingResolution
},
runCommand: async () => undefined,
})
// then
expect(resolveCallCount).toBe(2)
expect(result).toEqual(missingResolution)
})
})
+68 -3
View File
@@ -10,6 +10,7 @@ import { findRepoRoot, findRepoRootFromImporter, resolveCodexInstallerBinDir, ru
const EXPECTED_OMO_COMPONENT_BINS = [
{ name: "omo", target: join("components", "ulw-loop", "dist", "cli.js") },
{ name: "omo-comment-checker", target: join("components", "comment-checker", "dist", "cli.js") },
{ name: "omo-git-bash-hook", target: join("components", "git-bash", "dist", "cli.js") },
{ name: "omo-lsp", target: join("components", "lsp", "dist", "cli.js") },
{ name: "omo-rules", target: join("components", "rules", "dist", "cli.js") },
{ name: "omo-start-work-continuation", target: join("components", "start-work-continuation", "dist", "cli.js") },
@@ -153,10 +154,12 @@ describe("install-codex", () => {
expect(skillNames).toContain("ulw-loop")
expect(skillNames).not.toContain("planing-prometheustic")
const mcpManifest = JSON.parse(await readFile(join(pluginPath ?? "", ".mcp.json"), "utf8")) as {
mcpServers: { ast_grep: { args: string[] }; lsp: { args: string[] } }
mcpServers: { ast_grep: { args: string[] }; git_bash: { args: string[] }; lsp: { args: string[] } }
}
expect(mcpManifest.mcpServers.ast_grep.args[0]).toBe(join(pluginPath ?? "", "components", "ast-grep-mcp", "dist", "cli.js"))
expect((await stat(mcpManifest.mcpServers.ast_grep.args[0] ?? "")).isFile()).toBe(true)
expect(mcpManifest.mcpServers.git_bash.args[0]).toBe(join(pluginPath ?? "", "components", "git-bash-mcp", "dist", "cli.js"))
expect((await stat(mcpManifest.mcpServers.git_bash.args[0] ?? "")).isFile()).toBe(true)
expect(mcpManifest.mcpServers.lsp.args[0]).toBe(join(pluginPath ?? "", "components", "lsp-tools-mcp", "dist", "cli.js"))
expect(mcpManifest.mcpServers.lsp.args[0]).not.toContain("components/lsp/packages")
expect(mcpManifest.mcpServers.lsp.args[0]?.startsWith(pluginPath ?? "")).toBe(true)
@@ -175,7 +178,64 @@ describe("install-codex", () => {
legacyCacheMissing = error instanceof Error
}
expect(legacyCacheMissing).toBe(true)
}, { timeout: 15_000 })
})
test("#given simulated Windows Codex install #when installing omo #then enables git_bash MCP and trusts shell hooks", async () => {
// given
const codexHome = await mkdtemp(join(tmpdir(), "omo-codex-home-git-bash-win-"))
const binDir = await mkdtemp(join(tmpdir(), "omo-codex-bin-git-bash-win-"))
const repoRoot = process.cwd()
// when
const result = await runCodexInstaller({
codexHome,
binDir,
repoRoot,
platform: "win32",
gitBashResolver: () => ({ found: true, path: "C:\\Program Files\\Git\\bin\\bash.exe", source: "program-files" }),
runCommand: async () => undefined,
})
// then
const configContent = await readFile(join(codexHome, "config.toml"), "utf8")
expect(configContent).toContain('[plugins."omo@sisyphuslabs".mcp_servers.git_bash]')
expect(configContent).toContain("enabled = true")
expect(configContent).toContain("pre_tool_use")
expect(configContent).toContain("post_compact")
expect(result.gitBashPath).toBe("C:\\Program Files\\Git\\bin\\bash.exe")
const pluginPath = result.installed[0]?.path ?? ""
const mcpManifest = JSON.parse(await readFile(join(pluginPath, ".mcp.json"), "utf8")) as {
readonly mcpServers: { readonly git_bash: { readonly args: readonly string[] } }
}
expect(mcpManifest.mcpServers.git_bash.args[0]).toBe(join(pluginPath, "components", "git-bash-mcp", "dist", "cli.js"))
expect((await stat(mcpManifest.mcpServers.git_bash.args[0] ?? "")).isFile()).toBe(true)
})
test("#given simulated Linux Codex install #when installing omo #then keeps git_bash manifest but disables policy exposure", async () => {
// given
const codexHome = await mkdtemp(join(tmpdir(), "omo-codex-home-git-bash-linux-"))
const binDir = await mkdtemp(join(tmpdir(), "omo-codex-bin-git-bash-linux-"))
const repoRoot = process.cwd()
// when
const result = await runCodexInstaller({
codexHome,
binDir,
repoRoot,
platform: "linux",
runCommand: async () => undefined,
})
// then
const configContent = await readFile(join(codexHome, "config.toml"), "utf8")
expect(configContent).toContain('[plugins."omo@sisyphuslabs".mcp_servers.git_bash]')
expect(configContent).toContain("enabled = false")
const pluginPath = result.installed[0]?.path ?? ""
const mcpManifest = JSON.parse(await readFile(join(pluginPath, ".mcp.json"), "utf8")) as {
readonly mcpServers: { readonly git_bash: { readonly args: readonly string[] } }
}
expect(mcpManifest.mcpServers.git_bash.args[0]).toBe(join(pluginPath, "components", "git-bash-mcp", "dist", "cli.js"))
})
test("#given codex installer #when installing omo #then links omo-prefixed component CLIs to existing cached runtimes", async () => {
// given
@@ -267,6 +327,7 @@ describe("install-codex", () => {
const snapshotMcpManifest: {
readonly mcpServers: {
readonly ast_grep: { readonly args: readonly string[] }
readonly git_bash: { readonly args: readonly string[] }
readonly lsp: { readonly args: readonly string[] }
}
} = JSON.parse(await readFile(join(snapshotPluginPath, ".mcp.json"), "utf8"))
@@ -274,13 +335,17 @@ describe("install-codex", () => {
join(snapshotPluginPath, "components", "ast-grep-mcp", "dist", "cli.js"),
)
expect((await stat(snapshotMcpManifest.mcpServers.ast_grep.args[0] ?? "")).isFile()).toBe(true)
expect(snapshotMcpManifest.mcpServers.git_bash.args[0]).toBe(
join(snapshotPluginPath, "components", "git-bash-mcp", "dist", "cli.js"),
)
expect((await stat(snapshotMcpManifest.mcpServers.git_bash.args[0] ?? "")).isFile()).toBe(true)
expect(snapshotMcpManifest.mcpServers.lsp.args[0]).toBe(
join(snapshotPluginPath, "components", "lsp-tools-mcp", "dist", "cli.js"),
)
expect(snapshotMcpManifest.mcpServers.lsp.args[0]).not.toContain("../../lsp-tools-mcp")
expect(snapshotMcpManifest.mcpServers.lsp.args[0]).not.toContain("components/lsp/packages")
expect((await stat(snapshotMcpManifest.mcpServers.lsp.args[0] ?? "")).isFile()).toBe(true)
}, { timeout: 15_000 })
})
test("#given autonomous permissions requested #when installing omo #then writes Codex autonomy settings", async () => {
// given
+1
View File
@@ -129,6 +129,7 @@ export async function runCodexInstaller(options: CodexInstallOptions = {}): Prom
marketplaceName: marketplace.name,
marketplaceSource: codexMarketplaceSource(marketplaceRoot),
pluginNames: marketplace.plugins.map((plugin) => plugin.name),
platform,
trustedHookStates,
agentConfigs: [...agentConfigs.values()].sort((left, right) => left.name.localeCompare(right.name)),
autonomousPermissions: options.autonomousPermissions === true,