Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
2567d78
Add artifact table and core artifacts API
RhysSullivan Jul 28, 2026
d3f0f8b
Add MCP-Apps shell package and first-party render-ui tools
RhysSullivan Jul 28, 2026
1b7bd7c
Add artifact tool tests and port the shell browser suite
RhysSullivan Jul 28, 2026
0bd7cdb
Ship the MCP-Apps shell in binaries and revive the sunpeak e2e harness
RhysSullivan Jul 28, 2026
6858efb
Add the Artifacts console pages and embed the MCP-Apps shell
RhysSullivan Jul 28, 2026
7432bac
Add artifacts e2e scenarios
RhysSullivan Jul 28, 2026
5a66ede
Load the artifact shell in the browser only
RhysSullivan Jul 28, 2026
57c9819
Host the artifact shell in an iframe over the MCP-Apps protocol
RhysSullivan Jul 28, 2026
9d5486f
Connect the artifact shell bridge when the frame mounts, not on load
RhysSullivan Jul 28, 2026
001b961
Use the prod execution cap for interactive dev instances
RhysSullivan Jul 28, 2026
6f3f2ef
Make artifact code purely declarative tools.*
RhysSullivan Jul 28, 2026
9d88c21
Scope artifact deep links to the session's org
RhysSullivan Jul 28, 2026
11ee201
Reject React hooks called inside loop bodies in artifact code
RhysSullivan Jul 28, 2026
b5041f3
Rename the render-ui tool to create-artifact
RhysSullivan Jul 28, 2026
cd55319
Style artifacts with executor's design system
RhysSullivan Jul 28, 2026
b02a1d1
Fill the viewport when an artifact is shorter than it
RhysSullivan Jul 29, 2026
f823411
Bind artifact tool paths to connections instead of pinning them
RhysSullivan Jul 29, 2026
2a79907
Use the integration placeholder in the artifacts e2e comment
RhysSullivan Jul 29, 2026
24730d1
Update an artifact in place by passing its id to create-artifact
RhysSullivan Jul 29, 2026
db38001
Reject chart primitives that no ChartContainer wraps
RhysSullivan Jul 29, 2026
e15812b
Trial-render an artifact before saving it
RhysSullivan Jul 29, 2026
a143790
Skip the artifact smoke render where the runtime forbids codegen
RhysSullivan Jul 29, 2026
f543148
Cover artifact tweak-in-place and the refused render in e2e
RhysSullivan Jul 29, 2026
9a0af9e
Record artifact updates and create-time render checking
RhysSullivan Jul 29, 2026
ef5612e
Show artifacts as a preview card grid
RhysSullivan Jul 29, 2026
0e2e54e
Run the artifact smoke render in a sandbox
RhysSullivan Jul 29, 2026
5107e23
Load QuickJS-WASM on cloud
RhysSullivan Jul 29, 2026
9f65995
Leave the generated harness bundle unformatted
RhysSullivan Jul 29, 2026
92aea90
Forward execute options and settled-check through cloud engine decora…
RhysSullivan Jul 29, 2026
22a0cb6
Make an artifact approval survive the request that paused it
RhysSullivan Jul 29, 2026
17c27a1
Cover approving a destructive action from an artifact
RhysSullivan Jul 29, 2026
af584ec
Pass scenario options to the unskipped run-panel scenario
RhysSullivan Jul 29, 2026
ccf435a
Degrade an expired approval into a next action
RhysSullivan Jul 29, 2026
77c0390
Say what the approval modal is for, and let a tool be trusted once
RhysSullivan Jul 29, 2026
4c523ca
Keep the smoke render's markup instead of discarding it
RhysSullivan Jul 29, 2026
8fc9027
Add an allowlist sanitizer for artifact preview markup
RhysSullivan Jul 29, 2026
67f6212
Store an artifact preview on the row
RhysSullivan Jul 29, 2026
8e2b774
Save a layout preview when an artifact is created
RhysSullivan Jul 29, 2026
b095e16
Draw the artifact's real layout on its gallery card
RhysSullivan Jul 29, 2026
1b3be00
Upgrade the preview to real pixels once an artifact settles
RhysSullivan Jul 29, 2026
deb0de2
Read the host through a ref when relaying a preview
RhysSullivan Jul 29, 2026
57ab17c
Keep the shell's build graph out of the console bundle
RhysSullivan Jul 29, 2026
ab61e0f
Measure the preview scale instead of computing it in CSS
RhysSullivan Jul 29, 2026
56005b6
Compile the layout tracks a preview needs
RhysSullivan Jul 29, 2026
c2b612b
Scan the document, not the bundle, for external references
RhysSullivan Jul 29, 2026
0612084
Capture the settled render as markup, not pixels
RhysSullivan Jul 29, 2026
57ec2ac
Give an artifact a viewport to lay out against
RhysSullivan Jul 29, 2026
4f70d09
Teach the app layout an artifact can now use
RhysSullivan Jul 29, 2026
4cb3f7f
Pin both sides of the display-mode contract in the browser
RhysSullivan Jul 29, 2026
ee7426b
Assert the detail page behaves like an app, not a document
RhysSullivan Jul 29, 2026
947f8cb
Add an artifacts opt-out for MCP connections
RhysSullivan Jul 29, 2026
0efdf00
Hold one loading surface from navigation to rendered artifact
RhysSullivan Jul 29, 2026
8f7937e
Land the skeleton on the artifact's own pixels
RhysSullivan Jul 29, 2026
2bd8791
Pin both sides of the render-signal contract in a real browser
RhysSullivan Jul 29, 2026
4fb6a7d
Report a paint the host asked about after the fact
RhysSullivan Jul 29, 2026
5e76f9f
Wait on one blank block instead of the last saved render
RhysSullivan Jul 29, 2026
9f4c420
Make MCP artifacts opt-in per connection
RhysSullivan Jul 29, 2026
a8b9af4
Fix artifact shell production packaging
RhysSullivan Jul 29, 2026
77303c5
Defer artifact shell dev build
RhysSullivan Jul 29, 2026
b0575c6
Load shell bundler on demand
RhysSullivan Jul 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/olive-crabs-repeat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"executor": patch
---

Add an Artifacts tab. Interactive components an agent generates with `render-ui` are saved and listed in the console, and each one has its own page that renders it live — the page an MCP client without MCP Apps support deep-links to. Artifacts can be renamed and deleted from the console, and agents find them again by title.
1 change: 1 addition & 0 deletions .oxfmtrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"*.tsbuildinfo",
"executor-*.tgz",
"**/routeTree.gen.ts",
"**/smoke-harness-bundle.gen.ts",
"apps/cloud/src/services/executor-schema.ts"
]
}
25 changes: 25 additions & 0 deletions .oxlintrc.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,31 @@
"executor/no-try-catch-or-throw": "off",
},
},
{
// boundary: the MCP-Apps shell is browser code, not Effect domain code.
// It runs inside a sandboxed iframe and talks to the host over
// postMessage and the MCP bridge — both of which are throw/Promise APIs
// with no Effect runtime present. `new Function` evaluation of generated
// JSX, CSS harvesting off `document.styleSheets`, and JSON on the wire
// all surface untyped JS errors that must be caught and rendered rather
// than modelled. Scoped to the iframe payload plus its build config; the
// package's Effect-facing loader (`src/shell-html.ts`) is deliberately
// NOT listed and uses inline suppressions instead.
"files": [
"packages/hosts/mcp-apps-shell/src/shell/**/*.{ts,tsx}",
"packages/hosts/mcp-apps-shell/vite.config.shell.ts",
],
"rules": {
"executor/no-double-cast": "off",
"executor/no-error-constructor": "off",
"executor/no-instanceof-error": "off",
"executor/no-json-parse": "off",
"executor/no-promise-catch": "off",
"executor/no-promise-reject": "off",
"executor/no-try-catch-or-throw": "off",
"executor/no-unknown-error-message": "off",
},
},
{
"files": ["packages/kernel/core/src/code-recovery.ts"],
"rules": {
Expand Down
31 changes: 31 additions & 0 deletions apps/cli/src/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,32 @@ const resolveQuickJsWasmPath = (): string => {
return wasmPath;
};

// ---------------------------------------------------------------------------
// MCP-Apps shell (mcp-app.html)
//
// The MCP host serves the shell as the `ui://executor/shell.html` resource, and
// `@executor-js/mcp-apps-shell` reads it from disk at runtime. That runtime
// `fs.readFile` is invisible to `bun build --compile`, so without this the
// packaged binary would serve the "Shell not built" placeholder and every
// generated UI would render blank. Build it if missing and copy it next to the
// executable, where the loader finds it via `process.execPath` — the same
// colocation trick used for `libsql.node` / `emscripten-module.wasm`.
// ---------------------------------------------------------------------------

const mcpAppsShellDir = resolve(repoRoot, "packages/hosts/mcp-apps-shell");
const mcpAppsShellHtml = join(mcpAppsShellDir, "dist/mcp-app.html");

const ensureMcpAppsShell = async (): Promise<string> => {
if (!existsSync(mcpAppsShellHtml)) {
console.log("Building MCP-Apps shell (mcp-app.html)...");
await $`bun run --cwd ${mcpAppsShellDir} build:shell`.quiet();
}
if (!existsSync(mcpAppsShellHtml)) {
throw new Error(`MCP-Apps shell missing at ${mcpAppsShellHtml} after build:shell`);
}
return mcpAppsShellHtml;
};

const resolveOnePasswordCoreWasmPath = (): string => {
const req = createRequire(join(repoRoot, "packages/plugins/onepassword/package.json"));
const sdkPkg = req.resolve("@1password/sdk/package.json");
Expand Down Expand Up @@ -398,6 +424,7 @@ const buildBinaries = async (targets: Target[], mode: BuildMode) => {
await writeFile(embeddedMigrationsPath, `${embeddedMigrations}\n`);

const quickJsWasmPath = resolveQuickJsWasmPath();
const mcpAppsShellPath = await ensureMcpAppsShell();
const onePasswordCoreWasmPath = resolveOnePasswordCoreWasmPath();
const workerBundlerDistPath = resolveWorkerBundlerDistPath();
const packedWorkerBundlerSource = await createPackedWorkerBundlerSource(workerBundlerDistPath);
Expand All @@ -423,6 +450,10 @@ const buildBinaries = async (targets: Target[], mode: BuildMode) => {
// Copy QuickJS WASM next to binary — loaded at runtime by the server
await cp(quickJsWasmPath, join(binDir, "emscripten-module.wasm"));

// Copy the MCP-Apps shell next to the binary. Platform-independent HTML,
// read from execDir at runtime by @executor-js/mcp-apps-shell.
await cp(mcpAppsShellPath, join(binDir, "mcp-app.html"));

// Copy 1Password's sdk-core WASM next to the binary. The patched
// sdk-core loader falls back to this sibling file when bun --compile
// bakes a build-machine node_modules path into __dirname.
Expand Down
52 changes: 40 additions & 12 deletions apps/cli/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1337,14 +1337,19 @@ const runBackgroundDaemonStart = (input: {
// Stdio MCP session: a pure stdio <-> HTTP bridge to the owning local daemon.
// ---------------------------------------------------------------------------

const mcpUrlForActiveLocalServer = (
connection: ExecutorServerConnection,
elicitationMode: "browser" | "model",
): URL => {
const url = new URL("/mcp", connection.origin);
if (elicitationMode === "browser") {
const mcpUrlForActiveLocalServer = (input: {
readonly connection: ExecutorServerConnection;
readonly elicitationMode: "browser" | "model";
readonly artifacts: boolean;
}): URL => {
const url = new URL("/mcp", input.connection.origin);
if (input.elicitationMode === "browser") {
url.searchParams.set("elicitation_mode", "browser");
}
// Only the opt-in is spelled out; the default endpoint stays clean.
if (input.artifacts) {
url.searchParams.set("artifacts", "true");
}
return url;
};

Expand All @@ -1359,11 +1364,16 @@ const mcpUrlForActiveLocalServer = (
const runMcpHttpBridge = async (input: {
readonly manifest: ExecutorLocalServerManifest;
readonly elicitationMode: "browser" | "model";
readonly artifacts: boolean;
}): Promise<void> => {
const stdio = new StdioServerTransport();
const authorization = getExecutorServerAuthorizationHeader(input.manifest.connection);
const http = new StreamableHTTPClientTransport(
mcpUrlForActiveLocalServer(input.manifest.connection, input.elicitationMode),
mcpUrlForActiveLocalServer({
connection: input.manifest.connection,
elicitationMode: input.elicitationMode,
artifacts: input.artifacts,
}),
authorization ? { requestInit: { headers: { Authorization: authorization } } } : undefined,
);

Expand Down Expand Up @@ -1438,7 +1448,10 @@ const runMcpHttpBridge = async (input: {
}
};

const runStdioMcpSession = (input: { readonly elicitationMode: "browser" | "model" }) =>
const runStdioMcpSession = (input: {
readonly elicitationMode: "browser" | "model";
readonly artifacts: boolean;
}) =>
Effect.gen(function* () {
// `executor mcp` never owns the local database. If a local server is already
// running, bridge this stdio client to it; otherwise ensure a durable
Expand All @@ -1450,7 +1463,11 @@ const runStdioMcpSession = (input: { readonly elicitationMode: "browser" | "mode
const active = yield* readActiveLocalServerManifest();
if (active) {
yield* Effect.promise(() =>
runMcpHttpBridge({ manifest: active, elicitationMode: input.elicitationMode }),
runMcpHttpBridge({
manifest: active,
elicitationMode: input.elicitationMode,
artifacts: input.artifacts,
}),
);
return;
}
Expand All @@ -1472,7 +1489,11 @@ const runStdioMcpSession = (input: { readonly elicitationMode: "browser" | "mode
);
}
yield* Effect.promise(() =>
runMcpHttpBridge({ manifest: elected, elicitationMode: input.elicitationMode }),
runMcpHttpBridge({
manifest: elected,
elicitationMode: input.elicitationMode,
artifacts: input.artifacts,
}),
);
});

Expand Down Expand Up @@ -2781,11 +2802,18 @@ const mcpCommand = Command.make(
"Choose the stdio approval flow: browser approval or a CLI resume tool exposed to the model.",
),
),
artifacts: Options.boolean("artifacts")
.pipe(Options.withDefault(false))
.pipe(
Options.withDescription(
"Serve the artifact surface on this connection: the artifact tools, the app shell resource, and the artifact skills. Withheld by default.",
),
),
},
({ scope, elicitationMode }) =>
({ scope, elicitationMode, artifacts }) =>
Effect.gen(function* () {
applyScope(scope);
yield* runStdioMcpSession({ elicitationMode });
yield* runStdioMcpSession({ elicitationMode, artifacts });
}),
).pipe(Command.withDescription("Start an MCP server over stdio"));

Expand Down
15 changes: 15 additions & 0 deletions apps/cloud/drizzle/0013_curly_rocket_racer.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
CREATE TABLE "artifact" (
"id" varchar(255) NOT NULL,
"title" text NOT NULL,
"description" text,
"code" text NOT NULL,
"created_at" timestamp NOT NULL,
"updated_at" timestamp NOT NULL,
"row_id" varchar(255) PRIMARY KEY NOT NULL,
"tenant" varchar(255) NOT NULL,
"owner" varchar(255) NOT NULL,
"subject" varchar(255) NOT NULL
);
--> statement-breakpoint
ALTER TABLE "definition" ALTER COLUMN "name" SET DATA TYPE varchar(255);--> statement-breakpoint
CREATE UNIQUE INDEX "artifact_uidx" ON "artifact" USING btree ("tenant","owner","subject","id");
1 change: 1 addition & 0 deletions apps/cloud/drizzle/0014_new_karma.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE "artifact" ADD COLUMN "bindings" json;
1 change: 1 addition & 0 deletions apps/cloud/drizzle/0015_equal_the_leader.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE "artifact" ADD COLUMN "preview" text;
Loading
Loading