Skip to main content
A shared agent has a shared problem. If the HR application can read every employee record, then everyone who talks to it can, whatever their own access happens to be. Copying your permission model into the agent’s prompt is not a fix. The model can be talked out of it. With standing identity, the agent acts as itself — every caller gets the same Cedar answer from the agent’s grants. With delegated identity, each call carries a verified person, and Cedar authorizes on that person instead.

Hand this to an AI agent. It sets up act-as for Eva, Alice, and Carol on the HR application, then shows different answers from the same UI.

Open in Cursor
In this tutorial, you will switch from standing hr-assistant to a delegated agent, delegated-hr. Eva (HR), Alice (self), and Carol (peer) click the same buttons and get different answers. The app has no logic that knows the difference. To do this, you will:
1

Register a delegated agent

Configure act-as verification on delegated-hr and make it mandatory. Leave hr-assistant standing.
2

Write rules about the person

Deploy Cedar that reads groups and compares act_as.sub to tool arguments.
3

Check the identities

Simulate Eva, Alice, and Carol before making a live call.
4

How this application implements it

Keep the token and act-as secret server-side. Sign per request.
5

Verify the results

See different answers from one UI, and each in audit.

Background

Earlier tutorials used hr-assistant standing. This one registers a second agent, delegated-hr, that requires act-as. Cedar reads principal.act_as, so Eva, Alice, and Carol can get different answers for the same tool. You register a second agent rather than flipping hr-assistant to require act-as. Those settings would break earlier tutorials that call with no person attached. delegated-hr is the application agent that borrows each end user’s identity. Act-as carries that person on each call. The boundary is enforced at the gateway, not in the model’s prompt. That only works if your backend talks to Dome. Keep the token and signing secret on the server; the browser only says who is using the app. A frontend that called Dome directly could forge or omit act-as.

Prerequisites

For this tutorial, you will need: Confirm the workspace before you start:
The workspace should read sandbox-get-started. Switch back if it does not:
Clone the reference app if you do not already have it from the previous tutorial:
If you already cloned it for Build a governed app, just cd into that directory. You will check out this tutorial’s branch when you wire act-as.
This tutorial runs entirely in a sandbox. In a production workspace, the identity provider is wired once by an operator, and the rules that read its claims are owned by security.

Register a delegated agent

Register delegated-hr rather than reusing hr-assistant. Standing vs delegated is chosen per agent. hr-assistant stays the standing editor/curl agent from earlier tutorials. delegated-hr is the purpose-built application agent: it authenticates as itself, but every governed call must present a verified end user, and Cedar authorizes on that person. This tutorial uses HMAC — a shared secret your backend uses to sign act-as. That is fine for a sandbox. In production, use OIDC instead: register with --actas-method oidc and forward a JWT from your identity provider so Dome verifies the person against the IdP, not a secret your service holds. Generate the signing secret first so you can save it:
Two flags carry the meaning:
  • --actas-method hmac tells the gateway how to verify the identity your service presents. Shared-secret HMAC is the sandbox path here; production uses oidc.
  • --actas-required rejects any call arriving without an identity. Without it, act-as is accepted when present and ignored when absent, and a caller who omits it gets the agent’s own permissions.
Mint a credential for the application:
--actas-allowed-group, --actas-allowed-email, and --actas-allowed-subject add an admission-level allowlist, rejecting identities outside it before Cedar runs. That is a coarse gate for narrowing which population an agent may serve. Leave it unset here so Cedar makes every decision.

Write rules about the person

Cedar sees the end user as principal.act_as, a record carrying sub, email, roles, groups, and any custom claims the verified identity provided. Rules can read those the same way they read anything else. This policy does four things:
  • Anyone with a verified identity can list employees and view the org chart
  • Someone in the hr group can open any employee record and read salary
  • Anyone else can open only their own record (act_as.sub matches employee_id)
  • Calls with no act-as identity, and tools outside that set, are denied
So Eva (HR) gets Alice’s record and salary; Alice gets her own record only; Carol gets neither for Alice. The reference repo ships delegated-hr.cedar. Create it (or copy from the clone):
delegated-hr.cedar
The principal has act_as guard matters. act_as is absent from the entity entirely when no identity was verified, and reading an absent attribute is an evaluation error rather than a false. Guard it, and an anonymous call falls through to a clean deny. Self-lookup compares resource.arguments.employee_id to principal.act_as.sub. Demo personas set sub to the employee id (E005 Eva, E001 Alice, E003 Carol), so Alice can open her own record while Carol cannot open Alice’s. Payroll (get_salary) stays HR-only. If you completed Call a model through a pool, Chat still needs llm:invoke on employee-summary. Tool rules do not cover inference. Create delegated-hr-llm.cedar (same shape as hr-assistant-llm.cedar, scoped to this agent):
delegated-hr-llm.cedar
Deploy both files scoped to the new agent, grant gateway access, and (for Chat) permit the pool:
Apply both Cedar files in one deploy so the tool rules and the pool rule stay active together. A second dome rules apply at the same agent scope replaces the previous bundle rather than layering beside it.

Check the identities

Simulation accepts an end-user identity, so you can test a rule that reads act_as without a signing secret, a service, or a live call. Pass --eval-arguments whenever the rule compares employee_id. Eva (HR) looking up Alice:
Expect ALLOW. Alice looking up herself (sub E001):
Expect ALLOW. Carol looking up Alice:
Expect DENY. Confirm salary is HR-only:
Expect ALLOW then DENY. Confirm that the directory stays open to everyone by swapping the resource for demo-hr/hr/list_employees, and that omitting the act-as flags entirely denies the employee lookup rather than erroring. This is the loop worth keeping. A rule that reads identity claims has more branches than a rule that reads a tool name, and simulation is where you find the branch you forgot.

How this application implements it

Act-as is a server-side concern. The gateway authenticates the agent with Authorization: Bearer dome_… and the end user with X-Dome-Act-As. Both headers are secrets your browser must never hold. Same frontend/backend split as Build a governed app. With act-as the stakes are higher: if the browser held the agent token or signing secret, anyone with DevTools could impersonate anyone. Check out this tutorial’s branch, which adds the Act as picker and HMAC signing, then point the server at delegated-hr:
Update .env with the delegated-hr token and the secret you generated above. These values are read only by the Node process, never shipped to Vite or the browser:
.env
Do not prefix these with VITE_. Do not put DOME_TOKEN or DOME_ACTAS_SECRET in client bundles, public env, or browser storage. The frontend must not import them and must not fetch the Gateway URL.

How the application signs

The browser only sends a persona id (eva | alice | carol). The Hono proxy maps it to claims, signs with the server secret, and sets X-Dome-Act-As on the outbound Dome request. Cedar never sees a branch in App.tsx. Personas are fixed claims for the demo. There is still no allow/deny logic in the UI:
server/actas.ts
signHMACActAs builds the envelope the gateway verifies: identity fields + _ts, HMAC-SHA256 over canonical JSON | timestamp, then standard base64 of the signed payload. That string is the X-Dome-Act-As header value.
server/actas.ts
Tool and chat routes attach the full signed header on the real gateway request. redactActAs is only for the View gateway call sheet. It truncates the header in the UI trace so the signed blob is not dumped into the browser transcript. Dome still receives the unredacted value.
server/dome.ts
server/types.ts
Start the app:
Open http://localhost:5173. The Act as bar appears when DOME_ACTAS_SECRET is set.

Click through Eva, Alice, and Carol

Stay on Tools for the clearest contrast:
  1. Select Eva Martinez (HR · hr · E005).
  2. Click List employees. Allowed.
  3. Click Who is E001?. Allowed. Email arrives as [REDACTED] (Filter still applies).
  4. Click What is Alice’s salary?. Allowed (HR-only).
  5. Select Alice Johnson (Engineering · E001).
  6. Click Who is E001?. Allowed (self: sub matches employee_id). Email still redacted.
  7. Click What is Alice’s salary?. Denied.
  8. Select Carol Williams (Engineering · E003).
  9. Click Who is E001?. Denied. Open View gateway call and confirm X-Dome-Act-As on the request. The response carries the Cedar reason.
If you want, try the same walk on Chat — ask Who is E001 and how do I reach them? as Eva, then Alice, then Carol. Same prompt, different outcomes. Model and tool steps both carry the signed identity. One UI, one agent, one credential, three answers. Alice can still see herself. Carol cannot open Alice’s record. Only Eva reaches payroll.
For a real identity provider, configure the agent with --actas-method oidc and forward the JWT your application already validated as X-Dome-Act-As. The gateway verifies the signature against the provider’s discovery document, so your service never becomes the authority on who the caller is. HMAC is the sandbox path. Refer to Use OIDC or bound act-as (coming soon) for the production methods.
These are remediation steps if you get stuck:
  • The Act as bar does not appear. DOME_ACTAS_SECRET is missing or empty. Restart npm run dev after writing .env. Confirm /api/config returns "actAsEnabled": true.
  • 400 with act-as header required. The agent is registered with --actas-required and the call carried no identity. Confirm the app is sending a persona and that the secret is set so the proxy signs.
  • 403 with act-as verification failed. The signature did not verify. Usually the secret in .env is not the one the agent holds. Rotate both to a known value:
  • 403 with agent act-as method does not meet workspace policy. The workspace requires a stronger verification method than this agent uses, typically oidc rather than hmac. Check the policy before weakening anything:
  • Both users get the same answer. The rule is not reading the claim you think it is, or you are still on the hr-assistant token. Simulate both identities again and confirm .env uses the delegated-hr key:
  • A VITE_ env var or browser network tab shows calls to the Gateway / holds dome_…. Wrong shape. Move the token and act-as secret back to server .env and call Dome only from the Hono proxy. The frontend may only hit /api/* on your origin.
  • Chat fails with a pool / llm error while Tools work. Apply the pool rule to this agent:
  • The employee record arrives with a masked email for Eva or Alice. That is the response Filter from the first tutorial, still doing its job on this connection. Filters apply to the tool, not to the agent, so a second agent inherits them.

Verify the results

Confirm both decisions are on the record:
Each event carries the agent and the verified end user, so a denial is attributable to a person rather than to a shared service account. That is the audit property act-as buys you, and it is the one that matters during an investigation. To read only the rejection:
A rejected identity is recorded separately, as authorization.act_as.rejected, because a failed signature is a different problem from a denied call. The first means your service or its secret is misconfigured. The second means the rules did their job.
Quotas take end users as subjects too. dome model quota set --subject act-as --act-as E005 --limit 20 caps one person’s inference spend, which is how you stop a single runaway session from consuming a team’s budget.

Clean up

This is the last tutorial in the Get Started track. Delete the sandbox to remove the agents, rules, and grants together:
To keep the workspace and retire only this agent:
Rotate the act-as secret anywhere you copied it. It signs identity assertions, so treat it like the credential it is. Clear DOME_ACTAS_SECRET (or switch back to the hr-assistant token) if you return to earlier tutorials.

Next steps

You learned how to require verified end-user identity, authorize from the person, and attribute decisions to both agent and user. Continue with: