import { Schema } from "effect"; import { ElicitationDeclinedError } from "./elicitation";import type { StorageFailure } from "./fuma-runtime";import { ArtifactId, ConnectionName, IntegrationSlug, Owner, ProviderKey, ToolAddress,} from "./ids"; export interface UserActionableError { readonly __executorUserActionable: true; readonly userMessage: string; readonly code: string;} export const isUserActionableError = (value: unknown): value is UserActionableError => typeof value === "object" && value !== null && "__executorUserActionable" in value && value.__executorUserActionable === true && "userMessage" in value && typeof value.userMessage === "string" && value.userMessage.length > 0 && "code" in value && typeof value.code === "string" && value.code.length > 0; /* The failure set the SDK surfaces. `execute`'s invoke failures are ported from * v1 but re-keyed by `address` (the full `tools.<integration>.<owner>.<connection>.<tool>` * handle) instead of an opaque tool id. Storage failures reuse fuma-runtime's * `StorageError`/`UniqueViolationError` (`StorageFailure`) — not redefined here. */ // ---------------------------------------------------------------------------// Tool lifecycle// --------------------------------------------------------------------------- /* Tagged errors without an explicit `message` field define a `message` getter: * `Schema.TaggedErrorClass` instances are real Errors with `message: ""`, and * an empty message propagates everywhere errors are rendered — span * status.message in the tracer, Cause.pretty output, log lines — leaving the * failure unlabeled in telemetry. The getter is derived from the schema fields * (not an own property), so encoding/serialization is unaffected. */ export class ToolNotFoundError extends Schema.TaggedErrorClass<ToolNotFoundError>()( "ToolNotFoundError", { address: ToolAddress, suggestions: Schema.optional(Schema.Array(ToolAddress)), },) { override get message(): string { return `Tool not found: ${this.address}`; }} export class ToolInvocationError extends Schema.TaggedErrorClass<ToolInvocationError>()( "ToolInvocationError", { address: ToolAddress, message: Schema.String, cause: Schema.optional(Schema.Unknown), },) {} /** Tool invocation was rejected because a workspace `tool_policy` rule with * `action: "block"` matched. `pattern` is the matched policy pattern. */export class ToolBlockedError extends Schema.TaggedErrorClass<ToolBlockedError>()( "ToolBlockedError", { address: ToolAddress, pattern: Schema.String, },) { override get message(): string { return `Tool blocked by policy "${this.pattern}": ${this.address}`; }} /** Tool row exists but its owning plugin isn't loaded in this executor config. */export class PluginNotLoadedError extends Schema.TaggedErrorClass<PluginNotLoadedError>()( "PluginNotLoadedError", { address: ToolAddress, pluginId: Schema.String, },) { override get message(): string { return `Plugin "${this.pluginId}" is not loaded for tool: ${this.address}`; }} /** Tool was found but its owning plugin has no `invokeTool` handler. */export class NoHandlerError extends Schema.TaggedErrorClass<NoHandlerError>()("NoHandlerError", { address: ToolAddress, pluginId: Schema.String,}) { override get message(): string { return `Plugin "${this.pluginId}" has no invokeTool handler for tool: ${this.address}`; }} // ---------------------------------------------------------------------------// Integration / connection lifecycle// --------------------------------------------------------------------------- export class IntegrationNotFoundError extends Schema.TaggedErrorClass<IntegrationNotFoundError>()( "IntegrationNotFoundError", { slug: IntegrationSlug },) { override get message(): string { return `Integration not found: ${this.slug}`; }} /** An "add integration" operation targeted a slug (namespace) that is already * registered. The core `integrations.register` primitive upserts by design * (for idempotent boot re-registration); add-operation layers gate on this to * prevent silently clobbering an existing integration's tools, connections, * and policies. */export class IntegrationAlreadyExistsError extends Schema.TaggedErrorClass<IntegrationAlreadyExistsError>()( "IntegrationAlreadyExistsError", { slug: IntegrationSlug }, { httpApiStatus: 409 },) { override get message(): string { return `Integration already exists: ${this.slug}`; }} /** `integrations.remove` was called on an integration declared statically by a * plugin at startup (`canRemove: false`). */export class IntegrationRemovalNotAllowedError extends Schema.TaggedErrorClass<IntegrationRemovalNotAllowedError>()( "IntegrationRemovalNotAllowedError", { slug: IntegrationSlug },) { override get message(): string { return `Integration cannot be removed (declared statically by a plugin): ${this.slug}`; }} export class ConnectionNotFoundError extends Schema.TaggedErrorClass<ConnectionNotFoundError>()( "ConnectionNotFoundError", { owner: Owner, integration: IntegrationSlug, name: ConnectionName, },) { override get message(): string { return `Connection not found: ${this.integration}.${this.owner}.${this.name}`; }} /** A connection create request was rejected before anything was written: the * input is structurally invalid (no credential inputs for a credentialed * template, mixed pasted/external origins, …) or targets owner `user` in a * context that has no user subject. The message says which — it is safe to * show to the caller. */export class InvalidConnectionInputError extends Schema.TaggedErrorClass<InvalidConnectionInputError>()( "InvalidConnectionInputError", { message: Schema.String },) {} /** A connection references a credential provider key that isn't registered on * the executor. */export class CredentialProviderNotRegisteredError extends Schema.TaggedErrorClass<CredentialProviderNotRegisteredError>()( "CredentialProviderNotRegisteredError", { provider: ProviderKey },) { override get message(): string { return `Credential provider not registered: ${this.provider}`; }} /** A connection's value could not be resolved — the provider returned nothing, * or an OAuth token refresh failed and the user must re-auth. */export class CredentialResolutionError extends Schema.TaggedErrorClass<CredentialResolutionError>()( "CredentialResolutionError", { owner: Owner, integration: IntegrationSlug, name: ConnectionName, message: Schema.String, /** True when the stored grant is permanently invalid and the user must * sign in again (RFC 6749 §5.2 invalid_grant and friends). */ reauthRequired: Schema.optional(Schema.Boolean), /** The authorization server's RFC 6749 §5.2 error code (`invalid_grant`, * `invalid_client`, …), when the failure came from a token-endpoint * refusal. A typed field rather than message text so telemetry and * classification read it structurally — the misclassification where * `invalid_client` (rotated app secret, fleet-wide) surfaced as a vague * "degraded" was only findable by grepping persisted message strings. */ oauthErrorCode: Schema.optional(Schema.String), },) {} // ---------------------------------------------------------------------------// Artifacts// --------------------------------------------------------------------------- /** No artifact with this id is visible to the bound owner scope — it was never * saved, was deleted, or belongs to another subject. */export class ArtifactNotFoundError extends Schema.TaggedErrorClass<ArtifactNotFoundError>()( "ArtifactNotFoundError", { id: ArtifactId },) { override get message(): string { return `Artifact not found: ${this.id}`; }} // ---------------------------------------------------------------------------// Union — the failure channel of `execute`.// --------------------------------------------------------------------------- export type ExecuteError = | ToolNotFoundError | ToolInvocationError | ToolBlockedError | PluginNotLoadedError | NoHandlerError | ConnectionNotFoundError | CredentialProviderNotRegisteredError | CredentialResolutionError | ElicitationDeclinedError | StorageFailure; /** Convenience union spanning every typed error the SDK raises. */export type ExecutorError = | ExecuteError | IntegrationNotFoundError | IntegrationRemovalNotAllowedError | ArtifactNotFoundError;
import { Schema } from "effect";
import { ElicitationDeclinedError } from "./elicitation";
import type { StorageFailure } from "./fuma-runtime";
import {
ArtifactId,
ConnectionName,
IntegrationSlug,
Owner,
ProviderKey,
ToolAddress,
} from "./ids";
export interface UserActionableError {
readonly __executorUserActionable: true;
readonly userMessage: string;
readonly code: string;
}
export const isUserActionableError = (value: unknown): value is UserActionableError =>
typeof value === "object" &&
value !== null &&
"__executorUserActionable" in value &&
value.__executorUserActionable === true &&
"userMessage" in value &&
typeof value.userMessage === "string" &&
value.userMessage.length > 0 &&
"code" in value &&
typeof value.code === "string" &&
value.code.length > 0;
/* The failure set the SDK surfaces. `execute`'s invoke failures are ported from
* v1 but re-keyed by `address` (the full `tools.<integration>.<owner>.<connection>.<tool>`
* handle) instead of an opaque tool id. Storage failures reuse fuma-runtime's
* `StorageError`/`UniqueViolationError` (`StorageFailure`) — not redefined here. */
// ---------------------------------------------------------------------------
// Tool lifecycle
// ---------------------------------------------------------------------------
/* Tagged errors without an explicit `message` field define a `message` getter:
* `Schema.TaggedErrorClass` instances are real Errors with `message: ""`, and
* an empty message propagates everywhere errors are rendered — span
* status.message in the tracer, Cause.pretty output, log lines — leaving the
* failure unlabeled in telemetry. The getter is derived from the schema fields
* (not an own property), so encoding/serialization is unaffected. */
export class ToolNotFoundError extends Schema.TaggedErrorClass<ToolNotFoundError>()(
"ToolNotFoundError",
{
address: ToolAddress,
suggestions: Schema.optional(Schema.Array(ToolAddress)),
},
) {
override get message(): string {
return `Tool not found: ${this.address}`;
}
}
export class ToolInvocationError extends Schema.TaggedErrorClass<ToolInvocationError>()(
"ToolInvocationError",
{
address: ToolAddress,
message: Schema.String,
cause: Schema.optional(Schema.Unknown),
},
) {}
/** Tool invocation was rejected because a workspace `tool_policy` rule with
* `action: "block"` matched. `pattern` is the matched policy pattern. */
export class ToolBlockedError extends Schema.TaggedErrorClass<ToolBlockedError>()(
"ToolBlockedError",
{
address: ToolAddress,
pattern: Schema.String,
},
) {
override get message(): string {
return `Tool blocked by policy "${this.pattern}": ${this.address}`;
}
}
/** Tool row exists but its owning plugin isn't loaded in this executor config. */
export class PluginNotLoadedError extends Schema.TaggedErrorClass<PluginNotLoadedError>()(
"PluginNotLoadedError",
{
address: ToolAddress,
pluginId: Schema.String,
},
) {
override get message(): string {
return `Plugin "${this.pluginId}" is not loaded for tool: ${this.address}`;
}
}
/** Tool was found but its owning plugin has no `invokeTool` handler. */
export class NoHandlerError extends Schema.TaggedErrorClass<NoHandlerError>()("NoHandlerError", {
address: ToolAddress,
pluginId: Schema.String,
}) {
override get message(): string {
return `Plugin "${this.pluginId}" has no invokeTool handler for tool: ${this.address}`;
}
}
// ---------------------------------------------------------------------------
// Integration / connection lifecycle
// ---------------------------------------------------------------------------
export class IntegrationNotFoundError extends Schema.TaggedErrorClass<IntegrationNotFoundError>()(
"IntegrationNotFoundError",
{ slug: IntegrationSlug },
) {
override get message(): string {
return `Integration not found: ${this.slug}`;
}
}
/** An "add integration" operation targeted a slug (namespace) that is already
* registered. The core `integrations.register` primitive upserts by design
* (for idempotent boot re-registration); add-operation layers gate on this to
* prevent silently clobbering an existing integration's tools, connections,
* and policies. */
export class IntegrationAlreadyExistsError extends Schema.TaggedErrorClass<IntegrationAlreadyExistsError>()(
"IntegrationAlreadyExistsError",
{ slug: IntegrationSlug },
{ httpApiStatus: 409 },
) {
override get message(): string {
return `Integration already exists: ${this.slug}`;
}
}
/** `integrations.remove` was called on an integration declared statically by a
* plugin at startup (`canRemove: false`). */
export class IntegrationRemovalNotAllowedError extends Schema.TaggedErrorClass<IntegrationRemovalNotAllowedError>()(
"IntegrationRemovalNotAllowedError",
{ slug: IntegrationSlug },
) {
override get message(): string {
return `Integration cannot be removed (declared statically by a plugin): ${this.slug}`;
}
}
export class ConnectionNotFoundError extends Schema.TaggedErrorClass<ConnectionNotFoundError>()(
"ConnectionNotFoundError",
{
owner: Owner,
integration: IntegrationSlug,
name: ConnectionName,
},
) {
override get message(): string {
return `Connection not found: ${this.integration}.${this.owner}.${this.name}`;
}
}
/** A connection create request was rejected before anything was written: the
* input is structurally invalid (no credential inputs for a credentialed
* template, mixed pasted/external origins, …) or targets owner `user` in a
* context that has no user subject. The message says which — it is safe to
* show to the caller. */
export class InvalidConnectionInputError extends Schema.TaggedErrorClass<InvalidConnectionInputError>()(
"InvalidConnectionInputError",
{ message: Schema.String },
) {}
/** A connection references a credential provider key that isn't registered on
* the executor. */
export class CredentialProviderNotRegisteredError extends Schema.TaggedErrorClass<CredentialProviderNotRegisteredError>()(
"CredentialProviderNotRegisteredError",
{ provider: ProviderKey },
) {
override get message(): string {
return `Credential provider not registered: ${this.provider}`;
}
}
/** A connection's value could not be resolved — the provider returned nothing,
* or an OAuth token refresh failed and the user must re-auth. */
export class CredentialResolutionError extends Schema.TaggedErrorClass<CredentialResolutionError>()(
"CredentialResolutionError",
{
owner: Owner,
integration: IntegrationSlug,
name: ConnectionName,
message: Schema.String,
/** True when the stored grant is permanently invalid and the user must
* sign in again (RFC 6749 §5.2 invalid_grant and friends). */
reauthRequired: Schema.optional(Schema.Boolean),
/** The authorization server's RFC 6749 §5.2 error code (`invalid_grant`,
* `invalid_client`, …), when the failure came from a token-endpoint
* refusal. A typed field rather than message text so telemetry and
* classification read it structurally — the misclassification where
* `invalid_client` (rotated app secret, fleet-wide) surfaced as a vague
* "degraded" was only findable by grepping persisted message strings. */
oauthErrorCode: Schema.optional(Schema.String),
},
) {}
// ---------------------------------------------------------------------------
// Artifacts
// ---------------------------------------------------------------------------
/** No artifact with this id is visible to the bound owner scope — it was never
* saved, was deleted, or belongs to another subject. */
export class ArtifactNotFoundError extends Schema.TaggedErrorClass<ArtifactNotFoundError>()(
"ArtifactNotFoundError",
{ id: ArtifactId },
) {
override get message(): string {
return `Artifact not found: ${this.id}`;
}
}
// ---------------------------------------------------------------------------
// Union — the failure channel of `execute`.
// ---------------------------------------------------------------------------
export type ExecuteError =
| ToolNotFoundError
| ToolInvocationError
| ToolBlockedError
| PluginNotLoadedError
| NoHandlerError
| ConnectionNotFoundError
| CredentialProviderNotRegisteredError
| CredentialResolutionError
| ElicitationDeclinedError
| StorageFailure;
/** Convenience union spanning every typed error the SDK raises. */
export type ExecutorError =
| ExecuteError
| IntegrationNotFoundError
| IntegrationRemovalNotAllowedError
| ArtifactNotFoundError;