Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
279 changes: 279 additions & 0 deletions packages/core/sdk/src/executor-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
import type { Duration, Effect, Layer } from "effect";
import type { HttpClient } from "effect/unstable/http";

import type {
ConnectionRef,
ConnectionRefreshError,
CreateConnectionInput,
RemoveConnectionInput,
UpdateConnectionTokensInput,
} from "./connections";
import type { CredentialBindingsFacade } from "./credential-bindings";
import type { ElicitationDeclinedError, ElicitationHandler } from "./elicitation";
import type {
ConnectionInUseError,
ConnectionNotFoundError,
ConnectionProviderNotRegisteredError,
ConnectionReauthRequiredError,
ConnectionRefreshNotSupportedError,
NoHandlerError,
PluginNotLoadedError,
SecretInUseError,
SecretOwnedByConnectionError,
SourceRemovalNotAllowedError,
ToolBlockedError,
ToolInvocationError,
ToolNotFoundError,
} from "./errors";
import type { FumaDb, FumaTables, StorageFailure } from "./fuma-runtime";
import type { OAuthService } from "./oauth";
import type { OAuthEndpointUrlPolicy } from "./oauth-helpers";
import type {
CreateToolPolicyInput,
PolicyMatch,
RemoveToolPolicyInput,
ToolPolicy,
UpdateToolPolicyInput,
} from "./policies";
import type { AnyPlugin, PluginExtensions } from "./plugin";
import type { Scope } from "./scope";
import type { RemoveSecretInput, SecretRef, SetSecretInput } from "./secrets";
import type {
RefreshSourceInput,
RemoveSourceInput,
Source,
SourceDetectionResult,
Tool,
ToolListFilter,
ToolSchema,
} from "./types";
import type { Usage } from "./usages";

// ---------------------------------------------------------------------------
// Executor public types. Kept separate from createExecutor so the runtime
// module stays focused on wiring and lifecycle.
// ---------------------------------------------------------------------------

export type OnElicitation = ElicitationHandler | "accept-all";

export interface InvokeOptions {
/** Override the executor-level handler for this single call. */
readonly onElicitation?: OnElicitation;
}

// ---------------------------------------------------------------------------
// Executor — public surface. Every list/invoke/schema call is a direct
// core-table query (for dynamic rows) unioned with the in-memory static
// pool. No ToolRegistry, no SourceRegistry, no SecretStore services.
// ---------------------------------------------------------------------------

export type Executor<TPlugins extends readonly AnyPlugin[] = readonly []> = {
/**
* Precedence-ordered scope stack this executor was configured with.
* Innermost first. Consumers that need "the display scope" typically
* pick `scopes.at(-1)` (outermost, e.g. the organization) or
* `scopes[0]` (innermost, e.g. the current user-in-org) depending on
* what they're rendering.
*/
readonly scopes: readonly Scope[];

readonly tools: {
readonly list: (filter?: ToolListFilter) => Effect.Effect<readonly Tool[], StorageFailure>;
/** Fetch a tool's full schema view: JSON schemas with `$defs`
* attached from the core `definition` table, plus TypeScript
* preview strings rendered from them. Returns `null` for unknown
* tool ids. */
readonly schema: (toolId: string) => Effect.Effect<ToolSchema | null, StorageFailure>;
/** Every `$defs` entry across every source, grouped by source id.
* Used for bulk schema export and downstream TypeScript rendering. */
readonly definitions: () => Effect.Effect<
Record<string, Record<string, unknown>>,
StorageFailure
>;
readonly invoke: (
toolId: string,
args: unknown,
options?: InvokeOptions,
) => Effect.Effect<
unknown,
| ToolNotFoundError
| ToolBlockedError
| PluginNotLoadedError
| NoHandlerError
| ToolInvocationError
| ElicitationDeclinedError
| StorageFailure
>;
};

readonly sources: {
readonly list: () => Effect.Effect<readonly Source[], StorageFailure>;
readonly remove: (
input: RemoveSourceInput,
) => Effect.Effect<void, SourceRemovalNotAllowedError | StorageFailure>;
readonly refresh: (input: RefreshSourceInput) => Effect.Effect<void, StorageFailure>;
/** URL autodetection — fans out to every plugin's `detect` hook
* (if declared), returns every high/medium/low-confidence match.
* UI picks a winner from the list. */
readonly detect: (
url: string,
) => Effect.Effect<readonly SourceDetectionResult[], StorageFailure>;
/** All `$defs` registered for a single source, keyed by def name. */
readonly definitions: (
sourceId: string,
) => Effect.Effect<Record<string, unknown>, StorageFailure>;
};

readonly secrets: {
readonly get: (
id: string,
) => Effect.Effect<string | null, SecretOwnedByConnectionError | StorageFailure>;
readonly getAtScope: (
id: string,
scope: string,
) => Effect.Effect<string | null, SecretOwnedByConnectionError | StorageFailure>;
/** Fast-path existence check — hits the core `secret` routing table
* only, never calls the provider. Use this for UI state ("secret
* missing, prompt to add") to avoid keychain permission prompts
* or 1password IPC roundtrips on a pre-flight check. */
readonly status: (id: string) => Effect.Effect<"resolved" | "missing", StorageFailure>;
readonly set: (input: SetSecretInput) => Effect.Effect<SecretRef, StorageFailure>;
/** Delete a bare (non-connection-owned) secret. Connection-owned
* secrets are rejected with `SecretOwnedByConnectionError` — use
* `connections.remove` instead. Refuses with `SecretInUseError`
* if any plugin reports the secret as in use; the caller should
* show the `usages(id)` list and ask the user to detach first. */
readonly remove: (
input: RemoveSecretInput,
) => Effect.Effect<void, SecretOwnedByConnectionError | SecretInUseError | StorageFailure>;
readonly list: () => Effect.Effect<readonly SecretRef[], StorageFailure>;
/** Management view of visible secret rows. Unlike `list`, this does
* not collapse same-id rows across scopes, so UI that writes exact
* credential targets can show both personal and shared rows. */
readonly listAll: () => Effect.Effect<readonly SecretRef[], StorageFailure>;
/** All places this secret is referenced — fans out across every
* plugin's `usagesForSecret`. Used by the Secrets-tab "Used by"
* list and by `remove` for its RESTRICT check. */
readonly usages: (id: string) => Effect.Effect<readonly Usage[], StorageFailure>;
readonly providers: () => Effect.Effect<readonly string[]>;
};

readonly connections: {
readonly get: (id: string) => Effect.Effect<ConnectionRef | null, StorageFailure>;
readonly getAtScope: (
id: string,
scope: string,
) => Effect.Effect<ConnectionRef | null, StorageFailure>;
readonly list: () => Effect.Effect<readonly ConnectionRef[], StorageFailure>;
readonly create: (
input: CreateConnectionInput,
) => Effect.Effect<ConnectionRef, ConnectionProviderNotRegisteredError | StorageFailure>;
readonly updateTokens: (
input: UpdateConnectionTokensInput,
) => Effect.Effect<ConnectionRef, ConnectionNotFoundError | StorageFailure>;
readonly setIdentityLabel: (
id: string,
label: string | null,
) => Effect.Effect<void, ConnectionNotFoundError | StorageFailure>;
readonly accessToken: (
id: string,
) => Effect.Effect<
string,
| ConnectionNotFoundError
| ConnectionProviderNotRegisteredError
| ConnectionRefreshNotSupportedError
| ConnectionReauthRequiredError
| ConnectionRefreshError
| StorageFailure
>;
readonly accessTokenAtScope: (
id: string,
scope: string,
) => Effect.Effect<
string,
| ConnectionNotFoundError
| ConnectionProviderNotRegisteredError
| ConnectionRefreshNotSupportedError
| ConnectionReauthRequiredError
| ConnectionRefreshError
| StorageFailure
>;
/** Refuses with `ConnectionInUseError` if any plugin reports the
* connection as in use. */
readonly remove: (
input: RemoveConnectionInput,
) => Effect.Effect<void, ConnectionInUseError | StorageFailure>;
/** All places this connection is referenced — fans out across every
* plugin's `usagesForConnection`. */
readonly usages: (id: string) => Effect.Effect<readonly Usage[], StorageFailure>;
readonly providers: () => Effect.Effect<readonly string[]>;
};

/** Shared credential slot bindings. Plugins decide what slot keys mean;
* core owns scoped storage, resolution status, and usage visibility. */
readonly credentialBindings: CredentialBindingsFacade;

/** Shared OAuth service. Hosts use this through the core HTTP OAuth group;
* plugins see the same service as `ctx.oauth`. */
readonly oauth: OAuthService;

readonly policies: {
/** All policies visible across the executor's scope stack, sorted
* by (innermost-scope-first, position ascending) — i.e. the order
* in which they're evaluated by first-match-wins. */
readonly list: () => Effect.Effect<readonly ToolPolicy[], StorageFailure>;
/** Create a new policy. Defaults to the top of the target scope's
* list (highest precedence) when `position` is omitted. */
readonly create: (input: CreateToolPolicyInput) => Effect.Effect<ToolPolicy, StorageFailure>;
readonly update: (input: UpdateToolPolicyInput) => Effect.Effect<ToolPolicy, StorageFailure>;
readonly remove: (input: RemoveToolPolicyInput) => Effect.Effect<void, StorageFailure>;
/** Resolve the effective policy for a tool id by walking the scope-
* stacked policy list with first-match-wins semantics. Returns
* `undefined` when no rule matches (caller falls back to the
* plugin's `resolveAnnotations` output). */
readonly resolve: (toolId: string) => Effect.Effect<PolicyMatch | undefined, StorageFailure>;
};

readonly close: () => Effect.Effect<void, StorageFailure>;
} & PluginExtensions<TPlugins>;

export interface ExecutorDb {
readonly db: FumaDb<any>;
readonly close?: () => Effect.Effect<void, StorageFailure> | Promise<void> | void;
}

export type ExecutorDbInput = FumaDb<any> | ExecutorDb;

export type ExecutorDbFactory = (config: {
readonly tables: FumaTables;
}) => ExecutorDbInput | Effect.Effect<ExecutorDbInput, StorageFailure>;

export interface ExecutorConfig<TPlugins extends readonly AnyPlugin[] = readonly []> {
/**
* Precedence-ordered scope stack. Innermost first; typical shape is
* `[userInOrgScope, orgScope]`. Reads on scoped tables walk the
* stack (first hit wins for shadow-by-id consumers like secrets and
* blobs); writes require callers to name an explicit target scope.
* Must be non-empty.
*/
readonly scopes: readonly Scope[];
readonly db?: ExecutorDbInput | ExecutorDbFactory;
readonly plugins?: TPlugins;
/**
* How to respond when a tool requests user input mid-invocation. Pass
* `"accept-all"` for tests / non-interactive hosts, or a handler
* `(ctx) => Effect<ElicitationResponse>` for interactive ones.
* Required at construction so per-invoke calls don't have to thread
* an options arg.
*/
readonly onElicitation: OnElicitation;
readonly httpClientLayer?: Layer.Layer<HttpClient.HttpClient>;
readonly oauthEndpointUrlPolicy?: OAuthEndpointUrlPolicy;
readonly sourceDetection?: {
readonly maxUrlLength?: number;
readonly maxDetectors?: number;
readonly maxResults?: number;
readonly timeout?: Duration.Input;
readonly hostedOutboundPolicy?: boolean;
};
}
Loading
Loading