Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.)_
Expand Down
74 changes: 74 additions & 0 deletions packages/plugins/src/__tests__/ecosystem-runtime.test.ts
Original file line number Diff line number Diff line change
@@ -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')
})
})
10 changes: 10 additions & 0 deletions packages/plugins/src/ecosystem/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
90 changes: 90 additions & 0 deletions packages/plugins/src/ecosystem/runtime.ts
Original file line number Diff line number Diff line change
@@ -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<PluginRunResult>
}

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<PluginRunResult> {
// `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
})
}
26 changes: 24 additions & 2 deletions packages/plugins/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
Loading