refactor(plugins): name shared catalog and manifest schema parts (#30612)

* plugins: name the shared catalog and manifest schema parts

Pure refactor. Marketplace entry/header fields and the duplicate-category
and duplicate-id checks become named pieces, the bounded catalog JSON read
is split from schema parsing, and the v1 manifest object is named apart
from its refinement, so a second catalog file and manifest version can
reuse them.

* plugins: share the plugin tree-identity check

Pure refactor. Install integrity and instructional-content checks each
inlined the full-hash-or-legacy-prefix comparison; both now call
pluginTreeHashMatches. The tree limits are exported for reuse.
This commit is contained in:
Neil
2026-10-19 00:34:42 -07:00
committed by GitHub
parent 36ed236394
commit 5be871e537
6 changed files with 132 additions and 113 deletions
+2 -2
View File
@@ -12,8 +12,8 @@ import { pluginPathSegmentError } from '../../shared/plugins/plugin-path-safety'
* are refused outright (installed trees must be self-contained).
*/
const MAX_PLUGIN_FILES = 2_000
const MAX_PLUGIN_TOTAL_BYTES = 50 * 1024 * 1024
export const MAX_PLUGIN_FILES = 2_000
export const MAX_PLUGIN_TOTAL_BYTES = 50 * 1024 * 1024
type PluginFile = { path: string; size: number }
+7 -6
View File
@@ -7,6 +7,12 @@ export type HashAddressedPluginContent = {
export type PluginContentIntegrityResult = { ok: true } | { ok: false; error: string }
/** Whether a computed tree hash is the recorded identity. Early P0 installs used a
* 128-bit SHA-256 prefix as the directory name; new installs use the full digest. */
export function pluginTreeHashMatches(actual: string, expected: string): boolean {
return actual === expected || (expected.length === 32 && actual.startsWith(expected))
}
/** Dev trees are intentionally mutable; installed hash-addressed trees are not. */
export async function verifyHashAddressedPluginContent(
plugin: HashAddressedPluginContent
@@ -18,12 +24,7 @@ export async function verifyHashAddressedPluginContent(
if (!actual.ok) {
return { ok: false, error: actual.error }
}
const matchesCurrentHash = actual.hash === plugin.contentHash
// Early P0 installs used a 128-bit SHA-256 prefix as the directory name.
// Honor that existing address while all new installs use the full digest.
const matchesLegacyPrefix =
plugin.contentHash.length === 32 && actual.hash.startsWith(plugin.contentHash)
if (!matchesCurrentHash && !matchesLegacyPrefix) {
if (!pluginTreeHashMatches(actual.hash, plugin.contentHash)) {
return {
ok: false,
error: `content hash mismatch (expected ${plugin.contentHash}, got ${actual.hash})`
@@ -1,5 +1,6 @@
import { hasInstructionalPluginContributions } from '../../shared/plugins/plugin-consent-fingerprint'
import { hashPluginTree } from './plugin-content-hash'
import { pluginTreeHashMatches } from './plugin-content-integrity'
import type { ValidDiscoveredPlugin } from './plugin-discovery'
/** Instructional bytes execute later, so every read must still match the tree
@@ -19,10 +20,7 @@ export async function verifyInstructionalPluginContent(
`plugin ${plugin.pluginKey} instructional content is unreadable: ${actual.error}`
)
}
const matches =
actual.hash === plugin.consentContentHash ||
(plugin.consentContentHash.length === 32 && actual.hash.startsWith(plugin.consentContentHash))
if (!matches) {
if (!pluginTreeHashMatches(actual.hash, plugin.consentContentHash)) {
throw new Error(
`plugin ${plugin.pluginKey} instructional content changed since it was reviewed`
)
+15 -7
View File
@@ -40,24 +40,32 @@ export async function fetchPluginMarketplace(
export async function readPluginMarketplaceIndex(
rootDirectory: string
): Promise<PluginMarketplace> {
const path = join(rootDirectory, PLUGIN_MARKETPLACE_FILENAME)
const raw = await readMarketplaceJson(rootDirectory, PLUGIN_MARKETPLACE_FILENAME)
return parseMarketplaceFile(PLUGIN_MARKETPLACE_FILENAME, () => pluginMarketplaceSchema.parse(raw))
}
async function readMarketplaceJson(rootDirectory: string, filename: string): Promise<unknown> {
const chunks: Buffer[] = []
let totalBytes = 0
for await (const chunk of createReadStream(path)) {
for await (const chunk of createReadStream(join(rootDirectory, filename))) {
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
totalBytes += bytes.byteLength
if (totalBytes > MARKETPLACE_INDEX_MAX_BYTES) {
throw new Error(`${PLUGIN_MARKETPLACE_FILENAME} exceeds its size limit`)
throw new Error(`${filename} exceeds its size limit`)
}
chunks.push(bytes)
}
return parseMarketplaceFile(filename, () =>
JSON.parse(Buffer.concat(chunks, totalBytes).toString('utf8'))
)
}
function parseMarketplaceFile<T>(filename: string, parse: () => T): T {
try {
return pluginMarketplaceSchema.parse(
JSON.parse(Buffer.concat(chunks, totalBytes).toString('utf8'))
)
return parse()
} catch (error) {
throw new Error(
`invalid ${PLUGIN_MARKETPLACE_FILENAME}: ${error instanceof Error ? error.message : String(error)}`
`invalid ${filename}: ${error instanceof Error ? error.message : String(error)}`
)
}
}
+55 -56
View File
@@ -73,62 +73,61 @@ const eventContributionSchema = z.object({
on: z.enum(PLUGIN_EVENT_NAMES)
})
export const pluginManifestSchema = z
.object({
manifestVersion: z.literal(1),
id: pluginIdSchema,
/** Publisher slug; canonical identity is `<publisher>.<id>` — bare-id
* global uniqueness is unverifiable without a registry. */
publisher: pluginIdSchema,
name: z.string().min(1).max(256),
version: z.string().regex(SEMVER_RE, 'must be semver'),
description: z.string().max(4096).optional(),
author: z
.object({ name: z.string().min(1).max(256), url: z.string().max(2048).optional() })
.optional(),
repository: z.string().max(2048).optional(),
icon: pluginRelativePathSchema.optional(),
/** Minimum host version gate; the host refuses to load below it. */
engines: z.object({ orca: orcaEngineRangeSchema }),
/** Host-API major version this plugin targets. */
pluginApi: z.literal(1),
/** Node entry executed inside the out-of-process plugin worker. */
main: pluginRelativePathSchema.optional(),
contributes: z
.object({
panels: z.array(panelContributionSchema).max(PLUGIN_PANEL_LIMIT).default([]),
commands: z.array(commandContributionSchema).max(PLUGIN_COMMAND_LIMIT).default([]),
events: z.array(eventContributionSchema).max(PLUGIN_EVENT_SUBSCRIPTION_LIMIT).default([]),
languagePacks: z
.array(pluginLanguagePackContributionSchema)
.max(PLUGIN_LANGUAGE_PACK_LIMIT)
.default([]),
keybindings: z
.array(pluginKeybindingContributionSchema)
.max(PLUGIN_KEYBINDING_LIMIT)
.default([]),
vmRecipes: z
.array(pluginVmRecipeContributionSchema)
.max(PLUGIN_VM_RECIPE_LIMIT)
.default([]),
agents: z
.array(pluginAgentProfileContributionSchema)
.max(PLUGIN_AGENT_PROFILE_LIMIT)
.default([])
})
.strict()
.default(() => ({
panels: [],
commands: [],
events: [],
languagePacks: [],
keybindings: [],
vmRecipes: [],
agents: []
})),
capabilities: z.array(pluginCapabilitySchema).max(32).default([])
})
.superRefine(validatePluginManifestContributions)
const pluginManifestV1Object = z.object({
manifestVersion: z.literal(1),
id: pluginIdSchema,
/** Publisher slug; canonical identity is `<publisher>.<id>` — bare-id
* global uniqueness is unverifiable without a registry. */
publisher: pluginIdSchema,
name: z.string().min(1).max(256),
version: z.string().regex(SEMVER_RE, 'must be semver'),
description: z.string().max(4096).optional(),
author: z
.object({ name: z.string().min(1).max(256), url: z.string().max(2048).optional() })
.optional(),
repository: z.string().max(2048).optional(),
icon: pluginRelativePathSchema.optional(),
/** Minimum host version gate; the host refuses to load below it. */
engines: z.object({ orca: orcaEngineRangeSchema }),
/** Host-API major version this plugin targets. */
pluginApi: z.literal(1),
/** Node entry executed inside the out-of-process plugin worker. */
main: pluginRelativePathSchema.optional(),
contributes: z
.object({
panels: z.array(panelContributionSchema).max(PLUGIN_PANEL_LIMIT).default([]),
commands: z.array(commandContributionSchema).max(PLUGIN_COMMAND_LIMIT).default([]),
events: z.array(eventContributionSchema).max(PLUGIN_EVENT_SUBSCRIPTION_LIMIT).default([]),
languagePacks: z
.array(pluginLanguagePackContributionSchema)
.max(PLUGIN_LANGUAGE_PACK_LIMIT)
.default([]),
keybindings: z
.array(pluginKeybindingContributionSchema)
.max(PLUGIN_KEYBINDING_LIMIT)
.default([]),
vmRecipes: z.array(pluginVmRecipeContributionSchema).max(PLUGIN_VM_RECIPE_LIMIT).default([]),
agents: z
.array(pluginAgentProfileContributionSchema)
.max(PLUGIN_AGENT_PROFILE_LIMIT)
.default([])
})
.strict()
.default(() => ({
panels: [],
commands: [],
events: [],
languagePacks: [],
keybindings: [],
vmRecipes: [],
agents: []
})),
capabilities: z.array(pluginCapabilitySchema).max(32).default([])
})
export const pluginManifestSchema = pluginManifestV1Object.superRefine(
validatePluginManifestContributions
)
export type PluginManifest = z.infer<typeof pluginManifestSchema>
export type PluginPanelContribution = z.infer<typeof panelContributionSchema>
+51 -38
View File
@@ -53,50 +53,63 @@ export const pluginMarketplaceGitSourceSchema = z.strictObject({
ref: z.string().trim().min(1).max(4_096)
})
export const pluginMarketplaceEntrySchema = z
.strictObject({
/** Canonical `<publisher>.<id>` identity expected in the source manifest. */
id: z.string().refine(isQualifiedPluginKey, 'invalid qualified plugin key'),
source: pluginMarketplaceGitSourceSchema,
description: z.string().min(1).max(4_096).optional(),
categories: z
.array(marketplaceCategorySchema)
.max(PLUGIN_MARKETPLACE_CATEGORY_LIMIT)
.default([])
})
.superRefine((entry, context) => {
const seen = new Set<string>()
for (const [index, category] of entry.categories.entries()) {
if (seen.has(category)) {
context.addIssue({
code: z.ZodIssueCode.custom,
path: ['categories', index],
message: `duplicate category: ${category}`
})
}
seen.add(category)
const marketplaceEntryFields = {
/** Canonical `<publisher>.<id>` identity expected in the source manifest. */
id: z.string().refine(isQualifiedPluginKey, 'invalid qualified plugin key'),
source: pluginMarketplaceGitSourceSchema,
description: z.string().min(1).max(4_096).optional(),
categories: z.array(marketplaceCategorySchema).max(PLUGIN_MARKETPLACE_CATEGORY_LIMIT).default([])
}
function rejectDuplicateCategories(
entry: { categories: string[] },
context: z.RefinementCtx
): void {
const seen = new Set<string>()
for (const [index, category] of entry.categories.entries()) {
if (seen.has(category)) {
context.addIssue({
code: z.ZodIssueCode.custom,
path: ['categories', index],
message: `duplicate category: ${category}`
})
}
})
seen.add(category)
}
}
function rejectDuplicatePluginIds(
marketplace: { plugins: { id: string }[] },
context: z.RefinementCtx
): void {
const seen = new Set<string>()
for (const [index, plugin] of marketplace.plugins.entries()) {
if (seen.has(plugin.id)) {
context.addIssue({
code: z.ZodIssueCode.custom,
path: ['plugins', index, 'id'],
message: `duplicate plugin id: ${plugin.id}`
})
}
seen.add(plugin.id)
}
}
export const pluginMarketplaceEntrySchema = z
.strictObject(marketplaceEntryFields)
.superRefine(rejectDuplicateCategories)
const marketplaceHeaderFields = {
name: z.string().min(1).max(256),
owner: marketplaceOwnerSchema
}
export const pluginMarketplaceSchema = z
.strictObject({
name: z.string().min(1).max(256),
owner: marketplaceOwnerSchema,
...marketplaceHeaderFields,
plugins: z.array(pluginMarketplaceEntrySchema).max(PLUGIN_MARKETPLACE_ENTRY_LIMIT)
})
.superRefine((marketplace, context) => {
const seen = new Set<string>()
for (const [index, plugin] of marketplace.plugins.entries()) {
if (seen.has(plugin.id)) {
context.addIssue({
code: z.ZodIssueCode.custom,
path: ['plugins', index, 'id'],
message: `duplicate plugin id: ${plugin.id}`
})
}
seen.add(plugin.id)
}
})
.superRefine(rejectDuplicatePluginIds)
/** Host-derived trust metadata. Marketplace JSON cannot self-award either bit. */
export const pluginMarketplaceTrustMetadataSchema = z