Operations
defineOperations binds each action an application exposes to one permission leaf, with a data loader, a results permission and risk meta. Agent tools, automations, chat commands and approval prompts read the same frozen table by name.
defineOperations(permissions, definitions) names the actions an application exposes and binds each to one permission. Agent tools, automations, chat commands and approval prompts read the same table instead of each keeping its own.
import { defineOperations } from "permdock";
export const operations = defineOperations(permissions, {
sendInvoice: {
permission: permissions.billing.invoice.submit,
data: (input: { id: string }) => db.invoice.find(input.id),
meta: {
risk: "high",
effects: ["send", "external"],
title: "Send invoice",
},
},
listInvoices: {
permission: permissions.billing.invoice.read,
results: permissions.billing.invoice.read,
},
});It returns a frozen OperationMap. Each entry is a frozen Operation { name, permission, data?, results?, meta }:
| Field | Meaning |
|---|---|
permission | The leaf a caller must hold to run the operation |
data | Loads the row the check runs on from the input; OperationInput<typeof operations.sendInvoice> is its parameter type |
results | The leaf each result row is filtered by |
meta | { risk?, effects?, title? }; {} when absent |
risk is one of low, medium, high, critical, exported in that order as OPERATION_RISKS with the guard isOperationRisk. effects lists distinct values of read, write, delete, send, spend, external. A host reads meta to label, confirm or gate an operation; PermDock never decides on it.
defineOperations throws at definition time for:
- a
permissionorresultsoutsidepermissions(matched by key); - a
datathat is not a function; - meta outside those lists;
- a name that is not 1 to 128 of
A-Z a-z 0-9 _ . -, or that repeats another name case-insensitively.
Where operations are read
| Reader | What it takes from an operation |
|---|---|
| Agents | decideOperation(name, input, ctx) loads the row through data and decides permission; filterResults applies results |
| Automations | An automation lists the operations it runs; a run is narrowed to their permissions |
| Chat | protect(operation, handler) decides a chat command as its author |
| Delivery | approvalPrompt(request, { operation }) puts meta.title and meta.risk in the approval email or card |
| Catalog | permdock collect writes { name, permission, results?, meta } with permission keys when permdock.config.ts names an operations module |
With operations: "./src/operations.ts" in permdock.config.ts, permdock collect writes the map to the catalog as operations. Doctor PD067 reports an operation whose permission or results key the configured permissions module does not declare.
Why
- One table per application. The same action reached from REST, MCP, an AI tool, an automation and a chat command otherwise gets a lookup table per entry point, and they drift apart.
- An operation names a leaf. Operations are not a second permission vocabulary. Grants, snapshots, RLS and the catalog stay keyed by permission, and an operation only says which leaf an action needs and how to load its row.
- MCP tool-name characters. An operation name becomes a tool name unchanged, so it uses the characters MCP 2025-11-25 recommends for tool names. Names that differ only in case are refused because chat commands and some tool hosts compare names case-insensitively.
- Closed meta lists. A host can only gate on
riskandeffectsif every operation uses the same words, so an unknown value throws instead of passing through.
Last updated on
API keys and service accounts
Restricted credentials are opaque pdk_ keys stored as SHA-256 hashes; a user-bound key is its owner's live rights narrowed to the key, a service key is a service principal bounded by its creator, and tenant settings, revocation and events apply on every use.
Agents
permdock/agent is the runtime-neutral agent contract. Operations are decided by name, models are gated by permission, results are filtered by the operation's results permission, and the AI Gateway gets the caller as user and tags. The AI SDK, OpenAI Agents, Claude Agent SDK and Eve adapters each return one.