From a046e0c52a5b7d7a25268da612d69208bd4902d4 Mon Sep 17 00:00:00 2001 From: xNet Test Date: Wed, 17 Jun 2026 14:05:56 -0700 Subject: [PATCH] feat(plugins): run plugin code on the labs runtime ladder + surface ecosystem API (0194 Phase 1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ecosystem/runtime.ts: runPluginCode(ladder, {code, trustTier, …}) + ladderTierForTrust route user→sandbox (SES/QuickJS) and marketplace→app (iframe), and reject first-party (host realm only). The ladder is a structural port (PluginRuntimeLadder), not an @xnetjs/labs import — no plugins→labs cycle. runPluginCode is async so a first-party rejection surfaces as a rejected promise. - index: backfill the public API — surface runAiPluginPipeline (Phase 2), recommendExtensions (Phase 4), and the new runtime exports from the main package entry (they were added to the ecosystem barrel in #149 but never re-exported from @xnetjs/plugins). 6 new tests; plugins suite 482 green; typecheck/eslint/prettier/fallow clean. The registry switch to this runtime + the perf benchmark remain. Co-Authored-By: Claude Opus 4.8 --- ...NSIBILITY_FABRIC_PLUGINS_LABS_AI_EDITOR.md | 11 ++- .../src/__tests__/ecosystem-runtime.test.ts | 74 +++++++++++++++ packages/plugins/src/ecosystem/index.ts | 10 +++ packages/plugins/src/ecosystem/runtime.ts | 90 +++++++++++++++++++ packages/plugins/src/index.ts | 26 +++++- 5 files changed, 206 insertions(+), 5 deletions(-) create mode 100644 packages/plugins/src/__tests__/ecosystem-runtime.test.ts create mode 100644 packages/plugins/src/ecosystem/runtime.ts diff --git a/docs/explorations/0194_[_]_EXTENSIBILITY_FABRIC_PLUGINS_LABS_AI_EDITOR.md b/docs/explorations/0194_[_]_EXTENSIBILITY_FABRIC_PLUGINS_LABS_AI_EDITOR.md index 100b1c0f7..dbe3c7f76 100644 --- a/docs/explorations/0194_[_]_EXTENSIBILITY_FABRIC_PLUGINS_LABS_AI_EDITOR.md +++ b/docs/explorations/0194_[_]_EXTENSIBILITY_FABRIC_PLUGINS_LABS_AI_EDITOR.md @@ -516,10 +516,15 @@ classDiagram `PluginTrustTier`/`InstallProvenance`/`SandboxKind` preserved as aliases of the shared types; `LabTrustTier` (in `labs/runtime/types.ts`) aliased to the shared `TrustTier`. labs (46) + plugins (452) suites unchanged & green._ -- [ ] Add `packages/plugins/src/ecosystem/runtime.ts`: run user/marketplace-tier +- [~] Add `packages/plugins/src/ecosystem/runtime.ts`: run user/marketplace-tier plugin code on the labs `RuntimeLadder`; first-party stays host-realm. - _(deferred — needs the benchmark below + a port to avoid the `plugins→labs` - cycle, since labs already depends on plugins.)_ + _As-built: `runPluginCode(ladder, {code, trustTier, …})` + `ladderTierForTrust` + route `user`→`sandbox` (SES/QuickJS) and `marketplace`→`app` (iframe), and + **reject first-party** (host realm only). The ladder is a structural port + (`PluginRuntimeLadder`), not an `@xnetjs/labs` import, so there's no + `plugins→labs` cycle — the host passes its concrete ladder. The registry + *switch* to this (replacing the current sandbox) + the benchmark below are + the remaining work._ - [ ] Benchmark plugin activation + a representative editor interaction against 0184 budgets; gate the runtime switch on no regression. _(deferred with the runtime switch above.)_ diff --git a/packages/plugins/src/__tests__/ecosystem-runtime.test.ts b/packages/plugins/src/__tests__/ecosystem-runtime.test.ts new file mode 100644 index 000000000..bd096b2fa --- /dev/null +++ b/packages/plugins/src/__tests__/ecosystem-runtime.test.ts @@ -0,0 +1,74 @@ +/** + * Tests for running plugin code on the labs runtime ladder (0194 Phase 1). + */ + +import { describe, it, expect, vi } from 'vitest' +import { + ladderTierForTrust, + runPluginCode, + PluginRuntimeError, + type PluginRuntimeLadder, + type PluginRunInput +} from '../ecosystem/runtime' + +function fakeLadder() { + const inputs: PluginRunInput[] = [] + const ladder: PluginRuntimeLadder = { + run: vi.fn(async (input: PluginRunInput) => { + inputs.push(input) + return { ok: true, value: 'ran' } + }) + } + return { ladder, inputs } +} + +describe('ladderTierForTrust', () => { + it('maps user → sandbox and marketplace → app', () => { + expect(ladderTierForTrust('user')).toBe('sandbox') + expect(ladderTierForTrust('marketplace')).toBe('app') + }) + + it('throws for first-party (it runs in the host realm, not the ladder)', () => { + expect(() => ladderTierForTrust('first-party')).toThrow(PluginRuntimeError) + }) +}) + +describe('runPluginCode', () => { + it('runs user code on the deterministic sandbox rung', async () => { + const { ladder, inputs } = fakeLadder() + const result = await runPluginCode(ladder, { code: 'return 1', trustTier: 'user' }) + expect(result).toEqual({ ok: true, value: 'ran' }) + expect(inputs[0]).toEqual({ + language: 'javascript', + tier: 'sandbox', + code: 'return 1', + host: undefined + }) + }) + + it('runs marketplace code on the iframe app rung and forwards the host bridge', async () => { + const { ladder, inputs } = fakeLadder() + const host = { tools: {} } + await runPluginCode(ladder, { + code: 'x', + trustTier: 'marketplace', + language: 'typescript', + host + }) + expect(inputs[0]).toMatchObject({ tier: 'app', language: 'typescript', host }) + }) + + it('refuses first-party code (host realm only) without calling the ladder', async () => { + const { ladder } = fakeLadder() + await expect( + runPluginCode(ladder, { code: 'x', trustTier: 'first-party' }) + ).rejects.toBeInstanceOf(PluginRuntimeError) + expect(ladder.run).not.toHaveBeenCalled() + }) + + it('defaults the language to javascript', async () => { + const { ladder, inputs } = fakeLadder() + await runPluginCode(ladder, { code: 'x', trustTier: 'user' }) + expect(inputs[0].language).toBe('javascript') + }) +}) diff --git a/packages/plugins/src/ecosystem/index.ts b/packages/plugins/src/ecosystem/index.ts index 3c7d42429..4cdab6731 100644 --- a/packages/plugins/src/ecosystem/index.ts +++ b/packages/plugins/src/ecosystem/index.ts @@ -84,6 +84,16 @@ export type { AiAuthoredPlugin } from './ai-authoring' +// Run plugin code on the labs runtime ladder (0194 Phase 1) — port-based. +export { ladderTierForTrust, runPluginCode, PluginRuntimeError } from './runtime' +export type { + LadderRuntimeTier, + PluginRunInput, + PluginRunResult, + PluginRuntimeLadder, + RunPluginCodeInput +} from './runtime' + // AI→Lab→Plugin assembly line (0194 Phase 2) — generate → lab-test → consent → publish. export { runAiPluginPipeline } from './ai-pipeline' export type { diff --git a/packages/plugins/src/ecosystem/runtime.ts b/packages/plugins/src/ecosystem/runtime.ts new file mode 100644 index 000000000..c998aa2bf --- /dev/null +++ b/packages/plugins/src/ecosystem/runtime.ts @@ -0,0 +1,90 @@ +/** + * @xnetjs/plugins — run plugin code on the labs runtime ladder (0194 Phase 1). + * + * The unification the exploration calls for: instead of plugins maintaining their + * own sandbox, user/marketplace-tier plugin code runs on the *same* runtime + * ladder `@xnetjs/labs` uses (SES/QuickJS for `sandbox`, an iframe for `app`). + * One sandbox, one security audit — and plugins gain the ladder's Python/server + * tiers for free. + * + * The ladder is taken as a **structural port** (`PluginRuntimeLadder`), not an + * `@xnetjs/labs` import: labs already depends on `@xnetjs/plugins`, so a direct + * edge here would cycle. The host (web/electron) passes its concrete labs ladder. + * + * First-party code is trusted and runs in the host realm, NOT through the ladder + * — `runPluginCode` rejects a first-party tier so a caller can't accidentally + * sandbox (and slow) trusted code. + */ + +import type { TrustTier } from '@xnetjs/trust' + +/** The labs runtime tiers a plugin can target. */ +export type LadderRuntimeTier = 'sandbox' | 'app' | 'server' + +/** A single run on the ladder. */ +export interface PluginRunInput { + language: 'javascript' | 'typescript' + tier: LadderRuntimeTier + code: string + /** Host bridge the sandbox may call (capability-gated by the host). */ + host?: unknown +} + +export interface PluginRunResult { + ok: boolean + value?: unknown + logs?: string[] + error?: string +} + +/** The minimal slice of the labs `RuntimeLadder` this adapter needs. */ +export interface PluginRuntimeLadder { + run(input: PluginRunInput): Promise +} + +export class PluginRuntimeError extends Error { + constructor(message: string) { + super(message) + this.name = 'PluginRuntimeError' + } +} + +/** + * Map a plugin's trust tier to the ladder rung its code should run on. `user` + * code runs in the deterministic `sandbox` (SES/QuickJS); `marketplace` code in + * the `app` (iframe) rung. `first-party` has no ladder rung — it runs in the + * host realm — so this throws for it. + */ +export function ladderTierForTrust(tier: TrustTier): LadderRuntimeTier { + if (tier === 'first-party') { + throw new PluginRuntimeError('first-party plugin code runs in the host realm, not the ladder') + } + return tier === 'marketplace' ? 'app' : 'sandbox' +} + +export interface RunPluginCodeInput { + code: string + trustTier: TrustTier + language?: 'javascript' | 'typescript' + host?: unknown +} + +/** + * Run user/marketplace-tier plugin code on the labs ladder, choosing the rung by + * trust tier. Throws `PluginRuntimeError` for first-party (which belongs in the + * host realm). Returns the ladder's result unchanged. + */ +export async function runPluginCode( + ladder: PluginRuntimeLadder, + input: RunPluginCodeInput +): Promise { + // `async` so a first-party rejection surfaces as a rejected promise rather than + // a synchronous throw (callers `await` this). + const tier = ladderTierForTrust(input.trustTier) + return ladder.run({ + language: input.language ?? 'javascript', + tier, + code: input.code, + host: input.host + }) +} diff --git a/packages/plugins/src/index.ts b/packages/plugins/src/index.ts index 0a1d59827..01f41d76a 100644 --- a/packages/plugins/src/index.ts +++ b/packages/plugins/src/index.ts @@ -220,7 +220,15 @@ export { ScaffoldError, // AI-authored plugin transform scriptToPluginManifest, - AiAuthoringError + AiAuthoringError, + // Plugin runtime on the labs ladder (0194 Phase 1) + ladderTierForTrust, + runPluginCode, + PluginRuntimeError, + // AI→Lab→Plugin pipeline (0194 Phase 2) + runAiPluginPipeline, + // Marketplace recommendations (0194 Phase 4) + recommendExtensions } from './ecosystem' export type { InstallProvenance, @@ -251,7 +259,21 @@ export type { GeneratedScript, ScriptExecutor, ScriptToManifestInput, - AiAuthoredPlugin + AiAuthoredPlugin, + // Plugin runtime (0194 Phase 1) + LadderRuntimeTier, + PluginRunInput, + PluginRunResult, + PluginRuntimeLadder, + RunPluginCodeInput, + // AI→Lab→Plugin pipeline (0194 Phase 2) + LabRunOutcome, + AiPluginPipelinePorts, + AiPluginPipelineInput, + AiPluginPipelineResult, + // Marketplace recommendations (0194 Phase 4) + UsageSignal, + RecommendOptions } from './ecosystem' // Schemas