Skip to content
Open
Show file tree
Hide file tree
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
195 changes: 195 additions & 0 deletions rfcs/2026-03-20-smp-agent-web/2026-05-22-agent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
# Agent for Browser: Transpilation Breakdown

**Parent**: [SMP Client MVP](./2026-05-20-client-mvp.md)
**Depends on**: SMP Client (complete, 96 tests), encoding/encryption spike (complete)

## Rule

Every TypeScript function is a faithful transpilation of a specific Haskell function. Same name, same steps, same call chain. No inferences.

## Scope

The web widget JOINS connections (never creates addresses). It sends and receives messages. It handles the connection handshake. It does NOT create invitations, manage notifications, transfer files, or do remote control.

## Architecture difference from Haskell

Haskell agent uses SQLite + multiple background threads (subscriber, delivery workers, cleanup manager, NTF supervisor). Browser agent uses IndexedDB + event-driven architecture (WebSocket onmessage, Promises, no threads).

The protocol logic is identical. The concurrency model differs. The store interface differs. The transpilation focuses on the protocol logic.

## Breakdown into testable pieces

### Piece 1: Agent protocol types (Agent/Protocol.hs)

Already partially done (AgentMsgEnvelope, AgentMessage, APrivHeader, AMessage). What's missing for the handshake:

| Type | Haskell location | What's needed |
|------|-----------------|---------------|
| `SMPQueueInfo` | `Agent/Protocol.hs:1310-1327` | Binary encode/decode — version-dependent, complex |
| `SMPQueueUri` | `Agent/Protocol.hs:1344-1431` | Binary encode/decode + string encode/decode |
| `SMPQueueAddress` | `Agent/Protocol.hs:1350-1356` | `{smpServer, senderId, dhPublicKey, queueMode}` |
| `ConnectionRequestUri` | `Agent/Protocol.hs:1436-1441` | Binary encode/decode: `CRInvitationUri` + `CRContactUri` |
| `ConnReqUriData` | `Agent/Protocol.hs:1728-1734` | Binary encode/decode: `{crAgentVRange, crSmpQueues, crClientData}` |
| `SMPConfirmation` | `Agent/Protocol.hs:798-810` | Not wire-encoded — internal data structure for confirmation handling |
| `E2ERatchetParams` | `Crypto/Ratchet.hs:223-241` | Already have encode/decode in ratchet.ts — need to verify completeness |

**Test**: each encode/decode function tested byte-for-byte against Haskell via callNode.

### Piece 2: Connection handshake — joinConnection (Agent.hs)

The join flow, transpiled step by step:

```
joinConnection (Agent.hs:~1200-1300)
1. Parse ConnectionRequestUri (already have URI parsing)
2. Create RcvQueue on a selected server (newRcvQueue — Agent/Client.hs:1373)
- Generate X25519 DH keypair for queue
- Generate X25519/Ed25519 auth keypair
- Call createSMPQueue on SMP client
- Get back rcvId, sndId, srvDhKey
3. Store connection + queue in database
4. Generate X448 E2E ratchet params (generateRcvE2EParams — already have)
5. Build ConnInfo (profile data)
6. Encrypt ConnInfo with ratchet → encConnInfo
7. Build AgentConfirmation envelope
8. Wrap in ClientMessage + per-queue E2E encrypt → ClientMsgEnvelope
9. Send via SMP SEND to the contact address queue
10. Subscribe to own receive queue (SUB)
11. Return connection ID
```

Each step is independently testable. The full flow is an integration test.

**Key Haskell functions to transpile:**

| Function | File:lines | What it does |
|----------|-----------|--------------|
| `joinConnection` | `Agent.hs:~1200` | Top-level join |
| `joinConn` | `Agent.hs:~1230` | Internal join logic |
| `newRcvQueue` | `Agent/Client.hs:1373-1420` | Create queue on server |
| `sendConfirmation` | `Agent/Client.hs:1788-1794` | Encrypt+send confirmation |
| `sendInvitation` | `Agent/Client.hs:1796-1806` | Encrypt+send invitation |
| `mkAgentConfirmation` | `Agent.hs:~3700` | Build confirmation envelope |
| `agentCbEncrypt` | `Agent/Client.hs:2074-2082` | Per-queue E2E encrypt (already have) |

### Piece 3: Message processing — subscriber (Agent.hs)

Incoming message handling:

```
subscriber (Agent.hs:2912-2919)
→ reads from msgQ (populated by SMP client's onMessage callback)
→ processSMPTransmissions (Agent.hs:2997-3297)
→ for each transmission:
→ STEvent (server push MSG):
→ decryptClientMessage (per-queue E2E decrypt)
→ parse AgentMsgEnvelope
→ for AgentMsgEnvelope 'M':
→ agentRatchetDecrypt (double ratchet decrypt)
→ parse AgentMessage
→ dispatch on AMessage type:
→ HELLO: complete handshake
→ A_MSG body: deliver to user
→ A_RCVD: delivery receipt
→ QADD/QKEY/QUSE/QTEST: queue switching (skip for MVP)
→ EREADY: ratchet sync (skip for MVP)
```

**Key Haskell functions to transpile:**

| Function | File:lines | What it does |
|----------|-----------|--------------|
| `processSMPTransmissions` | `Agent.hs:2997-3297` | Top-level message dispatcher |
| `decryptClientMessage` | `Agent.hs:3282-3293` | Per-queue E2E decrypt + parse envelope |
| `agentRatchetDecrypt` | `Agent.hs:3757-3767` | Ratchet decrypt + store update |
| `helloMsg` | `Agent.hs:~3400` | Process HELLO (complete handshake) |
| `smpConfirmation` | `Agent.hs:~3500` | Process received confirmation |
| `smpInvitation` | `Agent.hs:~3600` | Process received invitation |

### Piece 4: Message sending — sendMessage (Agent.hs)

```
sendMessage (Agent.hs:~1500)
→ enqueueMessageB
→ agentRatchetEncryptHeader (get ratchet encrypt key)
→ rcEncryptMsg (encrypt message body)
→ store encrypted message in DB
→ createSndMsgDelivery (link to queue)
→ delivery worker sends via SMP SEND
```

**Key Haskell functions to transpile:**

| Function | File:lines | What it does |
|----------|-----------|--------------|
| `sendMessage` | `Agent.hs:~1500` | Top-level send |
| `enqueueMessageB` | `Agent.hs:~2020-2060` | Encode + encrypt + store |
| `agentRatchetEncrypt` | `Agent.hs:3742-3746` | Ratchet encrypt |
| `agentRatchetEncryptHeader` | `Agent.hs:3748-3754` | Ratchet encrypt header |
| `encodeAgentMsgStr` | `Agent.hs:2050-2054` | Encode AgentMessage to bytes |
| `runSmpQueueMsgDelivery` | `Agent.hs:2092-2220` | Delivery worker — read from DB, send via SMP |

### Piece 5: Message acknowledgment (Agent.hs)

```
ackMessage (Agent.hs:~1550)
→ withStore: mark message as acknowledged
→ send ACK to SMP server
→ optionally send delivery receipt (A_RCVD)
```

### Piece 6: Store interface (IndexedDB)

The agent uses ~50 distinct store operations. For the web widget MVP, we need:

| Store operation | What it does | Used by |
|----------------|--------------|---------|
| `createConnection` | Create connection record | joinConnection |
| `getConn` | Get connection by ID | all operations |
| `updateRcvIds` | Increment receive IDs | message processing |
| `createRcvMsg` | Store received message | message processing |
| `getRatchetForUpdate` | Get ratchet state for modify | encrypt/decrypt |
| `updateRatchet` | Store updated ratchet | encrypt/decrypt |
| `getSkippedMsgKeys` | Get skipped message keys | ratchet decrypt |
| `createSndMsg` | Store sent message | sendMessage |
| `createSndMsgDelivery` | Link message to queue | sendMessage |
| `getPendingQueueMsg` | Get next message to send | delivery worker |
| `updateSndMsgStatus` | Update delivery status | delivery worker |
| `deleteMsg` | Delete after ACK | ackMessage |

## Implementation order

1. **Piece 1**: Agent protocol types — `SMPQueueInfo`, `SMPQueueUri`, `ConnectionRequestUri`, `ConnReqUriData` encode/decode. Test each against Haskell.
2. **Piece 6**: Store interface — define TypeScript interface matching the needed operations. Implement with in-memory Map first (for testing), IndexedDB later.
3. **Piece 4**: Message sending — `sendMessage` + `agentRatchetEncrypt` + delivery. Test: encrypt in TS, decrypt in HS.
4. **Piece 3**: Message processing — `processSMPTransmissions` + `agentRatchetDecrypt`. Test: encrypt in HS, decrypt in TS.
5. **Piece 2**: Connection handshake — `joinConnection`. Test: TS joins, HS accepts, messages flow.
6. **Piece 5**: Message acknowledgment — `ackMessage`. Test: full roundtrip with ack.

Each piece is independently testable. Piece 1 uses callNode (no server). Pieces 3-5 use the SMP client REPL + real server. Piece 2 is the integration test that ties everything together.

## What to skip

| Feature | Why skip | Haskell functions |
|---------|----------|-------------------|
| Queue switching | Additive, not needed for MVP | `switchConnection`, QADD/QKEY/QUSE/QTEST handling |
| Ratchet sync | MVP shows error, suggests reconnecting | `synchronizeRatchet`, EREADY handling |
| Notifications | No webpush yet | All NTF functions |
| File transfer | Separate protocol | All XFTP functions |
| Remote control | Not in scope | All RC functions |
| Client notices | Server admin feature | `processClientNotices` |
| Cleanup manager | Can do manual cleanup | `cleanupManager` |
| Server management | Configured at init | `setProtocolServers`, `testProtocolServer` |
| Connection creation | Widget only joins | `createConnection`, short links |
| Delivery receipts | Can add later | A_RCVD handling, receipt sending |
| Multiple receive queues | Single queue per connection for MVP | Queue replacement logic |

## Files

| File | Action |
|------|--------|
| `smp-web/src/agent/protocol.ts` | Extend with SMPQueueInfo, ConnectionRequestUri, etc. |
| `smp-web/src/agent/store.ts` | New — store interface + in-memory implementation |
| `smp-web/src/agent/agent.ts` | New — agent logic: join, send, receive, ack |
| `smp-web/tests/agent-repl.ts` | New — agent REPL for testing (or extend client-repl) |
| `tests/SMPWebTests.hs` | Agent tests |
Loading
Loading