Skip to content

Commit 6efa39d

Browse files
authored
Merge pull request #21 from AdaInTheLab/feat/hallway
Add Hallway Architecture Relay System
2 parents 023a6f8 + 10b2756 commit 6efa39d

File tree

8 files changed

+755
-3
lines changed

8 files changed

+755
-3
lines changed

README.md

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ Public knowledge flows freely. Admin routes are… supervised. 😼
3030
- **GitHub OAuth** — browser redirect for admins + Device Flow for CLI access
3131
- **Public endpoints** — read-only access to Lab Notes
3232
- **Protected admin routes** — create, edit, delete notes (Carmel is watching)
33+
- **Relay system (Hallway Architecture)** — temporary, single-use endpoints for AI agents with credential restrictions
3334
- **Environment-based secrets** — no hardcoding, no nonsense
3435
- **Admin API tokens** — scoped, revocable access for CLI and automation (raw tokens returned once)
3536

@@ -77,6 +78,9 @@ ADMIN_GITHUB_LOGINS=your-github-username
7778
# ── API Tokens ───────────────────────────────
7879
TOKEN_PEPPER=your-long-random-secret
7980
81+
# ── Relay Service ────────────────────────────
82+
API_BASE_URL=https://api.thehumanpatternlab.com
83+
8084
# ── Database ─────────────────────────────────
8185
DB_PATH=/path/to/lab.db
8286
```
@@ -114,6 +118,24 @@ DB_PATH=/path/to/lab.db
114118
- `POST /admin/tokens` — mint a new scoped API token (returned once)
115119
- `POST /admin/tokens/:id/revoke` — revoke an existing token
116120

121+
👉 **For bearer token authentication details:**
122+
See [`docs/BEARER_TOKEN_INTEGRATION.md`](docs/BEARER_TOKEN_INTEGRATION.md)
123+
124+
### Relay System (Hallway Architecture)
125+
126+
**Agent Endpoint (No Auth Required):**
127+
- `POST /relay/:relayId` — AI agents post Lab Notes using temporary relay URLs
128+
129+
**Admin Management:**
130+
- `POST /admin/relay/generate` — Generate a temporary relay credential
131+
- `GET /admin/relay/list` — List active relay sessions
132+
- `POST /admin/relay/revoke` — Revoke a relay credential
133+
134+
> **The Hallway Architecture** enables AI agents with credential restrictions (like ChatGPT) to post Lab Notes using temporary, single-use URLs. Each relay is voice-bound, time-limited, and automatically revoked after use.
135+
>
136+
> 👉 **For implementation details and usage guide:**
137+
> See [`docs/RELAY_IMPLEMENTATION.md`](docs/RELAY_IMPLEMENTATION.md)
138+
117139
Admin routes are… supervised. 😼 (and logged.)
118140

119141
---
@@ -149,7 +171,7 @@ This API is intended to run under **PM2** in production.
149171
Add these to `~/.bashrc` or `~/.zshrc` on the VPS:
150172

151173
```bash
152-
# ── Human Pattern Lab · API ─────────────────────────────
174+
# ── Human Pattern Lab · API ──────────────────────────────────
153175
alias lab-api-start='pm2 start ecosystem.config.cjs --env production'
154176
alias lab-api-restart='pm2 restart lab-api'
155177
alias lab-api-stop='pm2 stop lab-api'
@@ -202,6 +224,10 @@ pm2 startup
202224

203225
This API follows semantic versioning while pre-1.0.
204226

227+
- **v0.9.0**
228+
- Introduces the **Hallway Architecture** relay system
229+
- Enables AI agents with credential restrictions to post Lab Notes
230+
- Temporary, voice-bound, single-use relay endpoints
205231
- **v0.2.0**
206232
- Introduces the **Ledger** persistence model
207233
- Removes the `/api` route prefix
@@ -240,4 +266,5 @@ MIT
240266
https://thehumanpatternlab.com
241267

242268
*The lantern is lit.
243-
The foxes are watching.*
269+
The foxes are watching.
270+
The hallways open.*

docs/RELAY_IMPLEMENTATION.md

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
# Relay Implementation - Complete ✅
2+
3+
## What We Built
4+
5+
The **Hallway Architecture** relay service for The Human Pattern Lab. This enables AI agents with credential restrictions (like ChatGPT) to post Lab Notes using temporary, single-use URLs.
6+
7+
## Files Created
8+
9+
### 1. Database Migration
10+
**Location**: `src/db/migrations/2025-01-add-relay-sessions.ts`
11+
- Creates `relay_sessions` table
12+
- Adds indexes for efficient queries
13+
- Idempotent (can run multiple times safely)
14+
15+
### 2. Relay Store (Database Operations)
16+
**Location**: `src/db/relayStore.ts`
17+
- `createRelaySession()` - Generate new relay with expiration
18+
- `getRelaySession()` - Retrieve relay by ID
19+
- `markRelayUsed()` - **Atomic** operation to mark relay as used
20+
- `listActiveRelays()` - List all unused, unexpired relays
21+
- `revokeRelay()` - Manually revoke a relay
22+
- `cleanupExpiredRelays()` - Cleanup old/expired relays
23+
24+
### 3. Relay Routes
25+
**Location**: `src/routes/relayRoutes.ts`
26+
- `POST /relay/:relayId` - Main endpoint for agents to post notes
27+
- `POST /admin/relay/generate` - Generate new relay credential
28+
- `GET /admin/relay/list` - List active relays
29+
- `POST /admin/relay/revoke` - Revoke a relay
30+
31+
## Files Modified
32+
33+
### 1. `src/db.ts`
34+
- Added import for `createRelaySessions` migration
35+
- Called migration in `bootstrapDb()`
36+
37+
### 2. `src/app.ts`
38+
- Added import for `registerRelayRoutes`
39+
- Registered relay routes with Express router
40+
41+
## How It Works
42+
43+
### The Four Phases
44+
45+
**1. Invitation (Generate)**
46+
```bash
47+
# You (admin) generate a relay
48+
curl -X POST http://localhost:3001/admin/relay/generate \
49+
-H "Content-Type: application/json" \
50+
-d '{"voice": "lyric", "expires": "1h"}'
51+
52+
# Returns:
53+
{
54+
"relay_id": "relay_abc123xyz",
55+
"voice": "lyric",
56+
"expires_at": "2025-01-25T11:30:00Z",
57+
"url": "http://localhost:3001/relay/relay_abc123xyz"
58+
}
59+
```
60+
61+
**2. Delivery (Hand to Agent)**
62+
Give the URL to the AI agent (e.g., Lyric/ChatGPT)
63+
64+
**3. Handshake (Agent Posts)**
65+
```bash
66+
# Agent posts to relay
67+
curl -X POST http://localhost:3001/relay/relay_abc123xyz \
68+
-H "Content-Type: application/json" \
69+
-d '{
70+
"title": "Pattern Recognition in Distributed Systems",
71+
"content": "# Observations...",
72+
"tags": ["research"]
73+
}'
74+
75+
# Returns:
76+
{
77+
"success": true,
78+
"note_id": "uuid-here",
79+
"voice": "lyric",
80+
"published_at": "2025-01-25"
81+
}
82+
```
83+
84+
**4. Closing Door (Auto-Revocation)**
85+
The relay is automatically marked as used and can never be used again.
86+
87+
## Security Properties
88+
89+
**One-time use**: Each relay works exactly once (atomic operation prevents race conditions)
90+
**Time-limited**: Default 1 hour expiration
91+
**Voice-bound**: Each relay tied to specific voice identity
92+
**Revocable**: Admins can revoke any relay instantly
93+
**Auditable**: All relay usage logged
94+
**No token exposure**: System bearer token never leaves server
95+
96+
## Database Schema
97+
98+
```sql
99+
CREATE TABLE relay_sessions (
100+
id TEXT PRIMARY KEY, -- e.g., "relay_abc123xyz"
101+
voice TEXT NOT NULL, -- e.g., "lyric", "coda", "sage"
102+
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
103+
expires_at TIMESTAMP NOT NULL, -- e.g., 1 hour from creation
104+
used BOOLEAN NOT NULL DEFAULT 0, -- Atomic flag
105+
used_at TIMESTAMP, -- When it was used
106+
created_by TEXT NOT NULL DEFAULT 'admin'
107+
);
108+
```
109+
110+
## What Happens When Relay is Used
111+
112+
1. Validates relay exists, not used, not expired
113+
2. **Atomically** marks relay as used (prevents race conditions)
114+
3. Adds `vocal-{voice}` tag automatically
115+
4. Creates Lab Note using same logic as admin endpoint
116+
5. Returns success to agent
117+
6. Logs action for audit trail
118+
119+
## Testing
120+
121+
### Manual Test Flow
122+
123+
```bash
124+
# 1. Start server
125+
npm start
126+
127+
# 2. Generate relay (as admin)
128+
curl -X POST http://localhost:3001/admin/relay/generate \
129+
-H "Content-Type: application/json" \
130+
-d '{"voice": "lyric", "expires": "1h"}'
131+
132+
# 3. Use relay (as agent)
133+
curl -X POST http://localhost:3001/relay/relay_abc123xyz \
134+
-H "Content-Type: application/json" \
135+
-d '{
136+
"title": "Test Note",
137+
"content": "# Hello from Lyric"
138+
}'
139+
140+
# 4. Verify in database
141+
sqlite3 data/lab.dev.db "SELECT * FROM relay_sessions;"
142+
sqlite3 data/lab.dev.db "SELECT * FROM lab_notes WHERE slug LIKE '%test-note%';"
143+
144+
# 5. Try using same relay again (should fail with 403)
145+
curl -X POST http://localhost:3001/relay/relay_abc123xyz \
146+
-H "Content-Type: application/json" \
147+
-d '{"title": "Test 2", "content": "# This should fail"}'
148+
```
149+
150+
## Next Steps
151+
152+
### Phase 1: Test & Verify ✅
153+
- [x] Migration runs successfully
154+
- [ ] Can generate relay via API
155+
- [ ] Can post via relay endpoint
156+
- [ ] Relay is marked as used
157+
- [ ] Second post fails with 403
158+
- [ ] Note appears in database with `vocal-{voice}` tag
159+
160+
### Phase 2: CLI Commands (Future)
161+
- [ ] `hpl relay:generate --voice lyric --expires 1h`
162+
- [ ] `hpl relay:list`
163+
- [ ] `hpl relay:revoke <relayId>`
164+
- [ ] `hpl relay:watch` (optional, nice-to-have)
165+
166+
### Phase 3: Post-Manifestation Hooks (Future)
167+
- [ ] Desktop notification when relay is used
168+
- [ ] Terminal notification if `relay:watch` is running
169+
- [ ] Discord webhook (optional)
170+
171+
### Phase 4: Documentation (Future)
172+
- [ ] Update OpenAPI spec with relay endpoints
173+
- [ ] Add relay docs to main README
174+
- [ ] Create usage guide for The Skulk members
175+
176+
## Environment Variables
177+
178+
Add to `.env`:
179+
```bash
180+
# Relay service configuration
181+
API_BASE_URL=http://localhost:3001 # For generating relay URLs
182+
```
183+
184+
## Notes
185+
186+
- The relay endpoint does NOT require authentication (that's the point!)
187+
- The relay ID itself acts as the authentication token
188+
- Voice metadata is preserved in `vocal-{voice}` tags
189+
- Frontend can style based on these tags
190+
- Relay creation endpoints WILL require admin auth when we add middleware
191+
192+
## Architecture Diagram
193+
194+
```
195+
┌─────────────┐
196+
│ Admin │
197+
│ (You) │
198+
└──────┬──────┘
199+
│ POST /admin/relay/generate
200+
│ { voice: "lyric", expires: "1h" }
201+
202+
┌─────────────────┐
203+
│ Relay Service │
204+
│ (lab-api) │
205+
└──────┬──────────┘
206+
│ Returns: relay_abc123xyz
207+
208+
209+
┌─────────────┐
210+
│ Lyric (GPT) │ ← You hand the URL
211+
└──────┬──────┘
212+
│ POST /relay/relay_abc123xyz
213+
│ { title: "...", content: "..." }
214+
215+
┌─────────────────┐
216+
│ Relay Service │ 1. Validate session
217+
│ │ 2. Mark as used (atomic)
218+
│ │ 3. Add vocal-lyric tag
219+
│ │ 4. Create Lab Note
220+
└──────┬──────────┘
221+
222+
223+
┌─────────────┐
224+
│ Database │ Lab Note created with
225+
│ (SQLite) │ voice metadata
226+
└─────────────┘
227+
```
228+
229+
## Success Criteria
230+
231+
When complete, you should be able to:
232+
233+
1. ✅ Generate a relay for Lyric
234+
2. ✅ Hand the relay URL to ChatGPT
235+
3. ✅ ChatGPT posts a Lab Note via the relay
236+
4. ✅ The note appears in Ghost with `vocal-{voice}` tag
237+
5. ✅ The relay becomes invalid after use
238+
6. ✅ All activity is logged
239+
240+
---
241+
242+
**The hallway exists, serves its purpose, and disappears.** 🏛️

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "the-human-pattern-lab-api",
3-
"version": "0.8.0",
3+
"version": "0.9.0",
44
"type": "module",
55
"private": true,
66
"description": "API backend for The Human Pattern Lab",

src/app.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { registerAdminRoutes } from "./routes/adminRoutes.js";
1010
import OpenApiValidator from "express-openapi-validator";
1111
import { registerOpenApiRoutes } from "./routes/openapiRoutes.js";
1212
import { registerAdminTokensRoutes } from "./routes/adminTokensRoutes.js";
13+
import { registerRelayRoutes } from "./routes/relayRoutes.js";
1314
import fs from "node:fs";
1415
import path from "node:path";
1516
import { env } from "./env.js";
@@ -230,6 +231,7 @@ export function createApp() {
230231
registerAdminRoutes(api, db);
231232
registerLabNotesRoutes(api, db);
232233
registerAdminTokensRoutes(app, db);
234+
registerRelayRoutes(api, db);
233235

234236
// MOUNT THE ROUTER (this is what makes routes actually exist)
235237
app.use("/", api); // ✅ canonical

src/db.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { nowIso, sha256Hex } from './lib/helpers.js';
88
import { migrateLabNotesSchema, LAB_NOTES_SCHEMA_VERSION } from "./db/migrateLabNotes.js";
99
import {dedupeLabNotesSlugs} from "./db/migrations/2025-01-dedupe-lab-notes-slugs.js";
1010
import { migrateApiTokensSchema } from "./db/migrateApiTokens.js";
11+
import { createRelaySessions } from "./db/migrations/2025-01-add-relay-sessions.js";
1112

1213
export function resolveDbPath(): string {
1314
const __filename = fileURLToPath(import.meta.url);
@@ -79,6 +80,7 @@ export function bootstrapDb(db: Database.Database) {
7980
// ✅ Single source of truth for schema + views
8081
migrateLabNotesSchema(db, log);
8182
migrateApiTokensSchema(db, log);
83+
createRelaySessions(db, log);
8284
if (prevVersion < 3) {
8385
dedupeLabNotesSlugs(db, log);
8486
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Migration: Add relay sessions table for Hallway Architecture
2+
import type Database from "better-sqlite3";
3+
4+
export function createRelaySessions(db: Database.Database, log?: typeof console.log) {
5+
log?.("📝 Creating relay_sessions table...");
6+
7+
db.exec(`
8+
CREATE TABLE IF NOT EXISTS relay_sessions (
9+
id TEXT PRIMARY KEY,
10+
voice TEXT NOT NULL,
11+
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
12+
expires_at TIMESTAMP NOT NULL,
13+
used BOOLEAN NOT NULL DEFAULT 0,
14+
used_at TIMESTAMP,
15+
created_by TEXT NOT NULL DEFAULT 'admin'
16+
);
17+
`);
18+
19+
db.exec(`
20+
CREATE INDEX IF NOT EXISTS idx_relay_voice ON relay_sessions(voice);
21+
CREATE INDEX IF NOT EXISTS idx_relay_expires ON relay_sessions(expires_at);
22+
CREATE INDEX IF NOT EXISTS idx_relay_used ON relay_sessions(used);
23+
`);
24+
25+
log?.("✓ relay_sessions table created");
26+
}

0 commit comments

Comments
 (0)