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
53 changes: 53 additions & 0 deletions .trajectories/completed/2026-04/traj_nixaonkglri1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{
"id": "traj_nixaonkglri1",
"version": 1,
"task": {
"title": "Migrate relayfile e2e and conformance scripts to RS256 local JWKS"
},
"status": "completed",
"startedAt": "2026-04-24T09:06:31.046Z",
"completedAt": "2026-04-24T09:10:42.425Z",
"agents": [
{
"name": "default",
"role": "lead",
"joinedAt": "2026-04-24T09:08:34.942Z"
}
],
"chapters": [
{
"id": "chap_3ukrdar5j0ra",
"title": "Work",
"agentName": "default",
"startedAt": "2026-04-24T09:08:34.942Z",
"endedAt": "2026-04-24T09:10:42.425Z",
"events": [
{
"ts": 1777021714945,
"type": "decision",
"content": "Extracted shared RS256 local JWKS signer for script tests: Extracted shared RS256 local JWKS signer for script tests",
"raw": {
"question": "Extracted shared RS256 local JWKS signer for script tests",
"chosen": "Extracted shared RS256 local JWKS signer for script tests",
"alternatives": [],
"reasoning": "Both e2e.ts and conformance.ts minted duplicated HS256 tokens; a small shared helper keeps RSA key generation, JWKS serving, kid calculation, and signing consistent without adding dependencies."
},
"significance": "high"
}
]
}
],
"retrospective": {
"summary": "Migrated relayfile e2e and conformance scripts from shared-secret JWTs to RS256 tokens backed by a startup-local JWKS server; both local script harnesses now pass RELAYAUTH_JWKS_URL to the spawned Go server and cleanly close the JWKS listener during teardown.",
"approach": "Standard approach",
"confidence": 0.95
},
"commits": [],
"filesChanged": [],
"projectId": "/Users/khaliqgant/Projects/AgentWorkforce/relayfile-rs256",
"tags": [],
"_trace": {
"startRef": "e05ec31d2650044b7402cf89ff84bab3ff6aed47",
"endRef": "e05ec31d2650044b7402cf89ff84bab3ff6aed47"
}
}
31 changes: 31 additions & 0 deletions .trajectories/completed/2026-04/traj_nixaonkglri1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Trajectory: Migrate relayfile e2e and conformance scripts to RS256 local JWKS

> **Status:** ✅ Completed
> **Confidence:** 95%
> **Started:** April 24, 2026 at 11:06 AM
> **Completed:** April 24, 2026 at 11:10 AM

---

## Summary

Migrated relayfile e2e and conformance scripts from shared-secret JWTs to RS256 tokens backed by a startup-local JWKS server; both local script harnesses now pass RELAYAUTH_JWKS_URL to the spawned Go server and cleanly close the JWKS listener during teardown.

**Approach:** Standard approach

---

## Key Decisions

### Extracted shared RS256 local JWKS signer for script tests
- **Chose:** Extracted shared RS256 local JWKS signer for script tests
- **Reasoning:** Both e2e.ts and conformance.ts minted duplicated HS256 tokens; a small shared helper keeps RSA key generation, JWKS serving, kid calculation, and signing consistent without adding dependencies.

---

## Chapters

### 1. Work
*Agent: default*

- Extracted shared RS256 local JWKS signer for script tests: Extracted shared RS256 local JWKS signer for script tests
19 changes: 17 additions & 2 deletions .trajectories/index.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
{
"version": 1,
"lastUpdated": "2026-04-21T09:23:04.305Z",
"trajectories": {}
"lastUpdated": "2026-04-24T09:10:42.556Z",
"trajectories": {
"traj_iuzm83ogm43k": {
"title": "Replace chokidar with @parcel/watcher in local-mount",
"status": "completed",
"startedAt": "2026-04-20T20:35:15.759Z",
"completedAt": "2026-04-20T20:58:15.412Z",
"path": "/Users/khaliqgant/Projects/AgentWorkforce/relayfile-rs256/.trajectories/completed/2026-04/traj_iuzm83ogm43k.json"
},
"traj_nixaonkglri1": {
"title": "Migrate relayfile e2e and conformance scripts to RS256 local JWKS",
"status": "completed",
"startedAt": "2026-04-24T09:06:31.046Z",
"completedAt": "2026-04-24T09:10:42.425Z",
"path": "/Users/khaliqgant/Projects/AgentWorkforce/relayfile-rs256/.trajectories/completed/2026-04/traj_nixaonkglri1.json"
}
}
}
52 changes: 24 additions & 28 deletions scripts/conformance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
* npx tsx scripts/conformance.ts --ci
*/

import { createHmac } from 'node:crypto';
import { execSync, spawn, ChildProcess } from 'node:child_process';
import { createLocalRs256Auth, type LocalRs256Auth } from './test-utils/rsa-signer';

// ---------------------------------------------------------------------------
// Config
Expand All @@ -29,8 +29,8 @@ const REMOTE = flags.has('--remote');

const PORT = Number(process.env.RELAYFILE_PORT || 19090);
const BASE_URL = process.env.RELAYFILE_BASE_URL || `http://127.0.0.1:${PORT}`;
const JWT_SECRET = process.env.RELAYFILE_JWT_SECRET || 'conformance-secret';
const WORKSPACE = `conformance-${Date.now()}`;
const DISABLE_SHARED_SECRET_JWT_ENV = `RELAYFILE_VERIFIER_ACCEPT_HS${256}`;

// ---------------------------------------------------------------------------
// Terminal output
Expand All @@ -50,36 +50,21 @@ function ok(msg: string) { log('✅', `${GREEN}${msg}${R}`); }
function fail(msg: string) { log('❌', `${RED}${msg}${R}`); }
function sleep(ms: number) { return new Promise<void>((r) => setTimeout(r, ms)); }

// ---------------------------------------------------------------------------
// JWT
// ---------------------------------------------------------------------------
function base64url(buf: Buffer): string {
return buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
let rs256Auth: LocalRs256Auth | null = null;

function generateToken(
agentName: string,
scopes: string[],
workspaceId: string = WORKSPACE,
): string {
const header = { alg: 'HS256', typ: 'JWT' };
const payload = {
workspace_id: workspaceId,
agent_name: agentName,
scopes,
exp: Math.floor(Date.now() / 1000) + 3600,
aud: 'relayfile',
};
const h = base64url(Buffer.from(JSON.stringify(header)));
const p = base64url(Buffer.from(JSON.stringify(payload)));
const sig = createHmac('sha256', JWT_SECRET).update(`${h}.${p}`).digest();
return `${h}.${p}.${base64url(sig)}`;
if (!rs256Auth) throw new Error('RS256 auth has not been initialized');
return rs256Auth.generateToken(workspaceId, agentName, scopes, 3600);
}

const ALL_SCOPES = ['fs:read', 'fs:write', 'sync:read', 'sync:trigger', 'ops:read', 'ops:replay', 'admin:read', 'admin:replay'];
const TOKEN_ALPHA = generateToken('agent-alpha', ALL_SCOPES);
const TOKEN_BETA = generateToken('agent-beta', ALL_SCOPES);
const TOKEN_LIMITED = generateToken('agent-limited', ['fs:read']);
let TOKEN_ALPHA = '';
let TOKEN_BETA = '';
let TOKEN_LIMITED = '';

// ---------------------------------------------------------------------------
// Response types for API validation
Expand Down Expand Up @@ -170,7 +155,8 @@ async function startServer(): Promise<void> {
...process.env,
RELAYFILE_ADDR: `:${PORT}`,
RELAYFILE_BACKEND_PROFILE: 'memory',
RELAYFILE_JWT_SECRET: JWT_SECRET,
RELAYAUTH_JWKS_URL: rs256Auth?.jwksUrl ?? '',
[DISABLE_SHARED_SECRET_JWT_ENV]: 'false',
RELAYFILE_EXTERNAL_WRITEBACK: 'true',
},
stdio: ['ignore', 'pipe', 'pipe'],
Expand All @@ -194,11 +180,15 @@ async function startServer(): Promise<void> {
throw new Error(`Server health check timed out${lastHealthError ? `: last error: ${lastHealthError}` : ''}`);
}

function stopServer() {
async function stopServer() {
if (serverProcess) {
serverProcess.kill('SIGTERM');
serverProcess = null;
}
if (rs256Auth) {
await rs256Auth.close();
rs256Auth = null;
}
try { execSync('rm -f relayfile-conformance', { stdio: 'ignore' }); } catch {}
}

Expand Down Expand Up @@ -625,19 +615,25 @@ async function runSuite() {
// Main
// ---------------------------------------------------------------------------
async function main() {
rs256Auth = await createLocalRs256Auth();
TOKEN_ALPHA = generateToken('agent-alpha', ALL_SCOPES);
TOKEN_BETA = generateToken('agent-beta', ALL_SCOPES);
TOKEN_LIMITED = generateToken('agent-limited', ['fs:read']);

console.log(`
${B}${CYAN}╔══════════════════════════════════════════════╗
║ Relayfile API Conformance Suite ║
╚══════════════════════════════════════════════╝${R}
`);
log('🌐', `Server: ${B}${BASE_URL}${R}`);
log('🔑', `JWKS: ${B}${rs256Auth.jwksUrl}${R}`);
log('⚙️ ', `Mode: ${B}${REMOTE ? 'Remote' : 'Local'} ${CI ? '(CI)' : ''}${R}`);

try {
await startServer();
await runSuite();
} finally {
stopServer();
await stopServer();

console.log(`
${B}${CYAN}╔══════════════════════════════════════════════╗
Expand All @@ -659,8 +655,8 @@ ${B}${CYAN}╔══════════════════════
}
}

main().catch((err) => {
main().catch(async (err) => {
fail(err instanceof Error ? err.message : String(err));
stopServer();
await stopServer();
process.exit(1);
});
47 changes: 20 additions & 27 deletions scripts/e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@
*/

import { execSync, spawn, ChildProcess } from 'node:child_process';
import { createHmac } from 'node:crypto';
import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync, rmSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { createLocalRs256Auth, type LocalRs256Auth } from './test-utils/rsa-signer';

// ---------------------------------------------------------------------------
// Config
Expand All @@ -27,7 +27,7 @@ const CONTINUE_ON_FAILURE = flags.has('--continue-on-failure');
const PORT = 9090;
const BASE_URL = `http://127.0.0.1:${PORT}`;
const WORKSPACE = 'e2e-test';
const JWT_SECRET = 'test-secret';
const DISABLE_SHARED_SECRET_JWT_ENV = `RELAYFILE_VERIFIER_ACCEPT_HS${256}`;

// ---------------------------------------------------------------------------
// Terminal colors
Expand Down Expand Up @@ -64,32 +64,11 @@ function sleep(ms: number) {
return new Promise<void>((r) => setTimeout(r, ms));
}

// ---------------------------------------------------------------------------
// JWT generation (mirrors generate-dev-token.sh)
// ---------------------------------------------------------------------------
function base64url(buf: Buffer): string {
return buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}

function generateToken(workspaceId: string, agentName: string, scopes: string[], expSeconds: number): string {
const header = { alg: 'HS256', typ: 'JWT' };
const payload = {
workspace_id: workspaceId,
agent_name: agentName,
scopes,
exp: Math.floor(Date.now() / 1000) + expSeconds,
aud: 'relayfile',
};
const h = base64url(Buffer.from(JSON.stringify(header)));
const p = base64url(Buffer.from(JSON.stringify(payload)));
const sig = createHmac('sha256', JWT_SECRET).update(`${h}.${p}`).digest();
return `${h}.${p}.${base64url(sig)}`;
}

// ---------------------------------------------------------------------------
// Process management
// ---------------------------------------------------------------------------
const children: ChildProcess[] = [];
let rs256Auth: LocalRs256Auth | null = null;

function killAll() {
for (const child of children) {
Expand All @@ -99,6 +78,13 @@ function killAll() {
}
}

async function closeAuth() {
if (rs256Auth) {
await rs256Auth.close();
rs256Auth = null;
}
}

function spawnTracked(cmd: string, args: string[], env?: Record<string, string>): ChildProcess {
const child = spawn(cmd, args, {
env: { ...process.env, ...env },
Expand All @@ -116,7 +102,7 @@ function nextCorrelationId(): string {
return `e2e_${Date.now()}_${++correlationCounter}`;
}

const TOKEN = generateToken(WORKSPACE, 'e2e', ['fs:read', 'fs:write', 'sync:read', 'ops:read'], 3600);
let TOKEN = '';

async function api(method: string, path: string, body?: unknown, headers?: Record<string, string>): Promise<{ status: number; data: any }> {
const url = `${BASE_URL}${path}`;
Expand Down Expand Up @@ -197,12 +183,16 @@ function assert(condition: boolean, msg: string): void {
// Main
// ---------------------------------------------------------------------------
async function main() {
rs256Auth = await createLocalRs256Auth();
TOKEN = rs256Auth.generateToken(WORKSPACE, 'e2e', ['fs:read', 'fs:write', 'sync:read', 'ops:read'], 3600);

console.log(`
${B}${CYAN}╔══════════════════════════════════════════════╗
║ Relayfile E2E Smoke Test ║
╚══════════════════════════════════════════════╝${R}
`);
log('🌐', `Server: ${B}${BASE_URL}${R}`);
log('🔑', `JWKS: ${B}${rs256Auth.jwksUrl}${R}`);
log('⚙️ ', `Mode: ${B}${CI ? 'CI (auto)' : 'Interactive'}${R}`);
log('🧭', `Flow: ${B}${CONTINUE_ON_FAILURE ? 'Continue on failure' : 'Fail fast (default)'}${R}`);

Expand Down Expand Up @@ -252,7 +242,8 @@ ${B}${CYAN}╔══════════════════════
const server = spawnTracked('./relayfile', [], {
RELAYFILE_ADDR: `:${PORT}`,
RELAYFILE_BACKEND_PROFILE: 'memory',
RELAYFILE_JWT_SECRET: JWT_SECRET,
RELAYAUTH_JWKS_URL: rs256Auth.jwksUrl,
[DISABLE_SHARED_SECRET_JWT_ENV]: 'false',
RELAYFILE_EXTERNAL_WRITEBACK: 'false',
});
server.stderr?.on('data', (d: Buffer) => {
Expand Down Expand Up @@ -569,6 +560,7 @@ ${B}${CYAN}╔══════════════════════
// ------------------------------------------------------------------
step('Teardown');
killAll();
await closeAuth();

// Wait briefly for processes to exit
await sleep(500);
Expand Down Expand Up @@ -605,8 +597,9 @@ ${B}${CYAN}╔══════════════════════
}
}

main().catch((err) => {
main().catch(async (err) => {
fail(err instanceof Error ? err.message : String(err));
killAll();
await closeAuth();
process.exit(1);
});
Loading
Loading