Deployment specification
Status: Draft
This document defines supported host shapes and the conditions under which a deployment may claim ephemeral, persistent, or durable behavior.
1. Deployment classes
Class E: ephemeral
- one process;
- in-memory queue and state;
- no recovery promise after process loss;
- suitable for libraries, scripts, tests, and request-scoped agents.
The examples/pr-work-orders work-order host is class E trusted-local evidence. Its one-attempt policy is process-local, its worktree is scoped to one run, and its branch update is locally compare-and-swap fenced. It makes no restart-recovery, hosted publication, or untrusted-code isolation claim. See pull-request work orders. The separately named work-order-action/ composes examples/pr-work-order-ingress into an operational five-job GitHub flow with an authenticated repository admission journal, networkless check container, independent network publisher, and bounded thread presentation. See work-order ingress.
Class P: persistent
- canonical conversation and session data survives process restart;
- no accepted-work settlement guarantee;
- suitable for interactive applications that can ask a client to retry.
Class DN: Node/SQLite durable
- durable admission and settlement;
- process restart recovery;
- one active scheduler node;
- local SQLite storage;
- host loss outside storage recovery objectives may still lose availability.
Class DC: Cloudflare durable
- one SQLite-backed Durable Object per Conversation;
- Cloudflare Workers provide stateless ingress;
- Durable Object storage owns Conversation state and queue order;
- alarms wake dirty or autonomously actionable work while stable external waits may quiesce;
- no PostgreSQL dependency.
No package or example may use "durable" without naming DN or DC and the tested adapter.
2. Node.js host
Node.js is the first production host.
Minimum host responsibilities:
- build a root Effect Layer;
- validate configuration before opening admission;
- start HTTP/transport endpoints and scheduler fibers in a Scope;
- expose readiness and liveness separately;
- stop admission during graceful shutdown;
- drain or release active Attempt ownership within a configured deadline;
- flush telemetry without blocking settlement indefinitely;
- close provider, database, sandbox, and MCP resources;
- exit nonzero on unrecoverable root-fiber failure.
The supported Node.js version is pinned in the package metadata and CI matrix.
3. Node process roles
A small deployment may combine roles. A larger deployment may separate:
- API/admission: authenticates clients and durably accepts submissions;
- scheduler: claims runnable submissions and starts attempts;
- worker: runs interpreter Attempts and renews ownership when the platform requires it;
- projector: builds read models and search indexes;
- reconciler: resolves recoverable or unknown operations;
- operator API/UI: exposes administrative actions and audit views.
Correctness cannot depend on role co-location or in-memory notifications.
3.1 Platform Effect services
The durable runtime requires capabilities rather than a platform name. The shipped inventory is exactly three ports, all owned by @effect-agent/session:
class ConversationStore extends Context.Service<ConversationStore, {...}>()(
"@effect-agent/session/ConversationStore",
) {}
class SubmissionLedger extends Context.Service<SubmissionLedger, {...}>()(
"@effect-agent/session/SubmissionLedger",
) {}
class WakeScheduler extends Context.Service<WakeScheduler, {...}>()(
"@effect-agent/session/WakeScheduler",
) {}2
3
4
5
6
7
8
9
10
11
ConversationStoreowns the canonical Conversation Log: materialization, fenced atomic batch append, bounded reads, resumable observation, export, tail inspection, and digest-bound disposable checkpoints (there is no separateCheckpointStoreport).SubmissionLedgerowns operational obligations: admission, readiness, FIFO-head claims, ownership tokens, producer-epoch fencing, lease renewal/release, canonical-input markers, settlement reservation/finalization, abort intent, nonterminal scans, and recovery snapshots. It absorbs the earlierAttemptOwnershipprose service; claims mint Attempt identity and fencing evidence atomically with queue-head selection.WakeScheduleris a pure liveness hint whose notifications may be dropped, coalesced, or duplicated. Workers pair the all-lanes stream with ledger scans. Public progress waits use a separate conversation-keyed, Scope-owned one-shot registration: subscribe first, read one canonical record second, then park. The canonical read is authoritative; the notification only tells the caller to read again.
Earlier drafts referred to a DurableStorage service; that was prose shorthand and no such port exists. Two further ports are explicitly deferred: an AttachmentStore (digest-addressed durable attachments) waits for a real attachment requirement, and a RecoveryScheduler waits for recovery cadence needs beyond the host's startup pass and wake/scan loop.
Node Layers implement these with local SQLite transactions, process ownership, and a local scheduler. Cloudflare Layers implement them with SQLite-backed Durable Object storage, object ownership, and alarms. The semantic coordinator depends on these services and has no conditional branch for node versus cloudflare.
4. Configuration
Configuration uses Effect Config and Effect Schema. It is resolved once during Layer construction and exposed as typed services.
Configuration families include:
- deployment identity and environment;
- host role enablement;
- model providers and routing;
- store connections and pools;
- ownership, retry, timeout, and queue limits;
- telemetry exporters;
- security and redaction policy;
- approval providers;
- sandbox policy;
- retention;
- feature flags and compatibility gates.
Secrets are resolved through a secret provider and wrapped as redacted values. Startup diagnostics may list missing secret names but never values.
5. Startup
The host performs these gates before readiness:
- decode all configuration;
- construct required Layers;
- connect to durable dependencies;
- verify store schema compatibility;
- verify framework and adapter feature compatibility;
- acquire or validate deployment identity;
- start schedulers and projectors;
- run a shallow self-check;
- enable admission;
- report ready.
Failure before step 9 means the process is not ready. A partially available provider may be tolerated only if routing policy has another eligible provider.
6. Shutdown
On shutdown:
- fail readiness and stop new admission;
- stop claiming new work;
- signal active runs to reach a safe point;
- continue Attempt ownership renewal during the drain window where applicable;
- commit any ready settlements;
- release or allow expiry of unresolved ownership;
- close resources in reverse Layer acquisition order;
- flush bounded telemetry;
- exit.
Forced termination is assumed possible at every step. The durability protocol, not graceful shutdown, provides correctness.
7. Health
Liveness answers whether the process event loop and root supervisor are operating. Readiness answers whether the process can fulfill its enabled role.
Readiness for admission requires:
- compatible durable store;
- ability to atomically admit;
- authorization configuration;
- at least one viable routing path if the API promises immediate execution.
Readiness for workers requires:
- compatible durable store;
- scheduler clock/ownership health;
- required model and capability Layers;
- no deployment-wide safety stop.
Provider degradation, queue saturation, and projection lag are surfaced separately from basic process health.
8. Scaling and backpressure
Scaling signals include:
- runnable submission age;
- queue depth by tenant and priority;
- active attempts;
- provider concurrency and rate-limit saturation;
- database transaction latency/conflicts;
- event subscriber lag;
- sandbox capacity;
- settlement obligation age.
Admission implements configured global, tenant, conversation, and principal quotas. Overload returns a typed rejection or a durably queued receipt according to policy. It never accepts work only into an unbounded in-memory queue.
9. Version changes during private development
There is no rolling data-version or migration promise.
- Node development deployments stop, replace code, and reset incompatible SQLite data.
- Cloudflare development deployments replace incompatible development namespaces when needed.
- Stored version mismatches fail before mutation.
- Production-like durability tests use one repository version at a time.
Rolling compatibility is designed only when internal deployment needs or external release require it.
10. Disaster recovery
Each durable adapter documents:
- recovery point objective;
- recovery time objective;
- backup schedule and encryption;
- restore verification frequency;
- point-in-time recovery procedure;
- reconciliation of restored ledger state;
- producer epoch invalidation after restore;
- handling of external side effects newer than the restored database.
Restore drills are part of release readiness for a durable compatibility label.
11. Cloudflare host
Cloudflare is a first-class target alongside Node. The mapping is:
- Workers for API and stateless orchestration;
- one SQLite-backed Durable Object for each Conversation's serialization, history, ledger, and alarms;
- R2 for large artifacts;
- an optional rebuildable store for cross-Conversation administration;
- platform-native observability adapters.
Cloudflare platform APIs are wrapped as Effect services and supplied through Layers. A Conversation runtime requires storage, scheduling/alarm, clock, attachment, and observability services rather than importing bindings in the engine.
CloudflareDurableRuntimeOptions.bindings accepts a per-incarnation callback returning a closed Effect. The callback runs after the Object's Conversation and producer identities are derived and receives the live DurableObjectState, raw Worker environment, conversationId, and producerId. This is the host boundary for capturing environment-backed resources such as Worker service bindings; database clients and other request-scoped resources remain outside the cached Durable Object runtime.
CloudflareDurableRuntimeOptions.runContext is the generic host Run-context boundary. It accepts either a closed CloudflareRunContextLayer or a per-incarnation factory receiving the same explicit Object state, environment, Conversation identity, and producer identity. The Layer must install a RunContextPreparation override containing model-context preparation, action-time Tool authorization, or both; its only permitted remaining requirement is Crypto.Crypto, which the platform supplies with BrowserCrypto. A host using the capabilities compactor closes its own service dependencies before returning the adapter:
runContext: ({ env }) =>
contextCompactorRunContextLayer.pipe(Layer.provide(makeContextCompactorLayer(env)));2
The factory is evaluated once and the Layer is built in the cached runtime Scope for each Durable Object incarnation. Normal Scope closure runs finalizers, but correctness never depends on a finalizer during eviction: Cloudflare may discard in-memory state and construct a new incarnation without a shutdown callback. The reconstructed Layer receives canonical history again before it prepares model context and canonical pending-Run authority again before it authorizes a resumed Tool Handler. With no override the explicit pass-through Layer has neither hook.
runContext, like bindings, toolReconciler, failpoints, and authorization services, is a non-serializable Layer option and is deliberately outside CloudflareDurableRuntimeConfigValue. That Schema continues to decode only scalar identities, cadences, limits, and storage gates before resources open.
NodeDurableRuntimeOptions.estimateCostMicrousd and CloudflareDurableRuntimeOptions.estimateCostMicrousd install the deployment's closed pricing authority (RUN-035). It receives provider, model, and raw Effect AI usage and returns a non-negative microdollar estimate plus optional service-tier/pricing-version identity. The host captures it in DurableRuntimeConfig, so every replacement Attempt applies the same authority; configured costBudgetMicrousd policies fail typed when no estimator exists.
NodeDurableRuntimeOptions.toolFailureObserver and CloudflareDurableRuntimeOptions.toolFailureObserver install the same engine-owned closed observer through toolFailureObserverLayer (RUN-036). Omitting either option explicitly provides absence, masking any observer in the surrounding Layer-acquisition context. These are trusted in-process construction values, outside the serialized configuration Schemas. The coordinator captures the reference once and explicitly provides it to each interpreter Attempt; ambient worker context cannot substitute another observer. Delivery adds no durable mutation or replay and does not change Code Mode's deployment class.
Durable Object storage is the only correctness-critical store for that Conversation. In-memory object state is a cache because objects may stop unexpectedly. Alarm work is idempotent because alarms execute at least once.
Conversation maintenance is incremental rather than a perpetual nonterminal poll. Each Object stores a versioned dirty/processed generation beside its single alarm slot:
- every public or routed mutation advances
dirtyand establishes the alarm in one storage transaction before its first durable effect; - a short incarnation-local gate serializes that pre-arm boundary with pass generation snapshots and acknowledgements, but never spans the mutation body or cross-Object I/O;
- a pass acknowledges only the generation it observed. It cannot acknowledge while an RPC/port mutation remains in flight, and a later racing generation therefore retains its alarm;
- after eviction the in-memory gate/count resets, while the durable unprocessed generation and pre-armed alarm cause recovery to resume any committed autonomous work;
- once classified as a stable externally-driven wait (
ApprovalPending, unresolved ordinary outcome, joined child, or child awaiting parent establishment), the observed generation is acknowledged and the alarm clears. Its resolving mutation re-establishes both generation and alarm before changing the wait; - ready FIFO followers behind a stable external wait do not make the lane actionable. Admission repair, an accepted abort, and pending terminalization still require maintenance, including an abort whose ownership claim was deferred. Neither elapsed time nor queued followers authorize resolving or aborting an unknown outcome;
- autonomous retry, indeterminate admission/establishment, lease recovery, and other locally actionable states leave the generation unprocessed and rearm with bounded backoff;
- a forced alarm with
processed >= dirtyreads only the maintenance record, clears the alarm, and returns. It performs no runtime recovery, ledger scan, or canonical-history read.
Child-to-parent settlement follows the same quiescent contract. After the exact child Settlement record is canonical but before child ledger finalization, the child routes the idempotent durable settlement marker to the parent. That routed mutation pre-arms and dirties the parent, so child eviction after finalization cannot strand a quiescent parent. Same-store ledgers accept this notification only for the exact terminalizing reservation; earlier states fail closed.
CloudflareConversationClient.awaitProgress(conversationId, afterSequence) carries that same Effect-native boundary across RPC. A normal wait performs no periodic read and creates no alarm loop. Every canonical append and the durable approval/unknown/abort/settlement transitions emit a best-effort hint after their authoritative commit. RPC interruption sends a scoped cancellation; an Object eviction rejects the old call, after which the client retries only a platform-classified reset with a fresh stub. The reconstructed Object subscribes and rereads canonical storage before parking, so disposable memory can neither strand the caller nor impersonate durable progress. Each logical client wait obtains one UUID from the explicit Crypto.Crypto capability and reuses it across reset attempts. That identity groups duplicate transport attempts for cancellation but remains disposable coordination state; canonical records alone establish durable progress. Host-supplied CloudflareDurableRuntimeOptions.operationAuthorizer decisions cross observation, progress, approval, and unknown-resolution RPCs as the typed OperationDenied.
The target is no longer experimental. The generic durability conformance suite passes inside workerd with the same adapter-neutral case arrays as the Node adapters. The eviction (per-failpoint ctx.abort() with alarm-only convergence), alarm-retry (double-fire and throw-retry), runtime-restart (Miniflare dispose/reopen over persisted storage), and fault-injection (failpoints on every durable mutation plus routed-transport faults) scenarios are implemented and green (see the platform-cloudflare test suites). The tested harness is workerd/Miniflare; the hosted production service, its observability adapters, and live soak remain explicitly unclaimed (see the certification suites), and hosted-service operation stays outside the claims until open-source preparation revisits it.
Native Conversation RPC tracing
Native RPC trace propagation is disabled by default. A host enables both ends explicitly:
conversationNamespaceLayer(env, "TASK_ORCHESTRATORS", { rpcTracing: true });
const ConversationBase = makeConversationObjectClass(
{ ...runtimeOptions, namespaceBinding: "TASK_ORCHESTRATORS", rpcTracing: true },
observability,
);2
3
4
5
The namespace service retains the stable binding name, not a trace context. Each enabled CloudflareConversationClient call creates a client span named binding/actualMethod, such as TASK_ORCHESTRATORS/submitEncoded or PERSONA_ADVISORS/observePage. It covers the native wait and host-response Schema decoding. The span records rpc.system.name = cloudflare, the fully qualified rpc.method, server.address = binding, and sentry.op = rpc. Names and these attributes contain no Conversation IDs, messages, Tool arguments, or capability URLs.
Inside that span, the client appends exactly one native argument carrying { _tag: "effect-cf/RpcTraceContext/v1", traceId, spanId, sampled }. It copies the current client span, including an unsampled decision, rather than the enclosing application span. Calls with propagation disabled, including Effect's non-propagating no-op spans, retain their exact original argument count. They do not append undefined. The ordinary disabled namespace retains its prior client instrumentation.
The Object factory passes rpcTracing: { service: namespaceBinding } to effect-cf 0.34.0 or a compatible release. The client uses effect-cf's RpcTracing.withRpcTraceContext and withRpcClientSpan: they validate span IDs, honor Tracer.DisablePropagation, and preserve the original failure while recording safe RPC failure status without its payload. effect-cf validates and strips the argument and exposes event metadata; application observability owns server roots. The library does not create durable invocation roots or exporter policy.
The factory's public return type retains DurableObject.RunSymbol, including the runtime and event Layer service requirements. An application can wrap the complete receiver effect through that hook, then delegate to effect-cf:
import { Effect } from "effect";
import { DurableObject, RpcTracing } from "effect-cf";
type Services = Effect.Services<
Parameters<InstanceType<typeof ConversationBase>[typeof DurableObject.RunSymbol]>[0]
>;
export class ConversationObject extends ConversationBase {
override [DurableObject.RunSymbol]<A, E>(
effect: Effect.Effect<A, E, Services>,
options: DurableObject.RunOptions = {},
): Promise<A> {
return super[DurableObject.RunSymbol](
options.rpc === undefined ? effect : RpcTracing.withRpcServerSpan(effect, options.rpc),
options,
);
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
options.event distinguishes RPC, alarm, and other native events; options.rpc supplies transient RPC metadata before the event Layer starts. Its arguments remain private and must not be logged. No invocation service is required by the observability Layer. Applications can separately root alarm and other durable invocations using options.event without reusing a prior caller's parent.
Trace context never enters a Submission, Receipt, canonical record, checkpoint, queued wake, alarm, or durable retry. A native progress reset creates a new client span for each retry and does not cache trace context in its request. Recovery and resumed work obtain fresh roots from the host's current event, not the original caller. Cross-Object port calls and wake hints keep their existing wire arguments. Scope ownership, cancellation, typed failures, and effect-cf's bounded flush lifecycle are unchanged.
Dynamic Worker Code Mode executor
The first isolated CodeExecutor adapter is a Cloudflare Dynamic Worker Layer in @effect-agent/platform-cloudflare. Each pass creates one fresh Worker through the Worker Loader with globalOutbound: null, supplies only the scoped Tool-broker RPC capability and explicitly allowed structured values, applies the configured Dynamic Worker CPU and subrequest limits plus an executor-owned wall-clock deadline (an asynchronously suspended pass consumes no CPU and must not outlive its deadline), invokes one fixed entrypoint, validates the returned envelope through Effect Schema, and disposes the entrypoint and Worker handles in Scope finalizers. The executor creates one RpcTarget in the caller's current event and passes it as the entrypoint invocation's argument. Workers RPC routes each callback to that target's owning event context and releases the remote stub with the invocation; no callback registry or request state lives at module scope. Host callbacks run one at a time on a Scope-owned child fiber of the pass so they inherit the execute Context and die with the pass Scope, while remaining a sibling of the guest RPC waiter so the return RPC is not coupled to the still-open entrypoint.run(); pass teardown closes callback admission, interrupts and awaits active work, and settles queued calls. One absolute monotonic deadline applies to the worker RPC and every host callback. A synchronous runaway program is stopped by platform CPU limits rather than relying on a JavaScript timer alone.
The adapter records no persistent state and adds no deployment-class claim beyond E: the DN and DC assemblies make no Code Mode claim until this specification says otherwise. The tested harness is workerd/Miniflare; hosted-platform evidence remains unclaimed. No cost or performance claim is made before measurement. Current Dynamic Workers billing counts no-ID load() use as a new Dynamic Worker per invocation, and any future stable-ID Worker caching must include tenant and binding context in cache identity.
Browser Run Quick Action page capture
The first PageCapture adapter (capability spec §9.2) is the Cloudflare Browser Run Quick Action Layer in @effect-agent/platform-cloudflare (browserQuickActionCaptureLayer). Each capture is one stateless quickAction() RPC on the Wrangler browser binding. The host resolves that binding explicitly and supplies it through BrowserQuickActionBrowserBinding.layer; both capture Layers visibly require the resulting Effect service (DEPLOY-010) and never read ambiently. The Layer accepts Cloudflare's pinned native BrowserRun and exposes the five supported actions as Effect methods using the native option types, so incompatible Quick Action options fail compilation. Native Promise rejection becomes a typed binding RPC error before the capture adapter maps it into the PageCapture error union. Construction-time host patterns reach the binding as allowRequestPattern, restricting navigation, redirects, and subrequests alike. A capture-owned Scope reads response bytes incrementally and stops at the first chunk exceeding the request budget. It cancels and unlocks its reader on success, failure, or interruption. Response Content-Type, not attacker-controlled page text, identifies the Cloudflare JSON response envelope. A successful native binding response without that documented envelope fails typed instead of being reinterpreted as a REST payload. Every returned link must satisfy the canonical bounded, credential-free HTTP(S) link Schema; malformed entries, unsupported schemes, embedded credentials, and over-limit collections fail typed instead of being discarded. The scrape action projects the portable selector list into Cloudflare's elements request and decodes the provider's grouped response through the portable scrape Schemas. Malformed groups, excess aggregate elements, excess attributes, non-finite geometry, and encoded responses beyond the caller budget fail typed; the adapter never returns a truncated scrape as success. HTTP 429 becomes a typed rate/quota failure; Retry-After is included only when conversion to milliseconds remains a safe integer. Foreign browser RPC and response-stream failures retain their original cause; provider envelope errors, rate-limit bodies, and non-success HTTP bodies remain bounded host-only diagnostics. Model-visible messages and cleanup logs use fixed operation, quota, or HTTP-status descriptions and never expose foreign diagnostic text.
The json Quick Action uses Cloudflare's separately billed Workers AI provider. It fails closed unless the host selects browserQuickActionWorkersAiCaptureLayer and supplies its visible BrowserQuickActionWorkersAi service. That service's authorizeAndAccount Effect runs before the browser RPC. Its narrow policy error maps to PageCaptureInferencePolicyError with a fixed public message; the host diagnostic remains only in the live cause. Successful results report cloudflare-workers-ai plus one model call alongside any X-Browser-Ms-Used observation. The ordinary browserQuickActionCaptureLayer has no Workers AI authority. The host-owned binding service keeps browser RPC authority visible in both adapter requirement channels, while the opt-in Layer also requires Workers AI authority. The adapter rejects typed the kitesurf engine the binding cannot select. browserRestCaptureLayer is the second PageCapture implementation: a Node-safe, explicit-credential Layer requiring Effect HttpClient. It keeps Cloudflare REST endpoint and envelope details in platform-cloudflare, uses the fixed https://api.cloudflare.com/client/v4/accounts/{account}/browser-rendering/{action} endpoint, and adds browser=kitesurf only for Kitesurf (Chromium omits that query). It preserves the same bounded stream decoding, typed error mapping, and explicit Workers AI authorization split as the binding. quickAction() requires a Worker compatibility date of 2026-03-24 or later and has no local implementation: local wrangler dev needs remote mode. Ordinary tests use a scripted binding inside workerd. An opt-in live smoke in the provider-owning examples/providers leaf additionally runs one real OpenAI-backed Agent through the production capability and adapter against Cloudflare's real markdown Quick Action. It reads Linear's public pricing page, compares two actual subscription prices, and prints a bounded page excerpt with the Agent's schema-validated recommendation. Its test-only Effect HTTP transport maps the binding contract onto the documented Quick Action REST endpoint, so the smoke needs no deployed Worker and does not invoke separately billed Workers AI extraction. It requires EFFECT_AGENT_LIVE=1, OPENAI_API_KEY, CLOUDFLARE_ACCOUNT_ID, and CLOUDFLARE_API_TOKEN:
vp run --no-cache -F @effect-agent/example-providers test test/browser-live-smoke.test.ts --reporter=verboseThe separate private examples/browser-run-worker-proof leaf closes the binding-transport evidence gap without a model. Its fixed opt-in Effect workflow deploys one collision-resistant temporary Worker with a native BROWSER binding and compatibility date 2026-03-24, invokes one bounded Markdown WebCapture.make handler against https://example.com/, validates the stable Example Domain fact, invokes WebCapture.makeScrape with two selectors and validates the grouped heading, captures one bounded PNG through PageScreenshot, validates its eight-byte PNG signature, discards the image bytes, and deletes the Worker through a Scope finalizer. The Worker leaves 11 seconds between each Quick Action to honor the Free plan request interval. The proof response contains only bounded validation metadata. The workflow fails if its generated name already exists, never retries an unresolved invocation, and surfaces deletion failure. The ordinary test suite scripts the deployment operations and verifies finalization without Cloudflare credentials. The hosted proof is:
vp run --no-cache -F @effect-agent/example-browser-run-worker-proof prove:liveBrowser Run interactive browser sessions
The scoped InteractiveBrowser adapter uses the explicitly supplied Browser Run browser binding and @cloudflare/puppeteer 1.1.0. It launches one browser pass, context, and page under Scope, with immediate reverse-order finalizers. keep_alive is only Cloudflare's inactivity setting; the caller's elapsed-time deadline is authoritative. network: { _tag: "ExactHosts", allowedHosts } preserves URL checks for navigation and intercepted requests on the owned page. This is a page-request policy, not session-wide network containment. Capacity refusal and remote expiry remain typed; uncertain actions are never retried or replayed. This capability has no durable session, registry, execution reconnect, or model Tool.
network: { _tag: "Unrestricted" } opens the same scoped browser/context/page without URL/host allowlist enforcement. Agent navigation and URL observations accept credential-free HTTP and HTTPS through InteractiveBrowserTargetUrl; intercepted requests continue without a network policy check. There is no URL/host or private-network containment guarantee, including for redirects, page resources, and human navigation. The finite action, elapsed-time, and per-result byte limits, receipts, live view, handoff, handoff state, and close behavior remain unchanged. This explicit opt-out does not satisfy the stronger PublicWeb requirement or issue #207.
network: { _tag: "PublicWeb" } fails with InteractiveBrowserUnsupportedError, feature: "policy", before any binding operation. The adapter does not launch a browser, navigate a test URL, connect for execution, or issue a viewer capability to probe support. This refusal is deliberate. The pinned client has no session guardrails, and even Cloudflare's newer API describes only hostname patterns and named hostname lists for HTTP/S traffic. That does not establish a public-address check at connection time, credential rejection, or coverage of every traffic source. Upgrading the client alone therefore cannot enable this policy.
The current boundary is:
| Traffic or action | ExactHosts behavior | PublicWeb requirement |
|---|---|---|
| Adapter navigation | Check credential-free HTTPS and exact URL.host before goto; validate the returned URL. | Permit unrelated public HTTPS sites without replacing the pass. |
| Redirects and page resources | Check URLs delivered to the original page's request interceptor; abort denied requests and invalidate subsequent operations. Third-party authorities must be listed too. | Enforce destination safety before each request and connection, including every redirect. |
| DNS and connection addresses | No connection-time address classification or DNS rebinding protection. Explicit hosts can resolve to non-public addresses. | Reject private, reserved, and internal destinations using the address actually connected to. |
| Popups and new targets | No interceptor is installed on other targets. Owning one automation page does not prevent a page from creating others. | Enforce before a target can send traffic, or prevent target creation. |
| Dedicated, shared, and service workers | Bypass service-worker handling for requests from the owned page; this does not stop workers from issuing their own traffic. | Enforce worker traffic or disable it before activity. |
| WebSockets and other channels | No socket or non-HTTP traffic boundary is established by page request interception. | Enforce the public-destination rules for secure sockets and disable other unsupported channels before traffic. |
| Hosted viewer navigation | Ordinary requests observed on the owned page use its interceptor, including during handoff. The viewer is a CDP capability and tab mode does not prevent bypass through other commands or targets. | Retain enforcement independently of viewer commands for the whole pass. |
Of the table's two columns, only ExactHosts describes a supported execution mode. It requires trusted pages and trusted operators. The adapter makes no claim that this mode confines hostile browser content or a viewer recipient. A host that requires the right-hand boundary must request PublicWeb and handle its typed refusal. No local IP blacklist, one-time DNS resolution, wildcard, page script patch, or after-the-fact target closure can establish the missing boundary.
This assessment uses the Cloudflare acquisition API and the provider's guardrail contract, checked on 2026-08-27. The latter latches hostname policy for the session lifetime but does not specify the stronger public-address boundary. A supported implementation needs a provider contract and verification of every row above, including human navigation, before this refusal can be removed. The Live View documentation describes the viewer's CDP connection and Human in the Loop describes handoff on the same target.
fill replaces the value of input, textarea, and select controls. It focuses the element, invokes a callable value setter from the element's prototype chain, then dispatches bubbling input and change events. Bypassing instance setters lets controlled React fields detect the change through onChange and update component state. A missing selector match or an element without a callable prototype setter fails with InteractiveBrowserActionError for fill.
Arbitrary CDP execution is intentionally absent. The pinned browser client cannot enforce an immutable session egress boundary, and Cloudflare's newer hostname-only guardrail cannot express the exact HTTPS-origin policy required by the portable contract. Request interception is not sufficient against arbitrary CDP domains. This deployment therefore makes no constrained-CDP or browser Code Mode claim.
The generic handle's screenshot and scroll use the existing Puppeteer page. Screenshots request PNG, check the returned signature and byte count, and reuse PageScreenshotResult without invoking the stateless capture port. Puppeteer buffers the image before the adapter can inspect it; the byte limit bounds returned data, not provider allocation or transport buffering. Scrolling applies the Schema-defined viewport deltas, then checks the observed page URL. Both operations use the same single-operation gate, action count, and absolute deadline as the other page operations.
browserRunInteractiveHostLayer() provides BrowserRunInteractiveHost from the same explicit BrowserRunInteractiveBinding. Its scoped open(policy) returns a private host session with handle, redacted sessionId, getLiveView, handoff, getHandoffState, and close. browserRunInteractiveLayer() projects only the generic handle. Hosts that need operator controls open through the host service and pass only session.handle to their browser workflow.
Live View and handoff use Cloudflare's CDP extensions on the owned page. Live View supports only mode: "tab", the UI Cloudflare requires for handoff. Full-browser and DevTools UI modes are outside this adapter's contract; tab mode itself is not an authorization boundary. Its request supplies expiresInMs between 60,000 and 3,600,000 milliseconds; handoff supplies at most 1,024 characters of instructions and a finite timeout. Both durations must fit within the pass's remaining time. The result URLs and handoff identifiers are redacted values. The pinned Puppeteer package does not type Cloudflare's extension commands, so a narrow SDK boundary returns unknown values for Effect Schema decoding. CDP sessions belong to Scope and detach during cleanup, including when acquisition completes after interruption. The host may initiate handoff and query its state, but the framework does not arbitrate controller ownership, await a human decision, or persist action receipts. Closing the session ends the pass; there is no separately documented provider command to cancel a handoff. URL expiry limits the initial viewer connection, not an already-open viewer. Human interaction does not pass through the framework's action counter. Hosts must authorize trusted operators and close the session or its Scope to terminate viewer access; checking an automation deadline does not revoke an active Live View connection.
handle.close and host-session close are Effects that invalidate the handle and share one reverse-order teardown with Scope cleanup. Explicit close reports typed failure; Scope cleanup logs fixed warnings without masking the original Exit. BrowserRunInteractiveHost.closeSession(sessionId) is an explicit cleanup operation for a host-retained redacted identity. It makes one bounded puppeteer.connect(binding, id) attempt only to close the remote browser, never to expose or resume a handle. A still-owned, expired, or unavailable provider session can make this operation fail typed. It performs no retry and is not a forced-close guarantee. The host remains responsible for authorizing cleanup, retaining private identity where needed, and reconciling failed cleanup after process loss. No effect-cf change is required.
The opt-in Worker proof uses compatibility date 2026-03-24 or later and one allowed public HTTPS page. It navigates, reads, scrolls, captures PNG bytes from the same session, creates a tab Live View, starts a bounded handoff, checks its active identity, and explicitly closes the session. It checks that the old handle rejects further actions and returns only bounded validation metadata. It discards all screenshot bytes and private provider values. Running its scripted tests is not live Cloudflare evidence, and the hosted proof is never a required CI gate.
Browser Run PNG screenshots
browserQuickActionScreenshotLayer adapts the explicit Browser Run binding to PageScreenshot. It requests binary PNG only, rejects Kitesurf before the RPC, validates image/png, and releases the response body on every exit. The returned bytes belong only to the direct caller and never enter framework persistence, telemetry, logs, or metadata.
The adapter records no persistent state and claims deployment class E only.
Browser Run REST crawls
browserRestCrawlLayer is the Node-safe Cloudflare implementation of the scoped PageCrawl contract (capability spec §9.4). Construction requires an account identifier, a redacted API token, and Effect HttpClient; every request targets the fixed https://api.cloudflare.com/client/v4/accounts/{account}/browser-rendering/crawl origin. The Layer creates one job, polls its lightweight status with Clock and Schedule, then lazily requests result cursors as the consumer pulls. It performs no retry or job reattachment and exposes no job handle.
The request disables external links and subdomains, while the adapter independently validates the exact starting host on every source and redirect-metadata URL. Incremental JSON reads have separate control and result transport bounds. The caller's page-count, per-page UTF-8 byte, aggregate byte, and absolute deadline limits remain authoritative across creation, polling, and pagination. A terminal provider status suppresses cancellation. Otherwise the consuming Scope issues exactly one DELETE after creation on failure, defect, deadline, interruption, or early close. Deletion failure emits one fixed bounded warning and leaves the primary Exit unchanged.
Cloudflare may retain crawl results for up to 14 days even though the framework does not persist or return their job identity. The opt-in provider smoke uses the link-free https://httpbin.org/html fixture, permits at most three pages on that exact host, and logs only bounded Markdown excerpts. It requires EFFECT_AGENT_LIVE=1, CLOUDFLARE_ACCOUNT_ID, and CLOUDFLARE_API_TOKEN:
vp run --no-cache -F @effect-agent/example-providers test test/browser-crawl-live-smoke.test.ts --reporter=verboseCurrent platform references:
- Browser Run Quick Actions
- Browser Run selector scrape
- Browser Run screenshot Quick Action
- Browser Run structured extraction and Workers AI
- Browser Run crawl endpoint
- Browser Run limits
- Browser Run Puppeteer
- Browser Run Live View and handoff
- SQLite-backed Durable Object storage
- Rules of Durable Objects
- Durable Object alarms
- Dynamic Worker Loader API
12. Packaging and release
- framework and platform code live in owner-gated
packages/*; private runnable benches may live in leafexamples/*, and there is no deployableapps/workspace; - a
mainpush may enter package publication only when its complete tree is the exact Changesets version regeneration from its first parent; the absence of pending changesets alone grants no release authority; - the root Bun catalog pins the exact Effect v4 version before 1.0;
- workspace manifests consume that version through
catalog:and may not introduce another copy; platform-cloudflarepublisheseffect-cfas a compatible caret peer while the root catalog selects its exact development version, so the host owns one shared Effect service identity;platform-nodeandplatform-cloudflareare Layer-assembly libraries, not application entrypoints;- releases include generated API docs, changelog, and supported Effect/platform versions;
- canary tags precede stable tags;
- durable adapters may have a maturity label independent of the core engine;
- examples pin package versions and identify their deployment class.
13. Requirements
- DEPLOY-001: Every deployment declares E, P, DN, or DC behavior.
- DEPLOY-002: Node.js is the first supported host.
- DEPLOY-003: Configuration is schema-validated before readiness.
- DEPLOY-004: Readiness is role-specific and distinct from liveness.
- DEPLOY-005: Shutdown stops admission before draining workers.
- DEPLOY-006: Correctness survives forced termination; it does not depend on graceful shutdown.
- DEPLOY-007: Admission has explicit bounded quota and overload behavior.
- DEPLOY-008: Private development fails clearly on incompatible stored versions and makes no rolling compatibility promise.
- DEPLOY-009: Durable adapters publish and test backup/restore procedures.
- DEPLOY-010: Cloudflare platform bindings are supplied as Effect services/Layers and remain experimental until they pass the shared durability suite.
- DEPLOY-011: The Dynamic Worker Code Mode executor denies ambient egress, enforces platform CPU and executor wall-clock limits, routes host calls through a pass-scoped RPC target owned by the caller's event context, disposes Worker and RPC handles in Scope finalizers, and claims deployment class
Eonly. - DEPLOY-012: Cloudflare Conversation maintenance is generation-incremental and quiescent for stable external waits; pre-armed mutations, pass acknowledgement, restart recovery, and autonomous rearming obey the protocol above.
- DEPLOY-013: A Cloudflare host context-preparation Layer is acquired once per Object incarnation, runs only after canonical resume reconstruction, never replaces canonical history, and is reconstructible without process-global state.
- DEPLOY-014: The Browser Run Quick Action page-capture adapter visibly requires the host-provided service for an explicitly resolved browser binding, applies the fixed browser-request allowlist, incrementally enforces the response byte budget with scoped reader cleanup, distinguishes response envelopes by trusted metadata, rejects malformed link and selector-scrape payloads, keeps platform refusals and safe backoff hints typed, denies Workers AI without its explicit host authorization and accounting service, and claims deployment class
Eonly. - DEPLOY-015: The Browser Run REST crawl adapter uses one fixed API origin and redacted token, creates one private job, applies an absolute deadline across polling and lazy bounded pagination, performs no retries or reattachment, and cancels a known-running job exactly once when its Scope exits; cancellation failure emits a fixed warning without changing the primary
Exit. - DEPLOY-016: Native Conversation RPC tracing requires explicit client and receiver opt-in, spans the native wait and response decoding with stable binding/method client spans, preserves disabled argument counts, and transports only transient current-span identity. It never resumes an old caller's trace from durable state or takes ownership of application roots and exporters.