Showing the changes to packages/plugins/apps/src/plugin (2 of 19 files). Show all
Unified · Split
| @@ -4,10 +4,12 @@ import { · 3 unmodified lines | |||
| 4 | definePlugin, | 4 | definePlugin, |
| 5 | tool, | 5 | tool, |
| 6 | ToolName, | 6 | ToolName, |
| 7 | ConnectionName, | ||
| 8 | IntegrationSlug, | 7 | IntegrationSlug, |
| 9 | AuthTemplateSlug, | 8 | AuthTemplateSlug, |
| 10 | connectionIdentifier, | 9 | connectionIdentifier, |
| 10 | type ConnectionRef, | ||
| 11 | type Owner, | ||
| 12 | type PluginCtx, | ||
| 11 | type ResolveToolsInput, | 13 | type ResolveToolsInput, |
| 12 | type ResolveToolsResult, | 14 | type ResolveToolsResult, |
| 13 | type InvokeToolInput, | 15 | type InvokeToolInput, |
| @@ -18,6 +20,7 @@ import type { AppsRuntime } from "./runtime"; · 4 unmodified lines | |||
| 18 | import { makeAppsStore } from "./store"; | 20 | import { makeAppsStore } from "./store"; |
| 19 | import type { ClientResolver, ConnectionCandidate } from "./bindings"; | 21 | import type { ClientResolver, ConnectionCandidate } from "./bindings"; |
| 20 | import type { IntegrationDecl, ToolDescriptor } from "../pipeline/descriptor"; | 22 | import type { IntegrationDecl, ToolDescriptor } from "../pipeline/descriptor"; |
| 23 | import { syncGitHubSource, type GitHubSyncResult } from "../source/github-source"; | ||
| 21 | 24 | ||
| 22 | export const APPS_INTEGRATION_SLUG = "apps"; | 25 | export const APPS_INTEGRATION_SLUG = "apps"; |
| 23 | export const APPS_PLUGIN_ID = "apps"; | 26 | export const APPS_PLUGIN_ID = "apps"; |
| @@ -42,6 +45,40 @@ const isRecord = (value: unknown): value is Record<string, unknown> => · 18 unmodified lines | |||
| 42 | 45 | ||
| 43 | const unique = (values: readonly string[]): readonly string[] => [...new Set(values)]; | 46 | const unique = (values: readonly string[]): readonly string[] => [...new Set(values)]; |
| 44 | 47 | ||
| 48 | const syncFailure = (message: string, path?: string): GitHubSyncResult => ({ | ||
| 49 | status: "failed", | ||
| 50 | tools: [], | ||
| 51 | skipped: [], | ||
| 52 | errors: [ | ||
| 53 | { | ||
| 54 | stage: "source", | ||
| 55 | message, | ||
| 56 | ...(path ? { diagnostics: [{ path, message }] } : {}), | ||
| 57 | }, | ||
| 58 | ], | ||
| 59 | }); | ||
| 60 | |||
| 61 | const sourceScopeFor = (input: { repo: string; connection: string }): string => | ||
| 62 | String(connectionIdentifier(`github ${input.connection} ${input.repo}`, "githubSource")); | ||
| 63 | |||
| 64 | const parseConnectionAddress = (address: string): ConnectionRef | null => { | ||
| 65 | const parts = address.split("."); | ||
| 66 | if (parts.length !== 4 || parts[0] !== "tools") return null; | ||
| 67 | const [, integration, owner, name] = parts; | ||
| 68 | if (!integration || !name) return null; | ||
| 69 | if (owner !== "org" && owner !== "user") return null; | ||
| 70 | return { | ||
| 71 | owner: owner as Owner, | ||
| 72 | integration: IntegrationSlug.make(integration), | ||
| 73 | name: connectionIdentifier(name), | ||
| 74 | }; | ||
| 75 | }; | ||
| 76 | |||
| 77 | const configBaseUrl = (config: unknown): string | undefined => | ||
| 78 | isRecord(config) && typeof config.baseUrl === "string" && config.baseUrl.length > 0 | ||
| 79 | ? config.baseUrl | ||
| 80 | : undefined; | ||
| 81 | |||
| 45 | const projectInputSchema = ( | 82 | const projectInputSchema = ( |
| 46 | schema: unknown, | 83 | schema: unknown, |
| 47 | integrations: Readonly<Record<string, IntegrationDecl>>, | 84 | integrations: Readonly<Record<string, IntegrationDecl>>, |
| @@ -94,6 +131,49 @@ export const appsPlugin = definePlugin((options?: AppsPluginOptions) => { · 46 unmodified lines | |||
| 94 | Effect.map((mapped) => mapped ?? scopeFromConnection(connectionName)), | 131 | Effect.map((mapped) => mapped ?? scopeFromConnection(connectionName)), |
| 95 | ); | 132 | ); |
| 96 | 133 | ||
| 134 | const ensureCatalogConnection = (scope: string, ctx: PluginCtx<AppsStoreShape>) => | ||
| 135 | Effect.gen(function* () { | ||
| 136 | const slug = IntegrationSlug.make(APPS_INTEGRATION_SLUG); | ||
| 137 | const existing = yield* ctx.core.integrations | ||
| 138 | .get(slug) | ||
| 139 | .pipe(Effect.orElseSucceed(() => null)); | ||
| 140 | if (!existing) { | ||
| 141 | yield* ctx.core.integrations.register({ | ||
| 142 | slug, | ||
| 143 | name: "Apps", | ||
| 144 | description: "User-authored, published custom tools.", | ||
| 145 | config: {}, | ||
| 146 | canRemove: false, | ||
| 147 | canRefresh: true, | ||
| 148 | }); | ||
| 149 | } | ||
| 150 | const rawName = connectionNameForScope(scope); | ||
| 151 | const connName = connectionIdentifier(rawName); | ||
| 152 | const conns = yield* ctx.connections | ||
| 153 | .list({ integration: slug }) | ||
| 154 | .pipe(Effect.orElseSucceed(() => [])); | ||
| 155 | if (!conns.some((c) => String(c.name) === String(connName))) { | ||
| 156 | yield* ctx.connections.create({ | ||
| 157 | owner: "user", | ||
| 158 | name: connName, | ||
| 159 | integration: slug, | ||
| 160 | template: AuthTemplateSlug.make("none"), | ||
| 161 | value: "", | ||
| 162 | }); | ||
| 163 | } | ||
| 164 | yield* runtime.deps.store | ||
| 165 | .putScopeForConnection(String(rawName), scope) | ||
| 166 | .pipe(Effect.orElseSucceed(() => undefined)); | ||
| 167 | yield* runtime.deps.store | ||
| 168 | .putScopeForConnection(String(connName), scope) | ||
| 169 | .pipe(Effect.orElseSucceed(() => undefined)); | ||
| 170 | return { | ||
| 171 | scope, | ||
| 172 | connection: String(connName), | ||
| 173 | ref: { owner: "user" as const, integration: slug, name: connName }, | ||
| 174 | }; | ||
| 175 | }); | ||
| 176 | |||
| 97 | return { | 177 | return { |
| 98 | id: APPS_PLUGIN_ID as "apps", | 178 | id: APPS_PLUGIN_ID as "apps", |
| 99 | packageName: "@executor-js/plugin-apps", | 179 | packageName: "@executor-js/plugin-apps", |
| @@ -135,59 +215,78 @@ export const appsPlugin = definePlugin((options?: AppsPluginOptions) => { · 35 unmodified lines | |||
| 135 | Effect.gen(function* () { | 215 | Effect.gen(function* () { |
| 136 | const scope = | 216 | const scope = |
| 137 | (args as { scope?: string } | undefined)?.scope ?? DEFAULT_CATALOG_SCOPE; | 217 | (args as { scope?: string } | undefined)?.scope ?? DEFAULT_CATALOG_SCOPE; |
| 138 | const slug = IntegrationSlug.make(APPS_INTEGRATION_SLUG); | 218 | const connected = yield* ensureCatalogConnection(scope, ctx); |
| 139 | const existing = yield* ctx.core.integrations | 219 | return { scope, connection: connected.connection }; |
| 140 | .get(slug) | 220 | }), |
| 141 | .pipe(Effect.orElseSucceed(() => null)); | 221 | }), |
| 142 | if (!existing) { | 222 | tool<AppsStoreShape>({ |
| 143 | yield* ctx.core.integrations.register({ | 223 | name: "sync_github_source", |
| 144 | slug, | 224 | description: "Sync a GitHub repository containing custom tool source files.", |
| 145 | name: "Apps", | 225 | execute: (args, { ctx }) => |
| 146 | description: "User-authored, published custom tools.", | 226 | Effect.gen(function* () { |
| 147 | config: {}, | 227 | const payload = isRecord(args) ? args : {}; |
| 148 | canRemove: false, | 228 | const repo = typeof payload.repo === "string" ? payload.repo : ""; |
| 149 | canRefresh: true, | 229 | const connection = typeof payload.connection === "string" ? payload.connection : ""; |
| 150 | }); | 230 | const ref = typeof payload.ref === "string" ? payload.ref : undefined; |
| 231 | const explicitScope = typeof payload.scope === "string" ? payload.scope : undefined; | ||
| 232 | if (!repo) return syncFailure('sync_github_source requires "repo"'); | ||
| 233 | if (!connection) return syncFailure('sync_github_source requires "connection"'); | ||
| 234 | |||
| 235 | const sourceConnection = parseConnectionAddress(connection); | ||
| 236 | if (!sourceConnection) { | ||
| 237 | return syncFailure( | ||
| 238 | `GitHub connection must be a connection address like tools.github.user.main; got "${connection}"`, | ||
| 239 | connection, | ||
| 240 | ); | ||
| 151 | } | 241 | } |
| 152 | const connName = ConnectionName.make(connectionNameForScope(scope)); | 242 | if (String(sourceConnection.integration) !== "github") { |
| 153 | const conns = yield* ctx.connections | 243 | return syncFailure( |
| 154 | .list({ integration: slug }) | 244 | `GitHub source sync requires a github connection; got "${sourceConnection.integration}"`, |
| 155 | .pipe(Effect.orElseSucceed(() => [])); | 245 | connection, |
| 156 | if (!conns.some((c) => String(c.name) === String(connName))) { | 246 | ); |
| 157 | yield* ctx.connections.create({ | ||
| 158 | owner: "user", | ||
| 159 | name: connName, | ||
| 160 | integration: slug, | ||
| 161 | template: AuthTemplateSlug.make("none"), | ||
| 162 | value: "", | ||
| 163 | }); | ||
| 164 | } | 247 | } |
| 165 | yield* runtime.deps.store | 248 | const existing = yield* ctx.connections |
| 166 | .putScopeForConnection(String(connName), scope) | 249 | .get(sourceConnection) |
| 167 | .pipe(Effect.orElseSucceed(() => undefined)); | 250 | .pipe(Effect.orElseSucceed(() => null)); |
| 168 | const normalized = connectionIdentifier(String(connName)); | 251 | if (!existing) return syncFailure(`GitHub connection not found: ${connection}`); |
| 169 | if (normalized !== String(connName)) { | 252 | const token = yield* ctx.connections |
| 170 | yield* runtime.deps.store | 253 | .resolveValue(sourceConnection) |
| 171 | .putScopeForConnection(normalized, scope) | 254 | .pipe(Effect.orElseSucceed(() => null)); |
| 172 | .pipe(Effect.orElseSucceed(() => undefined)); | 255 | const integration = yield* ctx.core.integrations |
| 256 | .get(sourceConnection.integration) | ||
| 257 | .pipe(Effect.orElseSucceed(() => null)); | ||
| 258 | const scope = explicitScope ?? sourceScopeFor({ repo, connection }); | ||
| 259 | const result = yield* syncGitHubSource({ | ||
| 260 | runtime, | ||
| 261 | scope, | ||
| 262 | repo, | ||
| 263 | ref, | ||
| 264 | token, | ||
| 265 | baseUrl: configBaseUrl(integration?.config), | ||
| 266 | }); | ||
| 267 | if (result.status === "published") { | ||
| 268 | const catalog = yield* ensureCatalogConnection(scope, ctx); | ||
| 269 | yield* ctx.connections.refresh(catalog.ref).pipe(Effect.orElseSucceed(() => [])); | ||
| 173 | } | 270 | } |
| 174 | return { scope, connection: String(connName) }; | 271 | return result; |
| 175 | }), | 272 | }), |
| 176 | }), | 273 | }), |
| 177 | ], | 274 | ], |
| 178 | }, | 275 | }, |
| 179 | ], | 276 | ], |
| 180 | 277 | ||
| 181 | resolveTools: ({ connection }: ResolveToolsInput<AppsStoreShape>) => | 278 | resolveTools: ({ ctx, connection }: ResolveToolsInput<AppsStoreShape>) => |
| 182 | Effect.gen(function* () { | 279 | Effect.gen(function* () { |
| 183 | const scope = yield* scopeFor(String(connection.name)); | 280 | const scope = yield* scopeFor(String(connection.name)); |
| 184 | const descriptor = yield* runtime.getDescriptor(scope); | 281 | const descriptor = yield* runtime.getDescriptor(scope); |
| 185 | if (!descriptor) return { tools: [] } satisfies ResolveToolsResult; | 282 | if (!descriptor) return { tools: [] } satisfies ResolveToolsResult; |
| 283 | const resolver = | ||
| 284 | makeResolver && ctx ? makeResolver({ ctx, scope, tool: "*" }) : runtime.deps.resolver; | ||
| 186 | const tools: ToolDef[] = []; | 285 | const tools: ToolDef[] = []; |
| 187 | for (const t of descriptor.tools) { | 286 | for (const t of descriptor.tools) { |
| 188 | const byRole: Record<string, readonly ConnectionCandidate[]> = {}; | 287 | const byRole: Record<string, readonly ConnectionCandidate[]> = {}; |
| 189 | for (const [role, decl] of Object.entries(t.integrations)) { | 288 | for (const [role, decl] of Object.entries(t.integrations)) { |
| 190 | byRole[role] = yield* runtime.deps.resolver | 289 | byRole[role] = yield* resolver |
| 191 | .listConnections({ integration: decl.integration }) | 290 | .listConnections({ integration: decl.integration }) |
| 192 | .pipe(Effect.orElseSucceed(() => [])); | 291 | .pipe(Effect.orElseSucceed(() => [])); |
| 193 | } | 292 | } |
| @@ -13,7 +13,7 @@ import { · 12 unmodified lines | |||
| 13 | type PublishOutput, | 13 | type PublishOutput, |
| 14 | } from "../pipeline/publish"; | 14 | } from "../pipeline/publish"; |
| 15 | import { PublishError } from "../pipeline/discover"; | 15 | import { PublishError } from "../pipeline/discover"; |
| 16 | import type { AppDescriptor, ToolDescriptor } from "../pipeline/descriptor"; | 16 | import type { AppDescriptor, AppSourceRef, ToolDescriptor } from "../pipeline/descriptor"; |
| 17 | import { bundleEntry } from "../pipeline/bundle"; | 17 | import { bundleEntry } from "../pipeline/bundle"; |
| 18 | import { | 18 | import { |
| 19 | buildBridge, | 19 | buildBridge, |
| @@ -42,6 +42,8 @@ export interface AppsRuntime { · 22 unmodified lines | |||
| 42 | readonly scope: string; | 42 | readonly scope: string; |
| 43 | readonly files: ReadonlyMap<string, string>; | 43 | readonly files: ReadonlyMap<string, string>; |
| 44 | readonly message?: string; | 44 | readonly message?: string; |
| 45 | readonly description?: string; | ||
| 46 | readonly source?: AppSourceRef; | ||
| 45 | }) => Effect.Effect<PublishOutput, PublishError>; | 47 | }) => Effect.Effect<PublishOutput, PublishError>; |
| 46 | readonly getDescriptor: (scope: string) => Effect.Effect<AppDescriptor | null>; | 48 | readonly getDescriptor: (scope: string) => Effect.Effect<AppDescriptor | null>; |
| 47 | /** Re-derive the published-descriptor pointer from the latest committed | 49 | /** Re-derive the published-descriptor pointer from the latest committed |
| @@ -264,6 +266,8 @@ export const makeAppsRuntime = (deps: AppsRuntimeDeps): AppsRuntime => { · 216 unmodified lines | |||
| 264 | scope: input.scope, | 266 | scope: input.scope, |
| 265 | files: input.files, | 267 | files: input.files, |
| 266 | commitMessage: input.message, | 268 | commitMessage: input.message, |
| 269 | description: input.description, | ||
| 270 | source: input.source, | ||
| 267 | }, | 271 | }, |
| 268 | ); | 272 | ); |
| 269 | yield* putDescriptorPointer(out.descriptor); | 273 | yield* putDescriptorPointer(out.descriptor); |