v0.24.3
A bug-fix release from a full audit of the execution path, invocation, catalog
rendering, downstream OAuth, and the route table, plus the one MCP 2026-07-28
requirement the previous inventory had missed. Two changes can be felt by a
deployment: /mcp now validates the browser Origin header, so a browser MCP
client hosted on an origin other than publicUrl or loopback needs
allowedOrigins; and the overload and shutdown JSON-RPC error codes moved
from -32001/-32002 to -31001/-31002. Everything else is a fix a
deployment can take without action. No storage format changed, but
fileStorage now refuses a second process on the same state file, which a
deployment sharing one file between two processes was never safe doing.
Added
allowedOrigins./mcpand every/mcp/<pool>path refuse a
disallowedOriginwith a fixed 403 before redirects, admission, auth, or
preflight, as the Streamable HTTP transport requires against DNS rebinding.
The default admits thepublicUrlorigin and HTTP(S) loopback at any port;
a list replaces the default;"*"keeps unrestricted CORS. Clients that
send noOriginare unaffected. Allowed preflight now returns 204 without
admission or auth and echoes valid requestedmcp-param-*header names.- Host-owned refresh recovery. A valid token response is a consumed
refresh token. When the request that owned a refresh is cancelled,
redirected to authorization, or invalidated before the SDK saves the
response, the coordinator persists the rotation itself, holds contenders
behind the pending-mutation marker until that write lands, and hands them
the saved rotation. A retired refresh token is never redeemed twice, and two
deterministic waves of eight overlapping scopes pin one grant per wave
(#526). - Downstream JSON-RPC
-32602refusals map toinvalid_args, and 4xx
responses with a JSON message keep that message, bounded, without inferring
retryability from prose. 429 staysrate_limitedand 408 becomestimeout. Retry-Afteraccepts the HTTP-date form as well as delta-seconds.- Personal connectors admit calls through their principal registry's own
budget instead of sharing one root budget; shutdown closes both, and
eviction never discards a live budget. records/mcp-2026-07-28.mdgains rows for Origin validation, deterministic
tools/listorder, the bearer challenge, SEP-2243 parameter headers, and
trace propagation, and corrects the stale extensions row.- Bounded
get_resultstash.results.maxStashBytes(default 8 MiB)
andresults.maxStashEntries(default 64) bound the paging stash per
runtime, with accounting that reserves capacity before concurrent writes
finish and reclaims expired entries before reuse. A refused stash keeps the
call successful with its preview and the paging-unavailable notice. - Byte-range paging. Stashed results are stored as a byte-addressable
envelope, soget_resultdecodes only the requested page instead of
re-encoding the whole result on every call. Entries stashed before the
upgrade still page until their TTL expires. - Identity-partitioned results. The stash partition derives from any
authenticated subject or principal, no longer only from providers that
declareactivityActorNamespace. Open deployments share one partition. - Sanitized
unavailabledetail. Unreachable downstreams may carry
details.host(origin only) anddetails.code(a closed allowlist of
network errnos, ortimeout) so an outage is distinguishable from a typo
without leaking a path, query, or credential. Activity stays payload-free. fileStoragewriter lock. A second instance or process opening the
same state file fails at construction naming the holder. The lock is
heartbeat-based, so a container restart that reuses a pid cannot wedge a
deployment, and each write uses a unique temp file. The returned store now
hasclose().- Logs survive every executor failure. QuickJS streams console entries
to the parent, so a program that is cancelled, killed at the deadline,
crashed, or lost to an IPC failure still returns what it printed.
Changed
/healthno longer names connectors: drift reports are keyed by a truncated
SHA-256 of the connector id and downstream admission is summed without ids.
connecta doctorkeeps its stale-allowlist signal.- Overload and shutdown JSON-RPC codes are
-31001and-31002, outside the
reserved range, per the specification's allocation policy. connecta.searchandconnecta.describespend the same host-call budget as
connecta.call; onlyconnecta.emitis exempt (L4, M7).- One per-call deadline now covers catalog resolution, admission, and the
connector call, so a hung connector fails with a catchabletimeoutinstead
of consuming the whole execution wall clock. - Compact describe caps each shape at 8,192 UTF-8 bytes and sets
inputSchemaTruncated/outputSchemaTruncated; useformat: "json"for
the exact schema. - Top-level discovery measures the complete tool result, both copies and JSON
escaping, against the 256,000-byte ceiling. - Downstream
isErrortext is bounded at 512 UTF-8 bytes, and error framing
fits the call's result cap in both result modes. - Open deployments warn whenever any connector is configured, since
api()
headers can carry secrets without declaring credential hooks.
Fixed
- A long downstream error could leak the sandbox's per-run secret to guest
code and let a program forge a typed failure. The QuickJS bridge sliced a
rejected host call at 4,000 characters, which clipped the authenticated
failure frame mid-JSON. Details are bounded before framing, the bridge
refuses an oversized frame whole, the prelude hides a malformed frame, and
the raw transport lives in a private closure so guest code cannot swap
Erroror reach the bridge. - A completed destructive call was reported as a retryable failure when the
get_resultstash write failed. The truncated preview is returned with a
paging-unavailable notice, activity records success, and
result_processing_failedis never retryable. - The OAuth refresh gate wedged permanently when a token fetch succeeded
but the SDK's save never ran, answering 503 to every later refresh in the
isolate until a forced reauthorization. - Compact schema rendering went quartic on
allOf-of-$refschemas. A
3.5 KB downstream schema took over four seconds of synchronous CPU, and the
describe path produced a 1.9 MB string from 1.8 KB. Rendering now spends a
shared 2,000-visit budget with memoized$refexpansion. call_toolin MCP result mode returned{"content":[]}for a downstream
result carrying onlystructuredContent; a text block is now synthesized,
and anullstructured value survives unwrapping.- In-program
connecta.searchreturnedoffset: nulland an empty page for a
non-numeric offset, and threw a rawTypeErrorfor a non-string query; both
areinvalid_args. - Caller-authored text in
get_result,authorize_connector,skills, and
search_toolsrefusals is bounded; aconnectorover 512 bytes is
invalid_args. A failed stash read is a typedunavailable. - Legacy
mcp-session-idDELETE now runs on credential rotation, generation
change, disconnect, and abandoned connects, not only on scope close. - Pool names outside
[a-z0-9_-]fell through to a generic 404 without CORS
or authentication; every/mcp/suffix now reaches the identical pool 404
after auth. - The absent-grant warning set was unbounded and keyed by caller-derivable
text; it is capped at 1,024 entries. requiredInputKeyscould name keys the schema did not declare.- Validation error detail embedded whole enums; drift digests recursed without
a depth bound; refresh token responses were read without a byte ceiling;
credential revision races could loop without bound. - Containment matching for escaped failures ignores messages under eight
characters, and a QuickJS host-result reply that cannot be serialized settles
the call instead of hanging until the wall deadline. - Every
connecta.callattempt spends one host call on entry, so unknown
addresses cannot loop for free; the escaped-failure list keeps the most
recent 64; an empty terminal error string is a failure, not a success. - The compact renderer renders
prefixItemsas a tuple, marks
dependentSchemasandif/then/elseshapes conditional with the
truncation flag, and resolves$dynamicReflike$ref. - The guarded transport's no-body-stream path enforces the byte ceiling on
text()andjson(). - The pool-name timing oracle is accepted and documented;
authorize_connector
honoring tool-level grants is documented and pinned.