Skip to content
View as .md

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

TierKindsWho may append
Conversationchat without a handover markerany participant, as themselves, via the member API or a scoped token
Run transcriptpi.*, signal.thread.status_changed, signal.thread.pi_heartbeat, signal.thread.coding_session_started, signal.thread.pi_faileda scoped stream token, only into the thread it is bound to; also members via the API (stamped as them, see below)
Runtime factsevery other signal.*, and chat with a handover markerservice-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). 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 (entry identity), dispatch (handover), sandbox-daytona (token claims), and the API.