Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/subsystems/core-host.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ Work a member hands a Bot with `message_bot` from its group Session runs in a hi

### Secret scope defaults (#545)

A key the owner saves is for **all of the owner's Bots** (`scope: 'all'`) unless the owner narrows it: a card's "Only <Bot>", the Secrets page's *Chosen Bots* or *No Bot*. That holds for a connector key too (`GITHUB_TOKEN`, `COMPOSIO_API_KEY`): `connector-connect` no longer passes `scope: []`, and a reconnect keeps the scope the key already has. A scope the owner chose is stored with `scopeSet: true` (`secret-update`, `secret-set` with a `scope`, a card's "Only <Bot>", and a deleted Bot leaving a picked list), so an empty list stays "No Bot". On load, an empty list **without** `scopeSet` is the old connector default and becomes `'all'` once (written back by the host that holds the team; a reading host widens it in memory only). An empty list the owner picked before #545 carries no marker and is widened too: there is no way to tell it apart. `'all'` is the owner's own use, the same rule as the connector tools: a Bot's shell gets the keys only on the owner's own turn (`settled` in `turn-origin.js`; a turn someone else started gets none), and outside a known turn only while the Bot is shared with nobody (`rt.sharedWith`); a friend's Bot (`remote`) never does. A capability (`canUseSecret`) mounts for the whole Agent, so on a Bot shared with anyone it gets only a key whose list names that Bot, never an `'all'` key. Owner decision 2026-10-20 (#359, AGENTS.md hard rule 4).
A key the owner saves is for **all of the owner's Bots** (`scope: 'all'`) unless the owner narrows it: a card's "Only <Bot>", the Secrets page's *Chosen Bots* or *No Bot*. That holds for a connector key too (`GITHUB_TOKEN`, `COMPOSIO_API_KEY`): `connector-connect` no longer passes `scope: []`, and a reconnect keeps the scope the key already has. A scope the owner chose is stored with `scopeSet: true` (`secret-update`, `secret-set` with a `scope`, a card's "Only <Bot>", and a deleted Bot leaving a picked list), so an empty list stays "No Bot". On load, an empty list **without** `scopeSet` is the old connector default and becomes `'all'` once (written back by the host that holds the team; a reading host widens it in memory only). An empty list the owner picked before #545 carries no marker and is widened too: there is no way to tell it apart. `'all'` is the owner's own use, the same rule as the connector tools: a Bot's shell gets the keys only on the owner's own turn (`settled` in `turn-origin.js`; a turn someone else started gets none), and outside a known turn only while the Bot is shared with nobody (`rt.sharedWith`); a friend's Bot (`remote`) never does. The boundary as measured on 2026-10-21 (#581): a Bot's **own** scheduled turn already counts as the owner's while that Bot is not shared, so a routine whose timer fires in its own Session gets the keys; a **group** round inherits the initiator's verdict, so a round a *shared* Bot opened on a scheduled turn reaches every member as `other` — the shared initiator by design, and the unshared members only because the relay carries that verdict — and none of them gets the keys. The routines avoid it by scheduling themselves in their own, unshared Sessions; that is a runtime route, no code change (#581, closed). A capability (`canUseSecret`) mounts for the whole Agent, so on a Bot shared with anyone it gets only a key whose list names that Bot, never an `'all'` key. Owner decision 2026-10-20 (#359, AGENTS.md hard rule 4).

### Per-Bot connector access (#335, #369)

Expand All @@ -210,7 +210,7 @@ The connector tools are mounted once for the whole DSH profile, so who may use t
**Decision (the owner, 2026-10-18, #369; it supersedes #346's "off by default, never on a shared Bot").** A connected connector is **on by default for the owner's own use** of every Bot (new, existing after the upgrade, and Bots that had no record). A Bot that is shared (grant, friend, face room, room code) is **still usable by its owner**. What is off by default is **other people's use**: when someone else drives the Bot, the tools are off unless the owner turned on that Bot's separate "also when others use it" switch, which is never disabled by sharing.

- **Per-Bot records** (only what differs from the defaults is stored): `bot.connectors = { github?: false, composio?: false }` is an explicit off for the owner's own use (absent = on; a legacy `true` from #346 reads as on); `bot.connectorsShared = { github?: true, composio?: true }` is an explicit on for other people's use (absent = off), and it only counts while the owner's own use is on (others get what the owner has, never more). `bot-connectors { botId, connector, enabled, scope: 'owner' | 'shared' }` writes them: turning the owner's use off also drops the shared offer, and turning the shared offer on while the owner's use is off is refused. Sharing a Bot (a grant, a room) changes neither record.
- **Whose turn it is** (`src/host/turn-origin.js`) is read from the messages a Bot Session's turn took in (`user/message` events, tracked in `turns.js` on the Session's `live` entry), never from the Bot's own words. `owner`: the owner typed it (DSH's `source.kind: 'user'`, or a signed-in author equal to the Bot's `ownerId`), answered a question card, or a Bot of the **same owner** passed on a request that was itself the owner's (`deliver` stamps `source.origin`; `message_bot` passes the calling Session's own verdict, other callers the sender's running turns; a sender with no running turn, a Bot of another owner or a remote sender stamps `other`). `other`: a friend's message (any `source.remote`), a Bot of another owner, another signed-in member's line, a relay nobody stamped, and the end-of-turn replies, retries and deferred requests that carry their turn's verdict on. `scheduled`: a due scheduled task, which counts as the owner's only while the Bot is not shared (DSH gives the task no author and a Bot can create tasks). A message that carries no one's request (DSH's own notices, the handoff note of a part switch, a mention receipt) neither grants nor takes away. A turn's verdict is the strictest of its messages and **sticky**: a friend's line that lands in the owner's running turn makes the rest of that turn `other`, and the next turn starts clean. A turn with no message that has a verdict, no running turn at all, a Session whose Bot is gone, and a stamp the host did not write all count as **`other` (fail closed)**.
- **Whose turn it is** (`src/host/turn-origin.js`) is read from the messages a Bot Session's turn took in (`user/message` events, tracked in `turns.js` on the Session's `live` entry), never from the Bot's own words. `owner`: the owner typed it (DSH's `source.kind: 'user'`, or a signed-in author equal to the Bot's `ownerId`), answered a question card, or a Bot of the **same owner** passed on a request that was itself the owner's (`deliver` stamps `source.origin`; `message_bot` passes the calling Session's own verdict, other callers the sender's running turns; a sender with no running turn, a Bot of another owner or a remote sender stamps `other`). `other`: a friend's message (any `source.remote`), a Bot of another owner, another signed-in member's line, a relay nobody stamped, and the end-of-turn replies, retries and deferred requests that carry their turn's verdict on. `scheduled`: a due scheduled task, which counts as the owner's only while the Bot is not shared (DSH gives the task no author and a Bot can create tasks). A relay keeps the sender's verdict, so a group round opened by a shared Bot's scheduled turn is `other` for every member, and an unshared member of such a round does not get the owner's verdict either (#581; each routine schedules its own Session instead of one shared Bot scheduling for the group). A message that carries no one's request (DSH's own notices, the handoff note of a part switch, a mention receipt) neither grants nor takes away. A turn's verdict is the strictest of its messages and **sticky**: a friend's line that lands in the owner's running turn makes the rest of that turn `other`, and the next turn starts clean. A turn with no message that has a verdict, no running turn at all, a Session whose Bot is gone, and a stamp the host did not write all count as **`other` (fail closed)**.
- **Rooms.** A Bot's turn in a room is read through the room as it is at the moment of the call (`othersInRoom` in `reach.js`): the owner's only while nobody but the owner and the owner's own Bots are in it; a friend's Bot, another person, another owner's Bot or a room joined by code makes every turn there `other`, because their lines are part of what the Bot reads. A room with other people therefore needs the Bot's "also when others use it" switch, even when the owner typed the line. A room of the owner's own Bots is the owner's only when what started the round was: a Bot post carries its calling turn's verdict (`post_to_group` stamps `source.origin`, a late reply stamps its turn's), the round takes the strictest verdict of the Bot posts that opened it and passes it to every member turn (`runTurn` → `deliver`), a post from a friend's Bot or one nobody stamped makes the round someone else's, and the `report_back` message to the poster is the owner's only when the round was and nobody else is in the room. Without this, a friend driving a Main Bot could post into an owner-only room and have the members use the owner's connectors (found in review of #370). This is the main place where the owner meets the shared-use switch on their own Bot; the owner's own chat with the Bot is never affected by sharing.
- **Enforcement is two layers.** A `ctx.tools.guard` refuses every `mcp__github__*` / `mcp__composio__*` call from a Bot Session (its own chat, a group Session, an earlier part; a sub-agent Session is followed to its parent Bot Session and that Session's turn) that is not allowed. **It fails closed:** an Agent whose Bot cannot be determined (a parent Session that is gone, or a parent chain deeper than eight) gets no connector tools and every call is refused, so an unresolved sub-agent never reads as a plain owner Session; a friend's Bot (`bot.remote`) never gets them; only a chain that really ends at a Session of no Bot (the DSH Agent, a plain Session) keeps them. `agent.ctx.tools.restrict({ deny })` takes the same tools out of that Agent's tool list. The list is the courtesy and the guard the enforcement: an idle Session, or a turn whose first message is not in yet, is listed as for the owner's next message, and the guard still refuses any call whose turn is not the owner's. The mask is refreshed when the Agent is created, on every `resolveAgent` (delivery), when a switch changes, when a turn starts, takes in another message or ends (`connectorTurnChanged`, inside the Session's event, before the model sees the message), when who a Bot is shared with changes for any reason (a grant, a person, friend or other owner's Bot joining or leaving one of its rooms, a room joined by code: every save compares each Bot's sharing with the last time), and on `tools/change` (a connector that mounts or goes away later). DSH's hidden "Tool added/removed" notices for these tools are not drawn in the chat (`cells.js`).
- **Turns that move or restart.** A turn that moves to a fresh part mid-way (`parts.js`) carries its verdict in the resume message's `origin`, and the resume record in `inflight.json` notes it, so a turn resumed after a restart keeps the owner's tools only when it was the owner's (a record with no verdict resumes as `other`).
Expand Down
Loading