Identity
A transcript proves a machine did something. It does not prove a person did.
Before this shipped, EventBase carried id, seq, ts, sessionId, turnId, causationId and source — and source is a category (user | model | tool | plugin | system | compactor), not a principal. Grepping the runtime for userId, actorId or principal returned nothing.
Three things were impossible as a direct result:
- Audit. "Who ran
bash rm -rf" had no answer. - RBAC. There was no subject to attach a role to.
- Cost attribution. Usage accounted per session, and sessions were anonymous.
The Principal
interface Principal {
readonly id: string; // stable, e.g. "u:ada@example.com"
readonly kind: 'human' | 'service';
readonly displayName?: string;
readonly claims?: Record<string, unknown>;
}It is stamped on EventBase and on ToolCallContext, so every event in the log and every tool call carries the subject that caused it.
{
"type": "tool_call_requested",
"seq": 118,
"sessionId": "01JZ…",
"turnId": "01JZ…",
"source": "model",
"principal": { "id": "u:ada@example.com", "kind": "human", "displayName": "Ada L." },
"tool": "bash"
}source and principal answer different questions, and both are kept. source: "model" says the model asked for the call; principal says on whose behalf the session is running.
Where it comes from
Identity resolves at the channel boundary, because that is the only place that knows who is actually there:
| Surface | Principal |
|---|---|
| Terminal | the OS user |
| HTTP | the authenticated token's subject |
| Slack, Telegram, other chat | the paired account |
| Mobile | the paired device's token subject |
| Runner | per connection |
| Schedule, webhook | a service principal naming the trigger |
A surface that cannot establish a human gets an explicit service principal rather than a blank field, so "a job did this" never looks the same as "nobody recorded it".
Why it landed first
It is a protocol change. It touches EventBase, the runner wire protocol (which bumped RUNNER_PROTOCOL_VERSION) and the desktop IPC contract. Doing it before the rest of the governance work was far cheaper than retrofitting it after, and the audit trail is only meaningful because the records it writes have a subject.
Because derived state is a pure fold over the log, adding the field invalidated nothing already written: older events simply carry no principal.
Multi-client sessions
The runner is explicitly multi-client, and cross-client abort is permitted by default. MOXXY_RUNNER_STRICT_ABORT=1 denies it, so one attached client cannot cancel another's turn. Worth setting anywhere two people may hold the same session.
Next
- The audit trail — what identity makes provable
- Policy and locked config
- Deploying to a fleet