Skip to the content

Capsid menu

src/tools/namespaces.ts

166 lines and 6 functions in DrDustinEdwards/capsid, at commit daa875b on 2026-10-24. It is in an import cycle. This file on GitHub.

Functions

The functions of namespaces.ts
FunctionLinesComplexityCoveredCRAPMutation
defineNamespaceOperations17 to 1711100%OkCRAP 1No data: mutation testing has not run on this repo yet
<anonymous:27>27 to 482100%OkCRAP 2No data: mutation testing has not run on this repo yet
<anonymous:46>46 to 461100%OkCRAP 1No data: mutation testing has not run on this repo yet
<anonymous:67>67 to 1021081%WatchCRAP 10.7No data: mutation testing has not run on this repo yet
<anonymous:115>115 to 1345100%OkCRAP 5No data: mutation testing has not run on this repo yet
<anonymous:153>153 to 16966%CriticalCRAP 36No data: mutation testing has not run on this repo yet

Imports

It imports src/agents-schema.ts, src/github.ts, src/limits.ts, src/namespace-delete.ts, src/scope.ts, src/store-guards.ts, src/tools/docs.ts, src/tools/surface.ts. Imported by src/tools/docs.ts.

Source

import { McpServer } from "@modelcontextprotocol/server";
import { z } from "zod";
import { IMPROVE_OVERRIDE_FLAGS } from "../scope";
import { surfaceOf } from "./surface";
import { NAMESPACE_DELETE_MAX_DOCUMENTS, performNamespaceDelete, previewNamespaceDelete } from "../namespace-delete";
import { parseReposList, REPO_SHAPE, requireSinglePrimary } from "../github";
import { auditStatement } from "../store-guards";
import { driverMintInstruction } from "../agents-schema";
import { bounded, MAX_CONFIRM_TOKEN, MAX_REPO_SELECTOR, MAX_REPOS_JSON, nsName } from "../limits";
import { ok, fail, type ToolCtx } from "./docs";

// The namespace operations: namespaces (capsid_find_docs action namespaces), and
// register_namespace, update_namespace and delete_namespace (capsid_admin_manage_namespaces
// actions register, update, delete_preview and delete_perform).
// defineDocOperations in ./docs registers them after the document tools.

export function defineNamespaceOperations(server: McpServer, ctx: ToolCtx): void {
  const { env, db, actor } = ctx;

  ctx.ops.define(
    "namespaces",
    {
      description:
        "List the namespaces this caller is scoped to and the repos each maps to. A scoped agent sees only its own namespaces.",
      inputSchema: {},
    },
    async () => {
      // unconsolidated = episodic and source docs not yet archived by the lint loop,
      // surfaced so every session sees which namespaces need a run. Not filtered on
      // status: the archive/ prefix is the only thing that takes a doc out of the loop.
      const { results } = await db
        .prepare(
          `SELECT n.namespace, n.repos, n.created_at,
                  (SELECT COUNT(*) FROM documents d
                   WHERE d.namespace = n.namespace AND d.type IN ('episodic', 'source')
                     AND d.path NOT LIKE 'archive/%') AS unconsolidated
           FROM namespaces n ORDER BY n.namespace`
        )
        .all();
      // Filtered to the caller's own namespaces. This tool takes no arguments, so
      // namespaceRefusal at the registrar has nothing to fire on. The mapping is what
      // scripts/mint-agents.ts uses to set the repos axis and what resolveRepo
      // resolves through, so handing it to a narrowed caller hands it the shape of the
      // boundary it sits behind.
      const scoped = ctx.agent.scopes.namespaces;
      const visible = scoped === "*" ? results : results.filter((row) => scoped.includes(String((row as { namespace: string }).namespace)));
      return ok(visible);
    }
  );

  // Register a namespace: the one row in the namespaces table that repo tools and the
  // namespaces list read. Writing documents to a new namespace label does not create
  // it. Create-only: it will not overwrite an existing mapping. Admin only (TOOL_GRANTS in src/scope.ts): an OAuth
  // session qualifies, a minted agent holding the write grant does not.
  ctx.ops.define(
    "register_namespace",
    {
      description:
        "Register a namespace in the namespaces table. Give repo as 'owner/name' (label defaults to 'primary'), or pass a repos JSON array like [{\"repo\":\"owner/name\",\"label\":\"primary\"}] for a multi-repo namespace. Refused if the namespace exists. Admin only: an OAuth session qualifies, a minted agent holding the write grant does not.",
      inputSchema: {
        namespace: nsName,
        repo: bounded(MAX_REPO_SELECTOR).optional(),
        label: bounded(MAX_REPO_SELECTOR).optional(),
        repos: bounded(MAX_REPOS_JSON).optional(),
      },
    },
    async ({ namespace, repo, label, repos }) => {
      const ns = namespace.trim();
      if (!ns) return fail("namespace is required");
      let list: Array<{ repo: string; label: string }>;
      if (repos) {
        const parsed = parseReposList(repos);
        if ("error" in parsed) return fail(parsed.error);
        list = parsed.list;
      } else {
        if (!repo || !REPO_SHAPE.test(repo)) {
          return fail('provide repo as "owner/name", or pass a repos JSON array');
        }
        list = [{ repo, label: (label ?? "primary").trim() || "primary" }];
      }
      // The same single-primary requirement as update_namespace. Two primaries or none
      // would make every repo tool resolve by accident, and update would refuse to fix
      // it in place.
      const primaryError = requireSinglePrimary(list);
      if (primaryError) return fail(primaryError);
      const existing = await db.prepare("SELECT namespace FROM namespaces WHERE namespace = ?1").bind(ns).first();
      if (existing) {
        return fail(`namespace already registered: ${ns}. Use capsid_admin_manage_namespaces action update to change its repo mapping; it snapshots the prior mapping to the audit log.`);
      }
      const reposJson = JSON.stringify(list);
      await db.batch([
        db.prepare("INSERT INTO namespaces (namespace, repos) VALUES (?1, ?2)").bind(ns, reposJson),
        auditStatement(db, actor, "register_namespace", ns, null, list),
      ]);
      return ok({
        namespace: ns,
        repos: list,
        action: "registered",
        driver_agent: null,
        next: driverMintInstruction(ns),
      });
    }
  );

  // Remap an existing namespace's repos; register_namespace is the create path.
  // Snapshots the prior mapping to the audit log. It does not rename the namespace: a
  // rename touches document keys, versions and audit history.
  ctx.ops.define(
    "update_namespace",
    {
      description:
        "Remap an existing namespace's repos. Pass repos as a JSON array like [{\"repo\":\"owner/name\",\"label\":\"primary\"},{\"repo\":\"owner/legacy\",\"label\":\"legacy\"}], with exactly one entry labeled \"primary\". The namespace must exist. Records the prior mapping in the audit log. Does not rename the namespace or move its documents. Admin only: an OAuth session qualifies, a minted agent holding the write grant does not.",
      inputSchema: { namespace: nsName, repos: bounded(MAX_REPOS_JSON) },
    },
    async ({ namespace, repos }) => {
      const ns = namespace.trim();
      if (!ns) return fail("namespace is required");
      const parsed = parseReposList(repos);
      if ("error" in parsed) return fail(parsed.error);
      const list = parsed.list;
      const primaryError = requireSinglePrimary(list);
      if (primaryError) return fail(primaryError);
      const existing = await db
        .prepare("SELECT repos FROM namespaces WHERE namespace = ?1")
        .bind(ns)
        .first<{ repos: string }>();
      if (!existing) return fail(`namespace not found: ${ns}. Use capsid_admin_manage_namespaces action register to create it.`);
      const reposJson = JSON.stringify(list);
      await db.batch([
        db.prepare("UPDATE namespaces SET repos = ?2 WHERE namespace = ?1").bind(ns, reposJson),
        auditStatement(db, actor, "update_namespace", ns, null, { old: existing.repos, new: reposJson }),
      ]);
      return ok({ namespace: ns, repos: list, action: "updated", previous: existing.repos });
    }
  );
  // Delete a namespace: a preview that counts everything naming it and signs a token,
  // then a perform that re-plans, requires the token to match and commits one guarded
  // batch. The rules and what is kept are in src/namespace-delete.ts. Admin only
  // (TOOL_GRANTS in src/scope.ts), like the other two tools that edit the mapping.
  ctx.ops.define(
    "delete_namespace",
    {
      description:
        `Delete a registered namespace. Two calls. action 'delete_preview' writes nothing: it counts every live and archived document, snapshot, edge, job by status, agent naming the namespace, and the ops_sites row, and returns a verdict. It is refused while any job in the namespace is open (queued, claimed or blocked) or any live agent names it in its scopes (cascade never reaches jobs or agents; end them with capsid_work_jobs or capsid_admin_manage_agents), while it is on the roster, while live documents exist and cascade is not true, while it holds signed policy documents (policy/) and allow_improve_paths is not true, and while it holds more live documents than one batch deletes (${NAMESPACE_DELETE_MAX_DOCUMENTS}; delete some with capsid_delete_doc or move them with capsid_write_doc action move first). An allowed preview returns a token bound to the namespace, cascade, allow_improve_paths, the counts and this caller, valid five minutes. action 'delete_perform' takes the same arguments and that token, re-reads the plan and refuses if anything counted changed, then in one batch snapshots every live document to document_versions and deletes it with the edges touching it, deletes the ops_sites row and the namespaces row, and writes one audit row 'namespace-delete' holding the edges and both rows whole. Archived documents, versions, the audit log, finished jobs and their records are kept. Admin only: an OAuth session qualifies, a minted agent holding the write grant does not.`,
      inputSchema: {
        namespace: nsName,
        action: z.enum(["preview", "perform"]),
        cascade: z.boolean().optional(),
        allow_improve_paths: z.boolean().optional(),
        token: bounded(MAX_CONFIRM_TOKEN).optional(),
      },
    },
    async ({ namespace, action, cascade, allow_improve_paths, token }) => {
      const ns = namespace.trim();
      if (!ns) return fail("namespace is required");
      const opts = { cascade: cascade === true, allowImprovePaths: allow_improve_paths === true };
      // The policy override is scoped as it is on a document delete: the flag is the
      // permission, the argument only the caller's intent.
      if (opts.allowImprovePaths) {
        const overrideRefusal = ctx.scope({ ...surfaceOf("delete_namespace", "perform"), namespace: ns, flags: IMPROVE_OVERRIDE_FLAGS });
        if (overrideRefusal) return fail(overrideRefusal);
      }
      const now = new Date();
      const answer =
        action === "preview"
          ? await previewNamespaceDelete(env, actor, ns, opts, now)
          : await performNamespaceDelete(env, actor, ns, opts, token, now);
      return answer.ok ? ok(answer.data) : fail(answer.refusal);
    }
  );
}