Skip to content

Commit 89eef03

Browse files
committed
chore: docs update
1 parent 1edaa8e commit 89eef03

5 files changed

Lines changed: 72 additions & 47 deletions

File tree

docs/express-best-practices.md

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -198,21 +198,25 @@ router.post(
198198

199199
### Reusable cross-cutting middleware
200200

201-
Factor recurring concerns (auth, token checks, request logging) into small middleware functions. The token guard is the model to follow:
201+
Factor recurring concerns (auth, token checks, request logging) into small middleware functions. The token guard is the model to follow — a factory parameterized by the connector credential, so the byte comparison lives in the credential and a route only says which far end it is for (`apps/server/src/middleware/requireCredential.ts`):
202202

203203
```ts
204-
// ✅ GOOD: a focused, reusable guard (apps/server/src/routes/internalMemory.ts)
205-
function requireInternalToken(
206-
req: Request,
207-
res: Response,
208-
next: NextFunction,
209-
): void {
210-
if (req.get("authorization") !== `Bearer ${INTERNAL_TOKEN}`) {
211-
res.status(401).json({ error: "Unauthorized" });
212-
return;
213-
}
214-
next();
204+
// ✅ GOOD: a focused, reusable guard parameterized by a credential
205+
export function requireCredential(
206+
credential: ConnectorCredential,
207+
): RequestHandler {
208+
return function guard(req: Request, res: Response, next: NextFunction): void {
209+
if (!credential.verify({ authorization: req.get("authorization") })) {
210+
res.status(401).json({ error: "Unauthorized" });
211+
return;
212+
}
213+
next();
214+
};
215215
}
216+
217+
// The internal API's guard is that factory bound to the Pi connector's credential
218+
// (apps/server/src/middleware/requireInternalToken.ts):
219+
export const requireInternalToken = requireCredential(piCredential);
216220
```
217221

218222
### Centralize error handling

docs/server/agent-communication.md

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -126,15 +126,30 @@ A2A participant when the registry resolves one:
126126
**How Prime reads a sub-agent's room:** "the room" is the shared session
127127
transcript persisted in the `SessionStore`. When Prime (or any agent) calls
128128
`read_room`, the tool hits `GET /internal/agents/room?sessionId=...`, and
129-
`handleRoom` returns the tail of `store.getMessages(sessionId)`:
129+
`handleRoom` returns the tail of `store.getMessages(sessionId)` — unless the
130+
caller names both `conversationId` and `participantId`, in which case it returns
131+
that Conversation projected through the reader's context policy (verbatim tail
132+
plus any digests; see [conversations.md](./conversations.md)):
130133
131-
```138:151:apps/server/src/routes/internalAgents.ts
134+
```327:347:apps/server/src/routes/internalAgents.ts
132135
async function handleRoom(
133136
store: SessionStore,
134137
query: RoomQuery,
135138
res: Response,
139+
context: ContextEngine | undefined,
140+
memberships: MembershipRegistry | undefined,
136141
): Promise<void> {
137-
...
142+
const limit = roomLimit(query.limit);
143+
if (context && memberships && query.conversationId && query.participantId) {
144+
const projected = await projectRoom(context, store, memberships, {
145+
sessionId: query.sessionId,
146+
conversationId: query.conversationId,
147+
participantId: query.participantId,
148+
limit,
149+
});
150+
res.json(projected);
151+
return;
152+
}
138153
const all = await store.getMessages(query.sessionId);
139154
res.json({ messages: all.slice(-limit) });
140155
}
@@ -268,7 +283,7 @@ Pi-RPC stdin; the reply is Pi-RPC stdout → manager handler → WS out.
268283
> `autoRelayToPrime: false`, so they react in isolation and only reach Prime when
269284
> they explicitly call `message_prime`.
270285
271-
## 5. Remote sub-agents (an alternative host)
286+
## 5. Remote sub-agents (an alternative connector)
272287
273288
A sub-agent does not have to be a local `pi` child. A **remote environment**
274289
can connect over a dedicated Socket.IO namespace (`/remote-env`) and host
@@ -302,6 +317,8 @@ rather than mis-routed. See [connectors.md](./connectors.md).
302317
another agent SDK.
303318
304319
The wire shapes live in `@tangent/shared/remoteSubagent.ts`, shared by both
305-
sides so the protocol cannot drift. Remote sub-agents are **not** revived after
306-
a server restart (`reviveSubagents` skips `host: "remote"` rows); they
307-
re-establish when their environment reconnects.
320+
sides so the protocol cannot drift. On a server restart, `ConnectorRegistry.revive`
321+
routes every non-terminal persisted row to its connector by recorded kind. The
322+
remote connector does not re-spawn a process it does not own: it restores the row
323+
as a `detached` tab, which becomes reachable again only when the environment
324+
reconnects and streams a turn.

docs/server/egress-and-security.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,11 @@ extensions present that token as `Authorization: Bearer <token>` on every call
1919
to the server's internal API.
2020

2121
`INTERNAL_TOKEN` ([config.ts](../../server/src/config.ts)) is a per-start
22-
`randomUUID()` unless pinned via env. Every internal router installs a middleware
23-
that rejects any request whose bearer does not match:
22+
`randomUUID()` unless pinned via env. Every internal router installs
23+
`requireInternalToken``requireCredential(piCredential)`
24+
([requireInternalToken.ts](../../server/src/middleware/requireInternalToken.ts)) —
25+
which rejects any request the Pi connector's credential does not verify. The byte
26+
comparison lives in the credential facet, not the route:
2427

2528
- `/internal/agents` ([internalAgents.ts](../../server/src/routes/internalAgents.ts))
2629
- `/internal/memory` ([internalMemory.ts](../../server/src/routes/internalMemory.ts))

docs/server/index.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -228,11 +228,12 @@ Every `pi` child is spawned with `TANGENT_INTERNAL_URL` and
228228
`TANGENT_INTERNAL_TOKEN` in its environment
229229
([apps/server/src/pi/piAgentManager.ts](../../apps/server/src/pi/piAgentManager.ts)).
230230
The extensions present that token as `Authorization: Bearer <token>` on every
231-
call to `/internal/*`. Each internal router rejects requests whose bearer does not
232-
match `INTERNAL_TOKEN`, which is a per-start `randomUUID()` unless pinned via env
233-
([apps/server/src/config.ts](../../apps/server/src/config.ts)). This keeps
234-
arbitrary local processes from driving a session's agents, memory, triggers, or
235-
resources. Details in [egress-and-security.md](./egress-and-security.md).
231+
call to `/internal/*`. Each internal router installs `requireInternalToken`
232+
(`requireCredential(piCredential)`), which rejects any request the Pi connector's
233+
credential does not verify against `INTERNAL_TOKEN` — a per-start `randomUUID()`
234+
unless pinned via env ([apps/server/src/config.ts](../../apps/server/src/config.ts)).
235+
This keeps arbitrary local processes from driving a session's agents, memory,
236+
triggers, or resources. Details in [egress-and-security.md](./egress-and-security.md).
236237

237238
## Configuration / environment surface
238239

docs/server/sessions-and-storage.md

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@ interface; the routes and socket handlers depend only on it. The production
2929
implementation is
3030
[`SqliteSessionStore`](../../apps/server/src/store/sqliteSessionStore.ts), backed by
3131
the shared `tangent.db` connection (`openDb()` applies pending drizzle migrations
32-
on startup). It composes the participant and resource stores so a roster write
33-
mirrors into the `participants` table and a pinned artifact into the resource
34-
catalog. `InMemorySessionStore` still exists but is a test fake, not the
32+
on startup). It reads and writes the roster on the `participants` table directly
33+
and composes the resource store so a pinned artifact is mirrored into the
34+
resource catalog. `InMemorySessionStore` still exists but is a test fake, not the
3535
production backend.
3636

3737
`createSession` allocates a `randomUUID()`, derives `rootPath =
@@ -56,24 +56,24 @@ The relational state lives in [db/schema.ts](../../apps/server/src/store/db/sche
5656
Chat history is deliberately **not** a table — it stays as JSONL on disk. Schema
5757
changes go exclusively through drizzle migrations; never alter tables ad-hoc.
5858

59-
| Table | Holds |
60-
| --------------------- | ---------------------------------------------------------------------------------------------- |
61-
| `sessions` | one row per session (mirrors the `Session` wire contract, plus `archived`, `user_identity`). |
62-
| `session_assets` | pinned artifacts, scoped to a session, oldest-first. |
63-
| `session_agents` | the agent roster (Prime + sub-agents) with connector facets; still the write authority today. |
64-
| `participants` | session-scoped actor identities (`human` / `agent` / `automation`), capabilities, presence. |
65-
| `conversations` | per-Conversation `seq` counter and its owning agent; maps a Conversation id to its transcript. |
66-
| `memberships` | a `(participant, conversation)` attachment: reaction spec, ingress, transcript visibility. |
67-
| `runs` | one unit of work by one participant: status, ingress, home conversation, external id, cursor. |
68-
| `resources` | the catalog: `file` / `memory` / `attachment` / `artifact`, pointing at bytes by `uri`. |
69-
| `resource_references` | a resource surfaced into a Conversation (surfacing + citation, not a filesystem gate). |
70-
| `resource_grants` | per-Membership refinement of a reference; default-permissive (an empty table changes nothing). |
71-
| `session_views` | when each user last opened a session. |
72-
73-
`session_agents` remains the write authority for the roster; `participants` is
74-
mirrored from it (and derived read-through for a session the backfill never
75-
touched). The deprecated `host` column survives beside the connector facets. A
76-
later cleanup folds these away.
59+
| Table | Holds |
60+
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
61+
| `sessions` | one row per session (mirrors the `Session` wire contract, plus `archived`, `user_identity`). |
62+
| `session_assets` | pinned artifacts, scoped to a session, oldest-first. |
63+
| `participants` | the roster: session-scoped actor identities (`human` / `agent` / `automation`), capabilities, presence, and each agent's connector facets + `agent_payload`. |
64+
| `conversations` | per-Conversation `seq` counter and its owning agent; maps a Conversation id to its transcript. |
65+
| `memberships` | a `(participant, conversation)` attachment: reaction spec, ingress, transcript visibility. |
66+
| `runs` | one unit of work by one participant: status, ingress, home conversation, external id, cursor. |
67+
| `resources` | the catalog: `file` / `memory` / `attachment` / `artifact`, pointing at bytes by `uri`. |
68+
| `resource_references` | a resource surfaced into a Conversation (surfacing + citation, not a filesystem gate). |
69+
| `resource_grants` | per-Membership refinement of a reference; default-permissive (an empty table changes nothing). |
70+
| `session_views` | when each user last opened a session. |
71+
72+
`participants` is the roster's only store. The old `session_agents` table (and
73+
its deprecated `host` column) is gone; `SessionStore.listAgents` /
74+
`recordAgent` / `setAgentStatus` now read and write `participants` directly,
75+
mapping an agent's `agent_payload` back to a `SessionAgent`. Connector kind is a
76+
facet column, defaulting to `pi-stdio` when unset.
7777

7878
---
7979

0 commit comments

Comments
 (0)