History

Consolidate custom tools plugin wiring

Rhys Sullivan committed 4 months ago

Commit 3dba8101 parent aeb483318 files · +497 −310Browse files

Unified · Split

18 files
  1. apps/host-selfhost/executor.config.ts +3 −2
  2. apps/host-selfhost/src/app.ts +2 −4
  3. apps/host-selfhost/src/apps-backings.ts +52 −0
  4. apps/host-selfhost/src/apps-route.ts +0 −166
  5. apps/host-selfhost/src/apps.ts +0 −71
  6. apps/host-selfhost/src/testing/test-app.ts +2 −4
  7. bun.lock +3 −0
  8. packages/plugins/apps/package.json +5 −0
  9. packages/plugins/apps/src/api.ts +22 −2
  10. packages/plugins/apps/src/index.ts +2 −0
  11. packages/plugins/apps/src/plugin/apps-plugin.test.ts +1 −1
  12. packages/plugins/apps/src/plugin/apps-plugin.ts +214 −50
  13. packages/plugins/apps/src/plugin/backings.ts +28 −0
  14. packages/plugins/apps/src/plugin/handlers.ts +49 −0
  15. packages/plugins/apps/src/plugin/resolver.ts +2 −6
  16. packages/plugins/apps/src/plugin/routes.ts +104 −0
  17. packages/plugins/apps/src/plugin/self-host-runtime.ts +7 −3
  18. packages/plugins/apps/src/react/custom-tools-client.web.test.ts +1 −1
apps/host-selfhost/executor.config.ts+3−2
@@ -6,9 +6,10 @@ import { mcpHttpPlugin } from "@executor-js/plugin-mcp/api"; · 5 unmodified lines
6import { graphqlHttpPlugin } from "@executor-js/plugin-graphql/api";6import { graphqlHttpPlugin } from "@executor-js/plugin-graphql/api";
7import { encryptedSecretsPlugin } from "@executor-js/plugin-encrypted-secrets";7import { encryptedSecretsPlugin } from "@executor-js/plugin-encrypted-secrets";
8import { toolkitsPlugin } from "@executor-js/plugin-toolkits/server";8import { toolkitsPlugin } from "@executor-js/plugin-toolkits/server";
9import { appsPlugin } from "@executor-js/plugin-apps/api";
9 10
10import { resolveSecretKey } from "./src/config";11import { resolveSecretKey } from "./src/config";
11import { getSelfHostAppsSubsystem } from "./src/apps";12import { getSelfHostAppsBackings } from "./src/apps-backings";
12 13
13// ---------------------------------------------------------------------------14// ---------------------------------------------------------------------------
14// Single source of truth for the self-hosted app's plugin list.15// Single source of truth for the self-hosted app's plugin list.
@@ -37,7 +38,7 @@ export default defineExecutorConfig({ · 22 unmodified lines
37 mcpHttpPlugin({ dangerouslyAllowStdioMCP: false }),38 mcpHttpPlugin({ dangerouslyAllowStdioMCP: false }),
38 graphqlHttpPlugin(),39 graphqlHttpPlugin(),
39 toolkitsPlugin({ activeToolkitSlug }),40 toolkitsPlugin({ activeToolkitSlug }),
40 getSelfHostAppsSubsystem().plugin,41 appsPlugin({ backings: getSelfHostAppsBackings() }),
41 // First writable secret provider -> the default for `secrets.set`.42 // First writable secret provider -> the default for `secrets.set`.
42 encryptedSecretsPlugin({ key: resolveSecretKey() }),43 encryptedSecretsPlugin({ key: resolveSecretKey() }),
43 ] as const,44 ] as const,
apps/host-selfhost/src/app.ts+2−4
@@ -22,8 +22,7 @@ import { makeSelfHostMcpSeams } from "./mcp"; · 21 unmodified lines
22import { selfHostPlugins } from "./plugins";22import { selfHostPlugins } from "./plugins";
23import { ErrorCaptureLive } from "./observability";23import { ErrorCaptureLive } from "./observability";
24import { oauthCallbackSignInRedirectLocation } from "./auth/oauth-callback-login";24import { oauthCallbackSignInRedirectLocation } from "./auth/oauth-callback-login";
25import { closeSelfHostAppsSubsystem } from "./apps";25import { closeSelfHostAppsBackings } from "./apps-backings";
26import { makeSelfHostAppsSyncRoute } from "./apps-route";
27 26
28// ===========================================================================27// ===========================================================================
29// The self-hosted Executor app, as ONE `ExecutorApp.make` call.28// The self-hosted Executor app, as ONE `ExecutorApp.make` call.
@@ -121,7 +120,6 @@ export const makeSelfHostApp = async (options: MakeSelfHostAppOptions = {}) => { · 91 unmodified lines
121 makeSelfHostAdminApiLayer({ betterAuth, db: dbHandle, mountPrefix: "/api" }),120 makeSelfHostAdminApiLayer({ betterAuth, db: dbHandle, mountPrefix: "/api" }),
122 // Public system API: /api/health + /api/setup-status (unauthenticated).121 // Public system API: /api/health + /api/setup-status (unauthenticated).
123 makeSelfHostSystemApiLayer({ betterAuth, db: dbHandle, mountPrefix: "/api" }),122 makeSelfHostSystemApiLayer({ betterAuth, db: dbHandle, mountPrefix: "/api" }),
124 makeSelfHostAppsSyncRoute({ identity: identityLayer, db: dbHandle }),
125 // Swagger UI at /docs, over the /api-prefixed spec (matches the served paths).123 // Swagger UI at /docs, over the /api-prefixed spec (matches the served paths).
126 HttpApiSwagger.layer(composePluginApi(selfHostPlugins).prefix("/api"), { path: "/docs" }),124 HttpApiSwagger.layer(composePluginApi(selfHostPlugins).prefix("/api"), { path: "/docs" }),
127 ],125 ],
@@ -142,7 +140,7 @@ export const makeSelfHostApp = async (options: MakeSelfHostAppOptions = {}) => { · 14 unmodified lines
142 toWebHandler,140 toWebHandler,
143 betterAuth,141 betterAuth,
144 closeDb: async () => {142 closeDb: async () => {
145 await closeSelfHostAppsSubsystem();143 await closeSelfHostAppsBackings();
146 await mcp.close();144 await mcp.close();
147 await dbHandle.close();145 await dbHandle.close();
148 },146 },
apps/host-selfhost/src/apps-backings.ts+52−0
@@ -0,0 +1,52 @@
1import { join } from "node:path";
2
3import { Effect } from "effect";
4
5import {
6 makeGitArtifactStore,
7 makeLibsqlScopeDb,
8 makeQuickjsToolSandbox,
9 makeSqliteAppsStore,
10 type AppsBackings,
11} from "@executor-js/plugin-apps/api";
12
13import { resolveDataDir } from "./config";
14
15interface SelfHostAppsBackings {
16 readonly backings: AppsBackings;
17 readonly close: () => Promise<void>;
18}
19
20const createBackings = (dataDir: string): SelfHostAppsBackings => {
21 const appsDir = join(dataDir, "apps");
22 const scopeDb = makeLibsqlScopeDb({
23 root: join(appsDir, "scope-db"),
24 });
25 return {
26 backings: {
27 artifactStore: makeGitArtifactStore({ root: join(appsDir, "artifacts") }),
28 scopeDb,
29 sandbox: makeQuickjsToolSandbox(),
30 store: makeSqliteAppsStore({ path: join(appsDir, "store.sqlite") }),
31 },
32 close: async () => {
33 await Effect.runPromise(scopeDb.close().pipe(Effect.orElseSucceed(() => undefined)));
34 },
35 };
36};
37
38let current: { readonly dataDir: string; readonly value: SelfHostAppsBackings } | undefined;
39
40export const getSelfHostAppsBackings = (): AppsBackings => {
41 const dataDir = resolveDataDir();
42 if (current && current.dataDir === dataDir) return current.value.backings;
43 const value = createBackings(dataDir);
44 current = { dataDir, value };
45 return value.backings;
46};
47
48export const closeSelfHostAppsBackings = async (): Promise<void> => {
49 const value = current?.value;
50 current = undefined;
51 await value?.close();
52};
apps/host-selfhost/src/apps-route.ts+0−166
@@ -1,166 +0,0 @@
1import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http";
2import { Data, Effect, Layer, Predicate } from "effect";
3
4import {
5 IdentityProvider,
6 RequestOrgSlug,
7 RequestWebOrigin,
8 makeScopedExecutor,
9 type Principal,
10} from "@executor-js/api/server";
11import { IntegrationSlug, ToolAddress } from "@executor-js/sdk";
12
13import { SelfHostScopedExecutorSeams } from "./execution";
14import { SelfHostDb, type SelfHostDbHandle } from "./db/self-host-db";
15import type { SelfHostPlugins } from "./plugins";
16
17class AppsSyncRouteError extends Data.TaggedError("AppsSyncRouteError")<{
18 readonly status: number;
19 readonly message: string;
20}> {}
21
22const isAppsSyncRouteError = (failure: unknown): failure is AppsSyncRouteError =>
23 Predicate.isTagged("AppsSyncRouteError")(failure);
24
25const parseJson = (request: Request): Effect.Effect<unknown, AppsSyncRouteError> =>
26 Effect.tryPromise({
27 try: () => request.json() as Promise<unknown>,
28 catch: () =>
29 new AppsSyncRouteError({
30 status: 400,
31 message: "Invalid JSON body",
32 }),
33 });
34
35const buildExecutor = (principal: Principal, request: Request) => {
36 const withOrigin = makeScopedExecutor<SelfHostPlugins>(
37 principal.accountId,
38 principal.organizationId,
39 principal.organizationName,
40 ).pipe(Effect.provideService(RequestWebOrigin, { origin: new URL(request.url).origin }));
41 return principal.organizationSlug
42 ? withOrigin.pipe(Effect.provideService(RequestOrgSlug, { slug: principal.organizationSlug }))
43 : withOrigin;
44};
45
46const sourceSlugFromRoute = () =>
47 HttpRouter.params.pipe(
48 Effect.map((params) => params.slug ?? ""),
49 Effect.flatMap((slug) =>
50 slug.length > 0
51 ? Effect.succeed(slug)
52 : Effect.fail(
53 new AppsSyncRouteError({
54 status: 400,
55 message: "Missing custom tools source slug",
56 }),
57 ),
58 ),
59 );
60
61export interface SelfHostAppsSyncRouteDeps {
62 readonly identity: Layer.Layer<IdentityProvider>;
63 readonly db: SelfHostDbHandle;
64}
65
66const routeHandler = (
67 run: (request: Request) => Effect.Effect<unknown, unknown, unknown>,
68): Effect.Effect<HttpServerResponse.HttpServerResponse, never, unknown> =>
69 Effect.gen(function* () {
70 const httpRequest = yield* HttpServerRequest.HttpServerRequest;
71 const request = yield* HttpServerRequest.toWeb(httpRequest).pipe(
72 Effect.catch(() => Effect.succeed(null)),
73 );
74 if (request === null) {
75 return HttpServerResponse.text("Internal Server Error", { status: 500 });
76 }
77 return yield* run(request).pipe(
78 Effect.map((body) => HttpServerResponse.jsonUnsafe(body)),
79 Effect.catch((failure: unknown) => {
80 if (Predicate.isTagged("Unauthorized")(failure)) {
81 return Effect.succeed(HttpServerResponse.text("Unauthorized", { status: 401 }));
82 }
83 if (Predicate.isTagged("NoOrganization")(failure)) {
84 return Effect.succeed(HttpServerResponse.text("Forbidden", { status: 403 }));
85 }
86 if (Predicate.isTagged("Unavailable")(failure)) {
87 return Effect.succeed(HttpServerResponse.text("Unavailable", { status: 503 }));
88 }
89 if (isAppsSyncRouteError(failure)) {
90 const message = failure.message;
91 const status = failure.status;
92 return Effect.succeed(HttpServerResponse.jsonUnsafe({ error: message }, { status }));
93 }
94 return Effect.succeed(HttpServerResponse.text("Internal Server Error", { status: 500 }));
95 }),
96 Effect.catchCause(() =>
97 Effect.succeed(HttpServerResponse.text("Internal Server Error", { status: 500 })),
98 ),
99 );
100 });
101
102export const makeSelfHostAppsSyncRoute = ({ identity, db }: SelfHostAppsSyncRouteDeps) => {
103 const services = Layer.merge(
104 SelfHostScopedExecutorSeams.pipe(Layer.provide(Layer.succeed(SelfHostDb)(db))),
105 identity,
106 );
107 return Layer.mergeAll(
108 HttpRouter.add(
109 "GET",
110 "/api/apps/sources/github",
111 routeHandler((request) =>
112 Effect.gen(function* () {
113 const identityProvider = yield* IdentityProvider;
114 const principal = yield* identityProvider.authenticate(request);
115 const executor = yield* buildExecutor(principal, request);
116 return yield* executor.execute(ToolAddress.make("executor.apps.list_github_sources"), {});
117 }),
118 ),
119 ),
120 HttpRouter.add(
121 "POST",
122 "/api/apps/sources/github/sync",
123 routeHandler((request) =>
124 Effect.gen(function* () {
125 const identityProvider = yield* IdentityProvider;
126 const principal = yield* identityProvider.authenticate(request);
127 const payload = yield* parseJson(request);
128 const executor = yield* buildExecutor(principal, request);
129 return yield* executor.execute(
130 ToolAddress.make("executor.apps.sync_github_source"),
131 payload,
132 );
133 }),
134 ),
135 ),
136 HttpRouter.add(
137 "GET",
138 "/api/apps/sources/github/:slug",
139 routeHandler((request) =>
140 Effect.gen(function* () {
141 const identityProvider = yield* IdentityProvider;
142 const principal = yield* identityProvider.authenticate(request);
143 const slug = yield* sourceSlugFromRoute();
144 const executor = yield* buildExecutor(principal, request);
145 return yield* executor.execute(ToolAddress.make("executor.apps.get_github_source"), {
146 slug,
147 });
148 }),
149 ),
150 ),
151 HttpRouter.add(
152 "DELETE",
153 "/api/apps/sources/github/:slug",
154 routeHandler((request) =>
155 Effect.gen(function* () {
156 const identityProvider = yield* IdentityProvider;
157 const principal = yield* identityProvider.authenticate(request);
158 const slug = yield* sourceSlugFromRoute();
159 const executor = yield* buildExecutor(principal, request);
160 yield* executor.integrations.remove(IntegrationSlug.make(slug));
161 return { removed: true };
162 }),
163 ),
164 ),
165 ).pipe(HttpRouter.provideRequest(services));
166};
apps/host-selfhost/src/apps.ts+0−71
@@ -1,71 +0,0 @@
1import { join } from "node:path";
2
3import { Effect } from "effect";
4
5import {
6 appsPlugin,
7 BindingError,
8 makeSelfHostAppsRuntime,
9 makeSqliteAppsStore,
10 type AppsRuntime,
11 type ClientResolver,
12} from "@executor-js/plugin-apps/api";
13import type { PluginCtx } from "@executor-js/sdk";
14
15import { resolveDataDir } from "./config";
16import { makeSelfHostAppsResolver } from "./apps-resolver";
17
18const missingResolver = (): ClientResolver => ({
19 listConnections: () => Effect.succeed([]),
20 resolveConnection: () => Effect.succeed(null),
21 call: ({ integration, connection }) =>
22 Effect.fail(
23 new BindingError({
24 role: integration,
25 integration,
26 requestedConnection: connection,
27 message: "apps resolver is unavailable outside a request-scoped executor",
28 }),
29 ),
30});
31
32const createSubsystem = (dataDir: string) => {
33 const appsDir = join(dataDir, "apps");
34 const store = makeSqliteAppsStore({ path: join(appsDir, "store.sqlite") });
35 const host = makeSelfHostAppsRuntime({
36 dataDir: appsDir,
37 store,
38 resolver: missingResolver(),
39 });
40 const plugin = appsPlugin({
41 runtime: host.runtime,
42 makeResolver: ({ ctx }) => makeSelfHostAppsResolver({ ctx: ctx as PluginCtx }),
43 });
44 return {
45 runtime: host.runtime,
46 plugin,
47 close: host.close,
48 };
49};
50
51export interface SelfHostAppsSubsystem {
52 readonly runtime: AppsRuntime;
53 readonly plugin: ReturnType<typeof appsPlugin>;
54 readonly close: () => Promise<void>;
55}
56
57let current: { readonly dataDir: string; readonly subsystem: SelfHostAppsSubsystem } | undefined;
58
59export const getSelfHostAppsSubsystem = (): SelfHostAppsSubsystem => {
60 const dataDir = resolveDataDir();
61 if (current && current.dataDir === dataDir) return current.subsystem;
62 const subsystem = createSubsystem(dataDir);
63 current = { dataDir, subsystem };
64 return subsystem;
65};
66
67export const closeSelfHostAppsSubsystem = async (): Promise<void> => {
68 const subsystem = current?.subsystem;
69 current = undefined;
70 await subsystem?.close();
71};
apps/host-selfhost/src/testing/test-app.ts+2−4
@@ -30,8 +30,7 @@ import { · 29 unmodified lines
30} from "../mcp/session-store";30} from "../mcp/session-store";
31import { selfHostPlugins } from "../plugins";31import { selfHostPlugins } from "../plugins";
32import { ErrorCaptureLive } from "../observability";32import { ErrorCaptureLive } from "../observability";
33import { closeSelfHostAppsSubsystem } from "../apps";33import { closeSelfHostAppsBackings } from "../apps-backings";
34import { makeSelfHostAppsSyncRoute } from "../apps-route";
35 34
36// ===========================================================================35// ===========================================================================
37// Self-host TEST harness — the throwaway composition tests use to exercise the36// Self-host TEST harness — the throwaway composition tests use to exercise the
@@ -204,7 +203,6 @@ export const makeSelfHostTestApp = async ( · 166 unmodified lines
204 },203 },
205 extensions: {204 extensions: {
206 routes: [205 routes: [
207 makeSelfHostAppsSyncRoute({ identity: options.identity, db: dbHandle }),
208 HttpApiSwagger.layer(composePluginApi(selfHostPlugins).prefix("/api"), { path: "/docs" }),206 HttpApiSwagger.layer(composePluginApi(selfHostPlugins).prefix("/api"), { path: "/docs" }),
209 ],207 ],
210 },208 },
@@ -220,7 +218,7 @@ export const makeSelfHostTestApp = async ( · 9 unmodified lines
220 handler: web.handler,218 handler: web.handler,
221 dispose: async () => {219 dispose: async () => {
222 await web.dispose();220 await web.dispose();
223 await closeSelfHostAppsSubsystem();221 await closeSelfHostAppsBackings();
224 await sessionStore.close();222 await sessionStore.close();
225 await dbHandle.close();223 await dbHandle.close();
226 },224 },
bun.lock+3−0
@@ -797,6 +797,7 @@ · 796 unmodified lines
797 "devDependencies": {797 "devDependencies": {
798 "@effect/atom-react": "catalog:",798 "@effect/atom-react": "catalog:",
799 "@effect/vitest": "catalog:",799 "@effect/vitest": "catalog:",
800 "@executor-js/api": "workspace:*",
800 "@executor-js/react": "workspace:*",801 "@executor-js/react": "workspace:*",
801 "@types/node": "catalog:",802 "@types/node": "catalog:",
802 "@types/react": "catalog:",803 "@types/react": "catalog:",
@@ -808,12 +809,14 @@ · 5 unmodified lines
808 },809 },
809 "peerDependencies": {810 "peerDependencies": {
810 "@effect/atom-react": "catalog:",811 "@effect/atom-react": "catalog:",
812 "@executor-js/api": "workspace:*",
811 "@executor-js/react": "workspace:*",813 "@executor-js/react": "workspace:*",
812 "effect": "catalog:",814 "effect": "catalog:",
813 "react": "catalog:",815 "react": "catalog:",
814 },816 },
815 "optionalPeers": [817 "optionalPeers": [
816 "@effect/atom-react",818 "@effect/atom-react",
819 "@executor-js/api",
817 "@executor-js/react",820 "@executor-js/react",
818 "react",821 "react",
819 ],822 ],
packages/plugins/apps/package.json+5−0
@@ -40,6 +40,7 @@ · 39 unmodified lines
40 "devDependencies": {40 "devDependencies": {
41 "@effect/atom-react": "catalog:",41 "@effect/atom-react": "catalog:",
42 "@effect/vitest": "catalog:",42 "@effect/vitest": "catalog:",
43 "@executor-js/api": "workspace:*",
43 "@executor-js/react": "workspace:*",44 "@executor-js/react": "workspace:*",
44 "@types/node": "catalog:",45 "@types/node": "catalog:",
45 "@types/react": "catalog:",46 "@types/react": "catalog:",
@@ -51,6 +52,7 @@ · 5 unmodified lines
51 },52 },
52 "peerDependencies": {53 "peerDependencies": {
53 "@effect/atom-react": "catalog:",54 "@effect/atom-react": "catalog:",
55 "@executor-js/api": "workspace:*",
54 "@executor-js/react": "workspace:*",56 "@executor-js/react": "workspace:*",
55 "effect": "catalog:",57 "effect": "catalog:",
56 "react": "catalog:"58 "react": "catalog:"
@@ -59,6 +61,9 @@ · 2 unmodified lines
59 "@effect/atom-react": {61 "@effect/atom-react": {
60 "optional": true62 "optional": true
61 },63 },
64 "@executor-js/api": {
65 "optional": true
66 },
62 "@executor-js/react": {67 "@executor-js/react": {
63 "optional": true68 "optional": true
64 },69 },
packages/plugins/apps/src/api.ts+22−2
@@ -1,16 +1,35 @@
1// Host-facing plugin surface for @executor-js/plugin-apps.1// Host-facing plugin surface for @executor-js/plugin-apps.
2export {2import { definePlugin } from "@executor-js/sdk/core";
3 appsPlugin,3
4import {
5 appsPlugin as appsCorePlugin,
4 APPS_INTEGRATION_SLUG,6 APPS_INTEGRATION_SLUG,
5 APPS_PLUGIN_ID,7 APPS_PLUGIN_ID,
6 type AppsPluginOptions,8 type AppsPluginOptions,
7} from "./plugin/apps-plugin";9} from "./plugin/apps-plugin";
10import { AppsGroup } from "./plugin/routes";
11import { AppsHandlers, AppsExtensionService } from "./plugin/handlers";
12
13export const appsPlugin = definePlugin((options?: AppsPluginOptions) => ({
14 ...appsCorePlugin(options),
15 routes: () => AppsGroup,
16 handlers: () => AppsHandlers,
17 extensionService: AppsExtensionService,
18}));
19
20export { APPS_INTEGRATION_SLUG, APPS_PLUGIN_ID, type AppsPluginOptions };
21export { AppsGroup } from "./plugin/routes";
22export { AppsHandlers, AppsExtensionService } from "./plugin/handlers";
8export {23export {
9 makeSelfHostAppsRuntime,24 makeSelfHostAppsRuntime,
10 type SelfHostAppsRuntime,25 type SelfHostAppsRuntime,
11 type SelfHostAppsRuntimeOptions,26 type SelfHostAppsRuntimeOptions,
12} from "./plugin/self-host-runtime";27} from "./plugin/self-host-runtime";
13export { type AppsRuntime, type GitHubCustomToolsSourceSummary } from "./plugin/runtime";28export { type AppsRuntime, type GitHubCustomToolsSourceSummary } from "./plugin/runtime";
29export { makeAppsRuntimeFromBackings, type AppsBackings } from "./plugin/backings";
30export { makeGitArtifactStore } from "./backing/git-artifact-store";
31export { makeLibsqlScopeDb } from "./backing/libsql-scope-db";
32export { makeQuickjsToolSandbox } from "./backing/quickjs-tool-sandbox";
14export { makeSqliteAppsStore } from "./backing/sqlite-apps-store";33export { makeSqliteAppsStore } from "./backing/sqlite-apps-store";
15export {34export {
16 BindingError,35 BindingError,
@@ -18,6 +37,7 @@ export { · 1 unmodified line
18 type ConnectionCandidate,37 type ConnectionCandidate,
19 type RoleBindings,38 type RoleBindings,
20} from "./plugin/bindings";39} from "./plugin/bindings";
40export { makePluginCtxAppsResolver } from "./plugin/resolver";
21export {41export {
22 fetchGitHubSource,42 fetchGitHubSource,
23 parseGitHubSourceUrl,43 parseGitHubSourceUrl,
packages/plugins/apps/src/index.ts+2−0
@@ -28,6 +28,7 @@ export { · 27 unmodified lines
28} from "./source/github-source";28} from "./source/github-source";
29 29
30export { makeAppsRuntime, type AppsRuntime, type AppsRuntimeDeps } from "./plugin/runtime";30export { makeAppsRuntime, type AppsRuntime, type AppsRuntimeDeps } from "./plugin/runtime";
31export { makeAppsRuntimeFromBackings, type AppsBackings } from "./plugin/backings";
31export {32export {
32 makeAppsStore,33 makeAppsStore,
33 type AppsStore,34 type AppsStore,
@@ -50,6 +51,7 @@ export { · 16 unmodified lines
50 type ClientResolver,51 type ClientResolver,
51 type BindingContext,52 type BindingContext,
52} from "./plugin/bindings";53} from "./plugin/bindings";
54export { makePluginCtxAppsResolver } from "./plugin/resolver";
53export {55export {
54 makeSelfHostAppsRuntime,56 makeSelfHostAppsRuntime,
55 type SelfHostAppsRuntime,57 type SelfHostAppsRuntime,
packages/plugins/apps/src/plugin/apps-plugin.test.ts+1−1
@@ -62,7 +62,7 @@ describe("appsPlugin custom-tools contract", () => { · 61 unmodified lines
62 inMemory: true,62 inMemory: true,
63 });63 });
64 const runtime = host.runtime;64 const runtime = host.runtime;
65 const plugin = appsPlugin({ runtime });65 const plugin = appsPlugin({ backings: host.backings });
66 const appIntegration = IntegrationSlug.make("rhys-tools");66 const appIntegration = IntegrationSlug.make("rhys-tools");
67 const appConfig = {67 const appConfig = {
68 kind: "github",68 kind: "github",
packages/plugins/apps/src/plugin/apps-plugin.ts+214−50
@@ -14,11 +14,17 @@ import { · 13 unmodified lines
14 type PluginCtx,14 type PluginCtx,
15 type ResolveToolsInput,15 type ResolveToolsInput,
16 type ResolveToolsResult,16 type ResolveToolsResult,
17 StorageError,
18 type StorageFailure,
17 type ToolDef,19 type ToolDef,
20 type IntegrationRemovalNotAllowedError,
18} from "@executor-js/sdk";21} from "@executor-js/sdk";
19 22
20import type { IntegrationDecl, ToolDescriptor } from "../pipeline/descriptor";23import type { IntegrationDecl, ToolDescriptor } from "../pipeline/descriptor";
21import { PublishError } from "../pipeline/discover";24import { PublishError } from "../pipeline/discover";
25import { ArtifactStoreError } from "../seams/artifact-store";
26import { ScopeDbError } from "../seams/scope-db";
27import { ToolSandboxError } from "../seams/tool-sandbox";
22import {28import {
23 parseGitHubSourceUrl,29 parseGitHubSourceUrl,
24 syncGitHubSource,30 syncGitHubSource,
@@ -28,7 +34,9 @@ import { · 3 unmodified lines
28import { slugifyCustomToolsAppName, validateCustomToolsAppSlug } from "../source/app-slug";34import { slugifyCustomToolsAppName, validateCustomToolsAppSlug } from "../source/app-slug";
29import type { AppsRuntime, GitHubCustomToolsSourceSummary } from "./runtime";35import type { AppsRuntime, GitHubCustomToolsSourceSummary } from "./runtime";
30import { makeAppsStore, type AppDescriptorRecord, type GitHubSourceTokenRef } from "./store";36import { makeAppsStore, type AppDescriptorRecord, type GitHubSourceTokenRef } from "./store";
31import type { ClientResolver, ConnectionCandidate } from "./bindings";37import { BindingError, type ClientResolver, type ConnectionCandidate } from "./bindings";
38import { makePluginCtxAppsResolver } from "./resolver";
39import { makeAppsRuntimeFromBackings, type AppsBackings } from "./backings";
32 40
33export const APPS_INTEGRATION_SLUG = "apps";41export const APPS_INTEGRATION_SLUG = "apps";
34export const APPS_PLUGIN_ID = "apps";42export const APPS_PLUGIN_ID = "apps";
@@ -44,17 +52,43 @@ interface AppsGitHubSourceConfig { · 9 unmodified lines
44 readonly token?: GitHubSourceTokenRef;52 readonly token?: GitHubSourceTokenRef;
45}53}
46 54
47export interface AppsPluginOptions {55type ResolverFactory = (input: {
56 readonly ctx: unknown;
57 readonly scope: string;
58 readonly tool: string;
59}) => ClientResolver;
60
61export type AppsPluginOptions =
62 | {
63 readonly backings: AppsBackings;
64 readonly runtime?: never;
65 readonly makeResolver?: ResolverFactory;
66 }
67 | {
68 readonly runtime: AppsRuntime;
69 readonly backings?: never;
70 readonly makeResolver?: ResolverFactory;
71 };
72
73interface AppsStoreShape {
48 readonly runtime: AppsRuntime;74 readonly runtime: AppsRuntime;
49 readonly makeResolver?: (input: {
50 readonly ctx: unknown;
51 readonly scope: string;
52 readonly tool: string;
53 }) => ClientResolver;
54}75}
55 76
56interface AppsStoreShape {77export interface AppsPluginExtension {
57 readonly runtime: AppsRuntime;78 readonly runtime: AppsRuntime;
79 readonly syncGitHubSource: (input: unknown) => Effect.Effect<GitHubSyncResult, StorageFailure>;
80 readonly listGitHubSources: () => Effect.Effect<{
81 readonly sources: readonly GitHubCustomToolsSourceSummary[];
82 }>;
83 readonly getGitHubSource: (slug: string) => Effect.Effect<{
84 readonly source: GitHubCustomToolsSourceSummary | null;
85 }>;
86 readonly removeGitHubSource: (
87 slug: string,
88 ) => Effect.Effect<
89 { readonly removed: true },
90 StorageFailure | IntegrationRemovalNotAllowedError
91 >;
58}92}
59 93
60const isRecord = (value: unknown): value is Record<string, unknown> =>94const isRecord = (value: unknown): value is Record<string, unknown> =>
@@ -65,6 +99,13 @@ const asString = (value: unknown): string | undefined => · 4 unmodified lines
65 99
66const unique = (values: readonly string[]): readonly string[] => [...new Set(values)];100const unique = (values: readonly string[]): readonly string[] => [...new Set(values)];
67 101
102const isPluginCtx = (value: unknown): value is PluginCtx =>
103 isRecord(value) &&
104 isRecord(value.connections) &&
105 typeof value.connections.list === "function" &&
106 typeof value.connections.get === "function" &&
107 typeof value.execute === "function";
108
68const syncFailure = (message: string, path?: string): GitHubSyncResult => ({109const syncFailure = (message: string, path?: string): GitHubSyncResult => ({
69 status: "failed",110 status: "failed",
70 tools: [],111 tools: [],
@@ -78,6 +119,98 @@ const syncFailure = (message: string, path?: string): GitHubSyncResult => ({ · 7 unmodified lines
78 ],119 ],
79});120});
80 121
122const missingResolver = (): ClientResolver => ({
123 listConnections: () => Effect.succeed([]),
124 resolveConnection: () => Effect.succeed(null),
125 call: ({ integration, connection }) =>
126 Effect.fail(
127 new BindingError({
128 role: integration,
129 integration,
130 requestedConnection: connection,
131 message: "apps resolver is unavailable outside a request-scoped executor",
132 }),
133 ),
134});
135
136const missingBackingsMessage = "apps plugin requires backings or runtime";
137
138const missingPublishError = (): PublishError =>
139 new PublishError({
140 message: missingBackingsMessage,
141 stage: "project",
142 diagnostics: [],
143 });
144
145const missingStorageError = (): StorageError =>
146 new StorageError({
147 message: missingBackingsMessage,
148 cause: missingBackingsMessage,
149 });
150
151const missingRuntime = (): AppsRuntime => ({
152 publish: () => Effect.fail(missingPublishError()),
153 getDescriptor: () => Effect.succeed(null),
154 listGitHubSources: () => Effect.succeed([]),
155 removeSource: () => Effect.fail(missingPublishError()),
156 repair: () => Effect.fail(missingPublishError()),
157 invokeTool: () => Effect.fail(missingPublishError()),
158 deps: {
159 artifactStore: {
160 forScope: () =>
161 Effect.fail(
162 new ArtifactStoreError({
163 message: missingBackingsMessage,
164 }),
165 ),
166 removeScope: () =>
167 Effect.fail(
168 new ArtifactStoreError({
169 message: missingBackingsMessage,
170 }),
171 ),
172 },
173 scopeDb: {
174 forScope: () =>
175 Effect.fail(
176 new ScopeDbError({
177 message: missingBackingsMessage,
178 }),
179 ),
180 removeScope: () =>
181 Effect.fail(
182 new ScopeDbError({
183 message: missingBackingsMessage,
184 }),
185 ),
186 close: () => Effect.void,
187 },
188 sandbox: {
189 collect: () =>
190 Effect.fail(
191 new ToolSandboxError({
192 message: missingBackingsMessage,
193 kind: "collect",
194 }),
195 ),
196 invoke: () =>
197 Effect.fail(
198 new ToolSandboxError({
199 message: missingBackingsMessage,
200 kind: "invoke",
201 }),
202 ),
203 },
204 store: {
205 putDescriptor: () => Effect.fail(missingStorageError()),
206 getDescriptor: () => Effect.succeed(null),
207 removeDescriptor: () => Effect.fail(missingStorageError()),
208 listDescriptors: () => Effect.succeed([]),
209 },
210 resolver: missingResolver(),
211 },
212});
213
81const sourceTokenItemId = (tenant: string, scope: string): ProviderItemId =>214const sourceTokenItemId = (tenant: string, scope: string): ProviderItemId =>
82 ProviderItemId.make(`apps:github-source:${tenant}:${scope}:token`);215 ProviderItemId.make(`apps:github-source:${tenant}:${scope}:token`);
83 216
@@ -160,7 +293,11 @@ const projectTool = (descriptor: ToolDescriptor): ToolDef => ({ · 76 unmodified lines
160});293});
161 294
162export const appsPlugin = definePlugin((options?: AppsPluginOptions) => {295export const appsPlugin = definePlugin((options?: AppsPluginOptions) => {
163 const runtime = options?.runtime as AppsRuntime;296 const runtime =
297 options?.runtime ??
298 (options?.backings
299 ? makeAppsRuntimeFromBackings(options.backings, missingResolver())
300 : missingRuntime());
164 const makeResolver = options?.makeResolver;301 const makeResolver = options?.makeResolver;
165 302
166 const tenantFor = (ctx: Pick<PluginCtx, "owner"> | undefined): string => {303 const tenantFor = (ctx: Pick<PluginCtx, "owner"> | undefined): string => {
@@ -436,6 +573,66 @@ export const appsPlugin = definePlugin((options?: AppsPluginOptions) => { · 269 unmodified lines
436 Effect.orElseSucceed(() => null),573 Effect.orElseSucceed(() => null),
437 );574 );
438 575
576 const requestResolver = (input: {
577 readonly ctx: unknown;
578 readonly scope: string;
579 readonly tool: string;
580 }): ClientResolver =>
581 makeResolver
582 ? makeResolver(input)
583 : isPluginCtx(input.ctx)
584 ? makePluginCtxAppsResolver({ ctx: input.ctx })
585 : runtime.deps.resolver;
586
587 const syncSourceFromPayload = (ctx: PluginCtx<AppsStoreShape>, args: unknown) =>
588 Effect.gen(function* () {
589 const payload = isRecord(args) ? args : {};
590 const url =
591 typeof payload.url === "string"
592 ? payload.url.trim()
593 : typeof payload.repo === "string"
594 ? payload.repo.trim()
595 : "";
596 const ref = asString(payload.ref);
597 const providedToken = asString(payload.token) ?? null;
598 const slug = asString(payload.slug);
599 if (slug && !url) return yield* syncExistingSource(ctx, slug, providedToken);
600 if (!url) return syncFailure('sync_github_source requires "url"');
601 return yield* addSource(ctx, {
602 url,
603 ...(ref ? { ref } : {}),
604 ...(asString(payload.name) ? { name: asString(payload.name) } : {}),
605 token: providedToken,
606 });
607 }).pipe(
608 Effect.catchTags({
609 CredentialProviderNotRegisteredError: (err) => Effect.succeed(syncFailure(err.message)),
610 IntegrationNotFoundError: (err) => Effect.succeed(syncFailure(err.message)),
611 InvalidConnectionInputError: (err) => Effect.succeed(syncFailure(err.message)),
612 }),
613 );
614
615 const extensionFor = (ctx: PluginCtx<AppsStoreShape>): AppsPluginExtension => ({
616 runtime,
617 syncGitHubSource: (input) => syncSourceFromPayload(ctx, input),
618 listGitHubSources: () =>
619 Effect.gen(function* () {
620 const sources = yield* listSources(ctx);
621 return { sources };
622 }),
623 getGitHubSource: (slug) =>
624 Effect.gen(function* () {
625 if (!slug) return { source: null };
626 const source = yield* getSource(ctx, slug);
627 return { source };
628 }),
629 removeGitHubSource: (slug) =>
630 Effect.gen(function* () {
631 yield* ctx.core.integrations.remove(IntegrationSlug.make(slug));
632 return { removed: true as const };
633 }),
634 });
635
439 return {636 return {
440 id: APPS_PLUGIN_ID as "apps",637 id: APPS_PLUGIN_ID as "apps",
441 packageName: "@executor-js/plugin-apps",638 packageName: "@executor-js/plugin-apps",
@@ -455,9 +652,9 @@ export const appsPlugin = definePlugin((options?: AppsPluginOptions) => { · 13 unmodified lines
455 },652 },
456 },653 },
457 654
458 extension: () => ({ runtime }),655 extension: extensionFor,
459 656
460 staticSources: () => [657 staticSources: (self) => [
461 {658 {
462 id: APPS_PLUGIN_ID,659 id: APPS_PLUGIN_ID,
463 kind: "executor",660 kind: "executor",
@@ -466,47 +663,18 @@ export const appsPlugin = definePlugin((options?: AppsPluginOptions) => { · 2 unmodified lines
466 tool<AppsStoreShape>({663 tool<AppsStoreShape>({
467 name: "sync_github_source",664 name: "sync_github_source",
468 description: "Sync a GitHub repository containing custom tool source files.",665 description: "Sync a GitHub repository containing custom tool source files.",
469 execute: (args, { ctx }) =>666 execute: (args) => self.syncGitHubSource(args),
470 Effect.gen(function* () {
471 const payload = isRecord(args) ? args : {};
472 const url =
473 typeof payload.url === "string"
474 ? payload.url.trim()
475 : typeof payload.repo === "string"
476 ? payload.repo.trim()
477 : "";
478 const ref = asString(payload.ref);
479 const providedToken = asString(payload.token) ?? null;
480 const slug = asString(payload.slug);
481 if (slug && !url) return yield* syncExistingSource(ctx, slug, providedToken);
482 if (!url) return syncFailure('sync_github_source requires "url"');
483 return yield* addSource(ctx, {
484 url,
485 ...(ref ? { ref } : {}),
486 ...(asString(payload.name) ? { name: asString(payload.name) } : {}),
487 token: providedToken,
488 });
489 }),
490 }),667 }),
491 tool<AppsStoreShape>({668 tool<AppsStoreShape>({
492 name: "list_github_sources",669 name: "list_github_sources",
493 description: "List synced GitHub repositories that publish custom tools.",670 description: "List synced GitHub repositories that publish custom tools.",
494 execute: (_args, { ctx }) =>671 execute: () => self.listGitHubSources(),
495 Effect.gen(function* () {
496 const sources = yield* listSources(ctx);
497 return { sources };
498 }),
499 }),672 }),
500 tool<AppsStoreShape>({673 tool<AppsStoreShape>({
501 name: "get_github_source",674 name: "get_github_source",
502 description: "Read one synced GitHub custom-tools source.",675 description: "Read one synced GitHub custom-tools source.",
503 execute: (args, { ctx }) =>676 execute: (args) =>
504 Effect.gen(function* () {677 self.getGitHubSource(isRecord(args) ? (asString(args.slug) ?? "") : ""),
505 const slug = isRecord(args) ? asString(args.slug) : undefined;
506 if (!slug) return { source: null };
507 const source = yield* getSource(ctx, slug);
508 return { source };
509 }),
510 }),678 }),
511 ],679 ],
512 },680 },
@@ -547,9 +715,7 @@ export const appsPlugin = definePlugin((options?: AppsPluginOptions) => { · 34 unmodified lines
547 const descriptor = yield* runtime.getDescriptor(tenant, source.scope);715 const descriptor = yield* runtime.getDescriptor(tenant, source.scope);
548 const toolDesc = descriptor?.tools.find((t) => t.name === toolRow.name);716 const toolDesc = descriptor?.tools.find((t) => t.name === toolRow.name);
549 if (!toolDesc) return { inputSchema, outputSchema };717 if (!toolDesc) return { inputSchema, outputSchema };
550 const resolver = makeResolver718 const resolver = requestResolver({ ctx, scope: source.scope, tool: toolRow.name });
551 ? makeResolver({ ctx, scope: source.scope, tool: toolRow.name })
552 : runtime.deps.resolver;
553 const byRole: Record<string, readonly ConnectionCandidate[]> = {};719 const byRole: Record<string, readonly ConnectionCandidate[]> = {};
554 for (const [role, decl] of Object.entries(toolDesc.integrations)) {720 for (const [role, decl] of Object.entries(toolDesc.integrations)) {
555 byRole[role] = yield* resolver721 byRole[role] = yield* resolver
@@ -589,9 +755,7 @@ export const appsPlugin = definePlugin((options?: AppsPluginOptions) => { · 33 unmodified lines
589 diagnostics: [],755 diagnostics: [],
590 });756 });
591 }757 }
592 const resolver = makeResolver758 const resolver = requestResolver({ ctx, scope: source.scope, tool: toolRow.name });
593 ? makeResolver({ ctx, scope: source.scope, tool: toolRow.name })
594 : undefined;
595 return yield* runtime.invokeTool({759 return yield* runtime.invokeTool({
596 tenant,760 tenant,
597 scope: source.scope,761 scope: source.scope,
packages/plugins/apps/src/plugin/backings.ts+28−0
@@ -0,0 +1,28 @@
1import type { ArtifactStore } from "../seams/artifact-store";
2import type { ScopeDb } from "../seams/scope-db";
3import type { ToolSandbox } from "../seams/tool-sandbox";
4import { makeAppsRuntime, type AppsRuntime } from "./runtime";
5import type { AppsStore } from "./store";
6import type { ClientResolver } from "./bindings";
7
8export interface AppsBackings {
9 readonly artifactStore: ArtifactStore;
10 readonly scopeDb: ScopeDb;
11 readonly sandbox: ToolSandbox;
12 readonly store: AppsStore;
13 readonly defaultTenant?: string;
14 readonly resolver?: ClientResolver;
15}
16
17export const makeAppsRuntimeFromBackings = (
18 backings: AppsBackings,
19 fallbackResolver: ClientResolver,
20): AppsRuntime =>
21 makeAppsRuntime({
22 artifactStore: backings.artifactStore,
23 scopeDb: backings.scopeDb,
24 sandbox: backings.sandbox,
25 store: backings.store,
26 resolver: backings.resolver ?? fallbackResolver,
27 defaultTenant: backings.defaultTenant,
28 });
packages/plugins/apps/src/plugin/handlers.ts+49−0
@@ -0,0 +1,49 @@
1import { Context, Effect } from "effect";
2import { HttpApiBuilder } from "effect/unstable/httpapi";
3
4import { addGroup, capture } from "@executor-js/api";
5import type { AppsPluginExtension } from "./apps-plugin";
6import { AppsGroup } from "./routes";
7
8export class AppsExtensionService extends Context.Service<
9 AppsExtensionService,
10 AppsPluginExtension
11>()("AppsExtensionService") {}
12
13const ExecutorApiWithApps = addGroup(AppsGroup);
14
15export const AppsHandlers = HttpApiBuilder.group(ExecutorApiWithApps, "apps", (handlers) =>
16 handlers
17 .handle("listGithubSources", () =>
18 capture(
19 Effect.gen(function* () {
20 const ext = yield* AppsExtensionService;
21 return yield* ext.listGitHubSources();
22 }),
23 ),
24 )
25 .handle("syncGithubSource", ({ payload }) =>
26 capture(
27 Effect.gen(function* () {
28 const ext = yield* AppsExtensionService;
29 return yield* ext.syncGitHubSource(payload);
30 }),
31 ),
32 )
33 .handle("getGithubSource", ({ params }) =>
34 capture(
35 Effect.gen(function* () {
36 const ext = yield* AppsExtensionService;
37 return yield* ext.getGitHubSource(params.slug);
38 }),
39 ),
40 )
41 .handle("removeGithubSource", ({ params }) =>
42 capture(
43 Effect.gen(function* () {
44 const ext = yield* AppsExtensionService;
45 return yield* ext.removeGitHubSource(params.slug);
46 }),
47 ),
48 ),
49);
packages/plugins/apps/src/plugin/resolver.tsrenamed from apps/host-selfhost/src/apps-resolver.ts+2−6
@@ -9,11 +9,7 @@ import { · 8 unmodified lines
9 type Owner,9 type Owner,
10 type PluginCtx,10 type PluginCtx,
11} from "@executor-js/sdk";11} from "@executor-js/sdk";
12import {12import { BindingError, type ClientResolver, type ConnectionCandidate } from "./bindings";
13 BindingError,
14 type ClientResolver,
15 type ConnectionCandidate,
16} from "@executor-js/plugin-apps/api";
17 13
18const parseConnectionAddress = (address: string): ConnectionRef | null => {14const parseConnectionAddress = (address: string): ConnectionRef | null => {
19 const parts = address.split(".");15 const parts = address.split(".");
@@ -40,7 +36,7 @@ const toCandidate = (connection: { · 20 unmodified lines
40 ...(connection.owner !== undefined ? { owner: String(connection.owner) } : {}),36 ...(connection.owner !== undefined ? { owner: String(connection.owner) } : {}),
41});37});
42 38
43export const makeSelfHostAppsResolver = (input: { readonly ctx: PluginCtx }): ClientResolver => ({39export const makePluginCtxAppsResolver = (input: { readonly ctx: PluginCtx }): ClientResolver => ({
44 listConnections: ({ integration }) =>40 listConnections: ({ integration }) =>
45 input.ctx.connections.list({ integration: IntegrationSlug.make(integration) }).pipe(41 input.ctx.connections.list({ integration: IntegrationSlug.make(integration) }).pipe(
46 Effect.map((connections) => connections.map(toCandidate)),42 Effect.map((connections) => connections.map(toCandidate)),
packages/plugins/apps/src/plugin/routes.ts+104−0
@@ -0,0 +1,104 @@
1import { Schema } from "effect";
2import { HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi";
3import { IntegrationRemovalNotAllowedError, InternalError } from "@executor-js/sdk/shared";
4
5const GitHubSourcePayload = Schema.Struct({
6 url: Schema.optional(Schema.String),
7 repo: Schema.optional(Schema.String),
8 name: Schema.optional(Schema.String),
9 slug: Schema.optional(Schema.String),
10 ref: Schema.optional(Schema.String),
11 token: Schema.optional(Schema.String),
12});
13
14const SourceSlugParams = {
15 slug: Schema.String,
16};
17
18const SkippedArtifact = Schema.Struct({
19 path: Schema.String,
20 reason: Schema.Literals(["not supported yet", "unsupported file type", "ignored"]),
21});
22
23const FileDiagnostic = Schema.Struct({
24 path: Schema.String,
25 message: Schema.String,
26});
27
28const SyncErrorData = Schema.Struct({
29 stage: Schema.Literals(["source", "discover", "bundle", "collect", "project"]),
30 message: Schema.String,
31 diagnostics: Schema.optional(Schema.Array(FileDiagnostic)),
32});
33
34const GitHubSyncResponse = Schema.Struct({
35 status: Schema.Literals(["published", "up-to-date", "failed"]),
36 snapshotId: Schema.optional(Schema.String),
37 upstreamSha: Schema.optional(Schema.String),
38 tools: Schema.Array(Schema.String),
39 skipped: Schema.Array(SkippedArtifact),
40 errors: Schema.optional(Schema.Array(SyncErrorData)),
41});
42
43const GitHubSourceSummary = Schema.Struct({
44 slug: Schema.String,
45 name: Schema.String,
46 scope: Schema.String,
47 url: Schema.String,
48 repo: Schema.String,
49 ref: Schema.String,
50 hasToken: Schema.Boolean,
51 upstreamSha: Schema.String,
52 snapshotId: Schema.String,
53 description: Schema.optional(Schema.String),
54 publishedAt: Schema.String,
55 tools: Schema.Array(Schema.String),
56 skipped: Schema.Array(SkippedArtifact),
57});
58
59const GitHubSourcesResponse = Schema.Struct({
60 sources: Schema.Array(GitHubSourceSummary),
61});
62
63const GitHubSourceResponse = Schema.Struct({
64 source: Schema.NullOr(GitHubSourceSummary),
65});
66
67const RemoveSourceResponse = Schema.Struct({
68 removed: Schema.Boolean,
69});
70
71const DomainErrors = [InternalError] as const;
72const IntegrationRemovalNotAllowed = IntegrationRemovalNotAllowedError.annotate({
73 httpApiStatus: 409,
74});
75const RemoveSourceErrors = [InternalError, IntegrationRemovalNotAllowed] as const;
76
77export const AppsGroup = HttpApiGroup.make("apps")
78 .add(
79 HttpApiEndpoint.get("listGithubSources", "/apps/sources/github", {
80 success: GitHubSourcesResponse,
81 error: DomainErrors,
82 }),
83 )
84 .add(
85 HttpApiEndpoint.post("syncGithubSource", "/apps/sources/github/sync", {
86 payload: GitHubSourcePayload,
87 success: GitHubSyncResponse,
88 error: DomainErrors,
89 }),
90 )
91 .add(
92 HttpApiEndpoint.get("getGithubSource", "/apps/sources/github/:slug", {
93 params: SourceSlugParams,
94 success: GitHubSourceResponse,
95 error: DomainErrors,
96 }),
97 )
98 .add(
99 HttpApiEndpoint.delete("removeGithubSource", "/apps/sources/github/:slug", {
100 params: SourceSlugParams,
101 success: RemoveSourceResponse,
102 error: RemoveSourceErrors,
103 }),
104 );
packages/plugins/apps/src/plugin/self-host-runtime.ts+7−3
@@ -6,9 +6,10 @@ import { makeGitArtifactStore } from "../backing/git-artifact-store"; · 5 unmodified lines
6import { makeLibsqlScopeDb } from "../backing/libsql-scope-db";6import { makeLibsqlScopeDb } from "../backing/libsql-scope-db";
7import { makeQuickjsToolSandbox } from "../backing/quickjs-tool-sandbox";7import { makeQuickjsToolSandbox } from "../backing/quickjs-tool-sandbox";
8import type { ScopeDb } from "../seams/scope-db";8import type { ScopeDb } from "../seams/scope-db";
9import { makeAppsRuntime, type AppsRuntime } from "./runtime";9import { type AppsRuntime } from "./runtime";
10import type { AppsStore } from "./store";10import type { AppsStore } from "./store";
11import type { ClientResolver } from "./bindings";11import type { ClientResolver } from "./bindings";
12import { makeAppsRuntimeFromBackings, type AppsBackings } from "./backings";
12 13
13export interface SelfHostAppsRuntimeOptions {14export interface SelfHostAppsRuntimeOptions {
14 /** Data dir root; `<root>/artifacts` and `<root>/scope-db`. */15 /** Data dir root; `<root>/artifacts` and `<root>/scope-db`. */
@@ -24,6 +25,7 @@ export interface SelfHostAppsRuntimeOptions { · 9 unmodified lines
24 25
25export interface SelfHostAppsRuntime {26export interface SelfHostAppsRuntime {
26 readonly runtime: AppsRuntime;27 readonly runtime: AppsRuntime;
28 readonly backings: AppsBackings;
27 readonly scopeDb: ScopeDb;29 readonly scopeDb: ScopeDb;
28 readonly close: () => Promise<void>;30 readonly close: () => Promise<void>;
29}31}
@@ -39,17 +41,19 @@ export const makeSelfHostAppsRuntime = ( · 9 unmodified lines
39 root: inMem ? ":memory:" : join(options.dataDir, "scope-db"),41 root: inMem ? ":memory:" : join(options.dataDir, "scope-db"),
40 });42 });
41 const sandbox = makeQuickjsToolSandbox();43 const sandbox = makeQuickjsToolSandbox();
42 const runtime = makeAppsRuntime({44 const backings: AppsBackings = {
43 artifactStore,45 artifactStore,
44 scopeDb,46 scopeDb,
45 sandbox,47 sandbox,
46 store: options.store,48 store: options.store,
47 resolver: options.resolver,49 resolver: options.resolver,
48 defaultTenant: options.tenant,50 defaultTenant: options.tenant,
49 });51 };
52 const runtime = makeAppsRuntimeFromBackings(backings, options.resolver);
50 53
51 return {54 return {
52 runtime,55 runtime,
56 backings,
53 scopeDb,57 scopeDb,
54 close: async () => {58 close: async () => {
55 await Effect.runPromise(scopeDb.close().pipe(Effect.orElseSucceed(() => undefined)));59 await Effect.runPromise(scopeDb.close().pipe(Effect.orElseSucceed(() => undefined)));
packages/plugins/apps/src/react/custom-tools-client.web.test.tsrenamed from apps/host-selfhost/src/custom-tools-console.web.test.ts+1−1
@@ -12,7 +12,7 @@ import appsClientPlugin, { · 11 unmodified lines
12 syncCustomToolSource,12 syncCustomToolSource,
13 syncStatusLabel,13 syncStatusLabel,
14 validateGitHubSourceUrl,14 validateGitHubSourceUrl,
15} from "@executor-js/plugin-apps/client";15} from "./plugin-client";
16 16
17const jsonResponse = (body: unknown, init?: ResponseInit): Response =>17const jsonResponse = (body: unknown, init?: ResponseInit): Response =>
18 new Response(JSON.stringify(body), {18 new Response(JSON.stringify(body), {