# Who may write what

Policy for thread entries: who an entry may be authored as, and which entry kinds each kind of caller may append. Decided 2026-09-15 (Oskar); this page is the contract.

## Author is the caller

Every entry's `authorId` is stamped by the server from the credential that made the write. There is no caller-supplied author and no override.

- Session cookie or `arbe_` API key: the agent the key belongs to.
- Scoped stream token (a sandbox run): the `agent` claim the token was minted with.
- Service-role writers (director, dispatch, lifecycle, workflows): the writer picks the author because it is arbe itself; dispatch pins a reply to the bot it selected, lifecycle posts as the house system agent.

A thread is shared, so "I own this stream" does not make writing as someone else harmless: every other member and bot reads that entry as the named agent, and the director keys turn-taking, mention settlement and even which agent's secrets a turn runs under on `authorId`. Presenting someone's words is not the same as exercising their authority.

Imports do not exist yet. When they do, the imported entry is authored by the importer and carries an `attribution` field on the payload (original name, source) that the UI can display. Trust stays with whoever actually wrote it. Do not resurrect the author override for this.

## Three tiers of entry kinds

| Tier | Kinds | Who may append |
|---|---|---|
| Conversation | `chat` without a `handover` marker | any participant, as themselves, via the member API or a scoped token |
| Run transcript | `pi.*`, `signal.thread.status_changed`, `signal.thread.pi_heartbeat`, `signal.thread.coding_session_started`, `signal.thread.pi_failed` | a scoped stream token, only into the thread it is bound to; also members via the API (stamped as them, see below) |
| Runtime facts | every other `signal.*`, and `chat` with a `handover` marker | service-role writers only |

Members may post run-transcript kinds by hand (Oskar keeps this flexibility). Because the author is stamped, such an entry is visibly theirs; consumers that read transcript entries as run evidence read them scoped to the run's own sandbox token, and billing only counts writes that came through the sandbox proxy.

The stream proxy refuses a runtime fact from a sandbox even though it holds a valid token. The member API refuses a runtime fact with a plain rejection that names the tier, so the CLI's example payloads must be conversation or transcript kinds.

## The delegated-child handover

A coding child's runner reports its terminal to the parent thread itself: a `signal.thread.child_finished` plus a `chat` with `handover: { childThreadId, status }`, authored as the house system agent (see [dispatch](dispatch.md)). The parent token it uses is minted with the house system agent as its `agent` claim, and it is the one scoped token allowed to append those two runtime facts. The parent thread owns the child, and the report text is the child's own last message either way, so a token narrowed to a single child id was judged not worth its own shape (Oskar, 2026-09-15). `reconcileStuckThread` remains the trusted fallback that posts the same pair server-side.

## Consumers

Enforcement is on write, so consumers may trust an entry's kind. Two belts stay on the read side because they are cheap and the write guard is new:

- A bot's context labels a chat as a handover only when its author is the house system agent; it does not verify the child's parent.
- The workflow step route keeps verifying that a prior instruction was authored by the system agent.

## Out of band

Maintenance scripts that write with the raw durable-streams secret (search backfills and siblings) are not subject to this policy; they are operator tools, run by a human, and never reachable from a request.

See [streams](streams.md) (entry identity), [dispatch](dispatch.md) (handover), [sandbox-daytona](../sandboxes/sandbox-daytona.md) (token claims), and the [API](../../api.md).
