Skip to content

Commit 86430fb

Browse files
khaliqgantclaude
andcommitted
feat: add relayauth integration workflow
Workflow to add relayauth JWT verification to the Go server and mount daemon. Path-scoped access (relayfile:fs:write:/src/api/*), JWKS caching, backwards compat with existing relayfile JWTs. Depends on: @relayauth/sdk (for types reference), Go JWKS verification Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent cb814f2 commit 86430fb

File tree

1 file changed

+285
-0
lines changed

1 file changed

+285
-0
lines changed

workflows/integrate-relayauth.ts

Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
/**
2+
* integrate-relayauth.ts
3+
*
4+
* Integrates relayauth into relayfile: verify relayauth JWTs for all
5+
* filesystem operations. Scoped access to paths, revision-controlled
6+
* writes, and audit trail for every file operation.
7+
*
8+
* Depends on: @relayauth/sdk (TokenVerifier, ScopeChecker)
9+
*
10+
* Changes:
11+
* - Go server: verify relayauth JWTs (via JWKS) alongside existing JWT auth
12+
* - Go mount daemon: accept relayauth tokens for authentication
13+
* - TS SDK: pass relayauth tokens in requests
14+
* - Scope enforcement: relayfile:fs:read:/path, relayfile:fs:write:/path
15+
* - Path-scoped access: agent can only read/write files matching their scope paths
16+
*
17+
* Run: agent-relay run workflows/integrate-relayauth.ts
18+
*/
19+
20+
import { workflow } from '@agent-relay/sdk/workflows';
21+
22+
const RELAYFILE = '/Users/khaliqgant/Projects/AgentWorkforce/relayfile';
23+
const RELAYAUTH = '/Users/khaliqgant/Projects/AgentWorkforce/relayauth';
24+
25+
async function main() {
26+
const result = await workflow('integrate-relayauth-relayfile')
27+
.description('Add relayauth JWT verification to relayfile server and mount daemon')
28+
.pattern('dag')
29+
.channel('wf-relayfile-relayauth')
30+
.maxConcurrency(4)
31+
.timeout(3_600_000)
32+
33+
.agent('architect', {
34+
cli: 'claude',
35+
preset: 'lead',
36+
role: 'Design the integration, review code, fix issues',
37+
cwd: RELAYFILE,
38+
})
39+
.agent('go-dev', {
40+
cli: 'codex',
41+
preset: 'worker',
42+
role: 'Implement Go server and mount daemon auth changes',
43+
cwd: RELAYFILE,
44+
})
45+
.agent('sdk-dev', {
46+
cli: 'codex',
47+
preset: 'worker',
48+
role: 'Update TS SDK to pass relayauth tokens',
49+
cwd: RELAYFILE,
50+
})
51+
.agent('test-writer', {
52+
cli: 'codex',
53+
preset: 'worker',
54+
role: 'Write tests for auth integration',
55+
cwd: RELAYFILE,
56+
})
57+
.agent('reviewer', {
58+
cli: 'claude',
59+
preset: 'reviewer',
60+
role: 'Review for security, path-scoping correctness, backwards compat',
61+
cwd: RELAYFILE,
62+
})
63+
64+
// ── Phase 1: Read existing code ────────────────────────────────────
65+
66+
.step('read-go-auth', {
67+
type: 'deterministic',
68+
command: `cat ${RELAYFILE}/internal/httpapi/auth.go`,
69+
captureOutput: true,
70+
})
71+
72+
.step('read-go-server', {
73+
type: 'deterministic',
74+
command: `head -100 ${RELAYFILE}/internal/httpapi/server.go`,
75+
captureOutput: true,
76+
})
77+
78+
.step('read-mount-daemon', {
79+
type: 'deterministic',
80+
command: `cat ${RELAYFILE}/cmd/relayfile-mount/main.go`,
81+
captureOutput: true,
82+
})
83+
84+
.step('read-ts-sdk-client', {
85+
type: 'deterministic',
86+
command: `head -60 ${RELAYFILE}/sdk/relayfile-sdk/src/client.ts`,
87+
captureOutput: true,
88+
})
89+
90+
.step('read-relayauth-types', {
91+
type: 'deterministic',
92+
command: `cat ${RELAYAUTH}/packages/types/src/token.ts && echo "=== SCOPE ===" && cat ${RELAYAUTH}/packages/types/src/scope.ts`,
93+
captureOutput: true,
94+
})
95+
96+
.step('read-relayauth-sdk', {
97+
type: 'deterministic',
98+
command: `cat ${RELAYAUTH}/packages/sdk/src/verify.ts`,
99+
captureOutput: true,
100+
})
101+
102+
// ── Phase 2: Write tests + Implement ──────────────────────────────
103+
104+
.step('write-tests', {
105+
agent: 'test-writer',
106+
dependsOn: ['read-go-auth', 'read-relayauth-types'],
107+
task: `Write tests for relayfile + relayauth integration.
108+
109+
Current Go auth:
110+
{{steps.read-go-auth.output}}
111+
112+
RelayAuth token format:
113+
{{steps.read-relayauth-types.output}}
114+
115+
Create ${RELAYFILE}/internal/httpapi/relayauth_test.go:
116+
117+
Tests (Go testing package):
118+
1. Valid relayauth JWT with relayfile:fs:read:* → can read any file
119+
2. Valid relayauth JWT with relayfile:fs:read:/src/* → can read /src/foo.ts, cannot read /config/secret.yaml
120+
3. Valid relayauth JWT with relayfile:fs:write:/src/api/* → can write /src/api/route.ts, cannot write /src/ui/page.tsx
121+
4. Expired JWT → 401
122+
5. JWT with wrong audience (not "relayfile") → 401
123+
6. Legacy relayfile JWT (existing format) → still works
124+
7. No token → 401
125+
8. Path-scoped write: agent writes to allowed path → 200
126+
9. Path-scoped write: agent writes to disallowed path → 403
127+
10. SponsorChain present in auth context
128+
129+
Mock JWKS by serving a test public key on a local HTTP server in the test.
130+
Write to disk.`,
131+
verification: { type: 'exit_code' },
132+
})
133+
134+
.step('implement-go-auth', {
135+
agent: 'go-dev',
136+
dependsOn: ['read-go-auth', 'read-go-server', 'read-relayauth-sdk', 'write-tests'],
137+
task: `Add relayauth JWT verification to the Go server.
138+
139+
Current Go auth:
140+
{{steps.read-go-auth.output}}
141+
142+
Current server:
143+
{{steps.read-go-server.output}}
144+
145+
RelayAuth SDK (reference for token format):
146+
{{steps.read-relayauth-sdk.output}}
147+
148+
Changes to ${RELAYFILE}/internal/httpapi/auth.go:
149+
150+
1. Add JWKS fetching: fetch public keys from RELAYAUTH_JWKS_URL env var
151+
- Cache JWKS with 5-minute TTL
152+
- Use crypto/rsa + encoding/json to parse JWK → public key
153+
154+
2. Add JWT verification function: verifyRelayAuthToken(tokenString) → (claims, error)
155+
- Parse JWT header to get kid (key ID)
156+
- Look up public key from cached JWKS
157+
- Verify RS256 signature
158+
- Check exp, iss, aud ("relayfile" must be in aud array)
159+
- Return parsed claims including scopes, sponsorId, sponsorChain
160+
161+
3. Update the auth middleware chain:
162+
- Try relayauth JWT first (if RELAYAUTH_JWKS_URL is configured)
163+
- If valid: extract scopes, attach to request context
164+
- If not valid (not a JWT, wrong format): fall back to existing auth
165+
- Keep existing auth fully functional as fallback
166+
167+
4. Add path-scoped access enforcement:
168+
- New function: checkPathScope(scopes []string, action string, path string) bool
169+
- For each relayfile:fs:{action}:{pathPattern} scope, check if the request path matches
170+
- Wildcard matching: /src/* matches /src/foo.ts and /src/api/route.ts
171+
- Apply on read and write operations
172+
173+
5. Add env var: RELAYAUTH_JWKS_URL (optional — if not set, relayauth is disabled)
174+
175+
Write changes to disk. Use standard library only (no external JWT packages).`,
176+
verification: { type: 'exit_code' },
177+
})
178+
179+
.step('implement-mount-auth', {
180+
agent: 'go-dev',
181+
dependsOn: ['read-mount-daemon', 'implement-go-auth'],
182+
task: `Update mount daemon to accept relayauth tokens.
183+
184+
Current mount daemon:
185+
{{steps.read-mount-daemon.output}}
186+
187+
The mount daemon already accepts a --token flag. The change is minimal:
188+
relayauth tokens are JWTs — they work with the existing --token flag.
189+
The server-side auth (implemented in the previous step) handles verification.
190+
191+
Changes to ${RELAYFILE}/cmd/relayfile-mount/main.go:
192+
1. Add --relayauth-url flag (optional) for documentation/logging purposes
193+
2. Log at startup: "Authenticating via relayauth" if token looks like a JWT (has 3 dot-separated parts)
194+
3. No functional change needed — the token is passed as Bearer header, server verifies
195+
196+
Write changes to disk. Keep it minimal.`,
197+
verification: { type: 'exit_code' },
198+
})
199+
200+
.step('implement-ts-sdk', {
201+
agent: 'sdk-dev',
202+
dependsOn: ['read-ts-sdk-client'],
203+
task: `Update the TS SDK to support relayauth tokens.
204+
205+
Current SDK client:
206+
{{steps.read-ts-sdk-client.output}}
207+
208+
The SDK already accepts a token in RelayFileClientOptions. The change is:
209+
210+
1. Add optional relayauthToken to RelayFileClientOptions:
211+
relayauthToken?: string | (() => string | Promise<string>);
212+
213+
2. When relayauthToken is set, use it as the Bearer token instead of the regular token.
214+
This allows the SDK to work with either relayfile-native tokens or relayauth tokens.
215+
216+
3. Add a helper: RelayFileClient.fromRelayAuth(baseUrl, relayauthToken)
217+
Convenience factory that creates a client authenticated via relayauth.
218+
219+
Edit ${RELAYFILE}/sdk/relayfile-sdk/src/client.ts
220+
Write to disk.`,
221+
verification: { type: 'exit_code' },
222+
})
223+
224+
.step('verify-files', {
225+
type: 'deterministic',
226+
dependsOn: ['implement-go-auth', 'implement-mount-auth', 'implement-ts-sdk', 'write-tests'],
227+
command: `cd ${RELAYFILE} && echo "=== Go files ===" && ls internal/httpapi/relayauth_test.go 2>&1 && echo "=== Go build ===" && go build ./... 2>&1 | tail -5; echo "EXIT: $?"`,
228+
captureOutput: true,
229+
failOnError: false,
230+
})
231+
232+
// ── Phase 3: Review + Fix ─────────────────────────────────────────
233+
234+
.step('run-tests', {
235+
type: 'deterministic',
236+
dependsOn: ['verify-files'],
237+
command: `cd ${RELAYFILE} && go test ./internal/httpapi/... 2>&1 | tail -20; echo "EXIT: $?"`,
238+
captureOutput: true,
239+
failOnError: false,
240+
})
241+
242+
.step('review', {
243+
agent: 'reviewer',
244+
dependsOn: ['run-tests'],
245+
task: `Review the relayauth integration.
246+
247+
Test results:
248+
{{steps.run-tests.output}}
249+
250+
Read changed files:
251+
- cat ${RELAYFILE}/internal/httpapi/auth.go
252+
- cat ${RELAYFILE}/internal/httpapi/relayauth_test.go
253+
254+
Verify:
255+
1. JWKS caching has a TTL (not fetched on every request)
256+
2. Path-scoped access: /src/* correctly matches /src/foo.ts but not /config/x
257+
3. Backwards compat: existing JWT auth still works when RELAYAUTH_JWKS_URL not set
258+
4. No hardcoded keys or URLs
259+
5. RS256 verification uses standard library correctly
260+
6. SponsorChain is passed through to request context`,
261+
verification: { type: 'exit_code' },
262+
})
263+
264+
.step('fix', {
265+
agent: 'architect',
266+
dependsOn: ['review'],
267+
task: `Fix issues from review and tests.
268+
269+
Tests: {{steps.run-tests.output}}
270+
Review: {{steps.review.output}}
271+
272+
Fix all issues. Run go test ./... and verify clean.`,
273+
verification: { type: 'exit_code' },
274+
})
275+
276+
.onError('retry', { maxRetries: 1, retryDelayMs: 10_000 })
277+
.run({
278+
cwd: RELAYFILE,
279+
onEvent: (e: any) => console.log(`[${e.type}] ${e.stepName ?? e.step ?? ''} ${e.error ?? ''}`.trim()),
280+
});
281+
282+
console.log(`\nRelayfile + RelayAuth integration: ${result.status}`);
283+
}
284+
285+
main().catch(console.error);

0 commit comments

Comments
 (0)