wrkspc_ identity, name, tags, residency fields, and an irreversible archive lifecycle. Development, test, staging, and production are separate Workspaces—ordinary names or tags, not a Checkfu-specific stage field. Runtime and configuration resources are isolated by Workspace. Two operator-owned registries are platform-global: Harnesses and platform supply.
A SourceRepository is an Organization-owned logical Git repository identity: provider, owner, repository, default ref, and display metadata. It deliberately contains no credentials and names no IntegrationConnection. A later synchronization must select its IntegrationConnection explicitly, so repository identity never becomes ambient provider authority. Administrators create and update SourceRepositories; every Organization member may read the directory, and no Workspace can observe another Organization’s entries.
Ordinary API keys are hard-bound to one Workspace, matching CMA; request headers cannot select another. Workspace quotas are live execution-admission policy: concurrent Runs, monthly model tokens, and monthly Sandbox seconds are enforced from authoritative reservations and UsageLedger receipts. PermissionAssignments, ActionPolicies, ActionApprovals, and Workspace retention policy are implemented, including the governance decision endpoint.
Principals
A Principal is the end user or service identity on whose behalf an Agent acts. The customer’s backend authenticates its own users and asserts the Principal when it calls Checkfu. Checkfu API keys stay server-side; they are never exposed to those end users. Every Run records its Principal so capability access, audit records, and usage can be attributed consistently.Groups
A PrincipalGroup is a Workspace-scoped set of Principals. PrincipalGroups are first-class PermissionAssignment subjects: grant a PrincipalGroup access once, then manage its membership without rewriting every PermissionAssignment. Group names and membership change together through optimisticexpected_resource_version updates, so two directory syncs cannot silently overwrite each other. Deleting a Principal removes it from every PrincipalGroup before the Principal disappears.
This complete directory flow creates two Principals, lists the directory, creates a Group, and updates its name and membership. The cURL tab assumes jq is installed; both tabs assume the authentication variables from Get access are exported.
"subject": { "kind": "group", "group": "grp_…" }. List responses use the standard data / next_page envelope; send a non-null next_page back as page until it becomes null.
PermissionAssignments
A PermissionAssignment is the universal permission primitive:A subject may use a resource with one permission.The same primitive feeds one authorization layer and one audit surface.
PermissionAssignment
subject and resource discriminate on kind, and each subject kind carries its own ID field:
Each resource kind admits only the permissions that make sense for it, so an impossible PermissionAssignment is rejected rather than silently ignored:
Note the hyphens: resource kinds are hyphenated (
memory-store) while subject kinds are underscored (agent_definition).
On agent-definition, who enforces a permission differs by permission. invoke is enforced fail-closed at Session admission; every drive additionally enforces invoke or steer, chosen by the event — a user.message with no live Run takes invoke, and every other drive event (or a user.message during a live Run) takes steer. act_as is enforced wherever a Session runs under an acting Principal that is not the requester — installed and automation Sessions. That Principal is derived from the installation’s placement or the stored Automation, never asserted on the request: Session create rejects a caller-supplied acted_as outright rather than checking it. use_agent is live agent→agent thread authority: the coordinator’s published roster defines addressability, and this PermissionAssignment can only narrow it. The PermissionAssignment is enforced for each child-thread spawn and primary-to-child follow-up; a child-to-primary report continues the already admitted relationship and needs no reverse PermissionAssignment. A row whose subject is a principal is inert, so it is not how you give a person access.
Two are conditional: observe and steer are enforced by the trusted backend that exposes a Session view, and approve on the action-approval-response route only when your backend asserts acted_as. One is not PermissionAssignment-gated at all: use_tool on an agent’s own Custom tools is unconditionally ActionPolicy-only and defaults to allow — no PermissionAssignment is consulted, and only a deny/require_approval ActionPolicy stops the call. (use_tool is PermissionAssignment-enforced on a tool-source.)
edit and view are decidable (POST /v1/action-policies/evaluate answers them for any subject) but agent registry reads and mutations authenticate your API key and carry no end-user identity, so your backend is their enforcement point: mint the rows, then ask the check before serving your own edit or read route. See the embedded-builder guide for the working pattern. view on an agent definition is platform-enforced in exactly one place, the principal-scoped MCP lane. Other routes that do carry an asserted Principal are enforced normally — GET /v1/transcripts requires principal_id and checks that Principal’s view on the SurfaceScope you query.
approve is load-bearing: a Principal holding it on the resource an ActionApproval’s subject maps to — the surface or connection of a ToolInvocation ActionApproval, the declaring agent-definition or its surface for a custom-tool ActionApproval — can answer that ActionApproval without holding a Workspace approval-responder role, when a trusted backend asserts them via acted_as. See ActionApprovals.
PermissionAssignments are immutable. They are created and deleted, never patched, so an audited subject/resource/permission fact cannot be edited out from under its audit record. In v1 a PermissionAssignment enumerates a single resource. D6 retains that evaluator until measured latency, relation-composition, or row-volume evidence justifies a relationship-engine migration; scope expressions are not a promised next step.
Who may write a PermissionAssignment is a separate question from what a PermissionAssignment permits, and it is answered by the API key’s role, not by another PermissionAssignment: creating or deleting a PermissionAssignment or an ActionPolicy needs an admin or root key, while every Workspace role may read them. PermissionAssignments govern what agents and Principals may touch at runtime; key roles govern the control plane that authors them. Nothing recurses.
Filter the PermissionAssignment list
GET /v1/permission-assignments accepts two optional filter pairs: resource_kind + resource_id, and subject_kind + subject_id. Both use the same kind vocabularies as the tables above, and they combine. Each pair is all-or-nothing: a kind without its ID (or the reverse) returns 400 validation.malformed rather than an unfiltered list, so a typo cannot silently widen the answer. Filtering by both pairs asks the audit-shaped question directly: which PermissionAssignments let this subject touch this resource.
With subject_kind=principal, adding effective=true also returns rows whose subject is a Group the principal currently belongs to — each row verbatim, so a group-derived row still names its Group and your UI can render why access exists. Membership resolves through the same membership index the check uses, though the check tests strictly more, so this read is the weaker of the two by design. It reports rows, never permission to act: it composes no ActionPolicy, and a time-bounded row is returned with its bounds intact whether or not its window is open. POST /v1/action-policies/evaluate remains the only authorization answer. Follow next_page rather than treating a short page as the end — revoked rows are hidden after the page is taken, so a page can be short or even empty while more rows follow. effective=true is not available to runner keys.
ActionPolicy and the decision
A PermissionAssignment answers whether an action is permitted. An ActionPolicy determines its disposition. Every check returns one of three dispositions (allow, require_approval, or deny) together with the reason it reached that answer and the PermissionAssignments and ActionPolicy rules that matched.
The decision algorithm has three steps:
- No matching PermissionAssignment →
deny. ActionPolicies are never consulted, because there is nothing to modify. - Otherwise, the strongest matching active ActionPolicy rule wins, ordered
allow<require_approval<deny. - No ActionPolicy rule matched →
allow, on the strength of the PermissionAssignment alone.
POST /v1/action-policies/evaluate returns: whether the access
exists, and what an ActionPolicy did to it. A tool call at runtime asks a second, narrower
question described below.
An ActionPolicy rule first matches on subject kind and ID, resource kind and ID,
permission, and optionally a surface scope. Every one of those structural
fields is a glob pattern where * and ? are the only operators, and a bare
* is accepted for a whole subject or resource kind.
A rule may additionally carry up to 16 conjunctive argument conditions. Each
condition walks a bounded path through the operation’s canonical arguments and
uses one of the closed operators exact, one_of, not_one_of, range,
prefix, contains, or subset. Operands are either JSON scalar literals (or
bounded scalar sets where the operator requires one) or references to a
server-derived trusted fact: workspace_id, acted_as, surface_scope_id, or
connection_provider. The browser can author that reference, but it never
asserts the fact’s value. If a path, shape, or referenced fact is unavailable,
evaluation fails closed: an allow condition does not match, while a
require_approval or deny restriction still applies.
POST /v1/action-policies/evaluate runs exactly this decision without performing the action, which makes it the right way to pre-flight a UI: grey out what would be denied instead of letting a user discover it mid-Run.
At runtime, a PermissionAssignment alone does not execute a mutation
A tool call runs the decision above on every mandatory check — the acting Principal and, when installed, the AgentInstallation, on both the Connection and the tool source — and then applies one more rule before anything executes:- Any check that denies → the call is denied. Any check that requires approval → the call requires approval.
- Otherwise, a call proven to be a safe read runs.
- Otherwise, the call runs only if every mandatory check reached
allowbecause an ActionPolicy rule matched (reason: "action_policy_allows"). - Anything else requires approval.
allow from
POST /v1/action-policies/evaluate — correctly, because the access exists — while the same tuple on a
mutating tool call becomes an ActionApproval. PermissionAssignment answers whether the
agent may touch the resource at all; an explicit allow ActionPolicy rule is what lets it touch
the resource without a human. That default is deliberate: an unreviewed mutation is the
failure worth being conservative about.
A “safe read” is narrow. It requires safety hints marking the tool read_only, not
destructive, and not requires_approval, and a read-only transport method. MCP-bound
tools always POST, and their hints are authored by the upstream server and refreshed on
every sync, so an MCP-bound tool never reaches step 2 and never auto-allows on hints alone.
An explicit allow ActionPolicy is the only way to auto-allow one.
A Custom tool is the one family judged on ActionPolicy
alone: it has no PermissionAssignment to check, so with no ActionPolicy in the way the park proceeds.
An ActionApproval is what require_approval produces. It freezes the full tool-call context at request time so the eventual decision cannot be replayed against changed arguments or a different Connection.
Author and preflight in the console
The Governance page exposes the same primitives without adding a second policy language:- New PermissionAssignment creates one exact, immutable subject × resource tuple and only offers permissions valid for that resource kind. Revoke and recreate to change it.
- New ActionPolicy authors one or more glob rules and their optional bounded argument conditions. Rule IDs must be unique inside the ActionPolicy; a matching
denyremains stronger thanrequire_approval, which remains stronger thanallow. Trusted-fact operands are references resolved by the server at the operation boundary. ActionPolicies are immutable, so replace rather than edit one. - Structural preview sends one exact subject, resource, permission, and optional SurfaceScope to
POST /v1/action-policies/evaluate. It returns the disposition, reason, matching PermissionAssignment IDs, and matching ActionPolicy rules without performing the action.
action_policy_id with
GET /v1/action-policies/{id} to read the exact immutable ActionPolicy without scanning the
registry.
Retention model
Every Workspace has one retention policy.standard may set a default TTL and per-content-class overrides; omitted TTLs retain content until explicit erasure. zdr is non-persistence, not encryption: customer execution content may cross the live request, Runner, model, and already-attached Session stream, but it never enters the Session log, control database, archive, queue, snapshot, or replay receipt. Transient Sandbox storage uses an ephemeral_zdr release and is destroyed rather than snapshotted.
Model wire evidence is the one content class whose ceiling a Workspace cannot raise. Every model-gateway-witnessed model call in a standard-retention Session records a wire.call event, and the exact bytes behind its digests are retained for at most 30 days, even where the Workspace policy sets no TTL at all. A shorter Workspace or wire_frame TTL still wins; nothing lengthens the ceiling. Resolving a digest back to bytes is an operator route, not a tenant one: it requires an admin or root key and a digest the named event actually references, and after the ceiling passes the references stay structurally in the event while the content resolves 404. A zdr Workspace constructs no wire evidence in the first place. The difference is the absence of wire.call, not a filtered copy of it.
ZDR deliberately preserves content-free authority: event positions and types, lifecycle state, immutable release and execution pins, authorization and idempotency HMACs, Runner lease identity, canonical error types, AuditRecords, Budget reservations, and UsageLedger facts. Event pages and exports report omitted positions as reason: "zdr" instead of pretending the sequence was continuous.
The runnable ZDR boundary is intentionally narrow. A managed Runner must already be blocked in the Workspace’s live rendezvous before a pending or idle Session accepts a user.message; the request needs an Idempotency-Key, no attachments and no typed content blocks, a ZDR-compatible Sandbox release, and only credential-free mounts. Output is offered best-effort only to subscribers already attached when it is produced, at most once; even an attached subscriber may miss bytes, and reconnect cannot recover them. Running-message steering, custom-tool or approval continuation, Outcomes and Reports, Automation and multiagent-thread launch, ConnectedRuntime execution, Checkpoints, retained output or artifact capture, Git checkout plans, and Memory writeback fail closed rather than retaining a reduced copy.
Next steps
API reference
Explore the typed contract behind Workspaces, Sessions, and Runs.
Platform architecture
See how tenancy, PermissionAssignments, and policy compose into the control plane.