From 919ae3870f1182b30bd51b36a4fc11f13b6930ea Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Fri, 25 Sep 2026 05:50:35 +0000 Subject: [PATCH 1/3] Remove the development runtime App-asset path --- .changeset/remove-runtime-app-assets.md | 5 + examples/rsc-agent-runtime/README.md | 19 +- examples/rsc-agent-runtime/rsbuild.config.ts | 35 +-- .../src/dev/environment-checkpoint-store.ts | 4 +- .../src/dev/generation-materializer.ts | 162 +--------- .../src/dev/rsbuild-runtime-session.ts | 103 +------ .../src/runtime/contracts.ts | 16 - .../tests/dev-provider.integration.test.ts | 290 +++--------------- .../environment-checkpoint-store.test.ts | 9 +- .../tests/generation-materializer.test.ts | 288 +---------------- .../tests/rsbuild-config.test.ts | 7 +- .../tests/support/compiler-cohort.ts | 9 - packages/agent-bundle/src/api.ts | 2 - packages/agent-bundle/src/dev/index.ts | 2 - .../agent-bundle/src/dev/project-service.ts | 27 +- .../src/dev/runtime-controller.ts | 18 +- .../agent-bundle/src/dev/runtime-protocol.ts | 13 +- .../agent-bundle/src/dev/runtime-provider.ts | 23 -- .../agent-bundle/src/dev/runtime-routes.ts | 18 +- .../agent-bundle/src/dev/workbench-server.ts | 1 - .../agent-bundle/tests/dev-services.test.ts | 24 +- .../agent-bundle/tests/dev-workbench.test.ts | 5 +- .../tests/runtime-provider.test.ts | 94 +++--- .../agent-bundle/tests/runtime-routes.test.ts | 10 +- packages/workbench/src/runtime-client.ts | 29 +- .../workbench/tests/runtime-backend.test.ts | 1 - .../workbench/tests/runtime-client.test.ts | 27 +- .../tests/runtime-contract-compile.test.ts | 11 +- .../tests/runtime-controller.test.ts | 1 - .../workbench/tests/runtime-model.test.ts | 14 +- 30 files changed, 143 insertions(+), 1124 deletions(-) create mode 100644 .changeset/remove-runtime-app-assets.md diff --git a/.changeset/remove-runtime-app-assets.md b/.changeset/remove-runtime-app-assets.md new file mode 100644 index 000000000..0514439c8 --- /dev/null +++ b/.changeset/remove-runtime-app-assets.md @@ -0,0 +1,5 @@ +--- +'agent-bundle': minor +--- + +Remove the development runtime App-asset path: `DevRuntimeSession.readAsset`, the `/api/runtime/assets` route (now a 400 invalid path), the `DevRuntimeAssetRequest` and `DevRuntimePreparedMcpApp` exports, the prepared-runtime `apps` input, `DevRuntimeStatus.hmrReady`, the `mcp-app` runtime surface kind, the `mcp-protocol`, `resource-selection`, `sandbox/csp`, and `app-bridge` diagnostic phases, and the launch and credential fields on `DevRuntimePreparedMcpServer`, which is now `{ id, name, targets }`. diff --git a/examples/rsc-agent-runtime/README.md b/examples/rsc-agent-runtime/README.md index 9952fb004..b976636f5 100644 --- a/examples/rsc-agent-runtime/README.md +++ b/examples/rsc-agent-runtime/README.md @@ -82,16 +82,15 @@ production RSC/runtime artifacts are built by its explicit Rsbuild production command (`pnpm --filter @agent-bundle/rsc-agent-runtime-demo build`); its provider uses a separate long-lived Rsbuild development session only when an `agent-bundle dev` project opts into `dev.runtime.provider`. That session -compiles each change into a runtime generation, publishes its hook, MCP tool, -resource, and App surfaces to the Workbench runtime routes, and serves -generation assets through them; no browser connects to its loopback Rsbuild -server. The session uses development entries and output roots while compiling every -environment in production mode: production decoders cannot read development -Flight payloads, and Rsbuild only inlines the App's scripts and styles in -production mode. `@rsbuild/plugin-react` is configured as -`pluginReact({ fastRefresh: false })` and the App environment sets -`hmr: false`: the self-contained App document never receives a browser HMR -credential or connection. The `widget` and `app` web environments set +compiles the `rsc` and `widget` environments into a runtime generation and +publishes its hook, MCP tool, and resource surfaces to the Workbench runtime +routes; no browser connects to its loopback Rsbuild server. The `app` +environment is compiled only by the production build. The session uses +development entries and output roots while compiling every environment in +production mode, because production decoders cannot read development Flight +payloads. `@rsbuild/plugin-react` is configured as +`pluginReact({ fastRefresh: false })`, so no refresh runtime is injected into +the self-contained App document. The `widget` and `app` web environments set `overrideBrowserslist: ['chrome >= 144']` for the Chromium MCP App hosts (Cursor 3.18.25 still ships Chromium 144; Claude Desktop Electron 42 and Cursor 3.19.7 ship Chromium 148; ChatGPT/Codex Desktop reports Chromium 151). diff --git a/examples/rsc-agent-runtime/rsbuild.config.ts b/examples/rsc-agent-runtime/rsbuild.config.ts index 6773841cb..50abf4208 100644 --- a/examples/rsc-agent-runtime/rsbuild.config.ts +++ b/examples/rsc-agent-runtime/rsbuild.config.ts @@ -62,10 +62,10 @@ export interface RscRuntimeCompileSnapshot { export type RscRuntimeActivationOutcome = 'activated' | 'failed'; export type RscRuntimeCompileFailureKind = 'provider-lifecycle' | 'source-build'; -export type RscRuntimeCompileEnvironmentName = 'app' | 'rsc' | 'widget'; +export type RscRuntimeCompileEnvironmentName = 'rsc' | 'widget'; export type RscRuntimeCompileEnvironmentHashes = Readonly>; -const compileEnvironmentNames: readonly RscRuntimeCompileEnvironmentName[] = Object.freeze(['app', 'rsc', 'widget'] as const); +const compileEnvironmentNames: readonly RscRuntimeCompileEnvironmentName[] = Object.freeze(['rsc', 'widget'] as const); const isCompileEnvironmentName = (value: string): value is RscRuntimeCompileEnvironmentName => (compileEnvironmentNames as readonly string[]).includes(value); @@ -296,7 +296,7 @@ export const createRscRuntimeRsbuildConfig = ( if (development && options.compilerRoot === undefined) { throw new TypeError('Development RSC runtime config requires compilerRoot.'); } - const root = (name: 'rsc' | 'widget' | 'app', productionRoot: string): string => + const root = (name: 'rsc' | 'widget', productionRoot: string): string => development ? join(options.compilerRoot as string, name) : productionRoot; return { @@ -322,7 +322,7 @@ export const createRscRuntimeRsbuildConfig = ( pluginReact(rscRuntimeReactPluginOptions), pluginRSC({ environments: { server: 'rsc', client: 'widget' } }), emitRuntimeManifest(), - selfContainedAppPlugin(), + ...(development ? [] : [selfContainedAppPlugin()]), ...(options.onCompile === undefined ? [] : [runtimeCompileObserverPlugin(options.onCompile)]), ], environments: { @@ -383,15 +383,7 @@ export const createRscRuntimeRsbuildConfig = ( rspack: { name: 'widget' }, }, }, - app: { - ...(development ? { - dev: { - // The development session serves no browser client; the compiled - // App must never receive a browser HMR credential or connection. - hmr: false, - liveReload: false, - }, - } : {}), + ...(development ? {} : { app: { html: { inject: 'body' }, // Self-contained documents, asserted by `selfContainedAppPlugin`: every // script, style, licence comment, and asset of any size is inlined, @@ -399,17 +391,12 @@ export const createRscRuntimeRsbuildConfig = ( output: { cleanDistPath: false, dataUriLimit: Number.MAX_SAFE_INTEGER, - distPath: { - ...(development ? {} : { js: './' }), - root: root('app', 'dist/app'), + distPath: { js: './', root: 'dist/app' }, + filename: { + assets: '[name][ext]', + css: '[name].css', + js: '[name].js', }, - ...(development ? {} : { - filename: { - assets: '[name][ext]', - css: '[name].css', - js: '[name].js', - }, - }), filenameHash: false, inlineScripts: true, inlineStyles: true, @@ -431,7 +418,7 @@ export const createRscRuntimeRsbuildConfig = ( output: { asyncChunks: false }, }, }, - }, + } }), }, }; }; diff --git a/examples/rsc-agent-runtime/src/dev/environment-checkpoint-store.ts b/examples/rsc-agent-runtime/src/dev/environment-checkpoint-store.ts index 65685fc4e..736be211b 100644 --- a/examples/rsc-agent-runtime/src/dev/environment-checkpoint-store.ts +++ b/examples/rsc-agent-runtime/src/dev/environment-checkpoint-store.ts @@ -24,10 +24,10 @@ import { copyTree, digestBytes, writeFileDurably } from './durable-tree.js'; * hash. */ -export type RscRuntimeEnvironmentName = 'app' | 'rsc' | 'widget'; +export type RscRuntimeEnvironmentName = 'rsc' | 'widget'; export const rscRuntimeEnvironmentNames: readonly RscRuntimeEnvironmentName[] = - Object.freeze(['app', 'rsc', 'widget'] as const); + Object.freeze(['rsc', 'widget'] as const); export type RscEnvironmentCohortHashes = Readonly>; diff --git a/examples/rsc-agent-runtime/src/dev/generation-materializer.ts b/examples/rsc-agent-runtime/src/dev/generation-materializer.ts index a1f08bd2c..8046cf2e2 100644 --- a/examples/rsc-agent-runtime/src/dev/generation-materializer.ts +++ b/examples/rsc-agent-runtime/src/dev/generation-materializer.ts @@ -11,9 +11,7 @@ import type { RscStagedEnvironmentCheckpoint, } from './environment-checkpoint-store.js'; import type { - RscRuntimeAppDefinition, RscRuntimeGenerationMetadata, - RscRuntimeSurfaceAsset, SerializedRuntimeDefinition, } from '../runtime/contracts.js'; import type { JsonObject, JsonValue } from 'agent-bundle'; @@ -29,7 +27,7 @@ import type { RuntimeGenerationValidationInput, } from 'agent-bundle/api'; -export type { RscRuntimeGenerationMetadata, RscRuntimeSurfaceAsset } from '../runtime/contracts.js'; +export type { RscRuntimeGenerationMetadata } from '../runtime/contracts.js'; const definitionFile = 'rsc/runtime-definition.json'; const runtimeAssetsFile = 'rsc/runtime-assets.json'; @@ -453,109 +451,6 @@ const validateClientReferenceRelationship = async ( } }; -const contentTypeFor = (path: string): RscRuntimeSurfaceAsset['contentType'] | undefined => { - if (path.endsWith('.js')) return 'application/javascript'; - if (path.endsWith('.json')) return 'application/json'; - if (path.endsWith('.css')) return 'text/css'; - if (path.endsWith('.html')) return 'text/html'; - return undefined; -}; - -const surfaceAssets = ( - preparedRuntime: DevRuntimePreparedProject, - assets: readonly RuntimeGenerationAsset[], -): Readonly> => { - const widgetAssets = assets.flatMap((asset): RscRuntimeSurfaceAsset[] => { - if (!asset.path.startsWith('widget/')) return []; - const contentType = contentTypeFor(asset.path); - if (contentType === undefined) return []; - const requestPath = asset.path.slice('widget'.length); - return [Object.freeze({ - bytes: asset.bytes, - contentType, - generationPath: asset.path, - requestPath, - sha256: asset.sha256, - })]; - }); - const appHtmlAssets = assets.flatMap((asset): RscRuntimeSurfaceAsset[] => { - if (!asset.path.startsWith('app/') || !asset.path.endsWith('.html')) return []; - return [Object.freeze({ - bytes: asset.bytes, - contentType: 'text/html', - generationPath: asset.path, - requestPath: asset.path.slice('app'.length), - sha256: asset.sha256, - })]; - }); - const surfaces: Record = {}; - for (const app of preparedRuntime.apps) { - const surfaceId = `mcp.${app.name}`; - if (surfaces[surfaceId] !== undefined) throw new Error('Runtime generation has duplicate App surface definitions.'); - const resourcePath = appResourcePath(app.resourceUri); - const html = appHtmlAssets.filter((asset) => asset.requestPath === resourcePath); - if (html.length !== 1) throw new Error(`Runtime generation App ${JSON.stringify(app.resourceUri)} has no unique captured HTML asset.`); - surfaces[surfaceId] = Object.freeze([ - ...widgetAssets.map((asset) => Object.freeze({ ...asset })), - Object.freeze({ ...html[0]! }), - ]); - } - return Object.freeze(surfaces); -}; - -const appResourcePath = (uri: string): string => { - let parsed: URL; - try { - parsed = new URL(uri); - } catch { - throw new TypeError('Runtime generation App resource URI is invalid.'); - } - if (parsed.protocol !== 'ui:' || parsed.host.length === 0 || parsed.search.length > 0 || parsed.hash.length > 0) { - throw new TypeError('Runtime generation App resource URI is invalid.'); - } - const origin = `ui://${parsed.host}`; - if (!uri.startsWith(origin)) throw new TypeError('Runtime generation App resource URI is invalid.'); - const path = uri.slice(origin.length); - const segments = path.startsWith('/') ? path.slice(1).split('/') : []; - if (segments.length === 0 || segments.some((segment) => !isSafeSegment(segment) || decodeURIComponent(segment) !== segment)) { - throw new TypeError('Runtime generation App resource URI is invalid.'); - } - return `/${segments.join('/')}`; -}; - -const validateAppSurfaceAssets = ( - apps: readonly RscRuntimeAppDefinition[], - surfaces: Readonly>, -): void => { - const expected = new Set(); - for (const app of apps) { - const surfaceId = `mcp.${app.name}`; - if (expected.has(surfaceId)) throw new TypeError('Runtime generation has duplicate App surface definitions.'); - expected.add(surfaceId); - const resourcePath = appResourcePath(app.resourceUri); - const appHtml = surfaces[surfaceId]?.filter((asset) => - asset.contentType === 'text/html' && asset.generationPath.startsWith('app/'), - ) ?? []; - if (appHtml.length !== 1 || appHtml[0]!.generationPath !== `app${resourcePath}` || appHtml[0]!.requestPath !== resourcePath) { - throw new TypeError(`Runtime generation App ${JSON.stringify(app.resourceUri)} has no canonical captured HTML asset.`); - } - } - if (Object.keys(surfaces).length !== expected.size || Object.keys(surfaces).some((surfaceId) => !expected.has(surfaceId))) { - throw new TypeError('Runtime generation App surface assets are not owned by App definitions.'); - } -}; - -const appDefinitions = (preparedRuntime: DevRuntimePreparedProject): readonly RscRuntimeAppDefinition[] => - freezeJson(preparedRuntime.apps.map((app) => ({ - id: app.id, - name: app.name, - resourceUri: app.resourceUri, - })).sort((left, right) => { - const leftJson = canonicalJson(left); - const rightJson = canonicalJson(right); - return leftJson < rightJson ? -1 : leftJson > rightJson ? 1 : 0; - })) as unknown as readonly RscRuntimeAppDefinition[]; - const metadataFromSnapshot = async ( snapshot: RscRuntimeCapturedGenerationSnapshot, assets: readonly RuntimeGenerationAsset[], @@ -577,10 +472,8 @@ const metadataFromSnapshot = async ( return [entry, `rsc/${path}`]; }))); return Object.freeze({ - appDefinitions: appDefinitions(snapshot.preparedRuntime), entries, stateStoreId, - surfaceAssets: surfaceAssets(snapshot.preparedRuntime, assets), }); }; @@ -594,7 +487,7 @@ export const captureRuntimeGenerationSnapshot = async ( // live compiler roots, which the next parallel compile may already be // rewriting. The definition executable and the generated definition // artifacts likewise run against and land in the candidate's own copy. - const { app, rsc, widget } = input.cohort; + const { rsc, widget } = input.cohort; const candidateRsc = join(input.candidate.root, 'rsc'); const runtimeAssets = await parseRuntimeAssets(rsc.root); await copyDeclaredRscAssets(rsc, runtimeAssets, candidateRsc); @@ -602,7 +495,6 @@ export const captureRuntimeGenerationSnapshot = async ( await writeFileDurably(join(candidateRsc, 'runtime-definition.json'), Buffer.from(canonicalJson(definition))); await emitRuntimeArtifacts(candidateRsc, definition); await fsyncPath(candidateRsc); - await copyCheckpointTree(app, join(input.candidate.root, 'app')); await copyCheckpointTree(widget, join(input.candidate.root, 'widget')); await fsyncPath(input.candidate.root); const assets = await walkRegularFiles(input.candidate.root); @@ -619,12 +511,12 @@ export const captureRuntimeGenerationSnapshot = async ( const decodeMetadata = (value: JsonValue): RscRuntimeGenerationMetadata => { if (!isJsonObject(value)) throw new TypeError('Runtime generation metadata is malformed.'); - const required = ['appDefinitions', 'entries', 'stateStoreId', 'surfaceAssets']; + const required = ['entries', 'stateStoreId']; if (Object.keys(value).some((key) => !required.includes(key)) || required.some((key) => !(key in value))) { throw new TypeError('Runtime generation metadata has an invalid schema.'); } - const { appDefinitions, entries, stateStoreId, surfaceAssets } = value; - if (typeof stateStoreId !== 'string' || !Array.isArray(appDefinitions) || !isJsonObject(entries) || !isJsonObject(surfaceAssets)) { + const { entries, stateStoreId } = value; + if (typeof stateStoreId !== 'string' || !isJsonObject(entries)) { throw new TypeError('Runtime generation metadata is malformed.'); } if (stateStoreId.length === 0 || @@ -632,42 +524,9 @@ const decodeMetadata = (value: JsonValue): RscRuntimeGenerationMetadata => { throw new TypeError('Runtime generation entries are malformed.'); } - const decodedAppDefinitions = appDefinitions.map((value): RscRuntimeAppDefinition => { - if (!isJsonObject(value)) throw new TypeError('Runtime generation App definition is malformed.'); - const fields = ['id', 'name', 'resourceUri']; - if (Object.keys(value).some((key) => !fields.includes(key)) || fields.some((field) => !(field in value)) || - typeof value.id !== 'string' || typeof value.name !== 'string' || typeof value.resourceUri !== 'string') { - throw new TypeError('Runtime generation App definition is malformed.'); - } - return Object.freeze({ id: value.id, name: value.name, resourceUri: value.resourceUri }); - }); - - const decodedSurfaceAssets: Record = {}; - for (const [surfaceId, value] of Object.entries(surfaceAssets)) { - if (surfaceId.length === 0 || !Array.isArray(value)) throw new TypeError('Runtime generation surface assets are malformed.'); - decodedSurfaceAssets[surfaceId] = Object.freeze(value.map((value): RscRuntimeSurfaceAsset => { - if (!isJsonObject(value)) throw new TypeError('Runtime generation surface asset is malformed.'); - const fields = ['bytes', 'contentType', 'generationPath', 'requestPath', 'sha256']; - if (Object.keys(value).some((key) => !fields.includes(key)) || fields.some((field) => !(field in value)) || - typeof value.bytes !== 'number' || !Number.isSafeInteger(value.bytes) || value.bytes < 0 || typeof value.generationPath !== 'string' || - typeof value.requestPath !== 'string' || typeof value.sha256 !== 'string' || !sha256Expression.test(value.sha256) || - (value.contentType !== 'application/javascript' && value.contentType !== 'application/json' && value.contentType !== 'text/css' && value.contentType !== 'text/html')) { - throw new TypeError('Runtime generation surface asset is malformed.'); - } - return Object.freeze({ - bytes: value.bytes, - contentType: value.contentType, - generationPath: assertRelativeAssetPath(value.generationPath), - requestPath: value.requestPath, - sha256: value.sha256, - }); - })); - } return Object.freeze({ - appDefinitions: Object.freeze(decodedAppDefinitions), entries: Object.freeze(Object.fromEntries(requiredEntries.map((entry) => [entry, entries[entry] as string]))), stateStoreId, - surfaceAssets: Object.freeze(decodedSurfaceAssets), }); }; @@ -701,17 +560,6 @@ export const validateRscRuntimeGenerationMetadata = async ( throw new TypeError('Runtime generation definition is not canonical.'); } await validateClientReferenceRelationship(input.root, input.assets); - const declaredSurfaceAssets = metadata.surfaceAssets as Readonly>; - for (const [surface, descriptors] of Object.entries(declaredSurfaceAssets)) { - const requestPaths = new Set(); - for (const asset of descriptors) { - if (requestPaths.has(asset.requestPath) || assets.get(asset.generationPath)?.sha256 !== asset.sha256 || assets.get(asset.generationPath)?.bytes !== asset.bytes || contentTypeFor(asset.generationPath) !== asset.contentType) { - throw new TypeError(`Runtime generation surface ${JSON.stringify(surface)} is invalid.`); - } - requestPaths.add(asset.requestPath); - } - } - validateAppSurfaceAssets(metadata.appDefinitions, declaredSurfaceAssets); return metadata; }; diff --git a/examples/rsc-agent-runtime/src/dev/rsbuild-runtime-session.ts b/examples/rsc-agent-runtime/src/dev/rsbuild-runtime-session.ts index 599c95747..3bfffa48a 100644 --- a/examples/rsc-agent-runtime/src/dev/rsbuild-runtime-session.ts +++ b/examples/rsc-agent-runtime/src/dev/rsbuild-runtime-session.ts @@ -46,7 +46,6 @@ import { DevRuntimeUnavailableError, createRuntimeGenerationStore, type DevRuntimeAsset, - type DevRuntimeAssetRequest, type DevRuntimeDescriptor, type DevRuntimeDiagnostic, type DevRuntimeEventInput, @@ -76,7 +75,6 @@ const descriptor: DevRuntimeDescriptor = Object.freeze({ label: 'RSC agent runtime', schemaVersion: 1, }); -const maximumAssetBytes = 8 * 1024 * 1024; const stateStoreId = 'playground'; const maximumInvocationWorkers = 4; const maximumInvocationStdoutBytes = 4 * 1024 * 1024; @@ -463,7 +461,6 @@ const clonePrepared = (prepared: DevRuntimePreparedProject): DevRuntimePreparedP deepFreeze(structuredClone(prepared)); const preparedRuntimeAuthorityDigest = (prepared: DevRuntimePreparedProject): string => digestValue({ - apps: prepared.apps, provider: prepared.provider, servers: prepared.servers, }); @@ -508,10 +505,6 @@ export interface RsbuildRuntimeSessionStartTesting { * while an activation is in flight) can be injected deterministically. */ readonly beforeActivationCommit?: () => Promise | void; - readonly beforeAssetRead?: (input: Readonly<{ - readonly request: DevRuntimeAssetRequest; - readonly runtimeGenerationId: string; - }>) => Promise | void; readonly afterInvocationWorkerResponse?: (input: Readonly<{ readonly runId: string; readonly surfaceId: string; @@ -560,7 +553,6 @@ export class RsbuildRuntimeSession implements DevRuntimeSession { readonly #stateKernel: ReturnType; readonly #activeRuns = new Map(); readonly #terminalRuns = new Map(); - readonly #surfaceAssetApps = new Map(); readonly #surfaces = new Map(); readonly #testing: RsbuildRuntimeSessionStartTesting; readonly #maximumRunHistory: number; @@ -584,7 +576,6 @@ export class RsbuildRuntimeSession implements DevRuntimeSession { #evictionTail: Promise = Promise.resolve(); #generationSequence = 0; #failureTail: Promise = Promise.resolve(); - #hmrReady = false; #reconcileDegraded = false; #latestPreparedRuntime: DevRuntimePreparedProject; #latestRscCohortRevision = 0; @@ -617,7 +608,6 @@ export class RsbuildRuntimeSession implements DevRuntimeSession { this.#status = Object.freeze({ descriptor, diagnostics: Object.freeze([]), - hmrReady: false, state: 'starting', }); } @@ -755,48 +745,6 @@ export class RsbuildRuntimeSession implements DevRuntimeSession { return task; } - async readAsset(request: DevRuntimeAssetRequest): Promise { - if (this.#closed || !this.#surfaces.has(request.surfaceId) || request.runtimeGenerationId.length === 0) return undefined; - const segments = request.path.map((segment) => { - if (!safeSegment(segment)) return undefined; - try { - return decodeURIComponent(segment) === segment ? segment : undefined; - } catch { - return undefined; - } - }); - if (segments.some((segment) => segment === undefined)) return undefined; - const requestPath = `/${segments.join('/')}`; - let lease; - try { - lease = await this.#generationStore.lease(request.runtimeGenerationId); - await this.#testing.beforeAssetRead?.(Object.freeze({ - request, - runtimeGenerationId: lease.generation.id, - })); - const app = this.#surfaceAssetApps.get(request.surfaceId); - if (app === undefined) return undefined; - const boundSurfaceId = this.#surfaceAssetBinding(lease.generation, app); - if (boundSurfaceId === undefined) return undefined; - const descriptor = lease.generation.manifest.metadata.surfaceAssets[boundSurfaceId] - ?.find((asset) => asset.requestPath === requestPath); - if (descriptor === undefined || descriptor.bytes > maximumAssetBytes) return undefined; - const assetSegments = descriptor.generationPath.split('/'); - if (assetSegments.some((segment) => !safeSegment(segment))) return undefined; - const path = join(lease.generation.root, ...assetSegments); - if (!isInside(lease.generation.root, path)) return undefined; - const details = await lstat(path); - if (!details.isFile() || details.isSymbolicLink() || details.size !== descriptor.bytes) return undefined; - const body = await readFile(path); - if (body.byteLength !== descriptor.bytes || createHash('sha256').update(body).digest('hex') !== descriptor.sha256) return undefined; - return Object.freeze({ body, contentType: descriptor.contentType }); - } catch { - return undefined; - } finally { - await lease?.release(); - } - } - async readRunFlight(runId: string): Promise { if (this.#closed || !safeSegment(runId)) return undefined; if (this.#evictingTerminalRuns.has(runId)) return undefined; @@ -1838,7 +1786,6 @@ export class RsbuildRuntimeSession implements DevRuntimeSession { #attachServer(server: StartDevServerResult['server']): void { if (this.#closed) return; this.#server = server; - this.#hmrReady = true; this.#setStatus(this.#active === undefined ? 'compiling' : 'active'); } @@ -2161,7 +2108,6 @@ export class RsbuildRuntimeSession implements DevRuntimeSession { preparedGeneration = undefined; this.#active = generation; this.#updateSurfaces(snapshot, snapshot.preparedRuntime); - this.#updateSurfaceAssetApps(snapshot.preparedRuntime); this.#reconcileDegraded = false; this.#setStatus('active'); this.#emit(Object.freeze({ runtimeGenerationId: generation.id, type: 'runtime.generation.activated' })); @@ -2181,7 +2127,6 @@ export class RsbuildRuntimeSession implements DevRuntimeSession { try { const definition = JSON.parse(await readFile(join(active.root, 'rsc', 'runtime-definition.json'), 'utf8')) as SerializedRuntimeDefinition; this.#updateSurfaces({ definition }, prepared); - this.#updateSurfaceAssetApps(prepared); if (this.#reconcileDegraded) { this.#reconcileDegraded = false; this.#setStatus('active'); @@ -2196,7 +2141,6 @@ export class RsbuildRuntimeSession implements DevRuntimeSession { async #close(): Promise { this.#closed = true; this.#invocationAbort.abort(new Error('RSC runtime session is closing.')); - this.#hmrReady = false; this.#pendingCohortIds.clear(); this.#settleCompileObservation(); for (const worker of this.#workers.values()) { @@ -2207,7 +2151,6 @@ export class RsbuildRuntimeSession implements DevRuntimeSession { const checkpointStoreClose = this.#checkpointStore.close(); void checkpointStoreClose.catch(() => undefined); this.#setStatus('closed'); - this.#surfaceAssetApps.clear(); while (this.#captureTasks.size > 0) await Promise.all([...this.#captureTasks]); while (this.#invocations.size > 0) await Promise.allSettled([...this.#invocations]); while (this.#runReadTasks.size > 0) { @@ -2308,14 +2251,13 @@ export class RsbuildRuntimeSession implements DevRuntimeSession { ...(vector === undefined ? {} : { activeVector: vector, lastGoodVector: vector }), descriptor, diagnostics: Object.freeze([...diagnostics]), - hmrReady: this.#hmrReady, state, }); } #updateSurfaces( snapshot: Pick, - prepared: Pick, + prepared: Pick, ): void { this.#surfaces.clear(); for (const hook of snapshot.definition.nativeHooks) { @@ -2349,41 +2291,6 @@ export class RsbuildRuntimeSession implements DevRuntimeSession { fixtures: Object.freeze([]), })); } - for (const app of prepared.apps) { - this.#surfaces.set(`mcp.${app.name}`, Object.freeze({ - id: `mcp.${app.name}`, - kind: 'mcp-app', - label: app.name, - readOnly: true, - targets: Object.freeze([...app.targets]), - fixtures: Object.freeze([]), - })); - } - } - - #updateSurfaceAssetApps(prepared: Pick): void { - this.#surfaceAssetApps.clear(); - for (const app of prepared.apps) { - this.#surfaceAssetApps.set(`mcp.${app.name}`, app); - } - } - - #surfaceAssetBinding( - generation: RuntimeGeneration, - app: DevRuntimePreparedProject['apps'][number], - ): string | undefined { - const metadata = generation.manifest.metadata; - const exact = metadata.appDefinitions.find((candidate) => - candidate.id === app.id && candidate.resourceUri === app.resourceUri, - ); - if (exact !== undefined) { - const surfaceId = `mcp.${exact.name}`; - return metadata.surfaceAssets[surfaceId] === undefined ? undefined : surfaceId; - } - const matches = metadata.appDefinitions.filter((candidate) => - candidate.resourceUri === app.resourceUri && metadata.surfaceAssets[`mcp.${candidate.name}`] !== undefined, - ); - return matches.length === 1 ? `mcp.${matches[0]!.name}` : undefined; } #vector(generation: RuntimeGeneration, stateVersion = 0): RuntimeVector { @@ -2462,13 +2369,5 @@ export class RsbuildRuntimeSession implements DevRuntimeSession { if (!isInside(context.projectRoot, resolve(context.projectRoot, prepared.provider))) { throw new Error('RSC runtime provider declaration escapes the project root.'); } - for (const source of [ - ...prepared.servers.flatMap((server) => [server.cwd, server.source]), - ...prepared.apps.flatMap((app) => [app.source, app.template]), - ]) { - if (source !== undefined && !isInside(context.projectRoot, resolve(context.projectRoot, source))) { - throw new Error('RSC runtime prepared declaration contains a path outside the project root.'); - } - } } } diff --git a/examples/rsc-agent-runtime/src/runtime/contracts.ts b/examples/rsc-agent-runtime/src/runtime/contracts.ts index e7f46e3f7..1a9e83e1e 100644 --- a/examples/rsc-agent-runtime/src/runtime/contracts.ts +++ b/examples/rsc-agent-runtime/src/runtime/contracts.ts @@ -165,23 +165,7 @@ export interface SerializedRuntimeDefinition { resources: RuntimeResourceDefinition[]; } -export interface RscRuntimeSurfaceAsset { - readonly bytes: number; - readonly contentType: 'application/javascript' | 'application/json' | 'text/css' | 'text/html'; - readonly generationPath: string; - readonly requestPath: string; - readonly sha256: string; -} - -export interface RscRuntimeAppDefinition { - readonly id: string; - readonly name: string; - readonly resourceUri: string; -} - export interface RscRuntimeGenerationMetadata { - readonly appDefinitions: readonly RscRuntimeAppDefinition[]; readonly entries: Readonly>; readonly stateStoreId: string; - readonly surfaceAssets: Readonly>; } diff --git a/examples/rsc-agent-runtime/tests/dev-provider.integration.test.ts b/examples/rsc-agent-runtime/tests/dev-provider.integration.test.ts index 4a3a11f5d..de6b2e24c 100644 --- a/examples/rsc-agent-runtime/tests/dev-provider.integration.test.ts +++ b/examples/rsc-agent-runtime/tests/dev-provider.integration.test.ts @@ -1,3 +1,4 @@ +import { existsSync } from 'node:fs'; import { cp, lstat, mkdir, mkdtemp, readFile, readdir, rename, rm, symlink, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { basename, join } from 'node:path'; @@ -271,13 +272,13 @@ const introduceWorkerSyntaxError = async (projectRoot: string): Promise return line; }; -test('keeps compiler-App HMR out of the opaque browser child', () => { - const config = createRscRuntimeRsbuildConfig({ - compilerRoot: join(tmpdir(), 'rsc-provider-outer-hmr'), +test('compiles the App environment only for production builds', () => { + const development = createRscRuntimeRsbuildConfig({ + compilerRoot: join(tmpdir(), 'rsc-provider-dev-environments'), mode: 'development', }); - const app = config.environments?.app as Readonly<{ readonly dev?: unknown }> | undefined; - expect(app?.dev).toMatchObject({ hmr: false, liveReload: false }); + expect(Object.keys(development.environments ?? {}).sort()).toEqual(['rsc', 'widget']); + expect(createRscRuntimeRsbuildConfig({ mode: 'production' }).environments?.app).toBeDefined(); }); test('declares an optional runtime while keeping Claude and Codex artifacts buildable', async () => { @@ -332,100 +333,14 @@ test('declares an optional runtime while keeping Claude and Codex artifacts buil }); try { await waitFor(() => session.status().state === 'active'); - expect(session.status()).toMatchObject({ hmrReady: true, state: 'active' }); + expect(session.status()).toMatchObject({ state: 'active' }); expect(session.surfaces()).toEqual(expect.arrayContaining([ expect.objectContaining({ kind: 'hook' }), expect.objectContaining({ id: 'mcp.render_edit_timeline', kind: 'mcp-tool' }), expect.objectContaining({ id: 'mcp.edit-timeline', kind: 'mcp-resource' }), - expect.objectContaining({ id: 'mcp.timeline', kind: 'mcp-app' }), ])); const runtimeGenerationId = session.status().activeVector!.runtimeGenerationId; - await expect(session.readAsset({ - path: ['rsc', 'index.html'], - runtimeGenerationId, - surfaceId: 'mcp.timeline', - })).resolves.toMatchObject({ contentType: 'text/html' }); - await expect(session.readAsset({ - path: ['..'], - runtimeGenerationId, - surfaceId: 'mcp.timeline', - })).resolves.toBeUndefined(); - await expect(session.readAsset({ - path: ['rsc', 'index.html'], - runtimeGenerationId, - surfaceId: 'mcp.unknown', - })).resolves.toBeUndefined(); - for (const path of [ - ['rsc', 'missing.html'], - ['..'], - ['.'], - ['rsc\\index.html'], - ['rsc', 'index\0.html'], - ['%2e%2e'], - ]) { - await expect(session.readAsset({ - path, - runtimeGenerationId, - surfaceId: 'mcp.timeline', - })).resolves.toBeUndefined(); - } - await expect(session.readAsset({ - path: ['rsc', 'index.html'], - runtimeGenerationId: '', - surfaceId: 'mcp.timeline', - })).resolves.toBeUndefined(); - await expect(session.readAsset({ - path: ['rsc', 'index.html'], - runtimeGenerationId: 'generation-pruned', - surfaceId: 'mcp.timeline', - })).resolves.toBeUndefined(); - const assetPath = join( - runtimeStorageRoot, - 'generation-store', - 'generations', - runtimeGenerationId, - 'widget', - 'rsc', - 'index.html', - ); - const originalAsset = await readFile(assetPath); - const readTimelineAsset = () => session.readAsset({ - path: ['rsc', 'index.html'], - runtimeGenerationId, - surfaceId: 'mcp.timeline', - }); - const digestTampered = Buffer.from(originalAsset); - digestTampered[0] = digestTampered[0] === 0 ? 1 : 0; - await writeFile(assetPath, digestTampered); - await expect(readTimelineAsset()).resolves.toBeUndefined(); - await writeFile(assetPath, originalAsset); - await writeFile(assetPath, Buffer.alloc((8 * 1024 * 1024) + 1)); - await expect(readTimelineAsset()).resolves.toBeUndefined(); - await writeFile(assetPath, originalAsset); - await rm(assetPath); - await symlink(join(root, 'src', 'definition.ts'), assetPath); - await expect(readTimelineAsset()).resolves.toBeUndefined(); - await rm(assetPath); - await mkdir(assetPath); - await expect(readTimelineAsset()).resolves.toBeUndefined(); - await rm(assetPath, { recursive: true }); - await writeFile(assetPath, originalAsset); - - await session.reconcilePreparedRuntime({ - ...prepared.devRuntime!, - apps: prepared.devRuntime!.apps.map((app) => ({ - ...app, - _meta: { ...app._meta, 'openai/widgetDescription': 'Updated timeline description.' }, - })), - sourceRevision: `${prepared.devRuntime!.sourceRevision}-app-metadata`, - }); - expect(session.surfaces()).toEqual(expect.arrayContaining([ - expect.objectContaining({ id: 'mcp.render_edit_timeline', targets: prepared.devRuntime!.servers[0]!.targets }), - expect.objectContaining({ id: 'mcp.timeline', kind: 'mcp-app' }), - ])); - await expect(readTimelineAsset()).resolves.toMatchObject({ contentType: 'text/html' }); - const definitionPath = join(runtimeStorageRoot, 'generation-store', 'generations', runtimeGenerationId, 'rsc', 'runtime-definition.json'); await rename(definitionPath, `${definitionPath}.hidden`); await expect(session.reconcilePreparedRuntime({ @@ -467,7 +382,7 @@ test('declares an optional runtime while keeping Claude and Codex artifacts buil sourceRevision: `${prepared.devRuntime!.sourceRevision}-close-race`, })).rejects.toThrow('RSC runtime session is closed.'); await closing; - expect(session.status()).toMatchObject({ hmrReady: false, state: 'closed' }); + expect(session.status()).toMatchObject({ state: 'closed' }); } finally { await session.close(); } @@ -923,126 +838,6 @@ test('drains a deferred generation pipeline before close without publishing late } }, 30_000 * timeScale); -test('binds renamed and added App surfaces to the active generation assets without restoring removed surfaces', async () => { - const copied = await copyProviderExample(); - try { - const prepared = await new ProjectService({ includeDevRuntime: true, mode: 'development', root: copied.projectRoot }).prepare('dev'); - const session = await RsbuildRuntimeSession.start(startContext({ - projectRoot: copied.projectRoot, - preparedRuntime: prepared.devRuntime!, - providerSessionId: 'provider-reconciled-app-assets', - signal: new AbortController().signal, - storageRoot: join(copied.projectRoot, '.agent-bundle', 'runtime-reconciled-app-assets'), - })); - try { - await waitFor(() => session.status().state === 'active'); - const runtimeGenerationId = session.status().activeVector!.runtimeGenerationId; - const original = prepared.devRuntime!.apps[0]!; - await session.reconcilePreparedRuntime({ - ...prepared.devRuntime!, - apps: [ - { ...original, name: 'timeline-renamed' }, - { ...original, id: `${original.id}-added`, name: 'timeline-added' }, - ], - sourceRevision: `${prepared.devRuntime!.sourceRevision}-reconciled-app-assets`, - }); - - await expect(session.readAsset({ - path: ['rsc', 'index.html'], - runtimeGenerationId, - surfaceId: 'mcp.timeline-renamed', - })).resolves.toMatchObject({ contentType: 'text/html' }); - await expect(session.readAsset({ - path: ['rsc', 'index.html'], - runtimeGenerationId, - surfaceId: 'mcp.timeline-added', - })).resolves.toMatchObject({ contentType: 'text/html' }); - await expect(session.readAsset({ - path: ['rsc', 'index.html'], - runtimeGenerationId, - surfaceId: 'mcp.timeline', - })).resolves.toBeUndefined(); - - await changeWorkerImplementation(copied.projectRoot, 'reconciled-app-assets-generation-two'); - await waitFor(() => session.status().activeVector?.runtimeGenerationId !== runtimeGenerationId); - const nextRuntimeGenerationId = session.status().activeVector!.runtimeGenerationId; - for (const generationId of [runtimeGenerationId, nextRuntimeGenerationId]) { - await expect(session.readAsset({ - path: ['rsc', 'index.html'], - runtimeGenerationId: generationId, - surfaceId: 'mcp.timeline-renamed', - })).resolves.toMatchObject({ contentType: 'text/html' }); - await expect(session.readAsset({ - path: ['rsc', 'index.html'], - runtimeGenerationId: generationId, - surfaceId: 'mcp.timeline-added', - })).resolves.toMatchObject({ contentType: 'text/html' }); - await expect(session.readAsset({ - path: ['rsc', 'index.html'], - runtimeGenerationId: generationId, - surfaceId: 'mcp.timeline', - })).resolves.toBeUndefined(); - } - } finally { - await session.close(); - } - } finally { - await rm(copied.workspaceRoot, { force: true, recursive: true }); - } -}, 30_000 * timeScale); - -test('rebinds current App surfaces across retained generations after a later configuration reconcile', async () => { - const copied = await copyProviderExample(); - try { - const prepared = await new ProjectService({ includeDevRuntime: true, mode: 'development', root: copied.projectRoot }).prepare('dev'); - const session = await RsbuildRuntimeSession.start(startContext({ - projectRoot: copied.projectRoot, - preparedRuntime: prepared.devRuntime!, - providerSessionId: 'provider-reconciled-retained-app-assets', - signal: new AbortController().signal, - storageRoot: join(copied.projectRoot, '.agent-bundle', 'runtime-reconciled-retained-app-assets'), - })); - try { - await waitFor(() => session.status().state === 'active'); - const firstGenerationId = session.status().activeVector!.runtimeGenerationId; - await changeWorkerImplementation(copied.projectRoot, 'reconciled-retained-app-assets-generation-two'); - await waitFor(() => session.status().activeVector?.runtimeGenerationId !== firstGenerationId); - const secondGenerationId = session.status().activeVector!.runtimeGenerationId; - const original = prepared.devRuntime!.apps[0]!; - await session.reconcilePreparedRuntime({ - ...prepared.devRuntime!, - apps: [ - { ...original, name: 'timeline-renamed' }, - { ...original, id: `${original.id}-added`, name: 'timeline-added' }, - ], - sourceRevision: `${prepared.devRuntime!.sourceRevision}-reconciled-retained-app-assets`, - }); - - for (const generationId of [firstGenerationId, secondGenerationId]) { - await expect(session.readAsset({ - path: ['rsc', 'index.html'], - runtimeGenerationId: generationId, - surfaceId: 'mcp.timeline-renamed', - })).resolves.toMatchObject({ contentType: 'text/html' }); - await expect(session.readAsset({ - path: ['rsc', 'index.html'], - runtimeGenerationId: generationId, - surfaceId: 'mcp.timeline-added', - })).resolves.toMatchObject({ contentType: 'text/html' }); - await expect(session.readAsset({ - path: ['rsc', 'index.html'], - runtimeGenerationId: generationId, - surfaceId: 'mcp.timeline', - })).resolves.toBeUndefined(); - } - } finally { - await session.close(); - } - } finally { - await rm(copied.workspaceRoot, { force: true, recursive: true }); - } -}, 30_000 * timeScale); - test('activates a warm-cache definition change and republishes the changed tool description as its surface label', async () => { const copied = await copyProviderExample(); try { @@ -1112,20 +907,12 @@ test('aborts a stale activation transaction at its private preparation boundary' expect(session.status().activeVector?.runtimeGenerationId).toBe(firstGeneration); const reconciled = session.reconcilePreparedRuntime({ ...prepared.devRuntime!, - apps: prepared.devRuntime!.apps.map((app) => ({ - ...app, - source: './src/widget/App.tsx', - })), + servers: prepared.devRuntime!.servers.map((server) => ({ ...server, targets: ['portable'] })), sourceRevision: `${prepared.devRuntime!.sourceRevision}-store-superseding-prepared`, }); allow.resolve(); await reconciled; await new Promise((resolve) => { setTimeout(resolve, 50); }); - await expect(session.readAsset({ - path: ['rsc', 'index.html'], - runtimeGenerationId: 'generation-2', - surfaceId: 'mcp.timeline', - })).resolves.toBeUndefined(); expect(session.status().activeVector?.runtimeGenerationId).toBe(firstGeneration); expect(events.filter((event) => event.type === 'runtime.generation.activated' && event.runtimeGenerationId === 'generation-2')).toHaveLength(0); armBarrier = false; @@ -1543,52 +1330,50 @@ test('fails a wedged activation store step within the budget and releases its la } }); -test('retains a leased inactive generation through pruning and prunes it after the read releases', async () => { +test('retains a leased inactive generation through pruning and prunes it after the invocation releases', async () => { const copied = await copyProviderExample(); try { const prepared = await new ProjectService({ includeDevRuntime: true, mode: 'development', root: copied.projectRoot }).prepare('dev'); - const enteredRead = deferred(); - const releaseRead = deferred(); - let deferAssetRead = true; - const storageRoot = join(copied.projectRoot, '.agent-bundle', 'runtime-asset-lease'); + const enteredResponse = deferred(); + const releaseResponse = deferred(); + let deferResponse = true; + const storageRoot = join(copied.projectRoot, '.agent-bundle', 'runtime-invocation-lease'); const session = await RsbuildRuntimeSession.start(startContext({ projectRoot: copied.projectRoot, preparedRuntime: prepared.devRuntime!, - providerSessionId: 'provider-asset-lease', + providerSessionId: 'provider-invocation-lease', signal: new AbortController().signal, storageRoot, }), { - beforeAssetRead: async () => { - if (!deferAssetRead) return; - enteredRead.resolve(); - await releaseRead.promise; + afterInvocationWorkerResponse: async () => { + if (!deferResponse) return; + enteredResponse.resolve(); + await releaseResponse.promise; }, }); try { await waitFor(() => session.status().state === 'active'); const firstGeneration = session.status().activeVector!.runtimeGenerationId; - const heldRead = session.readAsset({ - path: ['rsc', 'index.html'], - runtimeGenerationId: firstGeneration, - surfaceId: 'mcp.timeline', + const heldInvocation = session.invoke({ + expectedGenerationId: firstGeneration, + input: {}, + surfaceId: 'mcp.render_edit_timeline', + target: 'portable', }); - await enteredRead.promise; + await enteredResponse.promise; let activeGeneration = firstGeneration; for (let marker = 2; marker <= 7; marker += 1) { await changeWorkerImplementation(copied.projectRoot, `lease-prune-${String(marker)}`); await waitFor(() => session.status().activeVector?.runtimeGenerationId !== activeGeneration); activeGeneration = session.status().activeVector!.runtimeGenerationId; } - expect((await lstat(join(storageRoot, 'generation-store', 'generations', firstGeneration))).isDirectory()).toBe(true); - releaseRead.resolve(); - await expect(heldRead).resolves.toMatchObject({ contentType: 'text/html' }); - deferAssetRead = false; - await new Promise((resolve) => { setTimeout(resolve, 100); }); - await expect(session.readAsset({ - path: ['rsc', 'index.html'], - runtimeGenerationId: firstGeneration, - surfaceId: 'mcp.timeline', - })).resolves.toBeUndefined(); + const firstGenerationRoot = join(storageRoot, 'generation-store', 'generations', firstGeneration); + expect((await lstat(firstGenerationRoot)).isDirectory()).toBe(true); + deferResponse = false; + releaseResponse.resolve(); + await expect(heldInvocation).resolves.toMatchObject({ status: 'succeeded', vector: { runtimeGenerationId: firstGeneration } }); + await changeWorkerImplementation(copied.projectRoot, 'lease-prune-after-release'); + await waitFor(() => !existsSync(firstGenerationRoot)); } finally { await session.close(); } @@ -1660,10 +1445,9 @@ test('returns a compiling session without treating provider activation work as a const returnedBeforeActivation = returnedSession; expect(returnedBeforeActivation?.status()).toMatchObject({ state: 'compiling' }); if (returnedBeforeActivation === undefined) throw new Error('RSC runtime session did not return while compiling.'); - const originalApp = prepared.devRuntime!.apps[0]!; const reconciling = returnedBeforeActivation.reconcilePreparedRuntime({ ...prepared.devRuntime!, - apps: [{ ...originalApp, name: 'timeline-startup' }], + servers: prepared.devRuntime!.servers.map((server) => ({ ...server, targets: ['portable'] })), sourceRevision: `${prepared.devRuntime!.sourceRevision}-startup-reconcile`, }); releaseActivation.resolve(); @@ -1671,11 +1455,9 @@ test('returns a compiling session without treating provider activation work as a await reconciling; await waitFor(() => session?.status().state === 'active'); - await expect(session.readAsset({ - path: ['rsc', 'index.html'], - runtimeGenerationId: session.status().activeVector!.runtimeGenerationId, - surfaceId: 'mcp.timeline-startup', - })).resolves.toMatchObject({ contentType: 'text/html' }); + expect(session.surfaces()).toEqual(expect.arrayContaining([ + expect.objectContaining({ id: 'mcp.render_edit_timeline', targets: ['portable'] }), + ])); } finally { releaseActivation.resolve(); await session?.close(); diff --git a/examples/rsc-agent-runtime/tests/environment-checkpoint-store.test.ts b/examples/rsc-agent-runtime/tests/environment-checkpoint-store.test.ts index f4d874257..15c5aad4e 100644 --- a/examples/rsc-agent-runtime/tests/environment-checkpoint-store.test.ts +++ b/examples/rsc-agent-runtime/tests/environment-checkpoint-store.test.ts @@ -22,14 +22,12 @@ import { createRuntimeGenerationStore } from 'agent-bundle/api'; import { writeCompilerCohort } from './support/compiler-cohort.ts'; const preparedRuntime = Object.freeze({ - apps: Object.freeze([]), provider: './src/dev/provider.ts', servers: Object.freeze([]), sourceRevision: 'prepared-r1', }); const cohortHashesFor = (suffix: string): RscEnvironmentCohortHashes => Object.freeze({ - app: `app-${suffix}`, rsc: `rsc-${suffix}`, widget: `widget-${suffix}`, }); @@ -82,7 +80,6 @@ test('assembles a cohort only once every environment checkpoint has landed (skew // The global after-compile hook can fire before a slower environment's // own after-environment hook finishes staging; acquisition must wait for // the exact hash instead of reading anything mutable. - await store.stage({ environment: 'app', hash: hashes.app, sourceRoot: join(compilerRoot, 'app') }); await store.stage({ environment: 'rsc', hash: hashes.rsc, sourceRoot: join(compilerRoot, 'rsc') }); let acquired = false; const pending = store.acquireCohort(hashes).then((cohort) => { @@ -110,7 +107,6 @@ test('fails a waiting cohort fast once a newer compilation supersedes the awaite const store = createCheckpointStore(join(storageRoot, 'environment-checkpoints')); try { await writeCompilerCohort(compilerRoot); - await store.stage({ environment: 'app', hash: 'app-one', sourceRoot: join(compilerRoot, 'app') }); await store.stage({ environment: 'rsc', hash: 'rsc-one', sourceRoot: join(compilerRoot, 'rsc') }); const waiting = store.acquireCohort(cohortHashesFor('one')); const observed = waiting.catch((error: unknown) => error); @@ -123,7 +119,7 @@ test('fails a waiting cohort fast once a newer compilation supersedes the awaite // A cohort naming an already-superseded hash rejects immediately. await store.stage({ environment: 'widget', hash: 'widget-three', sourceRoot: join(compilerRoot, 'widget') }); - await expect(store.acquireCohort({ app: 'app-one', rsc: 'rsc-one', widget: 'widget-two' })) + await expect(store.acquireCohort({ rsc: 'rsc-one', widget: 'widget-two' })) .rejects.toThrow('superseded by a newer compilation'); } finally { await store.close().catch(() => undefined); @@ -137,7 +133,6 @@ test('rejects cohorts whose environment checkpoint failed to stage', async () => const store = createCheckpointStore(join(storageRoot, 'environment-checkpoints')); try { await writeCompilerCohort(compilerRoot, { rscFiles: { 'undeclared.js': 'foreign-write' } }); - await store.stage({ environment: 'app', hash: 'app-one', sourceRoot: join(compilerRoot, 'app') }); await store.stage({ environment: 'widget', hash: 'widget-one', sourceRoot: join(compilerRoot, 'widget') }); await expect(store.stage({ environment: 'rsc', hash: 'rsc-one', sourceRoot: join(compilerRoot, 'rsc') })) .rejects.toThrow('undeclared'); @@ -145,7 +140,7 @@ test('rejects cohorts whose environment checkpoint failed to stage', async () => // Failures recorded before staging could run reject waiters the same way. store.recordStagingFailure({ environment: 'rsc', error: new Error('emitted outside its session root'), hash: 'rsc-two' }); - await expect(store.acquireCohort({ app: 'app-one', rsc: 'rsc-two', widget: 'widget-one' })) + await expect(store.acquireCohort({ rsc: 'rsc-two', widget: 'widget-one' })) .rejects.toThrow('failed to stage'); } finally { await store.close().catch(() => undefined); diff --git a/examples/rsc-agent-runtime/tests/generation-materializer.test.ts b/examples/rsc-agent-runtime/tests/generation-materializer.test.ts index b95830e64..29214dd2c 100644 --- a/examples/rsc-agent-runtime/tests/generation-materializer.test.ts +++ b/examples/rsc-agent-runtime/tests/generation-materializer.test.ts @@ -33,42 +33,11 @@ import { writeCompilerCohort } from './support/compiler-cohort.ts'; const sha256 = (value: string): string => createHash('sha256').update(value).digest('hex'); const preparedRuntime = Object.freeze({ - apps: Object.freeze([]), provider: './src/dev/provider.ts', servers: Object.freeze([]), sourceRevision: 'prepared-r1', }); -const preparedRuntimeWithApp = ( - app: Partial = {}, - runtime: Partial> = {}, -): DevRuntimePreparedProject => Object.freeze({ - apps: Object.freeze([Object.freeze({ - _meta: Object.freeze({ presentation: Object.freeze({ accent: 'indigo', version: 1 }) }), - id: 'timeline-app', - name: 'Timeline', - resourceUri: 'ui://rsc-agent-runtime/edit-timeline-v1.html', - serverId: 'timeline-server', - serverName: 'Timeline MCP', - source: '/workspace/plugin/agent-bundle.config.ts', - targets: Object.freeze(['claude', 'codex']), - template: '/workspace/plugin/src/app/edit-timeline.html', - ...app, - })]), - provider: './src/dev/provider.ts', - servers: Object.freeze([Object.freeze({ - command: 'node', - cwd: '/workspace/plugin', - id: 'timeline-server', - name: 'Timeline MCP', - source: '/workspace/plugin/agent-bundle.config.ts', - targets: Object.freeze(['claude', 'codex']), - transport: 'stdio' as const, - })]), - sourceRevision: 'prepared-r1', - ...runtime, -}); - const createStore = (storageRoot: string): DevRuntimeGenerationStore => createRuntimeGenerationStore({ metadataCodec: rscRuntimeGenerationMetadataCodec, @@ -99,7 +68,6 @@ const createCheckpointStore = (root: string): RscEnvironmentCheckpointStore => }); const cohortHashesFor = (suffix: string): RscEnvironmentCohortHashes => Object.freeze({ - app: `app-${suffix}`, rsc: `rsc-${suffix}`, widget: `widget-${suffix}`, }); @@ -254,7 +222,7 @@ test('resolves the coherent development compiler configuration through Rsbuild', }>>>; expect(configuredEnvironments.rsc?.tools?.rspack?.name).toBe('rsc'); expect(configuredEnvironments.widget?.tools?.rspack?.name).toBe('widget'); - expect(configuredEnvironments.app?.tools?.rspack?.name).toBe('app'); + expect(configuredEnvironments.app).toBeUndefined(); const rsbuild = await createRsbuild({ config: developmentConfig, cwd: process.cwd(), @@ -264,14 +232,12 @@ test('resolves the coherent development compiler configuration through Rsbuild', const bundlers = inspection.origin.bundlerConfigs; const rscBundler = bundlers.find((config) => config.name === 'rsc'); const widgetBundler = bundlers.find((config) => config.name === 'widget'); - const appBundler = bundlers.find((config) => config.name === 'app'); - expect(Object.keys(environments).sort()).toEqual(['app', 'rsc', 'widget']); + expect(Object.keys(environments).sort()).toEqual(['rsc', 'widget']); expect(environments.rsc?.output.target).toBe('node'); expect(environments.widget?.output.target).toBe('web'); expect(environments.rsc?.output.distPath.root).toBe(join(compilerRoot, 'rsc')); expect(environments.widget?.output.distPath.root).toBe(join(compilerRoot, 'widget')); - expect(environments.app?.output.distPath.root).toBe(join(compilerRoot, 'app')); expect(developmentConfig.mode).toBe('production'); expect(inspection.origin.rsbuildConfig.mode).toBe('production'); expect(inspection.origin.rsbuildConfig.dev.writeToDisk).toBe(true); @@ -280,11 +246,8 @@ test('resolves the coherent development compiler configuration through Rsbuild', expect(rscBundler?.output?.chunkFilename).toBe('chunks/[name].js'); expect(rscBundler?.output?.path).toBe(join(compilerRoot, 'rsc')); expect(widgetBundler?.output?.path).toBe(join(compilerRoot, 'widget')); - expect(appBundler?.output?.path).toBe(join(compilerRoot, 'app')); expect(rscBundler?.module?.rules?.some((rule) => typeof rule === 'object' && rule !== null && 'test' in rule && String(rule.test).includes('request-render'))).toBe(true); - expect(appBundler?.target).toEqual(expect.arrayContaining(['web'])); - expect(appBundler?.plugins?.some((plugin) => plugin?.constructor?.name.includes('ReactRefresh'))).toBe(false); expect(widgetBundler?.plugins?.some((plugin) => plugin?.constructor?.name.includes('ReactRefresh'))).toBe(false); const production = await createRsbuild({ @@ -338,231 +301,7 @@ test('captures immutable paired compiler outputs and records every digested asse 'widget/static/js/rsc/index.js', ])); expect(Object.keys(prepared.generation.manifest.metadata).sort()) - .toEqual(['appDefinitions', 'entries', 'stateStoreId', 'surfaceAssets']); - } finally { - await store.close().catch(() => undefined); - await rm(storageRoot, { force: true, recursive: true }); - } -}); - -test('captures canonical, ordered, frozen App definitions', async () => { - const storageRoot = await mkdtemp(join(tmpdir(), 'rsc-agent-runtime-app-definitions-')); - const compilerRoot = join(storageRoot, 'compiler'); - const store = createStore(storageRoot); - try { - await writeCompilerCohort(compilerRoot); - const metadataFor = async (id: string, prepared: DevRuntimePreparedProject) => { - const candidate = await store.begin({ id, sourceRevision: 'captured-r1' }); - const snapshot = await captureCompilerCohort({ - attemptId: `attempt-${id}`, - candidate, - compilerRoot, - preparedRuntime: prepared, - rscCohortRevision: 1, - sourceRevision: 'captured-r1', - }); - return (await materializeRuntimeGeneration({ snapshot, store })).generation.manifest.metadata; - }; - - const baselinePrepared = preparedRuntimeWithApp(); - const [timelineApp] = baselinePrepared.apps; - if (timelineApp === undefined) throw new Error('Baseline prepared App was not declared.'); - const activityApp = Object.freeze({ - ...timelineApp, - id: 'activity-app', - name: 'Activity', - resourceUri: 'ui://rsc-agent-runtime/activity-v1.html', - }); - const orderedForward = await metadataFor('ordered-forward', Object.freeze({ - ...baselinePrepared, - apps: Object.freeze([timelineApp, activityApp]), - })); - const orderedReverse = await metadataFor('ordered-reverse', Object.freeze({ - ...baselinePrepared, - apps: Object.freeze([activityApp, timelineApp]), - })); - expect(orderedForward.appDefinitions).toEqual([ - { id: 'activity-app', name: 'Activity', resourceUri: 'ui://rsc-agent-runtime/activity-v1.html' }, - { id: 'timeline-app', name: 'Timeline', resourceUri: 'ui://rsc-agent-runtime/edit-timeline-v1.html' }, - ]); - expect(orderedReverse.appDefinitions).toEqual(orderedForward.appDefinitions); - const [firstAppDefinition] = orderedForward.appDefinitions; - if (firstAppDefinition === undefined) throw new Error('Ordered App definition was malformed.'); - expect(Object.isFrozen(orderedForward.appDefinitions)).toBe(true); - expect(Object.isFrozen(firstAppDefinition)).toBe(true); - } finally { - await store.close().catch(() => undefined); - await rm(storageRoot, { force: true, recursive: true }); - } -}); - -test('captures the canonical generated HTML asset for each prepared App surface', async () => { - const storageRoot = await mkdtemp(join(tmpdir(), 'rsc-agent-runtime-app-html-')); - const compilerRoot = join(storageRoot, 'compiler'); - const store = createStore(storageRoot); - const html = '
Timeline
'; - try { - await writeCompilerCohort(compilerRoot, { appFiles: { 'edit-timeline-v1.html': html } }); - const candidate = await store.begin({ id: 'app-html', sourceRevision: 'source-app-html' }); - const snapshot = await captureCompilerCohort({ - attemptId: 'attempt-app-html', - candidate, - compilerRoot, - preparedRuntime: preparedRuntimeWithApp(), - rscCohortRevision: 1, - sourceRevision: 'source-app-html', - }); - const prepared = await materializeRuntimeGeneration({ snapshot, store }); - - expect(prepared.generation.manifest.metadata.surfaceAssets['mcp.Timeline']).toEqual(expect.arrayContaining([{ - bytes: Buffer.byteLength(html), - contentType: 'text/html', - generationPath: 'app/edit-timeline-v1.html', - requestPath: '/edit-timeline-v1.html', - sha256: sha256(html), - }])); - } finally { - await store.close().catch(() => undefined); - await rm(storageRoot, { force: true, recursive: true }); - } -}); - -test('rejects a traversal-normalized App URI even when a matching generated HTML file exists', async () => { - const storageRoot = await mkdtemp(join(tmpdir(), 'rsc-agent-runtime-app-html-traversal-')); - const compilerRoot = join(storageRoot, 'compiler'); - const store = createStore(storageRoot); - try { - await writeCompilerCohort(compilerRoot, { appFiles: { 'escaped.html': '
Escaped
' } }); - const candidate = await store.begin({ id: 'app-html-traversal', sourceRevision: 'source-app-html-traversal' }); - const snapshot = await captureCompilerCohort({ - attemptId: 'attempt-app-html-traversal', - candidate, - compilerRoot, - preparedRuntime: preparedRuntimeWithApp({ resourceUri: 'ui://rsc-agent-runtime/../escaped.html' }), - rscCohortRevision: 1, - sourceRevision: 'source-app-html-traversal', - }); - await expect(materializeRuntimeGeneration({ snapshot, store })).rejects.toThrow('resource URI is invalid'); - } finally { - await store.close().catch(() => undefined); - await rm(storageRoot, { force: true, recursive: true }); - } -}); - -test('rejects missing, duplicate, and symbolic-link App HTML capture inputs', async () => { - const storageRoot = await mkdtemp(join(tmpdir(), 'rsc-agent-runtime-app-html-invalid-')); - const compilerRoot = join(storageRoot, 'compiler'); - const store = createStore(storageRoot); - try { - await writeCompilerCohort(compilerRoot, { appFiles: {} }); - const missingCandidate = await store.begin({ id: 'app-html-missing', sourceRevision: 'source-app-html-missing' }); - const missingSnapshot = await captureCompilerCohort({ - attemptId: 'attempt-app-html-missing', candidate: missingCandidate, compilerRoot, preparedRuntime: preparedRuntimeWithApp(), rscCohortRevision: 1, sourceRevision: 'source-app-html-missing', - }); - await expect(materializeRuntimeGeneration({ snapshot: missingSnapshot, store })).rejects.toThrow('no unique captured HTML asset'); - - await writeCompilerCohort(compilerRoot); - const [timelineApp] = preparedRuntimeWithApp().apps; - if (timelineApp === undefined) throw new Error('Timeline App fixture was unavailable.'); - const duplicateCandidate = await store.begin({ id: 'app-html-duplicate', sourceRevision: 'source-app-html-duplicate' }); - const duplicateSnapshot = await captureCompilerCohort({ - attemptId: 'attempt-app-html-duplicate', - candidate: duplicateCandidate, - compilerRoot, - preparedRuntime: Object.freeze({ - ...preparedRuntimeWithApp(), - apps: Object.freeze([timelineApp, Object.freeze({ ...timelineApp, id: 'timeline-app-duplicate' })]), - }), - rscCohortRevision: 2, - sourceRevision: 'source-app-html-duplicate', - }); - await expect(materializeRuntimeGeneration({ snapshot: duplicateSnapshot, store })).rejects.toThrow('duplicate App surface'); - - await symlink(join(compilerRoot, 'app', 'edit-timeline-v1.html'), join(compilerRoot, 'app', 'linked.html')); - const linkedCandidate = await store.begin({ id: 'app-html-link', sourceRevision: 'source-app-html-link' }); - await expect(captureCompilerCohort({ - attemptId: 'attempt-app-html-link', candidate: linkedCandidate, compilerRoot, preparedRuntime: preparedRuntimeWithApp(), rscCohortRevision: 3, sourceRevision: 'source-app-html-link', - })).rejects.toThrow('symbolic links'); - } finally { - await store.close().catch(() => undefined); - await rm(storageRoot, { force: true, recursive: true }); - } -}); - -test('rejects a rewritten prepared App definition manifest on post-rename reload', async () => { - const storageRoot = await mkdtemp(join(tmpdir(), 'rsc-agent-runtime-persisted-app-definition-')); - const compilerRoot = join(storageRoot, 'compiler'); - const store = createStore(storageRoot); - try { - await writeCompilerCohort(compilerRoot); - const candidate = await store.begin({ id: 'persisted-app', sourceRevision: 'source-persisted-app' }); - const snapshot = await captureCompilerCohort({ - attemptId: 'attempt-persisted-app', - candidate, - compilerRoot, - preparedRuntime: preparedRuntimeWithApp(), - rscCohortRevision: 1, - sourceRevision: 'source-persisted-app', - }); - let waits = 0; - await expect(materializeRuntimeGeneration({ - guard: { - check: () => true, - wait: async () => { - waits += 1; - if (waits !== 1) return; - await rewriteGenerationManifest(snapshot.candidate.root, (metadata) => ({ - ...metadata, - appDefinitions: [{ - ...(metadata.appDefinitions as readonly Readonly>[])[0], - name: 'Tampered timeline', - }], - })); - }, - }, - snapshot, - store, - })).rejects.toMatchObject({ code: 'RUNTIME_GENERATION_INVALID' }); - expect(waits).toBe(1); - } finally { - await store.close().catch(() => undefined); - await rm(storageRoot, { force: true, recursive: true }); - } -}); - -test('rejects a persisted App surface manifest without its declared canonical HTML asset', async () => { - const storageRoot = await mkdtemp(join(tmpdir(), 'rsc-agent-runtime-persisted-app-surface-')); - const compilerRoot = join(storageRoot, 'compiler'); - const store = createStore(storageRoot); - try { - await writeCompilerCohort(compilerRoot); - const candidate = await store.begin({ id: 'persisted-app-surface', sourceRevision: 'source-persisted-app-surface' }); - const snapshot = await captureCompilerCohort({ - attemptId: 'attempt-persisted-app-surface', - candidate, - compilerRoot, - preparedRuntime: preparedRuntimeWithApp(), - rscCohortRevision: 1, - sourceRevision: 'source-persisted-app-surface', - }); - let waits = 0; - await expect(materializeRuntimeGeneration({ - guard: { - check: () => true, - wait: async () => { - waits += 1; - if (waits !== 1) return; - await rewriteGenerationManifest(snapshot.candidate.root, (metadata) => ({ - ...metadata, - surfaceAssets: Object.fromEntries(Object.entries(metadata.surfaceAssets as Readonly>[]>>) - .map(([surfaceId, assets]) => [surfaceId, assets.filter((asset) => asset.contentType !== 'text/html')])), - })); - }, - }, - snapshot, - store, - })).rejects.toMatchObject({ code: 'RUNTIME_GENERATION_INVALID' }); - expect(waits).toBe(1); + .toEqual(['entries', 'stateStoreId']); } finally { await store.close().catch(() => undefined); await rm(storageRoot, { force: true, recursive: true }); @@ -589,12 +328,6 @@ test('rejects a removed or replaced paired compiler asset after capture', async await writeFile(join(replacedCandidate.root, 'widget', 'static', 'js', 'rsc', 'index.js'), 'replaced-client-reference', 'utf8'); await expect(materializeRuntimeGeneration({ snapshot: replacedSnapshot, store })).rejects.toThrow('captured cohort'); - const appCandidate = await store.begin({ id: 'app-replaced', sourceRevision: 'source-app-replaced' }); - const appSnapshot = await captureCompilerCohort({ - attemptId: 'attempt-app-replaced', candidate: appCandidate, compilerRoot, preparedRuntime: preparedRuntimeWithApp(), rscCohortRevision: 3, sourceRevision: 'source-app-replaced', - }); - await writeFile(join(appCandidate.root, 'app', 'edit-timeline-v1.html'), 'replaced-App-HTML', 'utf8'); - await expect(materializeRuntimeGeneration({ snapshot: appSnapshot, store })).rejects.toThrow('captured cohort'); } finally { await store.close().catch(() => undefined); await rm(storageRoot, { force: true, recursive: true }); @@ -653,13 +386,12 @@ test('waits for grace-to-SIGKILL termination of a SIGTERM-ignoring definition ch } }, 10_000); -test('fails compile attempts unless stats contain one nonempty RSC, widget, and App hash', async () => { +test('fails compile attempts unless stats contain one nonempty RSC and widget hash', async () => { for (const children of [ [{ name: 'rsc', hash: 'rsc-hash' }], - [{ name: 'rsc', hash: 'rsc-hash' }, { name: 'widget', hash: 'widget-hash' }], - [{ name: 'rsc', hash: 'rsc-hash' }, { name: 'rsc', hash: 'second-rsc-hash' }, { name: 'widget', hash: 'widget-hash' }, { name: 'app', hash: 'app-hash' }], - [{ name: 'rsc', hash: 'rsc-hash' }, { name: 'widget' }, { name: 'app', hash: 'app-hash' }], - [{ name: 'rsc', hash: 'rsc-hash' }, { name: 'widget', hash: 'widget-hash' }, { name: 'app', hash: '' }], + [{ name: 'rsc', hash: 'rsc-hash' }, { name: 'rsc', hash: 'second-rsc-hash' }, { name: 'widget', hash: 'widget-hash' }], + [{ name: 'rsc', hash: 'rsc-hash' }, { name: 'widget' }], + [{ name: 'rsc', hash: 'rsc-hash' }, { name: 'widget', hash: '' }], ]) { const capture: Array>> = []; const enqueued: string[] = []; @@ -683,10 +415,8 @@ test('passes exact per-environment hashes to capture alongside the rsc and widge expect(capture).toHaveLength(1); expect(capture[0]).toMatchObject({ cohortChanged: true, - environmentHashes: { app: 'app-one', rsc: 'rsc-one', widget: 'widget-one' }, + environmentHashes: { rsc: 'rsc-one', widget: 'widget-one' }, }); - // The App environment ships through its own dev-server surface, so only - // rsc and widget hashes define the source revision. expect(capture[0]?.sourceRevision).toBe(sha256(JSON.stringify([['rsc', 'rsc-one'], ['widget', 'widget-one']]))); }); @@ -703,7 +433,6 @@ test('stages a checkpoint for every successful environment compilation and skips await observer.completeEnvironment({ distPath: '/compiler/rsc', hash: 'rsc-one', name: 'rsc' }); await observer.completeEnvironment({ distPath: '/compiler/widget', hash: 'widget-one', name: 'widget' }); - await observer.completeEnvironment({ distPath: '/compiler/app', hash: 'app-one', name: 'app' }); // Failed compilations, unexpected environments, and missing hashes stage // nothing; the global after-compile hook is the loud failure path. await observer.completeEnvironment({ distPath: '/compiler/rsc', hash: 'rsc-two', hasErrors: true, name: 'rsc' }); @@ -713,7 +442,6 @@ test('stages a checkpoint for every successful environment compilation and skips expect(staged).toEqual([ { distPath: '/compiler/rsc', environmentName: 'rsc', statsHash: 'rsc-one' }, { distPath: '/compiler/widget', environmentName: 'widget', statsHash: 'widget-one' }, - { distPath: '/compiler/app', environmentName: 'app', statsHash: 'app-one' }, ]); }); diff --git a/examples/rsc-agent-runtime/tests/rsbuild-config.test.ts b/examples/rsc-agent-runtime/tests/rsbuild-config.test.ts index 4cef57f72..e594efeae 100644 --- a/examples/rsc-agent-runtime/tests/rsbuild-config.test.ts +++ b/examples/rsc-agent-runtime/tests/rsbuild-config.test.ts @@ -33,7 +33,7 @@ test('sets an explicit Chromium browserslist on the web hosts only', () => { expect(webOutput(production, 'app')?.overrideBrowserslist).toEqual([...rscRuntimeBrowserHost]); expect(webOutput(production, 'widget')?.overrideBrowserslist).toEqual([...rscRuntimeBrowserHost]); expect(webOutput(production, 'rsc')?.overrideBrowserslist).toBeUndefined(); - expect(webOutput(development, 'app')?.overrideBrowserslist).toEqual([...rscRuntimeBrowserHost]); + expect(webOutput(development, 'app')).toBeUndefined(); expect(webOutput(development, 'widget')?.overrideBrowserslist).toEqual([...rscRuntimeBrowserHost]); }); @@ -46,15 +46,12 @@ test('resolved development topology keeps every React environment in production }); const inspection = await rsbuild.inspectConfig({ mode: 'development' }); const widgetBundler = inspection.origin.bundlerConfigs.find((config) => config.name === 'widget'); - const appBundler = inspection.origin.bundlerConfigs.find((config) => config.name === 'app'); expect(inspection.origin.rsbuildConfig.mode).toBe('production'); - expect(inspection.origin.environmentConfigs.app?.mode).toBe('production'); + expect(inspection.origin.environmentConfigs.app).toBeUndefined(); expect(inspection.origin.environmentConfigs.rsc?.mode).toBe('production'); expect(inspection.origin.environmentConfigs.widget?.mode).toBe('production'); - expect(inspection.origin.environmentConfigs.app?.output.overrideBrowserslist).toEqual([...rscRuntimeBrowserHost]); expect(inspection.origin.environmentConfigs.widget?.output.overrideBrowserslist).toEqual([...rscRuntimeBrowserHost]); - expect(appBundler?.plugins?.some((plugin) => plugin?.constructor?.name.includes('ReactRefresh'))).toBe(false); expect(widgetBundler?.plugins?.some((plugin) => plugin?.constructor?.name.includes('ReactRefresh'))).toBe(false); } finally { await rm(compilerRoot, { force: true, recursive: true }); diff --git a/examples/rsc-agent-runtime/tests/support/compiler-cohort.ts b/examples/rsc-agent-runtime/tests/support/compiler-cohort.ts index e7a88f5b7..f02e2621d 100644 --- a/examples/rsc-agent-runtime/tests/support/compiler-cohort.ts +++ b/examples/rsc-agent-runtime/tests/support/compiler-cohort.ts @@ -20,12 +20,6 @@ export const widgetFiles = { 'static/js/rsc/index.js': 'client-reference', } as const; -export const appFiles = { - 'edit-timeline-v1.html': '
Timeline
', - 'edit-timeline-v2.html': '
Timeline v2
', - 'activity-v1.html': '
Activity
', -} as const; - export const writeTree = async (root: string, files: Readonly>): Promise => { await Promise.all(Object.entries(files).map(async ([path, contents]) => { const destination = join(root, ...path.split('/')); @@ -37,15 +31,12 @@ export const writeTree = async (root: string, files: Readonly>; readonly rscFiles?: Readonly>; readonly widgetFiles?: Readonly>; }> = {}, ): Promise => { const rscRoot = join(compilerRoot, 'rsc'); await writeTree(rscRoot, { ...runtimeFiles, ...options.rscFiles }); - await mkdir(join(compilerRoot, 'app'), { recursive: true }); - await writeTree(join(compilerRoot, 'app'), options.appFiles ?? appFiles); await writeTree(join(compilerRoot, 'widget'), { ...widgetFiles, ...options.widgetFiles }); await writeFile(join(rscRoot, 'runtime-assets.json'), JSON.stringify({ allFiles: Object.keys(runtimeFiles).map((path) => `/${path}`), diff --git a/packages/agent-bundle/src/api.ts b/packages/agent-bundle/src/api.ts index 898948636..386813224 100644 --- a/packages/agent-bundle/src/api.ts +++ b/packages/agent-bundle/src/api.ts @@ -357,7 +357,6 @@ export { export type { CreateDevRuntimeProvider, DevRuntimeEventInput, - DevRuntimePreparedMcpApp, DevRuntimePreparedMcpServer, DevRuntimePreparedProject, DevRuntimeProvider, @@ -366,7 +365,6 @@ export type { } from './dev/runtime-provider.ts'; export type { DevRuntimeAsset, - DevRuntimeAssetRequest, DevRuntimeDescriptor, DevRuntimeDiagnostic, DevRuntimeDiagnosticPhase, diff --git a/packages/agent-bundle/src/dev/index.ts b/packages/agent-bundle/src/dev/index.ts index 5fb255f78..db2811720 100644 --- a/packages/agent-bundle/src/dev/index.ts +++ b/packages/agent-bundle/src/dev/index.ts @@ -182,7 +182,6 @@ export { export type { CreateDevRuntimeProvider, DevRuntimeEventInput, - DevRuntimePreparedMcpApp, DevRuntimePreparedMcpServer, DevRuntimePreparedProject, DevRuntimeProvider, @@ -191,7 +190,6 @@ export type { } from './runtime-provider.ts'; export type { DevRuntimeAsset, - DevRuntimeAssetRequest, DevRuntimeDescriptor, DevRuntimeDiagnostic, DevRuntimeDiagnosticPhase, diff --git a/packages/agent-bundle/src/dev/project-service.ts b/packages/agent-bundle/src/dev/project-service.ts index b6b26741f..a858e6111 100644 --- a/packages/agent-bundle/src/dev/project-service.ts +++ b/packages/agent-bundle/src/dev/project-service.ts @@ -37,14 +37,13 @@ import type { AgentBundleDevConfig, AgentBundleDevRuntimeConfig, AgentBundleToolsConfig, - NormalizedMcpApp, NormalizedMcpServer, NormalizedPlugin, } from '../core/types.ts'; import { emptyCompiledRouteGraph } from '../routes/graph.ts'; import { writeRouteTypes } from '../routes/typegen.ts'; import type { CompiledRouteGraph } from '../routes/types.ts'; -import type { DevRuntimePreparedMcpApp, DevRuntimePreparedMcpServer, DevRuntimePreparedProject } from './runtime-provider.ts'; +import type { DevRuntimePreparedMcpServer, DevRuntimePreparedProject } from './runtime-provider.ts'; import { freezeJsonValue, type JsonObject, type JsonValue, type SourceStatus } from './types.ts'; import { deepFreeze } from '../core/freeze.ts'; @@ -527,33 +526,10 @@ const appMetadata = (value: unknown): JsonObject => { return freezeJsonValue(snapshot) as JsonObject; }; -const stringRecord = (value: Readonly> | undefined): Readonly> | undefined => - value === undefined ? undefined : Object.freeze({ ...value }); - const preparedMcpServer = (server: NormalizedMcpServer): DevRuntimePreparedMcpServer => Object.freeze({ - ...(server.args === undefined ? {} : { args: Object.freeze([...server.args]) }), - ...(server.command === undefined ? {} : { command: server.command }), - ...(server.cwd === undefined ? {} : { cwd: server.cwd }), - ...(server.env === undefined ? {} : { env: stringRecord(server.env) }), - ...(server.headers === undefined ? {} : { headers: stringRecord(server.headers) }), id: server.id, name: server.name, - ...(server.source === undefined ? {} : { source: server.source }), targets: Object.freeze([...server.targets]), - transport: server.transport, - ...(server.url === undefined ? {} : { url: server.url }), -}); - -const preparedMcpApp = (app: NormalizedMcpApp): DevRuntimePreparedMcpApp => Object.freeze({ - ...(app._meta === undefined ? {} : { _meta: appMetadata(app._meta) }), - id: app.id, - name: app.name, - resourceUri: app.resourceUri, - serverId: app.serverId, - serverName: app.serverName, - source: app.source, - targets: Object.freeze([...app.targets]), - ...(app.template === undefined ? {} : { template: app.template }), }); const preparedRuntime = ( @@ -561,7 +537,6 @@ const preparedRuntime = ( model: NormalizedPlugin, revision: string, ): DevRuntimePreparedProject => Object.freeze({ - apps: Object.freeze((model.mcpApps ?? []).map(preparedMcpApp)), provider: declaration.provider, servers: Object.freeze(model.mcpServers.map(preparedMcpServer)), sourceRevision: revision, diff --git a/packages/agent-bundle/src/dev/runtime-controller.ts b/packages/agent-bundle/src/dev/runtime-controller.ts index 22ddbf867..98e8dee3f 100644 --- a/packages/agent-bundle/src/dev/runtime-controller.ts +++ b/packages/agent-bundle/src/dev/runtime-controller.ts @@ -13,7 +13,6 @@ import { } from './runtime-provider.ts'; import type { DevRuntimeAsset, - DevRuntimeAssetRequest, DevRuntimeDescriptor, DevRuntimeDiagnostic, DevRuntimeInvocationRequest, @@ -49,7 +48,6 @@ const statusFor = ( ): DevRuntimeStatus => Object.freeze({ descriptor, diagnostics: Object.freeze([...diagnostics]), - hmrReady: false, state, }); @@ -192,10 +190,6 @@ const diagnosticPhases = new Set([ 'rsc-render', 'flight-decode', 'lowering-contract', - 'mcp-protocol', - 'resource-selection', - 'sandbox/csp', - 'app-bridge', 'provider-lifecycle', ]); @@ -220,10 +214,9 @@ const states = new Set([ ]); const snapshotStatus = (value: unknown): DevRuntimeStatus => { - const status = exactRecord(value, ['descriptor', 'diagnostics', 'hmrReady', 'state'], ['activeVector', 'lastGoodVector']); + const status = exactRecord(value, ['descriptor', 'diagnostics', 'state'], ['activeVector', 'lastGoodVector']); const state = ownDataValue(status, 'state'); - const hmrReady = ownDataValue(status, 'hmrReady'); - if (typeof state !== 'string' || !states.has(state as DevRuntimeStatus['state']) || typeof hmrReady !== 'boolean') return snapshotInvalid(); + if (typeof state !== 'string' || !states.has(state as DevRuntimeStatus['state'])) return snapshotInvalid(); const activeVector = Object.hasOwn(status, 'activeVector') ? snapshotVector(ownDataValue(status, 'activeVector')) : undefined; @@ -234,13 +227,12 @@ const snapshotStatus = (value: unknown): DevRuntimeStatus => { ...(activeVector === undefined ? {} : { activeVector }), descriptor: snapshotDescriptor(ownDataValue(status, 'descriptor')), diagnostics: Object.freeze(snapshotArray(ownDataValue(status, 'diagnostics')).map(snapshotDiagnostic)), - hmrReady, ...(lastGoodVector === undefined ? {} : { lastGoodVector }), state: state as DevRuntimeStatus['state'], }); }; -const surfaceKinds = new Set(['hook', 'mcp-tool', 'mcp-resource', 'mcp-app']); +const surfaceKinds = new Set(['hook', 'mcp-tool', 'mcp-resource']); const snapshotFixture = (value: unknown): DevRuntimeSurface['fixtures'][number] => { const fixture = exactRecord(value, ['id', 'label'], ['seed']); @@ -366,9 +358,6 @@ export class DevRuntimeController implements DevRuntimeSession { return this.#activeSession().invoke(request); } - readAsset(request: DevRuntimeAssetRequest): Promise { - return this.#activeSession().readAsset(request); - } readRunFlight(runId: string): Promise { return this.#activeSession().readRunFlight(runId); @@ -638,7 +627,6 @@ export class DevRuntimeController implements DevRuntimeSession { diagnostics: Object.freeze([lifecycleDiagnostic(restartRequired ? 'Development runtime declaration changed; restart required.' : undefined)]), - hmrReady: prior.hmrReady, ...(prior.lastGoodVector === undefined ? {} : { lastGoodVector: prior.lastGoodVector }), state, }); diff --git a/packages/agent-bundle/src/dev/runtime-protocol.ts b/packages/agent-bundle/src/dev/runtime-protocol.ts index a819550e2..4b488b262 100644 --- a/packages/agent-bundle/src/dev/runtime-protocol.ts +++ b/packages/agent-bundle/src/dev/runtime-protocol.ts @@ -21,10 +21,6 @@ export type DevRuntimeDiagnosticPhase = | 'rsc-render' | 'flight-decode' | 'lowering-contract' - | 'mcp-protocol' - | 'resource-selection' - | 'sandbox/csp' - | 'app-bridge' | 'provider-lifecycle'; export interface DevRuntimeDiagnostic { @@ -52,7 +48,7 @@ export interface DevRuntimeSurface { readonly fixtures: readonly DevRuntimeFixture[]; readonly id: string; readonly inputSchema?: JsonObject; - readonly kind: 'hook' | 'mcp-tool' | 'mcp-resource' | 'mcp-app'; + readonly kind: 'hook' | 'mcp-tool' | 'mcp-resource'; readonly label: string; readonly readOnly: boolean; readonly targets: readonly string[]; @@ -130,7 +126,6 @@ export type DevRuntimeStatus = Readonly<{ readonly descriptor: DevRuntimeDescriptor; readonly diagnostics: readonly DevRuntimeDiagnostic[]; /** The compiler endpoint can accept an HMR client; not proof that a browser is connected. */ - readonly hmrReady: boolean; readonly lastGoodVector?: RuntimeVector; readonly state: 'starting' | 'compiling' | 'active' | 'degraded' | 'failed' | 'closed'; }>; @@ -155,12 +150,6 @@ export interface DevRuntimeStateResetRequest { readonly stateStoreId: string; } -export interface DevRuntimeAssetRequest { - readonly path: readonly string[]; - readonly runtimeGenerationId: string; - readonly surfaceId: string; -} - export interface DevRuntimeAsset { readonly body: Uint8Array; readonly contentType: string; diff --git a/packages/agent-bundle/src/dev/runtime-provider.ts b/packages/agent-bundle/src/dev/runtime-provider.ts index 6e3c23ffd..632f58b89 100644 --- a/packages/agent-bundle/src/dev/runtime-provider.ts +++ b/packages/agent-bundle/src/dev/runtime-provider.ts @@ -1,7 +1,6 @@ import type { ArtifactStatus, JsonObject } from './types.ts'; import type { DevRuntimeAsset, - DevRuntimeAssetRequest, DevRuntimeDescriptor, DevRuntimeInvocationRequest, DevRuntimeReplayRequest, @@ -14,33 +13,12 @@ import type { /** Trusted normalized input from ProjectService; never serialize to the browser. */ export interface DevRuntimePreparedMcpServer { - readonly args?: readonly string[]; - readonly command?: string; - readonly cwd?: string; - readonly env?: Readonly>; - readonly headers?: Readonly>; readonly id: string; readonly name: string; - readonly source?: string; readonly targets: readonly string[]; - readonly transport: 'stdio' | 'streamable-http' | 'sse'; - readonly url?: string; -} - -export interface DevRuntimePreparedMcpApp { - readonly _meta?: JsonObject; - readonly id: string; - readonly name: string; - readonly resourceUri: string; - readonly serverId: string; - readonly serverName: string; - readonly source: string; - readonly targets: readonly string[]; - readonly template?: string; } export interface DevRuntimePreparedProject { - readonly apps: readonly DevRuntimePreparedMcpApp[]; readonly provider: string; readonly servers: readonly DevRuntimePreparedMcpServer[]; readonly sourceRevision: string; @@ -77,7 +55,6 @@ export interface DevRuntimeSession { readonly providerSessionId: string; close(): Promise; invoke(request: DevRuntimeInvocationRequest): Promise; - readAsset(request: DevRuntimeAssetRequest): Promise; readRunFlight(runId: string): Promise; reconcilePreparedRuntime(prepared: DevRuntimePreparedProject): Promise; replay(request: DevRuntimeReplayRequest): Promise; diff --git a/packages/agent-bundle/src/dev/runtime-routes.ts b/packages/agent-bundle/src/dev/runtime-routes.ts index 90b1f3f82..aaa9cd7aa 100644 --- a/packages/agent-bundle/src/dev/runtime-routes.ts +++ b/packages/agent-bundle/src/dev/runtime-routes.ts @@ -35,8 +35,7 @@ const agentDocumentResponseLimit = 16 * 1024 * 1024; type Route = | Readonly<{ readonly kind: 'status' | 'surfaces' | 'runs' | 'state-reset' }> - | Readonly<{ readonly id: string; readonly kind: 'run' | 'document' | 'flight' | 'replay' }> - | Readonly<{ readonly generation: string; readonly kind: 'asset'; readonly path: readonly string[]; readonly surfaceId: string }>; + | Readonly<{ readonly id: string; readonly kind: 'run' | 'document' | 'flight' | 'replay' }>; /** * Structural view of the optional `@agent-bundle/runtime` peer. The peer's own @@ -124,12 +123,6 @@ const route = (requestTarget: string | undefined): Route | undefined => { return Object.freeze({ id: segments[1], kind: segments[2] }); } } - if (segments[0] === 'assets' && segments[1] !== undefined && segments.length > 2) { - const query = onlyQuery(requestTarget, 'generation'); - const generation = query.get('generation'); - if (generation === null) return runtimePathError(); - return Object.freeze({ generation: decodedSegment(generation), kind: 'asset', path: Object.freeze(segments.slice(2)), surfaceId: segments[1] }); - } return runtimePathError(); }; @@ -397,14 +390,5 @@ export class RuntimeRoutes { if (method !== 'POST') return responseDiagnostic(response, diagnostic('AB8007', 'Route does not accept this method.', 405)); return writeJsonResponse(response, { state: await session.resetState(reset(await jsonBody(request))) }); } - if (parsed.kind !== 'asset') return; - if (method !== 'GET') return responseDiagnostic(response, diagnostic('AB8007', 'Route does not accept this method.', 405)); - const asset = await session.readAsset({ - path: parsed.path, - runtimeGenerationId: parsed.generation, - surfaceId: parsed.surfaceId, - }); - if (asset === undefined) throw new DevRuntimeUnavailableError('Runtime asset is not available.'); - return responseAsset(response, asset); } } diff --git a/packages/agent-bundle/src/dev/workbench-server.ts b/packages/agent-bundle/src/dev/workbench-server.ts index 3fe255236..a9aa41e3c 100644 --- a/packages/agent-bundle/src/dev/workbench-server.ts +++ b/packages/agent-bundle/src/dev/workbench-server.ts @@ -487,7 +487,6 @@ const startDevServerSession = async (options: StartDevServerOptions, platformRun let runtime: DevRuntimeController | undefined; if (initialPreparedProject.devRuntime !== undefined || initialPreparedProject.devRuntimeDiagnostic !== undefined) { const preparedRuntime = initialPreparedProject.devRuntime ?? Object.freeze({ - apps: Object.freeze([]), provider: '', servers: Object.freeze([]), sourceRevision: initialPreparedProject.source.revision ?? 'unknown', diff --git a/packages/agent-bundle/tests/dev-services.test.ts b/packages/agent-bundle/tests/dev-services.test.ts index a04824d51..515f68680 100644 --- a/packages/agent-bundle/tests/dev-services.test.ts +++ b/packages/agent-bundle/tests/dev-services.test.ts @@ -118,32 +118,12 @@ it('prepares a frozen server-only runtime declaration only for development calle await expect(readFile(sentinel, 'utf8')).rejects.toMatchObject({ code: 'ENOENT' }); expect(runtime.source.state).toBe('ready'); expect(runtime.devRuntime).toEqual({ - apps: [{ - _meta: { labels: ['one', 'two'], ui: { preferred: 'compact' } }, - id: 'mcp-app:timeline:dashboard', - name: 'dashboard', - resourceUri: 'ui://timeline/v1/dashboard', - serverId: 'mcp:timeline', - serverName: 'timeline', - source: join(root, 'src', 'app.ts'), - targets: ['portable'], - template: join(root, 'src', 'shell.html'), - }], provider: './src/dev/provider.ts', - servers: [expect.objectContaining({ - id: 'mcp:timeline', - name: 'timeline', - source: join(root, 'src', 'server.ts'), - targets: ['portable'], - transport: 'stdio', - })], + servers: [{ id: 'mcp:timeline', name: 'timeline', targets: ['portable'] }], sourceRevision: runtime.source.revision, }); expect(Object.isFrozen(runtime.devRuntime)).toBe(true); - expect(Object.isFrozen(runtime.devRuntime?.apps)).toBe(true); - expect(Object.isFrozen(runtime.devRuntime?.apps[0]!._meta)).toBe(true); - expect(Object.isFrozen(runtime.devRuntime?.apps[0]!._meta?.labels)).toBe(true); - expect('provenance' in runtime.devRuntime!.apps[0]!).toBe(false); + expect(Object.isFrozen(runtime.devRuntime?.servers[0]?.targets)).toBe(true); } finally { await removeTree(root); } diff --git a/packages/agent-bundle/tests/dev-workbench.test.ts b/packages/agent-bundle/tests/dev-workbench.test.ts index 2435089cd..33aefec19 100644 --- a/packages/agent-bundle/tests/dev-workbench.test.ts +++ b/packages/agent-bundle/tests/dev-workbench.test.ts @@ -707,14 +707,13 @@ it('does not reconcile a valid preparation released after foreground close begin " close: async () => { state.calls.push('close'); state.closes += 1; },", ' invoke: async () => { throw new Error(\'unused\'); },', " providerSessionId: 'provider-prepared-runtime-close',", - ' readAsset: async () => undefined,', ' readRunFlight: async () => undefined,', " reconcilePreparedRuntime: async () => { state.calls.push('reconcile'); state.reconciles += 1; },", ' replay: async () => { throw new Error(\'unused\'); },', " resetState: async () => ({ stateStoreId: 'state-prepared-runtime-close', stateVersion: 0 }),", ' run: () => undefined,', ' runs: () => [],', - " status: () => ({ descriptor: { environmentVariables: [], id: 'prepared-runtime-close', label: 'Prepared Runtime Close', schemaVersion: 1 }, diagnostics: [], hmrReady: true, state: 'active' }),", + " status: () => ({ descriptor: { environmentVariables: [], id: 'prepared-runtime-close', label: 'Prepared Runtime Close', schemaVersion: 1 }, diagnostics: [], state: 'active' }),", ' surfaces: () => [],', ' }),', '});', @@ -870,7 +869,7 @@ it('prepares the optional runtime once with the development config context befor ' return {', ' close: async () => undefined,', ' providerSessionId: context.providerSessionId,', - ' status: () => ({ descriptor: { environmentVariables: [], id: \'fixture-runtime\', label: \'Fixture runtime\', schemaVersion: 1 }, diagnostics: [], hmrReady: false, state: \'active\' }),', + ' status: () => ({ descriptor: { environmentVariables: [], id: \'fixture-runtime\', label: \'Fixture runtime\', schemaVersion: 1 }, diagnostics: [], state: \'active\' }),', ' surfaces: () => [],', ' };', ' },', diff --git a/packages/agent-bundle/tests/runtime-provider.test.ts b/packages/agent-bundle/tests/runtime-provider.test.ts index 890382013..ffaa74879 100644 --- a/packages/agent-bundle/tests/runtime-provider.test.ts +++ b/packages/agent-bundle/tests/runtime-provider.test.ts @@ -142,7 +142,6 @@ it('starts one provider from the trusted prepared snapshot with only declared en status: () => ({ descriptor: { environmentVariables: ['RUNTIME_TOKEN'], id: 'fixture-runtime', label: 'Fixture runtime', schemaVersion: 1 }, diagnostics: [], - hmrReady: true, state: 'active', }), } as unknown as DevRuntimeSession; @@ -152,7 +151,6 @@ it('starts one provider from the trusted prepared snapshot with only declared en emit: (event) => events.push(event), environment: { RUNTIME_TOKEN: 'allowed', UNDECLARED_SECRET: 'must-not-pass' }, preparedRuntime: { - apps: [], provider: './src/dev/provider.ts', servers: [], sourceRevision: 'source-1', @@ -178,7 +176,7 @@ it('starts one provider from the trusted prepared snapshot with only declared en }); expect(received?.environment).not.toHaveProperty('UNDECLARED_SECRET'); expect(controller.providerSessionId).toMatch(/^[0-9a-f-]{36}$/u); - expect(controller.status()).toMatchObject({ hmrReady: true, state: 'active' }); + expect(controller.status()).toMatchObject({ state: 'active' }); expect(events).toEqual([]); await controller.close(); }); @@ -192,8 +190,8 @@ it('refreshes controller endpoint snapshots before publishing a later runtime ac close: async () => undefined, reconcilePreparedRuntime: async () => undefined, status: () => activated - ? { activeVector: vector, descriptor, diagnostics: [], hmrReady: true, lastGoodVector: vector, state: 'active' as const } - : { descriptor, diagnostics: [], hmrReady: false, state: 'compiling' as const }, + ? { activeVector: vector, descriptor, diagnostics: [], lastGoodVector: vector, state: 'active' as const } + : { descriptor, diagnostics: [], state: 'compiling' as const }, surfaces: () => activated ? [surface] : [], } as unknown as DevRuntimeSession; const controller = new DevRuntimeController({ @@ -202,7 +200,7 @@ it('refreshes controller endpoint snapshots before publishing a later runtime ac observedEvents.push({ state: controller.status().state, surfaceCount: controller.surfaces().length, type: event.type }); }, environment: {}, - preparedRuntime: { apps: [], provider: './src/dev/provider.ts', servers: [], sourceRevision: 'source-1' }, + preparedRuntime: { provider: './src/dev/provider.ts', servers: [], sourceRevision: 'source-1' }, projectRoot: '/workspace/project', provider: { descriptor, @@ -215,7 +213,7 @@ it('refreshes controller endpoint snapshots before publishing a later runtime ac }); await controller.start(); - expect(controller.status()).toMatchObject({ hmrReady: false, state: 'compiling' }); + expect(controller.status()).toMatchObject({ state: 'compiling' }); expect(controller.status()).not.toHaveProperty('activeVector'); expect(controller.surfaces()).toEqual([]); @@ -224,7 +222,6 @@ it('refreshes controller endpoint snapshots before publishing a later runtime ac expect(controller.status()).toMatchObject({ activeVector: vector, - hmrReady: true, lastGoodVector: vector, state: 'active', }); @@ -258,12 +255,11 @@ it('refreshes authoritative failed and status snapshots before forwarding their close: async () => undefined, reconcilePreparedRuntime: async () => undefined, status: () => malformed - ? { activeVector: { runtimeGenerationId: 7 }, descriptor, diagnostics: [], hmrReady: true, state: 'active' as const } + ? { activeVector: { runtimeGenerationId: 7 }, descriptor, diagnostics: [], state: 'active' as const } : { activeVector: vector, descriptor, diagnostics: failed ? [sourceBuildDiagnostic] : [], - hmrReady: true, lastGoodVector: vector, state: 'active' as const, }, @@ -279,7 +275,7 @@ it('refreshes authoritative failed and status snapshots before forwarding their }); }, environment: {}, - preparedRuntime: { apps: [], provider: './src/dev/provider.ts', servers: [], sourceRevision: 'source-1' }, + preparedRuntime: { provider: './src/dev/provider.ts', servers: [], sourceRevision: 'source-1' }, projectRoot: '/workspace/project', provider: { descriptor, @@ -335,7 +331,6 @@ it('refreshes terminal run snapshots before completed or failed events without r activeVector: current, descriptor, diagnostics: Object.freeze([]), - hmrReady: true, lastGoodVector: current, state: 'active' as const, }); @@ -351,7 +346,7 @@ it('refreshes terminal run snapshots before completed or failed events without r })); }, environment: {}, - preparedRuntime: { apps: [], provider: './src/dev/provider.ts', servers: [], sourceRevision: 'source-1' }, + preparedRuntime: { provider: './src/dev/provider.ts', servers: [], sourceRevision: 'source-1' }, projectRoot: '/workspace/project', provider: { descriptor, @@ -389,14 +384,14 @@ it('does not overwrite a controller-owned lifecycle failure while publishing its artifactStatus: () => ({ state: 'missing' }), emit: () => undefined, environment: {}, - preparedRuntime: { apps: [], provider: './src/dev/provider.ts', servers: [], sourceRevision: 'source-1' }, + preparedRuntime: { provider: './src/dev/provider.ts', servers: [], sourceRevision: 'source-1' }, projectRoot: '/workspace/project', provider: { descriptor, start: async () => ({ close: async () => undefined, reconcilePreparedRuntime: async () => { throw new Error('Reconcile failed.'); }, - status: () => ({ activeVector: vector, descriptor, diagnostics: [], hmrReady: true, lastGoodVector: vector, state: 'active' as const }), + status: () => ({ activeVector: vector, descriptor, diagnostics: [], lastGoodVector: vector, state: 'active' as const }), surfaces: () => [surface], } as unknown as DevRuntimeSession), }, @@ -404,7 +399,7 @@ it('does not overwrite a controller-owned lifecycle failure while publishing its }); await controller.start(); - await controller.reconcileDeclaration({ apps: [], provider: './src/dev/provider.ts', servers: [], sourceRevision: 'source-2' }); + await controller.reconcileDeclaration({ provider: './src/dev/provider.ts', servers: [], sourceRevision: 'source-2' }); expect(controller.status()).toMatchObject({ activeVector: vector, @@ -447,7 +442,7 @@ it('detaches and freezes complete activation status and surface snapshots', asyn artifactStatus: () => ({ state: 'missing' }), emit: () => undefined, environment: {}, - preparedRuntime: { apps: [], provider: './src/dev/provider.ts', servers: [], sourceRevision: 'source-1' }, + preparedRuntime: { provider: './src/dev/provider.ts', servers: [], sourceRevision: 'source-1' }, projectRoot: '/workspace/project', provider: { descriptor: mutableDescriptor, @@ -461,11 +456,10 @@ it('detaches and freezes complete activation status and surface snapshots', asyn activeVector: mutableVector, descriptor: mutableDescriptor, diagnostics: [mutableDiagnostic], - hmrReady: true, lastGoodVector: mutableVector, state: 'active' as const, } - : { descriptor: mutableDescriptor, diagnostics: [], hmrReady: false, state: 'compiling' as const }, + : { descriptor: mutableDescriptor, diagnostics: [], state: 'compiling' as const }, surfaces: () => activated ? [mutableSurface] : [], } as unknown as DevRuntimeSession; }, @@ -514,22 +508,22 @@ it('degrades instead of publishing malformed activation snapshots', async () => const malformed = [ { name: 'partial vector', - status: { activeVector: { runtimeGenerationId: 7 }, descriptor, diagnostics: [], hmrReady: true, state: 'active' }, + status: { activeVector: { runtimeGenerationId: 7 }, descriptor, diagnostics: [], state: 'active' }, surfaces: [], }, { name: 'malformed surface', - status: { activeVector: vector, descriptor, diagnostics: [], hmrReady: true, state: 'active' }, + status: { activeVector: vector, descriptor, diagnostics: [], state: 'active' }, surfaces: [{}], }, { name: 'cyclic schema', - status: { activeVector: vector, descriptor, diagnostics: [], hmrReady: true, state: 'active' }, + status: { activeVector: vector, descriptor, diagnostics: [], state: 'active' }, surfaces: [{ ...surface, inputSchema: cyclicSchema }], }, { name: 'BigInt fixture seed', - status: { activeVector: vector, descriptor, diagnostics: [], hmrReady: true, state: 'active' }, + status: { activeVector: vector, descriptor, diagnostics: [], state: 'active' }, surfaces: [{ ...surface, fixtures: [{ id: 'after-edit', label: 'After file edit', seed: 1n }] }], }, ] as const; @@ -542,7 +536,7 @@ it('degrades instead of publishing malformed activation snapshots', async () => artifactStatus: () => ({ state: 'missing' }), emit: (event) => { published.push(event.type); }, environment: {}, - preparedRuntime: { apps: [], provider: './src/dev/provider.ts', servers: [], sourceRevision: 'source-1' }, + preparedRuntime: { provider: './src/dev/provider.ts', servers: [], sourceRevision: 'source-1' }, projectRoot: '/workspace/project', provider: { descriptor, @@ -553,7 +547,7 @@ it('degrades instead of publishing malformed activation snapshots', async () => reconcilePreparedRuntime: async () => undefined, status: () => activated ? invalid.status - : { descriptor, diagnostics: [], hmrReady: false, state: 'compiling' as const }, + : { descriptor, diagnostics: [], state: 'compiling' as const }, surfaces: () => activated ? invalid.surfaces : [], } as unknown as DevRuntimeSession; }, @@ -585,7 +579,7 @@ it('accepts acyclic shared JSON fragments in activation snapshots', async () => artifactStatus: () => ({ state: 'missing' }), emit: () => undefined, environment: {}, - preparedRuntime: { apps: [], provider: './src/dev/provider.ts', servers: [], sourceRevision: 'source-1' }, + preparedRuntime: { provider: './src/dev/provider.ts', servers: [], sourceRevision: 'source-1' }, projectRoot: '/workspace/project', provider: { descriptor, @@ -595,8 +589,8 @@ it('accepts acyclic shared JSON fragments in activation snapshots', async () => close: async () => undefined, reconcilePreparedRuntime: async () => undefined, status: () => activated - ? { activeVector: vector, descriptor, diagnostics: [], hmrReady: true, state: 'active' as const } - : { descriptor, diagnostics: [], hmrReady: false, state: 'compiling' as const }, + ? { activeVector: vector, descriptor, diagnostics: [], state: 'active' as const } + : { descriptor, diagnostics: [], state: 'compiling' as const }, surfaces: () => activated ? [{ ...surface, @@ -649,7 +643,7 @@ it('buffers synchronous startup failure and status until controller snapshots in }); }, environment: {}, - preparedRuntime: { apps: [], provider: './src/dev/provider.ts', servers: [], sourceRevision: 'source-1' }, + preparedRuntime: { provider: './src/dev/provider.ts', servers: [], sourceRevision: 'source-1' }, projectRoot: '/workspace/project', provider: { descriptor, @@ -660,7 +654,7 @@ it('buffers synchronous startup failure and status until controller snapshots in return { close: async () => undefined, reconcilePreparedRuntime: async () => undefined, - status: () => ({ descriptor, diagnostics: [sourceBuildDiagnostic], hmrReady: true, state: 'degraded' as const }), + status: () => ({ descriptor, diagnostics: [sourceBuildDiagnostic], state: 'degraded' as const }), surfaces: () => [surface], } as unknown as DevRuntimeSession; }, @@ -691,7 +685,7 @@ it('buffers synchronous startup activation until controller snapshots install', }); }, environment: {}, - preparedRuntime: { apps: [], provider: './src/dev/provider.ts', servers: [], sourceRevision: 'source-1' }, + preparedRuntime: { provider: './src/dev/provider.ts', servers: [], sourceRevision: 'source-1' }, projectRoot: '/workspace/project', provider: { descriptor, @@ -702,7 +696,7 @@ it('buffers synchronous startup activation until controller snapshots install', return { close: async () => undefined, reconcilePreparedRuntime: async () => undefined, - status: () => ({ activeVector: vector, descriptor, diagnostics: [], hmrReady: true, lastGoodVector: vector, state: 'active' as const }), + status: () => ({ activeVector: vector, descriptor, diagnostics: [], lastGoodVector: vector, state: 'active' as const }), surfaces: () => [surface], } as unknown as DevRuntimeSession; }, @@ -721,7 +715,7 @@ it('buffers synchronous startup activation until controller snapshots install', it('drops buffered startup lifecycle events after close or topology failure', async () => { const descriptor = { environmentVariables: [], id: 'fixture-runtime', label: 'Fixture runtime', schemaVersion: 1 } as const; - const prepared = { apps: [], provider: './src/dev/provider.ts', servers: [], sourceRevision: 'source-1' } as const; + const prepared = { provider: './src/dev/provider.ts', servers: [], sourceRevision: 'source-1' } as const; for (const transition of ['close', 'topology'] as const) { let closeCalls = 0; let resolveSession: ((session: DevRuntimeSession) => void) | undefined; @@ -752,7 +746,7 @@ it('drops buffered startup lifecycle events after close or topology failure', as resolveSession?.({ close: async () => { closeCalls += 1; }, reconcilePreparedRuntime: async () => undefined, - status: () => ({ activeVector: vector, descriptor, diagnostics: [], hmrReady: true, state: 'active' }), + status: () => ({ activeVector: vector, descriptor, diagnostics: [], state: 'active' }), surfaces: () => [surface], } as unknown as DevRuntimeSession); await starting; @@ -775,7 +769,7 @@ it('sanitizes a failed activation refresh without recursively publishing runtime artifactStatus: () => ({ state: 'missing' }), emit: (event) => { published.push(event.type); }, environment: {}, - preparedRuntime: { apps: [], provider: './src/dev/provider.ts', servers: [], sourceRevision: 'source-1' }, + preparedRuntime: { provider: './src/dev/provider.ts', servers: [], sourceRevision: 'source-1' }, projectRoot: '/workspace/project', provider: { descriptor, @@ -784,7 +778,7 @@ it('sanitizes a failed activation refresh without recursively publishing runtime return { close: async () => undefined, reconcilePreparedRuntime: async () => undefined, - status: () => ({ descriptor, diagnostics: [], hmrReady: activated, state: activated ? 'active' as const : 'compiling' as const }), + status: () => ({ descriptor, diagnostics: [], state: activated ? 'active' as const : 'compiling' as const }), surfaces: () => { if (activated) throw new Error('Activation surface snapshot failed.'); return []; @@ -816,7 +810,7 @@ it('aborts a timed-out provider start and closes a late session exactly once', a artifactStatus: () => ({ state: 'missing' }), emit: () => undefined, environment: {}, - preparedRuntime: { apps: [], provider: './src/dev/provider.ts', servers: [], sourceRevision: 'source-1' }, + preparedRuntime: { provider: './src/dev/provider.ts', servers: [], sourceRevision: 'source-1' }, projectRoot: '/workspace/project', provider: { descriptor: { environmentVariables: [], id: 'fixture-runtime', label: 'Fixture runtime', schemaVersion: 1 }, @@ -846,7 +840,7 @@ it('contains synchronous provider and malformed status failures as failed runtim artifactStatus: () => ({ state: 'missing' }), emit: () => undefined, environment: {}, - preparedRuntime: { apps: [], provider: './src/dev/provider.ts', servers: [], sourceRevision: 'source-1' }, + preparedRuntime: { provider: './src/dev/provider.ts', servers: [], sourceRevision: 'source-1' }, projectRoot: '/workspace/project', storageRoot: '/workspace/project/.agent-bundle/runtime', } as const; @@ -887,7 +881,7 @@ it('reconciles the newest revision exactly once after a deferred provider start artifactStatus: () => ({ state: 'missing' }), emit: () => undefined, environment: {}, - preparedRuntime: { apps: [], provider: './src/dev/provider.ts', servers: [], sourceRevision: 'source-1' }, + preparedRuntime: { provider: './src/dev/provider.ts', servers: [], sourceRevision: 'source-1' }, projectRoot: '/workspace/project', provider: { descriptor: { environmentVariables: [], id: 'fixture-runtime', label: 'Fixture runtime', schemaVersion: 1 }, @@ -899,7 +893,6 @@ it('reconciles the newest revision exactly once after a deferred provider start await new Promise((resolvePromise) => setImmediate(resolvePromise)); await controller.reconcilePreparedRuntime({ - apps: [], provider: './src/dev/provider.ts', servers: [], sourceRevision: 'source-2', @@ -907,7 +900,7 @@ it('reconciles the newest revision exactly once after a deferred provider start resolveSession?.({ close: async () => undefined, reconcilePreparedRuntime: async (prepared: DevRuntimePreparedProject) => { reconciled.push(prepared.sourceRevision); }, - status: () => ({ descriptor: { environmentVariables: [], id: 'fixture-runtime', label: 'Fixture runtime', schemaVersion: 1 }, diagnostics: [], hmrReady: true, state: 'active' }), + status: () => ({ descriptor: { environmentVariables: [], id: 'fixture-runtime', label: 'Fixture runtime', schemaVersion: 1 }, diagnostics: [], state: 'active' }), surfaces: () => [], } as unknown as DevRuntimeSession); @@ -922,14 +915,14 @@ it('latches a provider path change on an active session and revokes its run capa artifactStatus: () => ({ state: 'missing' }), emit: () => undefined, environment: {}, - preparedRuntime: { apps: [], provider: './src/dev/provider.ts', servers: [], sourceRevision: 'source-1' }, + preparedRuntime: { provider: './src/dev/provider.ts', servers: [], sourceRevision: 'source-1' }, projectRoot: '/workspace/project', provider: { descriptor, start: async () => ({ close: async () => undefined, runs: () => [], - status: () => ({ descriptor, diagnostics: [], hmrReady: true, state: 'active' }), + status: () => ({ descriptor, diagnostics: [], state: 'active' }), surfaces: () => [], }) as unknown as DevRuntimeSession, }, @@ -939,7 +932,6 @@ it('latches a provider path change on an active session and revokes its run capa expect(controller.runs(1)).toEqual([]); await controller.reconcileDeclaration({ - apps: [], provider: './src/dev/replaced-provider.ts', servers: [], sourceRevision: 'source-2', @@ -952,7 +944,7 @@ it('latches a provider path change on an active session and revokes its run capa it('latches every topology failure across a pending runtime start', async () => { const descriptor = { environmentVariables: [], id: 'fixture-runtime', label: 'Fixture runtime', schemaVersion: 1 } as const; - const prepared = { apps: [], provider: './src/dev/provider.ts', servers: [], sourceRevision: 'source-1' } as const; + const prepared = { provider: './src/dev/provider.ts', servers: [], sourceRevision: 'source-1' } as const; const topologyChanges: readonly Readonly<{ readonly apply: (controller: DevRuntimeController) => Promise; readonly name: string; @@ -982,7 +974,7 @@ it('latches every topology failure across a pending runtime start', async () => resolveSession?.({ close: async () => { closeCalls += 1; }, reconcilePreparedRuntime: async () => undefined, - status: () => ({ descriptor, diagnostics: [], hmrReady: true, state: 'active' }), + status: () => ({ descriptor, diagnostics: [], state: 'active' }), surfaces: () => [], } as unknown as DevRuntimeSession); await starting; @@ -996,7 +988,7 @@ it('latches every topology failure across a pending runtime start', async () => it('retains a topology failure when it races an accepted runtime reconcile', async () => { const descriptor = { environmentVariables: [], id: 'fixture-runtime', label: 'Fixture runtime', schemaVersion: 1 } as const; - const prepared = { apps: [], provider: './src/dev/provider.ts', servers: [], sourceRevision: 'source-1' } as const; + const prepared = { provider: './src/dev/provider.ts', servers: [], sourceRevision: 'source-1' } as const; let closeCalls = 0; let emit: Parameters[0]['emit'] | undefined; let resolveReconcile: (() => void) | undefined; @@ -1014,7 +1006,7 @@ it('retains a topology failure when it races an accepted runtime reconcile', asy return { close: async () => { closeCalls += 1; }, reconcilePreparedRuntime: async () => reconcileGate, - status: () => ({ descriptor, diagnostics: [], hmrReady: true, state: 'active' }), + status: () => ({ descriptor, diagnostics: [], state: 'active' }), surfaces: () => [], } as unknown as DevRuntimeSession; }, @@ -1037,7 +1029,7 @@ it('retains a topology failure when it races an accepted runtime reconcile', asy it('latches runtime removal and diagnostics as restart-required failures', async () => { const descriptor = { environmentVariables: [], id: 'fixture-runtime', label: 'Fixture runtime', schemaVersion: 1 } as const; - const prepared = { apps: [], provider: './src/dev/provider.ts', servers: [], sourceRevision: 'source-1' } as const; + const prepared = { provider: './src/dev/provider.ts', servers: [], sourceRevision: 'source-1' } as const; const controller = () => new DevRuntimeController({ artifactStatus: () => ({ state: 'missing' }), emit: () => undefined, @@ -1048,7 +1040,7 @@ it('latches runtime removal and diagnostics as restart-required failures', async descriptor, start: async () => ({ close: async () => undefined, - status: () => ({ descriptor, diagnostics: [], hmrReady: true, state: 'active' }), + status: () => ({ descriptor, diagnostics: [], state: 'active' }), surfaces: () => [], }) as unknown as DevRuntimeSession, }, @@ -1083,7 +1075,7 @@ it('observes a late provider close rejection and preserves it across a concurren artifactStatus: () => ({ state: 'missing' }), emit: () => undefined, environment: {}, - preparedRuntime: { apps: [], provider: './src/dev/provider.ts', servers: [], sourceRevision: 'source-1' }, + preparedRuntime: { provider: './src/dev/provider.ts', servers: [], sourceRevision: 'source-1' }, projectRoot: '/workspace/project', provider: { descriptor: { environmentVariables: [], id: 'fixture-runtime', label: 'Fixture runtime', schemaVersion: 1 }, diff --git a/packages/agent-bundle/tests/runtime-routes.test.ts b/packages/agent-bundle/tests/runtime-routes.test.ts index e1a5610e6..fce4432c1 100644 --- a/packages/agent-bundle/tests/runtime-routes.test.ts +++ b/packages/agent-bundle/tests/runtime-routes.test.ts @@ -68,7 +68,6 @@ const runtimeStatus = Object.freeze({ schemaVersion: 1, }, diagnostics: [], - hmrReady: false, state: 'active' as const, } satisfies DevRuntimeStatus); @@ -92,9 +91,6 @@ class MemoryRuntime implements DevRuntimeSession { this.#run = Object.freeze({ ...succeededRun, input: request.input, target: request.target }); return this.#run; } - async readAsset(): Promise { - return { body: new Uint8Array([1, 2, 3]), contentType: 'application/javascript; charset=utf-8' }; - } async readRunFlight(runId: string): Promise { return runId === this.#run.id ? { body: this.#flight, contentType: 'application/octet-stream' } @@ -120,7 +116,6 @@ class StartingRuntime extends MemoryRuntime { return { descriptor: runtimeStatus.descriptor, diagnostics: [], - hmrReady: false, state: 'starting', }; } @@ -510,8 +505,8 @@ it('rejects malformed, stale, undeclared, and excessive runtime inputs at the fi }); expect(tooLarge.status).toBe(413); - const traversal = await fetch(`${server.url}/api/runtime/assets/hook.after-edit/%2e%2e/main.js?generation=g1`, { headers }); - expect(traversal.status).toBe(400); + const removedAssets = await fetch(`${server.url}/api/runtime/assets/hook.after-edit/main.js?generation=g1`, { headers }); + expect(removedAssets.status).toBe(400); } finally { await server.close(); } @@ -532,7 +527,6 @@ it('accepts only the literal method and query matrix for every runtime route', a { acceptedMethod: 'GET', acceptedPath: '/api/runtime/runs/run-a/document', headers: privateHeaders, invalidMethod: 'POST', queryPath: '/api/runtime/runs/run-a/document?extra=1&extra=2' }, { acceptedMethod: 'POST', acceptedPath: '/api/runtime/runs/run-a/replay', body: JSON.stringify({ mode: 'exact', runId: 'run-a' }), headers: jsonHeaders, invalidMethod: 'GET', queryPath: '/api/runtime/runs/run-a/replay?extra=1&extra=2' }, { acceptedMethod: 'POST', acceptedPath: '/api/runtime/state/reset', body: JSON.stringify({ stateStoreId: 'state-a' }), headers: jsonHeaders, invalidMethod: 'GET', queryPath: '/api/runtime/state/reset?extra=1&extra=2' }, - { acceptedMethod: 'GET', acceptedPath: '/api/runtime/assets/hook.after-edit/main.js?generation=g1', headers: privateHeaders, invalidMethod: 'HEAD', queryPath: '/api/runtime/assets/hook.after-edit/main.js?generation=g1&generation=g2' }, ]; try { diff --git a/packages/workbench/src/runtime-client.ts b/packages/workbench/src/runtime-client.ts index 10ef5dc1f..4af44e5ea 100644 --- a/packages/workbench/src/runtime-client.ts +++ b/packages/workbench/src/runtime-client.ts @@ -1,5 +1,4 @@ import type { - DevRuntimeAssetRequest, DevRuntimeDiagnostic, DevRuntimeInspectionEnvelope, DevRuntimeInvocationRequest, @@ -37,7 +36,6 @@ export type RuntimeBootstrap = }>; const runtimeAssetLimit = 4 * 1024 * 1024; -const runtimeAssetContentTypes = new Set(['application/javascript', 'application/json', 'text/css', 'text/html']); const runtimeErrorCode = 'AB8206'; type RuntimeJsonValue = DevRuntimeInvocationRequest['input']; type RuntimeJsonObject = NonNullable; @@ -48,10 +46,6 @@ const diagnosticPhases = new Set([ 'rsc-render', 'flight-decode', 'lowering-contract', - 'mcp-protocol', - 'resource-selection', - 'sandbox/csp', - 'app-bridge', 'provider-lifecycle', ]); @@ -154,11 +148,11 @@ const diagnostic = (value: unknown): DevRuntimeDiagnostic => { const status = (value: unknown): DevRuntimeStatus => { const response = record(value); - if (!hasOnly(response, ['activeVector', 'descriptor', 'diagnostics', 'hmrReady', 'lastGoodVector', 'state']) || + if (!hasOnly(response, ['activeVector', 'descriptor', 'diagnostics', 'lastGoodVector', 'state']) || !isRecord(response.descriptor) || !hasOnly(response.descriptor, ['environmentVariables', 'id', 'label', 'schemaVersion']) || !Array.isArray(response.descriptor.environmentVariables) || response.descriptor.environmentVariables.some((entry) => !nonemptyString(entry)) || !nonemptyString(response.descriptor.id) || !nonemptyString(response.descriptor.label) || response.descriptor.schemaVersion !== 1 || - typeof response.hmrReady !== 'boolean' || !Array.isArray(response.diagnostics) || + !Array.isArray(response.diagnostics) || !['starting', 'compiling', 'active', 'degraded', 'failed', 'closed'].includes(response.state as string)) { throw invalid('Runtime route returned an invalid status.'); } @@ -171,7 +165,6 @@ const status = (value: unknown): DevRuntimeStatus => { schemaVersion: 1, }), diagnostics: Object.freeze(response.diagnostics.map(diagnostic)), - hmrReady: response.hmrReady, ...(response.lastGoodVector === undefined ? {} : { lastGoodVector: vector(response.lastGoodVector) }), state: response.state as DevRuntimeStatus['state'], }); @@ -181,7 +174,7 @@ const surface = (value: unknown): DevRuntimeSurface => { const response = record(value); if (!hasOnly(response, ['defaultTarget', 'fixtures', 'id', 'inputSchema', 'kind', 'label', 'readOnly', 'targets']) || (response.defaultTarget !== undefined && !nonemptyString(response.defaultTarget)) || !Array.isArray(response.fixtures) || - !nonemptyString(response.id) || !['hook', 'mcp-tool', 'mcp-resource', 'mcp-app'].includes(response.kind as string) || + !nonemptyString(response.id) || !['hook', 'mcp-tool', 'mcp-resource'].includes(response.kind as string) || !nonemptyString(response.label) || typeof response.readOnly !== 'boolean' || !Array.isArray(response.targets)) { throw invalid('Runtime route returned an invalid surface.'); } @@ -479,22 +472,6 @@ export class RuntimeClient { } } - async readAsset(request: DevRuntimeAssetRequest): Promise { - this.#requireProvider(); - if (request.path.length === 0) throw invalid('Runtime asset path is not valid.'); - try { - const path = request.path.map((segment) => opaqueSegment(segment, 'Runtime asset path segment')).join('/'); - return await this.#readBoundedBlob( - `/api/runtime/assets/${opaqueSegment(request.surfaceId, 'Runtime surface ID')}/${path}?generation=${opaqueSegment(request.runtimeGenerationId, 'Runtime generation ID')}`, - (contentType) => runtimeAssetContentTypes.has(contentType), - 'Runtime asset response is not valid.', - 'Runtime asset exceeds the allowed size.', - ); - } catch (error) { - throw runtimeError(error); - } - } - async #readBoundedBlob( path: string, allowedContentType: (contentType: string) => boolean, diff --git a/packages/workbench/tests/runtime-backend.test.ts b/packages/workbench/tests/runtime-backend.test.ts index 0fa9a152d..130944a21 100644 --- a/packages/workbench/tests/runtime-backend.test.ts +++ b/packages/workbench/tests/runtime-backend.test.ts @@ -93,7 +93,6 @@ const fixture = () => { activeVector: vector, descriptor: Object.freeze({ environmentVariables: Object.freeze([]), id: 'rsc', label: 'RSC', schemaVersion: 1 as const }), diagnostics: Object.freeze([]), - hmrReady: true, state: 'active' as const, }), surfaces: Object.freeze([surface]), diff --git a/packages/workbench/tests/runtime-client.test.ts b/packages/workbench/tests/runtime-client.test.ts index 84d05c567..8932a6955 100644 --- a/packages/workbench/tests/runtime-client.test.ts +++ b/packages/workbench/tests/runtime-client.test.ts @@ -34,7 +34,6 @@ const status = Object.freeze({ activeVector: vector, descriptor: Object.freeze({ environmentVariables: [], id: 'rsc', label: 'RSC', schemaVersion: 1 as const }), diagnostics: Object.freeze([]), - hmrReady: true, lastGoodVector: vector, state: 'active' as const, }) satisfies DevRuntimeStatus; @@ -42,7 +41,7 @@ const status = Object.freeze({ const surface = Object.freeze({ fixtures: Object.freeze([{ id: 'fixture-a', label: 'Fixture A' }]), id: 'app-weather', - kind: 'mcp-app' as const, + kind: 'mcp-tool' as const, label: 'Weather App', readOnly: false, targets: Object.freeze(['portable']), @@ -91,7 +90,6 @@ const deferred = (): Deferred => { }; const runtimeFetch = (options: { - readonly asset?: Response; readonly document?: Response; readonly flight?: Response; readonly runs?: readonly DevRuntimeRun[]; @@ -126,9 +124,6 @@ const runtimeFetch = (options: { }], }); } - if (url === '/api/runtime/assets/app-weather/assets/weather%20app.js?generation=generation%20a') { - return options.asset ?? new Response(new Uint8Array([1, 2, 3]), { headers: { 'content-type': 'application/javascript' } }); - } throw new Error(`Unexpected route request ${url}.`); }, requests, @@ -320,7 +315,6 @@ it('rejects every runtime mutation and protected read before an available bootst await expect(client.readRun('run a')).rejects.toMatchObject({ code: 'AB8201' }); await expect(client.replayRun({ mode: 'exact', runId: 'run a' })).rejects.toMatchObject({ code: 'AB8201' }); await expect(client.resetState({ stateStoreId: 'state-a' })).rejects.toMatchObject({ code: 'AB8201' }); - await expect(client.readAsset({ path: ['assets', 'weather app.js'], runtimeGenerationId: 'generation a', surfaceId: 'app-weather' })).rejects.toMatchObject({ code: 'AB8201' }); expect(fixture.requests).toEqual([]); }); @@ -366,13 +360,11 @@ it('uses the exact imported request bodies and encoded opaque runtime paths', as await client.readRun('run a'); await client.replayRun({ expectedGenerationId: 'generation-a', mode: 'exact', runId: 'run a' }); await client.resetState({ expectedGenerationId: 'generation-a', seed: { city: 'London' }, stateStoreId: 'state-a' }); - await expect(client.readAsset({ path: ['assets', 'weather app.js'], runtimeGenerationId: 'generation a', surfaceId: 'app-weather' })).resolves.toBeInstanceOf(Blob); await expect(client.readRunFlight('run a')).resolves.toBeInstanceOf(Blob); expect(fixture.requests.map((request) => request.url)).toContain('/api/runtime/runs/run%20a'); expect(fixture.requests.map((request) => request.url)).toContain('/api/runtime/runs/run%20a/flight'); expect(fixture.requests.map((request) => request.url)).toContain('/api/runtime/runs/run%20a/replay'); - expect(fixture.requests.map((request) => request.url)).toContain('/api/runtime/assets/app-weather/assets/weather%20app.js?generation=generation%20a'); expect(fixture.requests.find((request) => request.url === '/api/runtime/runs')?.body).toBe( '{"expectedGenerationId":"generation-a","fixtureId":"fixture-a","input":{"city":"London"},"surfaceId":"app-weather","target":"portable"}', ); @@ -419,23 +411,6 @@ it('surfaces a complete sanitized generation conflict without retrying the prote expect(runs).toBe(1); }); -it('rejects oversized, untyped, or unsupported protected assets', async () => { - const oversized = new Response(new Uint8Array(4 * 1024 * 1024 + 1), { headers: { 'content-type': 'application/javascript' } }); - const missingType = new Response(new Uint8Array([1])); - const unsupportedType = new Response(new Uint8Array([1]), { headers: { 'content-type': 'text/plain' } }); - - for (const asset of [oversized, missingType, unsupportedType]) { - const fixture = runtimeFetch({ asset }); - const client = new RuntimeClient(new ForegroundRouteClient({ fetch: fixture.fetch })); - await client.bootstrap(); - await expect(client.readAsset({ - path: ['assets', 'weather app.js'], - runtimeGenerationId: 'generation a', - surfaceId: 'app-weather', - })).rejects.toMatchObject({ code: 'AB8206' }); - } -}); - it('rejects oversized or mistyped protected Flight payloads', async () => { const oversized = new Response(new Uint8Array(4 * 1024 * 1024 + 1), { headers: { 'content-type': 'application/octet-stream' } }); const wrongType = new Response(new Uint8Array([1]), { headers: { 'content-type': 'application/json' } }); diff --git a/packages/workbench/tests/runtime-contract-compile.test.ts b/packages/workbench/tests/runtime-contract-compile.test.ts index de29b91bd..8dd0293bf 100644 --- a/packages/workbench/tests/runtime-contract-compile.test.ts +++ b/packages/workbench/tests/runtime-contract-compile.test.ts @@ -1,7 +1,6 @@ import { expect, it } from '@rstest/core'; import type { - DevRuntimeAssetRequest, DevRuntimeDiagnostic, DevRuntimeInspectionEnvelope, DevRuntimeInvocationRequest, @@ -56,7 +55,7 @@ const surface = { fixtures: [{ id: 'fixture-a', label: 'Fixture A', seed: { city: 'London' } }], id: 'app/weather', inputSchema: { type: 'object' }, - kind: 'mcp-app', + kind: 'mcp-tool', label: 'Weather App', readOnly: false, targets: ['portable'], @@ -108,7 +107,6 @@ const status = { activeVector: vector, descriptor: { environmentVariables: ['NODE_ENV'], id: 'rsc', label: 'RSC Runtime', schemaVersion: 1 }, diagnostics: [diagnostic], - hmrReady: true, lastGoodVector: vector, state: 'active', } satisfies DevRuntimeStatus; @@ -133,12 +131,6 @@ const reset = { stateStoreId: 'state-a', } satisfies DevRuntimeStateResetRequest; -const asset = { - path: ['assets', 'weather.js'], - runtimeGenerationId: 'generation-a', - surfaceId: 'app/weather', -} satisfies DevRuntimeAssetRequest; - const statusResponse = { status } satisfies DevRuntimeStatusResponse; const surfacesResponse = { surfaces: [surface] } satisfies DevRuntimeSurfacesResponse; const runResponse = { run } satisfies DevRuntimeRunResponse; @@ -214,7 +206,6 @@ it('compiles RuntimeClient against the exact provider wire contract', async () = const effect: RuntimePendingEffect | undefined = effectFor(confirmed); expect({ - asset, appPreviewFixture, bootstrap, controlledInput, diff --git a/packages/workbench/tests/runtime-controller.test.ts b/packages/workbench/tests/runtime-controller.test.ts index 52a700cbb..185e00911 100644 --- a/packages/workbench/tests/runtime-controller.test.ts +++ b/packages/workbench/tests/runtime-controller.test.ts @@ -36,7 +36,6 @@ const status = Object.freeze({ schemaVersion: 1 as const, }), diagnostics: Object.freeze([]), - hmrReady: true, lastGoodVector: vector, state: 'active' as const, }) satisfies DevRuntimeStatus; diff --git a/packages/workbench/tests/runtime-model.test.ts b/packages/workbench/tests/runtime-model.test.ts index ba8e5c10f..a2edbca59 100644 --- a/packages/workbench/tests/runtime-model.test.ts +++ b/packages/workbench/tests/runtime-model.test.ts @@ -40,7 +40,6 @@ const status = (overrides: Partial = {}): DevRuntimeStatus => activeVector: vector(), descriptor: { environmentVariables: [], id: 'rsc', label: 'RSC Runtime', schemaVersion: 1 }, diagnostics: [], - hmrReady: true, lastGoodVector: vector(), state: 'active', ...overrides, @@ -54,7 +53,7 @@ const surface = (overrides: Partial = {}): DevRuntimeSurface ], id: 'weather', inputSchema: { type: 'object' }, - kind: 'mcp-app', + kind: 'mcp-tool', label: 'Weather', readOnly: false, targets: ['portable', 'chatgpt'], @@ -525,7 +524,7 @@ it('parses running and failed provider runs, optional runtime metadata, reset id await expect(foreign.readRun('foreign')).rejects.toMatchObject({ code: 'AB8206' }); }); -it('rejects invalid wrapper, opaque path, and asset headers through the public RuntimeClient boundary', async () => { +it('rejects invalid wrappers and opaque paths through the public RuntimeClient boundary', async () => { await expect(clientFor({ '/api/runtime/status': { nope: true } }).bootstrap()).rejects.toMatchObject({ code: 'AB8206' }); const bootstrappedRoutes = { '/api/runtime/runs?limit=50': { providerSessionId: 'provider-a', runs: [run('initial')] }, @@ -535,15 +534,6 @@ it('rejects invalid wrapper, opaque path, and asset headers through the public R const opaqueClient = clientFor({ ...bootstrappedRoutes, '/api/runtime/runs/..': { run: run('x') } }); await opaqueClient.bootstrap(); await expect(opaqueClient.readRun('..')).rejects.toMatchObject({ code: 'AB8206' }); - const assetClient = clientFor({ - ...bootstrappedRoutes, - '/api/runtime/assets/weather/assets/main.js?generation=generation-a': new Response(new Uint8Array([1]), { - headers: { 'content-length': '-1', 'content-type': 'application/javascript' }, - }), - }); - await assetClient.bootstrap(); - await expect(assetClient.readAsset({ path: ['assets', 'main.js'], runtimeGenerationId: 'generation-a', surfaceId: 'weather' })).rejects.toMatchObject({ code: 'AB8206' }); - await expect(assetClient.readAsset({ path: [], runtimeGenerationId: 'generation-a', surfaceId: 'weather' })).rejects.toMatchObject({ code: 'AB8206' }); }); it('covers runtime reducer invalid controls, ordered read effects, and settled lifecycle branches', () => { From cbbf82459ab8cebd218bc516057381fed0ce4aea Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Fri, 25 Sep 2026 06:19:00 +0000 Subject: [PATCH 2/3] Fix example README and prepared-runtime assertions --- examples/rsc-agent-runtime/README.md | 7 ++++--- .../tests/dev-provider.integration.test.ts | 6 +++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/examples/rsc-agent-runtime/README.md b/examples/rsc-agent-runtime/README.md index b976636f5..8568b6c03 100644 --- a/examples/rsc-agent-runtime/README.md +++ b/examples/rsc-agent-runtime/README.md @@ -86,9 +86,10 @@ compiles the `rsc` and `widget` environments into a runtime generation and publishes its hook, MCP tool, and resource surfaces to the Workbench runtime routes; no browser connects to its loopback Rsbuild server. The `app` environment is compiled only by the production build. The session uses -development entries and output roots while compiling every environment in -production mode, because production decoders cannot read development Flight -payloads. `@rsbuild/plugin-react` is configured as +development entries and output roots while compiling every +environment in production mode, because +production decoders cannot read development +Flight payloads. `@rsbuild/plugin-react` is configured as `pluginReact({ fastRefresh: false })`, so no refresh runtime is injected into the self-contained App document. The `widget` and `app` web environments set `overrideBrowserslist: ['chrome >= 144']` for the Chromium MCP App hosts diff --git a/examples/rsc-agent-runtime/tests/dev-provider.integration.test.ts b/examples/rsc-agent-runtime/tests/dev-provider.integration.test.ts index de6b2e24c..6a080cd83 100644 --- a/examples/rsc-agent-runtime/tests/dev-provider.integration.test.ts +++ b/examples/rsc-agent-runtime/tests/dev-provider.integration.test.ts @@ -294,10 +294,10 @@ test('declares an optional runtime while keeping Claude and Codex artifacts buil const prepared = await new ProjectService({ includeDevRuntime: true, mode: 'development', root }).prepare('dev'); expect(prepared.source.state).toBe('ready'); - expect(prepared.devRuntime).toMatchObject({ - apps: [expect.objectContaining({ name: 'timeline', resourceUri: 'ui://rsc-agent-runtime/edit-timeline-v1.html' })], + expect(prepared.devRuntime).toEqual({ provider: './src/dev/provider.ts', - servers: [expect.objectContaining({ name: 'timeline', transport: 'stdio' })], + servers: [{ id: 'mcp:timeline', name: 'timeline', targets: ['claude', 'codex', 'portable'] }], + sourceRevision: expect.any(String), }); // The example declares one semantic event route (src/events/tool/after.tsx) // that serves both hosts through the generated native wrappers. From 73d262fb7f399d7425c070266aa056b4012218e0 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Fri, 25 Sep 2026 06:23:56 +0000 Subject: [PATCH 3/3] Add PR number to changeset --- .changeset/remove-runtime-app-assets.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/remove-runtime-app-assets.md b/.changeset/remove-runtime-app-assets.md index 0514439c8..1c9099dc6 100644 --- a/.changeset/remove-runtime-app-assets.md +++ b/.changeset/remove-runtime-app-assets.md @@ -2,4 +2,4 @@ 'agent-bundle': minor --- -Remove the development runtime App-asset path: `DevRuntimeSession.readAsset`, the `/api/runtime/assets` route (now a 400 invalid path), the `DevRuntimeAssetRequest` and `DevRuntimePreparedMcpApp` exports, the prepared-runtime `apps` input, `DevRuntimeStatus.hmrReady`, the `mcp-app` runtime surface kind, the `mcp-protocol`, `resource-selection`, `sandbox/csp`, and `app-bridge` diagnostic phases, and the launch and credential fields on `DevRuntimePreparedMcpServer`, which is now `{ id, name, targets }`. +Remove the development runtime App-asset path: `DevRuntimeSession.readAsset`, the `/api/runtime/assets` route (now a 400 invalid path), the `DevRuntimeAssetRequest` and `DevRuntimePreparedMcpApp` exports, the prepared-runtime `apps` input, `DevRuntimeStatus.hmrReady`, the `mcp-app` runtime surface kind, the `mcp-protocol`, `resource-selection`, `sandbox/csp`, and `app-bridge` diagnostic phases, and the launch and credential fields on `DevRuntimePreparedMcpServer`, which is now `{ id, name, targets }`. (#856)