/** * The MCP broker's call path (DES-MCP-TOOLS-001 ยง6; slice S3). A worker on any seat reaches an * upstream MCP tool only through here (the garden shim, `wicked-garden run mcp call`, sends * `POST /api/v1/mcp/call {token, subject, args}`), and every step below runs in order: * * 1-3. **Judge.** The subject is resolved in the registry (unknown, disabled, `gone` or changed * tools go to the engine as `registered: false`, D-5), then core's `evaluateMcpCall` verifies * the token, judges the call for the unit the token is bound to (D-1 phase role, policy, * first use, the run's mode) and records the claim. `deny` and `ask` never reach an upstream. * 4. **Budget.** At most {@link DEFAULT_MCP_CALL_BUDGET} invoked calls per unit attempt * (`WICKED_MCP_CALL_BUDGET` overrides it). * 5. **Breaker.** After {@link BREAKER_THRESHOLD} consecutive upstream failures a tool is refused * for {@link BREAKER_OPEN_MS}; then one call is let through, and one more failure reopens it. * 6. **Invoke** with a deadline. The upstream's secret is resolved here, and only here. A `rest` * tool (slice S5a, `rest.ts`) sends only its allowlisted arguments, and only to its pinned * base URL: a request that would leave it is refused unsent, recorded as `guard_error` * (`rest_host_escape`, I6). * 7. **Retry** READ tools only, at most {@link MAX_READ_RETRIES} times, and only on transport * errors, 429 and 5xx. A write or destructive tool is never retried: a second attempt could * apply the write twice. * 8. **Post.** The result is scrubbed (the exact secrets injected on this call, then credential * field names and value shapes, D-2), then core's `evaluateMcpOutput` judges it; a deny * withholds it. A crash anywhere here is `guard_error`: refused, fail closed. * 9. **Record** (D-3). Every judged call is written to `calls.ndjson`; a call whose record cannot * be written is refused and its result withheld. The record then rides `/ws` * (`mcpCallCompleted`) and the bus (`wicked.crew.mcp_call.completed`). * * The reserved server `estate` is not a registered upstream: `mcp:estate/` is the estate * relay (wicked-core#1018, `estate-relay.ts`), a container seat's only way to its run's estate. * Its judge is the relay's own (the token resolved to its unit by the engine, a fixed tool list, * the run's scope); budget, record and publish are the steps above. * * A call that ran and then could not be recorded HAS happened upstream; refusing it withholds the * result from the worker, it does not undo the side effect. The record is written after the call * because the span it records (timing, status, bytes) only exists then. */ import type { McpCallBody, McpCallOutcome, McpCallRecord, McpCallResponse } from '../core/types.js'; import type { McpEngineGate } from '../core/adapter.js'; import type { McpCallRecordSink } from './call-records.js'; import { type Invoker } from './invoke.js'; import type { McpRegistry } from './registry.js'; import { type EstateRelay } from './estate-relay.js'; /** The bus event each call record rides. */ export declare const MCP_CALL_COMPLETED = "wicked.crew.mcp_call.completed"; export declare const DEFAULT_MCP_CALL_BUDGET = 200; export declare const BREAKER_THRESHOLD = 5; export declare const BREAKER_OPEN_MS = 60000; export declare const MAX_READ_RETRIES = 2; /** The HTTP status each outcome is answered with. */ export declare const OUTCOME_STATUS: Record; /** A call the broker answered: the HTTP status and the body. */ export interface McpBrokerAnswer { status: number; body: McpCallResponse | { error: string; code: 'invalid_token' | 'bad_request'; }; } export interface McpBrokerDeps { registry: Pick; /** The engine's gate, read per call; `null` = the linked engine has none (every call refused). */ engine: () => McpEngineGate | null; records: McpCallRecordSink; invoke?: Invoker; /** Called with every record once it is written (`/ws` + bus). Never throws into the call. */ publish?: (record: McpCallRecord) => void | Promise; now?: () => number; sleep?: (ms: number) => Promise; budgetPerUnit?: number; timeoutMs?: number; log?: (message: string) => void; /** The estate relay behind `mcp:estate/` (wicked-core#1018); absent = those calls are refused. */ estate?: EstateRelay; } export declare function budgetFromEnv(env?: NodeJS.ProcessEnv): number; export declare class McpBroker { private readonly deps; private readonly invoke; private readonly now; private readonly sleep; private readonly budget; private readonly timeoutMs; private readonly used; private readonly breakers; constructor(deps: McpBrokerDeps); call(body: McpCallBody): Promise; /** * `mcp:estate/`: the estate relay (module doc of `estate-relay.ts`). Resolve the token to * its unit and scope, refuse what the relay never makes, spend the unit's budget, run the call on * the host's estate, bound and scrub its result, then record it like any brokered call. A landed * proposal is also handed to the relay's audit hook. */ private callEstate; private record; /** * Answer a call that does not hand a result back, after recording it. A refusal is a refusal * whether or not its record lands; a failed write is logged, and the answer keeps its outcome * (a deny is never re-labelled `guard_error`, I5). */ private refuse; private publish; } //# sourceMappingURL=broker.d.ts.map