mirror of
https://github.com/nexu-io/open-design.git
synced 2026-09-28 05:22:59 +08:00
fix(daemon): scope Codex refresh to its managed install
Require the existing registration's managed discovery marker to match before refreshing it, so local, stable, and prerelease installs cannot take over the global name from one another. Cover the unrelated-owner path without invoking a config mutation.\n\nGenerated-By: looper 0.11.2 (runner=fixer, agent=codex)
This commit is contained in:
@@ -135,14 +135,39 @@ export async function installCodexMcp(spec: CodexInstallSpec): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
export type CodexRegistrationRefresh = 'refreshed' | 'absent' | 'unavailable';
|
||||
export type CodexRegistrationRefresh = 'refreshed' | 'absent' | 'unavailable' | 'not-owned';
|
||||
|
||||
// Rewrites an existing registration so it follows the runtime that is running
|
||||
// now. It never creates one: installing stays an explicit user action.
|
||||
export async function refreshExistingCodexMcp(spec: CodexInstallSpec): Promise<CodexRegistrationRefresh> {
|
||||
const status = await probeCodexInstall(spec.name);
|
||||
if (!status.available) return 'unavailable';
|
||||
if (!status.installed) return 'absent';
|
||||
interface CodexMcpGetOutput {
|
||||
transport?: {
|
||||
env?: Record<string, unknown>;
|
||||
};
|
||||
}
|
||||
|
||||
// Rewrites an existing registration only when its ownership marker proves it
|
||||
// belongs to this managed install. It never creates or takes over one.
|
||||
export async function refreshExistingCodexMcp(
|
||||
spec: CodexInstallSpec,
|
||||
ownershipEnvKey: string,
|
||||
): Promise<CodexRegistrationRefresh> {
|
||||
let result: CodexRunnerResult;
|
||||
try {
|
||||
result = await activeRunner().run(['mcp', 'get', spec.name, '--json']);
|
||||
} catch (err) {
|
||||
if ((err as NodeJS.ErrnoException | undefined)?.code === 'ENOENT') return 'unavailable';
|
||||
throw err;
|
||||
}
|
||||
if (result.exitCode !== 0) return 'absent';
|
||||
|
||||
let existing: CodexMcpGetOutput;
|
||||
try {
|
||||
existing = JSON.parse(result.stdout) as CodexMcpGetOutput;
|
||||
} catch {
|
||||
return 'not-owned';
|
||||
}
|
||||
const expectedOwner = spec.env[ownershipEnvKey];
|
||||
if (expectedOwner == null || existing.transport?.env?.[ownershipEnvKey] !== expectedOwner) {
|
||||
return 'not-owned';
|
||||
}
|
||||
await installCodexMcp(spec);
|
||||
return 'refreshed';
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Express } from 'express';
|
||||
import fs from 'node:fs';
|
||||
import { SIDECAR_ENV } from '@open-design/sidecar-proto';
|
||||
import { MCP_BOOTSTRAP_CONTRACT, SIDECAR_ENV } from '@open-design/sidecar-proto';
|
||||
import { buildMcpInstallPayload, type McpInstallPayload } from './mcp-install-info.js';
|
||||
import { installCodexMcp, probeCodexInstall, refreshExistingCodexMcp, uninstallCodexMcp } from './codex-cli.js';
|
||||
import { isManagedMcpBootstrapEnv } from './mcp-bootstrap.js';
|
||||
@@ -117,15 +117,18 @@ export function registerMcpRoutes(app: Express, ctx: RegisterMcpRoutesDeps) {
|
||||
// and only need to track its argv. See apps/daemon/src/codex-cli.ts.
|
||||
const CODEX_MCP_NAME = 'open-design';
|
||||
|
||||
// Under a managed outer, keep an existing Codex registration pointed at the
|
||||
// runtime that is running now. Registrations name a versioned payload, and a
|
||||
// payload version is only cleaned up after a newer one has started here, so
|
||||
// refreshing on every start keeps them valid. Never installs one.
|
||||
// Under a managed outer, keep its existing Codex registration pointed at the
|
||||
// runtime that is running now. The discovery value is derived from the
|
||||
// sidecar source/channel/namespace, so it also proves the globally named
|
||||
// registration belongs to this install before we refresh it.
|
||||
if (isManagedMcpBootstrapEnv(process.env)) {
|
||||
const timer = setTimeout(() => {
|
||||
const payload = computeInstallPayload();
|
||||
if (!payload.cliExists || !payload.nodeExists) return;
|
||||
refreshExistingCodexMcp({ name: CODEX_MCP_NAME, command: payload.command, args: payload.args, env: payload.env })
|
||||
refreshExistingCodexMcp(
|
||||
{ name: CODEX_MCP_NAME, command: payload.command, args: payload.args, env: payload.env },
|
||||
MCP_BOOTSTRAP_CONTRACT.DISCOVERY_ENV,
|
||||
)
|
||||
.then((outcome) => console.info('[mcp] codex registration refresh', { outcome }))
|
||||
.catch((err: unknown) => console.warn('[mcp] codex registration refresh failed', {
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
|
||||
@@ -198,27 +198,53 @@ describe('codex-cli uninstall', () => {
|
||||
});
|
||||
|
||||
describe('refreshExistingCodexMcp', () => {
|
||||
const ownershipEnvKey = 'OD_MCP_DISCOVERY';
|
||||
const spec = {
|
||||
name: 'open-design',
|
||||
command: '/Applications/Open Design.app/Contents/MacOS/node',
|
||||
args: ['/Applications/Open Design.app/cli.js', 'mcp'],
|
||||
env: { OD_MCP_BOOTSTRAP_ARGS: '["--headless","--od-mcp-managed"]' },
|
||||
env: {
|
||||
OD_MCP_BOOTSTRAP_ARGS: '["--headless","--od-mcp-managed"]',
|
||||
[ownershipEnvKey]: '{"daemon":["/owned/daemon.sock"],"desktop":["/owned/desktop.sock"]}',
|
||||
},
|
||||
};
|
||||
|
||||
it('rewrites an existing registration', async () => {
|
||||
const runner = makeStubRunner(async () => ({ exitCode: 0, stdout: '', stderr: '' }));
|
||||
it('rewrites a registration owned by the same managed install', async () => {
|
||||
const runner = makeStubRunner(async (call) => call.args[1] === 'get'
|
||||
? {
|
||||
exitCode: 0,
|
||||
stdout: JSON.stringify({ transport: { env: { [ownershipEnvKey]: spec.env[ownershipEnvKey] } } }),
|
||||
stderr: '',
|
||||
}
|
||||
: { exitCode: 0, stdout: '', stderr: '' });
|
||||
setCodexRunner(runner);
|
||||
await expect(refreshExistingCodexMcp(spec)).resolves.toBe('refreshed');
|
||||
await expect(refreshExistingCodexMcp(spec, ownershipEnvKey)).resolves.toBe('refreshed');
|
||||
expect(runner.calls.map((call) => call.args.slice(0, 3))).toEqual([
|
||||
['mcp', 'get', 'open-design'],
|
||||
['mcp', 'add', 'open-design'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('leaves an unrelated managed install registration byte-for-byte unchanged', async () => {
|
||||
const original = JSON.stringify({
|
||||
transport: {
|
||||
command: '/Applications/Open Design Prerelease.app/Contents/MacOS/node',
|
||||
args: ['/Applications/Open Design Prerelease.app/cli.js', 'mcp'],
|
||||
env: { [ownershipEnvKey]: '{"daemon":["/other/daemon.sock"],"desktop":["/other/desktop.sock"]}' },
|
||||
},
|
||||
});
|
||||
const runner = makeStubRunner(async () => ({ exitCode: 0, stdout: original, stderr: '' }));
|
||||
setCodexRunner(runner);
|
||||
|
||||
await expect(refreshExistingCodexMcp(spec, ownershipEnvKey)).resolves.toBe('not-owned');
|
||||
expect(runner.calls).toHaveLength(1);
|
||||
expect(runner.calls[0]?.args).toEqual(['mcp', 'get', 'open-design', '--json']);
|
||||
});
|
||||
|
||||
it('never creates a registration the user has not installed', async () => {
|
||||
const runner = makeStubRunner(async () => ({ exitCode: 1, stdout: '', stderr: 'not found' }));
|
||||
setCodexRunner(runner);
|
||||
await expect(refreshExistingCodexMcp(spec)).resolves.toBe('absent');
|
||||
await expect(refreshExistingCodexMcp(spec, ownershipEnvKey)).resolves.toBe('absent');
|
||||
expect(runner.calls).toHaveLength(1);
|
||||
});
|
||||
|
||||
@@ -228,6 +254,6 @@ describe('refreshExistingCodexMcp', () => {
|
||||
throw Object.assign(new Error('spawn codex ENOENT'), { code: 'ENOENT' });
|
||||
},
|
||||
});
|
||||
await expect(refreshExistingCodexMcp(spec)).resolves.toBe('unavailable');
|
||||
await expect(refreshExistingCodexMcp(spec, ownershipEnvKey)).resolves.toBe('unavailable');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user