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
6464 project, with workflows as the first consumer.
6565
6666 ```ts
67−type ArtifactKind =
68− | "workflow"
69− | "component"
70− | "ui"
71− | "template"
72− | "connector"
73− | "document";
67+type ArtifactKind = "workflow" | "component" | "ui" | "template" | "connector" | "document";
7468
7569 type ArtifactProject = {
7670 id: string;
@@ -171,18 +165,13 @@ export interface ArtifactStore { · 94 unmodified lines
171165 input: CreateArtifactProjectInput,
172166 ) => Effect.Effect<ArtifactProject, ArtifactError>;
173167
174− readonly readFile: (
175− ref: ArtifactRef,
176− path: string,
177− ) => Effect.Effect<Uint8Array, ArtifactError>;
168+ readonly readFile: (ref: ArtifactRef, path: string) => Effect.Effect<Uint8Array, ArtifactError>;
178169
179170 readonly writeFiles: (
180171 input: WriteArtifactFilesInput,
181172 ) => Effect.Effect<ArtifactCommit, ArtifactError>;
182173
183− readonly diff: (
184− input: ArtifactDiffInput,
185− ) => Effect.Effect<ArtifactDiff, ArtifactError>;
174+ readonly diff: (input: ArtifactDiffInput) => Effect.Effect<ArtifactDiff, ArtifactError>;
186175
187176 readonly forkProject: (
188177 input: ForkArtifactProjectInput,
@@ -365,10 +354,7 @@ export interface ArtifactStore { · 176 unmodified lines
365354 name: string;
366355 }) => Effect.Effect<ArtifactProject, ArtifactError>;
367356
368− readonly readFile: (
369− ref: ArtifactRef,
370− path: string,
371− ) => Effect.Effect<Uint8Array, ArtifactError>;
357+ readonly readFile: (ref: ArtifactRef, path: string) => Effect.Effect<Uint8Array, ArtifactError>;
372358
373359 readonly writeFiles: (input: {
374360 projectId: string;
@@ -422,10 +408,7 @@ Feature plugin: · 47 unmodified lines
422408 ```ts
423409 // @executor/workflows
424410 import { definePlugin, type PluginCtx } from "@executor/sdk";
425−import {
426− ArtifactStoreProtocol,
427− type ArtifactsProtocolCtx,
428−} from "@executor/artifacts";
411+import { ArtifactStoreProtocol, type ArtifactsProtocolCtx } from "@executor/artifacts";
429412
430413 type WorkflowCtx<TStore> = PluginCtx<TStore, ArtifactsProtocolCtx>;
431414
@@ -546,56 +529,47 @@ export async function summarizeTicket(input: SupportEmail) { · 114 unmodified lines
546529 E2E flow:
547530
548531 ```ts
549−it.effect(
550− "creates a code-first workflow artifact and indexes a visual graph",
551− () =>
552− Effect.gen(function* () {
553− const executor = yield* createExecutor({
554− scopes: [testScope],
555− adapter,
556− blobs,
557− plugins: [
558− artifactsLocalGitPlugin({ rootDir: tempDir }),
559− workflowsPlugin(),
560− ] as const,
561− protocols: {
562− "executor.artifacts.store": "artifacts-local-git",
532+it.effect("creates a code-first workflow artifact and indexes a visual graph", () =>
533+ Effect.gen(function* () {
534+ const executor = yield* createExecutor({
535+ scopes: [testScope],
536+ adapter,
537+ blobs,
538+ plugins: [artifactsLocalGitPlugin({ rootDir: tempDir }), workflowsPlugin()] as const,
539+ protocols: {
540+ "executor.artifacts.store": "artifacts-local-git",
541+ },
542+ });
543+
544+ const created = yield* executor.workflows.create({
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,
563560 },
564− });
565−
566− const created = yield* executor.workflows.create({
567− name: "Support triage",
568− files: [
569− {
570− type: "put",
571− path: "artifact.json",
572− contents: JSON.stringify({
573− schemaVersion: 1,
574− kind: "workflow",
575− entrypoints: ["src/workflows/support-triage.workflow.ts"],
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− }),
561+ ],
562+ });
563+
564+ const source = yield* executor.artifacts.readFile(
565+ { projectId: created.project.id, ref: created.commit.id },
566+ "src/workflows/support-triage.workflow.ts",
567+ );
568+ expect(new TextDecoder().decode(source)).toContain('"use workflow"');
569+
570+ expect(created.manifest.entrypoints).toEqual(["src/workflows/support-triage.workflow.ts"]);
571+ expect(created.manifest.graph.nodes.map((node) => node.kind)).toContain("step");
572+ }),
599573 );
600574 ```
601575
notes/dynamic-plugin-loading-v1.md+177−187
@@ -89,7 +89,7 @@ Surveyed five plugin systems in `.reference/`: · 88 unmodified lines
8989 - **emdash** — **closest match to our setup.** Astro/Vite + React, plugins are
9090 npm packages with separate `./` and `./admin` exports, registered via
9191 import-and-call in `astro.config.mjs`. `definePlugin({ id, hooks, routes,
92− admin })` declarative shape.
92+admin })` declarative shape.
9393 - **dynamic-software** — most ambitious; Cloudflare Worker Loader for cloud
9494 isolation, iframe + postMessage RPC for UI, Proxy-based typed API client.
9595
@@ -109,10 +109,10 @@ inference flows naturally; no codegen step. · 13 unmodified lines
109109
110110 ```ts
111111 // apps/local/executor.config.ts (after)
112−import { defineExecutorConfig } from "@executor-js/sdk"
113−import { openApiPlugin } from "@executor-js/plugin-openapi"
114−import { mcpPlugin } from "@executor-js/plugin-mcp"
115−import { fileSecretsPlugin } from "@executor-js/plugin-file-secrets"
112+import { defineExecutorConfig } from "@executor-js/sdk";
113+import { openApiPlugin } from "@executor-js/plugin-openapi";
114+import { mcpPlugin } from "@executor-js/plugin-mcp";
115+import { fileSecretsPlugin } from "@executor-js/plugin-file-secrets";
116116
117117 export default defineExecutorConfig({
118118 dialect: "sqlite",
@@ -121,7 +121,7 @@ export default defineExecutorConfig({ · 2 unmodified lines
121121 mcpPlugin({ dangerouslyAllowStdioMCP: true }),
122122 fileSecretsPlugin(),
123123 ] as const,
124−})
124+});
125125 ```
126126
127127 "Dynamic" means npm-installable, not load-by-string-name. Same model as emdash.
@@ -137,12 +137,12 @@ config becomes a need. · 9 unmodified lines
137137 "type": "module",
138138 "exports": {
139139 "./server": "./dist/server.js",
140− "./client": "./dist/client.js"
140+ "./client": "./dist/client.js",
141141 },
142142 "executor": {
143143 "id": "foo",
144− "version": "0.1.0"
145− }
144+ "version": "0.1.0",
145+ },
146146 }
147147 ```
148148
@@ -171,7 +171,7 @@ Client side: separate primitive `defineClientPlugin` lives in · 22 unmodified lines
171171 React types never leak into server bundles.
172172
173173 **Layering — extension is the canonical SDK; routes/handlers is optional HTTP transport.**
174−The HTTP layer is *not a peer* of the SDK; it's a transport over it. Plugin
174+The HTTP layer is _not a peer_ of the SDK; it's a transport over it. Plugin
175175 authors should treat extension as the implementation and write handlers as
176176 thin wrappers that delegate via the `self` parameter (same pattern as
177177 `staticSources(self) => [...]` already in the codebase). This keeps:
@@ -188,7 +188,7 @@ Three plugin shapes fall out of this layering: · 10 unmodified lines
188188 utility for other plugins or scripts. CLI/embedded consumers use
189189 `executor.<id>.method()`. Vite plugin notices no `./client` and skips the
190190 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:
192192 openapi, mcp, anything that needs a frontend. Routes are thin wrappers
193193 over extension methods.
194194 - **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
214214 `@executor-js/sdk` (server) and `@executor-js/sdk/client` (frontend). The SDK
215215 re-exports `Schema`, `HttpApi`, `HttpApiGroup`, `HttpApiEndpoint`,
216216 `HttpApiBuilder`, `Effect` so authors who don't want to dig into Effect
217−don't have to. Authors who *do* want Effect-native code keep importing from
217+don't have to. Authors who _do_ want Effect-native code keep importing from
218218 `effect` directly. This keeps the door open without forcing the dependency.
219219
220220 ### 6. Skip `capabilities` declaration for v1
@@ -247,7 +247,7 @@ already handles this exact pattern for secrets and works fine: · 26 unmodified lines
247247 secretProviders: (ctx) => [makeScopedProvider(...)]
248248 ```
249249
250−Each new pluggable capability gets its *own* typed field on `PluginSpec`,
250+Each new pluggable capability gets its _own_ typed field on `PluginSpec`,
251251 same shape. When artifacts lands, that's `artifactStore: () =>
252252 ArtifactStore`. If connection providers want to be modeled this way, the
253253 existing `connectionProviders` field already is.
@@ -335,24 +335,21 @@ server both import it: · 81 unmodified lines
335335
336336 ```ts
337337 // @executor-js/plugin-foo/src/shared.ts
338−import { HttpApiEndpoint, HttpApiGroup, Schema } from "@executor-js/sdk"
338+import { HttpApiEndpoint, HttpApiGroup, Schema } from "@executor-js/sdk";
339339
340340 export const Thing = Schema.Struct({
341341 id: Schema.String,
342342 name: Schema.String,
343−})
343+});
344344
345345 export const FooApi = HttpApiGroup.make("foo")
346− .add(
347− HttpApiEndpoint.get("listThings")`/things`
348− .addSuccess(Schema.Array(Thing)),
349− )
346+ .add(HttpApiEndpoint.get("listThings")`/things`.addSuccess(Schema.Array(Thing)))
350347 .add(
351348 HttpApiEndpoint.post("syncThing")`/sync/:id`
352349 .setPath(Schema.Struct({ id: Schema.String }))
353350 .addSuccess(Thing)
354351 .addError(SyncError),
355− )
352+ );
356353 ```
357354
358355 ```ts
@@ -429,49 +426,47 @@ Lives in `@executor-js/sdk/client`. Server bundles cannot import this module. · 70 unmodified lines
429426 ```ts
430427 // packages/core/sdk/src/client.ts
431428 export interface ClientPluginSpec<TId extends string = string> {
432− readonly id: TId
429+ readonly id: TId;
433430
434431 /** Pages contributed to the host's TanStack router. Mounted under
435432 * /plugins/{id}/{path}. Sidebar nav metadata declared on the route. */
436− readonly pages?: readonly PageDecl[]
433+ readonly pages?: readonly PageDecl[];
437434
438435 /** Dashboard / overview widgets the host can render in known slots. */
439− readonly widgets?: readonly WidgetDecl[]
436+ readonly widgets?: readonly WidgetDecl[];
440437
441438 /** Components the host can render in named slots (e.g., source-detail
442439 * panels, secret-picker variants). Slot names are part of the host
443440 * contract — plugin opts in by registering. */
444− readonly slots?: Record<string, SlotComponent>
441+ readonly slots?: Record<string, SlotComponent>;
445442
446443 /** Source-plugin contribution — populated by plugins that expose
447444 * `kind` rows in the core `source` table (openapi, mcp, graphql,
448445 * google-discovery). The host's sources page derives its provider
449446 * list from the union of every loaded plugin's `sourcePlugin`
450447 * via `useSourcePlugins()`. */
451− readonly sourcePlugin?: SourcePlugin
448+ readonly sourcePlugin?: SourcePlugin;
452449
453450 /** Secret-provider-plugin contribution — populated by plugins that
454451 * also ship a `secretProviders` server-side capability AND want to
455452 * expose a settings card on the host's secrets page. Surfaced via
456453 * `useSecretProviderPlugins()`. */
457− readonly secretProviderPlugin?: SecretProviderPlugin
454+ readonly secretProviderPlugin?: SecretProviderPlugin;
458455 }
459456
460457 type PageDecl = {
461− path: string // "/", "/edit/$id"
462− component: ComponentType
463− nav?: { label: string; section?: string }
464−}
458+ path: string; // "/", "/edit/$id"
459+ component: ComponentType;
460+ nav?: { label: string; section?: string };
461+};
465462
466463 type WidgetDecl = {
467− id: string
468− component: ComponentType<WidgetProps>
469− size?: "half" | "full"
470−}
464+ id: string;
465+ component: ComponentType<WidgetProps>;
466+ size?: "half" | "full";
467+};
471468
472−export const defineClientPlugin = <TId extends string>(
473− spec: ClientPluginSpec<TId>,
474−) => spec
469+export const defineClientPlugin = <TId extends string>(spec: ClientPluginSpec<TId>) => spec;
475470 ```
476471
477472 ### `createPluginAtomClient` — typed reactive client per plugin
@@ -483,24 +478,21 @@ authors write one line per atom. Same shape as the existing · 5 unmodified lines
483478
484479 ```ts
485480 // packages/core/sdk/src/client.ts (helper)
486−import { HttpApi } from "effect/unstable/httpapi"
487−import { FetchHttpClient } from "effect/unstable/http"
488−import * as AtomHttpApi from "effect/unstable/reactivity/AtomHttpApi"
481+import { HttpApi } from "effect/unstable/httpapi";
482+import { FetchHttpClient } from "effect/unstable/http";
483+import * as AtomHttpApi from "effect/unstable/reactivity/AtomHttpApi";
489484
490485 export const createPluginAtomClient = <G extends HttpApiGroup.Any>(
491486 group: G,
492487 opts: { pluginId: string },
493488 ) => {
494− const bundle = HttpApi.make(`plugin-${opts.pluginId}`).add(group)
495− return AtomHttpApi.Service<`Plugin_${string}Client`>()(
496− `Plugin_${opts.pluginId}Client`,
497− {
498− api: bundle,
499− httpClient: FetchHttpClient.layer,
500− baseUrl: `/_executor/plugins/${opts.pluginId}`,
501− },
502− )
503−}
489+ const bundle = HttpApi.make(`plugin-${opts.pluginId}`).add(group);
490+ return AtomHttpApi.Service<`Plugin_${string}Client`>()(`Plugin_${opts.pluginId}Client`, {
491+ api: bundle,
492+ httpClient: FetchHttpClient.layer,
493+ baseUrl: `/_executor/plugins/${opts.pluginId}`,
494+ });
495+};
504496 ```
505497
506498 Plugin author writes:
@@ -513,39 +505,34 @@ import { · 6 unmodified lines
513505 useAtomValue,
514506 useAtomSet,
515507 AsyncResult,
516−} from "@executor-js/sdk/client"
517−import { FooApi } from "./shared"
508+} from "@executor-js/sdk/client";
509+import { FooApi } from "./shared";
518510
519−const FooClient = createPluginAtomClient(FooApi, { pluginId: "foo" })
511+const FooClient = createPluginAtomClient(FooApi, { pluginId: "foo" });
520512
521513 export const fooThingsAtom = FooClient.query("foo", "listThings", {
522514 timeToLive: "30 seconds",
523515 reactivityKeys: ["foo:things"],
524−})
516+});
525517
526−export const fooSync = FooClient.mutation("foo", "syncThing")
518+export const fooSync = FooClient.mutation("foo", "syncThing");
527519
528520 const FooPage = () => {
529− const things = useAtomValue(fooThingsAtom)
530− const doSync = useAtomSet(fooSync, { mode: "promise" })
521+ const things = useAtomValue(fooThingsAtom);
522+ const doSync = useAtomSet(fooSync, { mode: "promise" });
531523
532524 return AsyncResult.match(things, {
533525 onInitial: () => <Skeleton />,
534526 onFailure: () => <p>Failed to load</p>,
535− onSuccess: ({ value }) => (
536− <Table
537− rows={value}
538− onSync={(id) => doSync({ path: { id } })}
539− />
540− ),
541− })
542−}
527+ onSuccess: ({ value }) => <Table rows={value} onSync={(id) => doSync({ path: { id } })} />,
528+ });
529+};
543530
544531 export default defineClientPlugin({
545532 id: "foo" as const,
546533 pages: [{ path: "/", component: FooPage, nav: { label: "Foo" } }],
547534 widgets: [{ id: "foo-status", component: FooStatus, size: "half" }],
548−})
535+});
549536 ```
550537
551538 Type inference: `FooClient.query("foo", "listThings", ...)` is fully typed
@@ -608,15 +595,17 @@ Single config, two consumers. · 56 unmodified lines
608595
609596 ```ts
610597 // apps/local/src/server/executor.ts (after)
611−import config from "../../executor.config.ts"
612−
613−const executor = yield* createExecutor({
614− scopes: [scope],
615− adapter,
616− blobs,
617− plugins: config.plugins,
618− onElicitation: "accept-all",
619−})
598+import config from "../../executor.config.ts";
599+
600+const executor =
601+ yield *
602+ createExecutor({
603+ scopes: [scope],
604+ adapter,
605+ blobs,
606+ plugins: config.plugins,
607+ onElicitation: "accept-all",
608+ });
620609 ```
621610
622611 The plugins are already configured (factory called) by the time
@@ -631,18 +620,16 @@ export default function executorVite(): Plugin { · 8 unmodified lines
631620 return {
632621 name: "executor-plugins",
633622 resolveId(id) {
634− if (id === "virtual:executor/plugins-client") return "\0" + id
623+ if (id === "virtual:executor/plugins-client") return "\0" + id;
635624 },
636625 async load(id) {
637− if (id !== "\0virtual:executor/plugins-client") return
638− const config = await loadExecutorConfig()
639− const imports = config.plugins
640− .map((p, i) => `import p${i} from "${p.id}/client"`)
641− .join("\n")
642− const list = config.plugins.map((_, i) => `p${i}`).join(", ")
643− return `${imports}\nexport const plugins = [${list}]`
626+ if (id !== "\0virtual:executor/plugins-client") return;
627+ const config = await loadExecutorConfig();
628+ const imports = config.plugins.map((p, i) => `import p${i} from "${p.id}/client"`).join("\n");
629+ const list = config.plugins.map((_, i) => `p${i}`).join(", ");
630+ return `${imports}\nexport const plugins = [${list}]`;
644631 },
645− }
632+ };
646633 }
647634 ```
648635
@@ -655,11 +642,11 @@ file — `__root.tsx` is the only place that touches · 6 unmodified lines
655642
656643 ```tsx
657644 // apps/local/src/routes/__root.tsx
658−import { createRootRoute } from "@tanstack/react-router"
659−import { ExecutorProvider } from "@executor-js/react/api/provider"
660−import { ExecutorPluginsProvider } from "@executor-js/sdk/client"
661−import { plugins as clientPlugins } from "virtual:executor/plugins-client"
662−import { Shell } from "../web/shell"
645+import { createRootRoute } from "@tanstack/react-router";
646+import { ExecutorProvider } from "@executor-js/react/api/provider";
647+import { ExecutorPluginsProvider } from "@executor-js/sdk/client";
648+import { plugins as clientPlugins } from "virtual:executor/plugins-client";
649+import { Shell } from "../web/shell";
663650
664651 export const Route = createRootRoute({
665652 component: () => (
@@ -669,15 +656,15 @@ export const Route = createRootRoute({ · 3 unmodified lines
669656 </ExecutorPluginsProvider>
670657 </ExecutorProvider>
671658 ),
672−})
659+});
673660 ```
674661
675662 ```tsx
676663 // e.g. inside packages/react/src/pages/sources.tsx
677−import { useSourcePlugins } from "@executor-js/sdk/client"
664+import { useSourcePlugins } from "@executor-js/sdk/client";
678665
679666 export function SourcesPage() {
680− const sourcePlugins = useSourcePlugins()
667+ const sourcePlugins = useSourcePlugins();
681668 // ...
682669 }
683670 ```
@@ -701,49 +688,48 @@ needs a Vite restart (not a hot update — config changed). · 17 unmodified lines
701688
702689 ```ts
703690 // src/shared.ts — only @executor-js/sdk imports, no raw effect imports
704−import { HttpApiEndpoint, HttpApiGroup, Schema } from "@executor-js/sdk"
691+import { HttpApiEndpoint, HttpApiGroup, Schema } from "@executor-js/sdk";
705692
706693 export const Greeting = Schema.Struct({
707694 message: Schema.String,
708695 count: Schema.Number,
709−})
710−export type Greeting = typeof Greeting.Type
696+});
697+export type Greeting = typeof Greeting.Type;
711698
712699 export const ExampleApi = HttpApiGroup.make("example").add(
713700 HttpApiEndpoint.post("greet", "/greet", {
714701 payload: Schema.Struct({ name: Schema.String }),
715702 success: Greeting,
716703 }),
717−)
704+);
718705 ```
719706
720707 ```ts
721708 // src/server.ts
722−import {
723− Context, definePlugin, Effect, HttpApi, HttpApiBuilder,
724−} from "@executor-js/sdk"
725−import { ExampleApi } from "./shared"
709+import { Context, definePlugin, Effect, HttpApi, HttpApiBuilder } from "@executor-js/sdk";
710+import { ExampleApi } from "./shared";
726711
727−const ExampleApiBundle = HttpApi.make("example").add(ExampleApi)
712+const ExampleApiBundle = HttpApi.make("example").add(ExampleApi);
728713
729714 interface ExampleExtension {
730715 readonly greet: (
731716 name: string,
732− ) => Effect.Effect<{ readonly message: string; readonly count: number }>
717+ ) => Effect.Effect<{ readonly message: string; readonly count: number }>;
733718 }
734719
735720 export class ExampleExtensionService extends Context.Service<
736− ExampleExtensionService, ExampleExtension,
721+ ExampleExtensionService,
722+ ExampleExtension
737723 >()("ExampleExtensionService") {}
738724
739725 const ExampleHandlers = HttpApiBuilder.group(ExampleApiBundle, "example", (h) =>
740726 h.handle("greet", ({ payload }) =>
741727 Effect.gen(function* () {
742− const ext = yield* ExampleExtensionService
743− return yield* ext.greet(payload.name)
728+ const ext = yield* ExampleExtensionService;
729+ return yield* ext.greet(payload.name);
744730 }),
745731 ),
746−)
732+);
747733
748734 export const examplePlugin = definePlugin(() => ({
749735 id: "example" as const,
@@ -754,57 +740,57 @@ export const examplePlugin = definePlugin(() => ({ · 4 unmodified lines
754740 extension: (ctx): ExampleExtension => ({
755741 greet: (name: string) =>
756742 Effect.sync(() => {
757− ctx.storage.count += 1
743+ ctx.storage.count += 1;
758744 return {
759745 message: `hello ${name}`,
760746 count: ctx.storage.count,
761− }
747+ };
762748 }),
763749 }),
764750
765751 routes: () => ExampleApi,
766752 handlers: () => ExampleHandlers,
767753 extensionService: ExampleExtensionService,
768−}))
754+}));
769755
770−export default examplePlugin
756+export default examplePlugin;
771757 ```
772758
773759 ```tsx
774760 // src/client.tsx
775−import {
776− defineClientPlugin,
777− createPluginAtomClient,
778− useAtomSet,
779−} from "@executor-js/sdk/client"
780−import { useState } from "react"
781−import { ExampleApi } from "./shared"
761+import { defineClientPlugin, createPluginAtomClient, useAtomSet } from "@executor-js/sdk/client";
762+import { useState } from "react";
763+import { ExampleApi } from "./shared";
782764
783−const ExampleClient = createPluginAtomClient(ExampleApi, { pluginId: "example" })
765+const ExampleClient = createPluginAtomClient(ExampleApi, { pluginId: "example" });
784766
785−const greetAtom = ExampleClient.mutation("example", "greet")
767+const greetAtom = ExampleClient.mutation("example", "greet");
786768
787769 const ExamplePage = () => {
788− const [name, setName] = useState("world")
789− const [result, setResult] = useState<string>()
790− const doGreet = useAtomSet(greetAtom, { mode: "promise" })
770+ const [name, setName] = useState("world");
771+ const [result, setResult] = useState<string>();
772+ const doGreet = useAtomSet(greetAtom, { mode: "promise" });
791773
792774 return (
793775 <div>
794776 <input value={name} onChange={(e) => setName(e.target.value)} />
795− <button onClick={async () => {
796− const g = await doGreet({ payload: { name } })
797− setResult(`${g.message} (#${g.count})`)
798− }}>Greet</button>
777+ <button
778+ onClick={async () => {
779+ const g = await doGreet({ payload: { name } });
780+ setResult(`${g.message} (#${g.count})`);
781+ }}
782+ >
783+ Greet
784+ </button>
799785 {result && <pre>{result}</pre>}
800786 </div>
801− )
802−}
787+ );
788+};
803789
804790 export default defineClientPlugin({
805791 id: "example" as const,
806792 pages: [{ path: "/", component: ExamplePage, nav: { label: "Example" } }],
807−})
793+});
808794 ```
809795
810796 ```jsonc
@@ -814,13 +800,13 @@ export default defineClientPlugin({ · 3 unmodified lines
814800 "type": "module",
815801 "exports": {
816802 "./server": "./dist/server.js",
817− "./client": "./dist/client.js"
803+ "./client": "./dist/client.js",
818804 },
819805 "executor": { "id": "example", "version": "0.1.0" },
820806 "peerDependencies": {
821807 "@executor-js/sdk": "workspace:*",
822− "react": "catalog:"
823− }
808+ "react": "catalog:",
809+ },
824810 }
825811 ```
826812
@@ -828,8 +814,8 @@ User adds it: · 1 unmodified line
828814
829815 ```ts
830816 // apps/local/executor.config.ts
831−import { examplePlugin } from "@executor-js/plugin-example/server"
832−plugins: [/* ... */, examplePlugin()]
817+import { examplePlugin } from "@executor-js/plugin-example/server";
818+plugins: [, /* ... */ examplePlugin()];
833819 ```
834820
835821 ## SDK-only plugin example
@@ -852,81 +838,82 @@ file with no `./client` export, no `routes`, no `handlers`. · 16 unmodified lines
852838 "type": "module",
853839 "exports": { "./server": "./dist/server.js" },
854840 "executor": { "id": "rateLimiter", "version": "0.1.0" },
855− "peerDependencies": { "@executor-js/sdk": "workspace:*" }
841+ "peerDependencies": { "@executor-js/sdk": "workspace:*" },
856842 }
857843 ```
858844
859845 ```ts
860846 // src/server.ts
861−import { definePlugin, defineSchema, Effect, Schema } from "@executor-js/sdk"
847+import { definePlugin, defineSchema, Effect, Schema } from "@executor-js/sdk";
862848
863849 interface RateLimiterConfig {
864− defaultLimit?: number
850+ defaultLimit?: number;
865851 }
866852
867−class RateLimitExceeded extends Schema.TaggedError<RateLimitExceeded>()(
868− "RateLimitExceeded",
869− { key: Schema.String, retryAfterMs: Schema.Number },
870−) {}
871−
872−export const rateLimiterPlugin = definePlugin(
873− (opts: RateLimiterConfig = {}) => ({
874− id: "rateLimiter" as const,
875−
876− schema: defineSchema({
877− rate_buckets: {
878− fields: {
879− key: { type: "string", primary: true },
880− tokens: { type: "number" },
881− updated_at: { type: "number" },
882− },
853+class RateLimitExceeded extends Schema.TaggedError<RateLimitExceeded>()("RateLimitExceeded", {
854+ key: Schema.String,
855+ retryAfterMs: Schema.Number,
856+}) {}
857+
858+export const rateLimiterPlugin = definePlugin((opts: RateLimiterConfig = {}) => ({
859+ id: "rateLimiter" as const,
860+
861+ schema: defineSchema({
862+ rate_buckets: {
863+ fields: {
864+ key: { type: "string", primary: true },
865+ tokens: { type: "number" },
866+ updated_at: { type: "number" },
883867 },
884− }),
868+ },
869+ }),
885870
886− storage: ({ adapter }) => ({
887− adapter,
888− defaultLimit: opts.defaultLimit ?? 60,
889− }),
871+ storage: ({ adapter }) => ({
872+ adapter,
873+ defaultLimit: opts.defaultLimit ?? 60,
874+ }),
890875
891− extension: (ctx) => ({
892− check: (key: string, cost = 1) =>
893− Effect.gen(function* () {
894− const bucket = yield* readOrCreateBucket(ctx.storage, key)
895− const refilled = refill(bucket, ctx.storage.defaultLimit)
896− if (refilled.tokens < cost) {
897− return yield* new RateLimitExceeded({
898− key,
899− retryAfterMs: estimateRetry(refilled),
900− })
901− }
902− yield* writeBucket(ctx.storage, key, refilled.tokens - cost)
903− return { allowed: true, remaining: refilled.tokens - cost }
904− }),
876+ extension: (ctx) => ({
877+ check: (key: string, cost = 1) =>
878+ Effect.gen(function* () {
879+ const bucket = yield* readOrCreateBucket(ctx.storage, key);
880+ const refilled = refill(bucket, ctx.storage.defaultLimit);
881+ if (refilled.tokens < cost) {
882+ return yield* new RateLimitExceeded({
883+ key,
884+ retryAfterMs: estimateRetry(refilled),
885+ });
886+ }
887+ yield* writeBucket(ctx.storage, key, refilled.tokens - cost);
888+ return { allowed: true, remaining: refilled.tokens - cost };
889+ }),
905890
906− reset: (key: string) =>
907− Effect.tryPromise(() =>
908− ctx.storage.adapter.delete({
909− model: "rate_buckets",
910− where: [["key", "=", key]],
911− }),
912− ),
913− }),
891+ reset: (key: string) =>
892+ Effect.tryPromise(() =>
893+ ctx.storage.adapter.delete({
894+ model: "rate_buckets",
895+ where: [["key", "=", key]],
896+ }),
897+ ),
914898 }),
915−)
899+}));
916900 ```
917901
918902 Pure programmatic consumption — works identically from CLI, tests,
919903 embedded library use, or another plugin's `extension`:
920904
921905 ```ts
922−const result = yield* executor.rateLimiter.check("user-123", 5)
906+const result = yield * executor.rateLimiter.check("user-123", 5);
923907 // ^? { allowed: true; remaining: number }
924908
925−yield* executor.rateLimiter.check("user-123", 1000).pipe(
926− Effect.catchTag("RateLimitExceeded", (err) =>
927− Effect.log(`hit limit on ${err.key}, retry in ${err.retryAfterMs}ms`),
928− ),
929−)
909+yield *
910+ executor.rateLimiter
911+ .check("user-123", 1000)
912+ .pipe(
913+ Effect.catchTag("RateLimitExceeded", (err) =>
914+ Effect.log(`hit limit on ${err.key}, retry in ${err.retryAfterMs}ms`),
915+ ),
916+ );
930917 ```
931918
932919 What the host does with this plugin:
@@ -1000,6 +987,7 @@ Build order — all shipped. · 67 unmodified lines
1000987 ## References
1001988
1002989 **Reference research (pre-design):**
990+
1003991 - emdash: `astro.config.mjs` import-and-call pattern, `package.json` exports
1004992 for separate admin entry — see `.reference/emdash/demos/plugins-demo/`.
1005993 - dynamic-software: PluginManifest + capability declarations idea (deferred);
@@ -1008,6 +996,7 @@ Build order — all shipped. · 2 unmodified lines
1008996 when we add capability enforcement).
1009997
1010998 **Shipped code:**
999+
10111000 - Plugin contract: `packages/core/sdk/src/plugin.ts` (`PluginSpec`,
10121001 `definePlugin`).
10131002 - Client contract: `packages/core/sdk/src/client.ts`
@@ -1030,6 +1019,7 @@ Build order — all shipped. · 16 unmodified lines
10301019 follows the same per-capability-typed-field pattern.
10311020
10321021 **Adjacent plans:**
1022+
10331023 - `notes/artifacts-workflows-and-generated-ui.md` — the artifacts/workflows
10341024 plan. Uses "protocol" terminology that we're not adopting; read its
10351025 "ArtifactStoreProtocol" as "the typed interface that goes into the
notes/mcp-conn-pool.md+9−7
@@ -4,7 +4,7 @@ · 3 unmodified lines
44
55 The MCP plugin **already pools connections within a session DO**. Production
66 telemetry attributing 17% of `plugin.mcp.connection.acquire` calls to a
7−fresh `plugin.mcp.connection.handshake` represents *cold-start sessions*,
7+fresh `plugin.mcp.connection.handshake` represents _cold-start sessions_,
88 not redundant in-session handshakes. No structural change is required;
99 this PR ships a strict regression test that pins the existing contract
1010 plus 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
6868 ## Re-reading the production trace
6969
7070 Trace `b7102047bed975da461c0519d1251de4`:
71+
7172 - `mcp.plugin.resolve_connector` 2.72s
72−- `plugin.mcp.connection.acquire` 2.72s
73+- `plugin.mcp.connection.acquire` 2.72s
7374 - `plugin.mcp.connection.handshake` 2.52s
74−- `executor.storage.transaction` 1.02s (token persist)
75−- `plugin.mcp.client.call_tool` 1.48s
75+- `executor.storage.transaction` 1.02s (token persist)
76+- `plugin.mcp.client.call_tool` 1.48s
7677
7778 Span nesting plus `resolve_connector ≈ acquire ≈ handshake` durations is
7879 the 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
8081 happens on a miss.
8182
8283 8h aggregate:
84+
8385 - `plugin.mcp.connection.handshake` count 4
84−- `plugin.mcp.connection.acquire` count 24
85−- `mcp.plugin.resolve_connector` count 4
86+- `plugin.mcp.connection.acquire` count 24
87+- `mcp.plugin.resolve_connector` count 4
8688
87−Acquire count is 6× handshake count. The cache *is* hitting on 20 of 24
89+Acquire count is 6× handshake count. The cache _is_ hitting on 20 of 24
8890 calls. The 4 misses are cold-start sessions (each MCP session DO's first
8991 tool call, plus any session where the connection went stale and was
9092 invalidated 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
2121
2222 1. `INSERT INTO connection` with the new pointer's `connectionId`,
2323 `provider = "openapi:oauth2"`, and `provider_state` = `{flow,
24− tokenUrl, clientIdSecretId, clientSecretSecretId, scopes}` lifted
24+tokenUrl, clientIdSecretId, clientSecretSecretId, scopes}` lifted
2525 from the legacy row. `access_token_secret_id` + `refresh_token_secret_id`
2626 reuse the legacy secret ids — tokens themselves never move, only
2727 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
8787
8888 ```ts
8989 interface ErrorCaptureShape {
90− readonly captureException: (cause: Cause<unknown>) => Effect<string>
90+ readonly captureException: (cause: Cause<unknown>) => Effect<string>;
9191 }
9292 ```
9393
@@ -130,7 +130,7 @@ Plugin authors write normal Effect code. Their extension method error · 36 unmodified lines
130130 unions look like:
131131
132132 ```ts
133−Effect.Effect<X, MyPluginTypedError | StorageError, never>
133+Effect.Effect<X, MyPluginTypedError | StorageError, never>;
134134 ```
135135
136136 Where `MyPluginTypedError` is the union of their own
@@ -152,9 +152,9 @@ class McpGroup extends HttpApiGroup.make("mcp") · 15 unmodified lines
152152 // …
153153 .addError(InternalError)
154154 .addError(McpOAuthError)
155− .addError(McpConnectionError)
155+ .addError(McpConnectionError) {
156156 // …
157−{}
157+}
158158 ```
159159
160160 No 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
5454 Effect.catchAll((e) => {
5555 const message = e instanceof Error ? e.message : String(e);
5656 return executor.sources.recordError({ source, message }).pipe(Effect.asVoid);
57−})
57+});
5858 ```
5959
6060 `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
7676 ## Scope
7777
7878 Not for the scope-refactor PR. Separate change:
79+
7980 - Schema migration adds two columns.
8081 - `executor.sources.recordError` on the core SDK.
8182 - Wire up in config-sync.
notes/old/indexes.md+1−1
@@ -25,7 +25,7 @@ Tables affected (all drop candidates): · 24 unmodified lines
2525 - `workos_vault_metadata_scope_id_idx`
2626
2727 Root cause: `core-schema.ts` and plugin schemas mark `scope_id: {
28−index: true }`. The drizzle generator should probably *skip* emitting
28+index: true }`. The drizzle generator should probably _skip_ emitting
2929 a single-column index on a column that's already the leftmost column
3030 of a composite primary key. Fix in two places:
3131
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
55
66 ## Suites at a glance
77
8−| file | pool | drives | what it proves |
9−|---|---|---|---|
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 |
8+| file | pool | drives | what it proves |
9+| ----------------------------------------------- | ----------------------------- | --------------------------------------------------- | --------------------------------------------------------------------------------------------- |
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 |
1212 | `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 |
1313
1414 ## The workerd cross-request I/O wall
@@ -86,12 +86,12 @@ Pattern used in `mcp-miniflare.e2e.node.test.ts`: · 71 unmodified lines
8686 3. Read the bound port from `HttpServer.HttpServer.address` (TcpAddress tag)
8787 inside a `Layer.effect` that produces the test service.
8888 4. 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`.
9090 5. SDK Client advertises `capabilities: { elicitation: { form: {} } }` and
9191 registers `client.setRequestHandler(ElicitRequestSchema, ...)` that returns
9292 `{ action: "accept", content: {} }`.
9393 6. Call `execute` with code that (a) calls `tools.openapi.addSource({ spec,
94− namespace })`, (b) invokes the POST operation.
94+namespace })`, (b) invokes the POST operation.
9595
9696 **Tool id format inside the `execute` sandbox.**
9797 `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
3535
3636 // definitions
3737 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>;
3941 readonly removeDefinitionsBySource: (sourceId: string) => Effect.Effect<void, Error>;
4042
4143 // secrets (routing rows; provider-specific data lives in provider)
@@ -45,10 +47,9 @@ export interface CoreStore { · 3 unmodified lines
4547 readonly removeSecret: (id: string) => Effect.Effect<void, Error>;
4648 }
4749
48−export const makeDefaultCoreStore = (deps: {
49− adapter: DBAdapter;
50− scope: Scope;
51−}): CoreStore => { /* typedAdapter<CoreSchema> wiring, one place */ };
50+export const makeDefaultCoreStore = (deps: { adapter: DBAdapter; scope: Scope }): CoreStore => {
51+ /* typedAdapter<CoreSchema> wiring, one place */
52+};
5253 ```
5354
5455 ## Wiring — defaults passed in deps, force the spread
@@ -70,9 +71,7 @@ export interface ExecutorConfig<TPlugins> { · 15 unmodified lines
7071
7172 // Inside createExecutor:
7273 const defaults = makeDefaultCoreStore({ adapter, scope });
73−const core = config.coreStorage
74− ? config.coreStorage({ adapter, scope, defaults })
75− : defaults;
74+const core = config.coreStorage ? config.coreStorage({ adapter, scope, defaults }) : defaults;
7675 // Executor now calls core.putTools(...), core.removeSource(...), etc.
7776 // No more adapter.create({ model: "tool", ... }) scattered through executor.ts.
7877 ```
@@ -83,7 +82,7 @@ const core = config.coreStorage · 4 unmodified lines
8382 with no type help.
8483 - Anyone implementing from scratch (test doubles, a non-drizzle backend)
8584 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_
8786 keeps the full `CoreStore` return type enforced.
8887
8988 ## What it unlocks
@@ -91,30 +90,39 @@ const core = config.coreStorage · 1 unmodified line
9190 **Algolia sidecar — no string-match, just method overrides:**
9291
9392 ```ts
94−const executor = yield* createExecutor({
95− scope, adapter, blobs, plugins,
96−
97− coreStorage: ({ defaults }) => ({
98− ...defaults,
99− putTools: (tools) =>
100− Effect.gen(function* () {
101− yield* defaults.putTools(tools);
102− yield* Effect.promise(() => algolia.saveObjects({
103− indexName: `tools_${scope.id}`,
104− objects: tools.map((t) => ({ objectID: t.id, ...t })),
105− }));
106− }),
107− removeToolsBySource: (sourceId) =>
108− Effect.gen(function* () {
109− const affected = yield* defaults.listTools();
110− yield* defaults.removeToolsBySource(sourceId);
111− yield* Effect.promise(() => algolia.deleteObjects({
112− indexName: `tools_${scope.id}`,
113− objectIDs: affected.filter((t) => t.sourceId === sourceId).map((t) => t.id),
114− }));
115− }),
116− }),
117−});
93+const executor =
94+ yield *
95+ createExecutor({
96+ scope,
97+ adapter,
98+ blobs,
99+ plugins,
100+
101+ coreStorage: ({ defaults }) => ({
102+ ...defaults,
103+ putTools: (tools) =>
104+ Effect.gen(function* () {
105+ yield* defaults.putTools(tools);
106+ yield* Effect.promise(() =>
107+ algolia.saveObjects({
108+ indexName: `tools_${scope.id}`,
109+ objects: tools.map((t) => ({ objectID: t.id, ...t })),
110+ }),
111+ );
112+ }),
113+ removeToolsBySource: (sourceId) =>
114+ Effect.gen(function* () {
115+ const affected = yield* defaults.listTools();
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+ });
118126 ```
119127
120128 Test 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
128136
129137 ```ts
130138 // before
131−storage: (deps) => makeDefaultOpenapiStore(deps)
139+storage: (deps) => makeDefaultOpenapiStore(deps);
132140
133141 // after
134−storage: ({ defaults }) => defaults // no-op, identical to leaving off `storage:`
142+storage: ({ defaults }) => defaults; // no-op, identical to leaving off `storage:`
135143
136144 // override one method
137−storage: ({ defaults }) => ({ ...defaults, upsertSource: myUpsert })
145+storage: ({ defaults }) => ({ ...defaults, upsertSource: myUpsert });
138146 ```
139147
140148 Mechanical sweep across openapi / mcp / graphql / workos-vault plugins.
@@ -152,6 +160,6 @@ Not intellectually hard, but a broad diff: · 11 unmodified lines
152160 factory renamed to follow the `defaults` contract.
153161 - Docs/readme updates showing the new override pattern.
154162
155−Ideally lands *before* pluggable search — the search sidecar story
163+Ideally lands _before_ pluggable search — the search sidecar story
156164 depends on this refactor to avoid the `if (model === "tool")` antipattern
157165 I'd otherwise be stuck with.
notes/old/promise-sdk-typed-errors.md+3−1
@@ -6,7 +6,7 @@ · 5 unmodified lines
66 `Effect.runPromise(...)`. That works but it's lossy at the Promise
77 boundary:
88
9−- The promise's *rejection type* is `unknown` (or `any` after
9+- The promise's _rejection type_ is `unknown` (or `any` after
1010 `try/catch`). Consumers have to `instanceof InternalError` /
1111 `err._tag === "..."` to discriminate, even though the underlying
1212 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
4343 or wrap them in a promise-native `Result<A, E>`?
4444
4545 **Expose `Exit`/`Cause`:**
46+
4647 - Pros: one fewer abstraction, no translation layer to maintain, cause
4748 preserves parallel/sequential composition.
4849 - Cons: consumers depend on `effect`'s API even though they're using a
4950 "Promise" SDK — partly defeats the abstraction.
5051
5152 **Wrap in a `Result<A, E>`:**
53+
5254 - Pros: `@executor-js/sdk/promise` consumers don't import `effect` at all,
5355 surface stays small.
5456 - 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
5454 takes):
5555
5656 ```ts
57−yield* sql`SET LOCAL app.scope_chain = ${toPgArray(chain)}`;
58−yield* sql`SET LOCAL app.write_scope = ${writeScope}`;
57+yield * sql`SET LOCAL app.scope_chain = ${toPgArray(chain)}`;
58+yield * sql`SET LOCAL app.write_scope = ${writeScope}`;
5959 ```
6060
6161 `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
6969 tables is whatever `executor-schema.ts` exports with a `scope_id`
7070 column — same set the scope adapter already knows about.
7171 2. Add an Effect layer that wraps the DbService to issue the two `SET
72− LOCAL`s at connection acquire. `postgres.js`'s `sql.begin` already
72+LOCAL`s at connection acquire. `postgres.js`'s `sql.begin` already
7373 gives us the transaction boundary.
7474 3. Leave `BYPASSRLS` off the worker's role so we actually get the
7575 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
2020 scope stack:
2121
2222 ```ts
23−[user, workspace, org]
23+[user, workspace, org];
2424 ```
2525
2626 Rows still belong to exactly one scope. Resolution walks the stack
@@ -68,12 +68,7 @@ For example: · 41 unmodified lines
6868
6969 ```ts
7070 oauth2: {
71− flow: "clientCredentials",
72− tokenUrl,
73− clientIdSlot,
74− clientSecretSlot,
75− connectionSlot,
76− scopes
71+ flow: ("clientCredentials", tokenUrl, clientIdSlot, clientSecretSlot, connectionSlot, scopes);
7772 }
7873 ```
7974
notes/old/scopes.md+6−6
@@ -10,15 +10,15 @@ scopes by construction. · 9 unmodified lines
1010
1111 - **SDK** — `createExecutor({ scope: Scope, adapter, blobs, plugins })`.
1212 One `Scope` per executor instance. `scopeAdapter(rootAdapter, {...},
13− schema)` wraps the adapter before it reaches plugin storage or the
13+schema)` wraps the adapter before it reaches plugin storage or the
1414 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` stamped
15+= scope.id` ANDed in; every write gets `scope_id = scope.id` stamped
1616 into the payload. Tx handles passed into transaction callbacks are
1717 also wrapped, so nested writes stay inside the same scope.
1818
1919 - **Plugins** — see a plain `DBAdapter`. They do not know or care about
2020 scope. Every plugin schema that wants isolation declares `scope_id:
21− { type: "string", required: true, index: true }`. Forgetting the
21+{ type: "string", required: true, index: true }`. Forgetting the
2222 column means the adapter passes the plugin's reads/writes through
2323 unscoped (documented failure mode, not silent) — tests catch the
2424 concrete cases we care about.
@@ -44,8 +44,8 @@ sources, tools, secrets, and plugin-owned source detail lookups. · 19 unmodified lines
4444
4545 ```ts
4646 interface ScopeContext {
47− readonly read: readonly string[]; // precedence-ordered, innermost first
48− readonly write: string; // exactly one scope for writes
47+ readonly read: readonly string[]; // precedence-ordered, innermost first
48+ readonly write: string; // exactly one scope for writes
4949 }
5050 ```
5151
@@ -91,7 +91,7 @@ Writes land in exactly one scope chosen by the caller. · 39 unmodified lines
9191 there. When a user invokes Gmail, secret resolution walks the scope
9292 stack (user → workspace → org) and only finds a token if that user
9393 personally oauthed. Policy on the source row — `auth_scope_mandate:
94− "user"` — forces this: secret lookup refuses to return
94+"user"` — forces this: secret lookup refuses to return
9595 workspace-level tokens even if one existed.
9696
9797 - **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
99
1010 1. `executor.tools.list()` — `SELECT * FROM tool WHERE scope_id = ?`.
1111 2. In JS, for every tool: normalize + tokenize `id / sourceId / name /
12− description`, score per field with weights, apply coverage + exact-
12+description`, score per field with weights, apply coverage + exact-
1313 phrase boosts, filter, sort, slice.
1414
1515 O(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
7474 ## Override shapes
7575
7676 **Replace entirely:**
77+
7778 ```ts
7879 search: ({ listTools }) => algoliaSearch({ listTools }),
7980 ```
8081
8182 **Decorate — mix in logging/timing:**
83+
8284 ```ts
8385 search: ({ defaults }) => ({
8486 query: (q) => Effect.gen(function* () {
@@ -91,6 +93,7 @@ search: ({ defaults }) => ({ · 6 unmodified lines
9193 ```
9294
9395 **Hybrid — try Algolia, fall back to local:**
96+
9497 ```ts
9598 search: ({ listTools, defaults }) => ({
9699 query: (q) =>
@@ -109,6 +112,7 @@ it's a storage concern. Handled by wrapping the core store (see · 12 unmodified lines
109112 delegate to the default and mirror to the external index.
110113
111114 The two halves are independent:
115+
112116 - Indexing only (manual reindex): override `coreStorage`, leave `search`
113117 as default.
114118 - Search only (some other populates the index): override `search`, leave
@@ -122,7 +126,7 @@ scorer: · 7 unmodified lines
122126 1. **Projection on `tools.list`** — expose a `select` option so search
123127 doesn't pull `input_schema` / `output_schema`. Biggest per-query win.
124128 2. **Postgres FTS** — `tsvector` generated column on `(name, description,
125− id, source_id)`, GIN index, use `websearch_to_tsquery`. Native
129+id, source_id)`, GIN index, use `websearch_to_tsquery`. Native
126130 ranking, fuzzy via `pg_trgm`. Would replace the in-memory scorer for
127131 the cloud app.
128132 3. **Per-scope LRU** — cache scored results; invalidate on source
notes/old/storage-migration.md+23−15
@@ -3,11 +3,13 @@ · 2 unmodified lines
33 ## What was done
44
55 ### 1. DBSchema alignment with better-auth
6+
67 - Vendored better-auth's `BetterAuthPluginDBSchema` pattern into `storage-core/src/schema.ts`
78 - Made `modelName` optional (falls back to the key), renamed `disableMigrations` → `disableMigration`, dropped `order`
89 - Removed explicit `modelName` from all plugin schemas (core-schema, openapi, mcp, graphql, google-discovery, workos-vault) — keys already match table names
910
1011 ### 2. @executor-js/cli package (packages/core/cli/)
12+
1113 - Mirrors better-auth's CLI approach: load config → collect plugin schemas → generate drizzle TS
1214 - `executor generate --config ./executor.config.ts --output ./src/services/executor-schema.ts`
1315 - Generator ported from better-auth's drizzle generator, adapted for our DBSchema
@@ -15,11 +17,13 @@ · 1 unmodified line
1517 - Uses jiti for config loading (supports TS configs)
1618
1719 ### 3. apps/cloud wired up
20+
1821 - `executor.config.ts` defines plugins with stub credentials (only used for schema shape)
1922 - `drizzle.config.ts` points at both `schema.ts` (cloud tables) and `executor-schema.ts` (executor tables)
2023 - Fresh `drizzle/0000_initial.sql` with all tables (cloud + core + plugin + blob), indexes, FKs
2124
2225 ### 4. apps/local wired up
26+
2327 - `executor.config.ts` defines plugins + dialect sqlite
2428 - `drizzle.config.ts` for drizzle-kit generate/push
2529 - Generated `executor-schema.ts` with all sqlite tables + blob table
@@ -27,6 +31,7 @@ · 1 unmodified line
2731 - At startup: `migrate(db, { migrationsFolder })` from `drizzle-orm/bun-sqlite/migrator` applies pending migrations before constructing the adapter
2832
2933 ### 5. Adapter DDL removed
34+
3035 - `makeSqliteAdapter` and `makePostgresAdapter` no longer run DDL — they return `DBAdapter` directly (not `Effect.Effect<DBAdapter>`)
3136 - `makeSqliteBlobStore` and `makePostgresBlobStore` no longer run DDL — they return `BlobStore` directly
3237 - `buildCreateTableStatements` deleted from `storage-file/compile.ts`
@@ -39,6 +44,7 @@ · 6 unmodified lines
3944 The `@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.
4045
4146 ### Local app migration
47+
4248 1. On first run, `migrate()` creates all tables from the initial migration
4349 2. Add a **sync-from-config step** after executor creation:
4450 - `loadConfig(configPath)` reads `executor.jsonc`
@@ -47,28 +53,30 @@ The `@executor-js/config` package already maintains `executor.jsonc` as a source · 2 unmodified lines
4753 3. 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.
4854
4955 ### What won't survive (acceptable losses)
56+
5057 - Tool invocation history / blobs (old KV format)
5158 - OAuth sessions (ephemeral, users re-auth)
5259 - Stale source state (re-fetching is actually a benefit)
5360
5461 ### Cloud migration
62+
5563 - Only ~2 users, just drop old tables and re-add manually
5664 - Or write a one-off SQL migration
5765
5866 ## Key files
5967
60−| File | Role |
61−|------|------|
62−| `packages/core/cli/src/commands/generate.ts` | CLI generate command |
63−| `packages/core/cli/src/generators/drizzle.ts` | Drizzle schema generator |
64−| `packages/core/sdk/src/config.ts` | `defineExecutorConfig` helper |
65−| `packages/core/config/src/schema.ts` | `executor.jsonc` schema |
66−| `packages/core/config/src/config-store.ts` | Plugin store → config file decorator |
67−| `packages/core/config/src/load.ts` | Config loading |
68−| `apps/cloud/executor.config.ts` | Cloud config for CLI |
69−| `apps/cloud/src/services/executor-schema.ts` | Generated drizzle schema |
70−| `apps/cloud/drizzle/0000_*.sql` | Fresh migration |
71−| `apps/local/executor.config.ts` | Local config for CLI |
72−| `apps/local/src/server/executor-schema.ts` | Generated drizzle schema |
73−| `apps/local/drizzle/0000_*.sql` | Fresh migration |
74−| `apps/local/src/server/executor.ts` | Local executor bootstrap with `migrate()` |
68+| File | Role |
69+| --------------------------------------------- | ----------------------------------------- |
70+| `packages/core/cli/src/commands/generate.ts` | CLI generate command |
71+| `packages/core/cli/src/generators/drizzle.ts` | Drizzle schema generator |
72+| `packages/core/sdk/src/config.ts` | `defineExecutorConfig` helper |
73+| `packages/core/config/src/schema.ts` | `executor.jsonc` schema |
74+| `packages/core/config/src/config-store.ts` | Plugin store → config file decorator |
75+| `packages/core/config/src/load.ts` | Config loading |
76+| `apps/cloud/executor.config.ts` | Cloud config for CLI |
77+| `apps/cloud/src/services/executor-schema.ts` | Generated drizzle schema |
78+| `apps/cloud/drizzle/0000_*.sql` | Fresh migration |
79+| `apps/local/executor.config.ts` | Local config for CLI |
80+| `apps/local/src/server/executor-schema.ts` | Generated drizzle schema |
81+| `apps/local/drizzle/0000_*.sql` | Fresh migration |
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
7272
7373 ```ts
7474 type PluginManifest = {
75− schemaVersion: string
76− id: string
77− name: string
78− version: string
79− description?: string
80− publisher?: PluginPublisher
81− license?: string
82− homepage?: string
83− repository?: string
84− categories?: PluginCategory[]
85− compatibility: PluginCompatibility
86− entrypoints: PluginEntrypoints
87− capabilities?: PluginCapabilities
88− extensions?: PluginExtensions
89− storage?: PluginStorageDeclaration
90− package?: PluginPackageDeclaration
91− metadata?: Record<string, unknown>
92−}
75+ schemaVersion: string;
76+ id: string;
77+ name: string;
78+ version: string;
79+ description?: string;
80+ publisher?: PluginPublisher;
81+ license?: string;
82+ homepage?: string;
83+ repository?: string;
84+ categories?: PluginCategory[];
85+ compatibility: PluginCompatibility;
86+ entrypoints: PluginEntrypoints;
87+ capabilities?: PluginCapabilities;
88+ extensions?: PluginExtensions;
89+ storage?: PluginStorageDeclaration;
90+ package?: PluginPackageDeclaration;
91+ metadata?: Record<string, unknown>;
92+};
9393 ```
9494
9595 This 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
135135 User-facing install config can stay simple:
136136
137137 ```ts
138−type PluginInstallSpec =
139− | string
140− | [string, Record<string, unknown>]
138+type PluginInstallSpec = string | [string, Record<string, unknown>];
141139 ```
142140
143141 The 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
146144
147145 ```ts
148146 type PluginPackageDeclaration = {
149− source?: "npm" | "git" | "github" | "local" | "registry"
150− packageName?: string
151− repository?: string
152−}
147+ source?: "npm" | "git" | "github" | "local" | "registry";
148+ packageName?: string;
149+ repository?: string;
150+};
153151 ```
154152
155153 This `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
160158
161159 ```ts
162160 type PluginCompatibility = {
163− executor: string
164− runtimeApi?: string
165− platforms?: PluginPlatform[]
166−}
161+ executor: string;
162+ runtimeApi?: string;
163+ platforms?: PluginPlatform[];
164+};
167165
168−type PluginPlatform = "local" | "cloud"
166+type PluginPlatform = "local" | "cloud";
169167 ```
170168
171169 `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
182180
183181 ```ts
184182 type PluginEntrypoints = {
185− main?: PluginCodeEntrypoint
186− sdk?: PluginCodeEntrypoint
187− source?: PluginCodeEntrypoint
188− secretStore?: PluginCodeEntrypoint
189− apiGroup?: PluginCodeEntrypoint
190− apiHandlers?: PluginCodeEntrypoint
191− routeHandlers?: Record<string, PluginCodeEntrypoint>
192− backgroundServices?: Record<string, PluginCodeEntrypoint>
193− reactSource?: PluginUiEntrypoint
194− reactSecretProvider?: PluginUiEntrypoint
195− ui?: PluginUiEntrypoint
196− adminUi?: PluginUiEntrypoint
197−}
183+ main?: PluginCodeEntrypoint;
184+ sdk?: PluginCodeEntrypoint;
185+ source?: PluginCodeEntrypoint;
186+ secretStore?: PluginCodeEntrypoint;
187+ apiGroup?: PluginCodeEntrypoint;
188+ apiHandlers?: PluginCodeEntrypoint;
189+ routeHandlers?: Record<string, PluginCodeEntrypoint>;
190+ backgroundServices?: Record<string, PluginCodeEntrypoint>;
191+ reactSource?: PluginUiEntrypoint;
192+ reactSecretProvider?: PluginUiEntrypoint;
193+ ui?: PluginUiEntrypoint;
194+ adminUi?: PluginUiEntrypoint;
195+};
198196
199197 type PluginCodeEntrypoint = {
200− module: string
201− export?: string
202− runtime?: PluginRuntimeTarget
203−}
198+ module: string;
199+ export?: string;
200+ runtime?: PluginRuntimeTarget;
201+};
204202
205−type PluginRuntimeTarget = "worker" | "isolate" | "host-trusted"
203+type PluginRuntimeTarget = "worker" | "isolate" | "host-trusted";
206204 ```
207205
208206 The 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
237235 ```ts
238236 type PluginLayerDeclaration = {
239237 sdk?: {
240− pluginId: string
241− entrypoint: keyof PluginEntrypoints
242− }
238+ pluginId: string;
239+ entrypoint: keyof PluginEntrypoints;
240+ };
243241 api?: {
244− requiresSdkPlugin: string
245− groupEntrypoint?: keyof PluginEntrypoints
246− handlersEntrypoint?: keyof PluginEntrypoints
247− }
242+ requiresSdkPlugin: string;
243+ groupEntrypoint?: keyof PluginEntrypoints;
244+ handlersEntrypoint?: keyof PluginEntrypoints;
245+ };
248246 react?: Array<{
249− kind: "source" | "secret-provider" | "settings" | "admin"
250− requiresSdkPlugin?: string
251− requiresApi?: boolean
252− entrypoint: keyof PluginEntrypoints
253− }>
254−}
247+ kind: "source" | "secret-provider" | "settings" | "admin";
248+ requiresSdkPlugin?: string;
249+ requiresApi?: boolean;
250+ entrypoint: keyof PluginEntrypoints;
251+ }>;
252+};
255253 ```
256254
257255 This 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
264262
265263 ```ts
266264 type PluginExtensions = {
267− sources?: SourceExtensionDeclaration[]
268− secretStores?: SecretStoreExtensionDeclaration[]
269− tools?: ToolExtensionDeclaration[]
270− routes?: RouteExtensionDeclaration[]
271− uiSurfaces?: UiSurfaceDeclaration[]
272− backgroundServices?: BackgroundServiceDeclaration[]
273−}
265+ sources?: SourceExtensionDeclaration[];
266+ secretStores?: SecretStoreExtensionDeclaration[];
267+ tools?: ToolExtensionDeclaration[];
268+ routes?: RouteExtensionDeclaration[];
269+ uiSurfaces?: UiSurfaceDeclaration[];
270+ backgroundServices?: BackgroundServiceDeclaration[];
271+};
274272 ```
275273
276274 Some 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
283281
284282 ```ts
285283 type PluginCapabilities = {
286− tools?: ToolCapabilityDeclaration[]
287− sources?: SourceCapabilityDeclaration[]
288− secrets?: SecretCapabilityDeclaration[]
289− network?: NetworkCapabilityDeclaration[]
290− storage?: StorageCapabilityDeclaration[]
291− scopes?: ScopeCapabilityDeclaration[]
292− filesystem?: FilesystemCapabilityDeclaration[]
293− execution?: ExecutionCapabilityDeclaration[]
294− host?: HostCapabilityDeclaration[]
295−}
284+ tools?: ToolCapabilityDeclaration[];
285+ sources?: SourceCapabilityDeclaration[];
286+ secrets?: SecretCapabilityDeclaration[];
287+ network?: NetworkCapabilityDeclaration[];
288+ storage?: StorageCapabilityDeclaration[];
289+ scopes?: ScopeCapabilityDeclaration[];
290+ filesystem?: FilesystemCapabilityDeclaration[];
291+ execution?: ExecutionCapabilityDeclaration[];
292+ host?: HostCapabilityDeclaration[];
293+};
296294 ```
297295
298296 Capabilities 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
344342 ```ts
345343 type PluginStorageDeclaration = {
346344 collections?: Array<{
347− name: string
348− kind: "kv" | "document" | "sql"
349− description?: string
350− }>
351−}
345+ name: string;
346+ kind: "kv" | "document" | "sql";
347+ description?: string;
348+ }>;
349+};
352350 ```
353351
354352 Storage 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
364362 ```ts
365363 type PluginArtifacts = {
366364 files?: Array<{
367− path: string
368− size?: number
369− sha256?: string
370− kind?: "code" | "asset" | "manifest" | "source-map"
371− }>
365+ path: string;
366+ size?: number;
367+ sha256?: string;
368+ kind?: "code" | "asset" | "manifest" | "source-map";
369+ }>;
372370 bundle?: {
373− format: "esm"
374− main: string
375− size?: number
376− sha256?: string
377− }
378−}
371+ format: "esm";
372+ main: string;
373+ size?: number;
374+ sha256?: string;
375+ };
376+};
379377 ```
380378
381379 Cloud 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