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.
- package/dist/adapters/agent.js +73 -0
- package/dist/adapters/ambient.js +35 -0
- package/dist/adapters/instructions.js +102 -0
- package/dist/adapters/mcp.js +237 -0
- package/dist/adapters/permissions.js +42 -0
- package/dist/adapters/plugins.js +484 -0
- package/dist/agency/invocation.js +97 -0
- package/dist/agency/prepared-input.js +124 -0
- package/dist/bridge/execution.js +119 -0
- package/dist/bridge/main.js +26 -0
- package/dist/bridge/plan.js +268 -0
- package/dist/bridge/version.js +2 -0
- package/dist/cli-options.js +96 -0
- package/dist/cli.js +41 -0
- package/dist/codex/agents.js +72 -0
- package/dist/codex/app-server.js +116 -0
- package/dist/codex/inspection.js +217 -0
- package/dist/codex/mcp-readiness.js +45 -0
- package/dist/codex/native-plugins.js +109 -0
- package/dist/codex/options.js +170 -0
- package/dist/codex/plugin-hooks.js +93 -0
- package/dist/codex/rpc.js +170 -0
- package/dist/codex/session-proxy.js +258 -0
- package/dist/codex/skill-policy.js +69 -0
- package/dist/codex/skill-views.js +104 -0
- package/dist/codex/toml.js +46 -0
- package/dist/commands/codex.js +118 -0
- package/dist/commands/doctor.js +58 -0
- package/dist/commands/passthrough.js +14 -0
- package/dist/diagnostics/plan.js +87 -0
- package/dist/diagnostics.js +39 -0
- package/dist/model.js +2 -0
- package/dist/runtime/ipc.js +316 -0
- package/dist/runtime/process.js +286 -0
- package/dist/runtime/resolve-command.js +38 -0
- package/dist/runtime/session.js +107 -0
- package/dist/validation.js +31 -0
- package/package.json +41 -4
- package/README.md +0 -3
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
import { readdir, realpath, stat } from "node:fs/promises";
|
|
2
|
+
import { createReadStream } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { parseDocument } from "yaml";
|
|
6
|
+
import { readJsonFile } from "../agency/prepared-input.js";
|
|
7
|
+
import { fail } from "../diagnostics.js";
|
|
8
|
+
import { readPluginAgent } from "./agent.js";
|
|
9
|
+
import { assertKnownFields, isRecord, nonEmptyString, record, stringArray, } from "../validation.js";
|
|
10
|
+
import { markdownDocument } from "./instructions.js";
|
|
11
|
+
import { readTextFile } from "../agency/prepared-input.js";
|
|
12
|
+
async function exists(file) {
|
|
13
|
+
try {
|
|
14
|
+
await stat(file);
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export async function componentPath(root, relative) {
|
|
22
|
+
if (path.isAbsolute(relative))
|
|
23
|
+
fail("PLUGIN_PATH_UNSUPPORTED", "compatibility", "Plugin component paths must be relative to the plugin root.", { path: root });
|
|
24
|
+
const resolvedRoot = await realpath(root);
|
|
25
|
+
const candidate = path.resolve(resolvedRoot, relative);
|
|
26
|
+
const actual = await realpath(candidate).catch(() => candidate);
|
|
27
|
+
const within = path.relative(resolvedRoot, actual);
|
|
28
|
+
if (within.startsWith(`..${path.sep}`) ||
|
|
29
|
+
within === ".." ||
|
|
30
|
+
path.isAbsolute(within))
|
|
31
|
+
fail("PLUGIN_PATH_UNSUPPORTED", "compatibility", "A plugin component escapes its package root.", { path: root });
|
|
32
|
+
return candidate;
|
|
33
|
+
}
|
|
34
|
+
function manifestPaths(value, fallback, component) {
|
|
35
|
+
if (value === undefined)
|
|
36
|
+
return [fallback];
|
|
37
|
+
return typeof value === "string" ? [value] : stringArray(value, component);
|
|
38
|
+
}
|
|
39
|
+
async function immediateFiles(root, extension) {
|
|
40
|
+
if (!(await exists(root)))
|
|
41
|
+
return [];
|
|
42
|
+
if ((await stat(root)).isFile())
|
|
43
|
+
return root.endsWith(extension) ? [root] : [];
|
|
44
|
+
return (await readdir(root, { withFileTypes: true }))
|
|
45
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(extension))
|
|
46
|
+
.map((entry) => path.join(root, entry.name))
|
|
47
|
+
.sort();
|
|
48
|
+
}
|
|
49
|
+
async function skillFiles(directory) {
|
|
50
|
+
if (!(await exists(directory)))
|
|
51
|
+
return [];
|
|
52
|
+
if ((await stat(directory)).isFile())
|
|
53
|
+
return path.basename(directory) === "SKILL.md" ? [directory] : [];
|
|
54
|
+
if (await exists(path.join(directory, "SKILL.md")))
|
|
55
|
+
return [path.join(directory, "SKILL.md")];
|
|
56
|
+
const files = [];
|
|
57
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
58
|
+
if (entry.isDirectory() &&
|
|
59
|
+
(await exists(path.join(directory, entry.name, "SKILL.md"))))
|
|
60
|
+
files.push(path.join(directory, entry.name, "SKILL.md"));
|
|
61
|
+
}
|
|
62
|
+
return files.sort();
|
|
63
|
+
}
|
|
64
|
+
export function validateSharedHooks(value, file) {
|
|
65
|
+
const document = record(value, "plugin hooks");
|
|
66
|
+
assertKnownFields(document, ["hooks"], "plugin hooks");
|
|
67
|
+
const events = record(document.hooks, "plugin hook events");
|
|
68
|
+
let count = 0;
|
|
69
|
+
for (const [event, matchers] of Object.entries(events)) {
|
|
70
|
+
if (event !== "SessionStart")
|
|
71
|
+
fail("HOOK_EVENT_UNVERIFIED", "compatibility", "Only the verified shared SessionStart command-hook contract is enabled. Other events require an equivalence test before use.", { path: file, field: event });
|
|
72
|
+
if (!Array.isArray(matchers))
|
|
73
|
+
fail("HOOK_FORMAT_UNSUPPORTED", "compatibility", "The hook event must contain native matcher groups.", { path: file });
|
|
74
|
+
for (const raw of matchers) {
|
|
75
|
+
const group = record(raw, "hook matcher");
|
|
76
|
+
assertKnownFields(group, ["matcher", "hooks"], "hook matcher");
|
|
77
|
+
if (group.matcher !== undefined && typeof group.matcher !== "string")
|
|
78
|
+
fail("HOOK_FORMAT_UNSUPPORTED", "compatibility", "A hook matcher must be a string.", { path: file });
|
|
79
|
+
if (!Array.isArray(group.hooks))
|
|
80
|
+
fail("HOOK_FORMAT_UNSUPPORTED", "compatibility", "A matcher group must contain hook handlers.", { path: file });
|
|
81
|
+
for (const raw of group.hooks) {
|
|
82
|
+
const handler = record(raw, "hook handler");
|
|
83
|
+
assertKnownFields(handler, [
|
|
84
|
+
"type",
|
|
85
|
+
"command",
|
|
86
|
+
"command_windows",
|
|
87
|
+
"powershell",
|
|
88
|
+
"timeout",
|
|
89
|
+
"async",
|
|
90
|
+
"statusMessage",
|
|
91
|
+
"additionalContextLimit",
|
|
92
|
+
], "hook handler");
|
|
93
|
+
if (handler.type !== "command" ||
|
|
94
|
+
typeof handler.command !== "string" ||
|
|
95
|
+
!handler.command.trim())
|
|
96
|
+
fail("HOOK_HANDLER_UNSUPPORTED", "compatibility", "Only native command hooks have a verified mapping.", { path: file });
|
|
97
|
+
if (handler.async === true)
|
|
98
|
+
fail("HOOK_ASYNC_UNVERIFIED", "compatibility", "Asynchronous hook ordering has not been verified.", { path: file, field: "async" });
|
|
99
|
+
for (const field of ["powershell", "command_windows", "statusMessage"])
|
|
100
|
+
if (handler[field] !== undefined &&
|
|
101
|
+
typeof handler[field] !== "string")
|
|
102
|
+
fail("HOOK_FORMAT_UNSUPPORTED", "compatibility", "Platform commands and status messages must be strings.", { path: file, field });
|
|
103
|
+
if (handler.powershell !== undefined &&
|
|
104
|
+
handler.command_windows !== undefined &&
|
|
105
|
+
handler.powershell !== handler.command_windows)
|
|
106
|
+
fail("HOOK_FORMAT_UNSUPPORTED", "compatibility", "The hook declares conflicting Windows commands.", { path: file, field: "powershell" });
|
|
107
|
+
for (const field of ["timeout", "additionalContextLimit"])
|
|
108
|
+
if (handler[field] !== undefined &&
|
|
109
|
+
(!Number.isInteger(handler[field]) ||
|
|
110
|
+
Number(handler[field]) < (field === "timeout" ? 1 : 0)))
|
|
111
|
+
fail("HOOK_FORMAT_UNSUPPORTED", "compatibility", "Hook timing and context limits must be valid integers.", { path: file, field });
|
|
112
|
+
if (handler.async !== undefined && typeof handler.async !== "boolean")
|
|
113
|
+
fail("HOOK_FORMAT_UNSUPPORTED", "compatibility", "The async hook field must be a boolean.", { path: file, field: "async" });
|
|
114
|
+
count++;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return count;
|
|
119
|
+
}
|
|
120
|
+
export async function inspectPlugin(root, selectedAgent, agentToolsPolicy = "strict") {
|
|
121
|
+
root = await realpath(root).catch(() => root);
|
|
122
|
+
const manifests = [
|
|
123
|
+
".codex-plugin/plugin.json",
|
|
124
|
+
".claude-plugin/plugin.json",
|
|
125
|
+
".github/plugin/plugin.json",
|
|
126
|
+
"plugin.json",
|
|
127
|
+
];
|
|
128
|
+
const availableManifests = (await Promise.all(manifests.map(async (relative) => ({
|
|
129
|
+
file: path.join(root, relative),
|
|
130
|
+
exists: await exists(path.join(root, relative)),
|
|
131
|
+
})))).filter((candidate) => candidate.exists);
|
|
132
|
+
const manifestFile = availableManifests[0]?.file;
|
|
133
|
+
if (!manifestFile)
|
|
134
|
+
fail("PLUGIN_MANIFEST_MISSING", "compatibility", "The prepared plugin has no supported manifest.", { path: root });
|
|
135
|
+
const manifest = record(await readJsonFile(manifestFile), "plugin manifest");
|
|
136
|
+
assertKnownFields(manifest, [
|
|
137
|
+
"name",
|
|
138
|
+
"version",
|
|
139
|
+
"description",
|
|
140
|
+
"author",
|
|
141
|
+
"homepage",
|
|
142
|
+
"repository",
|
|
143
|
+
"license",
|
|
144
|
+
"keywords",
|
|
145
|
+
"interface",
|
|
146
|
+
"displayName",
|
|
147
|
+
"skills",
|
|
148
|
+
"hooks",
|
|
149
|
+
"mcpServers",
|
|
150
|
+
"agents",
|
|
151
|
+
"commands",
|
|
152
|
+
], "plugin manifest");
|
|
153
|
+
const name = nonEmptyString(manifest.name, "plugin name");
|
|
154
|
+
for (const candidate of availableManifests.slice(1)) {
|
|
155
|
+
const alternate = record(await readJsonFile(candidate.file), "plugin manifest");
|
|
156
|
+
for (const field of [
|
|
157
|
+
"name",
|
|
158
|
+
"skills",
|
|
159
|
+
"hooks",
|
|
160
|
+
"mcpServers",
|
|
161
|
+
"agents",
|
|
162
|
+
"commands",
|
|
163
|
+
]) {
|
|
164
|
+
if (JSON.stringify(alternate[field]) !== JSON.stringify(manifest[field]))
|
|
165
|
+
fail("PLUGIN_MANIFEST_CONFLICT", "compatibility", "Engine-specific manifests declare different execution components. A verified compatibility view is required.", { path: candidate.file, field });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const spec = {
|
|
169
|
+
root,
|
|
170
|
+
name,
|
|
171
|
+
skills: [],
|
|
172
|
+
hooks: [],
|
|
173
|
+
mcpNames: [],
|
|
174
|
+
requiredMcpNames: [],
|
|
175
|
+
manualInvocationSkills: [],
|
|
176
|
+
nonUserInvocableSkills: [],
|
|
177
|
+
mcpFiles: [],
|
|
178
|
+
mcpNeedsAdapter: false,
|
|
179
|
+
findings: [],
|
|
180
|
+
agents: [],
|
|
181
|
+
};
|
|
182
|
+
for (const relative of manifestPaths(manifest.skills, "skills", "plugin skills")) {
|
|
183
|
+
const directory = await componentPath(root, relative);
|
|
184
|
+
if (manifest.skills !== undefined && !(await exists(directory)))
|
|
185
|
+
fail("PLUGIN_COMPONENT_MISSING", "compatibility", "A declared skill path does not exist.", { path: directory });
|
|
186
|
+
spec.skills.push(...(await skillFiles(directory)));
|
|
187
|
+
}
|
|
188
|
+
for (const file of spec.skills) {
|
|
189
|
+
const { metadata } = markdownDocument(await readTextFile(file), file);
|
|
190
|
+
assertKnownFields(metadata, [
|
|
191
|
+
"name",
|
|
192
|
+
"description",
|
|
193
|
+
"license",
|
|
194
|
+
"compatibility",
|
|
195
|
+
"metadata",
|
|
196
|
+
"disable-model-invocation",
|
|
197
|
+
"user-invocable",
|
|
198
|
+
"argument-hint",
|
|
199
|
+
], "skill frontmatter", file);
|
|
200
|
+
nonEmptyString(metadata.name, "skill name");
|
|
201
|
+
nonEmptyString(metadata.description, "skill description");
|
|
202
|
+
if (metadata["user-invocable"] !== undefined &&
|
|
203
|
+
typeof metadata["user-invocable"] !== "boolean")
|
|
204
|
+
fail("SKILL_POLICY_INVALID", "configuration", "user-invocable must be a boolean.", { path: file, field: "user-invocable" }, 5);
|
|
205
|
+
if (metadata["user-invocable"] === false)
|
|
206
|
+
spec.nonUserInvocableSkills.push(file);
|
|
207
|
+
if (metadata["argument-hint"] !== undefined) {
|
|
208
|
+
if (typeof metadata["argument-hint"] !== "string")
|
|
209
|
+
fail("SKILL_METADATA_INVALID", "configuration", "argument-hint must be a string.", { path: file, field: "argument-hint" }, 5);
|
|
210
|
+
spec.findings.push({
|
|
211
|
+
component: `skill:${spec.name}:${String(metadata.name)}`,
|
|
212
|
+
state: "adapted",
|
|
213
|
+
path: file,
|
|
214
|
+
field: "argument-hint",
|
|
215
|
+
reason: "The argument hint is retained as display metadata. It adds no execution or permission requirement; the native CLI may omit the Copilot hint display.",
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
if (metadata["disable-model-invocation"] !== undefined &&
|
|
219
|
+
typeof metadata["disable-model-invocation"] !== "boolean")
|
|
220
|
+
fail("SKILL_POLICY_INVALID", "configuration", "disable-model-invocation must be a boolean.", { path: file }, 5);
|
|
221
|
+
let implicitAllowed;
|
|
222
|
+
if (metadata.compatibility !== undefined && metadata.compatibility !== "")
|
|
223
|
+
fail("SKILL_COMPATIBILITY_UNVERIFIED", "compatibility", "The skill declares runtime compatibility requirements without a verified adapter.", { path: file, field: "compatibility" });
|
|
224
|
+
const nativeMetadata = path.join(path.dirname(file), "agents", "openai.yaml");
|
|
225
|
+
if (await exists(nativeMetadata)) {
|
|
226
|
+
const document = parseDocument(await readTextFile(nativeMetadata), {
|
|
227
|
+
uniqueKeys: true,
|
|
228
|
+
});
|
|
229
|
+
if (document.errors.length)
|
|
230
|
+
fail("SKILL_METADATA_INVALID", "configuration", "Native skill metadata must be valid YAML.", { path: nativeMetadata }, 5);
|
|
231
|
+
let raw;
|
|
232
|
+
try {
|
|
233
|
+
raw = document.toJS({ maxAliasCount: 20 });
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
fail("SKILL_METADATA_INVALID", "configuration", "Native skill metadata contains excessive aliases.", { path: nativeMetadata }, 5);
|
|
237
|
+
}
|
|
238
|
+
const extra = record(raw, "native skill metadata");
|
|
239
|
+
assertKnownFields(extra, ["interface", "policy", "dependencies"], "native skill metadata");
|
|
240
|
+
if (extra.interface !== undefined)
|
|
241
|
+
assertKnownFields(record(extra.interface, "skill interface"), [
|
|
242
|
+
"display_name",
|
|
243
|
+
"short_description",
|
|
244
|
+
"icon_small",
|
|
245
|
+
"icon_large",
|
|
246
|
+
"brand_color",
|
|
247
|
+
"default_prompt",
|
|
248
|
+
], "skill interface");
|
|
249
|
+
if (extra.policy !== undefined)
|
|
250
|
+
assertKnownFields(record(extra.policy, "skill policy"), ["allow_implicit_invocation"], "skill policy");
|
|
251
|
+
if (isRecord(extra.policy)) {
|
|
252
|
+
implicitAllowed = extra.policy.allow_implicit_invocation;
|
|
253
|
+
if (implicitAllowed !== undefined &&
|
|
254
|
+
typeof implicitAllowed !== "boolean")
|
|
255
|
+
fail("SKILL_POLICY_INVALID", "configuration", "Native implicit invocation policy must be a boolean.", { path: nativeMetadata }, 5);
|
|
256
|
+
}
|
|
257
|
+
if (extra.dependencies !== undefined) {
|
|
258
|
+
const dependencies = record(extra.dependencies, "skill dependencies");
|
|
259
|
+
assertKnownFields(dependencies, ["tools"], "skill dependencies");
|
|
260
|
+
if (!Array.isArray(dependencies.tools))
|
|
261
|
+
fail("SKILL_DEPENDENCIES_UNVERIFIED", "compatibility", "Native skill dependencies must declare a tool list.", { path: nativeMetadata });
|
|
262
|
+
for (const raw of dependencies.tools) {
|
|
263
|
+
const dependency = record(raw, "skill dependency");
|
|
264
|
+
assertKnownFields(dependency, ["type", "value", "description", "transport", "url"], "skill dependency");
|
|
265
|
+
if (dependency.type !== "mcp" ||
|
|
266
|
+
typeof dependency.value !== "string" ||
|
|
267
|
+
dependency.transport !== undefined ||
|
|
268
|
+
dependency.url !== undefined)
|
|
269
|
+
fail("SKILL_DEPENDENCIES_UNVERIFIED", "compatibility", "Only references to already selected MCP dependencies have a verified mapping.", { path: nativeMetadata });
|
|
270
|
+
spec.requiredMcpNames.push(dependency.value);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (metadata["disable-model-invocation"] === true &&
|
|
275
|
+
implicitAllowed !== false)
|
|
276
|
+
spec.manualInvocationSkills.push(file);
|
|
277
|
+
}
|
|
278
|
+
const agentFiles = [];
|
|
279
|
+
for (const relative of manifestPaths(manifest.agents, "agents", "plugin agents"))
|
|
280
|
+
agentFiles.push(...(await immediateFiles(await componentPath(root, relative), ".md")));
|
|
281
|
+
agentFiles.push(...(await immediateFiles(path.join(root, ".github", "agents"), ".md")));
|
|
282
|
+
const agentId = selectedAgent?.startsWith(`${name}:`)
|
|
283
|
+
? selectedAgent.slice(name.length + 1)
|
|
284
|
+
: undefined;
|
|
285
|
+
if (agentId) {
|
|
286
|
+
const primary = agentFiles.find((file) => path.basename(file).replace(/(?:\.agent)?\.md$/, "") === agentId);
|
|
287
|
+
if (primary)
|
|
288
|
+
spec.primaryAgentPath = primary;
|
|
289
|
+
}
|
|
290
|
+
const unselectedAgents = agentFiles.filter((file) => file !== spec.primaryAgentPath);
|
|
291
|
+
if (unselectedAgents.length && agentToolsPolicy !== "inherit") {
|
|
292
|
+
const first = unselectedAgents[0];
|
|
293
|
+
const { metadata } = markdownDocument(await readTextFile(first), first);
|
|
294
|
+
const tools = metadata.tools === undefined
|
|
295
|
+
? undefined
|
|
296
|
+
: typeof metadata.tools === "string"
|
|
297
|
+
? metadata.tools.split(",").map((tool) => tool.trim())
|
|
298
|
+
: stringArray(metadata.tools, "plugin agent tools");
|
|
299
|
+
if (tools !== undefined && !(tools.length === 1 && tools[0] === "*"))
|
|
300
|
+
fail("PLUGIN_AGENT_TOOL_SCOPE_UNSUPPORTED", "compatibility", "The plugin agent declares a builtin-tool allow-list without an equivalent native policy. Use --agent-tools=inherit only to explicitly accept native Codex tool permissions instead.", { component: name, path: first, field: "tools" });
|
|
301
|
+
fail("PLUGIN_AGENT_UNSUPPORTED", "compatibility", "Plugin Markdown subagents are not registered as native Codex agents. The entire selected plugin is blocked.", { path: unselectedAgents[0] });
|
|
302
|
+
}
|
|
303
|
+
for (const file of unselectedAgents) {
|
|
304
|
+
const agent = await readPluginAgent(file);
|
|
305
|
+
if (spec.agents.some((existing) => existing.name === agent.name))
|
|
306
|
+
fail("AGENT_CONFLICT", "compatibility", "The plugin declares multiple agents with the same name.", { component: name, path: file });
|
|
307
|
+
spec.agents.push(agent);
|
|
308
|
+
spec.findings.push({
|
|
309
|
+
component: `agent:${name}:${agent.name}`,
|
|
310
|
+
state: "adapted",
|
|
311
|
+
path: file,
|
|
312
|
+
field: "tools",
|
|
313
|
+
reason: "Explicit inherited-tool mode retains the agent instructions and registers a native Codex role. The source tools allow-list is not applied; native Codex permissions, tool access and managed requirements remain in force.",
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
for (const relative of manifestPaths(manifest.commands, "commands", "plugin commands")) {
|
|
317
|
+
const files = await immediateFiles(await componentPath(root, relative), ".md");
|
|
318
|
+
if (files.length || manifest.commands !== undefined)
|
|
319
|
+
fail("PLUGIN_COMMAND_UNSUPPORTED", "compatibility", "Copilot/Claude command registration is not a verified Codex plugin contract.", { path: path.join(root, relative) });
|
|
320
|
+
}
|
|
321
|
+
const copilotHooks = path.join(root, ".github", "hooks");
|
|
322
|
+
if ((await immediateFiles(copilotHooks, ".json")).length)
|
|
323
|
+
fail("HOOK_FORMAT_UNSUPPORTED", "compatibility", "Copilot hook packages require a verified event and blocking-semantics adapter.", { path: copilotHooks });
|
|
324
|
+
const hookPaths = manifest.hooks === undefined
|
|
325
|
+
? ["hooks/hooks.json"]
|
|
326
|
+
: typeof manifest.hooks === "string" || Array.isArray(manifest.hooks)
|
|
327
|
+
? manifestPaths(manifest.hooks, "", "plugin hooks")
|
|
328
|
+
: [];
|
|
329
|
+
if (isRecord(manifest.hooks))
|
|
330
|
+
fail("HOOK_FORMAT_UNSUPPORTED", "compatibility", "Inline plugin hook manifests require a verified native loading path.", { path: manifestFile });
|
|
331
|
+
for (const relative of hookPaths) {
|
|
332
|
+
const file = await componentPath(root, relative);
|
|
333
|
+
if (await exists(file))
|
|
334
|
+
spec.hooks.push({
|
|
335
|
+
file,
|
|
336
|
+
count: validateSharedHooks(await readJsonFile(file), file),
|
|
337
|
+
});
|
|
338
|
+
else if (manifest.hooks !== undefined)
|
|
339
|
+
fail("PLUGIN_COMPONENT_MISSING", "compatibility", "A declared hook file does not exist.", { path: file });
|
|
340
|
+
}
|
|
341
|
+
const mcpFiles = manifest.mcpServers === undefined
|
|
342
|
+
? [".mcp.json"]
|
|
343
|
+
: typeof manifest.mcpServers === "string"
|
|
344
|
+
? [manifest.mcpServers]
|
|
345
|
+
: [];
|
|
346
|
+
if (manifest.mcpServers !== undefined &&
|
|
347
|
+
typeof manifest.mcpServers !== "string")
|
|
348
|
+
fail("PLUGIN_MCP_ADAPTER_REQUIRED", "compatibility", "Inline MCP manifests require a prepared native compatibility view.", { path: manifestFile });
|
|
349
|
+
for (const relative of mcpFiles) {
|
|
350
|
+
const file = await componentPath(root, relative);
|
|
351
|
+
if (!(await exists(file))) {
|
|
352
|
+
if (manifest.mcpServers !== undefined)
|
|
353
|
+
fail("PLUGIN_COMPONENT_MISSING", "compatibility", "A declared MCP file does not exist.", { path: file });
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
const document = record(await readJsonFile(file), "plugin MCP");
|
|
357
|
+
spec.mcpFiles.push(file);
|
|
358
|
+
const servers = isRecord(document.mcpServers)
|
|
359
|
+
? document.mcpServers
|
|
360
|
+
: document;
|
|
361
|
+
for (const [serverName, raw] of Object.entries(servers)) {
|
|
362
|
+
const server = record(raw, "plugin MCP server");
|
|
363
|
+
assertKnownFields(server, [
|
|
364
|
+
"command",
|
|
365
|
+
"args",
|
|
366
|
+
"cwd",
|
|
367
|
+
"env",
|
|
368
|
+
"env_vars",
|
|
369
|
+
"url",
|
|
370
|
+
"type",
|
|
371
|
+
"tools",
|
|
372
|
+
"timeout",
|
|
373
|
+
"headers",
|
|
374
|
+
"oauthClientId",
|
|
375
|
+
"oauthPublicClient",
|
|
376
|
+
"oauthGrantType",
|
|
377
|
+
], "plugin MCP server");
|
|
378
|
+
if ([
|
|
379
|
+
"type",
|
|
380
|
+
"tools",
|
|
381
|
+
"timeout",
|
|
382
|
+
"headers",
|
|
383
|
+
"oauthClientId",
|
|
384
|
+
"oauthPublicClient",
|
|
385
|
+
"oauthGrantType",
|
|
386
|
+
].some((field) => server[field] !== undefined) ||
|
|
387
|
+
JSON.stringify(server).includes("${"))
|
|
388
|
+
spec.mcpNeedsAdapter = true;
|
|
389
|
+
spec.mcpNames.push(serverName);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
const agencyFile = path.join(root, "agency.json");
|
|
393
|
+
if (await exists(agencyFile)) {
|
|
394
|
+
const metadata = record(await readJsonFile(agencyFile), "agency.json");
|
|
395
|
+
assertKnownFields(metadata, [
|
|
396
|
+
"mcpAuth",
|
|
397
|
+
"engines",
|
|
398
|
+
"platforms",
|
|
399
|
+
"category",
|
|
400
|
+
"authors",
|
|
401
|
+
"governance",
|
|
402
|
+
"persona",
|
|
403
|
+
], "agency.json", agencyFile);
|
|
404
|
+
if (metadata.persona !== undefined) {
|
|
405
|
+
if (typeof metadata.persona !== "string")
|
|
406
|
+
fail("PLUGIN_PERSONA_INVALID", "configuration", "Plugin persona must be literal instruction text, not an execution or permission object.", { path: agencyFile, field: "persona" }, 5);
|
|
407
|
+
if (metadata.persona.trim()) {
|
|
408
|
+
spec.persona = metadata.persona;
|
|
409
|
+
spec.findings.push({
|
|
410
|
+
component: `plugin:${name}`,
|
|
411
|
+
state: "adapted",
|
|
412
|
+
path: agencyFile,
|
|
413
|
+
field: "persona",
|
|
414
|
+
reason: "The declared plugin persona is retained as developer-level instruction text; it does not replace native system instructions or enforce tool permissions.",
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
if (metadata.engines !== undefined) {
|
|
419
|
+
const engines = stringArray(metadata.engines, "agency.json.engines");
|
|
420
|
+
if (engines.length &&
|
|
421
|
+
!engines.includes("*") &&
|
|
422
|
+
!engines.includes("codex") &&
|
|
423
|
+
!engines.includes("copilot"))
|
|
424
|
+
fail("PLUGIN_ENGINE_UNSUPPORTED", "compatibility", "The plugin excludes both Codex and Agency's Copilot preparation path. No compatible source-engine preparation was verified.", { path: agencyFile, field: "engines" });
|
|
425
|
+
if (engines.includes("copilot") &&
|
|
426
|
+
!engines.includes("codex") &&
|
|
427
|
+
!engines.includes("*"))
|
|
428
|
+
spec.findings.push({
|
|
429
|
+
component: `plugin:${name}`,
|
|
430
|
+
state: "adapted",
|
|
431
|
+
path: agencyFile,
|
|
432
|
+
field: "engines",
|
|
433
|
+
reason: "Agency selected this package through its Copilot preparation path. Codex compatibility is validated for every handed-off component instead of requiring an upstream Codex engine label.",
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
if (metadata.platforms !== undefined) {
|
|
437
|
+
const platforms = stringArray(metadata.platforms, "agency.json.platforms");
|
|
438
|
+
const current = process.platform === "win32"
|
|
439
|
+
? "windows"
|
|
440
|
+
: process.platform === "darwin"
|
|
441
|
+
? "macos"
|
|
442
|
+
: process.platform;
|
|
443
|
+
if (platforms.length &&
|
|
444
|
+
!platforms.includes("*") &&
|
|
445
|
+
!platforms.includes(current))
|
|
446
|
+
fail("PLUGIN_PLATFORM_UNSUPPORTED", "compatibility", "The plugin excludes the current platform.", { path: agencyFile, field: "platforms" });
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
spec.findings.push({
|
|
450
|
+
component: `plugin:${name}`,
|
|
451
|
+
state: "native",
|
|
452
|
+
path: root,
|
|
453
|
+
reason: "Every declared and default-discoverable component passed the supported format checks. Native runtime readiness is checked separately.",
|
|
454
|
+
});
|
|
455
|
+
return spec;
|
|
456
|
+
}
|
|
457
|
+
export async function packageFingerprint(root) {
|
|
458
|
+
const hash = createHash("sha256");
|
|
459
|
+
let files = 0;
|
|
460
|
+
const visit = async (directory) => {
|
|
461
|
+
for (const entry of (await readdir(directory, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name, "en"))) {
|
|
462
|
+
if (entry.name === ".git")
|
|
463
|
+
continue;
|
|
464
|
+
const file = path.join(directory, entry.name);
|
|
465
|
+
if (++files > 20_000)
|
|
466
|
+
fail("PLUGIN_SIZE_LIMIT", "compatibility", "The plugin exceeds the supported file count.", { path: root });
|
|
467
|
+
if (entry.isSymbolicLink())
|
|
468
|
+
fail("PLUGIN_PATH_UNSUPPORTED", "compatibility", "Native cache equivalence for symbolic plugin resources has not been verified.", { path: file });
|
|
469
|
+
if (entry.isDirectory())
|
|
470
|
+
await visit(file);
|
|
471
|
+
else if (entry.isFile()) {
|
|
472
|
+
hash.update(`${path.relative(root, file).split(path.sep).join("/")}\0`);
|
|
473
|
+
if (process.platform !== "win32")
|
|
474
|
+
hash.update(`${(await stat(file)).mode & 0o111}\0`);
|
|
475
|
+
for await (const chunk of createReadStream(file))
|
|
476
|
+
hash.update(chunk);
|
|
477
|
+
hash.update("\0");
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
await visit(root);
|
|
482
|
+
return hash.digest("hex");
|
|
483
|
+
}
|
|
484
|
+
//# sourceMappingURL=plugins.js.map
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { captureProcess } from "../runtime/process.js";
|
|
3
|
+
import { fail } from "../diagnostics.js";
|
|
4
|
+
import { isRecord } from "../validation.js";
|
|
5
|
+
const UNSUPPORTED = [
|
|
6
|
+
"--keep-alive",
|
|
7
|
+
"--session-manager",
|
|
8
|
+
"--generate-result",
|
|
9
|
+
"--result-type",
|
|
10
|
+
"--use-custom-provider",
|
|
11
|
+
"--resume",
|
|
12
|
+
"--continue",
|
|
13
|
+
"--ui-server",
|
|
14
|
+
"--server",
|
|
15
|
+
"--headless",
|
|
16
|
+
"--acp",
|
|
17
|
+
];
|
|
18
|
+
export function agencyEnvironment(env) {
|
|
19
|
+
const inherited = Object.fromEntries(Object.entries(env).filter(([key]) => !key.toUpperCase().startsWith("AGENCYX_")));
|
|
20
|
+
return {
|
|
21
|
+
...inherited,
|
|
22
|
+
AGENCY_FORCE_PLUGIN_DIR: "1",
|
|
23
|
+
AGENCY_SESSION_CATEGORIZATION_DISABLED: "1",
|
|
24
|
+
AGENCY_PROMPT_CATEGORIZATION_DISABLED: "1",
|
|
25
|
+
AGENCY_SESSION_ANALYSIS_DISABLED: "1",
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export function validateAgencyOptions(args) {
|
|
29
|
+
for (const arg of args) {
|
|
30
|
+
const flag = arg.split("=", 1)[0];
|
|
31
|
+
if (UNSUPPORTED.includes(flag))
|
|
32
|
+
fail("AGENCY_FEATURE_UNSUPPORTED", "arguments", "This option depends on Copilot session or execution behavior. Use agencyx agency for the original command.", { field: flag }, 2);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export function explicitProfileMode(args) {
|
|
36
|
+
const merged = args.some((arg) => arg === "--profile" || arg.startsWith("--profile="));
|
|
37
|
+
const isolated = args.some((arg) => arg === "--profile-only" || arg.startsWith("--profile-only="));
|
|
38
|
+
if (merged && isolated)
|
|
39
|
+
fail("ARGUMENT_INVALID", "arguments", "--profile and --profile-only are mutually exclusive.", {}, 2);
|
|
40
|
+
if (isolated)
|
|
41
|
+
return "profile-only";
|
|
42
|
+
if (merged || args.includes("--no-workspace-profile"))
|
|
43
|
+
return "merged";
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
export async function resolveProfileMode(agencyPath, args, cwd, env) {
|
|
47
|
+
const explicit = explicitProfileMode(args);
|
|
48
|
+
if (explicit)
|
|
49
|
+
return explicit;
|
|
50
|
+
// These releases apply persistent workspace selection only under this official switch.
|
|
51
|
+
// The actual profile contents and merge remain entirely inside Agency.
|
|
52
|
+
if (!["1", "true", "yes"].includes((env.AGENCY_USE_WORKSPACE_SETTINGS ?? "").toLowerCase()))
|
|
53
|
+
return "merged";
|
|
54
|
+
const result = await captureProcess(agencyPath, ["config", "profile-selection", "get", "--json"], { cwd, env });
|
|
55
|
+
if (result.code !== 0)
|
|
56
|
+
return "unknown";
|
|
57
|
+
let selection;
|
|
58
|
+
try {
|
|
59
|
+
selection = JSON.parse(result.stdout);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return "unknown";
|
|
63
|
+
}
|
|
64
|
+
if (selection === null)
|
|
65
|
+
return "merged";
|
|
66
|
+
if (!isRecord(selection) || typeof selection.name !== "string")
|
|
67
|
+
return "unknown";
|
|
68
|
+
const profiles = await captureProcess(agencyPath, ["config", "profiles", "--json"], { cwd, env });
|
|
69
|
+
if (profiles.code !== 0)
|
|
70
|
+
return "unknown";
|
|
71
|
+
let names;
|
|
72
|
+
try {
|
|
73
|
+
names = JSON.parse(profiles.stdout);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return "unknown";
|
|
77
|
+
}
|
|
78
|
+
if (!Array.isArray(names))
|
|
79
|
+
return "unknown";
|
|
80
|
+
// Agency falls back to its default when a stored selection no longer exists.
|
|
81
|
+
if (!names.some((item) => isRecord(item) && item.name === selection.name))
|
|
82
|
+
return "merged";
|
|
83
|
+
if (selection.mode === "profile-only")
|
|
84
|
+
return "profile-only";
|
|
85
|
+
if (selection.mode === "profile")
|
|
86
|
+
return "merged";
|
|
87
|
+
return "unknown";
|
|
88
|
+
}
|
|
89
|
+
export function agencyArguments(launcherPath, originalArgs) {
|
|
90
|
+
return [
|
|
91
|
+
"copilot",
|
|
92
|
+
"--copilot-path",
|
|
93
|
+
path.resolve(launcherPath),
|
|
94
|
+
...originalArgs,
|
|
95
|
+
];
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=invocation.js.map
|