@oh-my-pi/pi-coding-agent 17.2.9 → 17.2.11

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 (281) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/dist/{CHANGELOG-8yy8h3bc.md → CHANGELOG-d9xenpn9.md} +83 -0
  3. package/dist/cli.js +10208 -12596
  4. package/dist/types/capability/mcp.d.ts +11 -0
  5. package/dist/types/capability/skill.d.ts +6 -0
  6. package/dist/types/cli/args.d.ts +1 -0
  7. package/dist/types/cli/command-help.d.ts +3 -0
  8. package/dist/types/commands/share.d.ts +25 -0
  9. package/dist/types/commit/agentic/index.d.ts +3 -1
  10. package/dist/types/commit/execute.d.ts +32 -0
  11. package/dist/types/commit/index.d.ts +2 -1
  12. package/dist/types/commit/pipeline.d.ts +7 -1
  13. package/dist/types/config/model-registry.d.ts +4 -0
  14. package/dist/types/config/model-resolver.d.ts +13 -0
  15. package/dist/types/config/settings-schema.d.ts +1 -34
  16. package/dist/types/discovery/agent-plugin-format.d.ts +143 -0
  17. package/dist/types/discovery/agent-plugins.d.ts +1 -0
  18. package/dist/types/discovery/contained-path.d.ts +34 -0
  19. package/dist/types/discovery/index.d.ts +1 -0
  20. package/dist/types/extensibility/custom-commands/types.d.ts +5 -5
  21. package/dist/types/extensibility/custom-tools/types.d.ts +3 -3
  22. package/dist/types/extensibility/extensions/loader.d.ts +9 -2
  23. package/dist/types/extensibility/extensions/types.d.ts +45 -8
  24. package/dist/types/extensibility/hooks/types.d.ts +5 -5
  25. package/dist/types/extensibility/shared-events.d.ts +3 -2
  26. package/dist/types/extensibility/skills.d.ts +5 -0
  27. package/dist/types/index.d.ts +2 -2
  28. package/dist/types/main.d.ts +1 -1
  29. package/dist/types/mcp/transports/header-policy.d.ts +45 -0
  30. package/dist/types/mcp/types.d.ts +15 -0
  31. package/dist/types/modes/acp/acp-agent.d.ts +1 -1
  32. package/dist/types/modes/acp/acp-client-bridge.d.ts +1 -1
  33. package/dist/types/modes/acp/acp-event-mapper.d.ts +1 -1
  34. package/dist/types/modes/acp/acp-mode.d.ts +1 -1
  35. package/dist/types/modes/components/agent-hub-projection.d.ts +38 -0
  36. package/dist/types/modes/components/agent-hub-renderer.d.ts +41 -0
  37. package/dist/types/modes/components/agent-hub.d.ts +14 -5
  38. package/dist/types/modes/components/agent-transcript-viewer.d.ts +2 -0
  39. package/dist/types/modes/components/chat-transcript-builder.d.ts +2 -0
  40. package/dist/types/modes/components/custom-editor.d.ts +1 -2
  41. package/dist/types/modes/components/model-browser.d.ts +9 -1
  42. package/dist/types/modes/components/status-line/types.d.ts +2 -0
  43. package/dist/types/modes/components/tool-execution.d.ts +2 -0
  44. package/dist/types/modes/interactive-mode.d.ts +8 -1
  45. package/dist/types/modes/utils/transcript-render-helpers.d.ts +3 -2
  46. package/dist/types/registry/agent-registry.d.ts +47 -0
  47. package/dist/types/registry/persisted-agents.d.ts +3 -1
  48. package/dist/types/secrets/index.d.ts +23 -1
  49. package/dist/types/session/agent-session-events.d.ts +2 -2
  50. package/dist/types/session/agent-session.d.ts +4 -2
  51. package/dist/types/session/messages.d.ts +1 -0
  52. package/dist/types/session/prewalk.d.ts +3 -1
  53. package/dist/types/session/session-entries.d.ts +10 -0
  54. package/dist/types/session/session-loader.d.ts +11 -1
  55. package/dist/types/session/session-manager.d.ts +9 -1
  56. package/dist/types/session/session-tools.d.ts +18 -1
  57. package/dist/types/session/turn-recovery.d.ts +7 -2
  58. package/dist/types/slash-commands/acp-builtins.d.ts +1 -1
  59. package/dist/types/slash-commands/available-commands.d.ts +1 -1
  60. package/dist/types/task/executor.d.ts +6 -0
  61. package/dist/types/task/index.d.ts +4 -3
  62. package/dist/types/task/read-only-policy.d.ts +3 -0
  63. package/dist/types/task/structured-subagent.d.ts +2 -0
  64. package/dist/types/task/types.d.ts +4 -0
  65. package/dist/types/tools/bash.d.ts +3 -3
  66. package/dist/types/tools/browser/launch.d.ts +2 -1
  67. package/dist/types/tools/browser/tab-worker.d.ts +2 -2
  68. package/dist/types/tools/path-utils.d.ts +1 -0
  69. package/dist/types/tools/read.d.ts +1 -5
  70. package/dist/types/tools/run-scope.d.ts +27 -1
  71. package/dist/types/tools/shell-tokenize.d.ts +16 -0
  72. package/dist/types/tools/terminal-output.d.ts +1 -1
  73. package/dist/types/utils/turndown.d.ts +1 -1
  74. package/dist/types/vibe/runtime.d.ts +2 -3
  75. package/dist/types/web/scrapers/readthedocs.d.ts +0 -3
  76. package/dist/types/web/search/providers/browser-headers.d.ts +2 -3
  77. package/examples/custom-tools/README.md +1 -1
  78. package/examples/extensions/README.md +1 -1
  79. package/examples/extensions/api-demo.ts +5 -6
  80. package/examples/extensions/chalk-logger.ts +1 -1
  81. package/examples/extensions/hello.ts +2 -2
  82. package/examples/extensions/reload-runtime.ts +1 -1
  83. package/examples/extensions/tools.ts +2 -2
  84. package/examples/extensions/with-deps/index.ts +1 -1
  85. package/examples/sdk/06-extensions.ts +1 -1
  86. package/package.json +15 -31
  87. package/scripts/bundle-dist.ts +1 -11
  88. package/scripts/legacy-pi-virtual-module.ts +4 -1
  89. package/src/advisor/runtime.ts +54 -0
  90. package/src/capability/mcp.ts +11 -0
  91. package/src/capability/skill.ts +6 -0
  92. package/src/cli/agents-cli.ts +1 -1
  93. package/src/cli/args.ts +24 -2
  94. package/src/cli/auth-broker-cli.ts +1 -1
  95. package/src/cli/auth-gateway-cli.ts +1 -1
  96. package/src/cli/bench-cli.ts +1 -1
  97. package/src/cli/claude-trace-cli.ts +1 -1
  98. package/src/cli/command-help.ts +4 -0
  99. package/src/cli/config-cli.ts +1 -1
  100. package/src/cli/dry-balance-cli.ts +1 -1
  101. package/src/cli/file-processor.ts +1 -1
  102. package/src/cli/flag-tables.ts +4 -0
  103. package/src/cli/gallery-cli.ts +1 -1
  104. package/src/cli/grep-cli.ts +1 -1
  105. package/src/cli/grievances-cli.ts +1 -1
  106. package/src/cli/help-extra.ts +1 -1
  107. package/src/cli/models-cli.ts +1 -1
  108. package/src/cli/plugin-cli.ts +1 -1
  109. package/src/cli/read-cli.ts +1 -1
  110. package/src/cli/setup-cli.ts +1 -1
  111. package/src/cli/shell-cli.ts +1 -1
  112. package/src/cli/ssh-cli.ts +1 -1
  113. package/src/cli/stats-cli.ts +1 -1
  114. package/src/cli/tiny-models-cli.ts +1 -1
  115. package/src/cli/ttsr-cli.ts +1 -1
  116. package/src/cli/update-cli.ts +1 -1
  117. package/src/cli/usage-cli.ts +1 -1
  118. package/src/cli/web-search-cli.ts +1 -1
  119. package/src/cli/worktree-cli.ts +1 -1
  120. package/src/cli-commands.ts +5 -0
  121. package/src/commands/commit.ts +16 -3
  122. package/src/commands/say.ts +1 -1
  123. package/src/commands/share.ts +71 -0
  124. package/src/commands/token.ts +1 -1
  125. package/src/commit/agentic/agent.ts +1 -1
  126. package/src/commit/agentic/index.ts +39 -21
  127. package/src/commit/cli.ts +1 -1
  128. package/src/commit/execute.ts +56 -0
  129. package/src/commit/index.ts +2 -1
  130. package/src/commit/pipeline.ts +20 -7
  131. package/src/config/model-discovery.ts +1 -1
  132. package/src/config/model-registry.ts +21 -1
  133. package/src/config/model-resolver.ts +54 -7
  134. package/src/config/settings-schema.ts +2 -33
  135. package/src/config/settings.ts +46 -0
  136. package/src/discovery/agent-plugin-format.ts +551 -0
  137. package/src/discovery/agent-plugins.ts +341 -0
  138. package/src/discovery/agents.ts +4 -2
  139. package/src/discovery/builtin-rules/ts-no-inline-cast-access.md +5 -4
  140. package/src/discovery/builtin-rules/ts-no-tiny-functions.md +1 -1
  141. package/src/discovery/claude-plugins.ts +21 -5
  142. package/src/discovery/contained-path.ts +78 -0
  143. package/src/discovery/helpers.ts +23 -9
  144. package/src/discovery/index.ts +1 -0
  145. package/src/discovery/omp-plugins.ts +20 -7
  146. package/src/eval/py/runner.py +38 -1
  147. package/src/exec/non-interactive-env.ts +1 -0
  148. package/src/extensibility/custom-commands/loader.ts +4 -4
  149. package/src/extensibility/custom-commands/types.ts +5 -5
  150. package/src/extensibility/custom-tools/loader.ts +4 -4
  151. package/src/extensibility/custom-tools/types.ts +3 -3
  152. package/src/extensibility/extensions/loader.ts +85 -20
  153. package/src/extensibility/extensions/runner.ts +6 -0
  154. package/src/extensibility/extensions/types.ts +51 -8
  155. package/src/extensibility/hooks/loader.ts +4 -5
  156. package/src/extensibility/hooks/types.ts +5 -5
  157. package/src/extensibility/plugins/legacy-pi-compat.ts +53 -0
  158. package/src/extensibility/plugins/marketplace/manager.ts +2 -1
  159. package/src/extensibility/shared-events.ts +3 -2
  160. package/src/extensibility/skills.ts +9 -0
  161. package/src/index.ts +2 -2
  162. package/src/internal-urls/memory-protocol.ts +5 -1
  163. package/src/internal-urls/skill-protocol.ts +14 -0
  164. package/src/internal-urls/vault-protocol.ts +2 -2
  165. package/src/launch/client.ts +11 -1
  166. package/src/launch/protocol.ts +7 -2
  167. package/src/launch/terminal-output.ts +1 -1
  168. package/src/main.ts +88 -23
  169. package/src/markit/converters/docx.ts +1 -1
  170. package/src/markit/converters/epub.ts +1 -1
  171. package/src/markit/converters/pptx.ts +1 -1
  172. package/src/markit/converters/xlsx.ts +1 -1
  173. package/src/mcp/config.ts +3 -0
  174. package/src/mcp/manager.ts +12 -9
  175. package/src/mcp/transports/header-policy.ts +95 -0
  176. package/src/mcp/transports/http.ts +35 -52
  177. package/src/mcp/transports/sse.ts +20 -27
  178. package/src/mcp/types.ts +15 -0
  179. package/src/modes/acp/acp-agent.ts +20 -9
  180. package/src/modes/acp/acp-client-bridge.ts +1 -1
  181. package/src/modes/acp/acp-event-mapper.ts +1 -1
  182. package/src/modes/acp/acp-mode.ts +1 -1
  183. package/src/modes/components/agent-dashboard.ts +2 -0
  184. package/src/modes/components/agent-hub-projection.ts +248 -0
  185. package/src/modes/components/agent-hub-renderer.ts +194 -0
  186. package/src/modes/components/agent-hub.ts +674 -207
  187. package/src/modes/components/agent-transcript-viewer.ts +3 -0
  188. package/src/modes/components/assistant-message.ts +1 -1
  189. package/src/modes/components/chat-transcript-builder.ts +3 -0
  190. package/src/modes/components/custom-editor.ts +0 -9
  191. package/src/modes/components/extensions/inspector-panel.ts +11 -6
  192. package/src/modes/components/footer.ts +1 -3
  193. package/src/modes/components/model-browser.ts +11 -3
  194. package/src/modes/components/model-hub.ts +4 -1
  195. package/src/modes/components/status-line/component.ts +1 -0
  196. package/src/modes/components/status-line/segments.ts +10 -7
  197. package/src/modes/components/status-line/types.ts +2 -0
  198. package/src/modes/components/tool-execution.ts +13 -16
  199. package/src/modes/components/tree-selector.ts +62 -3
  200. package/src/modes/controllers/command-controller.ts +4 -1
  201. package/src/modes/controllers/event-controller.ts +44 -20
  202. package/src/modes/controllers/extension-ui-controller.ts +2 -2
  203. package/src/modes/controllers/input-controller.ts +20 -2
  204. package/src/modes/controllers/mcp-command-controller.ts +81 -20
  205. package/src/modes/controllers/selector-controller.ts +41 -49
  206. package/src/modes/controllers/streaming-reveal.ts +1 -1
  207. package/src/modes/interactive-mode.ts +13 -2
  208. package/src/modes/rpc/rpc-mode.ts +2 -2
  209. package/src/modes/runtime-init.ts +1 -1
  210. package/src/modes/theme/theme.ts +2 -2
  211. package/src/modes/utils/transcript-render-helpers.ts +4 -2
  212. package/src/modes/utils/ui-helpers.ts +1 -0
  213. package/src/prompts/tools/computer.md +1 -1
  214. package/src/registry/agent-lifecycle.ts +14 -7
  215. package/src/registry/agent-registry.ts +65 -2
  216. package/src/registry/persisted-agents.ts +341 -16
  217. package/src/sdk.ts +6 -49
  218. package/src/secrets/index.ts +52 -1
  219. package/src/session/agent-session-events.ts +2 -2
  220. package/src/session/agent-session.ts +54 -16
  221. package/src/session/messages.ts +1 -0
  222. package/src/session/prewalk.ts +57 -17
  223. package/src/session/session-context.ts +3 -5
  224. package/src/session/session-entries.ts +10 -0
  225. package/src/session/session-handoff.ts +5 -1
  226. package/src/session/session-listing.ts +1 -1
  227. package/src/session/session-loader.ts +74 -8
  228. package/src/session/session-manager.ts +22 -2
  229. package/src/session/session-paths.ts +57 -9
  230. package/src/session/session-tools.ts +65 -4
  231. package/src/session/todo-tracker.ts +11 -1
  232. package/src/session/turn-recovery.ts +96 -74
  233. package/src/slash-commands/acp-builtins.ts +1 -1
  234. package/src/slash-commands/available-commands.ts +1 -1
  235. package/src/slash-commands/builtin-registry.ts +12 -8
  236. package/src/task/executor.ts +70 -20
  237. package/src/task/index.ts +30 -34
  238. package/src/task/isolation-runner.ts +24 -11
  239. package/src/task/persisted-revive.ts +12 -5
  240. package/src/task/read-only-policy.ts +27 -0
  241. package/src/task/structured-subagent.ts +14 -4
  242. package/src/task/types.ts +4 -0
  243. package/src/tools/auto-generated-guard.ts +1 -1
  244. package/src/tools/bash-interactive.ts +4 -4
  245. package/src/tools/bash-skill-urls.ts +15 -0
  246. package/src/tools/bash.ts +16 -17
  247. package/src/tools/browser/cmux/cmux-tab.ts +66 -18
  248. package/src/tools/browser/launch.ts +41 -12
  249. package/src/tools/browser/readable.ts +9 -9
  250. package/src/tools/browser/tab-supervisor.ts +12 -3
  251. package/src/tools/browser/tab-worker-entry.ts +1 -1
  252. package/src/tools/browser/tab-worker.ts +101 -12
  253. package/src/tools/debug.ts +1 -1
  254. package/src/tools/fetch.ts +1 -1
  255. package/src/tools/jtd-to-json-schema.ts +123 -21
  256. package/src/tools/path-utils.ts +9 -2
  257. package/src/tools/read.ts +21 -9
  258. package/src/tools/run-scope.ts +290 -4
  259. package/src/tools/shell-tokenize.ts +98 -0
  260. package/src/tools/terminal-output.ts +1 -1
  261. package/src/tools/todo.ts +1 -1
  262. package/src/utils/clipboard.ts +10 -2
  263. package/src/utils/external-editor.ts +4 -2
  264. package/src/utils/jj.ts +1 -1
  265. package/src/utils/turndown.ts +1 -2
  266. package/src/vibe/runtime.ts +5 -5
  267. package/src/web/scrapers/arxiv.ts +1 -1
  268. package/src/web/scrapers/go-pkg.ts +1 -1
  269. package/src/web/scrapers/iacr.ts +1 -1
  270. package/src/web/scrapers/readthedocs.ts +2 -1
  271. package/src/web/scrapers/twitter.ts +2 -2
  272. package/src/web/scrapers/types.ts +1 -1
  273. package/src/web/scrapers/wikipedia.ts +1 -1
  274. package/src/web/search/providers/browser-headers.ts +12 -39
  275. package/src/web/search/providers/codex.ts +3 -26
  276. package/src/web/search/providers/ecosia.ts +1 -1
  277. package/src/web/search/providers/exa.ts +1 -1
  278. package/src/web/search/providers/google.ts +1 -1
  279. package/src/web/search/providers/mojeek.ts +1 -1
  280. package/src/web/search/providers/startpage.ts +1 -1
  281. package/src/markit/converters/mammoth.d.ts +0 -24
@@ -0,0 +1,551 @@
1
+ /**
2
+ * Agent Plugins 1.0.0 format support (https://agent-plugins.org).
3
+ *
4
+ * Pure validation and transformation logic for the portable plugin package
5
+ * format: the closed `plugin.json` manifest (spec §5), the closed `mcp.json`
6
+ * MCP configuration (spec §7.2), `${PLUGIN_ROOT}`/`${PLUGIN_DATA}` placeholder
7
+ * expansion (spec §9.2), and package-boundary containment (spec §4.1).
8
+ *
9
+ * The discovery provider in `./agent-plugins.ts` wires this into the
10
+ * capability registry; `./claude-plugins.ts` and `./omp-plugins.ts` consult
11
+ * {@link classifyAgentPluginRoot} so roots governed by the standard are not
12
+ * double-loaded through legacy conventions.
13
+ */
14
+ import * as path from "node:path";
15
+ import { isRecord } from "@oh-my-pi/pi-utils";
16
+ import { readFile } from "../capability/fs";
17
+ import { isContainedResolved, realpathIfExists, resolveContainedPath } from "./contained-path";
18
+ import { registerPluginCacheInvalidator } from "./helpers";
19
+
20
+ /** Canonical `plugin.json` `$schema` identifier for Agent Plugins 1.0.0 (spec §5.2). */
21
+ export const AGENT_PLUGIN_MANIFEST_SCHEMA = "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json";
22
+ /** Canonical `mcp.json` `$schema` identifier for Agent Plugins 1.0.0 (spec §7.2.1). */
23
+ export const AGENT_PLUGIN_MCP_SCHEMA = "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json";
24
+ /** Prefix shared by every Agent Plugins canonical schema identifier, across spec versions. */
25
+ const AGENT_PLUGIN_SCHEMA_PREFIX = "https://agent-plugins.org/schemas/";
26
+
27
+ const MANIFEST_FIELDS: Record<string, true> = {
28
+ $schema: true,
29
+ name: true,
30
+ version: true,
31
+ description: true,
32
+ author: true,
33
+ homepage: true,
34
+ repository: true,
35
+ license: true,
36
+ keywords: true,
37
+ extensions: true,
38
+ };
39
+ const AUTHOR_FIELDS: Record<string, true> = { name: true, email: true, url: true };
40
+ const STDIO_FIELDS: Record<string, true> = { type: true, command: true, args: true, env: true, cwd: true };
41
+ const REMOTE_FIELDS: Record<string, true> = { type: true, url: true, headers: true };
42
+ /** Reserved subprocess environment names the client always supplies itself (spec §9.2). */
43
+ const RESERVED_ENV_NAMES: Record<string, true> = { PLUGIN_ROOT: true, PLUGIN_DATA: true };
44
+ /** RFC 9110 token characters, the only bytes legal in an HTTP field name. */
45
+ const HEADER_NAME_RE = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
46
+ // Concatenation avoids the noTemplateCurlyInString lint on literal placeholder names.
47
+ const PLUGIN_ROOT_VAR = "$" + "{PLUGIN_ROOT}";
48
+ const PLUGIN_DATA_VAR = "$" + "{PLUGIN_DATA}";
49
+
50
+ /** Validated portable manifest fields from a plugin's root `plugin.json` (spec §5). */
51
+ export interface AgentPluginManifest {
52
+ name: string;
53
+ version?: string;
54
+ description?: string;
55
+ author?: { name?: string; email?: string; url?: string };
56
+ homepage?: string;
57
+ repository?: string;
58
+ license?: string;
59
+ keywords?: string[];
60
+ /**
61
+ * Client-owned data keyed by reverse-domain namespace. Member values are
62
+ * opaque: this client implements no namespace, and §8.1/§11.1 require
63
+ * ignoring unimplemented namespaces without validating their values.
64
+ */
65
+ extensions?: Record<string, unknown>;
66
+ }
67
+
68
+ /**
69
+ * Outcome of parsing a candidate `plugin.json`:
70
+ * - `none` — the document does not target Agent Plugins (no recognized `$schema`);
71
+ * legacy plugin conventions may still apply to the directory.
72
+ * - `valid` — a conformant manifest; `warnings` carries the non-fatal violations
73
+ * the spec requires reporting (unknown top-level fields, non-object `extensions`).
74
+ * - `invalid` — the document targets Agent Plugins but violates the closed schema
75
+ * fatally; the plugin must be rejected and none of its components loaded (spec §5.2).
76
+ */
77
+ export type AgentPluginManifestResult =
78
+ | { status: "none" }
79
+ | { status: "valid"; manifest: AgentPluginManifest; warnings: string[] }
80
+ | { status: "invalid"; reason: string };
81
+
82
+ /** Enforce the plugin `name` constraints from spec §5.5. */
83
+ export function isValidAgentPluginName(name: string): boolean {
84
+ if (name.length < 1 || name.length > 64) return false;
85
+ if (!/^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/.test(name)) return false;
86
+ return !name.includes("--") && !name.includes("..");
87
+ }
88
+
89
+ /** The closed frontmatter field set from the skills-ref reference validator. */
90
+ const SKILL_FIELDS: Record<string, true> = {
91
+ name: true,
92
+ description: true,
93
+ license: true,
94
+ "allowed-tools": true,
95
+ metadata: true,
96
+ compatibility: true,
97
+ };
98
+ /** Skill `name` characters: Unicode letters/digits (Python `str.isalnum`) and hyphens. */
99
+ const SKILL_NAME_CHARS_RE = /^[\p{L}\p{N}-]+$/u;
100
+
101
+ /** Validate the skill `name` per the skills-ref reference rules (NFKC-normalized). */
102
+ function validateSkillName(raw: unknown, dirName: string): string | null {
103
+ if (typeof raw !== "string" || raw.trim().length === 0) return `missing required "name"`;
104
+ const name = raw.trim().normalize("NFKC");
105
+ if (Array.from(name).length > 64) return `"name" exceeds 64 characters`;
106
+ if (name !== name.toLowerCase()) return `"name" must be lowercase`;
107
+ if (name.startsWith("-") || name.endsWith("-")) return `"name" cannot start or end with a hyphen`;
108
+ if (name.includes("--")) return `"name" cannot contain consecutive hyphens`;
109
+ if (!SKILL_NAME_CHARS_RE.test(name)) return `invalid "name" ${JSON.stringify(name)}`;
110
+ if (name !== dirName.normalize("NFKC")) {
111
+ return `"name" ${JSON.stringify(name)} does not match directory ${JSON.stringify(dirName)}`;
112
+ }
113
+ return null;
114
+ }
115
+
116
+ /**
117
+ * Validate `SKILL.md` frontmatter against the Agent Skills specification
118
+ * (https://agentskills.io/specification), the source of truth for skill
119
+ * validity under Agent Plugins §7.1, mirroring the official skills-ref
120
+ * reference validator: the frontmatter schema is CLOSED to its six fields and
121
+ * any unexpected key rejects the skill. Returns the first violation, or `null`
122
+ * when the skill conforms. Frontmatter keys must be raw (unnormalized).
123
+ */
124
+ export function validateAgentSkillFrontmatter(frontmatter: Record<string, unknown>, dirName: string): string | null {
125
+ for (const key in frontmatter) {
126
+ if (!SKILL_FIELDS[key]) return `unexpected frontmatter field "${key}"`;
127
+ }
128
+
129
+ const nameViolation = validateSkillName(frontmatter.name, dirName);
130
+ if (nameViolation !== null) return nameViolation;
131
+
132
+ const description = frontmatter.description;
133
+ if (typeof description !== "string" || description.trim().length === 0) {
134
+ return `missing required "description"`;
135
+ }
136
+ if (description.length > 1024) return `"description" exceeds 1024 characters`;
137
+
138
+ const license = frontmatter.license;
139
+ if (license !== undefined && typeof license !== "string") return `"license" must be a string`;
140
+
141
+ const compatibility = frontmatter.compatibility;
142
+ if (compatibility !== undefined) {
143
+ if (typeof compatibility !== "string") return `"compatibility" must be a string`;
144
+ if (compatibility.length > 500) return `"compatibility" exceeds 500 characters`;
145
+ }
146
+
147
+ const metadata = frontmatter.metadata;
148
+ if (metadata !== undefined) {
149
+ if (!isRecord(metadata)) return `"metadata" must be a map of string keys to string values`;
150
+ for (const key in metadata) {
151
+ if (typeof metadata[key] !== "string") return `"metadata.${key}" must be a string`;
152
+ }
153
+ }
154
+
155
+ const allowedTools = frontmatter["allowed-tools"];
156
+ if (allowedTools !== undefined && typeof allowedTools !== "string") {
157
+ return `"allowed-tools" must be a string`;
158
+ }
159
+
160
+ return null;
161
+ }
162
+
163
+ /**
164
+ * Parse and validate a root `plugin.json` document against the closed
165
+ * Agent Plugins 1.0.0 manifest schema (spec §5.2–§5.5, §8.1).
166
+ */
167
+ export function parseAgentPluginManifest(raw: string): AgentPluginManifestResult {
168
+ let parsed: unknown;
169
+ try {
170
+ parsed = JSON.parse(raw);
171
+ } catch {
172
+ // Unparseable documents carry no $schema, so the package cannot claim
173
+ // Agent Plugins conformance; leave it to legacy conventions.
174
+ return { status: "none" };
175
+ }
176
+ if (!isRecord(parsed)) return { status: "none" };
177
+
178
+ const schema = parsed.$schema;
179
+ if (typeof schema !== "string" || !schema.startsWith(AGENT_PLUGIN_SCHEMA_PREFIX)) return { status: "none" };
180
+ if (schema !== AGENT_PLUGIN_MANIFEST_SCHEMA) {
181
+ return { status: "invalid", reason: `unsupported Agent Plugins version ($schema: ${schema})` };
182
+ }
183
+
184
+ const warnings: string[] = [];
185
+ // Unknown top-level fields are schema violations, but spec §5.2 requires
186
+ // reporting and ignoring them rather than rejecting the plugin.
187
+ for (const key in parsed) {
188
+ if (!MANIFEST_FIELDS[key]) warnings.push(`Ignoring unknown plugin.json field "${key}"`);
189
+ }
190
+
191
+ const name = parsed.name;
192
+ if (typeof name !== "string" || !isValidAgentPluginName(name)) {
193
+ return { status: "invalid", reason: `invalid plugin name ${JSON.stringify(name)}` };
194
+ }
195
+
196
+ const manifest: AgentPluginManifest = { name };
197
+ for (const field of ["version", "description", "homepage", "repository", "license"] as const) {
198
+ const value = parsed[field];
199
+ if (value === undefined) continue;
200
+ if (typeof value !== "string") return { status: "invalid", reason: `"${field}" must be a string` };
201
+ manifest[field] = value;
202
+ }
203
+
204
+ const keywords = parsed.keywords;
205
+ if (keywords !== undefined) {
206
+ if (!Array.isArray(keywords) || keywords.some(entry => typeof entry !== "string")) {
207
+ return { status: "invalid", reason: `"keywords" must be an array of strings` };
208
+ }
209
+ manifest.keywords = keywords as string[];
210
+ }
211
+
212
+ const author = parsed.author;
213
+ if (author !== undefined) {
214
+ if (!isRecord(author)) return { status: "invalid", reason: `"author" must be an object` };
215
+ for (const key in author) {
216
+ if (!AUTHOR_FIELDS[key]) return { status: "invalid", reason: `unknown "author" field "${key}"` };
217
+ if (typeof author[key] !== "string") return { status: "invalid", reason: `"author.${key}" must be a string` };
218
+ }
219
+ manifest.author = author as AgentPluginManifest["author"];
220
+ }
221
+
222
+ const extensions = parsed.extensions;
223
+ if (extensions !== undefined) {
224
+ if (!isRecord(extensions)) {
225
+ // Non-object extensions is the second explicitly non-fatal violation (spec §8.1).
226
+ warnings.push(`Ignoring non-object "extensions" field`);
227
+ } else {
228
+ // §8.1/§11.1: unimplemented namespace entries are ignored WITHOUT
229
+ // validating the contents of their values — even non-object values
230
+ // (another client's convention) never reject the plugin.
231
+ manifest.extensions = extensions;
232
+ }
233
+ }
234
+
235
+ return { status: "valid", manifest, warnings };
236
+ }
237
+
238
+ /**
239
+ * Expand `${PLUGIN_ROOT}` and `${PLUGIN_DATA}` in a configuration value
240
+ * (spec §9.2). Single non-recursive pass; replacement text is never rescanned,
241
+ * and no other placeholder or environment-variable expansion is performed.
242
+ */
243
+ export function expandAgentPluginPlaceholders(value: string, pluginRoot: string, pluginData: string): string {
244
+ return value.replace(/\$\{PLUGIN_(ROOT|DATA)\}/g, (_match, which: string) =>
245
+ which === "ROOT" ? pluginRoot : pluginData,
246
+ );
247
+ }
248
+
249
+ /** A validated `mcp.json` server entry, resolved to launch-ready values. */
250
+ export interface AgentPluginMcpServer {
251
+ name: string;
252
+ transport: "stdio" | "http" | "sse";
253
+ /** Absolute path for `./`-relative commands; bare executable token otherwise. */
254
+ command?: string;
255
+ args?: string[];
256
+ /** Expanded configured env plus the client-supplied `PLUGIN_ROOT`/`PLUGIN_DATA`. */
257
+ env?: Record<string, string>;
258
+ /** Absolute working directory; defaults to the plugin root for stdio servers. */
259
+ cwd?: string;
260
+ url?: string;
261
+ headers?: Record<string, string>;
262
+ }
263
+
264
+ /**
265
+ * Outcome of parsing a plugin's `mcp.json`:
266
+ * - `disabled` — the top-level document is invalid, so MCP is disabled for the
267
+ * plugin while other component types keep loading (spec §7.2.2 rule 2).
268
+ * - `ok` — the document is valid; individually invalid server entries are
269
+ * skipped with a warning (spec §7.2.2 rule 3).
270
+ */
271
+ export type AgentPluginMcpResult =
272
+ | { status: "disabled"; reason: string }
273
+ | { status: "ok"; servers: AgentPluginMcpServer[]; warnings: string[] };
274
+
275
+ export interface AgentPluginMcpOptions {
276
+ /** Filesystem-resolved plugin root. */
277
+ pluginRoot: string;
278
+ /** Client-managed persistent data directory for this plugin (spec §9.1). */
279
+ pluginData: string;
280
+ }
281
+
282
+ /** Loopback check for plain-HTTP MCP endpoints (spec §7.2.1): `localhost` or a loopback IP literal. */
283
+ function isLoopbackHost(hostname: string): boolean {
284
+ if (hostname === "localhost") return true;
285
+ if (hostname === "[::1]" || hostname === "::1") return true;
286
+ const octets = /^(\d{1,3})\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.exec(hostname);
287
+ return octets !== null && octets[1] === "127";
288
+ }
289
+
290
+ function validateRemoteUrl(rawUrl: string): string | null {
291
+ let url: URL;
292
+ try {
293
+ url = new URL(rawUrl);
294
+ } catch {
295
+ return "url is not an absolute URL";
296
+ }
297
+ if (url.protocol !== "http:" && url.protocol !== "https:") return "url must use http or https";
298
+ if (url.username || url.password) return "url must not contain user information";
299
+ if (url.hash) return "url must not contain a fragment";
300
+ if (url.protocol === "http:" && !isLoopbackHost(url.hostname)) {
301
+ return "non-loopback endpoints must use https";
302
+ }
303
+ return null;
304
+ }
305
+
306
+ function validateHeaders(headers: Record<string, unknown>): string | null {
307
+ const seen = new Set<string>();
308
+ for (const name in headers) {
309
+ const value = headers[name];
310
+ if (!HEADER_NAME_RE.test(name)) return `invalid header name "${name}"`;
311
+ if (typeof value !== "string") return `header "${name}" value must be a string`;
312
+ if (/[\0\r\n]/.test(value)) return `header "${name}" value contains control characters`;
313
+ const lower = name.toLowerCase();
314
+ if (seen.has(lower)) return `duplicate header name "${name}"`;
315
+ seen.add(lower);
316
+ }
317
+ return null;
318
+ }
319
+
320
+ /** Per-entry validation result: a resolved server, or the reason the entry is skipped. */
321
+ type ServerEntryResult = { server: AgentPluginMcpServer } | { error: string };
322
+
323
+ async function parseStdioServer(
324
+ name: string,
325
+ cfg: Record<string, unknown>,
326
+ { pluginRoot, pluginData }: AgentPluginMcpOptions,
327
+ ): Promise<ServerEntryResult> {
328
+ for (const key in cfg) {
329
+ if (!STDIO_FIELDS[key]) return { error: `unknown field "${key}"` };
330
+ }
331
+
332
+ // `command` is one executable token: a bare name resolved by platform search
333
+ // rules, or a plugin-relative `./` path. No placeholder expansion (spec §7.2.1).
334
+ const command = cfg.command;
335
+ if (typeof command !== "string" || command.length === 0) return { error: `"command" must be a non-empty string` };
336
+ let resolvedCommand: string;
337
+ if (command.startsWith("./")) {
338
+ resolvedCommand = path.resolve(pluginRoot, command);
339
+ if (!(await isContainedResolved(pluginRoot, resolvedCommand))) {
340
+ return { error: `"command" resolves outside the plugin root` };
341
+ }
342
+ } else if (command.includes("/") || command.includes("\\")) {
343
+ return { error: `"command" must be a bare executable name or a plugin-relative ./ path` };
344
+ } else {
345
+ resolvedCommand = command;
346
+ }
347
+
348
+ const args = cfg.args;
349
+ if (args !== undefined && (!Array.isArray(args) || args.some(entry => typeof entry !== "string"))) {
350
+ return { error: `"args" must be an array of strings` };
351
+ }
352
+ const expandedArgs = (args as string[] | undefined)?.map(arg =>
353
+ expandAgentPluginPlaceholders(arg, pluginRoot, pluginData),
354
+ );
355
+
356
+ const env = cfg.env;
357
+ const expandedEnv: Record<string, string> = {};
358
+ if (env !== undefined) {
359
+ if (!isRecord(env)) return { error: `"env" must be an object of strings` };
360
+ for (const key in env) {
361
+ const value = env[key];
362
+ if (typeof value !== "string") return { error: `"env.${key}" must be a string` };
363
+ if (RESERVED_ENV_NAMES[key]) return { error: `"env" must not set reserved variable ${key}` };
364
+ expandedEnv[key] = expandAgentPluginPlaceholders(value, pluginRoot, pluginData);
365
+ }
366
+ }
367
+ // The client supplies the reserved variables itself, after the configured
368
+ // overlay, replacing any equivalently named ambient entries (spec §9.1).
369
+ expandedEnv.PLUGIN_ROOT = pluginRoot;
370
+ expandedEnv.PLUGIN_DATA = pluginData;
371
+
372
+ // `cwd` accepts exactly three forms (spec §7.2.1); anything else — or a
373
+ // post-resolution escape from its governing directory — invalidates the entry.
374
+ let resolvedCwd = pluginRoot;
375
+ const cwd = cfg.cwd;
376
+ if (cwd !== undefined) {
377
+ if (typeof cwd !== "string") return { error: `"cwd" must be a string` };
378
+ const dataRooted = cwd === PLUGIN_DATA_VAR || cwd.startsWith(`${PLUGIN_DATA_VAR}/`);
379
+ const rootRooted = cwd === PLUGIN_ROOT_VAR || cwd.startsWith(`${PLUGIN_ROOT_VAR}/`);
380
+ if (!dataRooted && !rootRooted && !cwd.startsWith("./")) {
381
+ return { error: `"cwd" must be plugin-relative or rooted at ${PLUGIN_ROOT_VAR} or ${PLUGIN_DATA_VAR}` };
382
+ }
383
+ const expanded = expandAgentPluginPlaceholders(cwd, pluginRoot, pluginData);
384
+ resolvedCwd = path.resolve(pluginRoot, expanded);
385
+ const base = dataRooted ? pluginData : pluginRoot;
386
+ if (!(await isContainedResolved(base, resolvedCwd))) {
387
+ return { error: `"cwd" resolves outside ${dataRooted ? "the plugin data directory" : "the plugin root"}` };
388
+ }
389
+ }
390
+
391
+ return {
392
+ server: {
393
+ name,
394
+ transport: "stdio",
395
+ command: resolvedCommand,
396
+ ...(expandedArgs !== undefined && { args: expandedArgs }),
397
+ env: expandedEnv,
398
+ cwd: resolvedCwd,
399
+ },
400
+ };
401
+ }
402
+
403
+ function parseRemoteServer(name: string, cfg: Record<string, unknown>, transport: "http" | "sse"): ServerEntryResult {
404
+ for (const key in cfg) {
405
+ if (!REMOTE_FIELDS[key]) return { error: `unknown field "${key}"` };
406
+ }
407
+ const url = cfg.url;
408
+ if (typeof url !== "string" || url.length === 0) return { error: `"url" must be a non-empty string` };
409
+ const urlError = validateRemoteUrl(url);
410
+ if (urlError) return { error: urlError };
411
+
412
+ const headers = cfg.headers;
413
+ if (headers !== undefined) {
414
+ if (!isRecord(headers)) return { error: `"headers" must be an object of strings` };
415
+ const headerError = validateHeaders(headers);
416
+ if (headerError) return { error: headerError };
417
+ }
418
+
419
+ return {
420
+ server: {
421
+ name,
422
+ transport,
423
+ url,
424
+ ...(headers !== undefined && { headers: headers as Record<string, string> }),
425
+ },
426
+ };
427
+ }
428
+
429
+ /**
430
+ * Parse and validate a plugin's `mcp.json` against the closed Agent Plugins
431
+ * 1.0.0 MCP configuration (spec §7.2), resolving stdio commands and working
432
+ * directories against the plugin root and expanding plugin variables.
433
+ */
434
+ export async function parseAgentPluginMcp(raw: string, options: AgentPluginMcpOptions): Promise<AgentPluginMcpResult> {
435
+ let parsed: unknown;
436
+ try {
437
+ parsed = JSON.parse(raw);
438
+ } catch {
439
+ return { status: "disabled", reason: "mcp.json is not valid JSON" };
440
+ }
441
+ if (!isRecord(parsed)) return { status: "disabled", reason: "mcp.json must be a JSON object" };
442
+
443
+ // The plugin manifest already pinned Agent Plugins 1.0.0, so the version
444
+ // match rule of spec §10.1 collapses to requiring the exact 1.0.0 identifier.
445
+ if (parsed.$schema !== AGENT_PLUGIN_MCP_SCHEMA) {
446
+ return { status: "disabled", reason: `mcp.json $schema must be ${AGENT_PLUGIN_MCP_SCHEMA}` };
447
+ }
448
+ for (const key in parsed) {
449
+ if (key !== "$schema" && key !== "mcpServers") {
450
+ return { status: "disabled", reason: `mcp.json has unknown top-level field "${key}"` };
451
+ }
452
+ }
453
+ const servers = parsed.mcpServers;
454
+ if (!isRecord(servers)) return { status: "disabled", reason: `"mcpServers" must be an object` };
455
+
456
+ const items: AgentPluginMcpServer[] = [];
457
+ const warnings: string[] = [];
458
+ for (const name in servers) {
459
+ const cfg = servers[name];
460
+ let result: ServerEntryResult;
461
+ if (!isRecord(cfg)) {
462
+ result = { error: "server entry must be an object" };
463
+ } else if (cfg.type === "stdio") {
464
+ result = await parseStdioServer(name, cfg, options);
465
+ } else if (cfg.type === "streamable-http") {
466
+ result = parseRemoteServer(name, cfg, "http");
467
+ } else if (cfg.type === "sse") {
468
+ result = parseRemoteServer(name, cfg, "sse");
469
+ } else {
470
+ result = { error: `unknown transport type ${JSON.stringify(cfg.type)}` };
471
+ }
472
+ if ("error" in result) {
473
+ warnings.push(`Skipping MCP server "${name}": ${result.error}`);
474
+ continue;
475
+ }
476
+ items.push(result.server);
477
+ }
478
+
479
+ return { status: "ok", servers: items, warnings };
480
+ }
481
+
482
+ /**
483
+ * Classification of a plugin root directory against the Agent Plugins standard:
484
+ * - `none` — no Agent Plugins manifest; legacy plugin conventions govern.
485
+ * - `standard` — a valid Agent Plugin; the standard governs its portable
486
+ * components (skills and MCP servers). `realRoot` is the filesystem-resolved
487
+ * plugin root every package path is contained within.
488
+ * - `invalid` — the root claims Agent Plugins conformance but its manifest is
489
+ * fatally invalid; no component may be discovered or executed (spec §11.3).
490
+ */
491
+ export type AgentPluginRootStatus =
492
+ | { kind: "none" }
493
+ | { kind: "standard"; manifest: AgentPluginManifest; warnings: string[]; realRoot: string }
494
+ | { kind: "invalid"; reason: string };
495
+
496
+ const rootStatusCache = new Map<string, Promise<AgentPluginRootStatus>>();
497
+ registerPluginCacheInvalidator(() => rootStatusCache.clear());
498
+
499
+ /** Drop cached classifications (tests, or after plugin installs outside the shared registry flow). */
500
+ export function clearAgentPluginRootCache(): void {
501
+ rootStatusCache.clear();
502
+ }
503
+
504
+ async function classifyUncached(rootPath: string): Promise<AgentPluginRootStatus> {
505
+ const realRoot = await realpathIfExists(rootPath);
506
+ if (realRoot === null) return { kind: "none" };
507
+
508
+ // Spec §4.1 failure boundary 1: resolve plugin.json and prove containment
509
+ // BEFORE reading it — an escaping manifest is rejected without consuming a
510
+ // single byte from outside the package.
511
+ const manifest = await resolveContainedPath(realRoot, path.join(realRoot, "plugin.json"));
512
+ if (manifest.status === "missing") return { kind: "none" };
513
+ if (manifest.status === "outside") {
514
+ return { kind: "invalid", reason: "plugin.json resolves outside the plugin root" };
515
+ }
516
+
517
+ const raw = await readFile(manifest.realPath);
518
+ if (raw === null) return { kind: "none" };
519
+ const parsed = parseAgentPluginManifest(raw);
520
+ if (parsed.status === "none") return { kind: "none" };
521
+ if (parsed.status === "invalid") return { kind: "invalid", reason: parsed.reason };
522
+ return { kind: "standard", manifest: parsed.manifest, warnings: parsed.warnings, realRoot };
523
+ }
524
+
525
+ /**
526
+ * Determine whether a plugin root is governed by the Agent Plugins standard.
527
+ * Results are cached per root path; the cache clears with the shared plugin
528
+ * roots cache and via {@link clearAgentPluginRootCache}.
529
+ */
530
+ export function classifyAgentPluginRoot(rootPath: string): Promise<AgentPluginRootStatus> {
531
+ let cached = rootStatusCache.get(rootPath);
532
+ if (!cached) {
533
+ cached = classifyUncached(rootPath);
534
+ rootStatusCache.set(rootPath, cached);
535
+ }
536
+ return cached;
537
+ }
538
+
539
+ /**
540
+ * Whether a legacy plugin provider (claude-plugins, omp-plugins) may process a
541
+ * root for the given surface. Roots governed by the Agent Plugins standard keep
542
+ * their portable components (`skills`, `mcp`) exclusive to the standard loader,
543
+ * while client-specific surfaces (commands, hooks, tools, …) still load from
544
+ * hybrid packages. Fatally invalid Agent Plugins packages are rejected entirely.
545
+ */
546
+ export async function legacyProviderAllowed(rootPath: string, surface: "skills" | "mcp" | "other"): Promise<boolean> {
547
+ const status = await classifyAgentPluginRoot(rootPath);
548
+ if (status.kind === "none") return true;
549
+ if (status.kind === "invalid") return false;
550
+ return surface === "other";
551
+ }