@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.
- package/CHANGELOG.md +83 -0
- package/dist/{CHANGELOG-8yy8h3bc.md → CHANGELOG-d9xenpn9.md} +83 -0
- package/dist/cli.js +10208 -12596
- package/dist/types/capability/mcp.d.ts +11 -0
- package/dist/types/capability/skill.d.ts +6 -0
- package/dist/types/cli/args.d.ts +1 -0
- package/dist/types/cli/command-help.d.ts +3 -0
- package/dist/types/commands/share.d.ts +25 -0
- package/dist/types/commit/agentic/index.d.ts +3 -1
- package/dist/types/commit/execute.d.ts +32 -0
- package/dist/types/commit/index.d.ts +2 -1
- package/dist/types/commit/pipeline.d.ts +7 -1
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/model-resolver.d.ts +13 -0
- package/dist/types/config/settings-schema.d.ts +1 -34
- package/dist/types/discovery/agent-plugin-format.d.ts +143 -0
- package/dist/types/discovery/agent-plugins.d.ts +1 -0
- package/dist/types/discovery/contained-path.d.ts +34 -0
- package/dist/types/discovery/index.d.ts +1 -0
- package/dist/types/extensibility/custom-commands/types.d.ts +5 -5
- package/dist/types/extensibility/custom-tools/types.d.ts +3 -3
- package/dist/types/extensibility/extensions/loader.d.ts +9 -2
- package/dist/types/extensibility/extensions/types.d.ts +45 -8
- package/dist/types/extensibility/hooks/types.d.ts +5 -5
- package/dist/types/extensibility/shared-events.d.ts +3 -2
- package/dist/types/extensibility/skills.d.ts +5 -0
- package/dist/types/index.d.ts +2 -2
- package/dist/types/main.d.ts +1 -1
- package/dist/types/mcp/transports/header-policy.d.ts +45 -0
- package/dist/types/mcp/types.d.ts +15 -0
- package/dist/types/modes/acp/acp-agent.d.ts +1 -1
- package/dist/types/modes/acp/acp-client-bridge.d.ts +1 -1
- package/dist/types/modes/acp/acp-event-mapper.d.ts +1 -1
- package/dist/types/modes/acp/acp-mode.d.ts +1 -1
- package/dist/types/modes/components/agent-hub-projection.d.ts +38 -0
- package/dist/types/modes/components/agent-hub-renderer.d.ts +41 -0
- package/dist/types/modes/components/agent-hub.d.ts +14 -5
- package/dist/types/modes/components/agent-transcript-viewer.d.ts +2 -0
- package/dist/types/modes/components/chat-transcript-builder.d.ts +2 -0
- package/dist/types/modes/components/custom-editor.d.ts +1 -2
- package/dist/types/modes/components/model-browser.d.ts +9 -1
- package/dist/types/modes/components/status-line/types.d.ts +2 -0
- package/dist/types/modes/components/tool-execution.d.ts +2 -0
- package/dist/types/modes/interactive-mode.d.ts +8 -1
- package/dist/types/modes/utils/transcript-render-helpers.d.ts +3 -2
- package/dist/types/registry/agent-registry.d.ts +47 -0
- package/dist/types/registry/persisted-agents.d.ts +3 -1
- package/dist/types/secrets/index.d.ts +23 -1
- package/dist/types/session/agent-session-events.d.ts +2 -2
- package/dist/types/session/agent-session.d.ts +4 -2
- package/dist/types/session/messages.d.ts +1 -0
- package/dist/types/session/prewalk.d.ts +3 -1
- package/dist/types/session/session-entries.d.ts +10 -0
- package/dist/types/session/session-loader.d.ts +11 -1
- package/dist/types/session/session-manager.d.ts +9 -1
- package/dist/types/session/session-tools.d.ts +18 -1
- package/dist/types/session/turn-recovery.d.ts +7 -2
- package/dist/types/slash-commands/acp-builtins.d.ts +1 -1
- package/dist/types/slash-commands/available-commands.d.ts +1 -1
- package/dist/types/task/executor.d.ts +6 -0
- package/dist/types/task/index.d.ts +4 -3
- package/dist/types/task/read-only-policy.d.ts +3 -0
- package/dist/types/task/structured-subagent.d.ts +2 -0
- package/dist/types/task/types.d.ts +4 -0
- package/dist/types/tools/bash.d.ts +3 -3
- package/dist/types/tools/browser/launch.d.ts +2 -1
- package/dist/types/tools/browser/tab-worker.d.ts +2 -2
- package/dist/types/tools/path-utils.d.ts +1 -0
- package/dist/types/tools/read.d.ts +1 -5
- package/dist/types/tools/run-scope.d.ts +27 -1
- package/dist/types/tools/shell-tokenize.d.ts +16 -0
- package/dist/types/tools/terminal-output.d.ts +1 -1
- package/dist/types/utils/turndown.d.ts +1 -1
- package/dist/types/vibe/runtime.d.ts +2 -3
- package/dist/types/web/scrapers/readthedocs.d.ts +0 -3
- package/dist/types/web/search/providers/browser-headers.d.ts +2 -3
- package/examples/custom-tools/README.md +1 -1
- package/examples/extensions/README.md +1 -1
- package/examples/extensions/api-demo.ts +5 -6
- package/examples/extensions/chalk-logger.ts +1 -1
- package/examples/extensions/hello.ts +2 -2
- package/examples/extensions/reload-runtime.ts +1 -1
- package/examples/extensions/tools.ts +2 -2
- package/examples/extensions/with-deps/index.ts +1 -1
- package/examples/sdk/06-extensions.ts +1 -1
- package/package.json +15 -31
- package/scripts/bundle-dist.ts +1 -11
- package/scripts/legacy-pi-virtual-module.ts +4 -1
- package/src/advisor/runtime.ts +54 -0
- package/src/capability/mcp.ts +11 -0
- package/src/capability/skill.ts +6 -0
- package/src/cli/agents-cli.ts +1 -1
- package/src/cli/args.ts +24 -2
- package/src/cli/auth-broker-cli.ts +1 -1
- package/src/cli/auth-gateway-cli.ts +1 -1
- package/src/cli/bench-cli.ts +1 -1
- package/src/cli/claude-trace-cli.ts +1 -1
- package/src/cli/command-help.ts +4 -0
- package/src/cli/config-cli.ts +1 -1
- package/src/cli/dry-balance-cli.ts +1 -1
- package/src/cli/file-processor.ts +1 -1
- package/src/cli/flag-tables.ts +4 -0
- package/src/cli/gallery-cli.ts +1 -1
- package/src/cli/grep-cli.ts +1 -1
- package/src/cli/grievances-cli.ts +1 -1
- package/src/cli/help-extra.ts +1 -1
- package/src/cli/models-cli.ts +1 -1
- package/src/cli/plugin-cli.ts +1 -1
- package/src/cli/read-cli.ts +1 -1
- package/src/cli/setup-cli.ts +1 -1
- package/src/cli/shell-cli.ts +1 -1
- package/src/cli/ssh-cli.ts +1 -1
- package/src/cli/stats-cli.ts +1 -1
- package/src/cli/tiny-models-cli.ts +1 -1
- package/src/cli/ttsr-cli.ts +1 -1
- package/src/cli/update-cli.ts +1 -1
- package/src/cli/usage-cli.ts +1 -1
- package/src/cli/web-search-cli.ts +1 -1
- package/src/cli/worktree-cli.ts +1 -1
- package/src/cli-commands.ts +5 -0
- package/src/commands/commit.ts +16 -3
- package/src/commands/say.ts +1 -1
- package/src/commands/share.ts +71 -0
- package/src/commands/token.ts +1 -1
- package/src/commit/agentic/agent.ts +1 -1
- package/src/commit/agentic/index.ts +39 -21
- package/src/commit/cli.ts +1 -1
- package/src/commit/execute.ts +56 -0
- package/src/commit/index.ts +2 -1
- package/src/commit/pipeline.ts +20 -7
- package/src/config/model-discovery.ts +1 -1
- package/src/config/model-registry.ts +21 -1
- package/src/config/model-resolver.ts +54 -7
- package/src/config/settings-schema.ts +2 -33
- package/src/config/settings.ts +46 -0
- package/src/discovery/agent-plugin-format.ts +551 -0
- package/src/discovery/agent-plugins.ts +341 -0
- package/src/discovery/agents.ts +4 -2
- package/src/discovery/builtin-rules/ts-no-inline-cast-access.md +5 -4
- package/src/discovery/builtin-rules/ts-no-tiny-functions.md +1 -1
- package/src/discovery/claude-plugins.ts +21 -5
- package/src/discovery/contained-path.ts +78 -0
- package/src/discovery/helpers.ts +23 -9
- package/src/discovery/index.ts +1 -0
- package/src/discovery/omp-plugins.ts +20 -7
- package/src/eval/py/runner.py +38 -1
- package/src/exec/non-interactive-env.ts +1 -0
- package/src/extensibility/custom-commands/loader.ts +4 -4
- package/src/extensibility/custom-commands/types.ts +5 -5
- package/src/extensibility/custom-tools/loader.ts +4 -4
- package/src/extensibility/custom-tools/types.ts +3 -3
- package/src/extensibility/extensions/loader.ts +85 -20
- package/src/extensibility/extensions/runner.ts +6 -0
- package/src/extensibility/extensions/types.ts +51 -8
- package/src/extensibility/hooks/loader.ts +4 -5
- package/src/extensibility/hooks/types.ts +5 -5
- package/src/extensibility/plugins/legacy-pi-compat.ts +53 -0
- package/src/extensibility/plugins/marketplace/manager.ts +2 -1
- package/src/extensibility/shared-events.ts +3 -2
- package/src/extensibility/skills.ts +9 -0
- package/src/index.ts +2 -2
- package/src/internal-urls/memory-protocol.ts +5 -1
- package/src/internal-urls/skill-protocol.ts +14 -0
- package/src/internal-urls/vault-protocol.ts +2 -2
- package/src/launch/client.ts +11 -1
- package/src/launch/protocol.ts +7 -2
- package/src/launch/terminal-output.ts +1 -1
- package/src/main.ts +88 -23
- package/src/markit/converters/docx.ts +1 -1
- package/src/markit/converters/epub.ts +1 -1
- package/src/markit/converters/pptx.ts +1 -1
- package/src/markit/converters/xlsx.ts +1 -1
- package/src/mcp/config.ts +3 -0
- package/src/mcp/manager.ts +12 -9
- package/src/mcp/transports/header-policy.ts +95 -0
- package/src/mcp/transports/http.ts +35 -52
- package/src/mcp/transports/sse.ts +20 -27
- package/src/mcp/types.ts +15 -0
- package/src/modes/acp/acp-agent.ts +20 -9
- package/src/modes/acp/acp-client-bridge.ts +1 -1
- package/src/modes/acp/acp-event-mapper.ts +1 -1
- package/src/modes/acp/acp-mode.ts +1 -1
- package/src/modes/components/agent-dashboard.ts +2 -0
- package/src/modes/components/agent-hub-projection.ts +248 -0
- package/src/modes/components/agent-hub-renderer.ts +194 -0
- package/src/modes/components/agent-hub.ts +674 -207
- package/src/modes/components/agent-transcript-viewer.ts +3 -0
- package/src/modes/components/assistant-message.ts +1 -1
- package/src/modes/components/chat-transcript-builder.ts +3 -0
- package/src/modes/components/custom-editor.ts +0 -9
- package/src/modes/components/extensions/inspector-panel.ts +11 -6
- package/src/modes/components/footer.ts +1 -3
- package/src/modes/components/model-browser.ts +11 -3
- package/src/modes/components/model-hub.ts +4 -1
- package/src/modes/components/status-line/component.ts +1 -0
- package/src/modes/components/status-line/segments.ts +10 -7
- package/src/modes/components/status-line/types.ts +2 -0
- package/src/modes/components/tool-execution.ts +13 -16
- package/src/modes/components/tree-selector.ts +62 -3
- package/src/modes/controllers/command-controller.ts +4 -1
- package/src/modes/controllers/event-controller.ts +44 -20
- package/src/modes/controllers/extension-ui-controller.ts +2 -2
- package/src/modes/controllers/input-controller.ts +20 -2
- package/src/modes/controllers/mcp-command-controller.ts +81 -20
- package/src/modes/controllers/selector-controller.ts +41 -49
- package/src/modes/controllers/streaming-reveal.ts +1 -1
- package/src/modes/interactive-mode.ts +13 -2
- package/src/modes/rpc/rpc-mode.ts +2 -2
- package/src/modes/runtime-init.ts +1 -1
- package/src/modes/theme/theme.ts +2 -2
- package/src/modes/utils/transcript-render-helpers.ts +4 -2
- package/src/modes/utils/ui-helpers.ts +1 -0
- package/src/prompts/tools/computer.md +1 -1
- package/src/registry/agent-lifecycle.ts +14 -7
- package/src/registry/agent-registry.ts +65 -2
- package/src/registry/persisted-agents.ts +341 -16
- package/src/sdk.ts +6 -49
- package/src/secrets/index.ts +52 -1
- package/src/session/agent-session-events.ts +2 -2
- package/src/session/agent-session.ts +54 -16
- package/src/session/messages.ts +1 -0
- package/src/session/prewalk.ts +57 -17
- package/src/session/session-context.ts +3 -5
- package/src/session/session-entries.ts +10 -0
- package/src/session/session-handoff.ts +5 -1
- package/src/session/session-listing.ts +1 -1
- package/src/session/session-loader.ts +74 -8
- package/src/session/session-manager.ts +22 -2
- package/src/session/session-paths.ts +57 -9
- package/src/session/session-tools.ts +65 -4
- package/src/session/todo-tracker.ts +11 -1
- package/src/session/turn-recovery.ts +96 -74
- package/src/slash-commands/acp-builtins.ts +1 -1
- package/src/slash-commands/available-commands.ts +1 -1
- package/src/slash-commands/builtin-registry.ts +12 -8
- package/src/task/executor.ts +70 -20
- package/src/task/index.ts +30 -34
- package/src/task/isolation-runner.ts +24 -11
- package/src/task/persisted-revive.ts +12 -5
- package/src/task/read-only-policy.ts +27 -0
- package/src/task/structured-subagent.ts +14 -4
- package/src/task/types.ts +4 -0
- package/src/tools/auto-generated-guard.ts +1 -1
- package/src/tools/bash-interactive.ts +4 -4
- package/src/tools/bash-skill-urls.ts +15 -0
- package/src/tools/bash.ts +16 -17
- package/src/tools/browser/cmux/cmux-tab.ts +66 -18
- package/src/tools/browser/launch.ts +41 -12
- package/src/tools/browser/readable.ts +9 -9
- package/src/tools/browser/tab-supervisor.ts +12 -3
- package/src/tools/browser/tab-worker-entry.ts +1 -1
- package/src/tools/browser/tab-worker.ts +101 -12
- package/src/tools/debug.ts +1 -1
- package/src/tools/fetch.ts +1 -1
- package/src/tools/jtd-to-json-schema.ts +123 -21
- package/src/tools/path-utils.ts +9 -2
- package/src/tools/read.ts +21 -9
- package/src/tools/run-scope.ts +290 -4
- package/src/tools/shell-tokenize.ts +98 -0
- package/src/tools/terminal-output.ts +1 -1
- package/src/tools/todo.ts +1 -1
- package/src/utils/clipboard.ts +10 -2
- package/src/utils/external-editor.ts +4 -2
- package/src/utils/jj.ts +1 -1
- package/src/utils/turndown.ts +1 -2
- package/src/vibe/runtime.ts +5 -5
- package/src/web/scrapers/arxiv.ts +1 -1
- package/src/web/scrapers/go-pkg.ts +1 -1
- package/src/web/scrapers/iacr.ts +1 -1
- package/src/web/scrapers/readthedocs.ts +2 -1
- package/src/web/scrapers/twitter.ts +2 -2
- package/src/web/scrapers/types.ts +1 -1
- package/src/web/scrapers/wikipedia.ts +1 -1
- package/src/web/search/providers/browser-headers.ts +12 -39
- package/src/web/search/providers/codex.ts +3 -26
- package/src/web/search/providers/ecosia.ts +1 -1
- package/src/web/search/providers/exa.ts +1 -1
- package/src/web/search/providers/google.ts +1 -1
- package/src/web/search/providers/mojeek.ts +1 -1
- package/src/web/search/providers/startpage.ts +1 -1
- 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
|
+
}
|