Skip to main content

Isolation boundaries

Kaizen runs code and content it does not trust in separate places, with different things withheld from each. Three of those places are tiers of its own execution stack; the fourth is a third party's web page, which runs inside the first.

The three tiers​

TierRunsWithheld
Code sandboxMember-authored code (run_code, artifacts)Everything. Network off by default, no host access, no other workspace
Package runnerThird-party packaged tool codeSecrets. The process deliberately holds none of the backend's tokens or keys
Agent runtimeThe conversational loopData and secrets. It has the loop, not the authority

Each tier is a separate process with a separate resource envelope. None can reach the trusted backend's credentials.

A browser session is that fourth case, and it deliberately gets no fourth place: it runs in the code sandbox's own container, where the untrusted thing is a live web page instead of authored code. The container is the same; what can go wrong inside it is not, so it has its own section below.

The code sandbox​

Each run gets a locked-down, operator-created worker. The standard Kubernetes path uses the cluster's normal container runtime. A cluster administrator may configure a gVisor RuntimeClass for stronger syscall isolation, but gVisor is not an extension-install or Package Creator prerequisite. The environment is disposable and scoped to the run, and network egress is controlled rather than open — disabled entirely by default.

The standard runtime is a hardened shared-kernel boundary, so it does not offer gVisor's user-space-kernel protection. Regardless of runtime, unavailability fails closed: Kaizen never moves authored code into its backend or package runner as a fallback.

The sandbox is intentionally unprivileged and is not the deployment host. Artifact scripts that explicitly require root, sudo, visudo, or /etc/sudoers are classified as host-administration work before dispatch. The member can review or download them, but Kaizen does not show Run, does not auto-run them, and does not retry them in a more privileged environment.

Single-tenant while leased​

A ready sandbox becomes single-tenant the moment it is leased. Afterwards Kaizen kills leftover processes and strictly wipes the workspace before returning it to the pool.

A sandbox is discarded and replaced only when that reset cannot be proven clean. The reuse decision requires positive evidence — not merely the absence of an error.

Cross-replica ownership​

Controller-created pool state and member admission live in deployment-scoped Valkey state, not API process memory. Assignment, release, reset, retirement, and creation recovery are atomic under that shared authority. Raw member and session identifiers are hashed before storage.

Reset has a fenced operation token and cannot publish a worker as warm unless the wipe succeeds and the token still matches. Workers do not validate that token during dispatch, so Kaizen never transfers an expired worker endpoint to a new owner. It confirms controller deletion, then creates a clean replacement. Ambiguous creates remain counted until inventory reconciliation proves absence or confirms deletion.

Loss of Valkey or controller inventory closes admission. No replica may fall back to process-local allocation, because that would weaken deployment-wide capacity and single-tenant guarantees.

Keyed to actor and conversation​

The workspace key is actor plus conversation, which is what makes shared chats safe. In a collaborative room, participants never share:

  • files or durable workspace snapshots,
  • installed packages and caches,
  • environment values, processes, credentials, or provider grants,
  • private stdout, stderr, or generated files.

Authorized resources as a read-only projection​

Linked chat documents and artifacts cross into the sandbox only as an exact, bounded, read-only projection.

In a private chat the projection is the turn's own authorized attachment scope, resolved on the server under the initiating member. A request's document ids are treated as selectors and re-resolved; an id the member cannot read, including another member's private file, resolves to nothing and the run is refused rather than quietly narrowed. This matters because the composer clears its chips onto the message it sends, so later turns in the same chat carry no ids at all and the resolved scope — never the request — is the authority.

Private turns can also project an exact ready document discovered by authorized retrieval and explicitly requested for that turn. The same digest, size, path, count, and aggregate-byte checks apply. The request cannot be made by title or path and never widens a shared conversation's audience.

Every browser chat is a collaboration room, including one only its owner has joined. A file the member picked from their private Library is bound to that chat as an owner-scoped retrieval reference, not linked into the room, so it is never part of the room's shared manifest. It reaches a room sandbox only while the member is the room's entire audience: the run's admission snapshot must list exactly that member, and immediately before the bytes load the server re-reads the room's live audience version and active roster — direct members and members who reach the chat through a Project — and refuses with resource_audience_changed unless the version is unchanged and the member is still alone. The selection is re-resolved under the member, so a revoked or foreign document is never projected. A room anyone else can see never receives a private Library file. When the selection is withheld — the audience changed, a selected file is not ready, or the selection alone or together with the room's files exceeds the projection bound — a room with linked files of its own still runs on exactly those files, and a run that never named the shared-resource contract runs without it; the specific refusal reaches the member only when the run asked for files and the selection was its only source. The selection is offered to every run in such a room, not only to code that names the contract, so it carries the same bounds and live audience check on every call. A room that was already shared when the turn started never considers the selection and reports no attached file when it has none of its own.

Residual risk: outputs a run writes are linked into the room at the audience current when they are linked. A member who joins between the final audience re-read and the end of the run can therefore see an output derived from the private file, and outputs computed while the chat was private remain visible to anyone the chat is later shared with. Neither path exposes the original file itself.

Before code starts, the sandbox verifies the room's current audience version, resource-manifest digest, file paths, sizes, and content digests. Projected files live outside the writable workspace and are never included in its saved snapshot.

The manifest is compared with the one the turn was admitted with. A file the same run linked after it started — an earlier code run's output, a tool result, or an artifact it created — belongs to that run: its link records that turn and member, and was made after the point the turn was admitted at. It does not stop the run, and it is not projected into that run's read-only files; it is projected from the next turn. A file the run's own code wrote stays in its writable workspace. A new version of an artifact or document the turn was admitted with stops the run instead, because code in that reply could only read the previous version. Any other change between admission and dispatch, such as another participant's file, a file from a different turn, or a removed file, stops the run before any file is read.

This rule also covers native outputs retained from a prior run. A current room member may use an exact shared PowerPoint or other generated file in a later turn, but the model receives metadata rather than raw binary content and the sandbox receives only the digest-matched read-only bytes. The participant's writable workspace and the file creator's private artifact library remain separate.

If the actor, conversation, capability, manifest, or read-only projection cannot be proven, execution fails closed. Kaizen does not fall back to an owner workspace, a shared workspace, the host, or in-process execution. An isolation denial is terminal for that attempt; it never triggers a retry at a weaker boundary.

Shared-turn outputs​

Raw stdout, stderr, snapshots, and unretained workspace files stay private to the initiating participant. A validated file explicitly generated for a shared turn is linked automatically to that exact room through the normal size, type, audit, and conversation-resource policy. The shared projection contains only the durable file reference and bounded metadata, never file bytes or unrelated sandbox output.

Tool diagnostics have two projections. The model may receive bounded execution output needed to diagnose and repair its own failed attempt; members receive a sanitized status projection. Declared output filenames may drive transient generation progress, but those placeholders carry no resource identity or URL and cannot be used as an authorization capability. In a shared room a placeholder names its call by the run-bound room reference, never by the model provider's own call identifier.

Linking one retained output does not expose the rest of the workspace.

Native Pi proposal validation​

Native Pi package proposals are tested in this same sandbox tier before admission. Kaizen sends a server-derived profile, never a model-selected runtime or command, and the worker loads resources against the exact Pi dependency lock used by the product sidecar. Skill-only validation uses the real Pi skill loader; extensions must pass Node tests before the real extension loader runs. After those tests, the worker discards the test tree, materializes a fresh exact copy from the server-supplied files, and reinstalls declared dependencies within the original validation deadline. A test cannot rewrite the files that the loader subsequently observes, and preparation failure preserves the completed test evidence while failing the overall validation.

Authored extension modules execute only in a disposable child process. The trusted validator parent never imports them. The child drains a random parent-issued nonce and closes the advertised result descriptor before it loads authored code; the parent rejects any observation that does not return that nonce over its separate bounded channel. The reported extension tool and unsupported-registration surface remains discovery evidence produced by a process that executed authored code. The parent owns the channel and nonce, reads static skills, prompts, and themes itself, and observes the host runtime version independently. Stdout is diagnostic output, not a trusted result channel. Both Pi images patch the host dependency tree from fixed package manifests and verify the resulting tree during the image build before the validator is copied. The Python production lock requires pyasn1 0.6.4 or newer and CI audits its hash-pinned export, preventing a later resolution from restoring the vulnerable ASN.1 parser release. The worker verifies a canonical workspace and makes it the validator's working directory; the validator does not accept a package-root argument. Each declared resource is then resolved to a canonical path before either loader receives it. A resource that traverses outside the package or reaches outside through a symlink is rejected before Pi reads it.

The returned receipt is a bounded projection of the observed runtime, tests, surface, unsupported registrations, and diagnostics. It is bound to both the authored artifact hash and reviewed-contract digest. Any contract or file change invalidates it, and submission and approval require a current pass. Sandbox unavailability or loader failure has no backend or package-runner fallback.

Browser sessions execute the page, not the member​

An embedded browser session renders an arbitrary third-party page in the sandbox worker. A pinned media adapter sends the desktop to an authenticated client beside the chat; the page itself never crosses that boundary as HTML or script.

What it buys the member. The page is a top-level navigation in the worker's Chromium. Its scripts never execute in the member's origin, its cookies are never in the member's jar, and it cannot read the member's Kaizen session. The pane executes the checksum-pinned, rebuilt media client and receives media plus signaling from the sandbox, not executable target-site content. X-Frame-Options and frame-ancestors do not apply because only the trusted viewer is framed.

What it costs the deployment. The page now runs inside the deployment's own network position, so every control that used to be the member's browser's job has to be somewhere here instead.

Chromium runs without its own sandbox​

This is the honest weak point, so it is stated first. The worker keeps the outer container boundary — RuntimeDefault seccomp, every capability dropped, no privilege escalation, non-root — and Chromium cannot build its own sandbox inside that. Asked to try, it exits with No usable sandbox! before reaching a page. Verified rather than inherited, and re-verified after the container baseline changed.

The trade is deliberate. The same worker runs untrusted member code, so the seccomp profile and dropped capabilities are worth more than Chromium's internal layer; loosening them to gain the inner sandbox would weaken the boundary that contains everything else in the worker.

The consequence is that renderer isolation is gone. A renderer compromise is a compromise of the worker process, with no second layer between them, so the container is the process boundary rather than one of two. A deployment that wants two should attach the gVisor RuntimeClass described above.

The viewer runs on the application origin​

The second honest weak point. The media adapter's client is served through Kaizen's own relay and framed on the application origin, because the relay authenticates each hop with the member's ordinary session — an opaque-origin frame would carry no session and could not connect at all. The frame's sandbox attribute therefore grants allow-same-origin, which is another way of saying the attribute is not the boundary here.

What follows is bounded but real: bytes the worker serves execute with the member's authority on this origin. Kaizen pins the relayed response type with X-Content-Type-Options: nosniff and refuses the adapter's administrator operations, and the client itself is built from checksum-pinned source in the hardened image rather than fetched at runtime — so reaching this requires control of the worker image or of a live worker, not merely a page a member visited. Separating the viewport onto its own origin is the structural fix and is not in place; a deployment that needs that separation today should leave browser sessions off.

Chromium used to announce this to members as a warning bar across the top of every page: "You are using an unsupported command-line flag: --no-sandbox." That banner is now suppressed, because it appeared over a shared page and named a decision only an operator can change. Suppressing it changed nothing about isolation — this page is the statement, and it is the one to act on.

What that container still holds is deliberately little: a non-root uid, a read-only rootfs, a bounded writable directory, no member credentials, no backend tokens, and no other member's workspace. The boundary is thinner than it should be; what is behind it is small.

The debugger has no address at all​

DevTools has no authentication, so the Playwright bridge does not expose a CDP port. Playwright launches the headed Chromium itself and owns the browser over its inherited pipe. The media adapter captures that same X display and needs no CDP.

This matters because authored code runs in the same worker network namespace and under the same uid. A loopback listener would be reachable from the attacker's position and could reveal cookies, disable network interception, or navigate to local files. A pipe inherited by the trusted Node bridge cannot be connected to, scanned for, or guessed by a later code subprocess.

Every destination is admitted by the egress proxy​

The worker checks the requested opening URL against the verified session grant. Chromium then uses only the authenticated sandbox egress proxy, including for redirects, subresources, and scripted navigation. The proxy re-resolves and pins each public destination, and refuses loopback, private, link-local, metadata, and control-plane addresses. Chromium is launched with <-loopback> in its proxy bypass list, which is the opposite of receiving no bypass list: left alone, Chromium sends loopback and link-local straight out, never through --proxy-server. Subtracting them means a page asking for 169.254.169.254 or for the worker's own ports reaches the proxy, and the proxy refuses it.

That covers what travels over the network, which is not everything the member can type. A member drives this Chromium directly, so its address bar reaches schemes the proxy never sees: file:// reads the worker's own filesystem, and the admission check in front of Kaizen-initiated navigation does not run for a URL entered into the browser itself. A managed Chromium policy in the worker image blocks file://, filesystem:, view-source:file://, devtools://, chrome:// and chrome-untrusted://, and disables incognito, browser sign-in, and sync. It also disables file-selection dialogs, which is the same exposure by another route: a page's own file input would otherwise let a member hand any readable path to the site they are visiting, which blocking file:// navigation does not stop. Uploads and downloads are outside this feature by contract, so the dialogs are disabled rather than policed.

Verified in the built worker image, driven through the same Playwright entry point the runtime uses, with the policy present and then moved aside: file:///etc/passwd and chrome://version return ERR_BLOCKED_BY_ADMINISTRATOR with it, and /etc/passwd returns its contents without it. A blocked navigation leaves Chromium on its own error page, and the next navigation a second later succeeds - the same behavior as any failed navigation, not a wedged session. The policy is a property of the distro Chromium build. Playwright's own Chromium is compiled without enterprise-policy support, so a worker that ever launched that binary instead would lose this control without any error - which is why the launch path pins /usr/bin/chromium.

The media adapter receives no general network exception. The platform admits only the exact operator-configured plain-TCP TURN authority and port from browser-capable sandboxes. TURN REST credentials are short-lived and session-derived. This does not authorize public arbitrary URLs, embedded credentials, link-local targets, or metadata endpoints.

The profile does not outlive the lease​

A session is a lease, not a stored row. The browser profile — cookies, tokens, local storage, whatever the member authenticated to — lives in the worker's writable directory and is deleted when the session ends, whether that is the member closing it or the 15-minute idle reaper collecting it. It is never written to the workspace snapshot store, so there is no persisted logged-in browser anywhere in the deployment. What may be kept is something derived, such as a screenshot, which is an ordinary artifact.

A refusal names the reason, never the worker's address​

The worker is a capability the deployment may not have. It answers any browser action it cannot serve — a missing browser binary, a launch it refused, a dead debugger socket, an agent bridge that stopped answering — with its own one-line reason, and that sentence is what reaches the member and the audit record.

The worker's internal pod address is never part of that message. It used to be: a refused navigation surfaced the raw HTTP error, so the member was shown a cluster-internal URL they cannot reach, naming a component they cannot see, while the sentence that said what actually went wrong was discarded. An operator reading the audit trail could not tell a missing binary from a stalled session. A genuine server fault inside the worker stays distinguishable from a refusal, because the two are not the same problem.

Credential keystrokes are transit-only​

Member input travels from the browser media client over an encrypted WebRTC data channel to the worker, relayed by TURN when required. It does not cross Kaizen's API process as keyboard-event JSON. The adapter injects input into the X display and does not persist it, log it, add it to audit payloads, or make it replayable. Audit records session lifecycle and approved agent actions, never member input.

TURN credentials are not website credentials. They are short-lived, session-derived relay passwords issued from an operator secret and expire after at most one hour. The shared secret remains only in the API process.

Private text closes every other viewer's media signaling connection. The adapter then removes those WebRTC peers, stopping their media and input rather than relying on their client to hide received video. The assistant's page read and action paths are refused for the same interval.

caution

Password-field recognition reads the focused element's type in the top document. A secret collected in an ordinary text input, contenteditable, or a nested frame is not recognized automatically. Private text is the general member-controlled boundary; password detection is an additional refusal for conventional login forms.

The agent's authority is smaller than the member's​

The agent reads the page as an accessibility tree and never receives pixels. That text arrives wrapped in an explicit statement that it is untrusted page content to interpret and not instructions to obey, because the tool-result path has no marking of its own.

Acting is separated from reading and treated as sensitive: every action requires the member's confirmation, so a page can influence what the assistant says but never, unattended, what it does. Two narrower rules sit under that. The agent may act only on the origin the session was opened on — a member may follow a link, but the agent's authority does not follow them — and it never reads from or types into a password field.

note

Confirmation is the load-bearing control here, not the origin check or the tree's preface. Prompt injection from page content is not solved; it is bounded, by requiring a human for every effect.

The package runner​

Some capabilities come from packages that may include third-party code. That code runs in a separate, secretless environment — deliberately without the tokens or keys the trusted backend holds.

The property this buys: an untrusted package that executes successfully still cannot reach anything sensitive, because there is nothing sensitive in its process to reach. Containment does not depend on the package behaving.

The package runner executes admitted package tools; it is not the proposal-test environment. Keeping native Pi validation in the sandbox preserves the same deployment-selected isolation boundary used for other authored code without placing validation inside the credential-bearing backend.

Admission to a shared room is narrower than package or agent installation. A reviewed package manifest must classify each individual tool under the closed collaboration policy; missing and unknown classes remain absent. Actor-bound reads still execute as the initiating participant and expose only their normal sanitized result. The bundled Software Engineer admits its Hypa file-inspection tools this way, while its arbitrary shell tool remains unavailable in a shared room. Neither Pi nor package code can change that manifest class at runtime.

At invocation, the runner accepts package resources and member workspace files only through rooted relative paths. It rejects absolute paths, parent traversal, control characters, and malformed segments before touching the filesystem. A package file read is then checked twice: the lexical path must remain below its content-addressed package root, and the opened descriptor must resolve to a regular file under that same root. O_NOFOLLOW and descriptor revalidation make a symlink escape or a path swap fail closed instead of reading outside the package cache.

Egress control​

Outbound network access from a sandbox goes through the egress proxy, with per-run signed tokens and a selected destination policy. It is on by default and scoped to public addresses; a run still needs guardrail approval. Grants are signed with a key Kaizen derives from the deployment's field key, and the egress proxy verifies them with the public half — it holds no signing material, so a compromise of the proxy cannot produce a grant. There is no egress secret to configure.

SettingDefault
SANDBOX_EGRESS_ENABLEDon
SANDBOX_EGRESS_MODEpublic_internet
SANDBOX_EGRESS_HOST_ALLOWLISTempty
SANDBOX_EGRESS_PRIVATE_CIDRSRFC1918 and IPv6 ULA
SANDBOX_EGRESS_ALLOWED_PORTS80, 443
SANDBOX_EGRESS_TOKEN_TTL_SECONDS600 (10m; previously configured values remain valid up to 4h)

Package installation is handled separately, through a broker fronting Python and JavaScript registry paths with short-lived tokens and a per-file size ceiling. Dependencies install without granting general internet access.

The four modes are isolated, public Internet, configured private networks, and an exact host allowlist. Protected address classes remain denied in all modes. Every connection re-resolves DNS, rejects mixed allowed and disallowed answers, and connects to a policy-checked pinned address. Existing deployments remain off until an operator explicitly enables egress and configures the signing key.

What is in force, and what a denial records​

The mode is an administrator's decision and nothing else can move it. A member request, a tool argument, a model's output, and code running inside a sandbox are all outside the resolution that picks it, and a configured value this release cannot read resolves to isolation rather than to the open default.

Because a saved value is not a promise, Admin shows the scope actually in force. Setup carries a Sandbox network access entry naming the mode member-authored code really reaches, the ports the proxy enforces, and the destination classes denied in every mode. When the deployment cannot hold the configured scope — an unusable port list, no proxy address, no field key to sign a grant with — the entry reports isolation, says which prerequisite is missing, and asks for attention.

A refused connection tells the sandbox only that it was refused. The operator record of a refusal carries the reason and the destination's host and port; it never carries the resolved allowlist, the request path or query string, or anything a caller embedded in a URL.

Projects add access paths, not private authority​

A Project groups chats and can add a base read/write path to a linked chat. It does not add an implicit system prompt, instruction, knowledge collection, connector, tool, role, or approval.

Kaizen resolves the initiating participant's personal library, prompt chains, packages, credentials, connector sessions, confirmation state, MFA and IT approval, and writable sandbox exactly as it would outside the Project.

Project members do not share private workspace files, package caches, environment values, credentials, connector grants, approval tokens, or personal-library contents. What crosses into the room is what a member explicitly filed there: a linked chat resource, and a linked document, prompt or artifact whose pointer is marked shared. Each arrives as the same bounded read-only projection.

A Project grant is a read grant on exactly the linked item, resolved through one predicate for every kind rather than a rule per surface. It confers no write, rename, publish, delete, fork or re-share authority, it does not make the item visible in any other Project, and it ends the moment the linking member withdraws it or the reader leaves the Project. A pointer left private grants nothing, including to members of the Project it sits in. Existing links keep the access they were stored with: a behaviour change never silently widens a grant a member already made.

The same rule applies to demo source declarations. A shared Project, Demo Blueprint, bucket name, object key, prefix, endpoint, manifest, or signed URL does not grant AWS S3 or Cloudflare R2 access. A governed source tool must resolve a platform-managed connection under the current member's authority and return bounded coverage. Object-store credentials never enter the Blueprint, agent package, package runner, sandbox, or Project content.

Compression does not expand authority​

Tool-result compression may shorten eligible public run_code stdout and stderr after sandbox execution and after all authorization and policy filtering complete.

It changes only the model-bound projection. It does not open host-shell access, add network egress, read another workspace, or grant a tool. The worker keeps raw output canonical, and a failure falls back to the original already-authorized result.

Audit​

Sandbox dispatch and isolation denials correlate the initiating actor, conversation, audience version, capability, and authorized resource-manifest digest.

Audit payloads never contain workspace files, package contents, environment values, credentials, stdout, private snapshots, or arbitrary host paths. Workspace keys, resource bytes and filenames, raw run capabilities, object-store keys, and private output are prohibited from denial events.