Skip to main content
This guide is migration-fenced. Its embedded app architecture remains useful, but its Agent examples use the retired Draft/Release API and are not executable with checkfu.beta.agents. Use Agents until this guide is rebuilt on the CMA Agent and Session surfaces.
Your product can own the agent-builder experience while Checkfu owns definitions, immutable versions, PermissionAssignments, and Sessions. The runnable examples/agent-workbench example is a small customer product, Acme Workbench, rather than a second Checkfu console.

What the example proves

Two fixture teammates can sign in. Vivian creates an agent, saves a draft, publishes version 1, and starts a preview. She can then edit the draft without changing that Session, publish version 2, and start a new preview that resolves version 2. When Omar tries to start Vivian’s agent, the attempt reaches Checkfu admission and comes back policy.denied. The host does not substitute an application-only access check for that proof.
Direct Sessions execute as the requesting person Principal. The API rejects a caller-supplied acted_as, so this integration does not manufacture a distinct service actor. Installed agents have a separate collaboration identity model; use that model only when your product is actually embedding an agent into an external collaboration surface.

Run Acme Workbench

Prepare a Workspace with a Harness, model routing profile, and sandbox profile, then run:
The API key is bound to the Workspace; the SDK accepts no Workspace selector. The harness, model routing profile, and sandbox profile default to claude-code, primary, and standard; override them with CHECKFU_HARNESS, CHECKFU_MODEL_ROUTING_PROFILE, and CHECKFU_SANDBOX_PROFILE. The API key exists only in the Hono process. The browser calls the example’s /api routes and never sees the key or a private Checkfu endpoint.

Follow the server composition

The identity mapping is anchored by your stable user ID:
The fixture records the returned ID in its in-memory host store; your production app should persist it beside the user row. If the external ID already exists, the example lists and matches that exact anchor instead of guessing from a display name. Its agent document IDs are user-scoped random UUIDs, so a fixture restart cannot replay another user’s create idempotency key. Creating an agent writes a draft and two exact-resource PermissionAssignment rows, one for each permission:
Creation confers no authority: an agent with no PermissionAssignment rows is reachable by nobody, including its creator. That create-then-assign pair is the canonical owner bootstrap, and the SDK ships it as one call:

Who can talk to an agent

Reachability is a PermissionAssignment topology, never a field on the agent (DOMAIN §3.7):
  • Personal agent — only the owner holds invoke. That is exactly what the bootstrap above minted, and it is the whole privacy mechanism: when Omar tries Vivian’s agent, Checkfu admission denies because no PermissionAssignment names him.
  • Workspace agent — mint the same invoke PermissionAssignment with a Group subject. Every current member can talk to the agent through that one row; adding a member to the Group reaches the agent with no per-agent write, and removing them revokes. Let the creator pick people in your UI, put them in a Group, and grant the Group:
An “everyone in the Workspace” agent is the same shape pointed at a Group you maintain: there is no built-in all-principals subject, so add each new user to your everyone-Group on your signup path, or a new user cannot reach any Workspace agent until you do. To render “agents this user can talk to” — or to check access counting Groups — use the effective read: GET /v1/permission-assignments?subject_kind=principal&subject_id=…&effective=true returns the principal’s direct rows plus rows held through their Groups, each row verbatim (a group-derived row still names its Group, so your UI can show why access exists). It reports rows, not permission to act: a time-bounded row comes back with its bounds intact even outside its window, and POST /v1/action-policies/evaluate remains the one authorization answer.
A Workspace agent’s memory is shared by construction: the definition’s own memory_mounts (or the place’s composition) names one store, and every member who holds read on it sees the same content — so one user’s context can surface in another’s conversation. A store the agent definition owns is clamped to read-only at mount, and its canonical only advances through governed proposals, so this is a disclosure concern rather than a write-collision one. When shared context is wrong for your product, mount each user’s own MemoryStore per Session at creation: mounts are clamped against the acting Principal’s PermissionAssignments, so the shared agent reads Vivian’s store only in Vivian’s Sessions. Caller-named mounts are a direct-Session capability — installed Sessions refuse them.

What Checkfu enforces, and what stays yours

Checkfu enforces the conversation side fail-closed: invoke at Session admission, and invoke or steer on every drive — a user.message with no live Run needs invoke, and anything else needs steer. No PermissionAssignment, no Session — that is the denial the example proves end to end. (observe, and the browser tier’s own steer, are checked where a Session-client token is issued or used; approve on the action-approval-response route when you assert acted_as. act_as applies to installed and automation Sessions, whose acting Principal comes from the installation’s placement — Session create rejects a caller-supplied acted_as outright, so the direct Sessions this guide creates never involve it. An agent’s own Custom tools are ActionPolicy-only and default to allow — they run inside your app, so a PermissionAssignment is not what gates them.) The agent registry side is yours. Agent reads and mutations (GET/PATCH /v1/agents/…) and Session event reads over your API key carry no end-user identity, so Checkfu cannot know which of your users clicked “edit” — your backend is the trusted asserter (that is why the key never reaches the browser). “Only Vivian may edit or even see her agent’s instructions” is therefore your route guard. You don’t have to invent the policy store for it: mint edit/view PermissionAssignments exactly like invoke, and ask POST /v1/action-policies/evaluate before serving the registry call —
Checkfu decides; your route enforces. The Acme Workbench example keeps its own user-to-document ownership map instead, which is equally valid — the point is that one of those checks must exist in your backend, because the platform deliberately does not gate the agent registry per end user. Some routes do take an asserted Principal and check its PermissionAssignments — GET /v1/transcripts requires principal_id and checks that Principal’s view on the SurfaceScope before it answers. That still leaves you one job: the platform verifies what that Principal may do, never that the Principal is the user who signed in. Resolve principal_id from your own session, never from anything the browser sent, or you have handed every user the ability to read as anyone else. Save remains a draft PATCH with expected_version. Publish is a separate operation:
A preview uses the deployment’s current revision, which resolves once, at admission. The returned agent_deployment_revision_number and agent_release_number sit beside the transcript, so an old v1 card stays visibly v1 after v2 is published and deployed.
The selected save_note tool is declared inline on the Agent and served by checkfu.tools.serve in the host process. @checkfu/ui renders the Session status, tool call, and transcript; the server proxies only event routes for Sessions owned by the signed-in host user.

Verify the journey

The scripted journey asserts the draft/publish boundary, v1/v2 Session freezing, tool activity, direct-session attribution, and the platform-originated cross-user denial. The example README also lists every generated SDK namespace it composes.
The example does not claim cross-session memory. The versioned memory and history surface an honest recall demo needs is not generally available yet, so the demo mints no MemoryStore rather than faking recall in application state.

Per-visitor execution variance

An embedded product often wants distinct reviewed execution postures — a fast route and a deep route — without minting a definition per choice. That variance now lives in the deployment layer: publish one deployment per posture under a distinct key (fast-default, deep-review), each pinning its reviewed model-routing profile, and create Sessions against the deployment that matches the visitor’s choice:
Each deployment revision is validated at publication, resolves once at Session admission, and is immutable for the Session’s lifetime. This is the governed alternative to per-Session override bags: the reviewed deployment revision itself pins exactly the variation it allows, and there is no model_routing_profile_key knob on the Session-create wire.