Skip to content

0.26.0

Choose a tag to compare

@zackbart zackbart released this 25 Sep 05:51
· 31 commits to main since this release
49d4d31

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, including call_destructive_tool and 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_mismatch otherwise, 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) fails execution_interrupted
    rather than replaying reads it never journaled. Every error from a resumed
    play carries writes counts, 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 expires execute.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.resumableWrites defaults to whether the
    storage has compareAndSet; true without it refuses to construct.
    /health reports resumableWrites, and connecta doctor expects the eight
    tools and says when resumable writes are off. See code mode "Pausing and
    resuming" (W1–W11, X12).

  • Config approval exemptions (#566). execute.approval maps connector ids
    and connector.tool addresses 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 own approval: "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 row approval: "exempt", call_tool still refuses it, and no
    downstream annotation can grant it. An unknown connector id, or an api()
    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. ActivityOutcome gains paused and
    approved, both with zero attempts; ActivityCallSource gains
    resume_execution; ToolCallActivityEvent gains an optional approval
    ("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 an approval column; 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 any KVStorage with compareAndSet and list — 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: one id="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.ts for 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 an artifacts connector 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, and run_refresh.
    Version-changing writes name their expected base, and every write records
    its actor from the request's authorization. The connector skips approval, except for run_refresh,
    inside execute_code through its own approval: "never" — execute.approval: { artifacts: "ask" } turns that off — while
    still spending the write budget and recording activity; call_tool still
    refuses it. The connector serves a publishing guide as
    connector:artifacts, required before the first write. An optional
    renderCheck hook 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 needs publicUrl, and a configured connector named artifacts
    refuses to construct.

  • Artifact library and sandboxed viewer (#563). /artifacts lists 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. With artifactOrigin, 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_refresh attaches a versioned
    program for one data document on a manual, daily, or weekly schedule;
    run_refresh triggers it now, and get_refresh reports freshness and a
    bounded run history. The module's runDue() 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 new ConnectorCallErrorCode for a write whose base
    someone else already moved past, never retryable as-is, with an optional
    bounded current map (at most 20 whole-number entries) on
    ConnectorCallError and CallErrorDetails saying where things stand. A
    program reads it as err.details.current; a top-level call returns it in
    the structured error. A write refused with conflict inside a program is a
    known failure (W9), not an unknown outcome.

Changed

  • execute_code asks for a zero-argument program (#602). Its guidance now
    shows async () => { ... }, with connecta supplied as the sandbox global.
    Passing connecta as 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 preserve ctx.access. Alchemy is not a
    connecta dependency or a second deployment template.
  • execute_code source 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/list adds
    resume_execution everywhere. With resumable writes on, the MCP
    instructions say programs may write and pause, the execute_code
    description advertises the write budget and drops safety: "readOnly" from
    its search example (a program looking for a write would not find it), and
    the usage skill 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 for resume_execution while a direct
    call crosses call_destructive_tool.
  • A program's clock and randomness are pinned (code mode P6).
    Date.now(), new Date(), and Date() inside execute_code return the
    instant the run started and do not advance, and Math.random() is an sfc32
    stream seeded per run from the host's CSPRNG; on a Dynamic Worker,
    crypto.getRandomValues, crypto.randomUUID, and performance.now() follow
    the same pin. The replacements are locked like Error. 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: true measures from the host.

Fixed

  • Request admission recovers after missing Worker cleanup (#595). An
    admitted /mcp request 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 enables enable_request_signal for 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/initialized promise 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 at warn, a passing test's message at
    info — and never record it in activity. A CredentialTestResult.message is
    now log-only, and a Test refused because nothing usable is stored carries the
    same credential_required or credential_mismatch problem the Connections
    page shows.

  • Compact schemas group an array's union element. The compact format,
    the default for search_tools and connecta.search, rendered an array of a
    union as { op: "replace" } | { op: "append" }[], which reads as one
    operation or an array of the other; Linear save_issue's patch was 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 the typescript
    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 a listTools that
    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-deadline connecta.call that started it failed a
    concurrent connecta.search with 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).