0.26.0
0.26.0 — 2026-09-25
Programs can now write with host approval. When a program reaches a tool that
is not explicitly read-only, it pauses before the call and returns the exact
address, arguments, and a token. The new eighth MCP tool,
resume_execution, repeats that call to show the host the real write and
replays the program from its journal without resending an uncertain outcome.
tools/list now has eight tools on every deployment, so client allowlists and
connecta doctor installations pinned to 0.25 need updating. Resumable
writes turn on by default when storage supports compareAndSet (memory, file,
or the Worker example's D1 adapter); a program write that previously failed
now pauses. Cloudflare KV cannot make that claim atomically, so resumable
writes stay off there. Set execute.resumableWrites: false to silence its
startup warning. Program clocks and randomness are pinned for replay.
Artifacts arrive as an optional module with versioned JSON documents, a
sandboxed viewer and library, and scheduled refresh of one data document from
a read-only program. A deployment supplies a Node timer or Worker cron;
connecta starts no background job. Stored pages use the operator sign-in,
and a dedicated artifact origin may require signing in again because browser
storage belongs to one origin. Page scripts run with an opaque origin and no
network by default; only explicitly allowlisted script, style, and font
origins may load. Artifact version and configuration writes are approval-exempt inside
programs by default; deployment config can restore the approval prompt.
run_refresh requires host approval and is excluded from that exemption.
Deployments that omit the module need no
artifact storage, R2 bucket, cron, browser binding, or second domain.
Before upgrading a Worker deployment that uses D1 activity, add the
approval column shown below. Every admitted /mcp request, on Node and
Workers, has a five-minute total lifetime by default; set
admission.requests.maxDurationMs above any longer authorized request. The
Worker example also enables enable_request_signal for live disconnects.
To add artifacts to that example, use CAS-capable D1
storage, optionally R2 for bodies, and explicitly configure any CDN origins.
The Node template now pins 0.26.0 and shows the optional artifact timer.
ConnectorCallErrorCode gains conflict for stale artifact edits, while an
identity can opt into reviewed exact tool grants that remain visible only while
the loaded catalog classifies them read-only. The operator UI no longer
returns downstream OAuth or credential-test text in notices; callers that
read those route messages must use the fixed outcome and consult server logs
for diagnostics.
Added
-
Guarded read-only identity grants (#601). A deployment may put
{ tool: "connector.tool", requireReadOnly: true }in
identity.connectorAccess. The reviewed exact address stays visible only
while its loaded catalog explicitly says read-only without a contradictory
destructive hint. A reclassified tool disappears from discovery and every
call path, includingcall_destructive_tooland approval-exempt programs;
new names are never added automatically. Existing string grants keep their
additive meaning, and a pool can still only narrow the identity's view.
A stale catalog or a dishonest downstream annotation remains a trust limit. -
Resumable writes and
resume_execution(#565). A program's call to a
tool that is not explicitly read-only pauses the run before anything is
sent and returns{ paused: { address, args, token, expiresAt, nextAction, hint } }.resume_execution— always listed, annotated destructive — must
repeat the address and args exactly (approval_mismatchotherwise, before
anything is claimed); it claims the run by compare-and-set and replays the
program from a journal in the caller's result storage, answering every
earlier host call from its record — no catalog, permit, connector, or
activity — and sending the approved write once.approval: "tool"approves
the rest of the run's calls to that tool. Each live write is marked on the
run's header before it is sent, so racing or crashed resumes never send it
twice. A write sent but never answered — or answered with anything short
of a refusal or the downstream tool's own error — stops the run as
write_outcome_unknown, even beside a concurrent pause, and is never sent
again; a program that stops matching its journal fails
execution_diverged; a resumed play that sent writes and then ended without
pausing (a sandbox failure, a lapsed claim) failsexecution_interrupted
rather than replaying reads it never journaled. Every error from a resumed
play carrieswritescounts, and once a write landed or may have, its
advice is to check those before re-running rather than a plain re-run; a
run that sent writes keeps those counts past its deadline. Every paused-run
storage call has a deadline (the host-call deadline, at least 5 s), so
storage that stops answering fails the run rather than holding it. A paused
run survives a restart and expiresexecute.pausedRunTtlSeconds(default 1,800) after its
first pause;execute.maxWrites(default 10) caps a run's writes on top of
the host-call budget.execute.resumableWritesdefaults to whether the
storage hascompareAndSet;truewithout it refuses to construct.
/healthreportsresumableWrites, andconnecta doctorexpects the eight
tools and says when resumable writes are off. See code mode "Pausing and
resuming" (W1–W11,X12). -
Config approval exemptions (#566).
execute.approvalmaps connector ids
andconnector.tooladdresses to"never"or"ask": a program calls an
exempt write without pausing, while it still spends the write budget, is
journaled, and records activity. The address wins over the connector entry,
which wins over a connector's ownapproval: "never"default — reserved for
connectors connecta ships, such as the artifacts connector — so
"ask"switches such a default off. It works with resumable writes off
too, where an exempt write the program leaves unawaited still finishes
before the run ends, and one with an unknown outcome is the result. Exempt is never read-only: discovery keeps the tool approval-required
and marks its rowapproval: "exempt",call_toolstill refuses it, and no
downstream annotation can grant it. An unknown connector id, or anapi()
address its tools do not include, refuses to construct. The operator UI's
per-tool badge gains its third state, "exempt from approval". -
Activity for pauses and approvals.
ActivityOutcomegainspausedand
approved, both with zero attempts;ActivityCallSourcegains
resume_execution;ToolCallActivityEventgains an optionalapproval
("call"or"tool"), set only on an approval. All three are enums — the
approved arguments are never recorded. The Worker example's D1 activity
store writes anapprovalcolumn; add it with
ALTER TABLE tool_call_activity ADD COLUMN approval TEXT;before deploying
the updated adapter. The operator activity page labels both outcomes and
says what an approval covered. -
Artifact storage and validation, at
@zackbart/connecta/artifacts
(#562).kvArtifactStore(storage, { blobs?, prefix? })keeps artifacts
in anyKVStoragewithcompareAndSetandlist— one head record per
artifact, swapped by compare-and-set, with every earlier version immutable
beside it and bodies content-addressed — and refuses Cloudflare Workers KV
at construction, because a write that cannot compare-and-set its head can
lose a teammate's edit.validateArtifact({ kind, source, documents? })is
the static check every save runs: oneid="artifact-root", no frames,
plugins, forms, or<base>, external scripts only from explicitly
allowlisted origins, and no relative or unapproved network resource URLs,
with a line number and a fix in every message. The Worker example gains
r2-artifact-blobs.tsfor keeping bodies
in R2 beside a D1 store. The subpath adds no dependency and no Effect. -
The built-in artifacts connector (#562, #564).
createConnecta({ artifacts: artifacts({ store }) })appends anartifactsconnector with five reads —
list_artifacts,get_artifact,get_document,validate_artifact, and
get_refresh— and nine writes:create_artifact,update_artifact,patch_artifact(exact
find/replace, every edit matching once or nothing changes),
set_documents(plural and atomic),rollback_artifact,
archive_artifact,restore_artifact,set_refresh, andrun_refresh.
Version-changing writes name their expected base, and every write records
its actor from the request's authorization. The connector skips approval, except forrun_refresh,
insideexecute_codethrough its ownapproval: "never"—execute.approval: { artifacts: "ask" }turns that off — while
still spending the write budget and recording activity;call_toolstill
refuses it. The connector serves a publishing guide as
connector:artifacts, required before the first write. An optional
renderCheckhook receives the exact frame document and CSP a viewer will
load and can refuse a write; without one, static validation is the floor.
The module needspublicUrl, and a configured connector namedartifacts
refuses to construct. -
Artifact library and sandboxed viewer (#563).
/artifactslists pages;
/artifacts/<id>opens one after the same inbound sign-in as the operator
UI. A viewer snapshot pins exact view and document versions. Page HTML runs
in an opaque-origin frame with no fetch or worker access. A deployment may
allow exact CDN origins for scripts, styles, and fonts; none are allowed by
default. WithartifactOrigin, the main host redirects artifact paths and
the dedicated host serves no MCP or operator routes. A browser bearer stored
on the main origin must be entered again on the artifact origin; the redirect
carries no credential. -
Scheduled artifact refresh (#564).
set_refreshattaches a versioned
program for one data document on a manual, daily, or weekly schedule;
run_refreshtriggers it now, andget_refreshreports freshness and a
bounded run history. The module'srunDue()is called by the deployment's
Node timer or Worker cron, never by a core background loop. Runs use only
shared connectors' explicitly read-only tools within the setter's current
identity and pool grants, and stop if publishing permission is revoked.
Connector approval exemptions cannot permit a scheduled write. Runs claim
the artifact by compare-and-set and validate returned JSON, including any
configured render check, before publishing a new document version. A failure keeps the last good value and marks the page
stale without rendering raw failure text in the library or viewer. Each tick
starts at most ten due pages; the Node template and Worker example show the
optional wiring. -
conflict(#562). A newConnectorCallErrorCodefor a write whose base
someone else already moved past, never retryable as-is, with an optional
boundedcurrentmap (at most 20 whole-number entries) on
ConnectorCallErrorandCallErrorDetailssaying where things stand. A
program reads it aserr.details.current; a top-level call returns it in
the structured error. A write refused withconflictinside a program is a
known failure (W9), not an unknown outcome.
Changed
execute_codeasks for a zero-argument program (#602). Its guidance now
showsasync () => { ... }, withconnectasupplied as the sandbox global.
Passingconnectaas an arrow parameter shadows that global and fails; the
accepted source forms and execution rules have not changed.- Worker resource setup has an optional Alchemy guide (#567). It covers
account resources and adoption while keeping the Worker script, domains,
Browser Rendering, and cron deployment with Wrangler until Alchemy's
script-upload path is shown to preservectx.access. Alchemy is not a
connecta dependency or a second deployment template. execute_codesource intake (#576). Programs over 64 KiB of UTF-8
source fail before executor admission. The host recovers one fenced async
arrow even when prose surrounds it, a default-exported arrow, or a named
async function declaration, so Node and Workers receive the same program.- Eight tools, and instructions that follow the mode.
tools/listadds
resume_executioneverywhere. With resumable writes on, the MCP
instructions say programs may write and pause, theexecute_code
description advertises the write budget and dropssafety: "readOnly"from
its search example (a program looking for a write would not find it), and
theusageskill gains the pause, resume, and unknown-outcome guidance;
without them, the instructions and description read as they did in 0.25. - The operator UI's "needs approval" badge reads "asks for approval", and
the tool legend says a program pauses forresume_executionwhile a direct
call crossescall_destructive_tool. - A program's clock and randomness are pinned (code mode
P6).
Date.now(),new Date(), andDate()insideexecute_codereturn the
instant the run started and do not advance, andMath.random()is an sfc32
stream seeded per run from the host's CSPRNG; on a Dynamic Worker,
crypto.getRandomValues,crypto.randomUUID, andperformance.now()follow
the same pin. The replacements are locked likeError. Resumable writes
(#565) replay a paused program from the top, and a program that branched on
a moving clock or an unseeded draw could not be replayed; pinning every run
means none behaves differently for having paused. A program that timed its
own work now measures zero —diagnostics: truemeasures from the host.
Fixed
-
Request admission recovers after missing Worker cleanup (#595). An
admitted/mcprequest now has a configurable total lifetime, five minutes
by default, covering auth, tools, and response delivery. Its own timer aborts
live work, including connector calls. If workerd ends it without running
JavaScript cleanup, the next request or a queued request's timer reclaims
only the expired permit. An orphaned queued request cannot strand its next
permit indefinitely, and late cleanup cannot release a successor's permit.
The Worker example enablesenable_request_signalfor prompt live disconnect
cleanup. -
Legacy MCP handshake close (#572). A client that leaves while the older
protocol handshake is still initializing no longer leaves the SDK's
notifications/initializedpromise rejected without a handler. Successful
handshakes still send the notification. -
Operator notices no longer render downstream error text
(#568). The OAuth
connect/disconnect notice and the credential Test result showed whatever the
server sent back, and that text can come from a token endpoint's error body or
a provider's refusal, either of which can quote the secret it was sent. Each
notice now says a fixed sentence chosen by outcome, with the fix prompt for
that outcome, and the page never renders a route's words there even if an
older server sends them. The routes log the downstream's text instead — a
failed OAuth action or credential test atwarn, a passing test's message at
info— and never record it in activity. ACredentialTestResult.messageis
now log-only, and a Test refused because nothing usable is stored carries the
samecredential_requiredorcredential_mismatchproblem the Connections
page shows. -
Compact schemas group an array's union element. The
compactformat,
the default forsearch_toolsandconnecta.search, rendered an array of a
union as{ op: "replace" } | { op: "append" }[], which reads as one
operation or an array of the other; Linearsave_issue'spatchwas one
such array. A union, intersection, enum, or type list used as an array
element, a tuple's rest included, now renders parenthesized, as
({ op: "replace" } | { op: "append" })[], matching thetypescript
format. The renderer records which shapes it rendered at operator level
rather than rescanning its text, so a pipe in a literal, a constraint, or
property prose adds no parentheses, and every other shape renders byte for
byte as before (#569). -
A catalog refresh whose connector ignores abort held every later reader.
A refresh flight lived until its connector settled, so alistToolsthat
ignored its signal kept the flight open until the catalog was invalidated,
and every reader that joined it timed out in turn while the connector stayed
unlisted. A flight is now bounded by its owner's deadline, or by the default
30-second probe timeout for an owner with none. Past it, readers still
waiting and readers arriving later make a fresh attempt, and a late result
from the abandoned flight is neither cached nor persisted, so it cannot
overwrite the fresh one. Abandonment logs a warning
(#570). -
A joined catalog read ended on its starter's deadline. Readers of one
catalog in a request share one read, and that read ran under whichever
reader started it. A short-deadlineconnecta.callthat started it failed a
concurrentconnecta.searchwith the call's timeout, and a search cancelled
by its client cancelled a read others were still waiting on. The shared read
now carries its own signal and the probe timeout. Each reader waits under its
own deadline and cancellation, one that leaves fails alone, and the read is
cancelled only when every reader has gone. It is still one downstream read
per connector per request. The registry's cross-request refresh no longer
hands its owner's cancellation to joiners, which make a fresh attempt instead
(#571).