Permissions & ACL enforcement
Kaizen's central access-control decision is where the check happens. Everything else follows from it.
Enforcement is inside the query
Current workroom, owner, direct, and Project access, plus Project lifecycle checks, live inside the PostgreSQL repository query.
Workrooms narrow projects, not a member's own work
A member's chats and Library follow the member. Standing in a different
workroom does not hide them, because the workroom was never what authorized
them: every one of those reads matches the caller's principal_sub and
verified_email, and the workroom clause only narrowed a set the principal
predicates had already authorized. Hiding a member's own conversations because
they walked into another room is indistinguishable, to them, from losing the
account.
What replaced it is the set of workrooms the platform says the caller is
currently in, read from GET /api/workrooms/ under the caller's own forwarded
auth. That distinction carries the revocation the clause used to do silently:
Kaizen's membership rows are Kaizen's own and nothing reconciles them, so a
member removed from a workroom upstream would otherwise keep every row ever
written for them. A room the platform stops listing stops being reachable. If
the platform cannot be reached, the scope narrows to the single room the
request envelope attested rather than widening, and that failure is not cached.
The current room remains a preference: it sorts first in the chat list, ahead of rooms the member can also reach.
Projects are the exception and stay workroom-scoped, because that coupling is
enforced in the schema rather than in a predicate —
tomo_project_membership_guard refuses a project membership whose workroom
differs from its project's. Conversation search is still workroom-scoped too;
widening retrieval ranking is a separate change from widening a list.
Every lexical, semantic, exact, and fuzzy retrieval branch must consume that authorization relation before producing branch output, fusion input, snippets, facets, counts, cache writes, API projections, audit detail, or model context.
Raw candidate IDs and text never cross into application code for post-filtering.
Why not filter afterwards
Post-filtering is the intuitive design and it leaks. Even when the forbidden rows are withheld, their existence escapes through:
- result counts — "142 results" when you can see 3,
- facet totals — a Project name appearing in a filter you cannot open,
- pagination — page 4 existing when your rows end on page 1,
- latency — a query that is slow only when a large hidden set matched,
- error messages — "forbidden" distinguishing a real object from a missing one.
In-query enforcement removes the class rather than patching the instances.
The documented HNSW nuance
Kaizen does not overclaim. PostgreSQL's HNSW access method can inspect index rows before relational filters are fully applied, and those rows may influence internal index traversal. So Kaizen does not describe the method as authorization-prefiltered.
What it does instead:
- each indexed branch joins the full current authorization relation inside PostgreSQL,
- transaction-local iterative-scan settings are bounded,
- only authorized survivors are exposed, and exact reranking consumes only those survivors.
With indexed retrieval disabled, Kaizen materializes the complete eligible relation first and performs lexical scoring and exact vector distance, without traversing the protected indexes at all. That strict exact mode is the rollback posture as well as the correctness oracle.
Projections are not capabilities
A search card, a facet, a Project index row, a turns cache entry — all are
disposable projections.
A result is admitted only when the viewer has a current room-access path. Opening an anchor re-authorizes it. A stale card or a copied URL cannot preserve a revoked grant; Kaizen returns a generic unavailable state without revealing what changed.
Direct access to a chat does not reveal a linked Project's metadata unless the viewer is also an active Project member.
Access paths compose additively
A member may reach a conversation through more than one path:
| Path | Granted by |
|---|---|
| Owner | Creating it |
| Direct | A named invitation, accepted |
| Project | Active membership of a Project the chat is linked to |
Effective access is the union. Removing one path leaves the others intact — removing a direct grant does not revoke Project-derived access, and unlinking from a Project does not revoke a direct share.
Access stops only when the removed path was the member's final path. At that point transcript, live, write, resource, search, and export access all end together.
The chat header and library cards show the current viewer's own safe reason category — Owner, Direct, Project, or a combination — so the model is legible rather than surprising.
Sharing widens only through an explicit scope
Two rules cover almost every sharing question:
- A collection's contents are re-checked against the recipient's own permissions. Two people opening the same shared collection can legitimately see different items.
- A Project document link can be a scoped grant. The contributor may mark
the document Shared or Private. Shared admits active members of that exact
Project for read-only use; Private admits only viewers who already have
another source permission. The grant is derived in the repository query from
current membership, the exact pointer, its source owner, and its access
state. It never changes the source's global visibility or grants writes.
Read-only use covers the document's passages as well as its Library row:
upload search, attaching it to the viewer's own chat, filing it into and
listing it from a collection,
query_document, direct passage reads and citation resolution all apply the same rule in the same query that reads the content, so revoking the pointer, making it Private, or leaving the Project ends new passage reads on the next request, even for a stale document or passage identifier. Another Project that still shares the document keeps its own grant. Passages already retained in a conversation are not erased. Other Library kinds retain their existing source-specific authorization.
Shared chats are another deliberate scoped case: resources and results actually retained as part of the conversation become room content. That is the point of a shared room. It does not extend to the rest of anyone's library.
This includes native files produced by a sandbox turn. The stored file remains owned by the initiating member, while an immutable reference exposes only that exact retained version to current room members. Reads require current effective room access and a matching content digest; a guessed owner URL, stale room URL, or copied identifier grants nothing.
The in-app native-file preview follows that same read boundary. Private preview requests repeat the producing member's owner check. Shared preview requests resolve the exact active room reference before returning retained bytes or a bounded projection. PDF bytes go directly to the browser's native PDF viewer. PowerPoint bytes are parsed and rendered locally in the member's browser with bounded archive limits; other Office projections are parsed by Kaizen. No native document is sent to an external document-viewing service. Sharing a chat first pins files already retained by that exact chat, so a later participant sees the same room content without receiving a grant to the creator's broader artifact library. That participant can use the retained version in a later code-backed transformation: Kaizen supplies only safe metadata to model context and independently projects the exact digest-matched bytes read-only to the sandbox. The resulting file receives its own retained room reference; neither step grants access to another version or file.
Tools inherit, never escalate
Every tool call executes with the initiating member's permissions, re-resolved at call time rather than cached at turn start. An agent's knowledge scope narrows what it retrieves; it never widens what the runner may reach.
A delegated sub-agent inherits the caller's permissions and cannot exceed the capability ceiling of the agent that invoked it.
Which agents may be handed work is a member choice on top of an agent's reviewed defaults, and it is authorization-checked twice: the ids a member can add are only the agents their own ACL already admits, and the target is re-resolved under that same scope immediately before the child starts. A target the member cannot select is refused as unavailable even if it reached the turn's delegate set. Adding a sub-agent therefore redistributes work the member could already do; it grants nothing.
An id a model puts in a tool argument is a selector, never authority. Filing a Library item is the clearest case: the destination — a collection the caller must own, or a project they must be able to write to — is re-read and re-checked inside the write, and the item is re-resolved through the same visibility predicates retrieval uses, both after the model named them. A destination owned by another member, an item the caller's ACL does not admit, and an id that matches nothing produce one identical refusal and no partial write, so the surface cannot be used to discover what exists. Each destination routes to the member-side write that already owns it — the collections add-document path, the Project library pointer service — which is why the ownership check, the ACL re-resolution and the audit row cannot differ between a member's click and an agent's tool call. Library discovery is held to the same rule from the other direction: it reads through the member's own screens' repositories, returns identity without content, and treats a surface this deployment does not expose as one that does not exist.
A Prompt can carry an explicit Tool or Connector attachment for one step even
when that item is not part of the selected Agent's usual defaults. This is a
bounded configuration grant, not model-authored authority: only parsed
{{tool:…}} and {{connector:…}} tokens participate. Validation resolves the
token again under the running member, current deployment policy, live/active
catalog state, and the effective step Agent's capability ceiling before any
step executes. The runtime applies the result only to an execution-local Agent
copy. Ordinary prose, prior step output, Tool output, and Connector content
cannot create or alter an attachment, and the stored Agent remains unchanged.
The attached capability then crosses the same normal callback boundary. Current member permission, shared-room admission class, approval mode, write guard, resource authorization, and audit still apply at the call itself. Removing or disabling the capability makes a later validation or run fail closed rather than preserving authority from an older editor result.
Collaborative turns narrow this again through a closed tool-class map. Exact room resources and audience-intersection reads may be room-scoped. A small set of bounded, secret-free catalog and Studio metadata reads, plus guarded writes, remain actor-bound and recheck the initiating participant at callback time. Private document or history reads, unknown tools, and unreviewed dynamic operations are denied instead of being treated as safe merely because every participant can open the room. Admin-admitted package tools may carry a narrow actor-bound or actor-private class on their trusted wrapper. Undeclared classes still deny, and a package cannot self-assert audience or conversation-resource scope. Package code and model input never author that decision.
The durable pipeline also serves private chats. It admits an actor-private read or write only when the server-authored active participant roster contains the initiating member and nobody else, then rechecks that condition at callback time. Inviting a collaborator removes those private tools from later turns; workroom membership alone never widens the room audience.
Generated Kamiwaza tools
The generic Kamiwaza broker shares only bounded OpenAPI schema metadata across
requests. It never shares a member's response, role decision, token, or
workroom. Each invocation reconstructs the request from the current validated
forwarded envelope; model arguments cannot choose identity headers, a role, an
API base, or x-workroom-id.
Public/auth-exempt Core operations are not tools. Explicitly incompatible role surfaces are omitted before model discovery, but that is only exposure minimization: Kamiwaza Core still authorizes the exact resource and action and its denial is preserved. Workroom operations use the caller's current workroom scope unless Core itself exposes and authorizes a distinct administrative operation. Context Service uses the same rule and is never a background data feed into Kaizen.
Admin → Tools places every generated capability under the Kamiwaza root, a source family, a surface, and an exact endpoint. At the API layer, root and source selectors are tighten-only ceilings and do not grant their descendants. A disabled ancestor overrides stale exact true entries, while restoring it preserves the narrower child choices. A selected surface admits current and future endpoints under that surface, with exact false exceptions. The closed Kamiwaza SDK adapters use the same hard-ceiling rule for their root, SDK source, and SDK surface and remain additionally limited to the selected agent's source-reviewed grants.
The Admin UI's parent checkbox is an explicit enable-all or disable-all action. It sends one optimistic, server-resolved cascade: disabling adds only the parent ceiling, while enabling removes existing endpoint exceptions in that authorized subtree and selects its surfaces. The browser never supplies the subtree or a large list of endpoint names for the server to trust.
The broker recursively redacts credential-shaped JSON fields and secret query values in returned URLs before a result reaches the model, and it replaces Core 5xx response details with a generic error. Generated operations remain absent from collaborative chats until an operation class has a reviewed audience-safe projection; shared workroom membership is not treated as that proof.
The Core compiler also omits a request contract that can carry a plaintext
secret, private key, or ambiguous authentication or credential configuration.
Generic non-secret config objects remain available for schema-driven
connector settings, with a second recursive key and URL check immediately
before dispatch. Opaque credential references are allowed because the trusted
platform, not the model, resolves them.
External OpenAPI is a separate default-off trust boundary. Source registration stores metadata only and does not fetch a schema until an admitted tool source is discovered. Both discovery and execution must pass the signed sandbox-egress proxy; HTTPS URL validation, no redirects, DNS/address pinning, protected-range denial, port policy, payload bounds, and timeouts remain in force. The external request never receives the Kamiwaza forwarded identity envelope. Schema and API origins must match unless the source records explicit cross-origin consent and the administrator enables the separate cross-origin ceiling. Query-string credential operations are not discoverable unless their own default-off admin ceiling is enabled.
Authentication values travel from the native browser form to the trusted broker outside the model transcript. Plan signatures bind the accepted schema, ordinary arguments, actor, workroom, source, operation, effect, approval, and expiry. The form shows that signed API origin. Consumption also rechecks the live admin selection, both risk ceilings, and current visible, enabled source, so a parent, source, exact endpoint, cross-origin disable, or query-auth disable revokes an outstanding form. Use-once values exist only in browser and broker memory; NPE certificate and key files use temporary memory-backed storage solely to initialize TLS. Logs, audits, replay receipts, Redis, and PostgreSQL retain no value. Tool-result projection scrubs exact submitted values and common encodings from structured data, text, errors, URLs, package instructions, and binary metadata. Explicit Remember writes an encrypted DataHub/Catalog secret and returns an opaque URN; the write-only adapter has no secret read or resolve method. Ambiguous external writes and secret-store writes are recorded but never retried.
Demo plans are not authority
A Demo Blueprint can name a Project, environment binding, model, dataset, bucket, prefix, object, connector, or desired operation. None of those values grants access. Demo Builder can act only through a tool admitted by deployment policy and its agent grant, constructed under the current staff member's forwarded workroom envelope, and authorized for that exact call.
The bundle's inspect_kamiwaza and manage_kamiwaza_demo grants are capability
ceilings, not credentials. The kamiwaza-sdk==1.2.0 pin establishes API
compatibility but does not establish identity, permission, approval, or network
reachability. Kaizen constructs the SDK client only from the validated platform
principal; tool arguments cannot select a user, role, workroom, API URL, bearer,
cookie, PAT, or object-store secret. The package runner and pi sidecar receive
only schemas and bounded results. The forwarded member envelope is sent only to
the configured Kamiwaza API origin; redirects to another scheme, host, or port
do not carry its authorization, identity, or workroom headers. Delegated
resource requests stop if a redirect crosses that origin.
Admin → Tools can disable the complete SDK source or either exact adapter before runtime wiring. A member's conversation selection can narrow an admitted SDK grant but cannot restore an administratively disabled source or grant the SDK to an agent that does not already own it.
The Demo Reconciler uses that already-authenticated client directly. Its reuse
of SDK seeding primitives does not invoke kamiwaza-cli, read operator context
files, mint a CLI token, or grant shell authority. Workroom scoping and internal
Kaizen/model endpoint resolution happen inside the trusted backend; neither is
model-selectable connection material.
Apply also carries the exact Blueprint, environment, and preview digests from the preceding preview. Those deterministic digests detect target or desired-state drift; they are not approval tokens. The apply tool call performs no SDK mutation. It stages the exact request behind Kaizen's write guard, and only the current member's one-shot, unexpired approval receipt reaches the member action endpoint that rechecks actor, staged request, live environment, and digests before committing. The direct tool has no delete path. When the envelope or operation is absent or denied, the correct output is an unresolved operator plan. There is no chat-credential, package-runner, sandbox, or direct-HTTP fallback.
Cleanup is fenced again by current receipts. Generated ownership text cannot authorize deletion: by default only resources whose live receipt proves they were created for the Blueprint are eligible, and ambiguous or pre-existing resources remain untouched.
Corpus Tasks reauthorize every source
A folder or Collection selector is an enumeration instruction, not an access grant. Preview resolves it under the authenticated owner, and each run stores a versioned manifest of only the admitted sources. Every partition then checks current lifecycle and read authority again before loading exact source chunks.
Coverage and provenance are owner-scoped, paginated, and reauthorized when read. If access is later removed, historical lineage remains, but the title, path, source identifier, digest, revision, and locator are redacted. Missing, foreign, deleted, and revoked sources use the same unavailable response.
Corpus outputs remain private owner Artifacts. Shared conversation and Project destinations are refused until Kaizen can prove that every source and output is authorized for the complete audience.
Agent pipeline selection requires two independent checks
Explicit pipeline selection requires both a source-reviewed opaque client-preference scope and live Kaizen Admin authority. Neither check grants the other. The server derives both values from current trusted identity and authority; it never accepts them from a request body.
The self-profile lists only enabled, compatible, exact pipeline ID and version pairs from the static manifest. A protected create request must match one of those pairs exactly. A protected read also requires current conversation ownership. Shared-chat collaborators receive the same HTTP 404 as an absent route even when they can read the conversation. Callers who fail an eligibility or ownership check receive that response without pipeline names, assignment state, or conversation metadata.
Assignment and fallback audit records are access-restricted operational data. Member responses, conversation events, and ordinary audit projections exclude both actions and their payloads.
Denials reveal nothing
Authorization and policy failures use bounded reason codes — not_found,
forbidden, agent_not_shareable, conversation_not_shareable,
participant_limit, idempotency_conflict.
An audit record may identify the route family and target id when already authorized, but it does not reveal whether an unrelated chat, invitation, member, source, or resource exists. Unrelated, expired, invited-only, departed, and revoked members all receive the same not-found response as something that never existed.