src/tools/controls.ts
67 lines and 3 functions in DrDustinEdwards/capsid, at commit daa875b on 2026-10-24. It is in an import cycle. This file on GitHub.
Functions
| Function | Lines | Complexity | Covered | CRAP | Mutation |
|---|---|---|---|---|---|
| defineControlOperations | 14 to 70 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
| <anonymous:41> | 41 to 68 | 12 | 93% | WatchCRAP 12.1 | No data: mutation testing has not run on this repo yet |
| <anonymous:53> | 53 to 53 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
Imports
It imports src/controls-base.ts, src/controls.ts, src/limits.ts, src/ops-types/portal.ts, src/tools/docs.ts. Imported by src/server.ts.
Source
import { McpServer } from "@modelcontextprotocol/server";
import { z } from "zod";
import { CONTROLS, performControl, previewControl, TOKEN_TTL_SECONDS } from "../controls";
import { PORTAL_ACTIONS } from "../controls-base";
import { bounded, MAX_TITLE } from "../limits";
import type { PortalAction } from "../ops-types/portal";
import { fail, ok, type ToolCtx } from "./docs";
const MAX_PARAM = 1024;
const MAX_KEY = 64;
const MAX_TOKEN = 4096;
const ACCESS_PREFIX = "access:";
export function defineControlOperations(server: McpServer, ctx: ToolCtx): void {
const { env } = ctx;
// The admin controls the Portal's buttons run, reachable from chat as one tool (a ruled
// exception to the tool surface rule, 2026-10-03). One registry (src/controls.ts) is behind both surfaces, so a control
// is implemented once and a test holds the two lists equal. Admin only in whole
// (TOOL_GRANTS.capsid_admin_run_controls): the registrar refuses every other caller before this handler
// runs. The confirmation is the Portal's: preview returns the plan and a signed token for
// five minutes that binds the action, its params and this administrator, and perform
// takes only the token and spends it, so no confirmation is used twice.
ctx.ops.define(
"controls",
{
description: `The administrator's controls, the same ones Capsid Portal's buttons run: ${PORTAL_ACTIONS.join(", ")}. Admin only. Two steps, never one. action "list" returns each control, the params it takes and whether it needs a reason. action "preview" (control, params) reads the current state, WRITES NOTHING, and returns what will change, the audit rows it will write and a token good for ${TOKEN_TTL_SECONDS / 60} minutes. action "perform" (token) runs exactly what that preview described, once: the token binds the action, its params and you, and is spent when used, so a second perform of it is refused and you preview again. Performing from here writes a control-<action> audit row naming you and the surface; the Portal writes portal-<action>. Merging a pull request and minting a credential are deliberately not controls: a merge can start a deploy and stays with capsid_close_pr action merge behind can_merge, and a mint stays with capsid_admin_manage_agents. REFUSES: a caller that is not the administrator signed in through Access; an unknown control; a param the control does not take; a switch with no reason; a token that is malformed, expired, issued to someone else or already used.`,
inputSchema: {
action: z.enum(["list", "preview", "perform"]).describe("list | preview | perform."),
control: z
.enum(PORTAL_ACTIONS as unknown as [PortalAction, ...PortalAction[]])
.optional()
.describe("For preview: the control to preview."),
params: z
.record(bounded(MAX_KEY), bounded(MAX_PARAM))
.optional()
.describe(`For preview: the control's params, each a string (list says which). A reason is at most ${MAX_TITLE} characters.`),
token: bounded(MAX_TOKEN).optional().describe("For perform: the token the preview returned. Nothing else is read."),
},
},
async (args) => {
const now = new Date();
// The token binds an email, and a chat caller has one only when it is the administrator
// signed in through Access (agent:access:<email>). An operator key or a minted agent
// has none and cannot hold a control, whatever its grant.
if (!ctx.actor.startsWith(ACCESS_PREFIX) || ctx.actor.length === ACCESS_PREFIX.length) {
return fail(`controls needs the administrator signed in through Access; ${ctx.actor} is not.`);
}
const email = ctx.actor.slice(ACCESS_PREFIX.length);
try {
if (args.action === "list") {
return ok({
controls: CONTROLS.map((c) => ({ control: c.name, params: c.fields, reason_required: c.reasonRequired })),
next: 'Call action "preview" with a control and its params, read the plan, then call action "perform" with the token.',
});
}
if (args.action === "preview") {
if (!args.control) return fail('preview needs a control. Call action "list" for the names.');
const previewed = await previewControl(env, email, args.control, args.params ?? {}, now, "chat");
return previewed.ok ? ok(previewed.preview) : fail(previewed.refusal);
}
if (!args.token) return fail("perform needs the token a preview returned.");
const did = await performControl(env, email, args.token, null, now, "chat");
return did.ok ? ok({ action: did.action, summary: did.result.summary, warning: did.result.warning }) : fail(did.refusal);
} catch (err) {
return fail(err instanceof Error ? err.message : String(err));
}
}
);
}