fix(mcp): route first tasks to a trusted configured receiver

Conflict resolved for this base: its external grants can also be offered
rig_team and the inbox tools, so the external request instructions say
"Use only the tools this connection offers" without naming which tools
are absent, and the base's inbox instructions and instructionsFor stay
as they are.

(cherry picked from commit a0d68325024b41d6f6d131bd0d66d8e5e78f6de3)
This commit is contained in:
dev-driver committed 2026-11-07 05:10:10 +00:00
1 parent ddb9d25be7
commit e6090aa29a
6 files changed
+191 -13

No files matched your search

+17 -4
View File
@@ -25,6 +25,7 @@ export function mcpCommand(depsOverride?: StatusDeps): Command {
.option("--bind <host:port>", "With --http: where to listen (default 127.0.0.1:7437). Beyond loopback needs a grant (rig mcp token add)")
.option("--grant-service <url>", "With --http: check each request's key with the grant service at this URL instead of the token file")
.option("--grant-service-header-file <path>", "With --grant-service: a file holding one 'Name: value' header to send with each grant request")
.option("--task-destination <seat>", "With --http --grant-service: operator-configured local team seat for external requests that omit destination")
.option("--oauth-resource <url>", "With --grant-service and --oauth-issuer: the public https URL clients reach this server as (its path ends in /mcp), for OAuth discovery")
.option("--oauth-issuer <url>", "With --oauth-resource: the OAuth authorization server that issues the keys --grant-service checks")
.addHelpText("after", `
@@ -43,13 +44,20 @@ for this daemon's instance (its self id); the token file isn't read. A caller ac
must already exist here (created is enough; it needn't have been launched), and reaches only what the grant names. If
the grant service can't answer, the request is refused.
With --task-destination <member@rig>, external callers can send their first task without knowing a seat address.
Supply the intended receiver from trusted operator configuration for this server, never from a caller's input.
On every omitted destination, the receiver must be exactly one existing local node and within that request's grant.
Missing, ambiguous or forbidden receivers cause no write. This option doesn't grant access or select a receiver from
topology, names or task history. Explicit destinations keep their existing behavior; local and member calls still
require destination. A server with multiple teams needs a receiver valid for the calling grant, or explicit destinations.
With --grant-service, --oauth-resource and --oauth-issuer (both or neither) add OAuth discovery (RFC 9728) for clients
that sign in with OAuth. The resource is the public URL this server is reached as, and the issuer is the authorization
server that issues the keys --grant-service checks. GET /.well-known/oauth-protected-resource<resource path> then answers
with both, exactly as given, without a credential or a call to the grant service, and every 401 points there. Both must
be https with no userinfo, query or fragment, written as a URL writes them (a lowercase host, no default port). /mcp
stays the endpoint; a proxy in front forwards the metadata path unchanged.`)
.action(async (opts: { port?: string; http?: boolean; bind?: string; grantService?: string; grantServiceHeaderFile?: string; oauthResource?: string; oauthIssuer?: string }) => {
.action(async (opts: { port?: string; http?: boolean; bind?: string; grantService?: string; grantServiceHeaderFile?: string; taskDestination?: string; oauthResource?: string; oauthIssuer?: string }) => {
const deps = getDepsF();
let daemonUrl: string;
@@ -80,6 +88,11 @@ stays the endpoint; a proxy in front forwards the metadata path unchanged.`)
process.exitCode = 1;
return;
}
if (opts.taskDestination !== undefined && !(opts.http && opts.grantService)) {
console.error("--task-destination needs --http --grant-service");
process.exitCode = 1;
return;
}
if ((opts.oauthResource === undefined) !== (opts.oauthIssuer === undefined)) {
console.error("--oauth-resource and --oauth-issuer go together: give both, or neither");
process.exitCode = 1;
@@ -92,7 +105,7 @@ stays the endpoint; a proxy in front forwards the metadata path unchanged.`)
return;
}
if (opts.http) {
await serveHttp(daemonUrl, opts.bind ?? "127.0.0.1:7437", opts.grantService, opts.grantServiceHeaderFile, oauth);
await serveHttp(daemonUrl, opts.bind ?? "127.0.0.1:7437", opts.grantService, opts.grantServiceHeaderFile, oauth, opts.taskDestination);
return;
}
const client = deps.clientFactory(daemonUrl);
@@ -169,7 +182,7 @@ stays the endpoint; a proxy in front forwards the metadata path unchanged.`)
}
async function serveHttp(daemonUrl: string, bind: string, grantService?: string, grantServiceHeaderFile?: string,
oauth?: { resource: string; issuer: string }): Promise<void> {
oauth?: { resource: string; issuer: string }, taskDestination?: string): Promise<void> {
const match = /^(\[[^\]]+\]|[^:]+):(\d+)$/.exec(bind);
if (!match) {
console.error(`--bind takes host:port (for example 127.0.0.1:7437 or [::1]:7437), not ${bind}`);
@@ -181,7 +194,7 @@ async function serveHttp(daemonUrl: string, bind: string, grantService?: string,
let served;
try {
const header = grantServiceHeaderFile ? readGrantServiceHeader(grantServiceHeaderFile) : undefined;
served = await startMcpHttpServer({ daemonUrl, host, port: Number(match[2]), tokenFile,
served = await startMcpHttpServer({ daemonUrl, host, port: Number(match[2]), tokenFile, taskDestination,
...(grantService ? { grantService: { url: grantService, ...(header ? { header } : {}) } } : {}), ...(oauth ? { oauth } : {}) });
} catch (err) {
console.error(err instanceof McpHttpConfigError ? err.message : `Can't serve on ${bind}: ${(err as Error).message}`);
+10 -1
View File
@@ -310,6 +310,8 @@ export type McpHttpOptions = {
clientFactory?: (daemonUrl: string, sessionName?: string, principal?: McpPrincipal) => DaemonClient;
/** Grants come from an external grant service instead of the token file, which is then never read. */
grantService?: GrantServiceOptions;
/** Operator-configured receiver for external callers omitting a task destination; never an authority grant. */
taskDestination?: string;
/** With grantService: OAuth discovery for clients that sign in with OAuth. The public resource this server is reached
* as and the authorization server that issues its keys, both served exactly as given. */
oauth?: { resource: string; issuer: string };
@@ -338,6 +340,12 @@ function readBody(req: http.IncomingMessage): Promise<string> {
export async function startMcpHttpServer(opts: McpHttpOptions): Promise<{ url: string; authenticated: boolean; close(): Promise<void> }> {
const loopback = isLoopbackHost(opts.host);
const service = opts.grantService;
if (opts.taskDestination !== undefined) {
if (!service) throw new McpHttpConfigError("--task-destination needs --grant-service");
if (!/^[^\s@]+@[^\s@]+$/.test(opts.taskDestination) || opts.taskDestination.endsWith("@external")) {
throw new McpHttpConfigError("--task-destination takes one local team seat, member@rig");
}
}
const serviceUrl = service ? grantServiceUrl(service.url) : undefined;
// The metadata describes the keys a grant service checks, so it's served only with one.
if (opts.oauth && !service) throw new McpHttpConfigError("--oauth-resource and --oauth-issuer need --grant-service");
@@ -382,7 +390,8 @@ export async function startMcpHttpServer(opts: McpHttpOptions): Promise<{ url: s
const caller = (authInfo?.extra as { caller?: RequestCaller } | undefined)?.caller;
// Never a default seat: a request reaches the handler only with its caller decided.
if (!caller) throw new Error("a request reached the MCP handler without its caller");
return createMcpServer(makeClient(opts.daemonUrl, caller.seat, caller.principal), { seat: caller.seat, access: caller.access, presenceFile: () => presenceFile });
return createMcpServer(makeClient(opts.daemonUrl, caller.seat, caller.principal), { seat: caller.seat, access: caller.access,
taskDestination: opts.taskDestination, presenceFile: () => presenceFile });
});
const serveMcp = toNodeHandler(mcpHandler);
const bearer = (req: http.IncomingMessage): string | undefined => /^Bearer\s+(\S+)\s*$/i.exec(req.headers.authorization ?? "")?.[1];
+42 -3
View File
@@ -572,7 +572,13 @@ const SHARED_INSTRUCTIONS = [
"A question reserved for a person is theirs to answer: don't answer it on their behalf. rig_queue_decide records a decision only inside the decision room of the mode they have on, beside their answers and never as one.",
];
const INSTRUCTIONS = [...SHARED_INSTRUCTIONS, MEMBER_WORKFLOW].join("\n");
const EXTERNAL_INSTRUCTIONS = SHARED_INSTRUCTIONS.join("\n");
const EXTERNAL_INSTRUCTIONS = [
"This connection sends requests to an OpenRig team as the caller named by its grant. The daemon attributes requests to that caller. Use only the tools this connection offers.",
"To ask the connected team to do work, use rig_queue_create with the task in body. You may omit destination to use the receiver configured by the server operator. If no receiver is available, ask the operator to configure it; don't guess an address. An explicit destination still must be within this connection's grant.",
"Keep the returned qitemId and use rig_queue_show to read the result. Acceptance is not completed work. If a create times out, check that same qitemId before retrying and reuse it; never retry with a new id. Use revises for a requested revision of earlier work, preserving its lineage. rig_queue_list shows your own work, including an empty list before your first request.",
"Read returned files with rig_artifact_read, within the shared roots and items this connection may reach. A question reserved for a person is theirs to answer; don't answer on their behalf.",
].join("\n");
/** For an external grant that names claim and update: how it works the items addressed to it, messages included. */
const EXTERNAL_INBOX_INSTRUCTIONS = [...SHARED_INSTRUCTIONS,
"Items addressed to your seat are yours: rig_queue_list with owned set lists them. For each, claim it (rig_queue_claim), do it, then close it done with closure reason no-follow-on and the answer in resolution (rig_queue_update); the sender reads that answer on the same item. A message is an item too: read it, close it done, and reply, when it needs one, with a new item to its sender. Nothing promises to wake you, so check your items again.",
@@ -584,6 +590,25 @@ function instructionsFor(access: McpAccess | undefined): string {
return tools.has("rig_queue_claim") && tools.has("rig_queue_update") ? EXTERNAL_INBOX_INSTRUCTIONS : EXTERNAL_INSTRUCTIONS;
}
/** A configured default must identify exactly one existing local node. No topology relationship, name convention
* for a lead, caller identity or task history supplies a missing receiver. Resolve again on each omitted destination. */
async function taskDestinationExists(client: DaemonClient, destination: string): Promise<boolean> {
const rigName = destination.split("@")[1]!;
const rig = await resolveRigHandle(client, rigName, { strict: true });
if (rig.kind !== "resolved") return false;
const nodes = await client.get(`/api/rigs/${encodeURIComponent(rig.id)}/nodes`);
if (nodes.status !== 200 || !Array.isArray(nodes.data)) return false;
let matches = 0;
for (const entry of nodes.data as unknown[]) {
const { logicalId, rigName: name } = (entry ?? {}) as Record<string, unknown>;
if (typeof logicalId !== "string" || name !== rigName) return false;
const dot = logicalId.indexOf(".");
if (dot <= 0 || dot === logicalId.length - 1) return false;
if (`${logicalId.slice(0, dot)}-${logicalId.slice(dot + 1)}@${name}` === destination) matches++;
}
return matches === 1;
}
/**
* Maps a DaemonResponse to MCP tool result.
* Three error levels: HTTP status, in-body errors, and structural failure fields.
@@ -631,7 +656,7 @@ function mapResult(
* @param client - DaemonClient connected to the daemon
* @returns McpServer instance (not yet connected to a transport)
*/
export function createMcpServer(client: DaemonClient, options: { seat?: string; access?: McpAccess; rostersFolder?: () => string; presenceFile?: () => string } = {}): McpServer {
export function createMcpServer(client: DaemonClient, options: { seat?: string; access?: McpAccess; taskDestination?: string; rostersFolder?: () => string; presenceFile?: () => string } = {}): McpServer {
const server = new McpServer({
name: "openrig",
version: CLI_VERSION,
@@ -1199,7 +1224,9 @@ export function createMcpServer(client: DaemonClient, options: { seat?: string;
{
description: "Hand a task to another seat as a durable queue item. Returns its qitemId: read the answer later with rig_queue_show. If the call times out, check rig_queue_show with the same qitemId before retrying; never retry with a new id. To ask for a revision of earlier work, name it in revises: the new item opens with 'Revision of <id>' and records it in its chainOfRecord.",
inputSchema: z.object({
destination: z.string().describe("Destination seat (e.g. dev-impl@my-rig)"),
destination: access?.external
? z.string().optional().describe("Destination seat; omit to use the connected team's configured receiver")
: z.string().describe("Destination seat (e.g. dev-impl@my-rig)"),
body: z.string().describe("The task: what to do and what to return"),
summary: z.string().optional().describe("One-line summary"),
tags: z.array(z.string()).optional().describe("Tags, e.g. mission:<name>"),
@@ -1212,6 +1239,18 @@ export function createMcpServer(client: DaemonClient, options: { seat?: string;
annotations: ADDITIVE,
},
async ({ destination, body, summary, tags, priority, tier, qitemId, revises, alert }) => {
if (destination === undefined && access?.external) {
destination = options.taskDestination;
const unavailable = () => errorResult("The connected team's task receiver is unavailable. Ask the server operator to configure it.",
"task_destination_unavailable", "The connected team's task receiver is unavailable. Ask the server operator to configure it.");
if (!destination) return unavailable();
// Check scope before looking up the configured receiver: configuration never expands a grant or reveals
// another rig's destination. An explicit destination continues through the existing path below.
if (!scope?.destination(destination)) return outOfScope("Configured task destination");
try {
if (!await taskDestinationExists(client, destination)) return unavailable();
} catch { return unavailable(); }
}
if (scope && !scope.destination(destination)) return outOfScope(`Destination ${destination}`);
// A revision carries its original's lineage as a handoff does: the original's chain, then the original.
let lineage: { chainOfRecord: string[]; body: string } | undefined;
@@ -10,6 +10,7 @@ import type { AddressInfo } from "node:net";
import { serve, type ServerType } from "@hono/node-server";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { Client as ModernClient, StreamableHTTPClientTransport as ModernTransport } from "@modelcontextprotocol/client";
import type Database from "better-sqlite3";
import { createTestApp } from "../../daemon/test/helpers/test-app.js";
import { createDb } from "../../daemon/src/db/connection.js";
@@ -107,11 +108,12 @@ describe("rig mcp serve --http --grant-service on the daemon's routes", () => {
const rig = setup.rigRepo.listRigs?.().find((r: { name: string }) => r.name === rigName) ?? setup.rigRepo.createRig(rigName);
const node = setup.rigRepo.addNode(rig.id, `pod.${member}`, { role: "worker" });
setup.sessionRegistry.registerSession(node.id, `${member}@${rigName}`);
return node;
}
async function listen(header?: { name: string; value: string }) {
async function listen(header?: { name: string; value: string }, taskDestination?: string) {
const served = await startMcpHttpServer({ daemonUrl, host: "127.0.0.1", port: 0, tokenFile: path.join(home, "mcp-http-tokens.json"),
grantService: { url: serviceUrl, ...(header ? { header } : {}), timeoutMs: 1_000 } });
taskDestination, grantService: { url: serviceUrl, ...(header ? { header } : {}), timeoutMs: 1_000 } });
closers.push(() => served.close());
return served;
}
@@ -124,7 +126,8 @@ describe("rig mcp serve --http --grant-service on the daemon's routes", () => {
const result = await client.callTool({ name, arguments: args });
return { isError: result.isError === true, structured: result.structuredContent as Record<string, any>, text: (result.content as Array<{ text: string }>)[0]?.text ?? "" };
};
return Object.assign(call, { tools: async () => (await client.listTools()).tools.map((tool) => tool.name) });
return Object.assign(call, { tools: async () => (await client.listTools()).tools.map((tool) => tool.name),
catalog: () => client.listTools(), instructions: () => client.getInstructions() });
}
const post = (url: string, headers: Record<string, string> = {}) => fetch(url, {
@@ -134,6 +137,115 @@ describe("rig mcp serve --http --grant-service on the daemon's routes", () => {
const create = (sourceSession: string, destinationSession: string, body: string) =>
repo.create({ sourceSession, destinationSession, body, nudge: false } as never);
it("routes an address-free first task to the operator's receiver with caller attribution, retry identity and revision lineage", async () => {
seat("team-a", "lead"); // pod.lead exists without any delegates_to edge.
const { url } = await listen(undefined, "pod-lead@team-a");
const ask = await connect(url);
expect(rows()).toBe(0);
const catalog = (await ask.catalog()).tools;
expect(catalog.map((tool) => tool.name).sort()).toEqual(["rig_artifact_read", "rig_queue_create", "rig_queue_list", "rig_queue_show"]);
expect(catalog.find((tool) => tool.name === "rig_queue_create")?.inputSchema.required).not.toContain("destination");
expect(ask.instructions()).toContain("You may omit destination");
expect(ask.instructions()).not.toMatch(/rig_whoami|rig_team|rig_queue_claim|rig_queue_handoff|rig_queue_decide/);
const args = { qitemId: "qitem-first-task", body: "Create a useful file and return it" };
const first = await ask("rig_queue_create", args);
expect(first.isError).toBe(false);
expect(repo.getById(args.qitemId)).toMatchObject({ sourceSession: CALLER, destinationSession: "pod-lead@team-a", body: args.body });
const retry = await ask("rig_queue_create", args);
expect(retry.isError).toBe(false);
expect(retry.structured.qitemId).toBe(args.qitemId);
expect(rows()).toBe(1);
const revision = await ask("rig_queue_create", { body: "Improve the file", revises: args.qitemId });
expect(revision.isError).toBe(false);
expect(repo.getById(revision.structured.qitemId)).toMatchObject({ sourceSession: CALLER, destinationSession: "pod-lead@team-a",
chainOfRecord: [args.qitemId], body: `Revision of ${args.qitemId}.\n\nImprove the file` });
// A supplied destination still takes the original route, even with a different configured receiver.
expect((await ask("rig_queue_create", { destination: "other@team-a", body: "Explicit task" })).isError).toBe(false);
expect((await ask("rig_queue_show", { qitemId: args.qitemId })).isError).toBe(false);
fs.writeFileSync(path.join(shared, "first-result.md"), "Team's returned file");
db.prepare("UPDATE queue_items SET evidence_ref = ? WHERE qitem_id = ?").run(path.join(shared, "first-result.md"), args.qitemId);
expect((await ask("rig_artifact_read", { qitemId: args.qitemId })).text).toContain("Team's returned file");
expect((await ask("rig_artifact_read", { root: "proof", path: "first-result.md" })).structured.code).toBe("out_of_scope");
});
it("also serves an address-free first task to a client pinned to protocol 2026-07-28", async () => {
seat("team-a", "lead");
const { url } = await listen(undefined, "pod-lead@team-a");
const client = new ModernClient({ name: "first-task", version: "1" }, { versionNegotiation: { mode: { pin: "2026-07-28" } } });
await client.connect(new ModernTransport(new URL(url), { requestInit: { headers: { Authorization: `Bearer ${KEY}` } } }));
closers.push(() => client.close());
expect(client.getProtocolEra()).toBe("modern");
const tools = (await client.listTools()).tools;
expect(tools.find((tool) => tool.name === "rig_queue_create")?.inputSchema.required).not.toContain("destination");
const result = await client.callTool({ name: "rig_queue_create", arguments: { qitemId: "qitem-modern-first", body: "Create the file" } });
expect(result.isError).not.toBe(true);
expect(repo.getById("qitem-modern-first")).toMatchObject({ sourceSession: CALLER, destinationSession: "pod-lead@team-a" });
});
it("does not infer a receiver from grant metadata, caller identity or an empty queue", async () => {
const { url } = await listen(); // The fixture's environment.lead is deliberately not a routing contract.
const ask = await connect(url);
expect((await ask("rig_queue_create", { body: "First task" })).structured.code).toBe("task_destination_unavailable");
expect(rows()).toBe(0);
expect((await ask("rig_queue_create", { destination: "lead@team-a", body: "Explicit still works" })).isError).toBe(false);
});
it.each([CALLER, "cus-another-caller@team-a", "pod-lead@team-b"])("never uses configured receiver %s outside the caller's team scope", async (target) => {
const { url } = await listen(undefined, target);
const ask = await connect(url);
expect((await ask("rig_queue_create", { body: "First task" })).structured.code).toBe("out_of_scope");
expect(rows()).toBe(0);
});
it("requires one existing receiver and rejects ambiguous rig names without writing", async () => {
const { url } = await listen(undefined, "pod-lead@team-a");
const ask = await connect(url);
expect((await ask("rig_queue_create", { body: "Receiver absent" })).structured.code).toBe("task_destination_unavailable");
seat("team-a", "lead");
setup.rigRepo.createRig("team-a");
expect((await ask("rig_queue_create", { body: "Rig ambiguous" })).structured.code).toBe("task_destination_unavailable");
expect(rows()).toBe(0);
});
it("rechecks the grant and receiver after connection, with no write after removal or revocation", async () => {
const receiver = seat("team-a", "lead");
const { url } = await listen(undefined, "pod-lead@team-a");
const ask = await connect(url);
expect((await ask("rig_queue_create", { body: "Granted" })).isError).toBe(false);
setup.rigRepo.deleteNode(receiver.id);
expect((await ask("rig_queue_create", { body: "Receiver removed" })).structured.code).toBe("task_destination_unavailable");
seat("team-a", "lead");
answer = () => [200, active({ objects: { queue: ["show", "list"], artifact: ["read"] } })];
await expect(ask("rig_queue_create", { body: "Create permission removed" })).rejects.toThrow();
answer = () => [401, { schema: SCHEMA, status: "revoked" }];
await expect(ask("rig_queue_create", { body: "Revoked" })).rejects.toThrow();
expect(rows()).toBe(1);
});
it("rejects task destination configuration without grant service or with non-local seat syntax", async () => {
const base = { daemonUrl, host: "127.0.0.1", port: 0, tokenFile: path.join(home, "unused") };
await expect(startMcpHttpServer({ ...base, taskDestination: "pod-lead@team-a" })).rejects.toThrow("needs --grant-service");
for (const target of ["", "lead", "a@b@host", "person@external", "lead @team-a"]) {
await expect(startMcpHttpServer({ ...base, grantService: { url: serviceUrl }, taskDestination: target })).rejects.toThrow(McpHttpConfigError);
}
});
it("requires HTTP and a grant service for the task-destination CLI option", async () => {
const quiet = vi.spyOn(console, "error").mockImplementation(() => {});
const exitCode = process.exitCode;
try {
for (const args of [[], ["--http"]]) {
process.exitCode = undefined;
await mcpCommand().parseAsync(["serve", "--port", "1", "--task-destination", "pod-lead@team-a", ...args], { from: "user" });
expect(process.exitCode).toBe(1);
}
expect(quiet).toHaveBeenCalledWith("--task-destination needs --http --grant-service");
} finally {
process.exitCode = exitCode;
quiet.mockRestore();
}
});
it("asks the grant service on every request with the raw key and this daemon's self id, acts as the grant's seat, and never reads the token file", async () => {
const fileToken = await addGrant(path.join(home, "mcp-http-tokens.json"), { name: "local", seat: "local@team-a", rigs: ["team-a"], roots: [], workspace: false, owner: true });
const served = await listen({ name: "x-example-bypass", value: "preview-secret" });
@@ -73,6 +73,7 @@ it("serves a materialized, never-launched caller seat over a grant service on a
for (const [name, value] of Object.entries({ ...daemon.readEnv, OPENRIG_DB: dbPath })) if (value !== undefined) env[name] = value;
let log = "";
const server: ChildProcess = spawn(process.execPath, [rigBin, "mcp", "serve", "--http", "--bind", `127.0.0.1:${port}`,
"--task-destination", LEAD,
"--grant-service", `http://127.0.0.1:${(service.address() as AddressInfo).port}/api/connector/v1/grant`], { env, stdio: ["ignore", "pipe", "pipe"] });
server.stdout?.on("data", (chunk) => { log += chunk; });
server.stderr?.on("data", (chunk) => { log += chunk; });
@@ -98,9 +99,11 @@ it("serves a materialized, never-launched caller seat over a grant service on a
};
// The tools the grant's objects name.
expect((await client.listTools()).tools.map((tool) => tool.name).sort()).toEqual(["rig_queue_create", "rig_queue_list", "rig_queue_show"]);
const created = await call("rig_queue_create", { destination: LEAD, body: "please build it" });
const created = await call("rig_queue_create", { body: "please build it" });
const shown = await call("rig_queue_show", { qitemId: created.qitemId });
expect(shown.item).toMatchObject({ qitemId: created.qitemId, sourceSession: CALLER, destinationSession: LEAD });
const explicit = await call("rig_queue_create", { destination: LEAD, body: "Explicit destination still works" });
expect(explicit.destinationSession).toBe(LEAD);
const db = new Database(dbPath, { readonly: true, fileMustExist: true });
try {
+3 -1
View File
@@ -568,7 +568,7 @@ describe("MCP Server", () => {
await cleanup();
});
it("doesn't give an external caller's connection the member workflow or its authority, only the shared lines", async () => {
it("gives an external caller its request workflow without member tools or authority", async () => {
const server = createMcpServer(mockClient(), { seat: "cus-1-caller@team",
access: { principal: "cus-1", owner: false, rigs: ["team"], roots: [], workspace: false, external: { objects: { queue: ["create", "show", "list"] } } } });
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
@@ -579,6 +579,8 @@ describe("MCP Server", () => {
expect(instructions).toContain("reserved for a person");
expect(instructions).not.toContain(MEMBER_WORKFLOW_VERSION);
expect(instructions).not.toContain("basically authorized");
expect(instructions).toContain("You may omit destination");
expect(instructions).not.toMatch(/rig_whoami|rig_team|rig_queue_handoff|rig_queue_decide/);
await external.close();
await server.close();
});