Anchorage API reference
    Preparing search index...

    Module @proofoftech/breakwater

    breakwater

    Runtime guardrails for Mastra agents.

    @proofoftech/breakwater adds enforcement at the places where an agent can accept data, emit data, or cause a side effect. It provides Mastra processors for policy and role checks, an enforced connector wrapper for tools, a shared audit stream, and approval-gated adapters for Claude Code and Codex.

    Model instructions are not an authorization boundary. A production agent also needs code that can:

    • reject an unauthorized caller before the model runs;
    • inspect input, answer text, reasoning, and streaming object snapshots;
    • stop undeclared network access and unapproved writes at the tool boundary;
    • make retries idempotent and enforce execution budgets;
    • produce structured evidence for every allowed, denied, and failed decision.

    These controls operate at different boundaries and are designed to be used together.

    Boundary Public API What it enforces
    Guarded agent entry createGuardedAgent() Mandatory role and policy order, fixed execution limits, and a narrow call-option allowlist
    Agent input and output PolicyEngine Content policies across input, final output, and streaming channels
    Agent input RBACMiddleware Actor presence and an explicit role allowlist
    Tool execution createConnector() Permission manifests, egress, approvals, dry runs, idempotency, rate limits, isolation, and custom policies
    Every gate AuditLogger Structured, attributed audit events with sink failure isolation
    Local coding agents Agent CLI adapters The connector controls plus safe argv construction, timeouts, bounded output capture, and redacted diagnostics

    breakwater is safety middleware, not an agent runtime. Use @proofoftech/flowsafe when runs must survive restarts, wait for human approval, stream live state, schedule work, or run durable agent loops on Cloudflare.

    breakwater is ESM-only, requires Node.js 22.13.0 or newer (engine range >=22.13.0), and requires @mastra/core 1.73.0.

    npm install @proofoftech/breakwater @mastra/core@1.73.0
    

    Connector authors who define Zod schemas should also declare Zod directly:

    npm install zod
    

    The package supports the root import and these focused subpaths:

    import { PolicyEngine } from '@proofoftech/breakwater';
    import { createGuardedAgent } from '@proofoftech/breakwater/agent';
    import { AuditLogger } from '@proofoftech/breakwater/audit';
    import {
    createConnector,
    invokeConnector,
    } from '@proofoftech/breakwater/connector-sdk';
    import { RBACMiddleware } from '@proofoftech/breakwater/rbac';
    import { createCodexConnector } from '@proofoftech/breakwater/agent-cli';
    import { denyPatterns } from '@proofoftech/breakwater/policy-engine';

    Use createGuardedAgent() for supported protected agent execution. The factory installs role-based access control (RBAC) before application input processors, checks input asset URLs before the policy engine, then installs policy after application output processors.

    import { RequestContext } from '@mastra/core/request-context';
    import {
    ACTOR_CONTEXT_KEY,
    AGENT_AUDIT_CONTEXT_KEY,
    AuditLogger,
    createGuardedAgent,
    denyPatterns,
    piiSecrets,
    } from '@proofoftech/breakwater';

    const audit = new AuditLogger();
    const agent = createGuardedAgent({
    id: 'guarded-agent',
    name: 'Guarded agent',
    instructions: 'Answer only from approved business data.',
    model: 'openai/gpt-5',
    allowedRoles: ['operator', 'admin'],
    policies: [
    denyPatterns(['private signing key']),
    piiSecrets({ detectors: ['privateKey', 'awsAccessKey', 'jwt'] }),
    ],
    audit,
    maxSteps: 4,
    toolChoice: 'auto',
    });

    The host must authenticate the caller and derive trusted request context before invoking the handle:

    const requestContext = new RequestContext();
    requestContext.set(ACTOR_CONTEXT_KEY, {
    id: 'user-42',
    role: 'operator',
    });
    requestContext.set(AGENT_AUDIT_CONTEXT_KEY, {
    agentId: agent.id,
    runId: 'server_minted_run_id',
    entryPath: 'http-start',
    });

    const result = await agent.generate('Summarize the account.', {
    requestContext,
    });

    The handle exposes only unstructured generate() and stream(). Each call requires requestContext and may accept only runId, memory, and abortSignal. The factory fixes maxSteps and toolChoice, forces streaming policy hold-back, disables background continuations, and rejects processor, model, and structured-output overrides. It also rejects a configuration field that is neither one of its own options nor a Mastra AgentConfig field it keeps, so a misspelled option cannot reach Mastra unread.

    allowedRoles is an exact allowlist with no role hierarchy.

    A guarded agent refuses title-enabled configuration whenever it resolves a Memory, including function-valued and inherited memory. It also checks a Memory instance at construction. The guarded agent never generates a thread title, including when a host starts the durable loop with call-level memory.options.generateTitle. Disable generateTitle on the guarded agent's Memory: Mastra's title model call bypasses the input and output policies.

    The guarded durable loop loads the agent's memory, including thread history, working memory, and semantic recall, as the standard loops do. A memory input processor error, such as a failed thread-history read, or a failure to resolve the memory processors stops a durable call with input processor failed and one agent.input.processor error event. The event names the processor's id (message-history for history), or breakwater-memory when resolving the memory processors fails. A failed first memory lookup rejects the durable call with that error and writes no audit event. On generate() and stream(), Mastra rejects a memory input processor error and Breakwater writes no audit event.

    On a thread with stored memory, Mastra's MemoryInputFilter keeps input after the last assistant message. If the input ends with an assistant message, it keeps that message's trailing client tool outcomes. A caller message that reuses a stored message's id takes the stored copy; Mastra merges only client tool outcomes that advance a stored pending call, not replacement text. These operations precede Breakwater's input policies. Dropped caller content reaches neither the model nor the saved thread, and records no tripwire or audit event. Send the new turn rather than re-sending history.

    A caller's result, error or denial for a stored pending client tool call is evaluated as caller input when sent under the stored assistant message's id. Breakwater records client tool outcomes before memory processing and evaluates the merged parts that came from the caller. A stored output the caller does not re-send is not re-evaluated; one re-sent in the same state is. A caller message with two client tool outcomes for one tool call stops with input processor failed and an agent.input.processor error event naming breakwater-client-tool-outcome-recorder. When a host uses PolicyEngine as a processor on a plain Mastra agent with memory, it evaluates every client tool outcome in a merged message, including stored ones, because it has no guarded-agent provenance record.

    The guarded generate() and stream() methods refuse a caller message with role: 'system' with a TypeError, at the top level or inside the one level of nested list that Mastra flattens, because Mastra would give it instruction authority outside the input policies; put system instructions in the agent's instructions. They refuse a list nested deeper than that level too.

    The input policies read the text Mastra renders into the model prompt from the call's own messages, through Mastra's own message conversion, and every stored model output those messages carry. Replayed tool calls, tool results, reasoning and signals that remain caller input after memory processing are checked like new input. For a client-only tool with toModelOutput and no execute, the guarded agent maps, within the call's input, each replayed result part and each result part in a message an application input processor changes, stored results included, after application input processors and the input asset check, so the input policies read the mapped output the model receives. The mapper runs once per result part before the tool's onOutput hook. The policies also read the mapped version of a processor-changed message in the call's input, which maxTextLength counts. A mapper error on a result in either case, stored results in a processor-changed message included, or a tool-resolution or mapped-output normalization error stops the call with input processor failed and one agent.input.processor error event whose detail names breakwater-client-tool-output. They also read what the agent's application input processors add to the prompt, or change in it, outside the call's messages: each such system message, with the provider options Mastra sends with it, and each such message of another source, such as a memory, context or response message or a history message a processor rewrites through any source, read as the call's messages are, and a remembered message carrying the caller's client tool outcome that a processor keeps but moves out of the call's input, read whole, stored history included. A result in such a moved message gets no new toModelOutput mapping; an output already cached on it is read by the policies and sent to the model. A call the input chain refuses has its input, its response messages, and every other non-system message its application input processors added or changed, the refusing processor's own included, and a remembered message carrying the caller's client tool outcome a processor keeps but moves out of the call's input, removed from Mastra's message list before it stops. It saves none of them to memory, Mastra's durable loop generates no thread title from them, and on generate() and stream() the result's messages and rememberedMessages omit them, a history message such a processor changed included. Apart from client tool outcomes described above, history that memory loads and the agent's instructions are not evaluated unless an application input processor changes them, so check content where it is written to memory or returned by a tool. A caller message that carries a provider option a model adapter bundled with Mastra renders into its request as message content or role, such as any openaiCompatible option or an Anthropic document title, stops the call before the model. Provider options that replayed responses store as content, such as Anthropic citations, are read; provider metadata such as signatures, cache control and provider-held references like an OpenAI Responses itemId is not. A provider-executed tool result that Mastra places in an assistant message is read without the encrypted payload, file reference, base64 document or generated image that the provider returned, such as Anthropic web search's encryptedContent, where the root of the result has the shape a model adapter bundled with Mastra stores for that tool. The same field elsewhere in a result is read, such as in the tool definitions of an OpenAI Responses tool_search result, which that adapter sends as given when store is false, and so is the whole of a result that names a Google server-tool call or answers an Anthropic MCP call, which those adapters send on as content. Tool-call ids and binary file and image data are not inspected, and neither is the base64 data of a tool output's content items, which the adapters that send tool output as JSON text send as text. File and image media types and decoded text/* payloads, including data: URL payloads, are read as text; for text/* network URL data, the policies read the URL string, not the downloaded content. The Vercel AI Gateway forwards every provider option to a hosted service whose rendering Breakwater does not know. A caller message whose role, part type or tool-invocation state Breakwater does not classify, or that Mastra's conversion cannot read, stops the call before the model. Provider options given as a call option are outside what the input policies read, and the guarded handle refuses them.

    The memory call option takes only thread, a non-empty id or an object whose only field is that id, and resource, a non-empty string, and Mastra receives a frozen copy of them; set memory configuration and thread metadata on the agent's Memory.

    allowedInputAssetOrigins lists the http(s) origins user file and image parts may name as network URLs, whoever fetches them. The input asset check runs after application input processors and before the policy engine on generate(), stream(), and the durable loop; it checks the call's input, memory-loaded thread history, and processor additions. It runs again before each model step on the message list as it stands, so signals Mastra drains into a running loop are checked before Mastra downloads their assets. On the durable loop, signals drained before the first model step join the prompt after that step's check and bypass it.

    The check refuses other network schemes, URLs with credentials, and origins not listed. An absent or empty list refuses every network URL, while data: URLs remain inline data. A refusal stops the call and records an agent.input.asset event without the URL. Input the step cannot read stops the call with input message content is not classified and one agent.input.policy error event, as the policy engine's reading does.

    The client tool output step checks network URLs in the tool outputs the call supplies with the same rules, reasons, and audit events. It checks outputs it maps through a client-only tool's toModelOutput in caller input or a message an application input processor changes, including a stored client result it maps in a processor-changed remembered message. It also checks every stored model output on a caller message whose id memory does not hold, including server-tool outputs in a replayed transcript; stored outputs on remembered messages are not checked, including a completed call the caller re-sends. It reads the url of image-url and file-url content items and the data of media, image-data, and file-data items; data: URLs and base64 data stay inline data.

    A tool-output refusal's tripwire carries processorId: 'breakwater-client-tool-output'. A mapper error on such an output stops the call with input processor failed before the origin check. A stored output an application input processor writes on a remembered message or outside the call's input, or a caller-supplied stored output it moves out of that input, is not origin-checked; the input policies still read both. A model provider may fetch URLs in these unchecked outputs or remembered outputs.

    A refusal at a later step removes the call's input, including drained signals and remembered messages carrying the caller's client tool outcome, and its response messages from Mastra's message list. Nothing more of the refused call is saved to thread memory, but what Mastra already saved stays: on a durable run, the input and earlier steps saved at a suspension for tool approval, and each step saved under savePerStep. The call's input and response messages are removed regardless of what added them; other messages an application input processor added or changed outside those sources stay on the list but are not saved. On the durable loop, a signal Mastra drains after a step refusal continues the run with the refused call's input and responses removed, and the arriving signal is checked at its step.

    A resumed durable leg runs the check before each step, so a thread whose history names an origin absent from the configured list is refused on resume. A guarded agent that accepts remote file or image URLs, including URLs already stored in thread history, must list their origins. This option is independent of connector egress allowlists.

    Under the pinned Mastra version, parsed structured output bypasses the output-processor chain and reaches messages, persistence, and observability hooks before a wrapper could inspect it. Both guarded methods therefore reject structuredOutput before model execution. The factory also rejects object-only policies because no supported guarded invocation can expose their required channel. Structured-output support requires a future pre-persistence gate, not a post-generation wrapper.

    A host that starts a guarded agent's durable loop itself calls assertNoGuardedSystemMessages() on the call's messages and assertAcceptedCallProviderOptions() on its call-level provider options, as Flowsafe's agent host does. The call-level check accepts only a closed list of generation settings and refuses everything else.

    Application input processors may implement only processInput. The agent applies a processor's return value to the call's message list as Mastra's durable loop applies one, on both loops, except that a returned message Mastra holds as both remembered and call input stays both, such as a remembered assistant message with the caller's client tool outcome. A role: 'system' entry in a returned array becomes a system message on generate() and stream() too. A processor that throws other than through its abort or a TripWire, returns a value that cannot be applied, adds or changes a system message whose content is not a string or a list of text parts or that holds one of those fields in an accessor, or adds or changes a message that structuredClone cannot copy stops the call with the reason input processor failed and an agent.input.processor error event. What it adds or changes outside the call's input, and a remembered message carrying the caller's client tool outcome it keeps but moves out of that input, are read by the input policies as described above, so a policy hit on host context a processor adds, or on a system message it edits, the agent's instructions included, refuses the call, and text a processor adds is read even when a later processor removes it. The agent compares the message list before each processor runs with the list once its returned promise settles, so a change the processor makes after that, or content it serves through a Proxy that shows the check other values than Mastra renders, is not read. A processor registered on a Mastra through the agent receives that Mastra, as a Mastra BaseProcessor expects. Application output processors must implement both processOutputStream and processOutputResult.

    Both application input and output processors refuse processToolResult at construction because Mastra runs that hook on tool results after Breakwater's policies on the standard and durable loops. Move this logic into the tool's own result handling or an allowed processor hook.

    The factory refuses errorProcessors and errorProcessorDefaults at construction and sets errorProcessorDefaults: false. Guarded agents run none of Mastra's default error processors: provider-history rewriting, prefill retry or transient stream retry. Transient stream failures and prefill rejections therefore receive no automatic retry from those processors.

    Actor carries an optional kind — human, service, agent, or system. An absent kind means human, so existing hosts are unaffected. An actor value with a field other than id, role and kind resolves to no actor and is denied, so a misspelled kind cannot read as human.

    createGuardedAgent() and RBACMiddleware both accept allowedPrincipalKinds, which defaults to ['human']. An agent written before this option denies every automated caller until you widen it:

    const agent = createGuardedAgent({
    // ...
    allowedRoles: ['operator', 'admin'],
    allowedPrincipalKinds: ['human', 'system', 'service'],
    });

    The gate checks kind before role, and it does not consult allowedRoles for a non-human kind. An automated caller carries a role only because Actor.role is required; consulting it would either admit whatever role the host projected, or force you to allow that role and thereby admit real humans holding it. Both the processor gate and the direct-call gate enforce this.

    Flowsafe's agent host declares the matching half with allowedAutomation, which names each admitted kind together with the exact entry paths it may arrive on. See Durable agents.

    The narrow handle prevents accidental use of raw Mastra execution methods. It is not a sandbox against hostile code in the same JavaScript process. Use the authenticated Flowsafe agent host when callers cross an HTTP or principal boundary.

    PolicyEngine accepts any PolicyEvaluator. Policies can select the input or output phase and the output channels they inspect. Construction snapshots the policy list, selector arrays, names, hold-back hints, and evaluator callable so later replacement cannot change enforcement. Class-based evaluators retain their original receiver, so their instance fields, parameter properties and private fields keep working. Evaluator-owned instance or closure state remains application-owned and is not deep-cloned. A plain-object policy, such as a literal or a spread of a factory's output, may carry only the PolicyEvaluator fields:

    • answer is client-visible text and is also the only input channel.
    • reasoning is the model reasoning trace.
    • object is the latest canonical JSON structured-output snapshot.

    Use createContentPolicyGate() when trusted host code must apply the same ordered input-policy contract before content enters a framework-owned path that does not traverse Mastra's input processors. The gate accepts the exact text the downstream model will see plus an optional trusted RequestContext. An input that is not an object with a string text, that carries another field, or whose requestContext is not a RequestContext returns the error state without reaching a policy. It returns only allowed, denied, or evaluator-error state. On denial or evaluator error, the audit sink records the policy name with a static reason; evaluator reasons, inspected content, and thrown values reach neither the caller nor the audit sink. Every registered policy must be able to run on the input answer channel — one that could never be evaluated here is rejected at construction, not skipped. That includes maxTextLength, which defaults to the output phase: pass maxTextLength(n, { phases: ['input'] }) to use it at this boundary. The gate needs at least one policy; an empty list is rejected, because a gate with no policy allows every input.

    PolicyEngine aborts every policy denial with policy '<name>' denied the <phase>, discarding the evaluator's reason. Use policyDenialReason(policyName, phase), exported from @proofoftech/breakwater and @proofoftech/breakwater/policy-engine, to match result.tripwire.reason or a stream's tripwire chunk. Both Mastra agent loops use this reason in their tripwires, logs, and spans. Audit events use the static policy denied reason and identify the policy in detail.policy.

    The included policies are:

    Policy Default coverage Purpose
    denyPatterns(patterns) Input and output; answer, reasoning, and object Deny case-insensitive string matches or caller-supplied regular expressions
    maxTextLength(maxChars) Output answer Cap accumulated output length at maxChars, a finite number of at least 0
    piiSecrets(options) Input and output; answer, reasoning, and object Detect email, SSN, phone, Luhn-valid cards, AWS keys, PEM headers, JWTs, secret assignments, and high-entropy tokens
    classifierPolicy(options) Input and output answer Delegate to a synchronous or asynchronous classifier, with cadence and timeout controls

    piiSecrets() supports detector selection, exact or regular-expression allowlist exemptions, an entropy threshold, phase and channel selection, and a hold-back hint override. The entropyThreshold must be a number greater than 0 and at most about 6.066 bits per character, the entropy of a candidate that uses each of the 67 characters a candidate is drawn from equally often, and so the highest entropy a candidate can reach; a threshold at that maximum still detects such a candidate. Its detectors and denyPatterns() are best-effort text inspection. Unicode tricks, alternate encodings, or deliberately shaped secrets can evade pattern-based controls. Each included policy throws when called with a text that is not a string, which the engine and the content gate turn into their error path.

    classifierPolicy() fails closed when the classifier throws, returns no decision or exceeds its configured timeout: PolicyEngine records an error event and stops the call at input and in-stream on both of Mastra's agent loops, and throws an Error with the fixed policy evaluation failed message and no cause at the final result, which stops Mastra's standard loop. On Mastra's durable loop, output policies stop the stream a subscriber receives, including hold-back's terminal classification. Mastra logs a result-phase refusal; the saved thread message and returned result come from model output and are not filtered by output policies. The classifier's evaluateEveryChars cadence must be a positive safe integer. It has no automatic hold-back window because a classifier has no bounded match length. To buffer a whole channel until classification finishes, set holdBackChars: Number.POSITIVE_INFINITY on the returned evaluator.

    Guarded stream() disables eager tool execution, so no server tool runs before the step's stream-phase policies judge that step. Mastra's durable loop runs tools after the model step regardless.

    Without holdBack, each accumulated chunk is evaluated before that chunk is emitted, but text from earlier chunks may already be visible when a later chunk completes a forbidden pattern.

    With holdBack: true, the engine retains each policy's declared trailing window and releases only evaluated text. String patterns and the built-in secret detectors provide bounded windows. A regular expression in denyPatterns() defaults to buffering the full segment unless holdBackChars supplies a safe bound. A holdBackChars hint must be a number of at least 0, or Infinity. At a text or reasoning segment's end, or at finish when no end chunk arrives, hold-back classifies any text still below classifierPolicy()'s cadence before releasing the held tail. With holdBackChars: Infinity, a denied segment emits nothing. With a finite window, previously released text stays visible; the held suffix is released only after terminal classification. A host-written cadence evaluator does not receive this terminal classification behavior. The guarantee is per stream segment; text released at the end of an earlier segment cannot be withdrawn if a match completes in a later segment. A text or reasoning chunk whose text is not a string, or a step whose reasoning text is not a string, aborts the stream instead of passing unevaluated.

    Under the supported @mastra/core peer, only object chunks that traverse the processor chain reach a standalone engine. The engine validates those chunks as JSON, evaluates the canonical snapshot, and forwards the same canonical clone. Parsed generate() results and core's structured-output processor chunks bypass that chain. If an object-only policy sees no processor-visible object chunk, the engine requires an audit sink and aborts at the result boundary. JSON carried as answer text is still inspected by answer-inclusive policies.

    Create tools through createConnector() when policy must hold on agent, workflow, nested, and direct calls.

    import {
    createConnector,
    invokeConnector,
    } from '@proofoftech/breakwater/connector-sdk';
    import { z } from 'zod';

    const accountLookup = createConnector({
    id: 'crm.get-account',
    description: 'Read one CRM account',
    inputSchema: z.object({ accountId: z.string().min(1) }),
    outputSchema: z.object({ name: z.string() }),
    permissions: {
    sideEffect: 'read',
    egress: ['api.example-crm.com'],
    },
    policies: {
    networkEgress: { allowedDomains: ['api.example-crm.com'] },
    },
    execute: async ({ accountId }, _context, runtime) => {
    const response = await runtime.fetch(
    `https://api.example-crm.com/accounts/${accountId}`,
    );
    const account = (await response.json()) as { name: string };
    return { name: account.name };
    },
    });

    Call the connector outside an agent loop through the supported direct boundary:

    const account = await invokeConnector(accountLookup, {
    accountId: 'account_123',
    });

    Pass a trusted RequestContext when the connector uses grants, identity, dry-run, idempotency, or isolation keys. The options accept only requestContext, abortSignal, observe and toolCallId, and a present requestContext must be a RequestContext; anything else is refused with CONNECTOR_INVOCATION_OPTIONS_INVALID. invokeConnector() preserves Mastra schema validation and every Breakwater gate. It rejects plain tools and connectors whose ID, execution function, or schema surface changed after construction. Validation failures throw a redacted ConnectorValidationError with a stable kind/code, connector ID and input or output phase.

    The permission manifest is enforced:

    • sideEffect classifies the connector as read, write, destructive, or idempotent; any other value fails at construction.
    • egress lists exact hosts or leading *. wildcards. The organization allowlist gates the declaration, and runtime.fetch gates each actual request and redirect hop.
    • requiresApproval makes the server-minted approval grant mandatory.
    • requiredPermissions makes the trusted principal-permissions projection mandatory: the executing principal must hold every listed identifier, checked before dry-run and before the approval grant.
    • dryRun requires dryRunExecute and allows callers to request a side-effect-free simulation.
    • idempotencyKey requires a per-call key and a configured store.
    • rateLimit declares a fixed-window budget and requires a configured store.
    • requiresApproval, dryRun, idempotencyKey and background are booleans when present, and a field outside PermissionManifest fails at construction, so a misspelled requirement is refused rather than ignored.
    • background lets a read-only connector run as a Mastra background task and receive _background overrides. Mastra still owns actual background-task eligibility. Without the opt-in, the wrapper refuses a call Mastra's standard agent loop runs as a background task in the dispatching process; the refusal does not reach Mastra's durable loop, static background executors, calls nested inside background work, or a Flowsafe BackgroundTaskHost executor. The refusal is not final: Mastra retries a refused task, and a Mastra that starts on the same storage can recover a task that is still queued, or running with retries left, through a static executor. createGuardedAgent() disables background dispatch. On a raw Mastra agent, do not make a connector without the opt-in background-eligible or register one as a BackgroundTaskHost executor; see Run connectors in the background.

    Custom ToolPolicyEvaluator instances run before execution. Included evaluators cover declared network egress, cross-workflow scope, required tenant scope, and the direct-call background override defense. policies, like the connector configuration itself, accepts only its declared fields; networkEgress must be an object and evaluators an array of evaluators. The connector authoring guide contains the complete manifest, storage, invocation, egress, and testing contract.

    Use singleTenantConnectorPolicies() when one physically isolated deployment serves one organization. It requires D1-backed stores for declared idempotency and rate limits, production audit export or an explicit development opt-out, an organization egress allowlist, principal-permission wiring, and the background-execution policy. Set its idempotencyKeyMigration acknowledgement only after legacy connector writers sharing the D1 store are stopped and drained. The preset rejects tenantIsolation(), weakened destructive approval, incomplete manifest policy, and replacement of validated policy members.

    Build the preset once and pass the returned frozen policy set to each connector. Read Connector interface for the configuration contract.

    The connector wrapper reads these keys:

    Constant Runtime key Value Who should set it
    ACTOR_CONTEXT_KEY breakwater.actor { id, role, kind? } and no other field Authenticated host or getActor
    CONNECTOR_GRANTS_CONTEXT_KEY breakwater.connectorGrants ConnectorApprovalGrant[] Trusted approval service only
    CONNECTOR_EXECUTION_CONTEXT_KEY breakwater.connectorExecution ConnectorExecutionIdentity Trusted runtime only
    PRINCIPAL_PERMISSIONS_CONTEXT_KEY breakwater.principalPermissions PrincipalPermissions or null Trusted host resolver only
    DRY_RUN_CONTEXT_KEY breakwater.dryRun true; another present value but false is denied with DRY_RUN_INVALID Caller requesting simulation
    IDEMPOTENCY_KEY_CONTEXT_KEY breakwater.idempotencyKey Non-empty string Host-derived operation identity
    ISOLATION_SCOPE_CONTEXT_KEY breakwater.isolationScope Opaque non-empty string; another present value is denied with ISOLATION_SCOPE_INVALID Multi-tenant runtime only
    WORKFLOW_SCOPE_CONTEXT_KEY breakwater.workflowScope Current workflow ID Workflow runtime only

    Approval, permission, and isolation values are capabilities. Never accept them from a request body, model output, tool result, or client-controlled header. Flowsafe derives structured approval grants from approved records, projects the server-resolved principal permissions, and mints the current execution identity, workflow scope, and run ID on each run leg. Its physical data plane reserves and drops the isolation scope.

    Mastra's native requireApproval pauses an agent run, but it does not replace the Breakwater grant. Every execution path checks the structured grant against the runtime-owned identity. Legacy connector ID arrays fail closed.

    Durable-agent approvals use tool-call scope: connector ID, workflow, run, exact (stepPath, suspendedAt, resumeCount) suspension, and Mastra toolCallId must match. Workflow approvals use suspension scope because Mastra exposes no reproducible tool-call identity for an arbitrary workflow gate. Trusted standing grants use an explicit run scope.

    The same durable tool-call attempt may retry with the same toolCallId. A new model tool call has a new ID and requires approval. Use connector idempotency for side-effect replay protection; the grant is not a one-shot token.

    Development stores keep state in one JavaScript isolate:

    • InMemoryIdempotencyStore provides atomic same-isolate reservations and bounded replay storage. Its maxEntries bound must be a positive safe integer, and an option other than maxEntries is refused.
    • InMemoryRateLimitStore provides fixed windows per isolate.

    Production Cloudflare deployments can use:

    • D1IdempotencyStore, which uses an atomic insert claim, lease tokens, and stale-pending takeover across isolates.
    • D1RateLimitStore, which atomically increments shared fixed-window rows.

    The store's reach is the control's reach. In a Durable Object per run, in-memory rate limits become per-run budgets and in-memory replay protection does not cross isolates. Use shared stores when the declared behavior must hold across runs.

    Set D1IdempotencyStore.pendingTtlMs above the longest possible execution. The default is 900,000 ms. Agent CLI connectors reject a configured pending TTL that is not greater than their execution timeout. The store accepts only a positive safe integer up to 8,640,000,000,000,000 ms.

    New idempotency records use an opaque collision-proof v2 key. Before allowing new v2 records, set idempotencyKeyMigration: 'legacy-writers-drained' only after every old writer sharing the store has stopped and drained. Safe unscoped legacy records remain replayable. Scoped legacy records and unscoped business keys containing : are ambiguous and fail closed until an operator maps them to one proven identity. Atomic custom stores must implement non-mutating inspect() so pending legacy work cannot look absent.

    D1IdempotencyStore persists JSON-native connector results (plus a top-level undefined). It rejects values such as Date, Map, non-finite numbers, sparse arrays, or nested undefined that JSON would silently change; the wrapper then uses its existing degraded-final-write behavior rather than creating a type-changing replay record.

    Breakwater stores only the connector result. It does not hash arbitrary input or reject a changed body under the same key. Applications that promise request mismatch detection must canonicalize the request and store a fingerprint with the result at their gateway boundary.

    A fixed window can admit traffic on both sides of a boundary, approaching twice the nominal count in a short interval. Use another RateLimitStore implementation if you require token-bucket or GCRA semantics. Counts must be safe integers from 1 through Number.MAX_SAFE_INTEGER. D1RateLimitStore commits its increment and rollover cleanup in one D1 batch, so cleanup failure cannot consume quota for a rejected call.

    Every gate writes AuditEvent records with a timestamp, actor, action, resource, decision, optional reason, and optional detail.

    import {
    AuditLogger,
    combineAuditSinks,
    metricsAuditSink,
    } from '@proofoftech/breakwater/audit';

    const audit = new AuditLogger({
    maxBuffered: 2_000,
    sink: combineAuditSinks(
    metricsAuditSink(metricsRecorder),
    async (event) => auditQueue.send(event),
    ),
    onSinkError: (error, event) => {
    reportAuditExportFailure(error, event.action);
    },
    });

    AuditLogger keeps an in-memory ring buffer, defaulting to 1,000 events; maxBuffered must be a non-negative safe integer, and sink and onSinkError must be functions. hasExternalSink() answers true only for a sink function. Synchronous and asynchronous sink failures do not abort the guarded operation; they are reported through onSinkError. combineAuditSinks() runs every sink and aggregates failures; it needs at least one sink, and each must be a function. metricsAuditSink() emits the breakwater.audit.decision counter and observes breakwater.audit.duration_seconds when an event contains a finite, non-negative detail.durationSeconds; its recorder must have increment and observe functions. Both refuse other input when they are built, since the sink they return counts as external.

    Generic connector exceptions are rethrown to the caller, but their arbitrary messages are not copied into connector audit events. Audit reasons remain static unless breakwater created a private, safe error summary.

    Connector audit events also use trusted breakwater.auditContext correlation. Host-derived agent, deployment (tenantId), run, thread, resource, entry-path, and principal fields override same-named decision detail. Unknown and non-scalar context fields remain excluded. A present value that is not an object, lacks a non-empty agentId or entryPath, carries an unknown field, or carries an optional field that is neither undefined nor a non-empty string makes each boundary record an audit.context error event before its decision; the decision itself is unchanged.

    For durable Cloudflare Queues to SIEM export, use the flowsafe audit-export subpath.

    The agent CLI subpath wraps Claude Code and Codex as write-class, approval-required connectors.

    import { RequestContext } from '@mastra/core/request-context';
    import {
    createCodexConnector,
    DRY_RUN_CONTEXT_KEY,
    invokeConnector,
    } from '@proofoftech/breakwater';

    const codex = createCodexConnector({ cwd: '/srv/workspace' });
    const requestContext = new RequestContext();
    requestContext.set(DRY_RUN_CONTEXT_KEY, true);

    const preview = await invokeConnector(
    codex,
    {
    prompt: 'Add unit tests.',
    model: 'your-model-id',
    },
    { requestContext },
    );

    Set cwd on the connector from trusted host configuration; this is the recommended setup. The workspace is then fixed for every call, cwd leaves the input schema the model sees, and a cwd in a call's input is dropped by validation. Without the option, the model chooses the directory through its input, which the approver sees but no gate checks.

    The default runner is Node-only and:

    • spawns an argv array without a shell;
    • appends -- and the real prompt as the final positional argument;
    • selects workspace editing with Claude Code acceptEdits or Codex workspace-write;
    • terminates the child process tree after the configured timeout (a POSIX process group or absolute taskkill.exe /T /F from a drive-absolute local %SystemRoot% or %WINDIR%), and reports a distinct safe failure if tree termination itself fails;
    • retains only the tail of stdout and stderr, capped by UTF-8 bytes;
    • returns the CLI stdout as text and a prompt-redacted display command;
    • exposes static AgentCliError messages plus structured, non-secret metadata.

    The default timeout is 600,000 ms and the default retained output is 1 MiB per stream. An injected exec implementation is responsible for its own sandbox, process-tree termination, timeout, and output limits.

    The returned text is functional agent output and may contain sensitive data. The command redacts the prompt and --flag=value option values. Validation failures, error messages, error metadata, and breakwater-generated audit reasons do not contain the prompt or captured stdout and stderr.

    The adapter does not sandbox the child. It inherits the parent environment and credentials, and its working directory is the workspace the CLI may modify. Its manifest declares provider hosts, but a child process does not use ConnectorRuntime.fetch; actual socket enforcement belongs in the container, VM, or host firewall. See the CLI section of the connector guide before enabling real execution.

    runtime.fetch accepts an absolute HTTP(S) URL string or URL object, not a Request. It refuses an init.redirect other than 'follow', 'manual' or 'error'. It checks the initial host and every followed redirect. It strips authorization, cookie, and proxy-authorization on a cross-origin redirect, rewrites methods according to fetch redirect rules, refuses to replay one-shot bodies across 307 or 308 redirects, and defaults to 20 hops. In 'error' mode, a redirect response rejects with a TypeError (fetch failed) on every runtime, and the transport behind runtime.fetch (policies.fetch or the global fetch) always receives redirect: 'manual'.

    This enforcement cannot see:

    • global fetch calls made around runtime.fetch;
    • vendor SDKs that do not accept the injected fetch;
    • raw sockets or child-process network traffic.

    Route every connector request through runtime.fetch. Use host-level network controls when code outside that seam must also be constrained. Declare permissions.egressEnforcement: 'declaration-only' for traffic outside the guard; connectorEgressPosture() reads the resolved posture and connector audit events record it as detail.egressEnforcement.

    The root entry point re-exports the supported APIs from every subpath.

    Runtime exports Purpose
    createGuardedAgent Construct a guarded agent with a narrow execution handle
    isGuardedAgentHandle Check whether a value is a guarded handle
    GUARDED_AGENT_HOST_PROTOCOL Read guarded host compatibility metadata
    assertNoGuardedSystemMessages Refuse caller system messages before a host starts a durable loop
    assertAcceptedCallProviderOptions Accept a closed list of call-level generation settings and refuse everything else
    providerOptionsCarryContent Detect message-level provider options an input policy reads or refuses

    Type exports: GuardedAgentCallOptions, GuardedAgentConfig, GuardedAgentHandle, GuardedAgentHostProtocol, GuardedInputProcessor, GuardedOutputProcessor, and GuardedToolChoice.

    Runtime exports Purpose
    PolicyEngine Mastra input, stream-output, and final-output processor
    createContentPolicyGate Opaque, reusable input-policy gate for trusted host boundaries
    denyPatterns, maxTextLength, piiSecrets, classifierPolicy Included agent-boundary evaluators
    PII_SECRETS_DETECTOR_IDS Stable detector ID list
    extractMessageText Extract policy text from Mastra messages
    networkEgress, crossWorkflowIsolation, tenantIsolation, backgroundExecution Included tool-boundary evaluators
    approvalRequired Resolve approval from a manifest and organization policy
    egressDomainAllowed One-shot normalized exact or wildcard host match
    ISOLATION_SCOPE_CONTEXT_KEY, WORKFLOW_SCOPE_CONTEXT_KEY, LLM_BACKGROUND_OVERRIDE_KEY Stable scope and override keys

    Type exports: PolicyEngineOptions, PolicyEvaluator, PolicyContext, PolicyDecision, ContentPolicyGate, ContentPolicyGateInput, ContentPolicyGateOptions, ContentPolicyGateResult, PolicyPhase, OutputChannel, PiiSecretsOptions, PiiSecretsDetectorId, ClassifierPolicyOptions, ToolPolicyEvaluator, ToolCallContext, SideEffect, NetworkEgressOptions, CrossWorkflowIsolationOptions, BackgroundExecutionOptions, and WritePermissionsPolicy.

    Runtime exports Purpose
    RBACMiddleware, ROLES, ACTOR_CONTEXT_KEY, actorFromRequestContext Actor authorization and lookup
    isPermissionIdentifier, isPrincipalPermissions, PRINCIPAL_PERMISSIONS_CONTEXT_KEY Canonical permission identifiers and the trusted principal-permissions projection
    AuditLogger, combineAuditSinks, metricsAuditSink Buffered audit, sink fan-out, and metrics adaptation

    Type exports: Actor, Role, Permission, PrincipalKind, PrincipalPermissions, RBACMiddlewareOptions, AuditEvent, AuditSink, AuditLoggerOptions, and MetricsRecorder. The rbac subpath also re-exports AuditLogger and its original audit types for compatibility.

    Use the connector decision-code guide to classify failures and audit events. The exported ConnectorDecisionCode and ConnectorPolicyName types support machine handling while diagnostic policy names remain open strings. Retryability preserves the same operation identity; it does not add automatic retries.

    Runtime exports Purpose
    createConnector, connectorManifest, connectorEgressPosture Build an enforced Mastra connector and inspect its immutable manifest and resolved egress posture
    assertConnectorConformance, ConnectorConformanceError Assert supplied connector cases against trapped egress entry points and inspect the failure report
    invokeConnector Invoke an unmodified connector from trusted host or workflow code without fabricating a Mastra tool context
    singleTenantConnectorPolicies Build the validated connector-policy baseline for one physically isolated deployment
    ConnectorPolicyError, ConnectorStoreError, ConnectorEvaluatorError, ConnectorValidationError, ConnectorInvocationError Stable classification for authored connector failures
    CONNECTOR_DECISIONS, isConnectorDecisionCode, connectorDecisionRetryable Decision catalogue and retryability without parsing diagnostic prose
    CONNECTOR_GRANTS_CONTEXT_KEY, CONNECTOR_EXECUTION_CONTEXT_KEY, DRY_RUN_CONTEXT_KEY, IDEMPOTENCY_KEY_CONTEXT_KEY Stable connector request-context keys
    InMemoryIdempotencyStore, D1IdempotencyStore Development and durable replay stores
    inspectLegacyConnectorIdempotency, migrateLegacyConnectorIdempotency Inventory and atomically migrate one externally proven ambiguous legacy D1 row without exposing storage keys
    InMemoryRateLimitStore, D1RateLimitStore Development and durable fixed-window stores
    egressFetch, EgressDeniedError, EgressGuardError Standalone fetch guard and coded request/redirect refusals

    Type exports: Connector, ConnectorInvocationOptions, PermissionManifest, ConnectorConfig, ConnectorPolicies, SingleTenantConnectorPolicies, SingleTenantConnectorPoliciesOptions, SingleTenantAuditPosture, SingleTenantDurableStores, SingleTenantPermissionPosture, ConnectorApprovalGrant, ConnectorApprovalGrantBase, ConnectorApprovalSuspension, ConnectorExecutionIdentity, ConnectorRuntime, ConnectorEgressPosture, IdempotencyStore, AtomicIdempotencyStore, InspectableIdempotencyStore, IdempotencyInspection, IdempotencyRecord, IdempotencyReservation, RateLimitStore, LegacyConnectorIdempotencyIdentity, LegacyConnectorIdempotencyMigrationRequest, LegacyConnectorIdempotencyMigrationResult, D1IdempotencyStoreOptions, IdempotencyDatabase, IdempotencyBatchDatabase, IdempotencyStatement, IdempotencyBatchResult, D1RateLimitStoreOptions, RateLimitDatabase, RateLimitStatement, RateLimitBatchResult, EgressDenial, EgressFetchOptions, EgressFetchBase, EgressGuardedFetch, EgressRequestInit, EgressResponse, EgressResponseHeaders, ConnectorConformanceCase, ConnectorConformanceCaseResult, ConnectorConformanceEntryPoint, ConnectorConformanceEscape, ConnectorConformanceFactory, ConnectorConformanceFinding, ConnectorConformanceFindingCode, ConnectorConformanceOptions, ConnectorConformanceReport, ConnectorConformanceRequest, ConnectorConformanceResponse, ConnectorConformanceRuntime.

    Runtime exports Purpose
    createAgentCliConnector Wrap another positional-prompt CLI
    createClaudeCodeConnector, createCodexConnector Built-in adapters
    CLAUDE_CODE_CLI, CODEX_CLI Reusable adapter definitions
    AgentCliError Safe structured execution failure

    Type exports: AgentCliInput, AgentCliOutput, AgentCliExec, AgentCliExecResult, AgentCliDefinition, AgentCliConnectorOptions, AgentCliErrorCode, and AgentCliErrorMetadata.

    pnpm --filter @proofoftech/breakwater lint
    pnpm --filter @proofoftech/breakwater typecheck
    pnpm --filter @proofoftech/breakwater test
    pnpm --filter @proofoftech/breakwater build
    pnpm --filter @proofoftech/breakwater test:packed-consumer

    Apache-2.0. See LICENSE.

    Modules

    agent
    agent-cli
    audit
    connector-sdk
    index
    policy-engine
    rbac