Unified · Split
| @@ -6,9 +6,10 @@ import { mcpHttpPlugin } from "@executor-js/plugin-mcp/api"; · 5 unmodified lines | ||
| 6 | 6 | import { graphqlHttpPlugin } from "@executor-js/plugin-graphql/api"; |
| 7 | 7 | import { encryptedSecretsPlugin } from "@executor-js/plugin-encrypted-secrets"; |
| 8 | 8 | import { toolkitsPlugin } from "@executor-js/plugin-toolkits/server"; |
| 9 | +import { appsPlugin } from "@executor-js/plugin-apps/api"; | |
| 9 | 10 | |
| 10 | 11 | import { resolveSecretKey } from "./src/config"; |
| 11 | −import { getSelfHostAppsSubsystem } from "./src/apps"; | |
| 12 | +import { getSelfHostAppsBackings } from "./src/apps-backings"; | |
| 12 | 13 | |
| 13 | 14 | // --------------------------------------------------------------------------- |
| 14 | 15 | // Single source of truth for the self-hosted app's plugin list. |
| @@ -37,7 +38,7 @@ export default defineExecutorConfig({ · 22 unmodified lines | ||
| 37 | 38 | mcpHttpPlugin({ dangerouslyAllowStdioMCP: false }), |
| 38 | 39 | graphqlHttpPlugin(), |
| 39 | 40 | toolkitsPlugin({ activeToolkitSlug }), |
| 40 | − getSelfHostAppsSubsystem().plugin, | |
| 41 | + appsPlugin({ backings: getSelfHostAppsBackings() }), | |
| 41 | 42 | // First writable secret provider -> the default for `secrets.set`. |
| 42 | 43 | encryptedSecretsPlugin({ key: resolveSecretKey() }), |
| 43 | 44 | ] as const, |
| @@ -22,8 +22,7 @@ import { makeSelfHostMcpSeams } from "./mcp"; · 21 unmodified lines | ||
| 22 | 22 | import { selfHostPlugins } from "./plugins"; |
| 23 | 23 | import { ErrorCaptureLive } from "./observability"; |
| 24 | 24 | import { oauthCallbackSignInRedirectLocation } from "./auth/oauth-callback-login"; |
| 25 | −import { closeSelfHostAppsSubsystem } from "./apps"; | |
| 26 | −import { makeSelfHostAppsSyncRoute } from "./apps-route"; | |
| 25 | +import { closeSelfHostAppsBackings } from "./apps-backings"; | |
| 27 | 26 | |
| 28 | 27 | // =========================================================================== |
| 29 | 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 | 120 | makeSelfHostAdminApiLayer({ betterAuth, db: dbHandle, mountPrefix: "/api" }), |
| 122 | 121 | // Public system API: /api/health + /api/setup-status (unauthenticated). |
| 123 | 122 | makeSelfHostSystemApiLayer({ betterAuth, db: dbHandle, mountPrefix: "/api" }), |
| 124 | − makeSelfHostAppsSyncRoute({ identity: identityLayer, db: dbHandle }), | |
| 125 | 123 | // Swagger UI at /docs, over the /api-prefixed spec (matches the served paths). |
| 126 | 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 | 140 | toWebHandler, |
| 143 | 141 | betterAuth, |
| 144 | 142 | closeDb: async () => { |
| 145 | − await closeSelfHostAppsSubsystem(); | |
| 143 | + await closeSelfHostAppsBackings(); | |
| 146 | 144 | await mcp.close(); |
| 147 | 145 | await dbHandle.close(); |
| 148 | 146 | }, |
| @@ -0,0 +1,52 @@ | ||
| 1 | +import { join } from "node:path"; | |
| 2 | + | |
| 3 | +import { Effect } from "effect"; | |
| 4 | + | |
| 5 | +import { | |
| 6 | + makeGitArtifactStore, | |
| 7 | + makeLibsqlScopeDb, | |
| 8 | + makeQuickjsToolSandbox, | |
| 9 | + makeSqliteAppsStore, | |
| 10 | + type AppsBackings, | |
| 11 | +} from "@executor-js/plugin-apps/api"; | |
| 12 | + | |
| 13 | +import { resolveDataDir } from "./config"; | |
| 14 | + | |
| 15 | +interface SelfHostAppsBackings { | |
| 16 | + readonly backings: AppsBackings; | |
| 17 | + readonly close: () => Promise<void>; | |
| 18 | +} | |
| 19 | + | |
| 20 | +const 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 | + | |
| 38 | +let current: { readonly dataDir: string; readonly value: SelfHostAppsBackings } | undefined; | |
| 39 | + | |
| 40 | +export 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 | + | |
| 48 | +export const closeSelfHostAppsBackings = async (): Promise<void> => { | |
| 49 | + const value = current?.value; | |
| 50 | + current = undefined; | |
| 51 | + await value?.close(); | |
| 52 | +}; | |
| @@ -1,166 +0,0 @@ | ||
| 1 | −import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http"; | |
| 2 | −import { Data, Effect, Layer, Predicate } from "effect"; | |
| 3 | − | |
| 4 | −import { | |
| 5 | − IdentityProvider, | |
| 6 | − RequestOrgSlug, | |
| 7 | − RequestWebOrigin, | |
| 8 | − makeScopedExecutor, | |
| 9 | − type Principal, | |
| 10 | −} from "@executor-js/api/server"; | |
| 11 | −import { IntegrationSlug, ToolAddress } from "@executor-js/sdk"; | |
| 12 | − | |
| 13 | −import { SelfHostScopedExecutorSeams } from "./execution"; | |
| 14 | −import { SelfHostDb, type SelfHostDbHandle } from "./db/self-host-db"; | |
| 15 | −import type { SelfHostPlugins } from "./plugins"; | |
| 16 | − | |
| 17 | −class AppsSyncRouteError extends Data.TaggedError("AppsSyncRouteError")<{ | |
| 18 | − readonly status: number; | |
| 19 | − readonly message: string; | |
| 20 | −}> {} | |
| 21 | − | |
| 22 | −const isAppsSyncRouteError = (failure: unknown): failure is AppsSyncRouteError => | |
| 23 | − Predicate.isTagged("AppsSyncRouteError")(failure); | |
| 24 | − | |
| 25 | −const 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 | − | |
| 35 | −const 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 | − | |
| 46 | −const 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 | − | |
| 61 | −export interface SelfHostAppsSyncRouteDeps { | |
| 62 | − readonly identity: Layer.Layer<IdentityProvider>; | |
| 63 | − readonly db: SelfHostDbHandle; | |
| 64 | −} | |
| 65 | − | |
| 66 | −const 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 | − | |
| 102 | −export 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 | −}; | |
| @@ -1,71 +0,0 @@ | ||
| 1 | −import { join } from "node:path"; | |
| 2 | − | |
| 3 | −import { Effect } from "effect"; | |
| 4 | − | |
| 5 | −import { | |
| 6 | − appsPlugin, | |
| 7 | − BindingError, | |
| 8 | − makeSelfHostAppsRuntime, | |
| 9 | − makeSqliteAppsStore, | |
| 10 | − type AppsRuntime, | |
| 11 | − type ClientResolver, | |
| 12 | −} from "@executor-js/plugin-apps/api"; | |
| 13 | −import type { PluginCtx } from "@executor-js/sdk"; | |
| 14 | − | |
| 15 | −import { resolveDataDir } from "./config"; | |
| 16 | −import { makeSelfHostAppsResolver } from "./apps-resolver"; | |
| 17 | − | |
| 18 | −const 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 | − | |
| 32 | −const 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 | − | |
| 51 | −export interface SelfHostAppsSubsystem { | |
| 52 | − readonly runtime: AppsRuntime; | |
| 53 | − readonly plugin: ReturnType<typeof appsPlugin>; | |
| 54 | − readonly close: () => Promise<void>; | |
| 55 | −} | |
| 56 | − | |
| 57 | −let current: { readonly dataDir: string; readonly subsystem: SelfHostAppsSubsystem } | undefined; | |
| 58 | − | |
| 59 | −export 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 | − | |
| 67 | −export const closeSelfHostAppsSubsystem = async (): Promise<void> => { | |
| 68 | − const subsystem = current?.subsystem; | |
| 69 | − current = undefined; | |
| 70 | − await subsystem?.close(); | |
| 71 | −}; | |
| @@ -30,8 +30,7 @@ import { · 29 unmodified lines | ||
| 30 | 30 | } from "../mcp/session-store"; |
| 31 | 31 | import { selfHostPlugins } from "../plugins"; |
| 32 | 32 | import { ErrorCaptureLive } from "../observability"; |
| 33 | −import { closeSelfHostAppsSubsystem } from "../apps"; | |
| 34 | −import { makeSelfHostAppsSyncRoute } from "../apps-route"; | |
| 33 | +import { closeSelfHostAppsBackings } from "../apps-backings"; | |
| 35 | 34 | |
| 36 | 35 | // =========================================================================== |
| 37 | 36 | // 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 | 204 | extensions: { |
| 206 | 205 | routes: [ |
| 207 | − makeSelfHostAppsSyncRoute({ identity: options.identity, db: dbHandle }), | |
| 208 | 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 | 218 | handler: web.handler, |
| 221 | 219 | dispose: async () => { |
| 222 | 220 | await web.dispose(); |
| 223 | − await closeSelfHostAppsSubsystem(); | |
| 221 | + await closeSelfHostAppsBackings(); | |
| 224 | 222 | await sessionStore.close(); |
| 225 | 223 | await dbHandle.close(); |
| 226 | 224 | }, |
| @@ -797,6 +797,7 @@ · 796 unmodified lines | ||
| 797 | 797 | "devDependencies": { |
| 798 | 798 | "@effect/atom-react": "catalog:", |
| 799 | 799 | "@effect/vitest": "catalog:", |
| 800 | + "@executor-js/api": "workspace:*", | |
| 800 | 801 | "@executor-js/react": "workspace:*", |
| 801 | 802 | "@types/node": "catalog:", |
| 802 | 803 | "@types/react": "catalog:", |
| @@ -808,12 +809,14 @@ · 5 unmodified lines | ||
| 808 | 809 | }, |
| 809 | 810 | "peerDependencies": { |
| 810 | 811 | "@effect/atom-react": "catalog:", |
| 812 | + "@executor-js/api": "workspace:*", | |
| 811 | 813 | "@executor-js/react": "workspace:*", |
| 812 | 814 | "effect": "catalog:", |
| 813 | 815 | "react": "catalog:", |
| 814 | 816 | }, |
| 815 | 817 | "optionalPeers": [ |
| 816 | 818 | "@effect/atom-react", |
| 819 | + "@executor-js/api", | |
| 817 | 820 | "@executor-js/react", |
| 818 | 821 | "react", |
| 819 | 822 | ], |
| @@ -40,6 +40,7 @@ · 39 unmodified lines | ||
| 40 | 40 | "devDependencies": { |
| 41 | 41 | "@effect/atom-react": "catalog:", |
| 42 | 42 | "@effect/vitest": "catalog:", |
| 43 | + "@executor-js/api": "workspace:*", | |
| 43 | 44 | "@executor-js/react": "workspace:*", |
| 44 | 45 | "@types/node": "catalog:", |
| 45 | 46 | "@types/react": "catalog:", |
| @@ -51,6 +52,7 @@ · 5 unmodified lines | ||
| 51 | 52 | }, |
| 52 | 53 | "peerDependencies": { |
| 53 | 54 | "@effect/atom-react": "catalog:", |
| 55 | + "@executor-js/api": "workspace:*", | |
| 54 | 56 | "@executor-js/react": "workspace:*", |
| 55 | 57 | "effect": "catalog:", |
| 56 | 58 | "react": "catalog:" |
| @@ -59,6 +61,9 @@ · 2 unmodified lines | ||
| 59 | 61 | "@effect/atom-react": { |
| 60 | 62 | "optional": true |
| 61 | 63 | }, |
| 64 | + "@executor-js/api": { | |
| 65 | + "optional": true | |
| 66 | + }, | |
| 62 | 67 | "@executor-js/react": { |
| 63 | 68 | "optional": true |
| 64 | 69 | }, |
| @@ -1,16 +1,35 @@ | ||
| 1 | 1 | // Host-facing plugin surface for @executor-js/plugin-apps. |
| 2 | −export { | |
| 3 | − appsPlugin, | |
| 2 | +import { definePlugin } from "@executor-js/sdk/core"; | |
| 3 | + | |
| 4 | +import { | |
| 5 | + appsPlugin as appsCorePlugin, | |
| 4 | 6 | APPS_INTEGRATION_SLUG, |
| 5 | 7 | APPS_PLUGIN_ID, |
| 6 | 8 | type AppsPluginOptions, |
| 7 | 9 | } from "./plugin/apps-plugin"; |
| 10 | +import { AppsGroup } from "./plugin/routes"; | |
| 11 | +import { AppsHandlers, AppsExtensionService } from "./plugin/handlers"; | |
| 12 | + | |
| 13 | +export const appsPlugin = definePlugin((options?: AppsPluginOptions) => ({ | |
| 14 | + ...appsCorePlugin(options), | |
| 15 | + routes: () => AppsGroup, | |
| 16 | + handlers: () => AppsHandlers, | |
| 17 | + extensionService: AppsExtensionService, | |
| 18 | +})); | |
| 19 | + | |
| 20 | +export { APPS_INTEGRATION_SLUG, APPS_PLUGIN_ID, type AppsPluginOptions }; | |
| 21 | +export { AppsGroup } from "./plugin/routes"; | |
| 22 | +export { AppsHandlers, AppsExtensionService } from "./plugin/handlers"; | |
| 8 | 23 | export { |
| 9 | 24 | makeSelfHostAppsRuntime, |
| 10 | 25 | type SelfHostAppsRuntime, |
| 11 | 26 | type SelfHostAppsRuntimeOptions, |
| 12 | 27 | } from "./plugin/self-host-runtime"; |
| 13 | 28 | export { type AppsRuntime, type GitHubCustomToolsSourceSummary } from "./plugin/runtime"; |
| 29 | +export { makeAppsRuntimeFromBackings, type AppsBackings } from "./plugin/backings"; | |
| 30 | +export { makeGitArtifactStore } from "./backing/git-artifact-store"; | |
| 31 | +export { makeLibsqlScopeDb } from "./backing/libsql-scope-db"; | |
| 32 | +export { makeQuickjsToolSandbox } from "./backing/quickjs-tool-sandbox"; | |
| 14 | 33 | export { makeSqliteAppsStore } from "./backing/sqlite-apps-store"; |
| 15 | 34 | export { |
| 16 | 35 | BindingError, |
| @@ -18,6 +37,7 @@ export { · 1 unmodified line | ||
| 18 | 37 | type ConnectionCandidate, |
| 19 | 38 | type RoleBindings, |
| 20 | 39 | } from "./plugin/bindings"; |
| 40 | +export { makePluginCtxAppsResolver } from "./plugin/resolver"; | |
| 21 | 41 | export { |
| 22 | 42 | fetchGitHubSource, |
| 23 | 43 | parseGitHubSourceUrl, |
| @@ -28,6 +28,7 @@ export { · 27 unmodified lines | ||
| 28 | 28 | } from "./source/github-source"; |
| 29 | 29 | |
| 30 | 30 | export { makeAppsRuntime, type AppsRuntime, type AppsRuntimeDeps } from "./plugin/runtime"; |
| 31 | +export { makeAppsRuntimeFromBackings, type AppsBackings } from "./plugin/backings"; | |
| 31 | 32 | export { |
| 32 | 33 | makeAppsStore, |
| 33 | 34 | type AppsStore, |
| @@ -50,6 +51,7 @@ export { · 16 unmodified lines | ||
| 50 | 51 | type ClientResolver, |
| 51 | 52 | type BindingContext, |
| 52 | 53 | } from "./plugin/bindings"; |
| 54 | +export { makePluginCtxAppsResolver } from "./plugin/resolver"; | |
| 53 | 55 | export { |
| 54 | 56 | makeSelfHostAppsRuntime, |
| 55 | 57 | type SelfHostAppsRuntime, |
| @@ -62,7 +62,7 @@ describe("appsPlugin custom-tools contract", () => { · 61 unmodified lines | ||
| 62 | 62 | inMemory: true, |
| 63 | 63 | }); |
| 64 | 64 | const runtime = host.runtime; |
| 65 | − const plugin = appsPlugin({ runtime }); | |
| 65 | + const plugin = appsPlugin({ backings: host.backings }); | |
| 66 | 66 | const appIntegration = IntegrationSlug.make("rhys-tools"); |
| 67 | 67 | const appConfig = { |
| 68 | 68 | kind: "github", |
| @@ -14,11 +14,17 @@ import { · 13 unmodified lines | ||
| 14 | 14 | type PluginCtx, |
| 15 | 15 | type ResolveToolsInput, |
| 16 | 16 | type ResolveToolsResult, |
| 17 | + StorageError, | |
| 18 | + type StorageFailure, | |
| 17 | 19 | type ToolDef, |
| 20 | + type IntegrationRemovalNotAllowedError, | |
| 18 | 21 | } from "@executor-js/sdk"; |
| 19 | 22 | |
| 20 | 23 | import type { IntegrationDecl, ToolDescriptor } from "../pipeline/descriptor"; |
| 21 | 24 | import { PublishError } from "../pipeline/discover"; |
| 25 | +import { ArtifactStoreError } from "../seams/artifact-store"; | |
| 26 | +import { ScopeDbError } from "../seams/scope-db"; | |
| 27 | +import { ToolSandboxError } from "../seams/tool-sandbox"; | |
| 22 | 28 | import { |
| 23 | 29 | parseGitHubSourceUrl, |
| 24 | 30 | syncGitHubSource, |
| @@ -28,7 +34,9 @@ import { · 3 unmodified lines | ||
| 28 | 34 | import { slugifyCustomToolsAppName, validateCustomToolsAppSlug } from "../source/app-slug"; |
| 29 | 35 | import type { AppsRuntime, GitHubCustomToolsSourceSummary } from "./runtime"; |
| 30 | 36 | import { makeAppsStore, type AppDescriptorRecord, type GitHubSourceTokenRef } from "./store"; |
| 31 | −import type { ClientResolver, ConnectionCandidate } from "./bindings"; | |
| 37 | +import { BindingError, type ClientResolver, type ConnectionCandidate } from "./bindings"; | |
| 38 | +import { makePluginCtxAppsResolver } from "./resolver"; | |
| 39 | +import { makeAppsRuntimeFromBackings, type AppsBackings } from "./backings"; | |
| 32 | 40 | |
| 33 | 41 | export const APPS_INTEGRATION_SLUG = "apps"; |
| 34 | 42 | export const APPS_PLUGIN_ID = "apps"; |
| @@ -44,17 +52,43 @@ interface AppsGitHubSourceConfig { · 9 unmodified lines | ||
| 44 | 52 | readonly token?: GitHubSourceTokenRef; |
| 45 | 53 | } |
| 46 | 54 | |
| 47 | −export interface AppsPluginOptions { | |
| 55 | +type ResolverFactory = (input: { | |
| 56 | + readonly ctx: unknown; | |
| 57 | + readonly scope: string; | |
| 58 | + readonly tool: string; | |
| 59 | +}) => ClientResolver; | |
| 60 | + | |
| 61 | +export 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 | + | |
| 73 | +interface AppsStoreShape { | |
| 48 | 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 | |
| 56 | −interface AppsStoreShape { | |
| 77 | +export interface AppsPluginExtension { | |
| 57 | 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 | |
| 60 | 94 | const isRecord = (value: unknown): value is Record<string, unknown> => |
| @@ -65,6 +99,13 @@ const asString = (value: unknown): string | undefined => · 4 unmodified lines | ||
| 65 | 99 | |
| 66 | 100 | const unique = (values: readonly string[]): readonly string[] => [...new Set(values)]; |
| 67 | 101 | |
| 102 | +const 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 | + | |
| 68 | 109 | const syncFailure = (message: string, path?: string): GitHubSyncResult => ({ |
| 69 | 110 | status: "failed", |
| 70 | 111 | tools: [], |
| @@ -78,6 +119,98 @@ const syncFailure = (message: string, path?: string): GitHubSyncResult => ({ · 7 unmodified lines | ||
| 78 | 119 | ], |
| 79 | 120 | }); |
| 80 | 121 | |
| 122 | +const 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 | + | |
| 136 | +const missingBackingsMessage = "apps plugin requires backings or runtime"; | |
| 137 | + | |
| 138 | +const missingPublishError = (): PublishError => | |
| 139 | + new PublishError({ | |
| 140 | + message: missingBackingsMessage, | |
| 141 | + stage: "project", | |
| 142 | + diagnostics: [], | |
| 143 | + }); | |
| 144 | + | |
| 145 | +const missingStorageError = (): StorageError => | |
| 146 | + new StorageError({ | |
| 147 | + message: missingBackingsMessage, | |
| 148 | + cause: missingBackingsMessage, | |
| 149 | + }); | |
| 150 | + | |
| 151 | +const 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 | + | |
| 81 | 214 | const sourceTokenItemId = (tenant: string, scope: string): ProviderItemId => |
| 82 | 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 | |
| 162 | 295 | export 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 | 301 | const makeResolver = options?.makeResolver; |
| 165 | 302 | |
| 166 | 303 | const tenantFor = (ctx: Pick<PluginCtx, "owner"> | undefined): string => { |
| @@ -436,6 +573,66 @@ export const appsPlugin = definePlugin((options?: AppsPluginOptions) => { · 269 unmodified lines | ||
| 436 | 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 | 636 | return { |
| 440 | 637 | id: APPS_PLUGIN_ID as "apps", |
| 441 | 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 | 659 | id: APPS_PLUGIN_ID, |
| 463 | 660 | kind: "executor", |
| @@ -466,47 +663,18 @@ export const appsPlugin = definePlugin((options?: AppsPluginOptions) => { · 2 unmodified lines | ||
| 466 | 663 | tool<AppsStoreShape>({ |
| 467 | 664 | name: "sync_github_source", |
| 468 | 665 | description: "Sync a GitHub repository containing custom tool source files.", |
| 469 | − execute: (args, { ctx }) => | |
| 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 | − }), | |
| 666 | + execute: (args) => self.syncGitHubSource(args), | |
| 490 | 667 | }), |
| 491 | 668 | tool<AppsStoreShape>({ |
| 492 | 669 | name: "list_github_sources", |
| 493 | 670 | description: "List synced GitHub repositories that publish custom tools.", |
| 494 | − execute: (_args, { ctx }) => | |
| 495 | − Effect.gen(function* () { | |
| 496 | − const sources = yield* listSources(ctx); | |
| 497 | − return { sources }; | |
| 498 | − }), | |
| 671 | + execute: () => self.listGitHubSources(), | |
| 499 | 672 | }), |
| 500 | 673 | tool<AppsStoreShape>({ |
| 501 | 674 | name: "get_github_source", |
| 502 | 675 | description: "Read one synced GitHub custom-tools source.", |
| 503 | − execute: (args, { ctx }) => | |
| 504 | − Effect.gen(function* () { | |
| 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 | − }), | |
| 676 | + execute: (args) => | |
| 677 | + self.getGitHubSource(isRecord(args) ? (asString(args.slug) ?? "") : ""), | |
| 510 | 678 | }), |
| 511 | 679 | ], |
| 512 | 680 | }, |
| @@ -547,9 +715,7 @@ export const appsPlugin = definePlugin((options?: AppsPluginOptions) => { · 34 unmodified lines | ||
| 547 | 715 | const descriptor = yield* runtime.getDescriptor(tenant, source.scope); |
| 548 | 716 | const toolDesc = descriptor?.tools.find((t) => t.name === toolRow.name); |
| 549 | 717 | if (!toolDesc) return { inputSchema, outputSchema }; |
| 550 | − const resolver = makeResolver | |
| 551 | − ? makeResolver({ ctx, scope: source.scope, tool: toolRow.name }) | |
| 552 | − : runtime.deps.resolver; | |
| 718 | + const resolver = requestResolver({ ctx, scope: source.scope, tool: toolRow.name }); | |
| 553 | 719 | const byRole: Record<string, readonly ConnectionCandidate[]> = {}; |
| 554 | 720 | for (const [role, decl] of Object.entries(toolDesc.integrations)) { |
| 555 | 721 | byRole[role] = yield* resolver |
| @@ -589,9 +755,7 @@ export const appsPlugin = definePlugin((options?: AppsPluginOptions) => { · 33 unmodified lines | ||
| 589 | 755 | diagnostics: [], |
| 590 | 756 | }); |
| 591 | 757 | } |
| 592 | − const resolver = makeResolver | |
| 593 | − ? makeResolver({ ctx, scope: source.scope, tool: toolRow.name }) | |
| 594 | − : undefined; | |
| 758 | + const resolver = requestResolver({ ctx, scope: source.scope, tool: toolRow.name }); | |
| 595 | 759 | return yield* runtime.invokeTool({ |
| 596 | 760 | tenant, |
| 597 | 761 | scope: source.scope, |
| @@ -0,0 +1,28 @@ | ||
| 1 | +import type { ArtifactStore } from "../seams/artifact-store"; | |
| 2 | +import type { ScopeDb } from "../seams/scope-db"; | |
| 3 | +import type { ToolSandbox } from "../seams/tool-sandbox"; | |
| 4 | +import { makeAppsRuntime, type AppsRuntime } from "./runtime"; | |
| 5 | +import type { AppsStore } from "./store"; | |
| 6 | +import type { ClientResolver } from "./bindings"; | |
| 7 | + | |
| 8 | +export 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 | + | |
| 17 | +export 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 | + }); | |
| @@ -0,0 +1,49 @@ | ||
| 1 | +import { Context, Effect } from "effect"; | |
| 2 | +import { HttpApiBuilder } from "effect/unstable/httpapi"; | |
| 3 | + | |
| 4 | +import { addGroup, capture } from "@executor-js/api"; | |
| 5 | +import type { AppsPluginExtension } from "./apps-plugin"; | |
| 6 | +import { AppsGroup } from "./routes"; | |
| 7 | + | |
| 8 | +export class AppsExtensionService extends Context.Service< | |
| 9 | + AppsExtensionService, | |
| 10 | + AppsPluginExtension | |
| 11 | +>()("AppsExtensionService") {} | |
| 12 | + | |
| 13 | +const ExecutorApiWithApps = addGroup(AppsGroup); | |
| 14 | + | |
| 15 | +export 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 | +); | |
| @@ -9,11 +9,7 @@ import { · 8 unmodified lines | ||
| 9 | 9 | type Owner, |
| 10 | 10 | type PluginCtx, |
| 11 | 11 | } from "@executor-js/sdk"; |
| 12 | −import { | |
| 13 | − BindingError, | |
| 14 | − type ClientResolver, | |
| 15 | − type ConnectionCandidate, | |
| 16 | −} from "@executor-js/plugin-apps/api"; | |
| 12 | +import { BindingError, type ClientResolver, type ConnectionCandidate } from "./bindings"; | |
| 17 | 13 | |
| 18 | 14 | const parseConnectionAddress = (address: string): ConnectionRef | null => { |
| 19 | 15 | const parts = address.split("."); |
| @@ -40,7 +36,7 @@ const toCandidate = (connection: { · 20 unmodified lines | ||
| 40 | 36 | ...(connection.owner !== undefined ? { owner: String(connection.owner) } : {}), |
| 41 | 37 | }); |
| 42 | 38 | |
| 43 | −export const makeSelfHostAppsResolver = (input: { readonly ctx: PluginCtx }): ClientResolver => ({ | |
| 39 | +export const makePluginCtxAppsResolver = (input: { readonly ctx: PluginCtx }): ClientResolver => ({ | |
| 44 | 40 | listConnections: ({ integration }) => |
| 45 | 41 | input.ctx.connections.list({ integration: IntegrationSlug.make(integration) }).pipe( |
| 46 | 42 | Effect.map((connections) => connections.map(toCandidate)), |
| @@ -0,0 +1,104 @@ | ||
| 1 | +import { Schema } from "effect"; | |
| 2 | +import { HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi"; | |
| 3 | +import { IntegrationRemovalNotAllowedError, InternalError } from "@executor-js/sdk/shared"; | |
| 4 | + | |
| 5 | +const 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 | + | |
| 14 | +const SourceSlugParams = { | |
| 15 | + slug: Schema.String, | |
| 16 | +}; | |
| 17 | + | |
| 18 | +const SkippedArtifact = Schema.Struct({ | |
| 19 | + path: Schema.String, | |
| 20 | + reason: Schema.Literals(["not supported yet", "unsupported file type", "ignored"]), | |
| 21 | +}); | |
| 22 | + | |
| 23 | +const FileDiagnostic = Schema.Struct({ | |
| 24 | + path: Schema.String, | |
| 25 | + message: Schema.String, | |
| 26 | +}); | |
| 27 | + | |
| 28 | +const 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 | + | |
| 34 | +const 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 | + | |
| 43 | +const 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 | + | |
| 59 | +const GitHubSourcesResponse = Schema.Struct({ | |
| 60 | + sources: Schema.Array(GitHubSourceSummary), | |
| 61 | +}); | |
| 62 | + | |
| 63 | +const GitHubSourceResponse = Schema.Struct({ | |
| 64 | + source: Schema.NullOr(GitHubSourceSummary), | |
| 65 | +}); | |
| 66 | + | |
| 67 | +const RemoveSourceResponse = Schema.Struct({ | |
| 68 | + removed: Schema.Boolean, | |
| 69 | +}); | |
| 70 | + | |
| 71 | +const DomainErrors = [InternalError] as const; | |
| 72 | +const IntegrationRemovalNotAllowed = IntegrationRemovalNotAllowedError.annotate({ | |
| 73 | + httpApiStatus: 409, | |
| 74 | +}); | |
| 75 | +const RemoveSourceErrors = [InternalError, IntegrationRemovalNotAllowed] as const; | |
| 76 | + | |
| 77 | +export 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 | + ); | |
| @@ -6,9 +6,10 @@ import { makeGitArtifactStore } from "../backing/git-artifact-store"; · 5 unmodified lines | ||
| 6 | 6 | import { makeLibsqlScopeDb } from "../backing/libsql-scope-db"; |
| 7 | 7 | import { makeQuickjsToolSandbox } from "../backing/quickjs-tool-sandbox"; |
| 8 | 8 | import type { ScopeDb } from "../seams/scope-db"; |
| 9 | −import { makeAppsRuntime, type AppsRuntime } from "./runtime"; | |
| 9 | +import { type AppsRuntime } from "./runtime"; | |
| 10 | 10 | import type { AppsStore } from "./store"; |
| 11 | 11 | import type { ClientResolver } from "./bindings"; |
| 12 | +import { makeAppsRuntimeFromBackings, type AppsBackings } from "./backings"; | |
| 12 | 13 | |
| 13 | 14 | export interface SelfHostAppsRuntimeOptions { |
| 14 | 15 | /** Data dir root; `<root>/artifacts` and `<root>/scope-db`. */ |
| @@ -24,6 +25,7 @@ export interface SelfHostAppsRuntimeOptions { · 9 unmodified lines | ||
| 24 | 25 | |
| 25 | 26 | export interface SelfHostAppsRuntime { |
| 26 | 27 | readonly runtime: AppsRuntime; |
| 28 | + readonly backings: AppsBackings; | |
| 27 | 29 | readonly scopeDb: ScopeDb; |
| 28 | 30 | readonly close: () => Promise<void>; |
| 29 | 31 | } |
| @@ -39,17 +41,19 @@ export const makeSelfHostAppsRuntime = ( · 9 unmodified lines | ||
| 39 | 41 | root: inMem ? ":memory:" : join(options.dataDir, "scope-db"), |
| 40 | 42 | }); |
| 41 | 43 | const sandbox = makeQuickjsToolSandbox(); |
| 42 | − const runtime = makeAppsRuntime({ | |
| 44 | + const backings: AppsBackings = { | |
| 43 | 45 | artifactStore, |
| 44 | 46 | scopeDb, |
| 45 | 47 | sandbox, |
| 46 | 48 | store: options.store, |
| 47 | 49 | resolver: options.resolver, |
| 48 | 50 | defaultTenant: options.tenant, |
| 49 | − }); | |
| 51 | + }; | |
| 52 | + const runtime = makeAppsRuntimeFromBackings(backings, options.resolver); | |
| 50 | 53 | |
| 51 | 54 | return { |
| 52 | 55 | runtime, |
| 56 | + backings, | |
| 53 | 57 | scopeDb, |
| 54 | 58 | close: async () => { |
| 55 | 59 | await Effect.runPromise(scopeDb.close().pipe(Effect.orElseSucceed(() => undefined))); |
| @@ -12,7 +12,7 @@ import appsClientPlugin, { · 11 unmodified lines | ||
| 12 | 12 | syncCustomToolSource, |
| 13 | 13 | syncStatusLabel, |
| 14 | 14 | validateGitHubSourceUrl, |
| 15 | −} from "@executor-js/plugin-apps/client"; | |
| 15 | +} from "./plugin-client"; | |
| 16 | 16 | |
| 17 | 17 | const jsonResponse = (body: unknown, init?: ResponseInit): Response => |
| 18 | 18 | new Response(JSON.stringify(body), { |