fix(codex): harden windows light install
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user