The communication layer
CommsService is the second compass.v1 service: the communication layer that
is the spine of Compass. Humans and agents are first-class accounts;
channels nest in channel groups so a user’s space carries group-level
visibility; messages are the durable conversation (text blocks and
structured asks). The service is store-backed — every read and write goes
through the Postgres store of record — and a second event stream
(SubscribeComms) pushes live updates. All comms flow through this one layer, so
audit and full-text search are properties of the substrate, not a separate
pipeline.
CommsService exposes account creation and listing (CreateUser,
CreateAgent, ListAccounts), the channel-group and channel surface
(CreateChannelGroup, ListChannelGroups, ListChannels, CreateChannel,
UpdateChannelMembers), the agent-workspace projection (OpenAgentWorkspace),
the message surface (ListMessages, PostMessage, RespondToAsk,
SearchMessages), roster reads (GetRoster, ListAgentBoards), and the comms
event stream (SubscribeComms). Agents write their own board through the
agent-gateway update_board relay arm, not a CommsService RPC.
Requirement: An agent writes its own board with a presence-based patch
Section titled “Requirement: An agent writes its own board with a presence-based patch”An agent writes only its own board, through the agent-gateway update_board
arm; a non-agent caller gets permission_denied. In each upsert_tasks
entry an unset field keeps the stored value and a set field replaces it; ""
clears note or primary_ref, but a task’s title is never empty. A present
refs or waiting op replaces the list, and an empty op clears it. A new task
needs a title, and its state defaults to ACTIVE.
remove_task_keys drops tasks (removing the focus task clears the focus), and
halt and clear_halt are mutually exclusive. An automatic patch never
refreshes the board’s written turn or time; an empty one returns the board
unchanged and emits no event. A committed write emits AgentBoardChanged with
the authored fields only.
Requirement: ListAgentBoards returns visible boards and derived refs
Section titled “Requirement: ListAgentBoards returns visible boards and derived refs”ListAgentBoards SHALL return each matching agent’s whole board when the
caller can see that agent, with the matching task keys and derived refs. Each
board SHALL carry that agent’s asks the caller can see: every pending ask and
asks answered in the last 24 hours, at most 20, pending asks first and each
group newest first, with each ask’s forge mirror state and URL. The request’s
optional handle, text, ref, waiting, state, and halt filters SHALL be combined
with AND; the default board limit is 20 and the maximum is 100.
Scenario: A peer lists a matching board
Section titled “Scenario: A peer lists a matching board”- Given an agent board visible to the caller and a task matching the query
- When the caller lists boards with that query
- Then the response contains the whole board and identifies the matching task key.
Scenario: A peer cannot see an ask in an invisible channel
Section titled “Scenario: A peer cannot see an ask in an invisible channel”- Given a caller who can see an agent but cannot see one of its ask channels
- When the caller lists boards
- Then the response contains the agent’s board without that ask.
Requirement: The caller is the connection’s account, never a request field
Section titled “Requirement: The caller is the connection’s account, never a request field”Every CommsService RPC SHALL authorize against the account authenticated on
the connection, never an identity carried in the request (which would be
spoofable). On the local socket the caller is attributed to the bootstrap admin;
the network door’s token interceptor sets the real caller. No request message
carries a caller identity.
Requirement: Reads and writes are scoped to the caller’s visible set
Section titled “Requirement: Reads and writes are scoped to the caller’s visible set”The server SHALL scope every listing, read, and search to the channels, groups, and accounts the caller may see, enforced in the store (SQL), not at the RPC edge. An attached channel is visible to its members and to the anchor agent’s owner set. That visibility does not grant history access: message reads, search, topic reads, and writes require channel participation. TREE participation is the anchor agent, its owner, and the agents in the anchor’s subtree. A caller who does not participate SHALL receive no channel history by naming its id.
Scenario: A non-member lists a private channel’s messages
Section titled “Scenario: A non-member lists a private channel’s messages”- Given a channel the caller is not a member of
- When the caller calls
ListMessages(orSearchMessages) naming it - Then the result is empty, indistinguishable from an empty channel, rather than leaking the channel’s messages.
Requirement: A mutation is authorized against the caller’s membership
Section titled “Requirement: A mutation is authorized against the caller’s membership”Every mutating CommsService RPC SHALL authorize the caller in the store (SQL),
in the same transaction that performs the mutation, against the same visible set
that scopes a read — a caller who cannot see a channel SHALL NOT be able to
mutate it. Refusal SHALL be the not-found result, indistinguishable from a
nonexistent target, so a write probe never reveals that a private channel or
group exists. PostMessage, RespondToAsk, and UpdateChannelMembers SHALL
require the caller to be a member of the target channel; OpenAgentWorkspace
SHALL require the caller to be a member of the agent’s home channel.
Scenario: A non-member posts to a private channel
Section titled “Scenario: A non-member posts to a private channel”- Given a channel the caller is not a member of
- When the caller calls
PostMessage(orUpdateChannelMembers) naming it - Then the server returns not-found and persists nothing — the same result as a nonexistent channel, never a distinct not-authorized error.
Requirement: Creating a channel is authorized against its parent group
Section titled “Requirement: Creating a channel is authorized against its parent group”CreateChannel into a group SHALL be authorized against that parent group: the
caller SHALL own the group, or be an agent whose owning user owns the group, or
the group SHALL be visible to everyone (a shared group). A group the caller
neither owns nor may see — and an unknown group — SHALL both return not-found, so
a non-owner cannot probe which group ids exist. An ungrouped channel (a DM or
group-DM, or a top-level channel) has no parent group to authorize against; the
caller is a founding member by construction.
Scenario: A non-owner creates a channel in a private group
Section titled “Scenario: A non-owner creates a channel in a private group”- Given an owner-visibility group the caller neither owns nor belongs to
- When the caller calls
CreateChannelnaming that group - Then the server returns not-found, indistinguishable from a nonexistent group.
Requirement: Agent tools name a channel group by leaf name or path
Section titled “Requirement: Agent tools name a channel group by leaf name or path”Agent tools SHALL never pass group ids. create_channel and
create_channel_group SHALL name the parent group as a leaf name, or as a slash
path from the root when the name contains /; a group name SHALL NOT contain
/. A leading / SHALL anchor the path at the top level (/infra), and a
leading /~<handle>/ SHALL keep only top-level groups in that user’s namespace
(/~matt/eng). Resolution SHALL use the caller’s visible groups; unknown or
invisible groups SHALL return not-found. A ref that names more than one visible
group SHALL return invalid-argument, never pick one, and the error SHALL name
the anchored or owner-qualified ref for each match. The human RPC SHALL take ids
and reject the name fields.
A top-level group name SHALL be unique within one user’s namespace. An agent’s groups belong to its owning user’s namespace, so a user and that user’s agents cannot create same-named top-level groups. A nested group name SHALL be unique under its parent, whoever creates it: a shared group has no per-user namespaces, so a second child of the same name is a conflict.
Scenario: An ambiguous leaf name is rejected
Section titled “Scenario: An ambiguous leaf name is rejected”- Given two visible groups with the same leaf name
- When an agent tool names that group by its leaf name
- Then the call returns invalid-argument naming a ref for each group
Scenario: A slash path selects one of two same-named groups
Section titled “Scenario: A slash path selects one of two same-named groups”- Given two visible groups named
svcunder different parent groups - When an agent tool names one with its root slash path
- Then the call resolves to the group at that path
Scenario: An owner qualifier selects between two users’ top-level groups
Section titled “Scenario: An owner qualifier selects between two users’ top-level groups”- Given the caller’s own top-level group
engand another user’s shared top-level groupeng - When an agent tool names
/~<other-handle>/eng - Then the call resolves to the other user’s group
Scenario: An anchor selects the top-level group over a nested one
Section titled “Scenario: An anchor selects the top-level group over a nested one”- Given a top-level group
infraand a groupinfranested undereng - When an agent tool names
/infra - Then the call resolves to the top-level group, and
eng/infraresolves to the nested one
Scenario: A shared group holds one child of each name
Section titled “Scenario: A shared group holds one child of each name”- Given a shared group
pubwith a childinframade by one user - When another user creates
infraunderpub - Then the call returns a conflict, and
pub/infraresolves to the one child for both users