History

fmt

Rhys Sullivan committed 6 months ago

Commit c92c57d1 parent 87ab5c7265 files · +5705 −8140Browse files

Showing the changes to notes (16 of 265 files). Show all

Unified · Split

16 files
  1. notes/artifacts-workflows-and-generated-ui.md +45 −71
  2. notes/dynamic-plugin-loading-v1.md +177 −187
  3. notes/mcp-conn-pool.md +9 −7
  4. notes/old/connections-migration.md +1 −1
  5. notes/old/error-handling.md +4 −4
  6. notes/old/errored-sources.md +2 −1
  7. notes/old/indexes.md +1 −1
  8. notes/old/mcp-testing.md +6 −6
  9. notes/old/pluggable-storage.md +45 −37
  10. notes/old/promise-sdk-typed-errors.md +3 −1
  11. notes/old/rls.md +3 −3
  12. notes/old/scoped-source-auth.md +2 −7
  13. notes/old/scopes.md +6 −6
  14. notes/old/search.md +6 −2
  15. notes/old/storage-migration.md +23 −15
  16. notes/research/plugins/manifest-schema.md +92 −94
notes/artifacts-workflows-and-generated-ui.md+45−71
@@ -64,13 +64,7 @@ Do not make this workflow-specific. Model the generic thing as an artifact · 63 unmodified lines
64project, with workflows as the first consumer.64project, with workflows as the first consumer.
65 65
66```ts66```ts
67type ArtifactKind =67type ArtifactKind = "workflow" | "component" | "ui" | "template" | "connector" | "document";
68 | "workflow"
69 | "component"
70 | "ui"
71 | "template"
72 | "connector"
73 | "document";
74 68
75type ArtifactProject = {69type ArtifactProject = {
76 id: string;70 id: string;
@@ -171,18 +165,13 @@ export interface ArtifactStore { · 94 unmodified lines
171 input: CreateArtifactProjectInput,165 input: CreateArtifactProjectInput,
172 ) => Effect.Effect<ArtifactProject, ArtifactError>;166 ) => Effect.Effect<ArtifactProject, ArtifactError>;
173 167
174 readonly readFile: (168 readonly readFile: (ref: ArtifactRef, path: string) => Effect.Effect<Uint8Array, ArtifactError>;
175 ref: ArtifactRef,
176 path: string,
177 ) => Effect.Effect<Uint8Array, ArtifactError>;
178 169
179 readonly writeFiles: (170 readonly writeFiles: (
180 input: WriteArtifactFilesInput,171 input: WriteArtifactFilesInput,
181 ) => Effect.Effect<ArtifactCommit, ArtifactError>;172 ) => Effect.Effect<ArtifactCommit, ArtifactError>;
182 173
183 readonly diff: (174 readonly diff: (input: ArtifactDiffInput) => Effect.Effect<ArtifactDiff, ArtifactError>;
184 input: ArtifactDiffInput,
185 ) => Effect.Effect<ArtifactDiff, ArtifactError>;
186 175
187 readonly forkProject: (176 readonly forkProject: (
188 input: ForkArtifactProjectInput,177 input: ForkArtifactProjectInput,
@@ -365,10 +354,7 @@ export interface ArtifactStore { · 176 unmodified lines
365 name: string;354 name: string;
366 }) => Effect.Effect<ArtifactProject, ArtifactError>;355 }) => Effect.Effect<ArtifactProject, ArtifactError>;
367 356
368 readonly readFile: (357 readonly readFile: (ref: ArtifactRef, path: string) => Effect.Effect<Uint8Array, ArtifactError>;
369 ref: ArtifactRef,
370 path: string,
371 ) => Effect.Effect<Uint8Array, ArtifactError>;
372 358
373 readonly writeFiles: (input: {359 readonly writeFiles: (input: {
374 projectId: string;360 projectId: string;
@@ -422,10 +408,7 @@ Feature plugin: · 47 unmodified lines
422```ts408```ts
423// @executor/workflows409// @executor/workflows
424import { definePlugin, type PluginCtx } from "@executor/sdk";410import { definePlugin, type PluginCtx } from "@executor/sdk";
425import {411import { ArtifactStoreProtocol, type ArtifactsProtocolCtx } from "@executor/artifacts";
426 ArtifactStoreProtocol,
427 type ArtifactsProtocolCtx,
428} from "@executor/artifacts";
429 412
430type WorkflowCtx<TStore> = PluginCtx<TStore, ArtifactsProtocolCtx>;413type WorkflowCtx<TStore> = PluginCtx<TStore, ArtifactsProtocolCtx>;
431 414
@@ -546,56 +529,47 @@ export async function summarizeTicket(input: SupportEmail) { · 114 unmodified lines
546E2E flow:529E2E flow:
547 530
548```ts531```ts
549it.effect(532it.effect("creates a code-first workflow artifact and indexes a visual graph", () =>
550 "creates a code-first workflow artifact and indexes a visual graph",533 Effect.gen(function* () {
551 () =>534 const executor = yield* createExecutor({
552 Effect.gen(function* () {535 scopes: [testScope],
553 const executor = yield* createExecutor({536 adapter,
554 scopes: [testScope],537 blobs,
555 adapter,538 plugins: [artifactsLocalGitPlugin({ rootDir: tempDir }), workflowsPlugin()] as const,
556 blobs,539 protocols: {
557 plugins: [540 "executor.artifacts.store": "artifacts-local-git",
558 artifactsLocalGitPlugin({ rootDir: tempDir }),541 },
559 workflowsPlugin(),542 });
560 ] as const,543
561 protocols: {544 const created = yield* executor.workflows.create({
562 "executor.artifacts.store": "artifacts-local-git",545 name: "Support triage",
546 files: [
547 {
548 type: "put",
549 path: "artifact.json",
550 contents: JSON.stringify({
551 schemaVersion: 1,
552 kind: "workflow",
553 entrypoints: ["src/workflows/support-triage.workflow.ts"],
554 }),
555 },
556 {
557 type: "put",
558 path: "src/workflows/support-triage.workflow.ts",
559 contents: supportTriageWorkflowSource,
563 },560 },
564 });561 ],
565 562 });
566 const created = yield* executor.workflows.create({563
567 name: "Support triage",564 const source = yield* executor.artifacts.readFile(
568 files: [565 { projectId: created.project.id, ref: created.commit.id },
569 {566 "src/workflows/support-triage.workflow.ts",
570 type: "put",567 );
571 path: "artifact.json",568 expect(new TextDecoder().decode(source)).toContain('"use workflow"');
572 contents: JSON.stringify({569
573 schemaVersion: 1,570 expect(created.manifest.entrypoints).toEqual(["src/workflows/support-triage.workflow.ts"]);
574 kind: "workflow",571 expect(created.manifest.graph.nodes.map((node) => node.kind)).toContain("step");
575 entrypoints: ["src/workflows/support-triage.workflow.ts"],572 }),
576 }),
577 },
578 {
579 type: "put",
580 path: "src/workflows/support-triage.workflow.ts",
581 contents: supportTriageWorkflowSource,
582 },
583 ],
584 });
585
586 const source = yield* executor.artifacts.readFile(
587 { projectId: created.project.id, ref: created.commit.id },
588 "src/workflows/support-triage.workflow.ts",
589 );
590 expect(new TextDecoder().decode(source)).toContain('"use workflow"');
591
592 expect(created.manifest.entrypoints).toEqual([
593 "src/workflows/support-triage.workflow.ts",
594 ]);
595 expect(created.manifest.graph.nodes.map((node) => node.kind)).toContain(
596 "step",
597 );
598 }),
599);573);
600```574```
601 575
notes/dynamic-plugin-loading-v1.md+177−187
@@ -89,7 +89,7 @@ Surveyed five plugin systems in `.reference/`: · 88 unmodified lines
89- **emdash** — **closest match to our setup.** Astro/Vite + React, plugins are89- **emdash** — **closest match to our setup.** Astro/Vite + React, plugins are
90 npm packages with separate `./` and `./admin` exports, registered via90 npm packages with separate `./` and `./admin` exports, registered via
91 import-and-call in `astro.config.mjs`. `definePlugin({ id, hooks, routes,91 import-and-call in `astro.config.mjs`. `definePlugin({ id, hooks, routes,
92 admin })` declarative shape.92admin })` declarative shape.
93- **dynamic-software** — most ambitious; Cloudflare Worker Loader for cloud93- **dynamic-software** — most ambitious; Cloudflare Worker Loader for cloud
94 isolation, iframe + postMessage RPC for UI, Proxy-based typed API client.94 isolation, iframe + postMessage RPC for UI, Proxy-based typed API client.
95 95
@@ -109,10 +109,10 @@ inference flows naturally; no codegen step. · 13 unmodified lines
109 109
110```ts110```ts
111// apps/local/executor.config.ts (after)111// apps/local/executor.config.ts (after)
112import { defineExecutorConfig } from "@executor-js/sdk"112import { defineExecutorConfig } from "@executor-js/sdk";
113import { openApiPlugin } from "@executor-js/plugin-openapi"113import { openApiPlugin } from "@executor-js/plugin-openapi";
114import { mcpPlugin } from "@executor-js/plugin-mcp"114import { mcpPlugin } from "@executor-js/plugin-mcp";
115import { fileSecretsPlugin } from "@executor-js/plugin-file-secrets"115import { fileSecretsPlugin } from "@executor-js/plugin-file-secrets";
116 116
117export default defineExecutorConfig({117export default defineExecutorConfig({
118 dialect: "sqlite",118 dialect: "sqlite",
@@ -121,7 +121,7 @@ export default defineExecutorConfig({ · 2 unmodified lines
121 mcpPlugin({ dangerouslyAllowStdioMCP: true }),121 mcpPlugin({ dangerouslyAllowStdioMCP: true }),
122 fileSecretsPlugin(),122 fileSecretsPlugin(),
123 ] as const,123 ] as const,
124})124});
125```125```
126 126
127"Dynamic" means npm-installable, not load-by-string-name. Same model as emdash.127"Dynamic" means npm-installable, not load-by-string-name. Same model as emdash.
@@ -137,12 +137,12 @@ config becomes a need. · 9 unmodified lines
137 "type": "module",137 "type": "module",
138 "exports": {138 "exports": {
139 "./server": "./dist/server.js",139 "./server": "./dist/server.js",
140 "./client": "./dist/client.js"140 "./client": "./dist/client.js",
141 },141 },
142 "executor": {142 "executor": {
143 "id": "foo",143 "id": "foo",
144 "version": "0.1.0"144 "version": "0.1.0",
145 }145 },
146}146}
147```147```
148 148
@@ -171,7 +171,7 @@ Client side: separate primitive `defineClientPlugin` lives in · 22 unmodified lines
171React types never leak into server bundles.171React types never leak into server bundles.
172 172
173**Layering — extension is the canonical SDK; routes/handlers is optional HTTP transport.**173**Layering — extension is the canonical SDK; routes/handlers is optional HTTP transport.**
174The HTTP layer is *not a peer* of the SDK; it's a transport over it. Plugin174The HTTP layer is _not a peer_ of the SDK; it's a transport over it. Plugin
175authors should treat extension as the implementation and write handlers as175authors should treat extension as the implementation and write handlers as
176thin wrappers that delegate via the `self` parameter (same pattern as176thin wrappers that delegate via the `self` parameter (same pattern as
177`staticSources(self) => [...]` already in the codebase). This keeps:177`staticSources(self) => [...]` already in the codebase). This keeps:
@@ -188,7 +188,7 @@ Three plugin shapes fall out of this layering: · 10 unmodified lines
188 utility for other plugins or scripts. CLI/embedded consumers use188 utility for other plugins or scripts. CLI/embedded consumers use
189 `executor.<id>.method()`. Vite plugin notices no `./client` and skips the189 `executor.<id>.method()`. Vite plugin notices no `./client` and skips the
190 plugin in the frontend bundle entirely.190 plugin in the frontend bundle entirely.
191- **Both.** Extension *and* routes/handlers *and* a `./client`. Examples:191- **Both.** Extension _and_ routes/handlers _and_ a `./client`. Examples:
192 openapi, mcp, anything that needs a frontend. Routes are thin wrappers192 openapi, mcp, anything that needs a frontend. Routes are thin wrappers
193 over extension methods.193 over extension methods.
194- **HTTP-only.** Rare — webhook receivers, OAuth callback URLs. Routes194- **HTTP-only.** Rare — webhook receivers, OAuth callback URLs. Routes
@@ -214,7 +214,7 @@ Plugin authors can write a complete plugin importing only from · 19 unmodified lines
214`@executor-js/sdk` (server) and `@executor-js/sdk/client` (frontend). The SDK214`@executor-js/sdk` (server) and `@executor-js/sdk/client` (frontend). The SDK
215re-exports `Schema`, `HttpApi`, `HttpApiGroup`, `HttpApiEndpoint`,215re-exports `Schema`, `HttpApi`, `HttpApiGroup`, `HttpApiEndpoint`,
216`HttpApiBuilder`, `Effect` so authors who don't want to dig into Effect216`HttpApiBuilder`, `Effect` so authors who don't want to dig into Effect
217don't have to. Authors who *do* want Effect-native code keep importing from217don't have to. Authors who _do_ want Effect-native code keep importing from
218`effect` directly. This keeps the door open without forcing the dependency.218`effect` directly. This keeps the door open without forcing the dependency.
219 219
220### 6. Skip `capabilities` declaration for v1220### 6. Skip `capabilities` declaration for v1
@@ -247,7 +247,7 @@ already handles this exact pattern for secrets and works fine: · 26 unmodified lines
247secretProviders: (ctx) => [makeScopedProvider(...)]247secretProviders: (ctx) => [makeScopedProvider(...)]
248```248```
249 249
250Each new pluggable capability gets its *own* typed field on `PluginSpec`,250Each new pluggable capability gets its _own_ typed field on `PluginSpec`,
251same shape. When artifacts lands, that's `artifactStore: () =>251same shape. When artifacts lands, that's `artifactStore: () =>
252ArtifactStore`. If connection providers want to be modeled this way, the252ArtifactStore`. If connection providers want to be modeled this way, the
253existing `connectionProviders` field already is.253existing `connectionProviders` field already is.
@@ -335,24 +335,21 @@ server both import it: · 81 unmodified lines
335 335
336```ts336```ts
337// @executor-js/plugin-foo/src/shared.ts337// @executor-js/plugin-foo/src/shared.ts
338import { HttpApiEndpoint, HttpApiGroup, Schema } from "@executor-js/sdk"338import { HttpApiEndpoint, HttpApiGroup, Schema } from "@executor-js/sdk";
339 339
340export const Thing = Schema.Struct({340export const Thing = Schema.Struct({
341 id: Schema.String,341 id: Schema.String,
342 name: Schema.String,342 name: Schema.String,
343})343});
344 344
345export const FooApi = HttpApiGroup.make("foo")345export const FooApi = HttpApiGroup.make("foo")
346 .add(346 .add(HttpApiEndpoint.get("listThings")`/things`.addSuccess(Schema.Array(Thing)))
347 HttpApiEndpoint.get("listThings")`/things`
348 .addSuccess(Schema.Array(Thing)),
349 )
350 .add(347 .add(
351 HttpApiEndpoint.post("syncThing")`/sync/:id`348 HttpApiEndpoint.post("syncThing")`/sync/:id`
352 .setPath(Schema.Struct({ id: Schema.String }))349 .setPath(Schema.Struct({ id: Schema.String }))
353 .addSuccess(Thing)350 .addSuccess(Thing)
354 .addError(SyncError),351 .addError(SyncError),
355 )352 );
356```353```
357 354
358```ts355```ts
@@ -429,49 +426,47 @@ Lives in `@executor-js/sdk/client`. Server bundles cannot import this module. · 70 unmodified lines
429```ts426```ts
430// packages/core/sdk/src/client.ts427// packages/core/sdk/src/client.ts
431export interface ClientPluginSpec<TId extends string = string> {428export interface ClientPluginSpec<TId extends string = string> {
432 readonly id: TId429 readonly id: TId;
433 430
434 /** Pages contributed to the host's TanStack router. Mounted under431 /** Pages contributed to the host's TanStack router. Mounted under
435 * /plugins/{id}/{path}. Sidebar nav metadata declared on the route. */432 * /plugins/{id}/{path}. Sidebar nav metadata declared on the route. */
436 readonly pages?: readonly PageDecl[]433 readonly pages?: readonly PageDecl[];
437 434
438 /** Dashboard / overview widgets the host can render in known slots. */435 /** Dashboard / overview widgets the host can render in known slots. */
439 readonly widgets?: readonly WidgetDecl[]436 readonly widgets?: readonly WidgetDecl[];
440 437
441 /** Components the host can render in named slots (e.g., source-detail438 /** Components the host can render in named slots (e.g., source-detail
442 * panels, secret-picker variants). Slot names are part of the host439 * panels, secret-picker variants). Slot names are part of the host
443 * contract — plugin opts in by registering. */440 * contract — plugin opts in by registering. */
444 readonly slots?: Record<string, SlotComponent>441 readonly slots?: Record<string, SlotComponent>;
445 442
446 /** Source-plugin contribution — populated by plugins that expose443 /** Source-plugin contribution — populated by plugins that expose
447 * `kind` rows in the core `source` table (openapi, mcp, graphql,444 * `kind` rows in the core `source` table (openapi, mcp, graphql,
448 * google-discovery). The host's sources page derives its provider445 * google-discovery). The host's sources page derives its provider
449 * list from the union of every loaded plugin's `sourcePlugin`446 * list from the union of every loaded plugin's `sourcePlugin`
450 * via `useSourcePlugins()`. */447 * via `useSourcePlugins()`. */
451 readonly sourcePlugin?: SourcePlugin448 readonly sourcePlugin?: SourcePlugin;
452 449
453 /** Secret-provider-plugin contribution — populated by plugins that450 /** Secret-provider-plugin contribution — populated by plugins that
454 * also ship a `secretProviders` server-side capability AND want to451 * also ship a `secretProviders` server-side capability AND want to
455 * expose a settings card on the host's secrets page. Surfaced via452 * expose a settings card on the host's secrets page. Surfaced via
456 * `useSecretProviderPlugins()`. */453 * `useSecretProviderPlugins()`. */
457 readonly secretProviderPlugin?: SecretProviderPlugin454 readonly secretProviderPlugin?: SecretProviderPlugin;
458}455}
459 456
460type PageDecl = {457type PageDecl = {
461 path: string // "/", "/edit/$id"458 path: string; // "/", "/edit/$id"
462 component: ComponentType459 component: ComponentType;
463 nav?: { label: string; section?: string }460 nav?: { label: string; section?: string };
464}461};
465 462
466type WidgetDecl = {463type WidgetDecl = {
467 id: string464 id: string;
468 component: ComponentType<WidgetProps>465 component: ComponentType<WidgetProps>;
469 size?: "half" | "full"466 size?: "half" | "full";
470}467};
471 468
472export const defineClientPlugin = <TId extends string>(469export const defineClientPlugin = <TId extends string>(spec: ClientPluginSpec<TId>) => spec;
473 spec: ClientPluginSpec<TId>,
474) => spec
475```470```
476 471
477### `createPluginAtomClient` — typed reactive client per plugin472### `createPluginAtomClient` — typed reactive client per plugin
@@ -483,24 +478,21 @@ authors write one line per atom. Same shape as the existing · 5 unmodified lines
483 478
484```ts479```ts
485// packages/core/sdk/src/client.ts (helper)480// packages/core/sdk/src/client.ts (helper)
486import { HttpApi } from "effect/unstable/httpapi"481import { HttpApi } from "effect/unstable/httpapi";
487import { FetchHttpClient } from "effect/unstable/http"482import { FetchHttpClient } from "effect/unstable/http";
488import * as AtomHttpApi from "effect/unstable/reactivity/AtomHttpApi"483import * as AtomHttpApi from "effect/unstable/reactivity/AtomHttpApi";
489 484
490export const createPluginAtomClient = <G extends HttpApiGroup.Any>(485export const createPluginAtomClient = <G extends HttpApiGroup.Any>(
491 group: G,486 group: G,
492 opts: { pluginId: string },487 opts: { pluginId: string },
493) => {488) => {
494 const bundle = HttpApi.make(`plugin-${opts.pluginId}`).add(group)489 const bundle = HttpApi.make(`plugin-${opts.pluginId}`).add(group);
495 return AtomHttpApi.Service<`Plugin_${string}Client`>()(490 return AtomHttpApi.Service<`Plugin_${string}Client`>()(`Plugin_${opts.pluginId}Client`, {
496 `Plugin_${opts.pluginId}Client`,491 api: bundle,
497 {492 httpClient: FetchHttpClient.layer,
498 api: bundle,493 baseUrl: `/_executor/plugins/${opts.pluginId}`,
499 httpClient: FetchHttpClient.layer,494 });
500 baseUrl: `/_executor/plugins/${opts.pluginId}`,495};
501 },
502 )
503}
504```496```
505 497
506Plugin author writes:498Plugin author writes:
@@ -513,39 +505,34 @@ import { · 6 unmodified lines
513 useAtomValue,505 useAtomValue,
514 useAtomSet,506 useAtomSet,
515 AsyncResult,507 AsyncResult,
516} from "@executor-js/sdk/client"508} from "@executor-js/sdk/client";
517import { FooApi } from "./shared"509import { FooApi } from "./shared";
518 510
519const FooClient = createPluginAtomClient(FooApi, { pluginId: "foo" })511const FooClient = createPluginAtomClient(FooApi, { pluginId: "foo" });
520 512
521export const fooThingsAtom = FooClient.query("foo", "listThings", {513export const fooThingsAtom = FooClient.query("foo", "listThings", {
522 timeToLive: "30 seconds",514 timeToLive: "30 seconds",
523 reactivityKeys: ["foo:things"],515 reactivityKeys: ["foo:things"],
524})516});
525 517
526export const fooSync = FooClient.mutation("foo", "syncThing")518export const fooSync = FooClient.mutation("foo", "syncThing");
527 519
528const FooPage = () => {520const FooPage = () => {
529 const things = useAtomValue(fooThingsAtom)521 const things = useAtomValue(fooThingsAtom);
530 const doSync = useAtomSet(fooSync, { mode: "promise" })522 const doSync = useAtomSet(fooSync, { mode: "promise" });
531 523
532 return AsyncResult.match(things, {524 return AsyncResult.match(things, {
533 onInitial: () => <Skeleton />,525 onInitial: () => <Skeleton />,
534 onFailure: () => <p>Failed to load</p>,526 onFailure: () => <p>Failed to load</p>,
535 onSuccess: ({ value }) => (527 onSuccess: ({ value }) => <Table rows={value} onSync={(id) => doSync({ path: { id } })} />,
536 <Table528 });
537 rows={value}529};
538 onSync={(id) => doSync({ path: { id } })}
539 />
540 ),
541 })
542}
543 530
544export default defineClientPlugin({531export default defineClientPlugin({
545 id: "foo" as const,532 id: "foo" as const,
546 pages: [{ path: "/", component: FooPage, nav: { label: "Foo" } }],533 pages: [{ path: "/", component: FooPage, nav: { label: "Foo" } }],
547 widgets: [{ id: "foo-status", component: FooStatus, size: "half" }],534 widgets: [{ id: "foo-status", component: FooStatus, size: "half" }],
548})535});
549```536```
550 537
551Type inference: `FooClient.query("foo", "listThings", ...)` is fully typed538Type inference: `FooClient.query("foo", "listThings", ...)` is fully typed
@@ -608,15 +595,17 @@ Single config, two consumers. · 56 unmodified lines
608 595
609```ts596```ts
610// apps/local/src/server/executor.ts (after)597// apps/local/src/server/executor.ts (after)
611import config from "../../executor.config.ts"598import config from "../../executor.config.ts";
612 599
613const executor = yield* createExecutor({600const executor =
614 scopes: [scope],601 yield *
615 adapter,602 createExecutor({
616 blobs,603 scopes: [scope],
617 plugins: config.plugins,604 adapter,
618 onElicitation: "accept-all",605 blobs,
619})606 plugins: config.plugins,
607 onElicitation: "accept-all",
608 });
620```609```
621 610
622The plugins are already configured (factory called) by the time611The plugins are already configured (factory called) by the time
@@ -631,18 +620,16 @@ export default function executorVite(): Plugin { · 8 unmodified lines
631 return {620 return {
632 name: "executor-plugins",621 name: "executor-plugins",
633 resolveId(id) {622 resolveId(id) {
634 if (id === "virtual:executor/plugins-client") return "\0" + id623 if (id === "virtual:executor/plugins-client") return "\0" + id;
635 },624 },
636 async load(id) {625 async load(id) {
637 if (id !== "\0virtual:executor/plugins-client") return626 if (id !== "\0virtual:executor/plugins-client") return;
638 const config = await loadExecutorConfig()627 const config = await loadExecutorConfig();
639 const imports = config.plugins628 const imports = config.plugins.map((p, i) => `import p${i} from "${p.id}/client"`).join("\n");
640 .map((p, i) => `import p${i} from "${p.id}/client"`)629 const list = config.plugins.map((_, i) => `p${i}`).join(", ");
641 .join("\n")630 return `${imports}\nexport const plugins = [${list}]`;
642 const list = config.plugins.map((_, i) => `p${i}`).join(", ")
643 return `${imports}\nexport const plugins = [${list}]`
644 },631 },
645 }632 };
646}633}
647```634```
648 635
@@ -655,11 +642,11 @@ file — `__root.tsx` is the only place that touches · 6 unmodified lines
655 642
656```tsx643```tsx
657// apps/local/src/routes/__root.tsx644// apps/local/src/routes/__root.tsx
658import { createRootRoute } from "@tanstack/react-router"645import { createRootRoute } from "@tanstack/react-router";
659import { ExecutorProvider } from "@executor-js/react/api/provider"646import { ExecutorProvider } from "@executor-js/react/api/provider";
660import { ExecutorPluginsProvider } from "@executor-js/sdk/client"647import { ExecutorPluginsProvider } from "@executor-js/sdk/client";
661import { plugins as clientPlugins } from "virtual:executor/plugins-client"648import { plugins as clientPlugins } from "virtual:executor/plugins-client";
662import { Shell } from "../web/shell"649import { Shell } from "../web/shell";
663 650
664export const Route = createRootRoute({651export const Route = createRootRoute({
665 component: () => (652 component: () => (
@@ -669,15 +656,15 @@ export const Route = createRootRoute({ · 3 unmodified lines
669 </ExecutorPluginsProvider>656 </ExecutorPluginsProvider>
670 </ExecutorProvider>657 </ExecutorProvider>
671 ),658 ),
672})659});
673```660```
674 661
675```tsx662```tsx
676// e.g. inside packages/react/src/pages/sources.tsx663// e.g. inside packages/react/src/pages/sources.tsx
677import { useSourcePlugins } from "@executor-js/sdk/client"664import { useSourcePlugins } from "@executor-js/sdk/client";
678 665
679export function SourcesPage() {666export function SourcesPage() {
680 const sourcePlugins = useSourcePlugins()667 const sourcePlugins = useSourcePlugins();
681 // ...668 // ...
682}669}
683```670```
@@ -701,49 +688,48 @@ needs a Vite restart (not a hot update — config changed). · 17 unmodified lines
701 688
702```ts689```ts
703// src/shared.ts — only @executor-js/sdk imports, no raw effect imports690// src/shared.ts — only @executor-js/sdk imports, no raw effect imports
704import { HttpApiEndpoint, HttpApiGroup, Schema } from "@executor-js/sdk"691import { HttpApiEndpoint, HttpApiGroup, Schema } from "@executor-js/sdk";
705 692
706export const Greeting = Schema.Struct({693export const Greeting = Schema.Struct({
707 message: Schema.String,694 message: Schema.String,
708 count: Schema.Number,695 count: Schema.Number,
709})696});
710export type Greeting = typeof Greeting.Type697export type Greeting = typeof Greeting.Type;
711 698
712export const ExampleApi = HttpApiGroup.make("example").add(699export const ExampleApi = HttpApiGroup.make("example").add(
713 HttpApiEndpoint.post("greet", "/greet", {700 HttpApiEndpoint.post("greet", "/greet", {
714 payload: Schema.Struct({ name: Schema.String }),701 payload: Schema.Struct({ name: Schema.String }),
715 success: Greeting,702 success: Greeting,
716 }),703 }),
717)704);
718```705```
719 706
720```ts707```ts
721// src/server.ts708// src/server.ts
722import {709import { Context, definePlugin, Effect, HttpApi, HttpApiBuilder } from "@executor-js/sdk";
723 Context, definePlugin, Effect, HttpApi, HttpApiBuilder,710import { ExampleApi } from "./shared";
724} from "@executor-js/sdk"
725import { ExampleApi } from "./shared"
726 711
727const ExampleApiBundle = HttpApi.make("example").add(ExampleApi)712const ExampleApiBundle = HttpApi.make("example").add(ExampleApi);
728 713
729interface ExampleExtension {714interface ExampleExtension {
730 readonly greet: (715 readonly greet: (
731 name: string,716 name: string,
732 ) => Effect.Effect<{ readonly message: string; readonly count: number }>717 ) => Effect.Effect<{ readonly message: string; readonly count: number }>;
733}718}
734 719
735export class ExampleExtensionService extends Context.Service<720export class ExampleExtensionService extends Context.Service<
736 ExampleExtensionService, ExampleExtension,721 ExampleExtensionService,
722 ExampleExtension
737>()("ExampleExtensionService") {}723>()("ExampleExtensionService") {}
738 724
739const ExampleHandlers = HttpApiBuilder.group(ExampleApiBundle, "example", (h) =>725const ExampleHandlers = HttpApiBuilder.group(ExampleApiBundle, "example", (h) =>
740 h.handle("greet", ({ payload }) =>726 h.handle("greet", ({ payload }) =>
741 Effect.gen(function* () {727 Effect.gen(function* () {
742 const ext = yield* ExampleExtensionService728 const ext = yield* ExampleExtensionService;
743 return yield* ext.greet(payload.name)729 return yield* ext.greet(payload.name);
744 }),730 }),
745 ),731 ),
746)732);
747 733
748export const examplePlugin = definePlugin(() => ({734export const examplePlugin = definePlugin(() => ({
749 id: "example" as const,735 id: "example" as const,
@@ -754,57 +740,57 @@ export const examplePlugin = definePlugin(() => ({ · 4 unmodified lines
754 extension: (ctx): ExampleExtension => ({740 extension: (ctx): ExampleExtension => ({
755 greet: (name: string) =>741 greet: (name: string) =>
756 Effect.sync(() => {742 Effect.sync(() => {
757 ctx.storage.count += 1743 ctx.storage.count += 1;
758 return {744 return {
759 message: `hello ${name}`,745 message: `hello ${name}`,
760 count: ctx.storage.count,746 count: ctx.storage.count,
761 }747 };
762 }),748 }),
763 }),749 }),
764 750
765 routes: () => ExampleApi,751 routes: () => ExampleApi,
766 handlers: () => ExampleHandlers,752 handlers: () => ExampleHandlers,
767 extensionService: ExampleExtensionService,753 extensionService: ExampleExtensionService,
768}))754}));
769 755
770export default examplePlugin756export default examplePlugin;
771```757```
772 758
773```tsx759```tsx
774// src/client.tsx760// src/client.tsx
775import {761import { defineClientPlugin, createPluginAtomClient, useAtomSet } from "@executor-js/sdk/client";
776 defineClientPlugin,762import { useState } from "react";
777 createPluginAtomClient,763import { ExampleApi } from "./shared";
778 useAtomSet,
779} from "@executor-js/sdk/client"
780import { useState } from "react"
781import { ExampleApi } from "./shared"
782 764
783const ExampleClient = createPluginAtomClient(ExampleApi, { pluginId: "example" })765const ExampleClient = createPluginAtomClient(ExampleApi, { pluginId: "example" });
784 766
785const greetAtom = ExampleClient.mutation("example", "greet")767const greetAtom = ExampleClient.mutation("example", "greet");
786 768
787const ExamplePage = () => {769const ExamplePage = () => {
788 const [name, setName] = useState("world")770 const [name, setName] = useState("world");
789 const [result, setResult] = useState<string>()771 const [result, setResult] = useState<string>();
790 const doGreet = useAtomSet(greetAtom, { mode: "promise" })772 const doGreet = useAtomSet(greetAtom, { mode: "promise" });
791 773
792 return (774 return (
793 <div>775 <div>
794 <input value={name} onChange={(e) => setName(e.target.value)} />776 <input value={name} onChange={(e) => setName(e.target.value)} />
795 <button onClick={async () => {777 <button
796 const g = await doGreet({ payload: { name } })778 onClick={async () => {
797 setResult(`${g.message} (#${g.count})`)779 const g = await doGreet({ payload: { name } });
798 }}>Greet</button>780 setResult(`${g.message} (#${g.count})`);
781 }}
782 >
783 Greet
784 </button>
799 {result && <pre>{result}</pre>}785 {result && <pre>{result}</pre>}
800 </div>786 </div>
801 )787 );
802}788};
803 789
804export default defineClientPlugin({790export default defineClientPlugin({
805 id: "example" as const,791 id: "example" as const,
806 pages: [{ path: "/", component: ExamplePage, nav: { label: "Example" } }],792 pages: [{ path: "/", component: ExamplePage, nav: { label: "Example" } }],
807})793});
808```794```
809 795
810```jsonc796```jsonc
@@ -814,13 +800,13 @@ export default defineClientPlugin({ · 3 unmodified lines
814 "type": "module",800 "type": "module",
815 "exports": {801 "exports": {
816 "./server": "./dist/server.js",802 "./server": "./dist/server.js",
817 "./client": "./dist/client.js"803 "./client": "./dist/client.js",
818 },804 },
819 "executor": { "id": "example", "version": "0.1.0" },805 "executor": { "id": "example", "version": "0.1.0" },
820 "peerDependencies": {806 "peerDependencies": {
821 "@executor-js/sdk": "workspace:*",807 "@executor-js/sdk": "workspace:*",
822 "react": "catalog:"808 "react": "catalog:",
823 }809 },
824}810}
825```811```
826 812
@@ -828,8 +814,8 @@ User adds it: · 1 unmodified line
828 814
829```ts815```ts
830// apps/local/executor.config.ts816// apps/local/executor.config.ts
831import { examplePlugin } from "@executor-js/plugin-example/server"817import { examplePlugin } from "@executor-js/plugin-example/server";
832plugins: [/* ... */, examplePlugin()]818plugins: [, /* ... */ examplePlugin()];
833```819```
834 820
835## SDK-only plugin example821## SDK-only plugin example
@@ -852,81 +838,82 @@ file with no `./client` export, no `routes`, no `handlers`. · 16 unmodified lines
852 "type": "module",838 "type": "module",
853 "exports": { "./server": "./dist/server.js" },839 "exports": { "./server": "./dist/server.js" },
854 "executor": { "id": "rateLimiter", "version": "0.1.0" },840 "executor": { "id": "rateLimiter", "version": "0.1.0" },
855 "peerDependencies": { "@executor-js/sdk": "workspace:*" }841 "peerDependencies": { "@executor-js/sdk": "workspace:*" },
856}842}
857```843```
858 844
859```ts845```ts
860// src/server.ts846// src/server.ts
861import { definePlugin, defineSchema, Effect, Schema } from "@executor-js/sdk"847import { definePlugin, defineSchema, Effect, Schema } from "@executor-js/sdk";
862 848
863interface RateLimiterConfig {849interface RateLimiterConfig {
864 defaultLimit?: number850 defaultLimit?: number;
865}851}
866 852
867class RateLimitExceeded extends Schema.TaggedError<RateLimitExceeded>()(853class RateLimitExceeded extends Schema.TaggedError<RateLimitExceeded>()("RateLimitExceeded", {
868 "RateLimitExceeded",854 key: Schema.String,
869 { key: Schema.String, retryAfterMs: Schema.Number },855 retryAfterMs: Schema.Number,
870) {}856}) {}
871 857
872export const rateLimiterPlugin = definePlugin(858export const rateLimiterPlugin = definePlugin((opts: RateLimiterConfig = {}) => ({
873 (opts: RateLimiterConfig = {}) => ({859 id: "rateLimiter" as const,
874 id: "rateLimiter" as const,860
875 861 schema: defineSchema({
876 schema: defineSchema({862 rate_buckets: {
877 rate_buckets: {863 fields: {
878 fields: {864 key: { type: "string", primary: true },
879 key: { type: "string", primary: true },865 tokens: { type: "number" },
880 tokens: { type: "number" },866 updated_at: { type: "number" },
881 updated_at: { type: "number" },
882 },
883 },867 },
884 }),868 },
869 }),
885 870
886 storage: ({ adapter }) => ({871 storage: ({ adapter }) => ({
887 adapter,872 adapter,
888 defaultLimit: opts.defaultLimit ?? 60,873 defaultLimit: opts.defaultLimit ?? 60,
889 }),874 }),
890 875
891 extension: (ctx) => ({876 extension: (ctx) => ({
892 check: (key: string, cost = 1) =>877 check: (key: string, cost = 1) =>
893 Effect.gen(function* () {878 Effect.gen(function* () {
894 const bucket = yield* readOrCreateBucket(ctx.storage, key)879 const bucket = yield* readOrCreateBucket(ctx.storage, key);
895 const refilled = refill(bucket, ctx.storage.defaultLimit)880 const refilled = refill(bucket, ctx.storage.defaultLimit);
896 if (refilled.tokens < cost) {881 if (refilled.tokens < cost) {
897 return yield* new RateLimitExceeded({882 return yield* new RateLimitExceeded({
898 key,883 key,
899 retryAfterMs: estimateRetry(refilled),884 retryAfterMs: estimateRetry(refilled),
900 })885 });
901 }886 }
902 yield* writeBucket(ctx.storage, key, refilled.tokens - cost)887 yield* writeBucket(ctx.storage, key, refilled.tokens - cost);
903 return { allowed: true, remaining: refilled.tokens - cost }888 return { allowed: true, remaining: refilled.tokens - cost };
904 }),889 }),
905 890
906 reset: (key: string) =>891 reset: (key: string) =>
907 Effect.tryPromise(() =>892 Effect.tryPromise(() =>
908 ctx.storage.adapter.delete({893 ctx.storage.adapter.delete({
909 model: "rate_buckets",894 model: "rate_buckets",
910 where: [["key", "=", key]],895 where: [["key", "=", key]],
911 }),896 }),
912 ),897 ),
913 }),
914 }),898 }),
915)899}));
916```900```
917 901
918Pure programmatic consumption — works identically from CLI, tests,902Pure programmatic consumption — works identically from CLI, tests,
919embedded library use, or another plugin's `extension`:903embedded library use, or another plugin's `extension`:
920 904
921```ts905```ts
922const result = yield* executor.rateLimiter.check("user-123", 5)906const result = yield * executor.rateLimiter.check("user-123", 5);
923// ^? { allowed: true; remaining: number }907// ^? { allowed: true; remaining: number }
924 908
925yield* executor.rateLimiter.check("user-123", 1000).pipe(909yield *
926 Effect.catchTag("RateLimitExceeded", (err) =>910 executor.rateLimiter
927 Effect.log(`hit limit on ${err.key}, retry in ${err.retryAfterMs}ms`),911 .check("user-123", 1000)
928 ),912 .pipe(
929)913 Effect.catchTag("RateLimitExceeded", (err) =>
914 Effect.log(`hit limit on ${err.key}, retry in ${err.retryAfterMs}ms`),
915 ),
916 );
930```917```
931 918
932What the host does with this plugin:919What the host does with this plugin:
@@ -1000,6 +987,7 @@ Build order — all shipped. · 67 unmodified lines
1000## References987## References
1001 988
1002**Reference research (pre-design):**989**Reference research (pre-design):**
990
1003- emdash: `astro.config.mjs` import-and-call pattern, `package.json` exports991- emdash: `astro.config.mjs` import-and-call pattern, `package.json` exports
1004 for separate admin entry — see `.reference/emdash/demos/plugins-demo/`.992 for separate admin entry — see `.reference/emdash/demos/plugins-demo/`.
1005- dynamic-software: PluginManifest + capability declarations idea (deferred);993- dynamic-software: PluginManifest + capability declarations idea (deferred);
@@ -1008,6 +996,7 @@ Build order — all shipped. · 2 unmodified lines
1008 when we add capability enforcement).996 when we add capability enforcement).
1009 997
1010**Shipped code:**998**Shipped code:**
999
1011- Plugin contract: `packages/core/sdk/src/plugin.ts` (`PluginSpec`,1000- Plugin contract: `packages/core/sdk/src/plugin.ts` (`PluginSpec`,
1012 `definePlugin`).1001 `definePlugin`).
1013- Client contract: `packages/core/sdk/src/client.ts`1002- Client contract: `packages/core/sdk/src/client.ts`
@@ -1030,6 +1019,7 @@ Build order — all shipped. · 16 unmodified lines
1030 follows the same per-capability-typed-field pattern.1019 follows the same per-capability-typed-field pattern.
1031 1020
1032**Adjacent plans:**1021**Adjacent plans:**
1022
1033- `notes/artifacts-workflows-and-generated-ui.md` — the artifacts/workflows1023- `notes/artifacts-workflows-and-generated-ui.md` — the artifacts/workflows
1034 plan. Uses "protocol" terminology that we're not adopting; read its1024 plan. Uses "protocol" terminology that we're not adopting; read its
1035 "ArtifactStoreProtocol" as "the typed interface that goes into the1025 "ArtifactStoreProtocol" as "the typed interface that goes into the
notes/mcp-conn-pool.md+9−7
@@ -4,7 +4,7 @@ · 3 unmodified lines
4 4
5The MCP plugin **already pools connections within a session DO**. Production5The MCP plugin **already pools connections within a session DO**. Production
6telemetry attributing 17% of `plugin.mcp.connection.acquire` calls to a6telemetry attributing 17% of `plugin.mcp.connection.acquire` calls to a
7fresh `plugin.mcp.connection.handshake` represents *cold-start sessions*,7fresh `plugin.mcp.connection.handshake` represents _cold-start sessions_,
8not redundant in-session handshakes. No structural change is required;8not redundant in-session handshakes. No structural change is required;
9this PR ships a strict regression test that pins the existing contract9this PR ships a strict regression test that pins the existing contract
10plus a `plugin.mcp.cache_hit` span attribute so future telemetry can10plus a `plugin.mcp.cache_hit` span attribute so future telemetry can
@@ -68,11 +68,12 @@ mutating the cache key per call) trips immediately in CI. · 57 unmodified lines
68## Re-reading the production trace68## Re-reading the production trace
69 69
70Trace `b7102047bed975da461c0519d1251de4`:70Trace `b7102047bed975da461c0519d1251de4`:
71
71- `mcp.plugin.resolve_connector` 2.72s72- `mcp.plugin.resolve_connector` 2.72s
72- `plugin.mcp.connection.acquire` 2.72s73- `plugin.mcp.connection.acquire` 2.72s
73- `plugin.mcp.connection.handshake` 2.52s74- `plugin.mcp.connection.handshake` 2.52s
74- `executor.storage.transaction` 1.02s (token persist)75- `executor.storage.transaction` 1.02s (token persist)
75- `plugin.mcp.client.call_tool` 1.48s76- `plugin.mcp.client.call_tool` 1.48s
76 77
77Span nesting plus `resolve_connector ≈ acquire ≈ handshake` durations is78Span nesting plus `resolve_connector ≈ acquire ≈ handshake` durations is
78the cache-miss path: `resolveConnector` only runs (and emits its span)79the cache-miss path: `resolveConnector` only runs (and emits its span)
@@ -80,11 +81,12 @@ when the cache lookup actually invokes the lookup closure, which only · 1 unmodified line
80happens on a miss.81happens on a miss.
81 82
828h aggregate:838h aggregate:
84
83- `plugin.mcp.connection.handshake` count 485- `plugin.mcp.connection.handshake` count 4
84- `plugin.mcp.connection.acquire` count 2486- `plugin.mcp.connection.acquire` count 24
85- `mcp.plugin.resolve_connector` count 487- `mcp.plugin.resolve_connector` count 4
86 88
87Acquire count is 6× handshake count. The cache *is* hitting on 20 of 2489Acquire count is 6× handshake count. The cache _is_ hitting on 20 of 24
88calls. The 4 misses are cold-start sessions (each MCP session DO's first90calls. The 4 misses are cold-start sessions (each MCP session DO's first
89tool call, plus any session where the connection went stale and was91tool call, plus any session where the connection went stale and was
90invalidated by the retry path). Six tool calls per session is consistent92invalidated by the retry path). Six tool calls per session is consistent
notes/old/connections-migration.md+1−1
@@ -21,7 +21,7 @@ to preserve auth state across the upgrade. · 20 unmodified lines
21 21
221. `INSERT INTO connection` with the new pointer's `connectionId`,221. `INSERT INTO connection` with the new pointer's `connectionId`,
23 `provider = "openapi:oauth2"`, and `provider_state` = `{flow,23 `provider = "openapi:oauth2"`, and `provider_state` = `{flow,
24 tokenUrl, clientIdSecretId, clientSecretSecretId, scopes}` lifted24tokenUrl, clientIdSecretId, clientSecretSecretId, scopes}` lifted
25 from the legacy row. `access_token_secret_id` + `refresh_token_secret_id`25 from the legacy row. `access_token_secret_id` + `refresh_token_secret_id`
26 reuse the legacy secret ids — tokens themselves never move, only26 reuse the legacy secret ids — tokens themselves never move, only
27 ownership links flip.27 ownership links flip.
notes/old/error-handling.md+4−4
@@ -87,7 +87,7 @@ Owns the translation, the opaque wire schema, and the capture service. · 86 unmodified lines
87 87
88 ```ts88 ```ts
89 interface ErrorCaptureShape {89 interface ErrorCaptureShape {
90 readonly captureException: (cause: Cause<unknown>) => Effect<string>90 readonly captureException: (cause: Cause<unknown>) => Effect<string>;
91 }91 }
92 ```92 ```
93 93
@@ -130,7 +130,7 @@ Plugin authors write normal Effect code. Their extension method error · 36 unmodified lines
130unions look like:130unions look like:
131 131
132```ts132```ts
133Effect.Effect<X, MyPluginTypedError | StorageError, never>133Effect.Effect<X, MyPluginTypedError | StorageError, never>;
134```134```
135 135
136Where `MyPluginTypedError` is the union of their own136Where `MyPluginTypedError` is the union of their own
@@ -152,9 +152,9 @@ class McpGroup extends HttpApiGroup.make("mcp") · 15 unmodified lines
152 // …152 // …
153 .addError(InternalError)153 .addError(InternalError)
154 .addError(McpOAuthError)154 .addError(McpOAuthError)
155 .addError(McpConnectionError)155 .addError(McpConnectionError) {
156 // …156 // …
157{}157}
158```158```
159 159
160No per-endpoint `addError`. The framework encodes each tagged error by160No per-endpoint `addError`. The framework encodes each tagged error by
notes/old/errored-sources.md+2−1
@@ -54,7 +54,7 @@ source: { · 53 unmodified lines
54Effect.catchAll((e) => {54Effect.catchAll((e) => {
55 const message = e instanceof Error ? e.message : String(e);55 const message = e instanceof Error ? e.message : String(e);
56 return executor.sources.recordError({ source, message }).pipe(Effect.asVoid);56 return executor.sources.recordError({ source, message }).pipe(Effect.asVoid);
57})57});
58```58```
59 59
60`executor.sources.recordError` is a new core method that writes a60`executor.sources.recordError` is a new core method that writes a
@@ -76,6 +76,7 @@ a special case — current `removeSource` already works regardless of · 15 unmodified lines
76## Scope76## Scope
77 77
78Not for the scope-refactor PR. Separate change:78Not for the scope-refactor PR. Separate change:
79
79- Schema migration adds two columns.80- Schema migration adds two columns.
80- `executor.sources.recordError` on the core SDK.81- `executor.sources.recordError` on the core SDK.
81- Wire up in config-sync.82- Wire up in config-sync.
notes/old/indexes.md+1−1
@@ -25,7 +25,7 @@ Tables affected (all drop candidates): · 24 unmodified lines
25- `workos_vault_metadata_scope_id_idx`25- `workos_vault_metadata_scope_id_idx`
26 26
27Root cause: `core-schema.ts` and plugin schemas mark `scope_id: {27Root cause: `core-schema.ts` and plugin schemas mark `scope_id: {
28index: true }`. The drizzle generator should probably *skip* emitting28index: true }`. The drizzle generator should probably _skip_ emitting
29a single-column index on a column that's already the leftmost column29a single-column index on a column that's already the leftmost column
30of a composite primary key. Fix in two places:30of a composite primary key. Fix in two places:
31 31
notes/old/mcp-testing.md+6−6
@@ -5,10 +5,10 @@ deleting one silently drops coverage that the others can't replace. · 4 unmodified lines
5 5
6## Suites at a glance6## Suites at a glance
7 7
8| file | pool | drives | what it proves |8| file | pool | drives | what it proves |
9|---|---|---|---|9| ----------------------------------------------- | ----------------------------- | --------------------------------------------------- | --------------------------------------------------------------------------------------------- |
10| `apps/cloud/src/mcp-session.e2e.node.test.ts` | node | `InMemoryTransport` + SDK `Client` | engine + plugin wiring; schema drift; elicitation semantics |10| `apps/cloud/src/mcp-session.e2e.node.test.ts` | node | `InMemoryTransport` + SDK `Client` | engine + plugin wiring; schema drift; elicitation semantics |
11| `apps/cloud/src/mcp-flow.test.ts` | workerd (vitest-pool-workers) | `SELF.fetch` + hand-rolled JSON-RPC | edge HTTP pipeline that does not require a live multi-request DO session |11| `apps/cloud/src/mcp-flow.test.ts` | workerd (vitest-pool-workers) | `SELF.fetch` + hand-rolled JSON-RPC | edge HTTP pipeline that does not require a live multi-request DO session |
12| `apps/cloud/src/mcp-miniflare.e2e.node.test.ts` | node + miniflare-on-real-port | SDK `Client` + real `StreamableHTTPClientTransport` | the long-lived-socket DO runtime actually works end-to-end; elicitation round-trips real HTTP |12| `apps/cloud/src/mcp-miniflare.e2e.node.test.ts` | node + miniflare-on-real-port | SDK `Client` + real `StreamableHTTPClientTransport` | the long-lived-socket DO runtime actually works end-to-end; elicitation round-trips real HTTP |
13 13
14## The workerd cross-request I/O wall14## The workerd cross-request I/O wall
@@ -86,12 +86,12 @@ Pattern used in `mcp-miniflare.e2e.node.test.ts`: · 71 unmodified lines
863. Read the bound port from `HttpServer.HttpServer.address` (TcpAddress tag)863. Read the bound port from `HttpServer.HttpServer.address` (TcpAddress tag)
87 inside a `Layer.effect` that produces the test service.87 inside a `Layer.effect` that produces the test service.
884. Generate the spec with `OpenApi.fromApi(api)` and inject884. Generate the spec with `OpenApi.fromApi(api)` and inject
89 `servers: [{ url: `http://127.0.0.1:${port}` }]` before `JSON.stringify`.89 `servers: [{ url: `http://127.0.0.1:${port}` }]`before`JSON.stringify`.
905. SDK Client advertises `capabilities: { elicitation: { form: {} } }` and905. SDK Client advertises `capabilities: { elicitation: { form: {} } }` and
91 registers `client.setRequestHandler(ElicitRequestSchema, ...)` that returns91 registers `client.setRequestHandler(ElicitRequestSchema, ...)` that returns
92 `{ action: "accept", content: {} }`.92 `{ action: "accept", content: {} }`.
936. Call `execute` with code that (a) calls `tools.openapi.addSource({ spec,936. Call `execute` with code that (a) calls `tools.openapi.addSource({ spec,
94 namespace })`, (b) invokes the POST operation.94namespace })`, (b) invokes the POST operation.
95 95
96**Tool id format inside the `execute` sandbox.**96**Tool id format inside the `execute` sandbox.**
97`tools.<sourceId>.<group>.<operation>` for openapi. The Effect97`tools.<sourceId>.<group>.<operation>` for openapi. The Effect
notes/old/pluggable-storage.md+45−37
@@ -35,7 +35,9 @@ export interface CoreStore { · 34 unmodified lines
35 35
36 // definitions36 // definitions
37 readonly putDefinitions: (defs: readonly StoredDefinition[]) => Effect.Effect<void, Error>;37 readonly putDefinitions: (defs: readonly StoredDefinition[]) => Effect.Effect<void, Error>;
38 readonly listDefinitionsBySource: (sourceId: string) => Effect.Effect<readonly StoredDefinition[], Error>;38 readonly listDefinitionsBySource: (
39 sourceId: string,
40 ) => Effect.Effect<readonly StoredDefinition[], Error>;
39 readonly removeDefinitionsBySource: (sourceId: string) => Effect.Effect<void, Error>;41 readonly removeDefinitionsBySource: (sourceId: string) => Effect.Effect<void, Error>;
40 42
41 // secrets (routing rows; provider-specific data lives in provider)43 // secrets (routing rows; provider-specific data lives in provider)
@@ -45,10 +47,9 @@ export interface CoreStore { · 3 unmodified lines
45 readonly removeSecret: (id: string) => Effect.Effect<void, Error>;47 readonly removeSecret: (id: string) => Effect.Effect<void, Error>;
46}48}
47 49
48export const makeDefaultCoreStore = (deps: {50export const makeDefaultCoreStore = (deps: { adapter: DBAdapter; scope: Scope }): CoreStore => {
49 adapter: DBAdapter;51 /* typedAdapter<CoreSchema> wiring, one place */
50 scope: Scope;52};
51}): CoreStore => { /* typedAdapter<CoreSchema> wiring, one place */ };
52```53```
53 54
54## Wiring — defaults passed in deps, force the spread55## Wiring — defaults passed in deps, force the spread
@@ -70,9 +71,7 @@ export interface ExecutorConfig<TPlugins> { · 15 unmodified lines
70 71
71// Inside createExecutor:72// Inside createExecutor:
72const defaults = makeDefaultCoreStore({ adapter, scope });73const defaults = makeDefaultCoreStore({ adapter, scope });
73const core = config.coreStorage74const core = config.coreStorage ? config.coreStorage({ adapter, scope, defaults }) : defaults;
74 ? config.coreStorage({ adapter, scope, defaults })
75 : defaults;
76// Executor now calls core.putTools(...), core.removeSource(...), etc.75// Executor now calls core.putTools(...), core.removeSource(...), etc.
77// No more adapter.create({ model: "tool", ... }) scattered through executor.ts.76// No more adapter.create({ model: "tool", ... }) scattered through executor.ts.
78```77```
@@ -83,7 +82,7 @@ const core = config.coreStorage · 4 unmodified lines
83 with no type help.82 with no type help.
84- Anyone implementing from scratch (test doubles, a non-drizzle backend)83- Anyone implementing from scratch (test doubles, a non-drizzle backend)
85 gets no squigglies when they miss a method.84 gets no squigglies when they miss a method.
86- Defaults-in-deps gets "only write what you override" ergonomics *and*85- Defaults-in-deps gets "only write what you override" ergonomics _and_
87 keeps the full `CoreStore` return type enforced.86 keeps the full `CoreStore` return type enforced.
88 87
89## What it unlocks88## What it unlocks
@@ -91,30 +90,39 @@ const core = config.coreStorage · 1 unmodified line
91**Algolia sidecar — no string-match, just method overrides:**90**Algolia sidecar — no string-match, just method overrides:**
92 91
93```ts92```ts
94const executor = yield* createExecutor({93const executor =
95 scope, adapter, blobs, plugins,94 yield *
96 95 createExecutor({
97 coreStorage: ({ defaults }) => ({96 scope,
98 ...defaults,97 adapter,
99 putTools: (tools) =>98 blobs,
100 Effect.gen(function* () {99 plugins,
101 yield* defaults.putTools(tools);100
102 yield* Effect.promise(() => algolia.saveObjects({101 coreStorage: ({ defaults }) => ({
103 indexName: `tools_${scope.id}`,102 ...defaults,
104 objects: tools.map((t) => ({ objectID: t.id, ...t })),103 putTools: (tools) =>
105 }));104 Effect.gen(function* () {
106 }),105 yield* defaults.putTools(tools);
107 removeToolsBySource: (sourceId) =>106 yield* Effect.promise(() =>
108 Effect.gen(function* () {107 algolia.saveObjects({
109 const affected = yield* defaults.listTools();108 indexName: `tools_${scope.id}`,
110 yield* defaults.removeToolsBySource(sourceId);109 objects: tools.map((t) => ({ objectID: t.id, ...t })),
111 yield* Effect.promise(() => algolia.deleteObjects({110 }),
112 indexName: `tools_${scope.id}`,111 );
113 objectIDs: affected.filter((t) => t.sourceId === sourceId).map((t) => t.id),112 }),
114 }));113 removeToolsBySource: (sourceId) =>
115 }),114 Effect.gen(function* () {
116 }),115 const affected = yield* defaults.listTools();
117});116 yield* defaults.removeToolsBySource(sourceId);
117 yield* Effect.promise(() =>
118 algolia.deleteObjects({
119 indexName: `tools_${scope.id}`,
120 objectIDs: affected.filter((t) => t.sourceId === sourceId).map((t) => t.id),
121 }),
122 );
123 }),
124 }),
125 });
118```126```
119 127
120Test doubles become trivial — either hand-roll the whole `CoreStore`128Test doubles become trivial — either hand-roll the whole `CoreStore`
@@ -128,13 +136,13 @@ The factory name leaks. Rename to the defaults-in-deps shape: · 7 unmodified lines
128 136
129```ts137```ts
130// before138// before
131storage: (deps) => makeDefaultOpenapiStore(deps)139storage: (deps) => makeDefaultOpenapiStore(deps);
132 140
133// after141// after
134storage: ({ defaults }) => defaults // no-op, identical to leaving off `storage:`142storage: ({ defaults }) => defaults; // no-op, identical to leaving off `storage:`
135 143
136// override one method144// override one method
137storage: ({ defaults }) => ({ ...defaults, upsertSource: myUpsert })145storage: ({ defaults }) => ({ ...defaults, upsertSource: myUpsert });
138```146```
139 147
140Mechanical sweep across openapi / mcp / graphql / workos-vault plugins.148Mechanical sweep across openapi / mcp / graphql / workos-vault plugins.
@@ -152,6 +160,6 @@ Not intellectually hard, but a broad diff: · 11 unmodified lines
152 factory renamed to follow the `defaults` contract.160 factory renamed to follow the `defaults` contract.
153- Docs/readme updates showing the new override pattern.161- Docs/readme updates showing the new override pattern.
154 162
155Ideally lands *before* pluggable search — the search sidecar story163Ideally lands _before_ pluggable search — the search sidecar story
156depends on this refactor to avoid the `if (model === "tool")` antipattern164depends on this refactor to avoid the `if (model === "tool")` antipattern
157I'd otherwise be stuck with.165I'd otherwise be stuck with.
notes/old/promise-sdk-typed-errors.md+3−1
@@ -6,7 +6,7 @@ · 5 unmodified lines
6`Effect.runPromise(...)`. That works but it's lossy at the Promise6`Effect.runPromise(...)`. That works but it's lossy at the Promise
7boundary:7boundary:
8 8
9- The promise's *rejection type* is `unknown` (or `any` after9- The promise's _rejection type_ is `unknown` (or `any` after
10 `try/catch`). Consumers have to `instanceof InternalError` /10 `try/catch`). Consumers have to `instanceof InternalError` /
11 `err._tag === "..."` to discriminate, even though the underlying11 `err._tag === "..."` to discriminate, even though the underlying
12 Effect had a fully typed error union.12 Effect had a fully typed error union.
@@ -43,12 +43,14 @@ Do we expose Effect's `Exit` / `Cause` directly to promise consumers, · 30 unmodified lines
43or wrap them in a promise-native `Result<A, E>`?43or wrap them in a promise-native `Result<A, E>`?
44 44
45**Expose `Exit`/`Cause`:**45**Expose `Exit`/`Cause`:**
46
46- Pros: one fewer abstraction, no translation layer to maintain, cause47- Pros: one fewer abstraction, no translation layer to maintain, cause
47 preserves parallel/sequential composition.48 preserves parallel/sequential composition.
48- Cons: consumers depend on `effect`'s API even though they're using a49- Cons: consumers depend on `effect`'s API even though they're using a
49 "Promise" SDK — partly defeats the abstraction.50 "Promise" SDK — partly defeats the abstraction.
50 51
51**Wrap in a `Result<A, E>`:**52**Wrap in a `Result<A, E>`:**
53
52- Pros: `@executor-js/sdk/promise` consumers don't import `effect` at all,54- Pros: `@executor-js/sdk/promise` consumers don't import `effect` at all,
53 surface stays small.55 surface stays small.
54- Cons: another type to learn / document; loses some Cause structure56- Cons: another type to learn / document; loses some Cause structure
notes/old/rls.md+3−3
@@ -54,8 +54,8 @@ And app-side, once per request inside a transaction (so `SET LOCAL` · 53 unmodified lines
54takes):54takes):
55 55
56```ts56```ts
57yield* sql`SET LOCAL app.scope_chain = ${toPgArray(chain)}`;57yield * sql`SET LOCAL app.scope_chain = ${toPgArray(chain)}`;
58yield* sql`SET LOCAL app.write_scope = ${writeScope}`;58yield * sql`SET LOCAL app.write_scope = ${writeScope}`;
59```59```
60 60
61`current_setting('…', true)` returns null when the setting is missing,61`current_setting('…', true)` returns null when the setting is missing,
@@ -69,7 +69,7 @@ scope sees zero rows, not an error that might be handled into a 500. · 7 unmodified lines
69 tables is whatever `executor-schema.ts` exports with a `scope_id`69 tables is whatever `executor-schema.ts` exports with a `scope_id`
70 column — same set the scope adapter already knows about.70 column — same set the scope adapter already knows about.
712. Add an Effect layer that wraps the DbService to issue the two `SET712. Add an Effect layer that wraps the DbService to issue the two `SET
72 LOCAL`s at connection acquire. `postgres.js`'s `sql.begin` already72LOCAL`s at connection acquire. `postgres.js`'s `sql.begin` already
73 gives us the transaction boundary.73 gives us the transaction boundary.
743. Leave `BYPASSRLS` off the worker's role so we actually get the743. Leave `BYPASSRLS` off the worker's role so we actually get the
75 enforcement; grant it on the role used by the migration / admin75 enforcement; grant it on the role used by the migration / admin
notes/old/scoped-source-auth.md+2−7
@@ -20,7 +20,7 @@ The common primitive is not "org auth" or "user auth". It is an ordered · 19 unmodified lines
20scope stack:20scope stack:
21 21
22```ts22```ts
23[user, workspace, org]23[user, workspace, org];
24```24```
25 25
26Rows still belong to exactly one scope. Resolution walks the stack26Rows still belong to exactly one scope. Resolution walks the stack
@@ -68,12 +68,7 @@ For example: · 41 unmodified lines
68 68
69```ts69```ts
70oauth2: {70oauth2: {
71 flow: "clientCredentials",71 flow: ("clientCredentials", tokenUrl, clientIdSlot, clientSecretSlot, connectionSlot, scopes);
72 tokenUrl,
73 clientIdSlot,
74 clientSecretSlot,
75 connectionSlot,
76 scopes
77}72}
78```73```
79 74
notes/old/scopes.md+6−6
@@ -10,15 +10,15 @@ scopes by construction. · 9 unmodified lines
10 10
11- **SDK** — `createExecutor({ scope: Scope, adapter, blobs, plugins })`.11- **SDK** — `createExecutor({ scope: Scope, adapter, blobs, plugins })`.
12 One `Scope` per executor instance. `scopeAdapter(rootAdapter, {...},12 One `Scope` per executor instance. `scopeAdapter(rootAdapter, {...},
13 schema)` wraps the adapter before it reaches plugin storage or the13schema)` wraps the adapter before it reaches plugin storage or the
14 core-table writers. Every read on a scoped table gets `where scope_id14 core-table writers. Every read on a scoped table gets `where scope_id
15 = scope.id` ANDed in; every write gets `scope_id = scope.id` stamped15= scope.id` ANDed in; every write gets `scope_id = scope.id` stamped
16 into the payload. Tx handles passed into transaction callbacks are16 into the payload. Tx handles passed into transaction callbacks are
17 also wrapped, so nested writes stay inside the same scope.17 also wrapped, so nested writes stay inside the same scope.
18 18
19- **Plugins** — see a plain `DBAdapter`. They do not know or care about19- **Plugins** — see a plain `DBAdapter`. They do not know or care about
20 scope. Every plugin schema that wants isolation declares `scope_id:20 scope. Every plugin schema that wants isolation declares `scope_id:
21 { type: "string", required: true, index: true }`. Forgetting the21{ type: "string", required: true, index: true }`. Forgetting the
22 column means the adapter passes the plugin's reads/writes through22 column means the adapter passes the plugin's reads/writes through
23 unscoped (documented failure mode, not silent) — tests catch the23 unscoped (documented failure mode, not silent) — tests catch the
24 concrete cases we care about.24 concrete cases we care about.
@@ -44,8 +44,8 @@ sources, tools, secrets, and plugin-owned source detail lookups. · 19 unmodified lines
44 44
45```ts45```ts
46interface ScopeContext {46interface ScopeContext {
47 readonly read: readonly string[]; // precedence-ordered, innermost first47 readonly read: readonly string[]; // precedence-ordered, innermost first
48 readonly write: string; // exactly one scope for writes48 readonly write: string; // exactly one scope for writes
49}49}
50```50```
51 51
@@ -91,7 +91,7 @@ Writes land in exactly one scope chosen by the caller. · 39 unmodified lines
91 there. When a user invokes Gmail, secret resolution walks the scope91 there. When a user invokes Gmail, secret resolution walks the scope
92 stack (user → workspace → org) and only finds a token if that user92 stack (user → workspace → org) and only finds a token if that user
93 personally oauthed. Policy on the source row — `auth_scope_mandate:93 personally oauthed. Policy on the source row — `auth_scope_mandate:
94 "user"` — forces this: secret lookup refuses to return94"user"` — forces this: secret lookup refuses to return
95 workspace-level tokens even if one existed.95 workspace-level tokens even if one existed.
96 96
97- **Local global + per-folder.** `~/.executor/global` holds the outer97- **Local global + per-folder.** `~/.executor/global` holds the outer
notes/old/search.md+6−2
@@ -9,7 +9,7 @@ whatever) without baking any of it into the core. · 8 unmodified lines
9 9
101. `executor.tools.list()` — `SELECT * FROM tool WHERE scope_id = ?`.101. `executor.tools.list()` — `SELECT * FROM tool WHERE scope_id = ?`.
112. In JS, for every tool: normalize + tokenize `id / sourceId / name /112. In JS, for every tool: normalize + tokenize `id / sourceId / name /
12 description`, score per field with weights, apply coverage + exact-12description`, score per field with weights, apply coverage + exact-
13 phrase boosts, filter, sort, slice.13 phrase boosts, filter, sort, slice.
14 14
15O(N) per query over the scope. Fine at hundreds of tools, painful past15O(N) per query over the scope. Fine at hundreds of tools, painful past
@@ -74,11 +74,13 @@ type. Same pattern as plugin `storage:` options and the future `coreStorage:`. · 58 unmodified lines
74## Override shapes74## Override shapes
75 75
76**Replace entirely:**76**Replace entirely:**
77
77```ts78```ts
78search: ({ listTools }) => algoliaSearch({ listTools }),79search: ({ listTools }) => algoliaSearch({ listTools }),
79```80```
80 81
81**Decorate — mix in logging/timing:**82**Decorate — mix in logging/timing:**
83
82```ts84```ts
83search: ({ defaults }) => ({85search: ({ defaults }) => ({
84 query: (q) => Effect.gen(function* () {86 query: (q) => Effect.gen(function* () {
@@ -91,6 +93,7 @@ search: ({ defaults }) => ({ · 6 unmodified lines
91```93```
92 94
93**Hybrid — try Algolia, fall back to local:**95**Hybrid — try Algolia, fall back to local:**
96
94```ts97```ts
95search: ({ listTools, defaults }) => ({98search: ({ listTools, defaults }) => ({
96 query: (q) =>99 query: (q) =>
@@ -109,6 +112,7 @@ it's a storage concern. Handled by wrapping the core store (see · 12 unmodified lines
109delegate to the default and mirror to the external index.112delegate to the default and mirror to the external index.
110 113
111The two halves are independent:114The two halves are independent:
115
112- Indexing only (manual reindex): override `coreStorage`, leave `search`116- Indexing only (manual reindex): override `coreStorage`, leave `search`
113 as default.117 as default.
114- Search only (some other populates the index): override `search`, leave118- Search only (some other populates the index): override `search`, leave
@@ -122,7 +126,7 @@ scorer: · 7 unmodified lines
1221. **Projection on `tools.list`** — expose a `select` option so search1261. **Projection on `tools.list`** — expose a `select` option so search
123 doesn't pull `input_schema` / `output_schema`. Biggest per-query win.127 doesn't pull `input_schema` / `output_schema`. Biggest per-query win.
1242. **Postgres FTS** — `tsvector` generated column on `(name, description,1282. **Postgres FTS** — `tsvector` generated column on `(name, description,
125 id, source_id)`, GIN index, use `websearch_to_tsquery`. Native129id, source_id)`, GIN index, use `websearch_to_tsquery`. Native
126 ranking, fuzzy via `pg_trgm`. Would replace the in-memory scorer for130 ranking, fuzzy via `pg_trgm`. Would replace the in-memory scorer for
127 the cloud app.131 the cloud app.
1283. **Per-scope LRU** — cache scored results; invalidate on source1323. **Per-scope LRU** — cache scored results; invalidate on source
notes/old/storage-migration.md+23−15
@@ -3,11 +3,13 @@ · 2 unmodified lines
3## What was done3## What was done
4 4
5### 1. DBSchema alignment with better-auth5### 1. DBSchema alignment with better-auth
6
6- Vendored better-auth's `BetterAuthPluginDBSchema` pattern into `storage-core/src/schema.ts`7- Vendored better-auth's `BetterAuthPluginDBSchema` pattern into `storage-core/src/schema.ts`
7- Made `modelName` optional (falls back to the key), renamed `disableMigrations` → `disableMigration`, dropped `order`8- Made `modelName` optional (falls back to the key), renamed `disableMigrations` → `disableMigration`, dropped `order`
8- Removed explicit `modelName` from all plugin schemas (core-schema, openapi, mcp, graphql, google-discovery, workos-vault) — keys already match table names9- Removed explicit `modelName` from all plugin schemas (core-schema, openapi, mcp, graphql, google-discovery, workos-vault) — keys already match table names
9 10
10### 2. @executor-js/cli package (packages/core/cli/)11### 2. @executor-js/cli package (packages/core/cli/)
12
11- Mirrors better-auth's CLI approach: load config → collect plugin schemas → generate drizzle TS13- Mirrors better-auth's CLI approach: load config → collect plugin schemas → generate drizzle TS
12- `executor generate --config ./executor.config.ts --output ./src/services/executor-schema.ts`14- `executor generate --config ./executor.config.ts --output ./src/services/executor-schema.ts`
13- Generator ported from better-auth's drizzle generator, adapted for our DBSchema15- Generator ported from better-auth's drizzle generator, adapted for our DBSchema
@@ -15,11 +17,13 @@ · 1 unmodified line
15- Uses jiti for config loading (supports TS configs)17- Uses jiti for config loading (supports TS configs)
16 18
17### 3. apps/cloud wired up19### 3. apps/cloud wired up
20
18- `executor.config.ts` defines plugins with stub credentials (only used for schema shape)21- `executor.config.ts` defines plugins with stub credentials (only used for schema shape)
19- `drizzle.config.ts` points at both `schema.ts` (cloud tables) and `executor-schema.ts` (executor tables)22- `drizzle.config.ts` points at both `schema.ts` (cloud tables) and `executor-schema.ts` (executor tables)
20- Fresh `drizzle/0000_initial.sql` with all tables (cloud + core + plugin + blob), indexes, FKs23- Fresh `drizzle/0000_initial.sql` with all tables (cloud + core + plugin + blob), indexes, FKs
21 24
22### 4. apps/local wired up25### 4. apps/local wired up
26
23- `executor.config.ts` defines plugins + dialect sqlite27- `executor.config.ts` defines plugins + dialect sqlite
24- `drizzle.config.ts` for drizzle-kit generate/push28- `drizzle.config.ts` for drizzle-kit generate/push
25- Generated `executor-schema.ts` with all sqlite tables + blob table29- Generated `executor-schema.ts` with all sqlite tables + blob table
@@ -27,6 +31,7 @@ · 1 unmodified line
27- At startup: `migrate(db, { migrationsFolder })` from `drizzle-orm/bun-sqlite/migrator` applies pending migrations before constructing the adapter31- At startup: `migrate(db, { migrationsFolder })` from `drizzle-orm/bun-sqlite/migrator` applies pending migrations before constructing the adapter
28 32
29### 5. Adapter DDL removed33### 5. Adapter DDL removed
34
30- `makeSqliteAdapter` and `makePostgresAdapter` no longer run DDL — they return `DBAdapter` directly (not `Effect.Effect<DBAdapter>`)35- `makeSqliteAdapter` and `makePostgresAdapter` no longer run DDL — they return `DBAdapter` directly (not `Effect.Effect<DBAdapter>`)
31- `makeSqliteBlobStore` and `makePostgresBlobStore` no longer run DDL — they return `BlobStore` directly36- `makeSqliteBlobStore` and `makePostgresBlobStore` no longer run DDL — they return `BlobStore` directly
32- `buildCreateTableStatements` deleted from `storage-file/compile.ts`37- `buildCreateTableStatements` deleted from `storage-file/compile.ts`
@@ -39,6 +44,7 @@ · 6 unmodified lines
39The `@executor-js/config` package already maintains `executor.jsonc` as a source of truth for source configurations. The `config-store.ts` decorator intercepts `putSource`/`removeSource` on each plugin and mirrors to this file. So for existing users, `executor.jsonc` already has everything.44The `@executor-js/config` package already maintains `executor.jsonc` as a source of truth for source configurations. The `config-store.ts` decorator intercepts `putSource`/`removeSource` on each plugin and mirrors to this file. So for existing users, `executor.jsonc` already has everything.
40 45
41### Local app migration46### Local app migration
47
421. On first run, `migrate()` creates all tables from the initial migration481. On first run, `migrate()` creates all tables from the initial migration
432. Add a **sync-from-config step** after executor creation:492. Add a **sync-from-config step** after executor creation:
44 - `loadConfig(configPath)` reads `executor.jsonc`50 - `loadConfig(configPath)` reads `executor.jsonc`
@@ -47,28 +53,30 @@ The `@executor-js/config` package already maintains `executor.jsonc` as a source · 2 unmodified lines
473. Secrets survive untouched — they live in keychain/1password/file-secrets, not in the DB. The `secret` routing table gets re-populated when sources are added.533. Secrets survive untouched — they live in keychain/1password/file-secrets, not in the DB. The `secret` routing table gets re-populated when sources are added.
48 54
49### What won't survive (acceptable losses)55### What won't survive (acceptable losses)
56
50- Tool invocation history / blobs (old KV format)57- Tool invocation history / blobs (old KV format)
51- OAuth sessions (ephemeral, users re-auth)58- OAuth sessions (ephemeral, users re-auth)
52- Stale source state (re-fetching is actually a benefit)59- Stale source state (re-fetching is actually a benefit)
53 60
54### Cloud migration61### Cloud migration
62
55- Only ~2 users, just drop old tables and re-add manually63- Only ~2 users, just drop old tables and re-add manually
56- Or write a one-off SQL migration64- Or write a one-off SQL migration
57 65
58## Key files66## Key files
59 67
60| File | Role |68| File | Role |
61|------|------|69| --------------------------------------------- | ----------------------------------------- |
62| `packages/core/cli/src/commands/generate.ts` | CLI generate command |70| `packages/core/cli/src/commands/generate.ts` | CLI generate command |
63| `packages/core/cli/src/generators/drizzle.ts` | Drizzle schema generator |71| `packages/core/cli/src/generators/drizzle.ts` | Drizzle schema generator |
64| `packages/core/sdk/src/config.ts` | `defineExecutorConfig` helper |72| `packages/core/sdk/src/config.ts` | `defineExecutorConfig` helper |
65| `packages/core/config/src/schema.ts` | `executor.jsonc` schema |73| `packages/core/config/src/schema.ts` | `executor.jsonc` schema |
66| `packages/core/config/src/config-store.ts` | Plugin store → config file decorator |74| `packages/core/config/src/config-store.ts` | Plugin store → config file decorator |
67| `packages/core/config/src/load.ts` | Config loading |75| `packages/core/config/src/load.ts` | Config loading |
68| `apps/cloud/executor.config.ts` | Cloud config for CLI |76| `apps/cloud/executor.config.ts` | Cloud config for CLI |
69| `apps/cloud/src/services/executor-schema.ts` | Generated drizzle schema |77| `apps/cloud/src/services/executor-schema.ts` | Generated drizzle schema |
70| `apps/cloud/drizzle/0000_*.sql` | Fresh migration |78| `apps/cloud/drizzle/0000_*.sql` | Fresh migration |
71| `apps/local/executor.config.ts` | Local config for CLI |79| `apps/local/executor.config.ts` | Local config for CLI |
72| `apps/local/src/server/executor-schema.ts` | Generated drizzle schema |80| `apps/local/src/server/executor-schema.ts` | Generated drizzle schema |
73| `apps/local/drizzle/0000_*.sql` | Fresh migration |81| `apps/local/drizzle/0000_*.sql` | Fresh migration |
74| `apps/local/src/server/executor.ts` | Local executor bootstrap with `migrate()` |82| `apps/local/src/server/executor.ts` | Local executor bootstrap with `migrate()` |
notes/research/plugins/manifest-schema.md+92−94
@@ -72,24 +72,24 @@ The manifest should likely be a JSON-serializable object with these top-level se · 71 unmodified lines
72 72
73```ts73```ts
74type PluginManifest = {74type PluginManifest = {
75 schemaVersion: string75 schemaVersion: string;
76 id: string76 id: string;
77 name: string77 name: string;
78 version: string78 version: string;
79 description?: string79 description?: string;
80 publisher?: PluginPublisher80 publisher?: PluginPublisher;
81 license?: string81 license?: string;
82 homepage?: string82 homepage?: string;
83 repository?: string83 repository?: string;
84 categories?: PluginCategory[]84 categories?: PluginCategory[];
85 compatibility: PluginCompatibility85 compatibility: PluginCompatibility;
86 entrypoints: PluginEntrypoints86 entrypoints: PluginEntrypoints;
87 capabilities?: PluginCapabilities87 capabilities?: PluginCapabilities;
88 extensions?: PluginExtensions88 extensions?: PluginExtensions;
89 storage?: PluginStorageDeclaration89 storage?: PluginStorageDeclaration;
90 package?: PluginPackageDeclaration90 package?: PluginPackageDeclaration;
91 metadata?: Record<string, unknown>91 metadata?: Record<string, unknown>;
92}92};
93```93```
94 94
95This is a planning type, not a final API.95This is a planning type, not a final API.
@@ -135,9 +135,7 @@ The install surface should support package sources rather than assuming one cust · 39 unmodified lines
135User-facing install config can stay simple:135User-facing install config can stay simple:
136 136
137```ts137```ts
138type PluginInstallSpec =138type PluginInstallSpec = string | [string, Record<string, unknown>];
139 | string
140 | [string, Record<string, unknown>]
141```139```
142 140
143The string identifies the package source. Examples might include `@executor/openapi`, `github:owner/repo`, `https://github.com/owner/repo.git`, or `./plugins/my-plugin`. Tuple options are installation configuration, not manifest data.141The string identifies the package source. Examples might include `@executor/openapi`, `github:owner/repo`, `https://github.com/owner/repo.git`, or `./plugins/my-plugin`. Tuple options are installation configuration, not manifest data.
@@ -146,10 +144,10 @@ Resolved install metadata should be stored separately from the manifest. It shou · 2 unmodified lines
146 144
147```ts145```ts
148type PluginPackageDeclaration = {146type PluginPackageDeclaration = {
149 source?: "npm" | "git" | "github" | "local" | "registry"147 source?: "npm" | "git" | "github" | "local" | "registry";
150 packageName?: string148 packageName?: string;
151 repository?: string149 repository?: string;
152}150};
153```151```
154 152
155This `package` block is optional planning shape, not final schema. For npm packages, standard `package.json` fields may already provide most of this information.153This `package` block is optional planning shape, not final schema. For npm packages, standard `package.json` fields may already provide most of this information.
@@ -160,12 +158,12 @@ Compatibility should be explicit and checked before plugin code is loaded. · 4 unmodified lines
160 158
161```ts159```ts
162type PluginCompatibility = {160type PluginCompatibility = {
163 executor: string161 executor: string;
164 runtimeApi?: string162 runtimeApi?: string;
165 platforms?: PluginPlatform[]163 platforms?: PluginPlatform[];
166}164};
167 165
168type PluginPlatform = "local" | "cloud"166type PluginPlatform = "local" | "cloud";
169```167```
170 168
171`executor` should probably be a semver range for supported Executor versions.169`executor` should probably be a semver range for supported Executor versions.
@@ -182,27 +180,27 @@ Entrypoints describe executable code or assets the host may load. · 10 unmodified lines
182 180
183```ts181```ts
184type PluginEntrypoints = {182type PluginEntrypoints = {
185 main?: PluginCodeEntrypoint183 main?: PluginCodeEntrypoint;
186 sdk?: PluginCodeEntrypoint184 sdk?: PluginCodeEntrypoint;
187 source?: PluginCodeEntrypoint185 source?: PluginCodeEntrypoint;
188 secretStore?: PluginCodeEntrypoint186 secretStore?: PluginCodeEntrypoint;
189 apiGroup?: PluginCodeEntrypoint187 apiGroup?: PluginCodeEntrypoint;
190 apiHandlers?: PluginCodeEntrypoint188 apiHandlers?: PluginCodeEntrypoint;
191 routeHandlers?: Record<string, PluginCodeEntrypoint>189 routeHandlers?: Record<string, PluginCodeEntrypoint>;
192 backgroundServices?: Record<string, PluginCodeEntrypoint>190 backgroundServices?: Record<string, PluginCodeEntrypoint>;
193 reactSource?: PluginUiEntrypoint191 reactSource?: PluginUiEntrypoint;
194 reactSecretProvider?: PluginUiEntrypoint192 reactSecretProvider?: PluginUiEntrypoint;
195 ui?: PluginUiEntrypoint193 ui?: PluginUiEntrypoint;
196 adminUi?: PluginUiEntrypoint194 adminUi?: PluginUiEntrypoint;
197}195};
198 196
199type PluginCodeEntrypoint = {197type PluginCodeEntrypoint = {
200 module: string198 module: string;
201 export?: string199 export?: string;
202 runtime?: PluginRuntimeTarget200 runtime?: PluginRuntimeTarget;
203}201};
204 202
205type PluginRuntimeTarget = "worker" | "isolate" | "host-trusted"203type PluginRuntimeTarget = "worker" | "isolate" | "host-trusted";
206```204```
207 205
208The exact shape may change, but the manifest should avoid requiring the host to import a plugin just to discover what code exists.206The exact shape may change, but the manifest should avoid requiring the host to import a plugin just to discover what code exists.
@@ -237,21 +235,21 @@ Example planning shape: · 28 unmodified lines
237```ts235```ts
238type PluginLayerDeclaration = {236type PluginLayerDeclaration = {
239 sdk?: {237 sdk?: {
240 pluginId: string238 pluginId: string;
241 entrypoint: keyof PluginEntrypoints239 entrypoint: keyof PluginEntrypoints;
242 }240 };
243 api?: {241 api?: {
244 requiresSdkPlugin: string242 requiresSdkPlugin: string;
245 groupEntrypoint?: keyof PluginEntrypoints243 groupEntrypoint?: keyof PluginEntrypoints;
246 handlersEntrypoint?: keyof PluginEntrypoints244 handlersEntrypoint?: keyof PluginEntrypoints;
247 }245 };
248 react?: Array<{246 react?: Array<{
249 kind: "source" | "secret-provider" | "settings" | "admin"247 kind: "source" | "secret-provider" | "settings" | "admin";
250 requiresSdkPlugin?: string248 requiresSdkPlugin?: string;
251 requiresApi?: boolean249 requiresApi?: boolean;
252 entrypoint: keyof PluginEntrypoints250 entrypoint: keyof PluginEntrypoints;
253 }>251 }>;
254}252};
255```253```
256 254
257This is planning shape, not final schema. The important requirement is that hosts can opt into layers independently while still validating that the selected layers are compatible.255This is planning shape, not final schema. The important requirement is that hosts can opt into layers independently while still validating that the selected layers are compatible.
@@ -264,13 +262,13 @@ Extensions declare what the plugin contributes at a product level. · 6 unmodified lines
264 262
265```ts263```ts
266type PluginExtensions = {264type PluginExtensions = {
267 sources?: SourceExtensionDeclaration[]265 sources?: SourceExtensionDeclaration[];
268 secretStores?: SecretStoreExtensionDeclaration[]266 secretStores?: SecretStoreExtensionDeclaration[];
269 tools?: ToolExtensionDeclaration[]267 tools?: ToolExtensionDeclaration[];
270 routes?: RouteExtensionDeclaration[]268 routes?: RouteExtensionDeclaration[];
271 uiSurfaces?: UiSurfaceDeclaration[]269 uiSurfaces?: UiSurfaceDeclaration[];
272 backgroundServices?: BackgroundServiceDeclaration[]270 backgroundServices?: BackgroundServiceDeclaration[];
273}271};
274```272```
275 273
276Some extension declarations may be fully static. Others may be partial declarations that are completed by plugin registration at load time.274Some extension declarations may be fully static. Others may be partial declarations that are completed by plugin registration at load time.
@@ -283,16 +281,16 @@ Capabilities declare requested access. Grants are separate host state. · 6 unmodified lines
283 281
284```ts282```ts
285type PluginCapabilities = {283type PluginCapabilities = {
286 tools?: ToolCapabilityDeclaration[]284 tools?: ToolCapabilityDeclaration[];
287 sources?: SourceCapabilityDeclaration[]285 sources?: SourceCapabilityDeclaration[];
288 secrets?: SecretCapabilityDeclaration[]286 secrets?: SecretCapabilityDeclaration[];
289 network?: NetworkCapabilityDeclaration[]287 network?: NetworkCapabilityDeclaration[];
290 storage?: StorageCapabilityDeclaration[]288 storage?: StorageCapabilityDeclaration[];
291 scopes?: ScopeCapabilityDeclaration[]289 scopes?: ScopeCapabilityDeclaration[];
292 filesystem?: FilesystemCapabilityDeclaration[]290 filesystem?: FilesystemCapabilityDeclaration[];
293 execution?: ExecutionCapabilityDeclaration[]291 execution?: ExecutionCapabilityDeclaration[];
294 host?: HostCapabilityDeclaration[]292 host?: HostCapabilityDeclaration[];
295}293};
296```294```
297 295
298Capabilities should be deny-by-default. If a capability is missing, the runtime should deny that access.296Capabilities should be deny-by-default. If a capability is missing, the runtime should deny that access.
@@ -344,11 +342,11 @@ The manifest should let plugins declare storage needs separately from storage gr · 45 unmodified lines
344```ts342```ts
345type PluginStorageDeclaration = {343type PluginStorageDeclaration = {
346 collections?: Array<{344 collections?: Array<{
347 name: string345 name: string;
348 kind: "kv" | "document" | "sql"346 kind: "kv" | "document" | "sql";
349 description?: string347 description?: string;
350 }>348 }>;
351}349};
352```350```
353 351
354Storage declarations help the host pre-create, migrate, display, or approve plugin storage.352Storage declarations help the host pre-create, migrate, display, or approve plugin storage.
@@ -364,18 +362,18 @@ Executor does not need a custom publishing flow immediately. npm, GitHub/git, an · 9 unmodified lines
364```ts362```ts
365type PluginArtifacts = {363type PluginArtifacts = {
366 files?: Array<{364 files?: Array<{
367 path: string365 path: string;
368 size?: number366 size?: number;
369 sha256?: string367 sha256?: string;
370 kind?: "code" | "asset" | "manifest" | "source-map"368 kind?: "code" | "asset" | "manifest" | "source-map";
371 }>369 }>;
372 bundle?: {370 bundle?: {
373 format: "esm"371 format: "esm";
374 main: string372 main: string;
375 size?: number373 size?: number;
376 sha256?: string374 sha256?: string;
377 }375 };
378}376};
379```377```
380 378
381Cloud and local do not need to store cached artifacts the same way, but they should agree on immutable identity and verification where possible.379Cloud and local do not need to store cached artifacts the same way, but they should agree on immutable identity and verification where possible.

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t