import { Effect } from "effect"; import type { ArtifactStore } from "../seams/artifact-store"; import type { ScopeDb } from "../seams/scope-db"; 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"; // --------------------------------------------------------------------------- // AppsRuntime: the substrate-neutral core for published custom tools. // --------------------------------------------------------------------------- export interface AppsRuntimeDeps { readonly artifactStore: ArtifactStore; readonly scopeDb: ScopeDb; readonly sandbox: ToolSandbox; readonly store: AppsStore; /** Routes a bound integration method call to the real API (policy/audit). */ readonly resolver: ClientResolver; } export interface AppsRuntime { readonly publish: (input: { readonly scope: string; readonly files: ReadonlyMap; readonly message?: string; readonly description?: string; readonly source?: AppSourceRef; }) => Effect.Effect; readonly getDescriptor: (scope: string) => Effect.Effect; /** Re-derive the published-descriptor pointer from the latest committed * snapshot. */ readonly repair: (scope: string) => Effect.Effect; readonly invokeTool: (input: { 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; }) => 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: [], }); export const makeAppsRuntime = (deps: AppsRuntimeDeps): AppsRuntime => { const bundleCache = new Map(); const publishChains = new Map>(); const withScopePublishLock = (scope: string, run: () => Promise): Promise => { const prior = publishChains.get(scope) ?? Promise.resolve(); const next = prior.catch(() => undefined).then(run); publishChains.set( scope, next.catch(() => 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(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 = (scope: string): Effect.Effect => Effect.gen(function* () { const scopeStore = yield* deps.artifactStore.forScope(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, scope, latest.id); }); const requireDescriptor = (scope: string): Effect.Effect => deps.store.getDescriptor(scope).pipe( Effect.mapError( (c) => new PublishError({ message: String(c), stage: "project", diagnostics: [], }), ), Effect.flatMap((d) => d ? Effect.succeed(d) : recoverFromSnapshot(scope).pipe(Effect.orElseSucceed(() => null)), ), Effect.flatMap((d) => (d ? Effect.succeed(d) : Effect.fail(failNoDescriptor(scope)))), ); const putDescriptorPointer = (descriptor: AppDescriptor): Effect.Effect => deps.store.putDescriptor("org", descriptor).pipe( Effect.mapError( (c) => new PublishError({ message: String(c), stage: "project", diagnostics: [], }), ), ); const repairScope = (scope: string): Effect.Effect => Effect.gen(function* () { const descriptor = yield* recoverFromSnapshot(scope); if (!descriptor) return null; yield* putDescriptorPointer(descriptor); return descriptor; }); const invokeToolInternal = ( scope: string, descriptor: AppDescriptor, toolDesc: ToolDescriptor, args: unknown, resolver?: ClientResolver, ): 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 db = yield* deps.scopeDb.forScope(scope).pipe( Effect.mapError( (c) => new PublishError({ message: c.message, stage: "project", diagnostics: [], }), ), ); const bridge = buildBridge({ declared: toolDesc.integrations, bindings: resolved.bindings, db, resolver: activeResolver, }); const result = yield* deps.sandbox .invoke( code, { artifact: toolDesc.name, kind: "tool", input: resolved.input, roots }, bridge, ) .pipe( Effect.mapError((c) => { if ( c instanceof BindingError || c instanceof InputValidationError || c instanceof OutputValidationError ) { 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: () => withScopePublishLock(input.scope, () => Effect.runPromiseExit( Effect.gen(function* () { const out = yield* runPublish( { artifactStore: deps.artifactStore, sandbox: deps.sandbox, }, { scope: input.scope, files: input.files, commitMessage: input.message, description: input.description, source: input.source, }, ); yield* putDescriptorPointer(out.descriptor); return out; }), ), ), catch: (cause) => new PublishError({ message: cause instanceof Error ? cause.message : String(cause), stage: "project", diagnostics: [], }), }), (exit) => exit, ), getDescriptor: (scope) => deps.store.getDescriptor(scope).pipe( Effect.orElseSucceed(() => null), Effect.flatMap((pointer) => pointer ? Effect.succeed(pointer) : recoverFromSnapshot(scope).pipe(Effect.orElseSucceed(() => null)), ), ), repair: (scope) => repairScope(scope), invokeTool: (input) => Effect.gen(function* () { const descriptor = yield* requireDescriptor(input.scope); const toolDesc = descriptor.tools.find((t) => t.name === input.tool); if (!toolDesc) { return yield* Effect.fail( 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, ); }), }; };