agencyx-cli 0.0.0-stage → 0.1.0-dev.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/adapters/agent.js +73 -0
  2. package/dist/adapters/ambient.js +35 -0
  3. package/dist/adapters/instructions.js +102 -0
  4. package/dist/adapters/mcp.js +237 -0
  5. package/dist/adapters/permissions.js +42 -0
  6. package/dist/adapters/plugins.js +484 -0
  7. package/dist/agency/invocation.js +97 -0
  8. package/dist/agency/prepared-input.js +124 -0
  9. package/dist/bridge/execution.js +119 -0
  10. package/dist/bridge/main.js +26 -0
  11. package/dist/bridge/plan.js +268 -0
  12. package/dist/bridge/version.js +2 -0
  13. package/dist/cli-options.js +96 -0
  14. package/dist/cli.js +41 -0
  15. package/dist/codex/agents.js +72 -0
  16. package/dist/codex/app-server.js +116 -0
  17. package/dist/codex/inspection.js +217 -0
  18. package/dist/codex/mcp-readiness.js +45 -0
  19. package/dist/codex/native-plugins.js +109 -0
  20. package/dist/codex/options.js +170 -0
  21. package/dist/codex/plugin-hooks.js +93 -0
  22. package/dist/codex/rpc.js +170 -0
  23. package/dist/codex/session-proxy.js +258 -0
  24. package/dist/codex/skill-policy.js +69 -0
  25. package/dist/codex/skill-views.js +104 -0
  26. package/dist/codex/toml.js +46 -0
  27. package/dist/commands/codex.js +118 -0
  28. package/dist/commands/doctor.js +58 -0
  29. package/dist/commands/passthrough.js +14 -0
  30. package/dist/diagnostics/plan.js +87 -0
  31. package/dist/diagnostics.js +39 -0
  32. package/dist/model.js +2 -0
  33. package/dist/runtime/ipc.js +316 -0
  34. package/dist/runtime/process.js +286 -0
  35. package/dist/runtime/resolve-command.js +38 -0
  36. package/dist/runtime/session.js +107 -0
  37. package/dist/validation.js +31 -0
  38. package/package.json +41 -4
  39. package/README.md +0 -3
package/dist/cli.js ADDED
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+ import { readFile } from "node:fs/promises";
3
+ import { parseCommand, HELP } from "./cli-options.js";
4
+ import { asDiagnostic, errorExitCode, formatDiagnostic, } from "./diagnostics.js";
5
+ export async function main(args = process.argv.slice(2)) {
6
+ let json = false;
7
+ try {
8
+ const command = parseCommand(args);
9
+ json =
10
+ (command.kind === "doctor" || command.kind === "codex") && command.json;
11
+ switch (command.kind) {
12
+ case "help":
13
+ process.stdout.write(HELP);
14
+ return 0;
15
+ case "version": {
16
+ const manifest = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"));
17
+ process.stdout.write(`agencyx ${manifest.version}\n`);
18
+ return 0;
19
+ }
20
+ case "passthrough":
21
+ return await (await import("./commands/passthrough.js")).passthrough(command.args);
22
+ case "doctor":
23
+ return await (await import("./commands/doctor.js")).doctor(command.json);
24
+ case "codex":
25
+ return await (await import("./commands/codex.js")).runCodex(command);
26
+ case "bridge":
27
+ return await (await import("./bridge/main.js")).bridgeMain(args.slice(1));
28
+ }
29
+ }
30
+ catch (error) {
31
+ const diagnostic = asDiagnostic(error);
32
+ if (json)
33
+ process.stdout.write(`${JSON.stringify({ schemaVersion: 1, ready: false, errors: [diagnostic] }, null, 2)}\n`);
34
+ else
35
+ process.stderr.write(`${formatDiagnostic(diagnostic)}\n`);
36
+ return errorExitCode(error);
37
+ }
38
+ }
39
+ // Setting exitCode lets inherited terminal output and acknowledged IPC flush.
40
+ process.exitCode = await main();
41
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1,72 @@
1
+ import path from "node:path";
2
+ import { createHash } from "node:crypto";
3
+ import { chmod, mkdtemp, writeFile } from "node:fs/promises";
4
+ import { stringify } from "smol-toml";
5
+ import { combineInstructions } from "../adapters/instructions.js";
6
+ import { fail } from "../diagnostics.js";
7
+ import { isRecord } from "../validation.js";
8
+ export async function prepareAgents(packages, sessionRoot, inspection, instructions) {
9
+ const bindings = [];
10
+ const roles = Object.create(null);
11
+ const existing = isRecord(inspection.config.agents)
12
+ ? inspection.config.agents
13
+ : {};
14
+ if (!packages.some(({ spec }) => spec.agents.length))
15
+ return { bindings, overrides: {} };
16
+ if (existing.enabled === false)
17
+ fail("AGENT_RUNTIME_DISABLED", "compatibility", "Native Codex agents are disabled in the effective configuration.");
18
+ if (inspection.requirementsKnown === false ||
19
+ inspection.requirements?.agents !== undefined)
20
+ fail("AGENT_POLICY_UNVERIFIED", "compatibility", "Managed agent requirements must be verified before registering plugin agents.");
21
+ const directory = await mkdtemp(path.join(sessionRoot, "agents-"));
22
+ await chmod(directory, 0o700);
23
+ for (const { spec, resourceRoot } of packages) {
24
+ for (const agent of spec.agents) {
25
+ const name = `${spec.name}:${agent.name}`;
26
+ if (Object.hasOwn(existing, name) || Object.hasOwn(roles, name))
27
+ fail("AGENT_CONFLICT", "compatibility", "A native agent already uses the selected plugin agent's qualified name.", { component: name });
28
+ if (agent.model !== undefined && agent.model !== inspection.config.model)
29
+ fail("AGENT_MODEL_UNSUPPORTED", "compatibility", "The plugin agent model does not match the effective Codex model. No model-name substitution is performed.", { component: name, path: agent.sourcePath, field: "model" });
30
+ const relative = path.relative(spec.root, agent.sourcePath);
31
+ const source = path.join(resourceRoot, relative);
32
+ const body = combineInstructions(instructions ??
33
+ (typeof inspection.config.developer_instructions === "string"
34
+ ? inspection.config.developer_instructions
35
+ : undefined), [
36
+ `Plugin resources for this agent: ${JSON.stringify(resourceRoot)}.\nOriginal agent document: ${JSON.stringify(source)}.\nResolve plugin-relative resources within that package.`,
37
+ agent.instructions,
38
+ ], {});
39
+ const file = path.join(directory, `${createHash("sha256").update(name).digest("hex")}.toml`);
40
+ await writeFile(file, stringify({
41
+ name,
42
+ description: agent.description,
43
+ developer_instructions: body,
44
+ ...(agent.model !== undefined ? { model: agent.model } : {}),
45
+ }), { mode: 0o600 });
46
+ roles[name] = { description: agent.description, config_file: file };
47
+ bindings.push({
48
+ name,
49
+ pluginName: spec.name,
50
+ sourcePath: agent.sourcePath,
51
+ configFile: file,
52
+ ...(agent.declaredTools !== undefined
53
+ ? { declaredTools: agent.declaredTools }
54
+ : {}),
55
+ });
56
+ }
57
+ }
58
+ return { bindings, overrides: { agents: roles } };
59
+ }
60
+ export function verifyNativeAgents(bindings, inspection) {
61
+ const roles = isRecord(inspection.config.agents)
62
+ ? inspection.config.agents
63
+ : {};
64
+ for (const binding of bindings) {
65
+ const role = roles[binding.name];
66
+ if (roles.enabled === false ||
67
+ !isRecord(role) ||
68
+ role.config_file !== binding.configFile)
69
+ fail("AGENT_REGISTRATION_FAILED", "compatibility", "The native session did not retain the selected agent configuration.", { component: binding.name });
70
+ }
71
+ }
72
+ //# sourceMappingURL=agents.js.map
@@ -0,0 +1,116 @@
1
+ import net from "node:net";
2
+ import path from "node:path";
3
+ import { randomBytes } from "node:crypto";
4
+ import { writeFile } from "node:fs/promises";
5
+ import { setTimeout as delay } from "node:timers/promises";
6
+ import { fail } from "../diagnostics.js";
7
+ import { runInteractive, startProcess } from "../runtime/process.js";
8
+ import { websocketClient } from "./rpc.js";
9
+ import { configArguments } from "./toml.js";
10
+ import { createSessionProxy } from "./session-proxy.js";
11
+ import { registerSkillViews, verifySkillViews } from "./skill-views.js";
12
+ import { readNativeSkills } from "./inspection.js";
13
+ export async function reserveLoopbackPort() {
14
+ const server = net.createServer();
15
+ await new Promise((resolve, reject) => {
16
+ server.once("error", reject);
17
+ server.listen(0, "127.0.0.1", resolve);
18
+ });
19
+ const address = server.address();
20
+ if (!address || typeof address === "string") {
21
+ server.close();
22
+ fail("BACKEND_ADDRESS_INVALID", "codex", "A private loopback port could not be reserved.", {}, 6);
23
+ }
24
+ await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
25
+ return address.port;
26
+ }
27
+ export async function runAppServer(context, plan, options, env, signal) {
28
+ const token = randomBytes(32).toString("hex");
29
+ const tokenFile = path.join(context.sessionRoot, "remote-token");
30
+ await writeFile(tokenFile, token, { mode: 0o600 });
31
+ const endpoint = `ws://127.0.0.1:${await reserveLoopbackPort()}`;
32
+ const overrides = configArguments(plan.overrides);
33
+ const backend = startProcess(context.codexPath, [
34
+ ...options.configArgs,
35
+ ...overrides,
36
+ "app-server",
37
+ "--listen",
38
+ endpoint,
39
+ "--ws-auth",
40
+ "capability-token",
41
+ "--ws-token-file",
42
+ tokenFile,
43
+ ], {
44
+ cwd: context.cwd,
45
+ env,
46
+ stdio: ["ignore", "pipe", "pipe"],
47
+ windowsHide: true,
48
+ detached: process.platform !== "win32",
49
+ });
50
+ backend.child.stdout.resume();
51
+ backend.child.stderr.resume();
52
+ let client;
53
+ let proxy;
54
+ let shuttingDown = false;
55
+ let backendFailed = false;
56
+ let frontendStarted = false;
57
+ const frontendAbort = new AbortController();
58
+ const combinedSignal = AbortSignal.any([signal, frontendAbort.signal]);
59
+ void backend.exited.then(() => {
60
+ if (!shuttingDown) {
61
+ backendFailed = true;
62
+ frontendAbort.abort("SIGTERM");
63
+ }
64
+ }, () => {
65
+ backendFailed = true;
66
+ frontendAbort.abort("SIGTERM");
67
+ });
68
+ try {
69
+ const deadline = Date.now() + 15_000;
70
+ while (!client && Date.now() < deadline && !combinedSignal.aborted) {
71
+ try {
72
+ client = await websocketClient(endpoint, token, 1_000);
73
+ }
74
+ catch {
75
+ await delay(50);
76
+ }
77
+ }
78
+ if (!client)
79
+ fail("BACKEND_START_FAILED", "codex", "The private authenticated Codex app-server did not become ready.", {}, 6);
80
+ await client.initialize();
81
+ await registerSkillViews(client, plan.skillViews ?? []);
82
+ await verifySkillViews(plan.skillViews ?? [], await readNativeSkills(client, context.cwd));
83
+ client.close();
84
+ client = undefined;
85
+ const frontendToken = randomBytes(32).toString("hex");
86
+ proxy = await createSessionProxy(endpoint, token, frontendToken, plan);
87
+ const tokenEnv = `AGENCYX_REMOTE_TOKEN_${context.sessionId.replaceAll("-", "_")}`;
88
+ frontendStarted = true;
89
+ const result = await runInteractive(context.codexPath, [
90
+ "--remote",
91
+ proxy.endpoint,
92
+ "--remote-auth-token-env",
93
+ tokenEnv,
94
+ ...options.original,
95
+ ], {
96
+ cwd: context.cwd,
97
+ env: { ...env, [tokenEnv]: frontendToken },
98
+ signal: combinedSignal,
99
+ });
100
+ if (backendFailed && result.code === 0)
101
+ fail("BACKEND_EXITED", "codex", "The owned backend exited before its frontend completed.", {}, 6);
102
+ const protocolFailure = proxy.failure();
103
+ if (protocolFailure && result.code === 0)
104
+ fail(protocolFailure.code, protocolFailure.stage, protocolFailure.reason, {}, 6);
105
+ return result;
106
+ }
107
+ finally {
108
+ shuttingDown = true;
109
+ client?.close();
110
+ await proxy?.close();
111
+ await backend.stop();
112
+ if (frontendStarted)
113
+ process.stderr.write("Agencyx session ended; its private Codex backend has stopped.\n");
114
+ }
115
+ }
116
+ //# sourceMappingURL=app-server.js.map
@@ -0,0 +1,217 @@
1
+ import { AgencyxError, fail } from "../diagnostics.js";
2
+ import { isRecord, record } from "../validation.js";
3
+ import { captureProcess, startProcess } from "../runtime/process.js";
4
+ import { RpcError, stdioClient } from "./rpc.js";
5
+ import { verifyMcpReadiness } from "./mcp-readiness.js";
6
+ import { registerSkillViews, verifySkillViews } from "./skill-views.js";
7
+ export async function readNativeSkills(client, cwd) {
8
+ return nativeComponents(await client.request("skills/list", { cwds: [cwd], forceReload: true }), "skills").map((skill) => {
9
+ if (typeof skill.path !== "string" ||
10
+ typeof skill.name !== "string" ||
11
+ typeof skill.enabled !== "boolean")
12
+ fail("CODEX_PROTOCOL_INVALID", "codex", "Codex returned invalid skill metadata.", {}, 6);
13
+ return {
14
+ name: skill.name,
15
+ path: skill.path,
16
+ enabled: skill.enabled,
17
+ pluginId: typeof skill.pluginId === "string" ? skill.pluginId : null,
18
+ };
19
+ });
20
+ }
21
+ export function nativeComponents(value, component) {
22
+ const response = record(value, `Codex ${component}`);
23
+ if (!Array.isArray(response.data))
24
+ fail("CODEX_PROTOCOL_INVALID", "codex", "Codex returned an invalid component catalog.", {}, 6);
25
+ const result = [];
26
+ for (const raw of response.data) {
27
+ const entry = record(raw, `Codex ${component} entry`);
28
+ if (Array.isArray(entry.errors) && entry.errors.length)
29
+ fail("CODEX_COMPONENT_LOAD_FAILED", "configuration", "Codex reported component discovery errors. Check the original Codex diagnostics.", { component }, 5);
30
+ const items = entry[component];
31
+ if (!Array.isArray(items))
32
+ fail("CODEX_PROTOCOL_INVALID", "codex", "Codex returned an incomplete component catalog.", {}, 6);
33
+ for (const item of items)
34
+ result.push(record(item, `Codex ${component} item`));
35
+ }
36
+ return result;
37
+ }
38
+ export function nativePlugins(response) {
39
+ const body = record(response, "Codex plugins");
40
+ if (!Array.isArray(body.marketplaces))
41
+ fail("CODEX_PROTOCOL_INVALID", "codex", "Codex did not return a plugin marketplace list.", {}, 6);
42
+ const result = [];
43
+ for (const raw of body.marketplaces) {
44
+ const marketplace = record(raw, "Codex marketplace");
45
+ if (!Array.isArray(marketplace.plugins))
46
+ fail("CODEX_PROTOCOL_INVALID", "codex", "Codex returned an invalid marketplace entry.", {}, 6);
47
+ for (const item of marketplace.plugins) {
48
+ const plugin = record(item, "Codex plugin");
49
+ if (typeof plugin.id !== "string" ||
50
+ typeof plugin.name !== "string" ||
51
+ typeof plugin.installed !== "boolean" ||
52
+ typeof plugin.enabled !== "boolean")
53
+ fail("CODEX_PROTOCOL_INVALID", "codex", "Codex returned incomplete plugin state.", {}, 6);
54
+ result.push({
55
+ id: plugin.id,
56
+ name: plugin.name,
57
+ installed: plugin.installed,
58
+ enabled: plugin.enabled,
59
+ source: record(plugin.source, plugin.id),
60
+ managed: plugin.installPolicySource !== null &&
61
+ plugin.installPolicySource !== undefined,
62
+ ...(typeof marketplace.path === "string"
63
+ ? { marketplacePath: marketplace.path }
64
+ : {}),
65
+ });
66
+ }
67
+ }
68
+ return result;
69
+ }
70
+ export async function readNativePlugins(client, cwd) {
71
+ let response;
72
+ try {
73
+ response = await client.request("plugin/installed", { cwds: [cwd] });
74
+ }
75
+ catch (error) {
76
+ if (!(error instanceof RpcError) || error.rpcCode !== -32601)
77
+ throw error;
78
+ response = await client.request("plugin/list", {
79
+ cwds: [cwd],
80
+ marketplaceKinds: ["local"],
81
+ });
82
+ }
83
+ return nativePlugins(response);
84
+ }
85
+ export async function inspectCodex(codexPath, options, cwd, env, request = {}) {
86
+ const { selectedMcps, signal, skillViews = [] } = request;
87
+ if (options.nativeProfile) {
88
+ // The official app-server rejects --profile, even before the subcommand.
89
+ // Do not inspect the base config and pretend it describes a different runtime profile.
90
+ fail("CODEX_PROFILE_INSPECTION_UNSUPPORTED", "compatibility", "The installed app-server cannot inspect native Codex profiles. Agency profiles remain supported before --; native Codex --profile requires a verified runtime inspection path.", { field: "--profile" });
91
+ }
92
+ const help = await captureProcess(codexPath, ["--help"], { cwd, env });
93
+ const serverHelp = await captureProcess(codexPath, ["app-server", "--help"], {
94
+ cwd,
95
+ env,
96
+ });
97
+ if (help.code !== 0 || serverHelp.code !== 0)
98
+ fail("CODEX_PROTOCOL_UNSUPPORTED", "discovery", "The installed Codex does not expose the required official CLI and app-server interfaces.", {}, 3);
99
+ const running = startProcess(codexPath, [...options.configArgs, "app-server", "--listen", "stdio://"], {
100
+ cwd,
101
+ env,
102
+ stdio: ["pipe", "pipe", "pipe"],
103
+ windowsHide: true,
104
+ detached: process.platform !== "win32",
105
+ });
106
+ running.child.stderr.resume();
107
+ const client = stdioClient(running);
108
+ const cancel = () => {
109
+ void running.stop(signal?.reason === "SIGINT" ? "SIGINT" : "SIGTERM");
110
+ };
111
+ signal?.addEventListener("abort", cancel, { once: true });
112
+ if (signal?.aborted)
113
+ cancel();
114
+ try {
115
+ let initialized;
116
+ try {
117
+ initialized = await client.initialize();
118
+ }
119
+ catch {
120
+ fail("CODEX_CONFIG_INVALID", "configuration", "Codex could not initialize with the existing configuration. Check it with the original Codex CLI; agencyx will not repair or replace it.", {}, 5);
121
+ }
122
+ const init = record(initialized, "Codex initialization");
123
+ if (typeof init.codexHome !== "string")
124
+ fail("CODEX_PROTOCOL_UNSUPPORTED", "codex", "The installed Codex does not report its active state directory.", {}, 6);
125
+ const response = record(await client.request("config/read", { cwd, includeLayers: true }), "Codex configuration");
126
+ const config = record(response.config, "Codex effective configuration");
127
+ const plugins = await readNativePlugins(client, cwd);
128
+ await registerSkillViews(client, skillViews);
129
+ const skills = await readNativeSkills(client, cwd);
130
+ await verifySkillViews(skillViews, skills);
131
+ const hooks = nativeComponents(await client.request("hooks/list", { cwds: [cwd] }), "hooks").map((hook) => {
132
+ if (typeof hook.sourcePath !== "string" ||
133
+ typeof hook.eventName !== "string" ||
134
+ typeof hook.enabled !== "boolean" ||
135
+ typeof hook.trustStatus !== "string")
136
+ fail("CODEX_PROTOCOL_INVALID", "codex", "Codex returned invalid hook metadata.", {}, 6);
137
+ return {
138
+ sourcePath: hook.sourcePath,
139
+ eventName: hook.eventName,
140
+ enabled: hook.enabled,
141
+ trustStatus: hook.trustStatus,
142
+ pluginId: typeof hook.pluginId === "string" ? hook.pluginId : null,
143
+ ...(typeof hook.command === "string" ? { command: hook.command } : {}),
144
+ ...(typeof hook.matcher === "string" || hook.matcher === null
145
+ ? { matcher: hook.matcher }
146
+ : {}),
147
+ ...(typeof hook.key === "string" ? { key: hook.key } : {}),
148
+ ...(typeof hook.currentHash === "string"
149
+ ? { currentHash: hook.currentHash }
150
+ : {}),
151
+ ...(typeof hook.isManaged === "boolean"
152
+ ? { isManaged: hook.isManaged }
153
+ : {}),
154
+ };
155
+ });
156
+ if (selectedMcps)
157
+ await verifyMcpReadiness(client, selectedMcps);
158
+ let requirements = null;
159
+ let requirementsKnown = true;
160
+ try {
161
+ const response = record(await client.request("configRequirements/read", null), "Codex requirements");
162
+ requirementsKnown = Object.hasOwn(response, "requirements");
163
+ if (isRecord(response.requirements))
164
+ requirements = response.requirements;
165
+ else if (response.requirements !== null &&
166
+ response.requirements !== undefined)
167
+ fail("CODEX_PROTOCOL_INVALID", "codex", "Codex returned invalid managed requirements.", {}, 6);
168
+ }
169
+ catch (error) {
170
+ if (!(error instanceof RpcError) || error.rpcCode !== -32601)
171
+ throw error;
172
+ requirementsKnown = false;
173
+ }
174
+ return {
175
+ config,
176
+ rootMcps: config.mcp_servers === undefined
177
+ ? {}
178
+ : record(config.mcp_servers, "Codex mcp_servers"),
179
+ plugins,
180
+ skills,
181
+ hooks,
182
+ codexHome: init.codexHome,
183
+ requirements,
184
+ requirementsKnown,
185
+ ...(Array.isArray(response.layers)
186
+ ? {
187
+ sessionOverrides: response.layers.reduce((result, layer) => isRecord(layer) &&
188
+ isRecord(layer.name) &&
189
+ layer.name.type === "sessionFlags" &&
190
+ isRecord(layer.config)
191
+ ? layer.config
192
+ : result, {}),
193
+ }
194
+ : {}),
195
+ capabilities: {
196
+ noDaemon: help.stdout.includes("--no-daemon"),
197
+ remoteTui: help.stdout.includes("--remote-auth-token-env"),
198
+ websocketAuth: serverHelp.stdout.includes("--ws-token-file"),
199
+ },
200
+ };
201
+ }
202
+ catch (error) {
203
+ if (error instanceof AgencyxError)
204
+ throw error;
205
+ throw new AgencyxError({
206
+ code: "CODEX_INSPECTION_FAILED",
207
+ stage: "codex",
208
+ reason: "The effective Codex configuration could not be inspected.",
209
+ }, 6);
210
+ }
211
+ finally {
212
+ signal?.removeEventListener("abort", cancel);
213
+ client.close();
214
+ await running.stop();
215
+ }
216
+ }
217
+ //# sourceMappingURL=inspection.js.map
@@ -0,0 +1,45 @@
1
+ import { fail } from "../diagnostics.js";
2
+ import { isRecord, record, stringArray } from "../validation.js";
3
+ export function verifyMcpCatalog(name, value, expected) {
4
+ const response = record(value, "Codex MCP catalog");
5
+ if (!Array.isArray(response.data))
6
+ fail("CODEX_PROTOCOL_INVALID", "codex", "Codex returned an invalid MCP inventory.", { component: name }, 6);
7
+ const server = response.data.find((entry) => isRecord(entry) && entry.name === name);
8
+ if (!isRecord(server))
9
+ fail("MCP_NOT_DISCOVERED", "compatibility", "The selected MCP server is absent from Codex discovery.", { component: name });
10
+ if (server.authStatus === "notLoggedIn")
11
+ fail("MCP_AUTH_REQUIRED", "compatibility", "The selected MCP server requires authentication. Complete the original Agency authentication flow.", { component: name });
12
+ if (server.toolsError !== undefined && server.toolsError !== null)
13
+ fail("MCP_DISCOVERY_FAILED", "compatibility", "Codex could not retrieve the selected MCP tool catalog. Check Agency authentication and the proxy process. Raw server errors were withheld.", { component: name });
14
+ const tools = record(server.tools, "Codex MCP tools");
15
+ const names = Object.values(tools).map((tool) => {
16
+ if (!isRecord(tool) || typeof tool.name !== "string")
17
+ fail("CODEX_PROTOCOL_INVALID", "codex", "Codex returned incomplete tool metadata.", { component: name }, 6);
18
+ return tool.name;
19
+ });
20
+ const allowed = expected.enabled_tools === undefined
21
+ ? undefined
22
+ : stringArray(expected.enabled_tools, "enabled_tools");
23
+ const denied = expected.disabled_tools === undefined
24
+ ? []
25
+ : stringArray(expected.disabled_tools, "disabled_tools");
26
+ if (names.some((tool) => denied.includes(tool) ||
27
+ (allowed !== undefined && !allowed.includes(tool))))
28
+ fail("MCP_FILTER_NOT_ENFORCED", "compatibility", "The native MCP catalog exposes a tool outside the configured tool policy.", { component: name });
29
+ if (allowed?.some((tool) => !denied.includes(tool) && !names.includes(tool)))
30
+ fail("MCP_TOOLS_UNAVAILABLE", "compatibility", "A required allowed tool is missing from the native catalog.", { component: name });
31
+ if (names.length === 0 &&
32
+ server.toolsError === undefined &&
33
+ server.serverInfo === undefined)
34
+ fail("MCP_READINESS_UNVERIFIED", "compatibility", "This Codex version does not provide enough metadata to distinguish an empty catalog from a failed startup.", { component: name });
35
+ }
36
+ export async function verifyMcpReadiness(client, definitions) {
37
+ for (const [name, raw] of Object.entries(definitions)) {
38
+ const expected = record(raw, name);
39
+ if (expected.enabled === false)
40
+ continue;
41
+ const catalog = await client.request("mcpServerStatus/list", { serverName: name, detail: "toolsAndAuthOnly" }, 30_000);
42
+ verifyMcpCatalog(name, catalog, expected);
43
+ }
44
+ }
45
+ //# sourceMappingURL=mcp-readiness.js.map
@@ -0,0 +1,109 @@
1
+ import path from "node:path";
2
+ import { fail } from "../diagnostics.js";
3
+ import { isRecord } from "../validation.js";
4
+ import { packageFingerprint } from "../adapters/plugins.js";
5
+ function packageRoot(resource, relative) {
6
+ if (!path.isAbsolute(resource))
7
+ return undefined;
8
+ let root = resource;
9
+ for (const _part of relative.split(path.sep))
10
+ root = path.dirname(root);
11
+ return path.resolve(root, relative) === path.resolve(resource)
12
+ ? root
13
+ : undefined;
14
+ }
15
+ export async function bindNativePlugin(spec, inspection) {
16
+ // Pure MCP bundles and the materialized primary agent are fully consumed by
17
+ // Agency's final connection config / rendered-instruction adapter.
18
+ if (spec.skills.length === 0 && spec.hooks.every((hook) => hook.count === 0))
19
+ return undefined;
20
+ const expectedHash = await packageFingerprint(spec.root);
21
+ for (const native of inspection.plugins.filter((plugin) => plugin.name === spec.name && plugin.installed)) {
22
+ const roots = new Set();
23
+ for (const skill of spec.skills) {
24
+ const relative = path.relative(spec.root, skill);
25
+ for (const loaded of inspection.skills.filter((candidate) => candidate.pluginId === native.id)) {
26
+ const root = packageRoot(loaded.path, relative);
27
+ if (root)
28
+ roots.add(root);
29
+ }
30
+ }
31
+ for (const hook of spec.hooks) {
32
+ const relative = path.relative(spec.root, hook.file);
33
+ for (const loaded of inspection.hooks.filter((candidate) => candidate.pluginId === native.id)) {
34
+ const root = packageRoot(loaded.sourcePath, relative);
35
+ if (root)
36
+ roots.add(root);
37
+ }
38
+ }
39
+ for (const root of roots) {
40
+ if ((await packageFingerprint(root)) === expectedHash)
41
+ return { spec, native, runtimeRoot: root };
42
+ }
43
+ }
44
+ fail("PLUGIN_NOT_READY", "compatibility", "No equivalent installed native plugin was found. Cold plugin installation currently writes global Codex configuration and is not performed automatically. Prepare a compatible plugin explicitly in Codex or exclude it explicitly through Agency.", { component: spec.name, path: spec.root });
45
+ }
46
+ export function pluginOverrides(bindings, inspection, profileMode) {
47
+ const plugins = Object.create(null);
48
+ const selected = new Set(bindings.map((binding) => binding.native.id));
49
+ if (profileMode === "profile-only") {
50
+ plugins._default = { enabled: false };
51
+ const ids = new Set([
52
+ ...inspection.plugins.map((plugin) => plugin.id),
53
+ ...Object.keys(isRecord(inspection.config.plugins) ? inspection.config.plugins : {}).filter((key) => key !== "_default"),
54
+ ]);
55
+ for (const id of ids) {
56
+ if (selected.has(id))
57
+ continue;
58
+ if (inspection.plugins.some((plugin) => plugin.id === id && plugin.managed && plugin.enabled))
59
+ fail("PROFILE_PLUGIN_ISOLATION_UNSUPPORTED", "compatibility", "A managed native plugin cannot be excluded to satisfy Agency profile-only isolation.", { component: id });
60
+ plugins[id] = { enabled: false };
61
+ }
62
+ }
63
+ for (const binding of bindings) {
64
+ const mcpServers = Object.fromEntries(binding.spec.mcpNames.map((name) => [name, { enabled: false }]));
65
+ plugins[binding.native.id] = {
66
+ enabled: true,
67
+ ...(Object.keys(mcpServers).length ? { mcp_servers: mcpServers } : {}),
68
+ };
69
+ }
70
+ return Object.keys(plugins).length ? { plugins } : {};
71
+ }
72
+ export function verifyNativePlugins(bindings, inspection, profileMode) {
73
+ const selected = new Set(bindings.map((binding) => binding.native.id));
74
+ if (profileMode === "profile-only" &&
75
+ inspection.plugins.some((plugin) => plugin.enabled && !selected.has(plugin.id)))
76
+ fail("PROFILE_PLUGIN_ISOLATION_UNSUPPORTED", "compatibility", "Codex still enables a plugin outside the Agency final selection.");
77
+ for (const { spec, native, runtimeRoot } of bindings) {
78
+ if (!inspection.plugins.some((plugin) => plugin.id === native.id && plugin.installed && plugin.enabled))
79
+ fail("PLUGIN_NOT_ENABLED", "compatibility", "The selected native plugin did not become enabled for this session.", { component: native.id });
80
+ for (const file of spec.skills) {
81
+ const expected = path.resolve(runtimeRoot, path.relative(spec.root, file));
82
+ if (!inspection.skills.some((skill) => skill.pluginId === native.id &&
83
+ skill.enabled &&
84
+ path.resolve(skill.path) === expected))
85
+ fail("PLUGIN_SKILL_MISSING", "compatibility", "A required plugin skill is absent or disabled in the native catalog.", { component: native.id, path: file });
86
+ }
87
+ for (const hook of spec.hooks) {
88
+ const expected = path.resolve(runtimeRoot, path.relative(spec.root, hook.file));
89
+ const hooks = inspection.hooks.filter((loaded) => loaded.pluginId === native.id &&
90
+ path.resolve(loaded.sourcePath) === expected);
91
+ if (hooks.length !== hook.count ||
92
+ hooks.some((loaded) => !loaded.enabled))
93
+ fail("PLUGIN_HOOK_MISSING", "compatibility", "A required hook was omitted or disabled by native discovery.", { component: native.id, path: hook.file });
94
+ if (hooks.some((loaded) => !["trusted", "managed"].includes(loaded.trustStatus)))
95
+ fail("PLUGIN_HOOK_UNTRUSTED", "compatibility", "A required hook needs the original Codex trust flow. The wrapper does not create trust records or bypass trust.", { component: native.id, path: hook.file });
96
+ }
97
+ const config = isRecord(inspection.config.plugins)
98
+ ? inspection.config.plugins[native.id]
99
+ : undefined;
100
+ for (const name of spec.mcpNames) {
101
+ const mcp = isRecord(config) && isRecord(config.mcp_servers)
102
+ ? config.mcp_servers[name]
103
+ : undefined;
104
+ if (!isRecord(mcp) || mcp.enabled !== false)
105
+ fail("PLUGIN_MCP_DUPLICATE", "compatibility", "A plugin MCP was not suppressed at its native policy source.", { component: native.id, field: name });
106
+ }
107
+ }
108
+ }
109
+ //# sourceMappingURL=native-plugins.js.map