Repository navigation
gateway v0.23.0
v0.23.0
This release adds customer MCP registration, retained descriptor provenance,
source sandbox policy and posture, and action reconciliation after uncertain writes.
The release tag is also the milestone. The four implementation rows are
#348,
#349,
#350 and
#351.
What an operator must do
- Re-pin the
azure-devopsanddatabricksbindings, then recapture. Both
shipped write bindings move to binding version 3 withactionRecovery, so their
digests differ from v0.22.0's. A configuration pinned to the v0.22.0 digests
refuses to start once the catalog directory is replaced ("does not digest to the
pinned …; the catalog changed under the configuration"). Update the pin, then
recapture the descriptor approvals and statements that name the binding with
connect --replace. - Re-approve customer commands. A customer
commandserver now captures
descriptor snapshot version 4, which binds the resolved executable, a script
operand and, for a#!script, itsinterpreters, by path and digest. A version-3
approval of a command refuses withmcp-approval-reapproval-required;
connect --replaceapproves it again. A changed path or digest refuses with
mcp-executable-changedbefore anything starts, and a#!line the binding cannot
follow refuses withmcp-executable-unbindable(see
Executable binding and sandbox honesty). - Remove loader variables from wrapped platforms. A platform
environment
namingLD_*,DYLD_*orGCONV_PATHis refused for a customer command binding,
and from engine version 13 for every wrapped source
(sandbox-loader-variable-refused). - Install the sandbox launcher at a trusted location. bubblewrap is taken only
from/usr/bin/bwrap,/bin/bwrapor/usr/local/bin/bwrap, and sandbox-exec only
from/usr/bin/sandbox-exec, each root-owned and writable by neither group nor
others along its path; otherwisesandbox-launcher-untrusted, with no other
location or backend tried. On Linux with no trusted bubblewrap, the Landlock
fallback is reportedpartial: chmod and other metadata changes stay outside it.
A bubblewrap under a prefix an unprivileged user owns is never used. - Expect workspace-write by default from engine version 13. Every source
adapter is wrapped; an omittedsandboxsetting selects workspace-write.
enforcedmeans the filesystem write boundary holds; bubblewrap masks the local
sockets it finds, and local services still reachable over Unix sockets are a
stated limit of every report. A container source, which keeps its runtime socket,
ispartial. Each launch is bound to its source's probe: a policy changed after
the probe refuses until a restart measures it again, and a restricted launch whose
probe measured the boundary as not holding refuses. Public/posturereasons name
no path or account. - On Windows, engine version 14 refuses every
/act. Version 14 requires an
actionKey, and keyed dispatch is unsupported on Windows whatever the sandbox
mode. Keep a Windows gateway that must write at version 13 or below and send no
key (the workflow plugins' action key set to None). Restricted local commands
and, from version 13, the wrapped adapters for remote and container sources also
refuse there unless full access is selected. - Send an
actionKeyfrom the workflow plugins; upgrade the gateway first. The
repository's n8n node and Activepieces piece now send one by default, derived from
the intended write exactly as the request sends it (its platform, tool, arguments,
decision and cites), so a retry of the same write sends the same key. The
arguments are evaluated for each attempt: an argument from an expression that
changes per execution (n8n's$now, a random value) gives a retry a new key, and
the write would be sent again. Give such a workflow an explicit key (Given),
chosen once for the write and kept for its retries. Arguments that JSON does not
carry as they are (a Map, a Set) refuse derivation and need Given too. A
gateway before v0.23.0 refuses every act that carriesactionKey: upgrade the
gateway before the plugins, or set the plugins' action key to None until it is
upgraded. A v0.23.0 engine below version 14 accepts a key. - Back up the action journal with the store. The private
actions/directory
holds the dispatch history; receipt exports omit it, and restoring receipts alone
does not preserve it. - Drain pending keys before changing what an action is approved as, and before
upgrading. A new binding pin, a recaptured descriptor or executable approval,
another sandbox policy, another endpoint, or moving an opted-in deployment from
engine version 12 to 13 changes the action's identity, and a pending key then
refuses asaction-key-conflict. The identity also keeps the adapter command line
this release derives (less its install paths), so a later release that adds or
reorders a derived flag does the same: reconcile or drain pending uncertain keys
before upgrading the gateway. Never send such a write again under a new key. - Keep the identity issuer and the authority stable while keys are pending. A
key is scoped to the gateway authority and the authenticated issuer and subject.
Changing the configured issuer string, even for the same subject, or the
authority, moves every key to a new reservation, and a retry then sends the write
again. Settle pending keys before such a change (#352 settles this scope for the
agent).
Customer bindings and descriptor evidence
A customer binding registers a pinned container image, remote MCP endpoint or
local command through the existing source declaration. The operator declares each
allowed tool's operation and effect. Missing declarations and contradictions with
server annotations refuse registration. A version-3 descriptor snapshot pins the
complete approved tool descriptors; changed descriptors and unapproved tools
refuse acquisition before dispatch.
Registration retains descriptor bytes and a separately signed statement before
publishing configuration. Engine configuration 12 introduces per-operation
descriptorStatements; capture preserves newer configuration versions. Receipt-v3
acquisitions can reference the retained provenance. Both receipt readers verify its
canonical bytes, signature, binding and signing-key ordering. A statement with an
empty endpoint records a local capture and does not cover an acquisition over a
remote transport; both readers and the signer's preflight now require the remote
acquisition's own endpoint (SPEC.md §9). The registry is walked once at start for
these checks, and the second check runs outside the gateway-wide lock. Existing receipts
without these members make no new provenance claim. A gateway signature establishes
byte lineage and the gateway's capture assertion, not the customer's honesty or
the truth of a descriptor.
Source policy and honest posture
Engine configurations 13 and later wrap every configured source adapter and its
descendants with the trusted adapter-sandbox helper, including registration
checks. The sandbox settings object is optional; omitting it still selects
workspace-write by default. Permission
presets expand into policy-version-2 settings; the operator's filesystem mode is
separate from tool-effect approval. Keep the policy, credentials, signing material
and approval evidence outside a source's writable workspace.
Linux uses bubblewrap or a partial Landlock fallback; the latter does not confine
filesystem metadata changes. macOS uses Seatbelt. Local customer commands default
to workspace-write, and read-only and explicit full-access modes are available
where supported. Posture records each program's backend and measured limits.
The optional enclosing-boundary flag is an operator assertion, not independent
proof of enforcement. Container-daemon authority, network effects and readable
host data are outside this filesystem write-policy claim.
Windows restricted local commands are unavailable. Read-only and
workspace-write refuse before execution, including registration and acquisition
under an existing approval. In engine versions 13 and later this also refuses
the wrapped adapters for remote and container sources unless the operator
explicitly selects full access. Posture reports backend: none, state: not-enforced
and the unavailable-backend reason. The experimental token/ACL implementation is
not shipped. Explicit operator-selected danger-full-access remains unrestricted;
there is no automatic fallback and no sandbox job-containment guarantee in that
mode. Full access does not enable keyed action dispatch on an unsupported host.
Remote and container policy paths retain their own documented limits.
Executable binding and sandbox honesty
A pre-release batch closes the review findings dispositioned on #348 and #350.
#!interpreters are bound. A version-4 approval of a#!command now
carriesinterpreters: each program that runs before the script, by path and
digest, the program#!/usr/bin/env NAMEfinds on the server'sPATHincluded.
A re-pointed or changed interpreter refuses withmcp-executable-changed. Only
env NAMEandenv -S NAMEwith literal arguments are bound; any otherenv
form (an option, an assignment, a nestedenv, an expansion, an escape or a
quote) refuses withmcp-executable-unbindable.
Both readers and the shared vectors carry the member. Version 4 is unreleased, so
no approval changes shape.- What is digested is what runs. On Linux (amd64, arm64) a binary command runs
from a sealed in-memory copy of exactly the digested bytes, so an in-place rewrite
after the digest no longer runs. The check report's sandbox reason, and posture's
customer-command entries, say what is re-checked by path instead, and when. - Socket masking and posture. Bubblewrap masks every listening Unix socket
bound to a path, the socketDOCKER_HOSTorCONTAINER_HOSTnames and the
desktop runtimes' sockets, after the writable binds, as defence in depth. A
listening abstract socket, or a socket list that cannot be read, reports
partial; Seatbelt is alwayspartial.enforcedmeans the filesystem write
boundary holds: local services reached over Unix sockets may remain reachable
(a socket created after the launch, bound in another network namespace or by a
relative path is not masked), which every bubblewrap report states as a limit. - Public posture names no path or account again.
/postureprogram reasons
are fixed sentences;gateway posturekeeps the detail. - A measured failure refuses. A restricted launch whose probe measured the
boundary as not holding refuses, and a probe that timed out is not cached. - Smaller fixes.
gateway connectprints the sandbox mode, backend and state;
a policy path through a workspace link refuses, and the engine holds the policy
file's owner and mode at start; Landlock and Seatbelt allow/dev/nullwrites and
Seatbelt gets a resolved temporary directory; leftover temporary files no longer
fail a completed call; a FIFO at the command path refuses at once; argv[0] is the
configured word again; the script operand is the first operand naming a file; the
server's environment travels in a sealed memory file on Linux; a bubblewrap
launcher's program interpreter and library search path are held.
Known limits after the batch:
- An acquisition's receipt records no sandbox backend. Under engine version 13
every launch is bound to the backend its source's posture reports and refuses
otherwise; the registration check report andgateway connectshow it. - A
#!script, its interpreters, a script operand, and every customer command
file on macOS and Windows are executed or opened by path after a re-check taken
immediately before the start; a change after that re-check is not detected. A
binary's shared libraries, and code an interpreter loads by other means, are not
bound. - On Linux the server's
/proc/self/exenames the copy that runs: under
bubblewrap a read-only file in the sandbox, elsewhere the sealed memory file,
whose path a server that re-executes itself by name does not find. A host that
refuses to execute the sealed memory file refuses the start with
sandbox-sealed-exec-refused. - A listening abstract socket cannot be masked while the network namespace is
shared, which every mode does; its presence keeps the statepartial. A socket
that starts listening after a launch is not masked, and a server that needs a host
socket (a name service, a session bus) cannot reach one under a restricted mode. - On macOS the server's environment, credentials included, still travels in an
unlinked file in the private temporary directory. - The policy file's owner and mode are checked at start; a later change by root or
the signer takes effect after the next restart's probe, as before. - The executable binding follows the command and its
#!interpreters only. A
program that runs another program named in its arguments (nice X,timeout X,
xargs X) is bound as itself; the program it runs is not followed or digested.
Uncertain actions
Engine configuration 14 requires a caller-selected actionKey on /act; older
configurations can opt in by supplying it. Reuse that key for the same intended
action after a lost response. The gateway durably reserves it, and creates an
exclusive dispatch marker immediately before starting the write program. It never
sends a mutation that may have been sent again under the same authority and
authenticated issuer and subject, including across sessions, token renewal and
restart. The key is scoped to that issuer and subject: a retry that authenticates
as another subject reaches another reservation. The contract for an agent's
takeover of a run-owned write is settled in #352 before the agent adopts this.
The key identifies the approved action: the platform, tool, arguments, decision,
cites, binding pin, recovery contract and decision policy, and for the write and
read-back sources their binding, endpoint, transport, approval pins, descriptor
provenance and sandbox policy digest. Install paths, the platform's environment and
the OS user are not part of it, so an upgrade into another directory leaves a key
its own. Different approved content under the same key is refused as
action-key-conflict. A client must never send that write again under a new key:
a new key is a new action, and the conflict means the key already names a write
that may have been sent.
A write program that was never started (not found, refused before its start, or
a failed start) records not-dispatched in the journal and answers
action-not-dispatched; the same request under the same key may then be sent. A
crash after the reservation and before the dispatch marker leaves the key free in
the same way. A crash after the marker, or a sandbox helper's refusal after it
started, stays uncertain. A key records at most 16 attempts that were never
started: a retry loop through an outage of the program, the sandbox or the user
switch exhausts it, and it then refuses with action-journal-exhausted. An
exhausted key provably sent nothing, since every attempt was answered as not
dispatched: the client may send the write under a new key, or an operator may
remove that key's entry under actions/ once the cause is fixed. Within one
process, requests under one key run one at a time and different keys run
concurrently. A replayed response, a conflict, an invalid journal entry or an
action-not-dispatched answer leaves no session behind.
Binding version 3 declares idempotency and read-back support per write tool, or
explicitly declares unsupported recovery. A retained uncertain action can produce
a separate read-back acquisition receipt linked to that action; it must be the
action platform's <platform>/live MCP acquisition. A matching key and the
declared committed value report committed; other observations remain unknown.
Neither outcome authorizes another mutation dispatch. A target's response remains
evidence of what it reported, not proof of its truthfulness.
An uncertain action receipt records no target answer. Its result is the
gateway's uncertainty record, and its request may never have been sent. This
release's Go and TypeScript readers report every uncertain action as an
observation beside ok: action-uncertain, action-committed (a correlated
read-back reported committed) or action-mismatch (a read-back citing it does
not correlate, which also fails ok). Readers before this release accept an
uncertain action as an ordinary action receipt and never check read-back
correlation: they return ok for it, and for a mismatched read-back. Verify keyed
stores with this release's readers.
The shipped write bindings declare no generic recovery guarantee. A binding needs
an explicit pinned target contract to enable read-back. There is no new Drive
write binding; its separate personal storage protocol is unchanged.
Keyed dispatch requires supported Unix file and directory synchronization.
Unsupported hosts refuse before sending a keyed action. Known limits: a damaged
journal entry refuses its key until an operator checks the target and removes it;
the journal has no retention policy yet; the receipt a journal entry cites is
synced best-effort, so after a power loss that key can refuse instead of replaying
(see action reconciliation).
Validation and release gates
The acceptance cases include descriptor and owner-declaration refusal, signed
provenance mutation vectors in both readers, actual filesystem boundary probes,
Windows refusal without execution, and a target that commits then drops its
response. Reconciliation tests cover concurrent repeats, restart, synchronization
failure, changed source policy, unsupported recovery and tampered retained evidence.
Review statement
#348, #349, #350 and #351 were built by another vendor's model and reviewed
different-vendor by Claude Opus (Anthropic). The fix commits, the stack merges, the
re-read of #350 and both pre-release batches (#353, sandbox and executable
approval; #354, action reconciliation and provenance guards) are Claude-authored
and were checked under the review regime's exception for changes that implement
dispositioned findings. That is not a different-vendor claim for that material.
The PRs retain exact-head check and independent-review records. Publishing requires
the repository's different-vendor adversarial review and finding dispositions,
required CI at each merge candidate, and release evidence from the tagged main
commit. A local check or same-vendor clean-room review does not replace those gates.