> ## Documentation Index
> Fetch the complete documentation index at: https://checkfu.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Let your users build agents

> Run the Acme Workbench example: users author, publish, and preview governed agents inside your product.

<Warning>
  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](/concepts/agents) until
  this guide is rebuilt on the CMA Agent and Session surfaces.
</Warning>

Your product can own the agent-builder experience while Checkfu owns definitions, immutable
versions, PermissionAssignments, and Sessions. The runnable
[`examples/agent-workbench`](https://github.com/andyrewlee/checkfu/tree/main/examples/agent-workbench)
example is a small customer product, Acme Workbench, rather than a second Checkfu console.

## What the example proves

| Your product owns                                                   | Checkfu governs                                                          |
| ------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| Sign-in, user-to-document ownership, forms, and tool implementation | Principals, Agents, version history, PermissionAssignments, and Sessions |
| Which documents appear in each user's builder                       | Whether a Principal may invoke an exact Agent                            |
| The server-side API credential                                      | Admission, release resolution, event history, and runtime attribution    |

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.

<Note>
  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.
</Note>

## Run Acme Workbench

Prepare a Workspace with a Harness, model routing profile, and sandbox profile, then run:

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
CHECKFU_API_KEY=... \
CHECKFU_BASE_URL=http://127.0.0.1:8787 \
pnpm --filter @checkfu-examples/agent-workbench dev
```

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:

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
const principal = await checkfu.principals.create(
	process.env.CHECKFU_WORKSPACE_ID,
	{
		external_subject_id: `acme-user:${user.id}`,
		principal_type: "person",
    display_name: user.name,
  },
  { idempotencyKey: `acme-principal-${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:

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
const agent = await checkfu.agents.create(
  {
    name,
    instructions,
    harness: "claude-code",
    model_routing_profile_key: "primary",
    sandbox_profile_key: "standard",
    tools: selectedTools,
  },
  { idempotencyKey: `acme-agent-${documentId}` },
)

await Promise.all(["invoke", "steer"].map((permission) =>
  checkfu.permissionAssignments.create({
    subject: { kind: "principal", principal: ownerPrincipalId },
    resource: { kind: "agent-definition", id: agent.id },
    permission: 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:

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { createAgentWithOwner } from "@checkfu/sdk"

const { agent, permissionAssignments } = await createAgentWithOwner(checkfu, {
  definition: { name, instructions, harness: "claude-code", model_routing_profile_key: "primary", sandbox_profile_key: "standard" },
  owner: ownerPrincipalId,
  idempotencyKey: `acme-agent-${documentId}`,
})
```

## 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:

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { assignAgentAccess } from "@checkfu/sdk"

await assignAgentAccess(checkfu, {
  agentId: agent.id,
  subject: { kind: "group", group: teamGroupId },
  permission: "invoke",
})
```

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.

<Warning>
  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.
</Warning>

## 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 —

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
const decision = await checkfu.permissionAssignments.check({
  subject: { kind: "principal", principal: requesterPrincipalId },
  resource: { kind: "agent-definition", id: agent.id },
  permission: "edit",
})
if (decision.disposition !== "allow") return forbidden()
```

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:

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
await checkfu.agents.releases.publish(agent.id, {
  expected_version: agent.version,
  note: "Published from Acme Agent Workbench",
})
```

A preview uses the deployment's current revision, which
[resolves once, at admission](/concepts/sessions-and-runs#two-freeze-horizons). 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.

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
const session = await checkfu.sessions.create({
  agent_deployment_id: deployment.id,
  principal: ownerPrincipalId,
  initial_events: [{
    type: "user.message",
    payload: {
      content: prompt,
      authored_by: ownerPrincipalId,
      caused_by: { kind: "api" },
    },
  }],
})
```

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

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
pnpm --filter @checkfu-examples/agent-workbench test
pnpm --filter @checkfu-examples/agent-workbench typecheck
```

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.

<Warning>
  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.
</Warning>

## 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:

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
// Two deployments of one Agent in one Workspace:
//   key "fast-default"  → model_routing profile pinning the fast route
//   key "deep-review"   → model_routing profile pinning the deep route

// On POST /v1/sessions (direct create):
{ "agent_deployment_id": "adep_…", "principal": "prin_…" }
```

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.
