# Identities

Several agents share one workspace, each under its own token, acting as a seeded user, with a call log for grading.

Identities let several agents or test drivers share one workspace, each under its own token and acting as its own user. Bind a token to a user, and every call with that token acts as that user. Pass `X-Rystic-Actor` for one call, or `X-Rystic-Surface: client` to post the way a person types in Slack:

```bash
curl -s -X POST "$RYSTIC_URL/_rystic/scenario" -d '{"name":"design_studio"}' -o /dev/null
curl -s -X POST "$RYSTIC_URL/_rystic/credentials" -H 'Content-Type: application/json' -d '{"bindings":{"xoxp-pm-agent":"UWDC0000001","xoxp-worker-agent":"UWDC0000002"}}' -o /dev/null -w '%{http_code}\n'
# -> 204
curl -s "$RYSTIC_URL/api/auth.test" -H 'Authorization: Bearer xoxp-worker-agent' | jq -c '{user, user_id}'
# -> {"user":"noor","user_id":"UWDC0000002"}
curl -s "$RYSTIC_URL/api/auth.test" -H 'Authorization: Bearer xoxb-local' -H 'X-Rystic-Actor: UWDC0000001' | jq -c '{user, user_id}'
# -> {"user":"dana","user_id":"UWDC0000001"}
curl -s -X POST "$RYSTIC_URL/api/chat.postMessage" -H 'Authorization: Bearer xoxp-worker-agent' -d channel=CWDC0000004 -d 'text=on it' | jq -c '{ok, user: .message.user, bot_id: .message.bot_id}'
# -> {"ok":true,"user":"UWDC0000002","bot_id":"B000000001"}
curl -s -X POST "$RYSTIC_URL/api/chat.postMessage" -H 'Authorization: Bearer xoxp-worker-agent' -H 'X-Rystic-Surface: client' -d channel=CWDC0000004 -d 'text=on it, typed by hand' | jq -c '{ok, user: .message.user, bot_id: .message.bot_id}'
# -> {"ok":true,"user":"UWDC0000002","bot_id":null}
curl -s "$RYSTIC_URL/_rystic/calls?tail=1" | jq -c '.calls[] | {seq, principal, path, surface}'
# -> {"seq":4,"principal":"UWDC0000002","path":"/api/chat.postMessage","surface":"client"}
```

### Binding tokens to users

A binding names a token and the user id it acts as. The bindings last until a reset, and each MCP server instance or agent gets its own token. The token alone names the caller only when it is bound; in a loaded scenario every other token acts as the scenario's bot (`UWDCBOT0001`, `studiobot`, in `design_studio`). `X-Rystic-Actor: <user id>` acts as a seeded user instead, and wins over a binding.

### What each identity sees

Each identity sees only what it belongs to. A private channel, DM or group DM is listed, read and posted to only by its members; anyone else gets `channel_not_found`. Public channels are listed to everyone with a per-caller `is_member`, a user token reads one without joining, and posting needs a join. Put anything an agent must not see in a private channel. A DM opened from either side is one conversation. Two bots cannot DM each other (`cannot_dm_bot`), as on Slack, so agents that DM should be regular users with `xoxp-` tokens.

### Posting as a person

A post made with an app's user token carries the app's `bot_id`, `app_id` and `bot_profile` beside the poster's `user`, as on Slack. To post as a person, add `X-Rystic-Surface: client` to a `chat.postMessage` made as a seeded non-bot user (a bound token or `X-Rystic-Actor`). The message is the one that user would type in the Slack client: `user`, `team`, `text`, `blocks` and a `client_msg_id` (yours when you pass one, else minted), with no `bot_id`, `app_id` or `bot_profile`.

A `chat.scheduleMessage` made the same way schedules that message, and it posts as the person when it falls due. The header names no one; the binding or actor still does, and a bot caller's post is unchanged. Its only value is `client`; any other answers HTTP 400. Only `chat.postMessage`, `chat.scheduleMessage` and the client's [interaction methods](https://www.rystic.ai/docs/simulators/slack/features/interactivity.md) read it.

### The call log

`GET /_rystic/calls` lists every request since reset with its `principal`, for grading. Each entry carries `args`, the call's arguments with tokens redacted, and `ok`: a refusal Slack answers at HTTP 200 has `ok: false` and its code in `error`, and a method the simulator does not have is also marked `unknown_route: true`.

`?tail=N` returns the last N calls. The log keeps the last 100,000 calls; post `{"retain":0}` to `/_rystic/egress-retain` to keep them all.

---

Source: https://www.rystic.ai/docs/simulators/slack/features/identities · Markdown: https://www.rystic.ai/docs/simulators/slack/features/identities.md
