History

Enhance runtime configuration options in executor. Added support for specifying execution runtime in `.executor/executor.jsonc` with options: "quickjs", "ses", and "deno". Updated documentation and tests to reflect these changes. Implemented runtime selection logic in the control plane.

Rhys Sullivan committed 8 months ago

Commit 661ed291 parent e4deb6811 files · +260 −7Browse files

Unified · Split

11 files
  1. .changeset/bumpy-cloths-pay.md +5 −0
  2. ARCHITECTURE.md +2 −0
  3. README.md +11 −1
  4. apps/docs/developer/tool-catalog-and-execution.mdx +10 −0
  5. apps/executor/src/distribution/artifact.ts +12 −0
  6. apps/executor/src/distribution/distribution.test.ts +21 −4
  7. packages/control-plane/src/runtime/execution-runtime-config.test.ts +137 −0
  8. packages/control-plane/src/runtime/local-config.test.ts +17 −0
  9. packages/control-plane/src/runtime/local-config.ts +1 −0
  10. packages/control-plane/src/runtime/workspace-execution-environment.ts +36 −2
  11. packages/control-plane/src/schema/models/local-config.ts +8 −0
.changeset/bumpy-cloths-pay.md+5−0
@@ -0,0 +1,5 @@
1---
2"executor": patch
3---
4
5Support selecting runtime
ARCHITECTURE.md+2−0
@@ -117,6 +117,8 @@ and runs user-authored code against that environment. · 116 unmodified lines
117 117
118The default runtime executes code inside a QuickJS WebAssembly sandbox so tool calls stay proxied through the control plane.118The default runtime executes code inside a QuickJS WebAssembly sandbox so tool calls stay proxied through the control plane.
119 119
120The workspace can override that in `.executor/executor.jsonc` with `runtime: "quickjs" | "ses" | "deno"`.
121
120### Adapter packages122### Adapter packages
121 123
122Several packages exist to turn external systems into callable tools:124Several packages exist to turn external systems into callable tools:
README.md+11−1
@@ -228,11 +228,20 @@ At a high level, every execution follows the same loop: · 227 unmodified lines
228 228
2291. `executor` resolves the current local installation and workspace.2291. `executor` resolves the current local installation and workspace.
2302. It builds a tool catalog from built-in tools plus all connected workspace sources.2302. It builds a tool catalog from built-in tools plus all connected workspace sources.
2313. It runs your TypeScript inside the QuickJS sandbox runtime by default.2313. It runs your TypeScript inside the configured sandbox runtime. QuickJS is the default, and `.executor/executor.jsonc` can set `"runtime": "quickjs" | "ses" | "deno"`.
2324. Tool calls are dispatched through `executor` rather than directly from your code.2324. Tool calls are dispatched through `executor` rather than directly from your code.
2335. If a tool needs interaction, the run pauses and records a pending interaction.2335. If a tool needs interaction, the run pauses and records a pending interaction.
2346. Once the interaction is resolved, the execution continues and eventually completes or fails.2346. Once the interaction is resolved, the execution continues and eventually completes or fails.
235 235
236Example:
237
238```jsonc
239{
240 "runtime": "ses",
241 "sources": {}
242}
243```
244
236This gives you a stable surface for agent automation:245This gives you a stable surface for agent automation:
237 246
238- the agent sees a coherent catalog247- the agent sees a coherent catalog
@@ -317,6 +326,7 @@ If you are exploring the repo, these are the directories that matter most: · 78 unmodified lines
317- `apps/web`: local React web UI326- `apps/web`: local React web UI
318- `packages/server`: local HTTP server that serves API, MCP, and UI327- `packages/server`: local HTTP server that serves API, MCP, and UI
319- `packages/control-plane`: source management, secrets, persistence, execution, and inspection328- `packages/control-plane`: source management, secrets, persistence, execution, and inspection
329- `packages/runtime-deno-subprocess`: optional Deno subprocess runtime for TypeScript execution
320- `packages/runtime-quickjs`: default QuickJS sandbox runtime for TypeScript execution330- `packages/runtime-quickjs`: default QuickJS sandbox runtime for TypeScript execution
321- `packages/runtime-ses`: optional SES sandbox runtime for TypeScript execution331- `packages/runtime-ses`: optional SES sandbox runtime for TypeScript execution
322- `packages/executor-mcp`: MCP bridge for `execute` and `resume`332- `packages/executor-mcp`: MCP bridge for `execute` and `resume`
apps/docs/developer/tool-catalog-and-execution.mdx+10−0
@@ -7,6 +7,16 @@ Executor turns connected integrations into a workspace tool catalog. · 6 unmodified lines
7 7
8That catalog is the thing the runtime actually executes against.8That catalog is the thing the runtime actually executes against.
9 9
10The execution sandbox is selected from `.executor/executor.jsonc`:
11
12```jsonc
13{
14 "runtime": "quickjs"
15}
16```
17
18Supported values are `quickjs` (default), `ses`, and `deno`.
19
10## How tools are produced20## How tools are produced
11 21
12At a high level:22At a high level:
apps/executor/src/distribution/artifact.ts+12−0
@@ -225,6 +225,18 @@ export const buildDistributionPackage = async ( · 224 unmodified lines
225 join(repoRoot, "packages/runtime-deno-subprocess/src/deno-subprocess-worker.mjs"),225 join(repoRoot, "packages/runtime-deno-subprocess/src/deno-subprocess-worker.mjs"),
226 join(binDir, "deno-subprocess-worker.mjs"),226 join(binDir, "deno-subprocess-worker.mjs"),
227 );227 );
228 await runCommand({
229 command: "bun",
230 args: [
231 "build",
232 "./packages/runtime-ses/src/sandbox-worker.mjs",
233 "--target",
234 "node",
235 "--outfile",
236 join(binDir, "sandbox-worker.mjs"),
237 ],
238 cwd: repoRoot,
239 });
228 await writeFile(join(packageDir, "package.json"), createPackageJson({240 await writeFile(join(packageDir, "package.json"), createPackageJson({
229 packageName,241 packageName,
230 packageVersion,242 packageVersion,
apps/executor/src/distribution/distribution.test.ts+21−4
@@ -10,12 +10,16 @@ describe("distribution flow", () => { · 9 unmodified lines
10 const verifyInstallFlow = <R>(10 const verifyInstallFlow = <R>(
11 runCommand: (11 runCommand: (
12 args: ReadonlyArray<string>,12 args: ReadonlyArray<string>,
13 ) => Effect.Effect<{ stdout: string }, Error, R>,13 options?: {
14 readonly okExitCodes?: ReadonlyArray<number>;
15 },
16 ) => Effect.Effect<{ stdout: string; stderr: string }, Error, R>,
14 ) =>17 ) =>
15 Effect.gen(function* () {18 Effect.gen(function* () {
16 const harness = yield* DistributionHarness;19 const harness = yield* DistributionHarness;
17 20
18 yield* harness.writeProjectConfig(`{21 yield* harness.writeProjectConfig(`{
22 "runtime": "ses",
19 // local workspace config23 // local workspace config
20 "sources": {},24 "sources": {},
21}25}
@@ -65,6 +69,17 @@ describe("distribution flow", () => { · 43 unmodified lines
65 accountId: string;69 accountId: string;
66 };70 };
67 71
72 const sesCall = yield* runCommand(
73 [
74 "call",
75 'await fetch("https://example.com"); return 1;',
76 "--base-url",
77 harness.baseUrl,
78 ],
79 { okExitCodes: [1] },
80 );
81 expect(sesCall.stderr).toContain("fetch is disabled in SES executor");
82
68 yield* runCommand(["down", "--base-url", harness.baseUrl]);83 yield* runCommand(["down", "--base-url", harness.baseUrl]);
69 yield* runCommand(["up", "--base-url", harness.baseUrl]);84 yield* runCommand(["up", "--base-url", harness.baseUrl]);
70 85
@@ -84,11 +99,13 @@ describe("distribution flow", () => { · 13 unmodified lines
84 });99 });
85 100
86 it.live("boots a staged package artifact in a fresh home", () =>101 it.live("boots a staged package artifact in a fresh home", () =>
87 verifyInstallFlow((args) => Effect.flatMap(DistributionHarness, (harness) => harness.run(args)))102 verifyInstallFlow((args, options) =>
103 Effect.flatMap(DistributionHarness, (harness) => harness.run(args, options))
104 )
88 .pipe(Effect.provide(LocalDistributionHarnessLive)), 240_000);105 .pipe(Effect.provide(LocalDistributionHarnessLive)), 240_000);
89 106
90 it.live("boots an npm-installed package in a fresh home", () =>107 it.live("boots an npm-installed package in a fresh home", () =>
91 verifyInstallFlow((args) =>108 verifyInstallFlow((args, options) =>
92 Effect.flatMap(DistributionHarness, (harness) => harness.runInstalled(args))109 Effect.flatMap(DistributionHarness, (harness) => harness.runInstalled(args, options))
93 ).pipe(Effect.provide(LocalDistributionHarnessLive)), 240_000);110 ).pipe(Effect.provide(LocalDistributionHarnessLive)), 240_000);
94});111});
packages/control-plane/src/runtime/execution-runtime-config.test.ts+137−0
@@ -0,0 +1,137 @@
1import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
2import { tmpdir } from "node:os";
3import { join } from "node:path";
4import { describe, expect, it } from "@effect/vitest";
5import * as Effect from "effect/Effect";
6
7import {
8 createControlPlaneRuntime,
9} from "./index";
10import { withControlPlaneClient } from "./test-http-client";
11
12const writeProjectConfig = (
13 workspaceRoot: string,
14 config: Record<string, unknown>,
15) => {
16 const configDirectory = join(workspaceRoot, ".executor");
17 mkdirSync(configDirectory, { recursive: true });
18 writeFileSync(
19 join(configDirectory, "executor.jsonc"),
20 `${JSON.stringify(config, null, 2)}\n`,
21 "utf8",
22 );
23};
24
25const makeRuntime = (config: Record<string, unknown>) => {
26 const workspaceRoot = mkdtempSync(join(tmpdir(), "executor-runtime-config-"));
27 writeProjectConfig(workspaceRoot, config);
28
29 return Effect.acquireRelease(
30 createControlPlaneRuntime({
31 workspaceRoot,
32 homeConfigPath: join(workspaceRoot, ".executor-home.jsonc"),
33 homeStateDirectory: join(workspaceRoot, ".executor-home-state"),
34 }),
35 (runtime) => Effect.promise(() => runtime.close()).pipe(Effect.orDie),
36 );
37};
38
39describe("execution runtime config", () => {
40 it.scoped("defaults to QuickJS when no runtime is configured", () =>
41 Effect.gen(function* () {
42 const runtime = yield* makeRuntime({
43 sources: {},
44 });
45 const installation = runtime.localInstallation;
46
47 const execution = yield* withControlPlaneClient(
48 {
49 runtime,
50 accountId: installation.accountId,
51 },
52 (client) =>
53 client.executions.create({
54 path: {
55 workspaceId: installation.workspaceId,
56 },
57 payload: {
58 code: 'await fetch("https://example.com"); return 1;',
59 },
60 }),
61 );
62
63 expect(execution.execution.status).toBe("failed");
64 expect(execution.execution.errorText).toContain(
65 "fetch is disabled in QuickJS executor",
66 );
67 }),
68 60_000,
69 );
70
71 it.scoped("uses the SES runtime when configured in executor.jsonc", () =>
72 Effect.gen(function* () {
73 const runtime = yield* makeRuntime({
74 runtime: "ses",
75 sources: {},
76 });
77 const installation = runtime.localInstallation;
78
79 const execution = yield* withControlPlaneClient(
80 {
81 runtime,
82 accountId: installation.accountId,
83 },
84 (client) =>
85 client.executions.create({
86 path: {
87 workspaceId: installation.workspaceId,
88 },
89 payload: {
90 code: 'await fetch("https://example.com"); return 1;',
91 },
92 }),
93 );
94
95 expect(execution.execution.status).toBe("failed");
96 expect(execution.execution.errorText).toContain(
97 "fetch is disabled in SES executor",
98 );
99 }),
100 60_000,
101 );
102
103 it.scoped("uses the Deno runtime when configured in executor.jsonc", () =>
104 Effect.gen(function* () {
105 const runtime = yield* makeRuntime({
106 runtime: "deno",
107 sources: {},
108 });
109 const installation = runtime.localInstallation;
110
111 const execution = yield* withControlPlaneClient(
112 {
113 runtime,
114 accountId: installation.accountId,
115 },
116 (client) =>
117 client.executions.create({
118 path: {
119 workspaceId: installation.workspaceId,
120 },
121 payload: {
122 code: 'return typeof Deno !== "undefined";',
123 },
124 }),
125 );
126
127 if (execution.execution.status === "completed") {
128 expect(execution.execution.resultJson).toBe("true");
129 return;
130 }
131
132 expect(execution.execution.status).toBe("failed");
133 expect(execution.execution.errorText).toContain("Install Deno or set DENO_BIN");
134 }),
135 60_000,
136 );
137});
packages/control-plane/src/runtime/local-config.test.ts+17−0
@@ -8,6 +8,7 @@ import * as Effect from "effect/Effect"; · 7 unmodified lines
8 8
9import {9import {
10 loadLocalExecutorConfig,10 loadLocalExecutorConfig,
11 mergeLocalExecutorConfigs,
11 resolveDefaultHomeConfigCandidates,12 resolveDefaultHomeConfigCandidates,
12 resolveDefaultHomeStateDirectory,13 resolveDefaultHomeStateDirectory,
13 resolveHomeConfigPath,14 resolveHomeConfigPath,
@@ -27,6 +28,7 @@ describe("local-config", () => { · 13 unmodified lines
27 writeFile(28 writeFile(
28 join(configDirectory, "executor.jsonc"),29 join(configDirectory, "executor.jsonc"),
29 `{30 `{
31 "runtime": "ses",
30 // local workspace config32 // local workspace config
31 "sources": {33 "sources": {
32 "github": {34 "github": {
@@ -52,6 +54,7 @@ describe("local-config", () => { · 19 unmodified lines
52 expect(loaded.config?.sources?.github?.connection.endpoint).toBe(54 expect(loaded.config?.sources?.github?.connection.endpoint).toBe(
53 "https://api.github.com",55 "https://api.github.com",
54 );56 );
57 expect(loaded.config?.runtime).toBe("ses");
55 expect(context.homeStateDirectory).toContain("executor");58 expect(context.homeStateDirectory).toContain("executor");
56 }).pipe(Effect.provide(NodeFileSystem.layer)),59 }).pipe(Effect.provide(NodeFileSystem.layer)),
57 );60 );
@@ -146,4 +149,18 @@ describe("local-config", () => { · 88 unmodified lines
146 expect(resolvedPath).toBe(join(legacyConfigDirectory, "executor.jsonc"));149 expect(resolvedPath).toBe(join(legacyConfigDirectory, "executor.jsonc"));
147 }).pipe(Effect.provide(NodeFileSystem.layer)),150 }).pipe(Effect.provide(NodeFileSystem.layer)),
148 );151 );
152
153 it("lets project config override the merged runtime", () => {
154 const merged = mergeLocalExecutorConfigs(
155 {
156 runtime: "quickjs",
157 sources: {},
158 },
159 {
160 runtime: "deno",
161 },
162 );
163
164 expect(merged?.runtime).toBe("deno");
165 });
149});166});
packages/control-plane/src/runtime/local-config.ts+1−0
@@ -258,6 +258,7 @@ export const mergeLocalExecutorConfigs = ( · 257 unmodified lines
258 }258 }
259 259
260 return decodeLocalExecutorConfig({260 return decodeLocalExecutorConfig({
261 runtime: extra?.runtime ?? base?.runtime,
261 workspace: {262 workspace: {
262 ...(base?.workspace ?? {}),263 ...(base?.workspace ?? {}),
263 ...(extra?.workspace ?? {}),264 ...(extra?.workspace ?? {}),
packages/control-plane/src/runtime/workspace-execution-environment.ts+36−2
@@ -1,4 +1,5 @@
1import {1import {
2 type CodeExecutor,
2 createSystemToolMap,3 createSystemToolMap,
3 createToolCatalogFromEntries,4 createToolCatalogFromEntries,
4 createToolCatalogFromTools,5 createToolCatalogFromTools,
@@ -9,8 +10,15 @@ import { · 4 unmodified lines
9 type ToolInvoker,10 type ToolInvoker,
10 type ToolPath,11 type ToolPath,
11} from "@executor/codemode-core";12} from "@executor/codemode-core";
13import { makeDenoSubprocessExecutor } from "@executor/runtime-deno-subprocess";
12import { makeQuickJsExecutor } from "@executor/runtime-quickjs";14import { makeQuickJsExecutor } from "@executor/runtime-quickjs";
13import type { AccountId, Source } from "#schema";15import { makeSesExecutor } from "@executor/runtime-ses";
16import type {
17 AccountId,
18 LocalExecutorConfig,
19 LocalExecutorRuntime,
20 Source,
21} from "#schema";
14import * as Context from "effect/Context";22import * as Context from "effect/Context";
15import * as Either from "effect/Either";23import * as Either from "effect/Either";
16import * as Effect from "effect/Effect";24import * as Effect from "effect/Effect";
@@ -67,6 +75,26 @@ import { · 50 unmodified lines
67 75
68const asToolPath = (value: string): ToolPath => value as ToolPath;76const asToolPath = (value: string): ToolPath => value as ToolPath;
69 77
78const DEFAULT_EXECUTION_RUNTIME: LocalExecutorRuntime = "quickjs";
79
80export const resolveConfiguredExecutionRuntime = (
81 config: LocalExecutorConfig | null | undefined,
82): LocalExecutorRuntime => config?.runtime ?? DEFAULT_EXECUTION_RUNTIME;
83
84export const createCodeExecutorForRuntime = (
85 runtime: LocalExecutorRuntime,
86): CodeExecutor => {
87 switch (runtime) {
88 case "deno":
89 return makeDenoSubprocessExecutor();
90 case "ses":
91 return makeSesExecutor();
92 case "quickjs":
93 default:
94 return makeQuickJsExecutor();
95 }
96};
97
70const tokenize = (value: string): string[] =>98const tokenize = (value: string): string[] =>
71 value99 value
72 .trim()100 .trim()
@@ -652,6 +680,10 @@ export const createWorkspaceExecutionEnvironmentResolver = (input: { · 579 unmodified lines
652 ({ workspaceId, accountId, onElicitation }) =>680 ({ workspaceId, accountId, onElicitation }) =>
653 Effect.gen(function* () {681 Effect.gen(function* () {
654 const runtimeLocalWorkspace = yield* getRuntimeLocalWorkspaceOption();682 const runtimeLocalWorkspace = yield* getRuntimeLocalWorkspaceOption();
683 const loadedConfig =
684 runtimeLocalWorkspace === null
685 ? null
686 : yield* input.workspaceConfigStore.load(runtimeLocalWorkspace.context);
655 const localToolRuntime =687 const localToolRuntime =
656 runtimeLocalWorkspace === null688 runtimeLocalWorkspace === null
657 ? {689 ? {
@@ -675,7 +707,9 @@ export const createWorkspaceExecutionEnvironmentResolver = (input: { · 17 unmodified lines
675 onElicitation,707 onElicitation,
676 });708 });
677 709
678 const executor = makeQuickJsExecutor();710 const executor = createCodeExecutorForRuntime(
711 resolveConfiguredExecutionRuntime(loadedConfig?.config),
712 );
679 713
680 return {714 return {
681 executor,715 executor,
packages/control-plane/src/schema/models/local-config.ts+8−0
@@ -2,6 +2,12 @@ import { Schema } from "effect"; · 1 unmodified line
2 2
3import { SourceTransportSchema, StringMapSchema } from "./source";3import { SourceTransportSchema, StringMapSchema } from "./source";
4 4
5export const LocalExecutorRuntimeSchema = Schema.Literal(
6 "quickjs",
7 "ses",
8 "deno",
9);
10
5export const LocalConfigSecretProviderSourceSchema = Schema.Literal(11export const LocalConfigSecretProviderSourceSchema = Schema.Literal(
6 "env",12 "env",
7 "file",13 "file",
@@ -156,6 +162,7 @@ export const LocalConfigWorkspaceSchema = Schema.Struct({ · 148 unmodified lines
156});162});
157 163
158export const LocalExecutorConfigSchema = Schema.Struct({164export const LocalExecutorConfigSchema = Schema.Struct({
165 runtime: Schema.optional(LocalExecutorRuntimeSchema),
159 workspace: Schema.optional(LocalConfigWorkspaceSchema),166 workspace: Schema.optional(LocalConfigWorkspaceSchema),
160 sources: Schema.optional(167 sources: Schema.optional(
161 Schema.Record({168 Schema.Record({
@@ -182,4 +189,5 @@ export type LocalConfigSecretInput = typeof LocalConfigSecretInputSchema.Type; · 20 unmodified lines
182export type LocalConfigSource = typeof LocalConfigSourceSchema.Type;189export type LocalConfigSource = typeof LocalConfigSourceSchema.Type;
183export type LocalConfigPolicy = typeof LocalConfigPolicySchema.Type;190export type LocalConfigPolicy = typeof LocalConfigPolicySchema.Type;
184export type LocalConfigSecrets = typeof LocalConfigSecretsSchema.Type;191export type LocalConfigSecrets = typeof LocalConfigSecretsSchema.Type;
192export type LocalExecutorRuntime = typeof LocalExecutorRuntimeSchema.Type;
185export type LocalExecutorConfig = typeof LocalExecutorConfigSchema.Type;193export type LocalExecutorConfig = typeof LocalExecutorConfigSchema.Type;