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: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: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
invokePermissionAssignment 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:
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.
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 —
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:
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.
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
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 distinctkey (fast-default, deep-review), each
pinning its reviewed model-routing profile, and create Sessions against the deployment that
matches the visitor’s choice:
model_routing_profile_key knob on the Session-create wire.