feat(plugins): add VM recipe packs

This commit is contained in:
Neil
2026-07-13 03:33:43 -07:00
parent 3bc87fedcf
commit ca38781c92
44 changed files with 1838 additions and 219 deletions
@@ -63,6 +63,46 @@ describe('fingerprintPluginConsent', () => {
expect(fingerprintPluginConsent({ main: undefined, capabilities })).toBe(legacy)
})
it('requires re-consent when instructional content bytes change', () => {
const subject = {
main: undefined,
capabilities: [],
contributes: {
skills: [],
keybindings: [],
vmRecipes: [{ path: 'recipes/cloud.json' }],
agents: []
}
}
const first = fingerprintPluginConsent(subject, 'a'.repeat(64))
const second = fingerprintPluginConsent(subject, 'b'.repeat(64))
expect(second).not.toBe(first)
expect(
getPluginActivationState('orca-samples.recipes', second, {
pluginConsents: { 'orca-samples.recipes': first },
disabledPlugins: []
})
).toBe('pending')
})
it('does not invalidate inert content consent when only its content hash changes', () => {
const subject = {
main: undefined,
capabilities: [],
contributes: {
skills: [],
keybindings: [],
vmRecipes: [],
agents: []
}
}
expect(fingerprintPluginConsent(subject, 'a'.repeat(64))).toBe(
fingerprintPluginConsent(subject, 'b'.repeat(64))
)
})
})
describe('plugin install lockfile consent fingerprints', () => {
@@ -2,22 +2,47 @@ import { createHash } from 'node:crypto'
import { canonicalizeCapabilitySet } from './plugin-capabilities'
import type { PluginManifest } from './plugin-manifest'
type PluginConsentSubject = Pick<PluginManifest, 'capabilities' | 'main'> & {
contributes?: Partial<
Pick<PluginManifest['contributes'], 'skills' | 'keybindings' | 'vmRecipes' | 'agents'>
>
}
export function hasInstructionalPluginContributions(manifest: PluginConsentSubject): boolean {
const contributions = manifest.contributes
return Boolean(
contributions &&
((contributions.skills?.length ?? 0) > 0 ||
(contributions.keybindings?.length ?? 0) > 0 ||
(contributions.vmRecipes?.length ?? 0) > 0 ||
(contributions.agents?.length ?? 0) > 0)
)
}
/**
* Consent covers both the declared host capabilities and whether the plugin
* executes trusted Node code. A panel-only update that adds `main` crosses a
* trust boundary even when its capability list is unchanged.
*/
export function canonicalizePluginConsent(
manifest: Pick<PluginManifest, 'capabilities' | 'main'>
manifest: PluginConsentSubject,
contentIdentity?: string
): string {
const capabilities = canonicalizeCapabilitySet(manifest.capabilities)
// Preserve the original capability-only digest for panel/content plugins,
// while making the trusted Node tier a domain-separated fingerprint.
return manifest.main === undefined ? capabilities : `${capabilities}\0trusted-node-worker`
const workerIdentity = manifest.main === undefined ? '' : '\0trusted-node-worker'
// Instructional bytes execute later under user or agent authority, so
// approval is bound to their immutable install/dev-tree identity.
const instructionalIdentity = hasInstructionalPluginContributions(manifest)
? `\0instructional-content:${contentIdentity ?? 'unresolved'}`
: ''
return `${capabilities}${workerIdentity}${instructionalIdentity}`
}
export function fingerprintPluginConsent(
manifest: Pick<PluginManifest, 'capabilities' | 'main'>
manifest: PluginConsentSubject,
contentIdentity?: string
): string {
return `sha256-${createHash('sha256').update(canonicalizePluginConsent(manifest)).digest('base64')}`
return `sha256-${createHash('sha256')
.update(canonicalizePluginConsent(manifest, contentIdentity))
.digest('base64')}`
}
@@ -33,19 +33,22 @@ describe('plugin language-pack artifacts', () => {
})
})
it('prevents language packs from rewriting plugin consent and trust copy', () => {
expect(
parsePluginLanguagePackArtifact(
JSON.stringify({
auto: {
components: {
settings: { PluginConsentDialog: { disclaimer: 'This plugin is perfectly safe.' } }
it.each(['PluginConsentDialog', 'PluginVmRecipeConsentPreview'])(
'prevents language packs from rewriting %s security copy',
(component) => {
expect(
parsePluginLanguagePackArtifact(
JSON.stringify({
auto: {
components: {
settings: { [component]: { disclaimer: 'This plugin is perfectly safe.' } }
}
}
}
})
)
).toMatchObject({ ok: false, error: expect.stringContaining('protected security copy') })
})
})
)
).toMatchObject({ ok: false, error: expect.stringContaining('protected security copy') })
}
)
it.each([
['array leaf', { settings: { choices: ['one'] } }],
@@ -7,6 +7,7 @@ const PROTECTED_TRANSLATION_PREFIXES = [
'auto.components.settings.PluginInstallDialog',
'auto.components.settings.PluginRemoveDialog',
'auto.components.settings.PluginSettingsRow',
'auto.components.settings.PluginVmRecipeConsentPreview',
'auto.components.settings.PluginsSettingsSection'
]
@@ -0,0 +1,79 @@
import { describe, expect, it } from 'vitest'
import {
listPluginVmRecipeCommands,
parsePluginVmRecipeArtifact
} from './plugin-vm-recipe-artifact'
describe('plugin VM recipe artifacts', () => {
it('parses bounded lifecycle commands for verbatim consent', () => {
const recipe = parsePluginVmRecipeArtifact(
JSON.stringify({
schemaVersion: 1,
id: 'cloud-sandbox',
name: 'Cloud Sandbox',
create: './scripts/create.sh',
suspend: './scripts/suspend.sh',
resume: './scripts/resume.sh',
destroy: './scripts/destroy.sh'
})
)
expect(recipe).toMatchObject({ id: 'cloud-sandbox', name: 'Cloud Sandbox' })
expect(listPluginVmRecipeCommands(recipe)).toEqual([
{ phase: 'create', command: './scripts/create.sh' },
{ phase: 'suspend', command: './scripts/suspend.sh' },
{ phase: 'resume', command: './scripts/resume.sh' },
{ phase: 'destroy', command: './scripts/destroy.sh' }
])
})
it('requires paired suspend/resume commands and rejects unknown fields', () => {
expect(() =>
parsePluginVmRecipeArtifact(
JSON.stringify({
schemaVersion: 1,
id: 'broken',
name: 'Broken',
create: 'create',
suspend: 'suspend'
})
)
).toThrow('suspend and resume')
expect(() =>
parsePluginVmRecipeArtifact(
JSON.stringify({
schemaVersion: 1,
id: 'broken',
name: 'Broken',
create: 'create',
environment: { SECRET: 'value' }
})
)
).toThrow()
})
it('represents an explicitly disabled destroy action without executing text', () => {
const recipe = parsePluginVmRecipeArtifact(
JSON.stringify({
schemaVersion: 1,
id: 'managed',
name: 'Managed',
create: 'create',
destroy: 'none'
})
)
expect(recipe).toMatchObject({ destroyDisabled: true })
expect(recipe.destroy).toBeUndefined()
})
it('rejects NUL bytes and commands beyond the bounded artifact contract', () => {
for (const create of ['bad\0command', 'x'.repeat(32 * 1024 + 1)]) {
expect(() =>
parsePluginVmRecipeArtifact(
JSON.stringify({ schemaVersion: 1, id: 'bounded', name: 'Bounded', create })
)
).toThrow()
}
})
})
@@ -0,0 +1,65 @@
import { z } from 'zod'
import { ORCA_VM_RECIPE_ID_PATTERN, ORCA_VM_RECIPE_ID_RULE } from '../orca-yaml'
import type { OrcaVmRecipe } from '../types'
const recipeCommandSchema = z
.string()
.trim()
.min(1)
.max(32 * 1024)
.refine((value) => !value.includes('\0'), 'must not contain NUL bytes')
const pluginVmRecipeArtifactSchema = z
.object({
schemaVersion: z.literal(1),
id: z.string().regex(ORCA_VM_RECIPE_ID_PATTERN, ORCA_VM_RECIPE_ID_RULE),
name: z.string().trim().min(1).max(128),
description: z.string().trim().min(1).max(1024).optional(),
create: recipeCommandSchema,
suspend: recipeCommandSchema.optional(),
resume: recipeCommandSchema.optional(),
destroy: z.union([recipeCommandSchema, z.literal('none')]).optional()
})
.strict()
.superRefine((recipe, ctx) => {
if (Boolean(recipe.suspend) !== Boolean(recipe.resume)) {
ctx.addIssue({
code: 'custom',
path: recipe.suspend ? ['resume'] : ['suspend'],
message: 'suspend and resume must be declared together'
})
}
})
export type PluginVmRecipeCommand = {
phase: 'create' | 'suspend' | 'resume' | 'destroy'
command: string
}
export function parsePluginVmRecipeArtifact(raw: string): OrcaVmRecipe {
const parsed = pluginVmRecipeArtifactSchema.parse(JSON.parse(raw))
const destroyDisabled = parsed.destroy === 'none'
return {
id: parsed.id,
name: parsed.name,
create: parsed.create,
...(parsed.description ? { description: parsed.description } : {}),
...(parsed.suspend ? { suspend: parsed.suspend } : {}),
...(parsed.resume ? { resume: parsed.resume } : {}),
...(parsed.destroy && !destroyDisabled ? { destroy: parsed.destroy } : {}),
...(destroyDisabled ? { destroyDisabled: true } : {})
}
}
export function listPluginVmRecipeCommands(recipe: OrcaVmRecipe): PluginVmRecipeCommand[] {
return [
{ phase: 'create', command: recipe.create },
...(recipe.suspend ? [{ phase: 'suspend' as const, command: recipe.suspend }] : []),
...(recipe.resume ? [{ phase: 'resume' as const, command: recipe.resume }] : []),
...(recipe.destroy
? [{ phase: 'destroy' as const, command: recipe.destroy }]
: recipe.destroyDisabled
? [{ phase: 'destroy' as const, command: 'none' }]
: [])
]
}