import { Effect, Predicate } from "effect"; import type { InvokeOptions } from "@executor-js/sdk"; import type { ArtifactStore } from "../seams/artifact-store"; import { InputValidationError, OutputValidationError, type ToolSandbox, } from "../seams/tool-sandbox"; import { publish as runPublish, loadDescriptorFromSnapshot, type PublishOutput, } from "../pipeline/publish"; import { PublishError } from "../pipeline/discover"; import type { AppDescriptor, AppSourceRef, ToolDescriptor } from "../pipeline/descriptor"; import { bundleEntry } from "../pipeline/bundle"; import { buildBridge, rootsFor, resolveIntegrationBindings, type ClientResolver, BindingError, } from "./bindings"; import type { AppsStore } from "./store"; import { scopeAddress } from "../seams/scope-address"; import type { GitHubSkippedArtifact } from "../source/github-source"; // --------------------------------------------------------------------------- // AppsRuntime: the substrate-neutral core for published custom tools. // --------------------------------------------------------------------------- export interface AppsRuntimeDeps { readonly artifactStore: ArtifactStore; readonly sandbox: ToolSandbox; readonly store: AppsStore; /** Routes a bound integration method call to the real API (policy/audit). */ readonly resolver: ClientResolver; readonly defaultTenant?: string; } export interface GitHubCustomToolsSourceSummary { readonly slug: string; readonly name: string; readonly scope: string; readonly url: string; readonly repo: string; readonly ref: string; readonly hasToken: boolean; readonly upstreamSha: string; readonly snapshotId: string; readonly description?: string; readonly publishedAt: string; readonly tools: readonly string[]; readonly skipped: readonly GitHubSkippedArtifact[]; } export interface AppsRuntime { readonly publish: (input: { readonly tenant?: string; readonly scope: string; readonly files: ReadonlyMap; readonly message?: string; readonly description?: string; readonly source?: AppSourceRef; }) => Effect.Effect; readonly getDescriptor: ( tenantOrScope: string, scope?: string, ) => Effect.Effect; readonly listGitHubSources: ( tenant?: string, ) => Effect.Effect; readonly removeSource: (input: { readonly tenant?: string; readonly scope: string; }) => Effect.Effect; /** Re-derive the published-descriptor pointer from the latest committed * snapshot. */ readonly repair: ( tenantOrScope: string, scope?: string, ) => Effect.Effect; readonly invokeTool: (input: { readonly tenant?: string; readonly scope: string; readonly tool: string; readonly args: unknown; /** Optional per-request resolver override. The catalog invoke path supplies * one built from the request's executor context. */ readonly resolver?: ClientResolver; /** Optional caller options forwarded to bridged sub-calls. */ readonly invokeOptions?: InvokeOptions; }) => Effect.Effect< unknown, PublishError | BindingError | InputValidationError | OutputValidationError >; readonly deps: AppsRuntimeDeps; } const failNoDescriptor = (scope: string): PublishError => new PublishError({ message: `scope "${scope}" has no published app`, stage: "project", diagnostics: [], }); const isInvokePassthroughError = ( cause: unknown, ): cause is BindingError | InputValidationError | OutputValidationError => Predicate.isTagged("BindingError")(cause) || Predicate.isTagged("InputValidationError")(cause) || Predicate.isTagged("OutputValidationError")(cause); export const makeAppsRuntime = (deps: AppsRuntimeDeps): AppsRuntime => { const defaultTenant = deps.defaultTenant ?? "org"; const bundleCache = new Map(); const publishChains = new Map>(); const resolveTenant = (tenant?: string): string => tenant ?? defaultTenant; const resolveTenantScope = ( tenantOrScope: string, maybeScope?: string, ): { tenant: string; scope: string } => maybeScope === undefined ? { tenant: defaultTenant, scope: tenantOrScope } : { tenant: tenantOrScope, scope: maybeScope }; const withScopePublishLock = ( tenant: string, scope: string, run: () => Promise, ): Promise => { const key = `${tenant}:${scope}`; const prior = publishChains.get(key) ?? Promise.resolve(); const afterPrior = prior.then( () => undefined, () => undefined, ); const next = afterPrior.then(run); publishChains.set( key, next.then( () => undefined, () => undefined, ), ); return next; }; const bundleFor = ( descriptor: AppDescriptor, sourcePath: string, ): Effect.Effect => Effect.gen(function* () { const cacheKey = `${descriptor.snapshotId}:${sourcePath}`; const cached = bundleCache.get(cacheKey); if (cached) return cached; const scopeStore = yield* deps.artifactStore .forScope(scopeAddress(descriptor.tenant, descriptor.scope)) .pipe( Effect.mapError( (c) => new PublishError({ message: c.message, stage: "project", diagnostics: [], }), ), ); const files = yield* scopeStore.read(descriptor.snapshotId as never).pipe( Effect.mapError( (c) => new PublishError({ message: c.message, stage: "project", diagnostics: [], }), ), ); const bundle = yield* bundleEntry({ files, entry: sourcePath }).pipe( Effect.mapError( (c) => new PublishError({ message: c.message, stage: "bundle", diagnostics: [{ path: sourcePath, message: c.message }], }), ), ); bundleCache.set(cacheKey, bundle.code); return bundle.code; }); const recoverFromSnapshot = ( tenant: string, scope: string, ): Effect.Effect => Effect.gen(function* () { const scopeStore = yield* deps.artifactStore.forScope(scopeAddress(tenant, scope)).pipe( Effect.mapError( (c) => new PublishError({ message: c.message, stage: "project", diagnostics: [], }), ), ); const latest = yield* scopeStore.latest().pipe( Effect.mapError( (c) => new PublishError({ message: c.message, stage: "project", diagnostics: [], }), ), ); if (!latest) return null; return yield* loadDescriptorFromSnapshot(deps.artifactStore, tenant, scope, latest.id); }); const requireDescriptor = ( tenant: string, scope: string, ): Effect.Effect => deps.store.getDescriptor(tenant, scope).pipe( Effect.mapError( (c) => new PublishError({ message: String(c), stage: "project", diagnostics: [], }), ), Effect.flatMap((d) => d ? Effect.succeed(d) : recoverFromSnapshot(tenant, scope).pipe(Effect.orElseSucceed(() => null)), ), Effect.flatMap((d) => (d ? Effect.succeed(d) : Effect.fail(failNoDescriptor(scope)))), ); const putDescriptorPointer = ( tenant: string, descriptor: AppDescriptor, ): Effect.Effect => deps.store.putDescriptor(tenant, "org", descriptor).pipe( Effect.mapError( (c) => new PublishError({ message: String(c), stage: "project", diagnostics: [], }), ), ); const repairScope = ( tenant: string, scope: string, ): Effect.Effect => Effect.gen(function* () { const descriptor = yield* recoverFromSnapshot(tenant, scope); if (!descriptor) return null; yield* putDescriptorPointer(tenant, descriptor); return descriptor; }); const invokeToolInternal = ( scope: string, descriptor: AppDescriptor, toolDesc: ToolDescriptor, args: unknown, resolver?: ClientResolver, invokeOptions?: InvokeOptions, ): Effect.Effect< unknown, PublishError | BindingError | InputValidationError | OutputValidationError > => Effect.gen(function* () { const activeResolver = resolver ?? deps.resolver; const resolved = yield* resolveIntegrationBindings( toolDesc.integrations, args, activeResolver, ); const roots = rootsFor(toolDesc.integrations); const code = yield* bundleFor(descriptor, toolDesc.sourcePath); const bridge = buildBridge({ declared: toolDesc.integrations, bindings: resolved.bindings, resolver: activeResolver, invokeOptions, }); const result = yield* deps.sandbox .invoke( code, { artifact: toolDesc.name, kind: "tool", input: resolved.input, roots }, bridge, ) .pipe( Effect.mapError((c) => { if (isInvokePassthroughError(c)) { return c; } return new PublishError({ message: c.message, stage: "project", diagnostics: [{ path: toolDesc.sourcePath, message: c.message }], }); }), ); return result.output; }); return { deps, publish: (input) => Effect.flatMap( Effect.tryPromise({ try: () => { const tenant = resolveTenant(input.tenant); return withScopePublishLock(tenant, input.scope, () => Effect.runPromiseExit( Effect.gen(function* () { const out = yield* runPublish( { artifactStore: deps.artifactStore, sandbox: deps.sandbox, }, { tenant, scope: input.scope, files: input.files, commitMessage: input.message, description: input.description, source: input.source, }, ); yield* putDescriptorPointer(tenant, out.descriptor); return out; }), ), ); }, catch: (_cause) => new PublishError({ message: "publish failed before pipeline completed", stage: "project", diagnostics: [], }), }), (exit) => exit, ), getDescriptor: (tenantOrScope, maybeScope) => { const { tenant, scope } = resolveTenantScope(tenantOrScope, maybeScope); return deps.store.getDescriptor(tenant, scope).pipe( Effect.orElseSucceed(() => null), Effect.flatMap((pointer) => pointer ? Effect.succeed(pointer) : recoverFromSnapshot(tenant, scope).pipe(Effect.orElseSucceed(() => null)), ), ); }, listGitHubSources: (tenantInput) => { const tenant = resolveTenant(tenantInput); return deps.store.listDescriptors(tenant).pipe( Effect.orElseSucceed(() => []), Effect.flatMap((records) => { const githubRecords = records.flatMap((record) => { const source = record.descriptor.source; return source?.kind === "github" ? [{ record, source }] : []; }); return Effect.forEach(githubRecords, ({ record, source }) => { const { descriptor, publishedAt } = record; return Effect.succeed({ slug: descriptor.scope, name: source.repo.split("/").at(-1) ?? descriptor.scope, scope: descriptor.scope, url: source.url, repo: source.repo, ref: source.ref, hasToken: false, upstreamSha: source.upstreamSha, snapshotId: descriptor.snapshotId, ...(descriptor.description ? { description: descriptor.description } : {}), publishedAt: new Date(publishedAt).toISOString(), tools: descriptor.tools.map((tool) => tool.name), skipped: [ ...(source.skipped ?? []), ...(descriptor.skipped as readonly GitHubSkippedArtifact[]), ], } satisfies GitHubCustomToolsSourceSummary); }); }), ); }, repair: (tenantOrScope, maybeScope) => { const { tenant, scope } = resolveTenantScope(tenantOrScope, maybeScope); return repairScope(tenant, scope); }, removeSource: (input) => { const tenant = resolveTenant(input.tenant); const address = scopeAddress(tenant, input.scope); const toPublishError = (message: string) => (_cause: unknown) => new PublishError({ message, stage: "project", diagnostics: [], }); return Effect.gen(function* () { yield* deps.store .removeDescriptor(tenant, input.scope) .pipe(Effect.mapError(toPublishError("removeDescriptor failed"))); yield* deps.artifactStore .removeScope(address) .pipe(Effect.mapError(toPublishError("remove artifact scope failed"))); }); }, invokeTool: (input) => Effect.gen(function* () { const tenant = resolveTenant(input.tenant); const descriptor = yield* requireDescriptor(tenant, input.scope); const toolDesc = descriptor.tools.find((t) => t.name === input.tool); if (!toolDesc) { return yield* new PublishError({ message: `tool "${input.tool}" is not published in scope "${input.scope}"`, stage: "project", diagnostics: [], }); } return yield* invokeToolInternal( input.scope, descriptor, toolDesc, input.args, input.resolver, input.invokeOptions, ); }), }; };