@adea-ai/plugins 1.0.0

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 (79) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +74 -0
  3. package/dist/agent-plugins-catalog.d.ts +26 -0
  4. package/dist/agent-plugins-catalog.js +699 -0
  5. package/dist/agent-plugins-catalog.js.map +1 -0
  6. package/dist/agent-plugins-harness.d.ts +54 -0
  7. package/dist/agent-plugins-harness.js +212 -0
  8. package/dist/agent-plugins-harness.js.map +1 -0
  9. package/dist/agent-plugins-schema.d.ts +402 -0
  10. package/dist/agent-plugins-schema.js +200 -0
  11. package/dist/agent-plugins-schema.js.map +1 -0
  12. package/dist/consumer-index.d.ts +217 -0
  13. package/dist/consumer-index.js +358 -0
  14. package/dist/consumer-index.js.map +1 -0
  15. package/dist/display-name.d.ts +8 -0
  16. package/dist/display-name.js +160 -0
  17. package/dist/display-name.js.map +1 -0
  18. package/dist/harness.d.ts +13 -0
  19. package/dist/harness.js +169 -0
  20. package/dist/harness.js.map +1 -0
  21. package/dist/icon-mirror.d.ts +69 -0
  22. package/dist/icon-mirror.js +147 -0
  23. package/dist/icon-mirror.js.map +1 -0
  24. package/dist/icons.d.ts +65 -0
  25. package/dist/icons.js +268 -0
  26. package/dist/icons.js.map +1 -0
  27. package/dist/index.d.ts +304 -0
  28. package/dist/index.js +1877 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/portable-catalog.d.ts +9 -0
  31. package/dist/portable-catalog.js +175 -0
  32. package/dist/portable-catalog.js.map +1 -0
  33. package/dist/publication.d.ts +98 -0
  34. package/dist/publication.js +121 -0
  35. package/dist/publication.js.map +1 -0
  36. package/dist/schema.d.ts +705 -0
  37. package/dist/schema.js +267 -0
  38. package/dist/schema.js.map +1 -0
  39. package/dist/site-icons.d.ts +55 -0
  40. package/dist/site-icons.js +155 -0
  41. package/dist/site-icons.js.map +1 -0
  42. package/dist/sources.d.ts +54 -0
  43. package/dist/sources.js +306 -0
  44. package/dist/sources.js.map +1 -0
  45. package/package.json +102 -0
  46. package/schemas/agent-package.v1.json +387 -0
  47. package/schemas/catalog.v1.schema.json +407 -0
  48. package/schemas/compatibility.v1.schema.json +23 -0
  49. package/schemas/harness-profile.v1.json +75 -0
  50. package/schemas/installation-plan.v2.json +695 -0
  51. package/schemas/sources-lock.v1.schema.json +61 -0
  52. package/src/agent-plugins-catalog.test.ts +402 -0
  53. package/src/agent-plugins-catalog.ts +800 -0
  54. package/src/agent-plugins-harness.test.ts +272 -0
  55. package/src/agent-plugins-harness.ts +281 -0
  56. package/src/agent-plugins-schema.test.ts +27 -0
  57. package/src/agent-plugins-schema.ts +219 -0
  58. package/src/artifacts.test.ts +545 -0
  59. package/src/catalog-installation.test.ts +42 -0
  60. package/src/consumer-index.test.ts +465 -0
  61. package/src/consumer-index.ts +592 -0
  62. package/src/display-name.test.ts +56 -0
  63. package/src/display-name.ts +157 -0
  64. package/src/harness.ts +202 -0
  65. package/src/icon-mirror.ts +196 -0
  66. package/src/icons.test.ts +185 -0
  67. package/src/icons.ts +297 -0
  68. package/src/index.test.ts +888 -0
  69. package/src/index.ts +2538 -0
  70. package/src/portable-catalog.test.ts +186 -0
  71. package/src/portable-catalog.ts +203 -0
  72. package/src/publication.test.ts +117 -0
  73. package/src/publication.ts +171 -0
  74. package/src/schema.ts +309 -0
  75. package/src/site-icon-fallback.test.ts +267 -0
  76. package/src/site-icons.test.ts +171 -0
  77. package/src/site-icons.ts +161 -0
  78. package/src/sources.ts +360 -0
  79. package/src/upstream-headers.test.ts +148 -0
@@ -0,0 +1,272 @@
1
+ import { describe, it } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { mkdtemp, mkdir, writeFile, symlink, rm, realpath } from 'node:fs/promises'
4
+ import { tmpdir } from 'node:os'
5
+ import { join } from 'node:path'
6
+ import { compileAgentPackage } from './agent-plugins-catalog.js'
7
+ import {
8
+ PLUGIN_SCHEMA_ID,
9
+ MCP_SCHEMA_ID,
10
+ type HarnessProfile,
11
+ type McpServer,
12
+ } from './agent-plugins-schema.js'
13
+ import {
14
+ createInstallationPlan,
15
+ resolveMcpBinding,
16
+ expandPluginPaths,
17
+ verifyBindingPaths,
18
+ } from './agent-plugins-harness.js'
19
+
20
+ function profile(overrides: Partial<HarnessProfile> = {}): HarnessProfile {
21
+ return {
22
+ profileVersion: 1,
23
+ harness: 'example-harness',
24
+ runtimeVersion: 'fixture-1',
25
+ adapterVersion: 'fixture-1',
26
+ agentPlugins: {
27
+ versions: ['1.0.0'],
28
+ skills: true,
29
+ mcpTransports: ['stdio', 'streamable-http'],
30
+ },
31
+ components: { skillDirectories: true, mcpTransports: ['stdio', 'streamable-http'] },
32
+ ...overrides,
33
+ }
34
+ }
35
+ function packageFixture(extra: Record<string, string> = {}) {
36
+ const files = new Map(
37
+ Object.entries({
38
+ 'plugin.json': JSON.stringify({ $schema: PLUGIN_SCHEMA_ID, name: 'issues' }),
39
+ 'skills/issues/SKILL.md':
40
+ '---\nname: issues\ndescription: Work with issues.\n---\nInstructions.',
41
+ 'mcp.json': JSON.stringify({
42
+ $schema: MCP_SCHEMA_ID,
43
+ mcpServers: {
44
+ api: { type: 'streamable-http', url: 'https://example.test/mcp' },
45
+ local: { type: 'stdio', command: 'node' },
46
+ },
47
+ }),
48
+ ...extra,
49
+ }).map(([path, text]) => [path, new TextEncoder().encode(text)])
50
+ )
51
+ return compileAgentPackage({ files, name: 'issues' })
52
+ }
53
+ function input(pkg = packageFixture()) {
54
+ return {
55
+ pluginId: 'plugin:test:issues',
56
+ releaseId: `release:${'a'.repeat(64)}`,
57
+ instanceId: 'workspace-a/user-a',
58
+ source: {
59
+ repositoryUrl: 'https://github.com/example/issues',
60
+ commitSha: 'b'.repeat(40),
61
+ pluginSubdirectory: '.',
62
+ contentDigest: pkg.sourceDigest,
63
+ },
64
+ package: pkg,
65
+ profile: profile(),
66
+ }
67
+ }
68
+
69
+ describe('Control Plane installation plan v2', () => {
70
+ it('selects a single native package root without copying into harness-specific directories', () => {
71
+ const plan = createInstallationPlan(input())
72
+ assert.equal(plan.planVersion, 2)
73
+ assert.equal(plan.strategy, 'native-agent-plugin')
74
+ assert.equal(plan.compatibility, 'full')
75
+ assert.deepEqual(plan.selection.skillDirectories, ['skills/issues'])
76
+ assert.equal(plan.allowedToActivate, false)
77
+ assert.equal(plan.approvalRequired, true)
78
+ assert.ok(plan.packageKey.startsWith('packages/'))
79
+ })
80
+ it('uses component bindings for a non-native harness without changing package bytes', () => {
81
+ const original = input()
82
+ const native = createInstallationPlan(original)
83
+ const adapted = createInstallationPlan({
84
+ ...original,
85
+ profile: profile({ agentPlugins: { versions: [], skills: false, mcpTransports: [] } }),
86
+ })
87
+ assert.equal(adapted.strategy, 'component-adapter')
88
+ assert.equal(adapted.package.packageDigest, native.package.packageDigest)
89
+ assert.deepEqual(adapted.selection, native.selection)
90
+ })
91
+ it('does not treat a harness name as proof of support', () => {
92
+ const plan = createInstallationPlan({
93
+ ...input(),
94
+ profile: profile({
95
+ harness: 'codex',
96
+ agentPlugins: { versions: [], skills: false, mcpTransports: [] },
97
+ components: { skillDirectories: false, mcpTransports: [] },
98
+ }),
99
+ })
100
+ assert.equal(plan.compatibility, 'unsupported')
101
+ assert.equal(plan.strategy, 'unavailable')
102
+ })
103
+ it('supports explicit partial adoption but requires opt-in before selecting components', () => {
104
+ const p = profile({
105
+ agentPlugins: { versions: ['1.0.0'], skills: true, mcpTransports: [] },
106
+ components: { skillDirectories: false, mcpTransports: [] },
107
+ })
108
+ const blocked = createInstallationPlan({ ...input(), profile: p })
109
+ assert.equal(blocked.compatibility, 'partial')
110
+ assert.equal(blocked.strategy, 'unavailable')
111
+ assert.deepEqual(blocked.selection.skillDirectories, [])
112
+ const accepted = createInstallationPlan({ ...input(), profile: p, allowPartial: true })
113
+ assert.equal(accepted.strategy, 'native-agent-plugin')
114
+ assert.deepEqual(accepted.selection.skillDirectories, ['skills/issues'])
115
+ assert.equal(accepted.disabled.length, 2)
116
+ })
117
+ it('chooses a complete component adapter over a native loader with weaker capabilities', () => {
118
+ const plan = createInstallationPlan({
119
+ ...input(),
120
+ profile: profile({ agentPlugins: { versions: ['1.0.0'], skills: true, mcpTransports: [] } }),
121
+ })
122
+ assert.equal(plan.strategy, 'component-adapter')
123
+ assert.equal(plan.compatibility, 'full')
124
+ })
125
+ it('does not activate proprietary extras accidentally through a native loader', () => {
126
+ const pkg = packageFixture({ 'hooks/hooks.json': '{}' })
127
+ const plan = createInstallationPlan({ ...input(pkg), allowPartial: true })
128
+ assert.equal(plan.strategy, 'component-adapter')
129
+ assert.equal(plan.compatibility, 'partial')
130
+ assert.ok(plan.disabled.some((item) => item.component === 'hooks'))
131
+ })
132
+ it('preserves instance data across releases and harnesses but isolates other instances', () => {
133
+ const first = createInstallationPlan(input())
134
+ const updatedPkg = packageFixture({ 'CHANGELOG.md': 'A new release.' })
135
+ const updated = createInstallationPlan({
136
+ ...input(updatedPkg),
137
+ releaseId: `release:${'c'.repeat(64)}`,
138
+ profile: profile({ harness: 'another-harness' }),
139
+ })
140
+ assert.equal(first.dataKey, updated.dataKey)
141
+ assert.notEqual(first.packageKey, updated.packageKey)
142
+ assert.notEqual(
143
+ first.dataKey,
144
+ createInstallationPlan({ ...input(), instanceId: 'workspace-b/user-a' }).dataKey
145
+ )
146
+ assert.notEqual(
147
+ first.dataKey,
148
+ createInstallationPlan({ ...input(), pluginId: 'plugin:other:issues' }).dataKey
149
+ )
150
+ })
151
+ it('does not reuse a package cache across source commits with potentially different file modes', () => {
152
+ const original = input()
153
+ const other = { ...original, source: { ...original.source, commitSha: 'c'.repeat(40) } }
154
+ assert.notEqual(
155
+ createInstallationPlan(original).packageKey,
156
+ createInstallationPlan(other).packageKey
157
+ )
158
+ assert.equal(createInstallationPlan(original).dataKey, createInstallationPlan(other).dataKey)
159
+ })
160
+ it('rejects mismatched source provenance and unversioned capability claims', () => {
161
+ const args = input()
162
+ assert.throws(() =>
163
+ createInstallationPlan({
164
+ ...args,
165
+ source: { ...args.source, contentDigest: `sha256:${'f'.repeat(64)}` },
166
+ })
167
+ )
168
+ assert.throws(() =>
169
+ createInstallationPlan({ ...args, profile: { ...args.profile, runtimeVersion: '' } })
170
+ )
171
+ })
172
+ })
173
+
174
+ describe('MCP binding preparation', () => {
175
+ const roots = { pluginRoot: '/packages/with spaces', pluginData: '/data/with spaces' }
176
+ it('keeps command and arguments separate and overrides reserved variables last', () => {
177
+ const server: McpServer = {
178
+ type: 'stdio',
179
+ command: 'node',
180
+ args: ['${PLUGIN_ROOT}/run.mjs', '${UNRECOGNIZED}', '${PLUGIN_DATA}'],
181
+ env: { CONFIG: '${PLUGIN_ROOT}/config.json' },
182
+ }
183
+ const binding = resolveMcpBinding(server, roots, {
184
+ PLUGIN_ROOT: 'wrong',
185
+ PLUGIN_DATA: 'wrong',
186
+ SAFE: 'yes',
187
+ })
188
+ assert.equal(binding.type, 'stdio')
189
+ if (binding.type !== 'stdio') return
190
+ assert.equal(binding.command, 'node')
191
+ assert.equal(binding.shell, false)
192
+ assert.equal(binding.cwd, roots.pluginRoot)
193
+ assert.deepEqual(binding.args, [
194
+ '/packages/with spaces/run.mjs',
195
+ '${UNRECOGNIZED}',
196
+ '/data/with spaces',
197
+ ])
198
+ assert.equal(binding.env.PLUGIN_ROOT, roots.pluginRoot)
199
+ assert.equal(binding.env.PLUGIN_DATA, roots.pluginData)
200
+ assert.equal(binding.env.SAFE, 'yes')
201
+ })
202
+ it('expands once, leaving replacement text and unrelated placeholders literal', () => {
203
+ assert.equal(
204
+ expandPluginPaths('${PLUGIN_ROOT}/${PLUGIN_DATA}/${HOME}', {
205
+ pluginRoot: '/root/${PLUGIN_DATA}',
206
+ pluginData: '/state',
207
+ }),
208
+ '/root/${PLUGIN_DATA}//state/${HOME}'
209
+ )
210
+ })
211
+ it('handles case-insensitive environment overlays without reserved-name shadowing', () => {
212
+ const binding = resolveMcpBinding(
213
+ { type: 'stdio', command: 'node', env: { Path: '/safe' } },
214
+ roots,
215
+ { PATH: '/old', plugin_root: 'wrong', Plugin_Data: 'wrong' },
216
+ 'win32'
217
+ )
218
+ assert.equal(binding.type, 'stdio')
219
+ if (binding.type !== 'stdio') return
220
+ assert.equal(binding.env.PATH, undefined)
221
+ assert.equal(binding.env.Path, '/safe')
222
+ assert.equal(binding.env.plugin_root, undefined)
223
+ assert.equal(binding.env.Plugin_Data, undefined)
224
+ assert.equal(binding.env.PLUGIN_ROOT, roots.pluginRoot)
225
+ })
226
+ it('never expands remote URLs or headers and requires same-origin redirects', () => {
227
+ const binding = resolveMcpBinding(
228
+ {
229
+ type: 'streamable-http',
230
+ url: 'https://example.test/mcp',
231
+ headers: { 'X-Literal': '${PLUGIN_DATA}' },
232
+ },
233
+ roots
234
+ )
235
+ assert.deepEqual(binding, {
236
+ type: 'streamable-http',
237
+ url: 'https://example.test/mcp',
238
+ headers: { 'X-Literal': '${PLUGIN_DATA}' },
239
+ redirectPolicy: 'same-origin',
240
+ authorization: 'control-plane',
241
+ })
242
+ })
243
+ it('rejects filesystem-resolved escapes before activation', async () => {
244
+ const temporary = await mkdtemp(join(tmpdir(), 'adea-binding-'))
245
+ try {
246
+ const pluginRoot = join(temporary, 'package')
247
+ const pluginData = join(temporary, 'data')
248
+ await mkdir(pluginRoot)
249
+ await mkdir(pluginData)
250
+ await writeFile(join(temporary, 'outside'), 'never execute')
251
+ await symlink(join(temporary, 'outside'), join(pluginRoot, 'server'))
252
+ const canonical = {
253
+ pluginRoot: await realpath(pluginRoot),
254
+ pluginData: await realpath(pluginData),
255
+ }
256
+ await assert.rejects(
257
+ verifyBindingPaths({ type: 'stdio', command: './server' }, canonical),
258
+ /outside/
259
+ )
260
+ await symlink(temporary, join(pluginData, 'escape'))
261
+ await assert.rejects(
262
+ verifyBindingPaths(
263
+ { type: 'stdio', command: 'node', cwd: '${PLUGIN_DATA}/escape' },
264
+ canonical
265
+ ),
266
+ /outside/
267
+ )
268
+ } finally {
269
+ await rm(temporary, { recursive: true, force: true })
270
+ }
271
+ })
272
+ })
@@ -0,0 +1,281 @@
1
+ import { isAbsolute, resolve, relative, sep } from 'node:path'
2
+ import { realpath } from 'node:fs/promises'
3
+ import { createHash } from 'node:crypto'
4
+ import {
5
+ HarnessProfileSchema,
6
+ InstallationPlanSchema,
7
+ type HarnessProfile,
8
+ type McpServer,
9
+ type InstallationPlan,
10
+ } from './agent-plugins-schema.js'
11
+ import type { Catalog } from './schema.js'
12
+ import { verifyPortableCatalog, PACKAGE_METADATA_KEY } from './portable-catalog.js'
13
+ import { verifyAgentPackage, validateServer } from './agent-plugins-catalog.js'
14
+
15
+ export interface InstallationInput {
16
+ readonly pluginId: string
17
+ readonly releaseId: string
18
+ readonly instanceId: string
19
+ readonly source: InstallationPlan['source']
20
+ readonly package: unknown
21
+ readonly profile: HarnessProfile
22
+ readonly requiredCredentials?: readonly string[]
23
+ readonly requiredConnectors?: readonly string[]
24
+ readonly allowPartial?: boolean
25
+ }
26
+ export interface CatalogInstallationInput {
27
+ readonly catalog: Catalog
28
+ readonly pluginId: string
29
+ readonly releaseId?: string
30
+ readonly instanceId: string
31
+ readonly profile: HarnessProfile
32
+ readonly allowPartial?: boolean
33
+ }
34
+
35
+ /** Resolve the exact catalog release before planning. The caller must also authenticate
36
+ * the catalog publisher and verify its artifact integrity before invoking this API.
37
+ */
38
+ export function createCatalogInstallationPlan(input: CatalogInstallationInput): InstallationPlan {
39
+ verifyPortableCatalog(input.catalog)
40
+ const plugin = input.catalog.plugins.find((entry) => entry.pluginId === input.pluginId)
41
+ if (!plugin) throw new Error(`PLUGIN_NOT_FOUND: ${input.pluginId}`)
42
+ const release = plugin.availableReleases.find(
43
+ (entry) => entry.releaseId === (input.releaseId ?? plugin.currentReleaseId)
44
+ )
45
+ if (!release) throw new Error('RELEASE_NOT_FOUND')
46
+ if (release.contentResolution !== 'complete') throw new Error('COMPLETE_SOURCE_REQUIRED')
47
+ const pkg = release.releaseMetadata[PACKAGE_METADATA_KEY]
48
+ if (pkg === undefined)
49
+ throw new Error(
50
+ 'PORTABLE_RESYNC_REQUIRED: run a full catalog sync before planning portable installation'
51
+ )
52
+ return createInstallationPlan({
53
+ pluginId: plugin.pluginId,
54
+ releaseId: release.releaseId,
55
+ instanceId: input.instanceId,
56
+ profile: input.profile,
57
+ package: pkg,
58
+ allowPartial: input.allowPartial === true,
59
+ source: {
60
+ repositoryUrl: release.resolvedRepositoryUrl,
61
+ commitSha: release.resolvedCommitSha,
62
+ pluginSubdirectory: release.pluginSubdirectory,
63
+ contentDigest: release.canonicalContentDigest,
64
+ },
65
+ requiredCredentials: release.requiredCredentials,
66
+ requiredConnectors: release.requiredConnectors,
67
+ })
68
+ }
69
+
70
+ function identity(...parts: string[]): string {
71
+ return createHash('sha256').update(JSON.stringify(parts)).digest('hex')
72
+ }
73
+ /** A declarative proposal, never an execution grant. No brand-based support assumptions. */
74
+ export function createInstallationPlan(input: InstallationInput): InstallationPlan {
75
+ const profile = HarnessProfileSchema.parse(input.profile)
76
+ const pkg = verifyAgentPackage(input.package)
77
+ if (pkg.sourceDigest !== input.source.contentDigest)
78
+ throw new Error('Package recipe does not match the selected immutable release.')
79
+ if (!input.instanceId || input.instanceId.length > 256)
80
+ throw new Error('A stable installation instanceId is required.')
81
+ const disabled: InstallationPlan['disabled'] = []
82
+ for (const item of pkg.nonPortable) disabled.push({ component: item.kind, reason: item.reason })
83
+ for (const item of pkg.diagnostics.filter((entry) => entry.severity === 'error'))
84
+ disabled.push({ component: item.path, reason: item.message })
85
+
86
+ const nativeEligible =
87
+ profile.agentPlugins.versions.includes(pkg.specVersion) &&
88
+ pkg.nonPortable.length === 0 &&
89
+ !Object.keys(pkg.manifest?.extensions ?? {}).length
90
+ const nativeCoverage =
91
+ (profile.agentPlugins.skills ? pkg.skills.length : 0) +
92
+ Object.values(pkg.mcpServers).filter((server) =>
93
+ profile.agentPlugins.mcpTransports.includes(server.type)
94
+ ).length
95
+ const adapterCoverage =
96
+ (profile.components.skillDirectories ? pkg.skills.length : 0) +
97
+ Object.values(pkg.mcpServers).filter((server) =>
98
+ profile.components.mcpTransports.includes(server.type)
99
+ ).length
100
+ const native = nativeEligible && nativeCoverage > 0 && nativeCoverage >= adapterCoverage
101
+ const strategy =
102
+ pkg.status === 'unavailable'
103
+ ? 'unavailable'
104
+ : native
105
+ ? 'native-agent-plugin'
106
+ : 'component-adapter'
107
+ let skillDirectories: string[] = []
108
+ const servers = new Map<string, McpServer>()
109
+ if (strategy !== 'unavailable') {
110
+ if (native ? profile.agentPlugins.skills : profile.components.skillDirectories)
111
+ skillDirectories = pkg.skills.map((skill) => skill.path)
112
+ else
113
+ for (const skill of pkg.skills)
114
+ disabled.push({
115
+ component: `skill:${skill.name}`,
116
+ reason: 'The selected adapter cannot expose skill directories.',
117
+ })
118
+ for (const [name, server] of Object.entries(pkg.mcpServers)) {
119
+ if (
120
+ (native ? profile.agentPlugins.mcpTransports : profile.components.mcpTransports).includes(
121
+ server.type
122
+ )
123
+ )
124
+ servers.set(name, server)
125
+ else
126
+ disabled.push({
127
+ component: `mcp:${name}`,
128
+ reason: `No ${server.type} MCP binding on this harness adapter.`,
129
+ })
130
+ }
131
+ }
132
+ const hasContent = skillDirectories.length > 0 || servers.size > 0
133
+ const compatibility = !hasContent ? 'unsupported' : disabled.length > 0 ? 'partial' : 'full'
134
+ const selectedStrategy =
135
+ !hasContent || (compatibility === 'partial' && !input.allowPartial) ? 'unavailable' : strategy
136
+ return InstallationPlanSchema.parse({
137
+ planVersion: 2,
138
+ pluginId: input.pluginId,
139
+ releaseId: input.releaseId,
140
+ instanceId: input.instanceId,
141
+ profile,
142
+ source: input.source,
143
+ package: pkg,
144
+ // Git source identity also pins executable modes, which the v1 byte snapshot omits.
145
+ packageKey: pkg.packageDigest
146
+ ? `packages/${identity(input.source.repositoryUrl, input.source.commitSha, input.source.pluginSubdirectory, pkg.packageDigest)}`
147
+ : '',
148
+ // Deliberately excludes release, package digest and harness. Switching harnesses or updating
149
+ // the same approved instance preserves data; distinct scopes use distinct instance IDs.
150
+ dataKey: `instances/${identity(input.pluginId, input.instanceId)}`,
151
+ preserveDataAcrossUpdates: true,
152
+ strategy: selectedStrategy,
153
+ compatibility,
154
+ allowedToActivate: false,
155
+ approvalRequired: true,
156
+ selection:
157
+ selectedStrategy === 'unavailable'
158
+ ? { skillDirectories: [], mcpServers: {} }
159
+ : { skillDirectories, mcpServers: Object.fromEntries(servers) },
160
+ disabled,
161
+ requiredCredentials: [...(input.requiredCredentials ?? [])],
162
+ requiredConnectors: [...(input.requiredConnectors ?? [])],
163
+ preconditions: [
164
+ 'Verify catalog integrity/provenance and exact source digest before applying the package recipe.',
165
+ 'Use an immutable package root; preserve source file modes and complete skill directories/resources.',
166
+ 'Create a writable data directory for this instance; never key it by release or copy credentials between instances.',
167
+ 'Control Plane must authorize activation, filesystem/network access, executables and credential bindings.',
168
+ 'Recheck the actual harness/runtime/adapter profile and realpath containment immediately before activation.',
169
+ 'Native mode registers exactly one package root. Component mode registers only the selected skill directories and MCP bindings, never legacy hooks/config files.',
170
+ 'Do not install dependencies or run lifecycle scripts implicitly; review required executables and skill compatibility requirements.',
171
+ 'Skill allowed-tools metadata is advisory and never grants permission.',
172
+ 'Namespace registrations by installation identity; detect skill/server name collisions instead of overwriting existing harness configuration.',
173
+ 'Remote authorization is client-managed. Do not expand remote URLs/headers or forward configured headers to another origin without authorization.',
174
+ ],
175
+ })
176
+ }
177
+
178
+ export interface RuntimeRoots {
179
+ readonly pluginRoot: string
180
+ readonly pluginData: string
181
+ }
182
+ /** Exact, single-pass expansion. Replacement text is never scanned recursively. */
183
+ export function expandPluginPaths(value: string, roots: RuntimeRoots): string {
184
+ return value.replace(/\$\{(PLUGIN_ROOT|PLUGIN_DATA)\}/g, (_match, name: string) =>
185
+ name === 'PLUGIN_ROOT' ? roots.pluginRoot : roots.pluginData
186
+ )
187
+ }
188
+ function within(path: string, root: string): boolean {
189
+ const suffix = relative(root, path)
190
+ return suffix === '' || (suffix !== '..' && !suffix.startsWith(`..${sep}`) && !isAbsolute(suffix))
191
+ }
192
+ export type McpBinding =
193
+ | {
194
+ type: 'stdio'
195
+ command: string
196
+ args: string[]
197
+ cwd: string
198
+ env: Record<string, string>
199
+ shell: false
200
+ }
201
+ | {
202
+ type: 'streamable-http' | 'sse'
203
+ url: string
204
+ headers: Record<string, string>
205
+ redirectPolicy: 'same-origin'
206
+ authorization: 'control-plane'
207
+ }
208
+
209
+ /** Produce concrete Control Plane launch/connect descriptors, without launching or connecting. */
210
+ export function resolveMcpBinding(
211
+ input: McpServer,
212
+ roots: RuntimeRoots,
213
+ baseEnv: Readonly<Record<string, string>> = {},
214
+ platform: 'posix' | 'win32' = process.platform === 'win32' ? 'win32' : 'posix'
215
+ ): McpBinding {
216
+ const server = validateServer(input)
217
+ if (!isAbsolute(roots.pluginRoot) || !isAbsolute(roots.pluginData))
218
+ throw new Error('Runtime plugin roots must be absolute paths.')
219
+ if (server.type !== 'stdio')
220
+ return {
221
+ type: server.type,
222
+ url: server.url,
223
+ headers: { ...server.headers },
224
+ redirectPolicy: 'same-origin',
225
+ authorization: 'control-plane',
226
+ }
227
+ const command = server.command.startsWith('./')
228
+ ? resolve(roots.pluginRoot, server.command)
229
+ : server.command
230
+ if (server.command.startsWith('./') && !within(command, roots.pluginRoot))
231
+ throw new Error('Executable escapes the plugin root.')
232
+ const cwdInput = server.cwd ?? '${PLUGIN_ROOT}'
233
+ const cwdRoot = cwdInput.startsWith('${PLUGIN_DATA}') ? roots.pluginData : roots.pluginRoot
234
+ const expanded = expandPluginPaths(cwdInput, roots)
235
+ const cwd = resolve(roots.pluginRoot, expanded)
236
+ if (!within(cwd, cwdRoot)) throw new Error('Working directory escapes its declared root.')
237
+ const env: Record<string, string> = {}
238
+ const overlay = (name: string, value: string): void => {
239
+ if (platform === 'win32')
240
+ for (const existing of Object.keys(env))
241
+ if (existing.toUpperCase() === name.toUpperCase()) delete env[existing]
242
+ Object.defineProperty(env, name, {
243
+ value,
244
+ enumerable: true,
245
+ configurable: true,
246
+ writable: true,
247
+ })
248
+ }
249
+ for (const [key, value] of Object.entries(baseEnv)) overlay(key, value)
250
+ for (const [key, value] of Object.entries(server.env ?? {}))
251
+ overlay(key, expandPluginPaths(value, roots))
252
+ overlay('PLUGIN_ROOT', roots.pluginRoot)
253
+ overlay('PLUGIN_DATA', roots.pluginData)
254
+ return {
255
+ type: 'stdio',
256
+ command,
257
+ args: (server.args ?? []).map((arg) => expandPluginPaths(arg, roots)),
258
+ cwd,
259
+ env,
260
+ shell: false,
261
+ }
262
+ }
263
+
264
+ /** Mandatory filesystem preflight for Control Plane callers after safe materialization.
265
+ * Call immediately before activation inside the same protected installation boundary;
266
+ * a separate preflight is not a substitute for preventing concurrent filesystem mutation.
267
+ */
268
+ export async function verifyBindingPaths(input: McpServer, roots: RuntimeRoots): Promise<void> {
269
+ const binding = resolveMcpBinding(input, roots)
270
+ if (binding.type !== 'stdio' || input.type !== 'stdio') return
271
+ const root = await realpath(roots.pluginRoot)
272
+ const data = await realpath(roots.pluginData)
273
+ if (root !== roots.pluginRoot || data !== roots.pluginData)
274
+ throw new Error('Supply canonical filesystem-resolved runtime roots.')
275
+ const cwd = await realpath(binding.cwd)
276
+ const expected = input.cwd?.startsWith('${PLUGIN_DATA}') ? data : root
277
+ if (!within(cwd, expected))
278
+ throw new Error('Working directory resolves outside its declared root.')
279
+ if (input.command.startsWith('./') && !within(await realpath(binding.command), root))
280
+ throw new Error('Executable resolves outside the package root.')
281
+ }
@@ -0,0 +1,27 @@
1
+ import { it } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { readFile } from 'node:fs/promises'
4
+ import { join } from 'node:path'
5
+ import { z } from 'zod'
6
+ import {
7
+ AgentPackageSchema,
8
+ HarnessProfileSchema,
9
+ InstallationPlanSchema,
10
+ } from './agent-plugins-schema.js'
11
+
12
+ // Run test commands from the repository root, like the repository's Bun scripts.
13
+ for (const [name, schema] of [
14
+ ['agent-package.v1', AgentPackageSchema],
15
+ ['harness-profile.v1', HarnessProfileSchema],
16
+ ['installation-plan.v2', InstallationPlanSchema],
17
+ ] as const) {
18
+ it(`keeps ${name} JSON Schema synchronized with the runtime contract`, async () => {
19
+ const artifact = JSON.parse(
20
+ await readFile(join(process.cwd(), 'schemas', `${name}.json`), 'utf8')
21
+ )
22
+ assert.deepEqual(artifact, {
23
+ ...z.toJSONSchema(schema, { target: 'draft-2020-12' }),
24
+ $id: `https://adea.dev/schemas/plugins/${name}.json`,
25
+ })
26
+ })
27
+ }