| @@ -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 | |
| 20 | import type { IntegrationDecl, ToolDescriptor } from "../pipeline/descriptor"; | 23 | import type { IntegrationDecl, ToolDescriptor } from "../pipeline/descriptor"; |
| 21 | import { PublishError } from "../pipeline/discover"; | 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 | import { | 28 | import { |
| 23 | parseGitHubSourceUrl, | 29 | parseGitHubSourceUrl, |
| 24 | syncGitHubSource, | 30 | syncGitHubSource, |
| @@ -28,7 +34,9 @@ import { · 3 unmodified lines |
| 28 | import { slugifyCustomToolsAppName, validateCustomToolsAppSlug } from "../source/app-slug"; | 34 | import { slugifyCustomToolsAppName, validateCustomToolsAppSlug } from "../source/app-slug"; |
| 29 | import type { AppsRuntime, GitHubCustomToolsSourceSummary } from "./runtime"; | 35 | import type { AppsRuntime, GitHubCustomToolsSourceSummary } from "./runtime"; |
| 30 | import { makeAppsStore, type AppDescriptorRecord, type GitHubSourceTokenRef } from "./store"; | 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 | export const APPS_INTEGRATION_SLUG = "apps"; | 41 | export const APPS_INTEGRATION_SLUG = "apps"; |
| 34 | export const APPS_PLUGIN_ID = "apps"; | 42 | export 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 | |
| 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 | 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 | |
| 56 | interface AppsStoreShape { | 77 | export 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 | |
| 60 | const isRecord = (value: unknown): value is Record<string, unknown> => | 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 | const unique = (values: readonly string[]): readonly string[] => [...new Set(values)]; | 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 | const syncFailure = (message: string, path?: string): GitHubSyncResult => ({ | 109 | const 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 | |
| | 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 | const sourceTokenItemId = (tenant: string, scope: string): ProviderItemId => | 214 | const 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 | |
| 162 | export const appsPlugin = definePlugin((options?: AppsPluginOptions) => { | 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 | 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 = makeResolver | 718 | 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* resolver | 721 | 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 = makeResolver | 758 | 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, |