Skip to content
Merged
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
31 changes: 29 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Public knowledge flows freely. Admin routes are… supervised. 😼
- **GitHub OAuth** — browser redirect for admins + Device Flow for CLI access
- **Public endpoints** — read-only access to Lab Notes
- **Protected admin routes** — create, edit, delete notes (Carmel is watching)
- **Relay system (Hallway Architecture)** — temporary, single-use endpoints for AI agents with credential restrictions
- **Environment-based secrets** — no hardcoding, no nonsense
- **Admin API tokens** — scoped, revocable access for CLI and automation (raw tokens returned once)

Expand Down Expand Up @@ -77,6 +78,9 @@ ADMIN_GITHUB_LOGINS=your-github-username
# ── API Tokens ───────────────────────────────
TOKEN_PEPPER=your-long-random-secret

# ── Relay Service ────────────────────────────
API_BASE_URL=https://api.thehumanpatternlab.com

# ── Database ─────────────────────────────────
DB_PATH=/path/to/lab.db
```
Expand Down Expand Up @@ -114,6 +118,24 @@ DB_PATH=/path/to/lab.db
- `POST /admin/tokens` — mint a new scoped API token (returned once)
- `POST /admin/tokens/:id/revoke` — revoke an existing token

👉 **For bearer token authentication details:**
See [`docs/BEARER_TOKEN_INTEGRATION.md`](docs/BEARER_TOKEN_INTEGRATION.md)

### Relay System (Hallway Architecture)

**Agent Endpoint (No Auth Required):**
- `POST /relay/:relayId` — AI agents post Lab Notes using temporary relay URLs

**Admin Management:**
- `POST /admin/relay/generate` — Generate a temporary relay credential
- `GET /admin/relay/list` — List active relay sessions
- `POST /admin/relay/revoke` — Revoke a relay credential

> **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.
>
> 👉 **For implementation details and usage guide:**
> See [`docs/RELAY_IMPLEMENTATION.md`](docs/RELAY_IMPLEMENTATION.md)

Admin routes are… supervised. 😼 (and logged.)

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

```bash
# ── Human Pattern Lab · API ─────────────────────────────
# ── Human Pattern Lab · API ──────────────────────────────────
alias lab-api-start='pm2 start ecosystem.config.cjs --env production'
alias lab-api-restart='pm2 restart lab-api'
alias lab-api-stop='pm2 stop lab-api'
Expand Down Expand Up @@ -202,6 +224,10 @@ pm2 startup

This API follows semantic versioning while pre-1.0.

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

*The lantern is lit.
The foxes are watching.*
The foxes are watching.
The hallways open.*
242 changes: 242 additions & 0 deletions docs/RELAY_IMPLEMENTATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
# Relay Implementation - Complete ✅

## What We Built

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.

## Files Created

### 1. Database Migration
**Location**: `src/db/migrations/2025-01-add-relay-sessions.ts`
- Creates `relay_sessions` table
- Adds indexes for efficient queries
- Idempotent (can run multiple times safely)

### 2. Relay Store (Database Operations)
**Location**: `src/db/relayStore.ts`
- `createRelaySession()` - Generate new relay with expiration
- `getRelaySession()` - Retrieve relay by ID
- `markRelayUsed()` - **Atomic** operation to mark relay as used
- `listActiveRelays()` - List all unused, unexpired relays
- `revokeRelay()` - Manually revoke a relay
- `cleanupExpiredRelays()` - Cleanup old/expired relays

### 3. Relay Routes
**Location**: `src/routes/relayRoutes.ts`
- `POST /relay/:relayId` - Main endpoint for agents to post notes
- `POST /admin/relay/generate` - Generate new relay credential
- `GET /admin/relay/list` - List active relays
- `POST /admin/relay/revoke` - Revoke a relay

## Files Modified

### 1. `src/db.ts`
- Added import for `createRelaySessions` migration
- Called migration in `bootstrapDb()`

### 2. `src/app.ts`
- Added import for `registerRelayRoutes`
- Registered relay routes with Express router

## How It Works

### The Four Phases

**1. Invitation (Generate)**
```bash
# You (admin) generate a relay
curl -X POST http://localhost:3001/admin/relay/generate \
-H "Content-Type: application/json" \
-d '{"voice": "lyric", "expires": "1h"}'

# Returns:
{
"relay_id": "relay_abc123xyz",
"voice": "lyric",
"expires_at": "2025-01-25T11:30:00Z",
"url": "http://localhost:3001/relay/relay_abc123xyz"
}
```

**2. Delivery (Hand to Agent)**
Give the URL to the AI agent (e.g., Lyric/ChatGPT)

**3. Handshake (Agent Posts)**
```bash
# Agent posts to relay
curl -X POST http://localhost:3001/relay/relay_abc123xyz \
-H "Content-Type: application/json" \
-d '{
"title": "Pattern Recognition in Distributed Systems",
"content": "# Observations...",
"tags": ["research"]
}'

# Returns:
{
"success": true,
"note_id": "uuid-here",
"voice": "lyric",
"published_at": "2025-01-25"
}
```

**4. Closing Door (Auto-Revocation)**
The relay is automatically marked as used and can never be used again.

## Security Properties

✅ **One-time use**: Each relay works exactly once (atomic operation prevents race conditions)
✅ **Time-limited**: Default 1 hour expiration
✅ **Voice-bound**: Each relay tied to specific voice identity
✅ **Revocable**: Admins can revoke any relay instantly
✅ **Auditable**: All relay usage logged
✅ **No token exposure**: System bearer token never leaves server

## Database Schema

```sql
CREATE TABLE relay_sessions (
id TEXT PRIMARY KEY, -- e.g., "relay_abc123xyz"
voice TEXT NOT NULL, -- e.g., "lyric", "coda", "sage"
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP NOT NULL, -- e.g., 1 hour from creation
used BOOLEAN NOT NULL DEFAULT 0, -- Atomic flag
used_at TIMESTAMP, -- When it was used
created_by TEXT NOT NULL DEFAULT 'admin'
);
```

## What Happens When Relay is Used

1. Validates relay exists, not used, not expired
2. **Atomically** marks relay as used (prevents race conditions)
3. Adds `vocal-{voice}` tag automatically
4. Creates Lab Note using same logic as admin endpoint
5. Returns success to agent
6. Logs action for audit trail

## Testing

### Manual Test Flow

```bash
# 1. Start server
npm start

# 2. Generate relay (as admin)
curl -X POST http://localhost:3001/admin/relay/generate \
-H "Content-Type: application/json" \
-d '{"voice": "lyric", "expires": "1h"}'

# 3. Use relay (as agent)
curl -X POST http://localhost:3001/relay/relay_abc123xyz \
-H "Content-Type: application/json" \
-d '{
"title": "Test Note",
"content": "# Hello from Lyric"
}'

# 4. Verify in database
sqlite3 data/lab.dev.db "SELECT * FROM relay_sessions;"
sqlite3 data/lab.dev.db "SELECT * FROM lab_notes WHERE slug LIKE '%test-note%';"

# 5. Try using same relay again (should fail with 403)
curl -X POST http://localhost:3001/relay/relay_abc123xyz \
-H "Content-Type: application/json" \
-d '{"title": "Test 2", "content": "# This should fail"}'
```

## Next Steps

### Phase 1: Test & Verify ✅
- [x] Migration runs successfully
- [ ] Can generate relay via API
- [ ] Can post via relay endpoint
- [ ] Relay is marked as used
- [ ] Second post fails with 403
- [ ] Note appears in database with `vocal-{voice}` tag

### Phase 2: CLI Commands (Future)
- [ ] `hpl relay:generate --voice lyric --expires 1h`
- [ ] `hpl relay:list`
- [ ] `hpl relay:revoke <relayId>`
- [ ] `hpl relay:watch` (optional, nice-to-have)

### Phase 3: Post-Manifestation Hooks (Future)
- [ ] Desktop notification when relay is used
- [ ] Terminal notification if `relay:watch` is running
- [ ] Discord webhook (optional)

### Phase 4: Documentation (Future)
- [ ] Update OpenAPI spec with relay endpoints
- [ ] Add relay docs to main README
- [ ] Create usage guide for The Skulk members

## Environment Variables

Add to `.env`:
```bash
# Relay service configuration
API_BASE_URL=http://localhost:3001 # For generating relay URLs
```

## Notes

- The relay endpoint does NOT require authentication (that's the point!)
- The relay ID itself acts as the authentication token
- Voice metadata is preserved in `vocal-{voice}` tags
- Frontend can style based on these tags
- Relay creation endpoints WILL require admin auth when we add middleware

## Architecture Diagram

```
┌─────────────┐
│ Admin │
│ (You) │
└──────┬──────┘
│ POST /admin/relay/generate
│ { voice: "lyric", expires: "1h" }
┌─────────────────┐
│ Relay Service │
│ (lab-api) │
└──────┬──────────┘
│ Returns: relay_abc123xyz
┌─────────────┐
│ Lyric (GPT) │ ← You hand the URL
└──────┬──────┘
│ POST /relay/relay_abc123xyz
│ { title: "...", content: "..." }
┌─────────────────┐
│ Relay Service │ 1. Validate session
│ │ 2. Mark as used (atomic)
│ │ 3. Add vocal-lyric tag
│ │ 4. Create Lab Note
└──────┬──────────┘
┌─────────────┐
│ Database │ Lab Note created with
│ (SQLite) │ voice metadata
└─────────────┘
```

## Success Criteria

When complete, you should be able to:

1. ✅ Generate a relay for Lyric
2. ✅ Hand the relay URL to ChatGPT
3. ✅ ChatGPT posts a Lab Note via the relay
4. ✅ The note appears in Ghost with `vocal-{voice}` tag
5. ✅ The relay becomes invalid after use
6. ✅ All activity is logged

---

**The hallway exists, serves its purpose, and disappears.** 🏛️
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "the-human-pattern-lab-api",
"version": "0.8.0",
"version": "0.9.0",
"type": "module",
"private": true,
"description": "API backend for The Human Pattern Lab",
Expand Down
2 changes: 2 additions & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { registerAdminRoutes } from "./routes/adminRoutes.js";
import OpenApiValidator from "express-openapi-validator";
import { registerOpenApiRoutes } from "./routes/openapiRoutes.js";
import { registerAdminTokensRoutes } from "./routes/adminTokensRoutes.js";
import { registerRelayRoutes } from "./routes/relayRoutes.js";
import fs from "node:fs";
import path from "node:path";
import { env } from "./env.js";
Expand Down Expand Up @@ -230,6 +231,7 @@ export function createApp() {
registerAdminRoutes(api, db);
registerLabNotesRoutes(api, db);
registerAdminTokensRoutes(app, db);
registerRelayRoutes(api, db);

// MOUNT THE ROUTER (this is what makes routes actually exist)
app.use("/", api); // ✅ canonical
Expand Down
2 changes: 2 additions & 0 deletions src/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { nowIso, sha256Hex } from './lib/helpers.js';
import { migrateLabNotesSchema, LAB_NOTES_SCHEMA_VERSION } from "./db/migrateLabNotes.js";
import {dedupeLabNotesSlugs} from "./db/migrations/2025-01-dedupe-lab-notes-slugs.js";
import { migrateApiTokensSchema } from "./db/migrateApiTokens.js";
import { createRelaySessions } from "./db/migrations/2025-01-add-relay-sessions.js";

export function resolveDbPath(): string {
const __filename = fileURLToPath(import.meta.url);
Expand Down Expand Up @@ -79,6 +80,7 @@ export function bootstrapDb(db: Database.Database) {
// ✅ Single source of truth for schema + views
migrateLabNotesSchema(db, log);
migrateApiTokensSchema(db, log);
createRelaySessions(db, log);
if (prevVersion < 3) {
dedupeLabNotesSlugs(db, log);
}
Expand Down
26 changes: 26 additions & 0 deletions src/db/migrations/2025-01-add-relay-sessions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Migration: Add relay sessions table for Hallway Architecture
import type Database from "better-sqlite3";

export function createRelaySessions(db: Database.Database, log?: typeof console.log) {
log?.("📝 Creating relay_sessions table...");

db.exec(`
CREATE TABLE IF NOT EXISTS relay_sessions (
id TEXT PRIMARY KEY,
voice TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP NOT NULL,
used BOOLEAN NOT NULL DEFAULT 0,
used_at TIMESTAMP,
created_by TEXT NOT NULL DEFAULT 'admin'
);
`);

db.exec(`
CREATE INDEX IF NOT EXISTS idx_relay_voice ON relay_sessions(voice);
CREATE INDEX IF NOT EXISTS idx_relay_expires ON relay_sessions(expires_at);
CREATE INDEX IF NOT EXISTS idx_relay_used ON relay_sessions(used);
`);

log?.("✓ relay_sessions table created");
}
Loading