449 lines · 14.2 KB · TypeScript
RawDownload
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<string, string>;    readonly message?: string;    readonly description?: string;    readonly source?: AppSourceRef;  }) => Effect.Effect<PublishOutput, PublishError>;  readonly getDescriptor: (    tenantOrScope: string,    scope?: string,  ) => Effect.Effect<AppDescriptor | null>;  readonly listGitHubSources: (    tenant?: string,  ) => Effect.Effect<readonly GitHubCustomToolsSourceSummary[]>;  readonly removeSource: (input: {    readonly tenant?: string;    readonly scope: string;  }) => Effect.Effect<void, PublishError>;  /** Re-derive the published-descriptor pointer from the latest committed   *  snapshot. */  readonly repair: (    tenantOrScope: string,    scope?: string,  ) => Effect.Effect<AppDescriptor | null, PublishError>;  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<string, string>();  const publishChains = new Map<string, Promise<unknown>>();  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 = <A>(    tenant: string,    scope: string,    run: () => Promise<A>,  ): Promise<A> => {    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<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(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<AppDescriptor | null, PublishError> =>    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<AppDescriptor, PublishError> =>    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<void, PublishError> =>    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<AppDescriptor | null, PublishError> =>    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,        );      }),  };};