Summary
We should split Relaycast authentication into distinct principals instead of using the root workspace key for both administration and observation.
The immediate design question is: should /v1/ws?scope=workspace and read-only history/search REST APIs be accessible with the root rk_live_ workspace key, or should they use a separate observer credential?
Proposed answer: introduce scoped observer tokens, likely ot_live_, and make the workspace stream plus read-only REST surfaces use those tokens. Keep node credentials separate as nt_live_, agent credentials as at_live_, and reserve rk_live_ for workspace administration.
Motivation
Today the workspace stream is effectively a firehose. It is useful for dashboards, brokers, audit tools, backfill systems, and monitoring, but it is too broad to require handing those systems the root workspace key.
A root workspace key can administer the workspace. If that same key is used in browser dashboards, hosted monitors, brokers, or third-party observability tooling, a read-only integration becomes a full workspace compromise.
The new node-delivery direction also makes the boundary clearer:
- Agents should receive through their bound node route.
- Nodes should receive node-scoped delivery/context through node credentials.
- Workspace stream should be observer-only.
- Admin APIs should remain workspace-key-only.
So we should make observation a separate principal, not just another use of the workspace admin key.
Proposed Principal Model
rk_live_: workspace admin key
Root/admin credential for a workspace.
Allowed:
- Create, update, delete workspace configuration.
- Register/create agents.
- Create/rotate/revoke agent tokens.
- Create/register nodes.
- Create/rotate/revoke node tokens.
- Create/rotate/revoke observer tokens.
- Configure observer token scopes and filters.
- Administrative listing of tokens, agents, nodes, and workspace settings.
Not ideal long term:
- Opening
/v1/ws?scope=workspace directly.
- Being embedded into dashboards or monitoring tools.
- Being used by brokers that only need observation.
Migration option:
- Temporarily continue accepting
rk_live_ on the workspace stream for compatibility, but mark it deprecated and add a clear sunset path.
- For a major upgrade, make
/v1/ws?scope=workspace require ot_live_ only.
at_live_: agent token
One agent identity.
Allowed:
- Act as exactly one agent.
- Send messages as that agent.
- Read APIs that are already available to that agent.
- Open the agent websocket/direct route.
- Acknowledge/fail/defer deliveries owned by that agent.
Important invariant:
- A directly connected agent is represented internally as an implicit
direct_ws node-of-one, but it should not need a node token. The agent token authenticates the principal; the direct node is a routing/ledger abstraction.
Not allowed:
- Workspace firehose.
- Node control websocket.
- Admin token minting.
nt_live_: node token
One node identity.
Allowed:
- Open
/v1/node/ws for that node.
- Receive node-scoped deliveries and context.
- Heartbeat/report node inventory.
- Spawn/register/deregister agents for broker-controlled nodes, subject to node contract/capabilities.
- Complete node-owned invocations.
Not allowed:
- Workspace firehose.
- Root workspace administration.
- Arbitrary read-only message search outside scoped node context.
HTTP push nuance:
- HTTP push delivery auth, such as HMAC/bearer/static header webhook credentials, is adapter auth, not a Relaycast principal.
- If an HTTP receiver needs to call back to Relaycast, it should use a scoped
nt_live_ node token or a narrower one-shot delivery acknowledgement credential, not the root workspace key.
ot_live_: observer token
Read-only observer principal for realtime observation and read-only REST backfill/history/search.
Allowed, depending on scopes:
- Open
/v1/ws?scope=workspace.
- Read channels, threads, messages, DMs, reactions, agents, nodes, deliveries/activity, and search endpoints according to scopes and filters.
Not allowed:
- Sending messages.
- Creating/updating/deleting channels.
- Registering or mutating agents.
- Creating/rotating node or agent tokens.
- Invoking actions.
- Acknowledging/failing/defering deliveries.
- Opening
/v1/node/ws.
- Acting as an agent.
Observer Token Scopes
Suggested scope names:
stream:read - open workspace observer websocket.
messages:read - read channel message history.
threads:read - read thread replies.
dms:read - read direct/group DM history where allowed by filters.
channels:read - list/read channels and members.
search:read - search messages/content.
agents:read - list/read agents and agent status.
nodes:read - list/read node roster/status.
deliveries:read - read delivery status/activity where appropriate.
activity:read - read workspace activity feed.
files:read - read file metadata and/or download files, depending on how files are authorized.
reactions:read - read reactions, if we want finer granularity than messages:read.
Possible preset bundles:
Dashboard observer
For the full observer dashboard:
stream:read
messages:read
threads:read
dms:read
channels:read
search:read
agents:read
nodes:read
activity:read
files:read
Ops monitor
For node/agent health monitoring only:
stream:read
agents:read
nodes:read
activity:read
Search/backfill worker
For indexing/search only:
messages:read
threads:read
dms:read
channels:read
search:read
files:read
Realtime-only consumer
For a pure realtime subscriber:
Observer Token Filters
Scopes answer "what type of API can this token use?" Filters answer "which workspace data can it see?"
Suggested optional filters:
channel_ids: allow only specific channels.
channel_names: allow only specific channel names.
include_dms: boolean, default false for safer observer tokens.
dm_conversation_ids: allow only specific DM/group DM conversations.
agent_ids: limit agent/node/status visibility to conversations involving selected agents.
event_types: limit workspace stream event types.
created_after or retention override: optional backfill lower bound.
Filter behavior should be consistent across websocket and REST:
- If an observer token cannot read a channel via REST, it should not receive that channel's stream events.
- If it cannot read DMs, it should not receive
dm.received or group_dm.received stream events.
- If it cannot read files, message events should either omit file download URLs or include only metadata, depending on the file model.
REST Behavior
ot_live_ should be accepted by read-only endpoints only when the token has the required scope.
Candidate endpoint mapping:
| Endpoint family |
Required observer scope |
GET /v1/activity |
activity:read |
GET /v1/channels |
channels:read |
GET /v1/channels/:name |
channels:read |
GET /v1/channels/:name/members |
channels:read |
GET /v1/channels/:name/messages |
messages:read plus channel filter |
GET /v1/messages/:id/reactions |
messages:read or reactions:read |
GET /v1/messages/:id/thread / thread reads |
threads:read plus channel/conversation filter |
| DM list/read endpoints |
dms:read plus conversation filter |
| Search endpoints |
search:read plus filters applied to result set |
| Agent directory/list/status reads |
agents:read |
| Node roster/status reads |
nodes:read |
| Delivery status/activity reads |
deliveries:read or activity:read, depending on endpoint semantics |
| File metadata/download |
files:read plus message/channel/conversation filter |
All write endpoints should reject ot_live_ with 403 forbidden or 401 invalid_token depending on current auth semantics. Prefer 403 insufficient_scope when the token is valid but under-scoped.
WebSocket Behavior
/v1/ws?scope=workspace should eventually accept only ot_live_ with stream:read.
Behavior:
- Authenticate observer token.
- Resolve workspace.
- Apply event filters before publishing to that socket.
- Apply channel/conversation visibility filters before emitting message/DM/thread/file events.
- Stamp stream events the same way current workspace observer clients expect, unless we intentionally version the stream.
Open compatibility question:
- During migration, should
rk_live_ still be accepted for /v1/ws?scope=workspace with a deprecation warning/event/header, or should the major upgrade remove it immediately?
Token Lifecycle API
Potential workspace-admin endpoints:
POST /v1/observer-tokens
GET /v1/observer-tokens
GET /v1/observer-tokens/:id
PATCH /v1/observer-tokens/:id
POST /v1/observer-tokens/:id/rotate
DELETE /v1/observer-tokens/:id
Create request shape:
{
"name": "dashboard-prod",
"description": "Production read-only dashboard",
"scopes": ["stream:read", "messages:read", "search:read", "channels:read", "agents:read", "nodes:read"],
"filters": {
"channel_names": ["general", "ops"],
"include_dms": false,
"event_types": ["message.created", "agent.status.active", "agent.status.offline", "node.online", "node.offline"]
},
"expires_at": "2026-12-31T23:59:59Z"
}
Create response shape:
{
"ok": true,
"data": {
"id": "ot_...",
"name": "dashboard-prod",
"token": "ot_live_...",
"scopes": ["stream:read", "messages:read"],
"filters": { "include_dms": false },
"expires_at": "2026-12-31T23:59:59Z",
"created_at": "..."
}
}
List response should not return token material after creation/rotation.
Storage Model
Potential table: observer_tokens
Suggested columns:
id
workspace_id
name
description
token_hash
scopes JSON array
filters JSON object
status (active, revoked)
expires_at
created_by / created_by_type
last_used_at
created_at
updated_at
revoked_at
Indexes:
- unique token hash
(workspace_id, status)
(workspace_id, name) if names should be unique per workspace
(expires_at) if we sweep expired tokens
Auth Middleware Shape
Add an observer auth result alongside current workspace/agent/node auth results.
Conceptually:
type AuthPrincipal =
| { kind: 'workspace'; workspace }
| { kind: 'agent'; workspace; agent }
| { kind: 'node'; workspace; node }
| { kind: 'observer'; workspace; observerToken; scopes; filters };
Then route middleware can use requirements like:
requireWorkspaceAdmin
requireAgentToken
requireNodeToken
requireObserverScope('messages:read')
requireWorkspaceRead(['messages:read']) // accepts rk_live_ admin or ot_live_ scoped token?
Open design question:
- Should read-only REST endpoints accept both
rk_live_ and ot_live_, where rk_live_ is treated as admin with all read scopes?
- Or should some observer-specific endpoints require
ot_live_ only?
My bias: REST read endpoints can accept both rk_live_ and appropriately scoped ot_live_; workspace stream should move to ot_live_ only because it is commonly held open by less-trusted clients.
Error Semantics
Suggested errors:
401 invalid_token: token is absent, malformed, unknown, expired, or revoked.
403 insufficient_scope: valid observer token lacks the required scope.
403 filtered_resource: valid observer token has the scope but its filters exclude the requested channel/conversation/resource.
We should decide whether filtered resources should return 403 or 404 to avoid disclosing existence. For external observer tokens, 404 may be safer for channel/message/conversation reads.
Security Considerations
- Observer tokens must be read-only by construction.
- Store only token hashes.
- Support rotation and revocation.
- Track
last_used_at for audit/debugging.
- Optional expiration should be encouraged or required for generated dashboard tokens.
- DM access should be opt-in, not default.
- File download URLs need special care:
files:read should not accidentally bypass message/channel filters.
- Search results must enforce the same filters as direct history reads.
- Workspace stream must apply filters before events hit the socket.
- Avoid mixed-case compatibility fallbacks in wire fields; keep JSON snake_case.
Migration Plan
Possible major-upgrade path:
- Add observer token storage and admin CRUD endpoints.
- Add observer auth parsing for
ot_live_.
- Teach read-only REST endpoints to accept scoped observer principals.
- Teach workspace websocket auth to accept
ot_live_ with stream:read.
- Update dashboard to create/use an observer token instead of storing
rk_live_ for live stream/read operations.
- Deprecate or remove
rk_live_ support for /v1/ws?scope=workspace.
- Update README, OpenAPI, and TypeScript/Python/Rust/Swift SDKs.
- Add conformance tests for scope allow/deny, filters, stream auth, and REST auth.
Acceptance Criteria
ot_live_ tokens can be minted, listed, rotated, revoked, and expired by a workspace admin.
/v1/ws?scope=workspace accepts ot_live_ with stream:read.
/v1/ws?scope=workspace rejects at_live_ and nt_live_.
- The final major-upgrade behavior rejects
rk_live_ on workspace stream, or the compatibility behavior is explicitly documented and tested.
- Read-only REST endpoints accept
ot_live_ only with the required scopes.
- Write REST endpoints reject
ot_live_.
- Channel/conversation/event filters are enforced consistently across REST and websocket stream.
- Search applies observer filters before returning results.
- File reads/downloads respect observer scopes and filters.
- SDKs expose observer token creation and observer-auth clients without encouraging root workspace key use in dashboards.
- README and OpenAPI describe the four-principal model:
rk_live_, at_live_, nt_live_, ot_live_.
Open Questions
- Should
rk_live_ be rejected immediately on workspace stream as part of the major upgrade, or deprecated for one release?
- Should observer tokens be named
ot_live_, or should we use a more explicit prefix like obs_live_?
- Should observer token filters support both allowlists and denylists, or allowlists only?
- Should DM read access default to false even for dashboard presets?
- Should observer tokens be able to read delivery status, or should delivery reads remain agent/node/admin only?
- Should file download require both
files:read and the scope for the containing message/conversation?
- Do we need one-shot or delivery-scoped acknowledgement tokens for pure
http_push receivers, separate from nt_live_?
- Should
nt_live_ tokens get any read-only REST capabilities for their bound agents, or only node control APIs?
Summary
We should split Relaycast authentication into distinct principals instead of using the root workspace key for both administration and observation.
The immediate design question is: should
/v1/ws?scope=workspaceand read-only history/search REST APIs be accessible with the rootrk_live_workspace key, or should they use a separate observer credential?Proposed answer: introduce scoped observer tokens, likely
ot_live_, and make the workspace stream plus read-only REST surfaces use those tokens. Keep node credentials separate asnt_live_, agent credentials asat_live_, and reserverk_live_for workspace administration.Motivation
Today the workspace stream is effectively a firehose. It is useful for dashboards, brokers, audit tools, backfill systems, and monitoring, but it is too broad to require handing those systems the root workspace key.
A root workspace key can administer the workspace. If that same key is used in browser dashboards, hosted monitors, brokers, or third-party observability tooling, a read-only integration becomes a full workspace compromise.
The new node-delivery direction also makes the boundary clearer:
So we should make observation a separate principal, not just another use of the workspace admin key.
Proposed Principal Model
rk_live_: workspace admin keyRoot/admin credential for a workspace.
Allowed:
Not ideal long term:
/v1/ws?scope=workspacedirectly.Migration option:
rk_live_on the workspace stream for compatibility, but mark it deprecated and add a clear sunset path./v1/ws?scope=workspacerequireot_live_only.at_live_: agent tokenOne agent identity.
Allowed:
Important invariant:
direct_wsnode-of-one, but it should not need a node token. The agent token authenticates the principal; the direct node is a routing/ledger abstraction.Not allowed:
nt_live_: node tokenOne node identity.
Allowed:
/v1/node/wsfor that node.Not allowed:
HTTP push nuance:
nt_live_node token or a narrower one-shot delivery acknowledgement credential, not the root workspace key.ot_live_: observer tokenRead-only observer principal for realtime observation and read-only REST backfill/history/search.
Allowed, depending on scopes:
/v1/ws?scope=workspace.Not allowed:
/v1/node/ws.Observer Token Scopes
Suggested scope names:
stream:read- open workspace observer websocket.messages:read- read channel message history.threads:read- read thread replies.dms:read- read direct/group DM history where allowed by filters.channels:read- list/read channels and members.search:read- search messages/content.agents:read- list/read agents and agent status.nodes:read- list/read node roster/status.deliveries:read- read delivery status/activity where appropriate.activity:read- read workspace activity feed.files:read- read file metadata and/or download files, depending on how files are authorized.reactions:read- read reactions, if we want finer granularity thanmessages:read.Possible preset bundles:
Dashboard observer
For the full observer dashboard:
Ops monitor
For node/agent health monitoring only:
Search/backfill worker
For indexing/search only:
Realtime-only consumer
For a pure realtime subscriber:
Observer Token Filters
Scopes answer "what type of API can this token use?" Filters answer "which workspace data can it see?"
Suggested optional filters:
channel_ids: allow only specific channels.channel_names: allow only specific channel names.include_dms: boolean, default false for safer observer tokens.dm_conversation_ids: allow only specific DM/group DM conversations.agent_ids: limit agent/node/status visibility to conversations involving selected agents.event_types: limit workspace stream event types.created_afteror retention override: optional backfill lower bound.Filter behavior should be consistent across websocket and REST:
dm.receivedorgroup_dm.receivedstream events.REST Behavior
ot_live_should be accepted by read-only endpoints only when the token has the required scope.Candidate endpoint mapping:
GET /v1/activityactivity:readGET /v1/channelschannels:readGET /v1/channels/:namechannels:readGET /v1/channels/:name/memberschannels:readGET /v1/channels/:name/messagesmessages:readplus channel filterGET /v1/messages/:id/reactionsmessages:readorreactions:readGET /v1/messages/:id/thread/ thread readsthreads:readplus channel/conversation filterdms:readplus conversation filtersearch:readplus filters applied to result setagents:readnodes:readdeliveries:readoractivity:read, depending on endpoint semanticsfiles:readplus message/channel/conversation filterAll write endpoints should reject
ot_live_with403 forbiddenor401 invalid_tokendepending on current auth semantics. Prefer403 insufficient_scopewhen the token is valid but under-scoped.WebSocket Behavior
/v1/ws?scope=workspaceshould eventually accept onlyot_live_withstream:read.Behavior:
Open compatibility question:
rk_live_still be accepted for/v1/ws?scope=workspacewith a deprecation warning/event/header, or should the major upgrade remove it immediately?Token Lifecycle API
Potential workspace-admin endpoints:
Create request shape:
{ "name": "dashboard-prod", "description": "Production read-only dashboard", "scopes": ["stream:read", "messages:read", "search:read", "channels:read", "agents:read", "nodes:read"], "filters": { "channel_names": ["general", "ops"], "include_dms": false, "event_types": ["message.created", "agent.status.active", "agent.status.offline", "node.online", "node.offline"] }, "expires_at": "2026-12-31T23:59:59Z" }Create response shape:
{ "ok": true, "data": { "id": "ot_...", "name": "dashboard-prod", "token": "ot_live_...", "scopes": ["stream:read", "messages:read"], "filters": { "include_dms": false }, "expires_at": "2026-12-31T23:59:59Z", "created_at": "..." } }List response should not return token material after creation/rotation.
Storage Model
Potential table:
observer_tokensSuggested columns:
idworkspace_idnamedescriptiontoken_hashscopesJSON arrayfiltersJSON objectstatus(active,revoked)expires_atcreated_by/created_by_typelast_used_atcreated_atupdated_atrevoked_atIndexes:
(workspace_id, status)(workspace_id, name)if names should be unique per workspace(expires_at)if we sweep expired tokensAuth Middleware Shape
Add an observer auth result alongside current workspace/agent/node auth results.
Conceptually:
Then route middleware can use requirements like:
Open design question:
rk_live_andot_live_, whererk_live_is treated as admin with all read scopes?ot_live_only?My bias: REST read endpoints can accept both
rk_live_and appropriately scopedot_live_; workspace stream should move toot_live_only because it is commonly held open by less-trusted clients.Error Semantics
Suggested errors:
401 invalid_token: token is absent, malformed, unknown, expired, or revoked.403 insufficient_scope: valid observer token lacks the required scope.403 filtered_resource: valid observer token has the scope but its filters exclude the requested channel/conversation/resource.We should decide whether filtered resources should return
403or404to avoid disclosing existence. For external observer tokens,404may be safer for channel/message/conversation reads.Security Considerations
last_used_atfor audit/debugging.files:readshould not accidentally bypass message/channel filters.Migration Plan
Possible major-upgrade path:
ot_live_.ot_live_withstream:read.rk_live_for live stream/read operations.rk_live_support for/v1/ws?scope=workspace.Acceptance Criteria
ot_live_tokens can be minted, listed, rotated, revoked, and expired by a workspace admin./v1/ws?scope=workspaceacceptsot_live_withstream:read./v1/ws?scope=workspacerejectsat_live_andnt_live_.rk_live_on workspace stream, or the compatibility behavior is explicitly documented and tested.ot_live_only with the required scopes.ot_live_.rk_live_,at_live_,nt_live_,ot_live_.Open Questions
rk_live_be rejected immediately on workspace stream as part of the major upgrade, or deprecated for one release?ot_live_, or should we use a more explicit prefix likeobs_live_?files:readand the scope for the containing message/conversation?http_pushreceivers, separate fromnt_live_?nt_live_tokens get any read-only REST capabilities for their bound agents, or only node control APIs?