322 lines · 9.9 KB · TypeScript
RawDownload
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<string, string>;    readonly message?: string;    readonly description?: string;    readonly source?: AppSourceRef;  }) => Effect.Effect<PublishOutput, PublishError>;  readonly getDescriptor: (scope: string) => Effect.Effect<AppDescriptor | null>;  /** Re-derive the published-descriptor pointer from the latest committed   *  snapshot. */  readonly repair: (scope: string) => Effect.Effect<AppDescriptor | null, PublishError>;  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<string, string>();  const publishChains = new Map<string, Promise<unknown>>();   const withScopePublishLock = <A>(scope: string, run: () => Promise<A>): Promise<A> => {    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<string, PublishError> =>    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<AppDescriptor | null, PublishError> =>    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<AppDescriptor, PublishError> =>    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<void, PublishError> =>    deps.store.putDescriptor("org", descriptor).pipe(      Effect.mapError(        (c) =>          new PublishError({            message: String(c),            stage: "project",            diagnostics: [],          }),      ),    );   const repairScope = (scope: string): Effect.Effect<AppDescriptor | null, PublishError> =>    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,        );      }),  };};