# Operations

inseam's single named catalog of every action invocable against a running instance. A typed registry of `OperationDefinition`s with shared input/output validation, principal resolution, idempotency cache, rate-limit hook, and audit events — wrapped by an `Invoker` that every transport (HTTP, CLI, future gRPC, in-process native shell) wraps. Plugins contribute operations under prefixed namespaces.

**See also:** [design](../design/operations.md) · [spec](../spec/operations.md)

## What it does

Two surfaces, split by package:

- **Contract surface** (`@inseam/operations-contract`) — `defineOperation`, `OperationDefinition`, `OperationShape`, `OperationHandler`, `OperationContext`, `Principal`, the StandardSchema v1 alias used for input/output, the framework error classes (`InputValidationError`, `OutputValidationError`, `UnauthorizedError`, `RateLimitedError`, `NotFoundError`, `IdempotencyReplayError`, `OperationNotFoundError`, `InternalError`), the wire-error envelope (`WireError`, `WireStreamFrame`), and the introspection shape (`OperationDescribed`). Zero runtime deps; no `effect`.
- **Core surface** (`@inseam/core/operations`) — `openOperationRegistry`, `createInvoker`, `installCoreOperations`, the registry-level errors (`DuplicateOperationError`, `UnsupportedShapeError`, `OperationPrefixError`, `ReservedPrefixError`, `InvalidFilterFieldError`, `RegistryFrozenError`), the per-host `operationIdempotencyMigration`, and the `AuditSink` / `RateLimiter` seams.

A registered operation is described once and then invoked many times through the same pipeline:

1. Lookup by name (`OperationNotFoundError` otherwise).
2. Decode input through the operation's StandardSchema validator (`InputValidationError`).
3. Resolve principal — kind must be one of `network-owner`, `host-owner`, `user`, `agent`, `service` (else `UnauthorizedError`).
4. Idempotency check (unary mutations only): keyed on `(principal_id, operation, idempotency_key)`, 24h TTL, replay-with-different-input is `IdempotencyReplayError`.
5. Rate-limit through the host-supplied `RateLimiter` (`RateLimitedError`).
6. Audit prelude — `auditSink.invoked` with the canonical SHA-256 input digest.
7. Invoke handler; validate output; cache result for idempotent unary mutations; emit `auditSink.completed` or `auditSink.failed`.

V1 ships `unary` and `server-stream` shapes. `client-stream` and `bidirectional` are reserved in the vocabulary and rejected by `registry.add` at registration time. Streams do not participate in the idempotency cache; an `AbortSignal` from the transport propagates to the handler's `ctx.signal`, and post-abort yields are discarded.

Tagged-error convention: every operation error implements `OperationError` (`{ readonly _tag: string }`) with `class.name === _tag`. Handler-thrown errors that are neither framework errors nor declared in the operation's `errors` array are coerced to `InternalError` and surfaced in the log as `core.diagnostic.unhandled_operation_error`.

## How to use it

Define an operation:

```ts
import { defineOperation, NotFoundError } from "@inseam/operations-contract";
import { z } from "zod"; // or any StandardSchema-conformant validator

export const sourceGet = defineOperation({
  name: "source.get",
  shape: "unary",
  input: z.object({ id: z.string() }),
  output: z.object({ id: z.string(), uri: z.string() }),
  errors: [NotFoundError],
  idempotent: true,
  summary: "Fetch a Source by id.",
  handler: async (input, ctx) => {
    const row = await findSource(input.id);
    if (!row) throw new NotFoundError("source", input.id);
    return row;
  },
});
```

Wire a registry + invoker in a host:

```ts
import {
  openOperationRegistry, createInvoker, installCoreOperations,
  operationIdempotencyMigration,
} from "@inseam/core/operations";

const registry = openOperationRegistry({ store, clock, log });
installCoreOperations(registry);
registry.add(sourceGet);
registry.freeze();

const invoker = createInvoker(registry, { store, clock, log, auditSink, rateLimiter });

// In a transport:
const result = await invoker.invokeUnary({
  operation: "source.get",
  input: { id: "s-1" },
  principal,
  network: { id: networkId },
  host: { id: hostId },
  signal,
});
```

Two introspection operations ship in `installCoreOperations`:

- `operation.list` → `ListResult<OperationDescribed>` of every registered operation.
- `operation.describe({ name })` → the `OperationDescribed` for a single operation, or `NotFoundError`.

## How to extend it

A plugin contributes operations through a prefix-scoped slot. The plugin loader hands the plugin a slot bound to its package name and allowed prefixes; everything else (`connection.`, `source.`, `access.`, `principal.`, `network.`, `host.`, `plugin.`, `operation.`) is reserved.

```ts
// Inside a plugin's registration callback:
const ops = registries.operations.asPluginSlot({
  packageName: "@example/inseam-gmail",
  allowedPrefixes: ["gmail"],
});

ops.add(defineOperation({
  name: "gmail.message.fetch",
  shape: "unary",
  input: z.object({ id: z.string() }),
  output: GmailMessageSchema,
  errors: [GmailRateLimitedError],
  handler: async (input, ctx) => { /* ... */ },
}));
```

Naming convention: dot-separated, last segment is the verb. Read-only verbs (`get`, `list`, `watch`, `describe`) default `idempotent: true`; everything else defaults to `false`. `*.list` operations may declare `orderableFields` / `filterableFields` arrays; the registry validates each against the top-level properties of the output JSON Schema and throws `InvalidFilterFieldError` at registration time if a field is unknown.

## Where the code lives

- `pkgs/operations-contract/src/index.ts` — the entire contract surface.
- `pkgs/core/src/operations/registry.ts` — `openOperationRegistry`, `asPluginSlot`, reserved-prefix and list-field validation.
- `pkgs/core/src/operations/invoker.ts` — `createInvoker`, the 7-step pipeline, idempotency cache reads/writes.
- `pkgs/core/src/operations/core-operations.ts` — `operation.list` and `operation.describe`.
- `pkgs/core/src/operations/digest.ts` — canonical-JSON SHA-256 used for the audit `inputDigest`.
- `pkgs/core/src/operations/migration.ts` — `operation_idempotency_cache` table migration.

## Related

- [Plugin system](./plugin-system.md) — how a plugin acquires its operations slot via `RegistrationContext`.
- [Access](./access.md) — Principal kinds and how transports resolve them before invocation.
- [Store](./store.md) — the persistence port the idempotency cache writes through.
- [Events](./events.md) — separate from the audit sink; ops emit audit events directly, not through the typed event bus.
- Transport docs (`transport-http.md`, `transport-cli.md`) — *not yet written*; the layer that maps wire requests to `invokeUnary` / `invokeStream`.
