v1.2.0
Enterprise Mode, Browser Run, Durable Pi Review Lanes, First-Party Graphify in Pi, Session-Reliability Hardening, Subscribed-Mode Auto-Switch
Enterprise Mode is the headline. A single-tenant Codeflare that deploys into a customer's own Cloudflare account, fronted by their Cloudflare Access, with every LLM request routed through the customer's AI Gateway — no provider key, gateway URL, or token ever inside a container. The entire feature is gated behind one deploy var: with
ENTERPRISE_MODEunset, runtime behaviour is byte-identical to today across the Default, Onboarding, and SaaS deploy modes, and every enterprise surface ships a flag-off regression test. Alongside it: a general Browser Run capability, a substantially more durable Pi PR-boundary review subsystem, first-party native Graphify in Pi, and a run of session-reliability and subscription fixes.
Enterprise Mode
Single-tenant enterprise deployment
A new ENTERPRISE_MODE=active deploy var turns a deployment into a single-tenant install in the customer's own Cloudflare account. All users resolve to the unlimited tier and advanced mode, the subscription/billing UI is hidden, the launchable agent set is restricted, and user identity is owned entirely by the customer's Cloudflare Access rather than by Codeflare. Every change is additive and gated — flag unset means no enterprise env is emitted, the interceptor is never instantiated, and behaviour is unchanged.
LLM routing through the customer's AI Gateway
Enterprise LLM traffic is routed to the customer's AI Gateway via platform outbound-HTTPS interception (interceptOutboundHttps + ctx.exports), so no credential ever lives in the container and the interception path is platform-internal — it never traverses Cloudflare Access.
- REST-API transport. The container DO routes egress through an
LlmInterceptorWorkerEntrypoint that mapsapi.openai.com→api.cloudflare.com/.../ai/v1/<path>, authenticates withAuthorization: Bearer <AIG_TOKEN>, selects the gateway withcf-aig-gateway-id, and stamps a per-usercf-aig-metadataid (from DO props). Responses stream back zero-copy; the interceptor fails closed (503when the gateway is unset/unparseable,400on an unmapped host) and enforces an SSRF host-allowlist. - Worker-side route-pinning. A single operator var,
AIG_LANGUAGE_MODEL, is stamped onto each request'smodelfield on egress for model-routable endpoints (/chat/completions,/responses). Agents carry only a fixed, slash-free handle (codeflare) — the gateway route name never enters the container, so a model id can't be misparsed as a provider by an agent. - Agent roster. Enterprise runs
{copilot, pi, bash}. Claude Code is excluded because its bareclaude-…model id would force a per-request body rewrite on the REST Anthropic endpoint and break the zero-copy passthrough; Anthropic models remain reachable through Copilot/Pi by model id (including a dynamic route). - Per-user spend limits. Because every request is already stamped with the opaque per-user metadata id, operators can set dollar budgets per user under the gateway's Spend Limits — no code change required.
Cloudflare Access host-scoping
In enterprise mode the setup wizard provisions a host-scoped Cloudflare Access app (bare-host domain + a single whole-host destination) so one session cookie covers /app and /api uniformly. This removes the redirect loop a path-scoped app caused, where the cookie scoped to /app was never sent on /api/* and the SPA looped to the Access login. Default and SaaS deploys keep the existing path scoping, so /, the /public/* Stripe webhook, and /auth/* stay public.
Admin-surface suppression, route hardening, and Access-gated JIT
- Just-in-time provisioning. Any Cloudflare-Access-authenticated user is auto-provisioned an unlimited account on first request (no welcome email). An optional
ENTERPRISE_ACCESS_GROUP— set in the setup wizard and stored in KV, no redeploy — gates provisioning through the Accessget-identityendpoint and fails closed; existing admin or prior-JIT records pass through unchanged. - Backend route hardening (fail-closed). User-management, billing checkout/portal/switch,
/auth/subscribe,/request-access, and admin tier config all return403.billing/statusreturns an empty200(the frontend expects 200), the Stripe webhook acks and no-ops, and/auth/statusnever reportsuserCapacityReached. Preferences stays available, with effective mode forced to Pro. - Frontend surface suppression. Manage Users, Manage Subscriptions, the Usage and Subscription dropdown items, the Standard/Pro selector, and the quota banners + upgrade CTAs are all hidden; first-login enterprise users route straight to
/app/.
Enterprise deploy target
- A dedicated
enterpriseGitHub Environment with anenterpriseoption in the Deployworkflow_dispatchdropdown, deployable frommain(and fromdevelopfor iteration) whileproductionstays main-only. The Docker Hub backup workflow was brought to full enterprise parity, so a tenant deploy works identically through the Cloudflare registry or the Docker Hub fallback. One environment per tenant with a separate-accountCLOUDFLARE_ACCOUNT_ID/CLOUDFLARE_API_TOKEN. workers.devis now enabled on every deployment so the setup wizard can bootstrap on the<worker>.<account>.workers.devURL before any custom domain exists; custom domains still front prod/integration after setup, and the operator must gate the*.workers.devhostname behind Cloudflare Access.- Operator-grade README rewrite. Default mode is explained inline (the only mandatory config is the two required secrets); every optional mode and the full settings reference collapse under
<details>dropdowns. The Enterprise section gains an 8-step "Configuring your Cloudflare AI Gateway" walkthrough (create gateway →AIG_GATEWAY_URL→AIG_TOKEN→ model access → dynamic route →AIG_LANGUAGE_MODEL→ redeploy → spend limits).
Operator note.
AIG_TOKENmust be a Cloudflare API token with the Workers AI permission (the REST endpoint lives in the Workers AI namespace). An AI Gateway authentication token, or a token scoped to "AI Gateway: Run" — which is what the gateway dashboard's own button mints — is rejected witherror 10000. For BYOK, store the provider key under thedefaultalias, and use the provider-native model id (e.g.gpt-5.5) in the dynamic route rather than the dashboard's compact id.
Browser Run
A general, advanced-mode browser capability: chrome-devtools-mcp is wired to Cloudflare Browser Run's CDP /devtools endpoint, with a WebFetch fallback for bot-protected, login-walled, or JS-only pages. Public targets only.
Multi-agent runtime (Pi)
Pi Graphify is now first-party native tooling
Pi has no MCP client, so it previously carried a dead mcp.json plus the third-party @gaodes/pi-graphify wrapper. Both are removed in favour of a first-party native extension (graphify-native.ts) that registers graphify_query, graphify_path, and graphify_explain, shelling the same upstream Graphify engine Claude uses. Graph source is resolved in code — current/session repo graphify-out/graph.json, then the same-repo active-repo sentinel, then the merged global graph at ~/.graphify/global-graph.json — and the resolved graph details are returned so answers can be saved back into the same graph. It fails soft with a clear "build a graph first" message when no graph exists. The wrapper, lockfile entries, seed-generator mapping, and r2-seed stripping branch are all gone, and an entrypoint migration drops the legacy package from existing Pi settings so no duplicate tool providers register on upgrade.
Durable PR-boundary review lanes
The Pi review subsystem is rebuilt around disk as the cross-session source of truth.
- Detached headless lanes. Review lanes run as detached, headless
pichild processes (--mode json -p --no-session --no-extensions --no-context-files, plus a bounded review-inspection tool allowlist) that survive the spawning session exiting. Transcripts and lane state persist under.git/codeflare-review-jobs/<head>/, results under.git/sdd-review-results/<head>/. Lanes load only first-party Graphify and a minimal lane guard (plus context-mode when enabled) — never the recursive review machinery or subagents. - Disk-driven reaping and idle finalization. A reload-safe interval reaper reaps finished detached lanes even when no further prompt arrives, starts the next eligible lane (for example doc-updater after spec-reviewer), writes the merged summary, and starts the autofix flow when actionable findings remain — all with zero user input. PID liveness is verified via
/proc/<pid>/statstart-time so recycled Linux PIDs are never trusted or killed incorrectly; only verified-alive over-budget lane groups are killed, on a 15-minute lane budget. - Missed-boundary reconciliation. Triggering no longer depends on catching one fragile boundary event. Bounded reconciliation from
session_start, turn/resource ticks, andagent_endre-derives the review boundary from GitHub state and starts review only for an open, non-draft, enforcedmain/masterPR whose head is unacknowledged — with network throttling, a lag-tolerant enforced-head resolution for a just-pushed branch, and a PR-URL fallback when agh pr createcommand can't be parsed. The contract that passive lifecycle events never review a branch purely because it has a PR is preserved. - Observability. An append-only decision audit (
.git/codeflare-review-events.jsonl) records every branch (boundary_detected,lane_spawned/completed/failed,merge_blocked,review_acked, breaker, near-miss), and a read-only/review-statuscommand renders the canonical review state (PR / local / acked heads, per-lane status, summary, autofix, merge gate) plus a tail of that log. - Merged-summary and autofix gating. Autofix is sent only after the exact-head job is complete and every required lane result file exists; partial, missing, failed, timed-out, or pending lane sets cannot trigger it. Severity tally lines such as
CRITICAL: 0orHIGH: 2are excluded from finding extraction so they never create phantom rows. The autofix prompt states explicitly that a finding's age is never a reason to skip it; a user "wait for approval" instruction is honoured when live session context is present, and idle finalization keeps the default automatic-fix behaviour. - Generated artifacts auto-ack. Generated-only
graphify-out/**diffs require no review lanes (mirrored in Claude'slane-classifier.sh), while mixed diffs are still fully reviewed; the gate auto-acks generated-only graph diffs with a durable audit event. - Statusline. The local Pi statusline reads in-flight review state directly from disk, so completed/running/pending lane progress shows even on timer-driven changes, and clears once every lane completes.
Pi memory and vault-graph parity
All four knowledge-graph pipelines — vault-extract and memory-capture, on both runtimes — now converge on a single cumulative vault-graph.json built by a shared merge-vault-graph.py and publish that cumulative graph to the global graph as user_vault, so vault knowledge accumulates monotonically across extractions instead of being replaced each run. Pi is fully self-contained in .pi (a byte-identical copy of the merge script; nothing in .claude is read at runtime), with namespaced vault sentinels so the advanced-mode vault-monitor daemon and the Pi extension no longer collide, and clone-time Graphify triage now asks before touching a stale or unknown repo graph.
Session reliability
Sessions no longer get stuck falsely "stopped"
A live container could flip to a gray "stopped" dot mid-session and bounce the user back to the dashboard. A storage-persisted 90-second not-running confirmation window must now elapse across not-running ticks before a session is marked stopped — and it clears the instant the container reads running again — while the read path self-heals a session shown stopped that is demonstrably alive, bounding any false-stopped to a single alarm tick. KV status is the single authoritative source of truth (the read-path stale-status re-derivation was removed), and a persisted deliberate-stop marker survives a DO eviction so a user-stopped session never self-heals back to running.
Final R2 sync drained before stop
Stopping or deleting a session shortly after edits could lose everything since the last 15-minute cadence sync, because the platform SIGKILLs the container roughly three seconds after stop is signalled — well short of a sync that can take ~2 minutes. The Durable Object now drains a fresh bisync while the container is still fully alive, before signalling stop: a new awaitable host endpoint triggers the sync daemon and blocks until it reaches a terminal status (120-second budget, best-effort, under a 135-second teardown hard-cap). Stop and Delete both route through it identically.
Subscriptions
Subscribed-mode auto-switch on plan change
A Standard↔Pro subscription change now always updates the user's subscribed mode and re-seeds their R2 bucket with that mode's preseed document set (deleting the keys that belong only to the other mode), then flips the session-mode UI preference. When the Stripe price carries no mode metadata, the mode is recovered from the tier config's price-id slots, so the flip fires even when admins wire prices through tier slots rather than per-price metadata. The re-seed happens at webhook time; a running container picks up the new set on its next start.
Architecture & code quality
Architecture-decomposition backlog
A behaviour-preserving refactor pass landed across the worker: the container Durable Object split into config / router / lifecycle modules behind a typed discriminated-union internal dispatch; routes/vault.ts split into validation / auth / access / crypto; the host server's module-level mutable singletons hoisted into an explicit owned ServerState (after backfilling the units it previously had only E2E coverage for); and parseJsonBody plus external-API JSON (jwt.ts, stripe.ts, r2-admin.ts, and peers) gained Zod boundary validation. A cycle of security and billing/session hardening rode along — security headers on pre-Hono early returns, JWT-audience enforcement, deploy-credential revocation on restart, effective-tier maxSessions honoured in SaaS mode, dead-code removal, and coverage backfill.
CI / deploy / supply chain
- Enterprise deploy targets added to both
deploy.ymland the Docker Hub backup workflow;target=enterpriseis deployable from any branch whileproductionstays main-only. - Generated-seed drift guard — CI fails if the committed
agent-seed.generated.tsdiffers from a fresh generation, andgenerate:agent-seednow parse-checks every Pi extension (check-pi-extensions.mjs, via TypeScripttranspileModulediagnostics) so a syntax error can't be baked into the seed and surface only at Pi startup. - Workers test-pool stabilization — the known
workerdshutdown crash is now distinguished from real test failures and accepted only after Vitest reports all backend tests passed. - Shadow-pin simplification — the Pi preseed bump job now tracks
@gotgenes/pi-subagentsand context-mode only, now that Graphify ships as a native extension rather than an npm pin.
Dependencies (cumulative since v1.1.9)
@cloudflare/containers0.3.5 → 0.3.6;hono4.12.18 → 4.12.23;wrangler4.95.0 → 4.97.0;@cloudflare/vitest-pool-workers0.16.10 → 0.16.12oxlint1.63.0 → 1.68.0;knip6.12.2 → 6.15.0;vitest4.1.6 → 4.1.8;fast-check4.7.0 → 4.8.0 (root).vite8.0.14 → 8.0.16,oxlint1.67.0 → 1.68.0,knip+vitestmatched (web-ui)- Pi preseed:
@gotgenes/pi-subagents7.8.1 → 14.0.1;context-mode1.0.151 → 1.0.162;@gaodes/pi-graphifyremoved (now first-party native) - Claude Graphify plugin
graphifyy0.8.27 → 0.8.35; Dockerfilelazygit0.62.1 → 0.62.2 (SHA256 updated);github/codeql-actionv4 SHA-pin refreshed - Lockfiles regenerated to match.
CVE remediation
Suppressed in .trivyignore with explicit exposure-profile justification matching the established convention; every entry carries an individual, web-verified description.
- CVE-2025-13462 / CVE-2026-3644 / CVE-2026-4224 (cpython 3.11 base image) — incomplete control-character validation and a deeply-nested-DTD XML DoS, same class as the existing cpython tarfile/XML entries. The container parses no untrusted XML and extracts no attacker-controlled archives at runtime; no fix in Debian bookworm.
- CVE-2026-42504 (Go stdlib in rclone / lazygit) — net/mail-class DoS; fixed in Go 1.25.11 / 1.26.4, but rclone and lazygit upstream have not rebuilt yet. Same exposure profile as the other Go-stdlib rclone/lazygit entries.
Full Changelog: v1.1.9...v1.2.0