Skip to content

On the relay#662

Open
khaliqgant wants to merge 12 commits intomainfrom
on-the-relay
Open

On the relay#662
khaliqgant wants to merge 12 commits intomainfrom
on-the-relay

Conversation

@khaliqgant
Copy link
Copy Markdown
Member

@khaliqgant khaliqgant commented Mar 27, 2026

Summary

Adds agent-relay on <cli> — launch any agent in a permission-enforced relay workspace.

  • agent-relay on codex — boots relayauth + relayfile, provisions scoped tokens, syncs files, launches codex with --dangerously-bypass-approvals-and-sandbox
  • agent-relay off — stops services, cleans up mounts
  • agent-relay on --scan — preview what the agent will see
  • agent-relay on --doctor — check prerequisites

How it works

  1. Reads .agentignore / .agentreadonly from project root (zero config)
  2. Compiles dotfiles into per-file scopes and ACL rules
  3. Mints scoped JWT tokens (no blanket fs:read/fs:write)
  4. Seeds project files into relayfile workspace (excludes .relay/, .git/, node_modules/)
  5. Syncs workspace via relayfile-mount with permission enforcement:
    • Ignored files never appear locally
    • Readonly files are chmod 444
    • Writes to denied files are reverted via fsnotify
  6. Launches agent with sandbox bypass flag (relay IS the sandbox)
  7. On exit: syncs changes back, cleans up workspace

New files

src/cli/commands/on.ts              — Commander.js command registration (on/off)
src/cli/commands/on/start.ts        — Main "agent-relay on" flow
src/cli/commands/on/stop.ts         — "agent-relay off" flow
src/cli/commands/on/dotfiles.ts     — .agentignore/.agentreadonly parser + compiler
src/cli/commands/on/token.ts        — JWT signing (HS256, no shell subprocess)
src/cli/commands/on/workspace.ts    — Relayfile workspace creation + seeding + ACL seeding
src/cli/commands/on/provision.ts    — Orchestrates token + workspace + ACLs
src/cli/commands/on/services.ts     — Start/stop relayauth + relayfile locally
src/cli/commands/on/prereqs.ts      — Check node, go, wrangler, relayfile binary
src/cli/commands/on/scan.ts         — Preview permissions without launching

Auto sandbox flags

CLI Flag applied
codex --dangerously-bypass-approvals-and-sandbox
claude --dangerously-skip-permissions
gemini --yolo
aider --yes

Test plan

  • agent-relay on --doctor checks prerequisites
  • agent-relay on --scan shows permission summary
  • Codex tested end-to-end: secrets/ invisible, README.md read-only, src/ writable
  • Claude tested end-to-end
  • agent-relay on --workspace rw_xxx join flow (pending shareable workspaces)

🤖 Generated with Claude Code


Open with Devin

khaliqgant and others added 2 commits March 27, 2026 14:58
AgentDefinition has model on the constraints object, not at the top level.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

- postinstall.js: fix TOCTOU race in patchRelayauthCoreExports (existsSync before readFileSync)
- start.ts: fix TOCTOU races in ensureProvisioned, fix biased random in generateWorkspaceId, fix signal handler resolving before agent exit code captured
- relay.ts: fix biased random in generateWorkspaceId, fix ensureStarted race (client set before wireEvents), wrap persistWorkspaceMapping in try-catch, clear client on error
- prereqs.ts: fix unreachable parent-directory fallbacks (path.join always returns string)
- services.ts: fix unreachable fallbacks in pickFirstString, fix fd leak in spawnLogged, add detached:true for service processes
- on.ts: remove ...overrides spread that overwrites ?? defaults with undefined
- validator.ts: fix duplicate "Check 5" comment numbering

Co-Authored-By: My Senior Dev <dev@myseniordev.com>
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 15 additional findings in Devin Review.

Open in Devin Review

Comment on lines +1144 to +1147
this.relayApiKey = resolvedKey;
this.resolvedWorkspaceId = resolvedWorkspaceId;
this.applyWorkspaceEnv(resolvedWorkspaceId, resolvedKey);
this.persistWorkspaceMapping(resolvedWorkspaceId, resolvedKey);
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 SDK ensureRelaycastApiKey writes to disk without error handling, breaking read-only environments

The refactored ensureRelaycastApiKey now calls this.persistWorkspaceMapping() on every code path (relay.ts:1140, relay.ts:1157, relay.ts:1169), which calls writeFileSync to .relay/workspaces.json. None of these calls are wrapped in try/catch. The previous implementation (relay.ts:948-968 old) never wrote files when RELAY_API_KEY was provided via env vars. This is a behavioral regression: users in read-only filesystem environments (Docker containers, CI) who simply set RELAY_API_KEY will now get an uncaught EACCES/EROFS error that prevents the SDK from starting.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@miyaontherelay
Copy link
Copy Markdown
Contributor

Added a second coverage-focused pass on top of the earlier CI/review fixes.\n\nWhat landed:\n- new tests for \n- new tests for \n- new tests for \n\nCoverage improvements from this pass:\n- overall: 58.72%\n- : 98.75%\n- : 90.9%\n- : 100%\n\nEarlier first-pass improvements on this branch are still in place too:\n- : 87.77%\n- : 82.47%\n- : 80.99%\n\nValidation:\n- full

agent-relay@3.2.21 pretest:coverage
npm run build

agent-relay@3.2.21 build
npm run clean && npm run build:rust && turbo run build --filter='./packages/*' && tsc

agent-relay@3.2.21 clean
rm -rf dist && find packages -maxdepth 2 -name dist -type d -exec rm -rf {} + 2>/dev/null || true

agent-relay@3.2.21 build:rust
if command -v ~/.cargo/bin/cargo >/dev/null 2>&1; then ~/.cargo/bin/cargo build --release --bin agent-relay-broker && mkdir -p packages/sdk/bin && cp target/release/agent-relay-broker packages/sdk/bin/agent-relay-broker.new && mv -f packages/sdk/bin/agent-relay-broker.new packages/sdk/bin/agent-relay-broker && echo '✓ broker binary (agent-relay-broker) built and copied to packages/sdk/bin/'; else echo '⚠ Rust not installed, using prebuilt binaries from bin/'; fi

✓ broker binary (agent-relay-broker) built and copied to packages/sdk/bin/
• Packages in scope: @agent-relay/acp-bridge, @agent-relay/brand, @agent-relay/cloud, @agent-relay/config, @agent-relay/hooks, @agent-relay/memory, @agent-relay/openclaw, @agent-relay/policy, @agent-relay/sdk, @agent-relay/telemetry, @agent-relay/trajectory, @agent-relay/user-directory, @agent-relay/utils
• Running build in 13 packages
• Remote caching disabled
@agent-relay/config:build: cache miss, executing 3a117769928a3a94
@agent-relay/telemetry:build: cache miss, executing 0c0a7f976c32cd32
@agent-relay/telemetry:build:
@agent-relay/config:build:
@agent-relay/telemetry:build: > @agent-relay/telemetry@3.2.21 build
@agent-relay/config:build: > @agent-relay/config@3.2.21 build
@agent-relay/telemetry:build: > tsc
@agent-relay/telemetry:build:
@agent-relay/config:build: > tsc
@agent-relay/config:build:
@agent-relay/trajectory:build: cache miss, executing 6b29fee74c8fa352
@agent-relay/cloud:build: cache miss, executing 95243399640fb01f
@agent-relay/sdk:build: cache miss, executing 793158e92e81e49c
@agent-relay/utils:build: cache miss, executing 8c3bbffa5f729796
@agent-relay/policy:build: cache miss, executing db41b63826d8bfa7
@agent-relay/trajectory:build:
@agent-relay/trajectory:build: > @agent-relay/trajectory@3.2.21 build
@agent-relay/trajectory:build: > tsc
@agent-relay/trajectory:build:
@agent-relay/policy:build:
@agent-relay/policy:build: > @agent-relay/policy@3.2.21 build
@agent-relay/policy:build: > tsc
@agent-relay/policy:build:
@agent-relay/cloud:build:
@agent-relay/cloud:build: > @agent-relay/cloud@3.2.21 build
@agent-relay/cloud:build: > tsc
@agent-relay/cloud:build:
@agent-relay/sdk:build:
@agent-relay/sdk:build: > @agent-relay/sdk@3.2.21 prebuild
@agent-relay/sdk:build: > npm --prefix ../config run build
@agent-relay/sdk:build:
@agent-relay/utils:build:
@agent-relay/utils:build: > @agent-relay/utils@3.2.21 build
@agent-relay/utils:build: > npm run clean && npm run build:esm && npm run build:cjs
@agent-relay/utils:build:
@agent-relay/sdk:build:
@agent-relay/sdk:build: > @agent-relay/config@3.2.21 build
@agent-relay/sdk:build: > tsc
@agent-relay/sdk:build:
@agent-relay/utils:build:
@agent-relay/utils:build: > @agent-relay/utils@3.2.21 clean
@agent-relay/utils:build: > rm -rf dist
@agent-relay/utils:build:
@agent-relay/utils:build:
@agent-relay/utils:build: > @agent-relay/utils@3.2.21 build:esm
@agent-relay/utils:build: > tsc
@agent-relay/utils:build:
@agent-relay/sdk:build:
@agent-relay/sdk:build: > @agent-relay/sdk@3.2.21 build
@agent-relay/sdk:build: > tsc -p tsconfig.build.json
@agent-relay/sdk:build:
@agent-relay/utils:build:
@agent-relay/utils:build: > @agent-relay/utils@3.2.21 build:cjs
@agent-relay/utils:build: > node ./scripts/build-cjs.mjs
@agent-relay/utils:build:
@agent-relay/utils:build:
@agent-relay/utils:build: dist/cjs/discovery.js 11.3kb
@agent-relay/utils:build: dist/cjs/precompiled-patterns.js 10.3kb
@agent-relay/utils:build: dist/cjs/relay-pty-path.js 9.3kb
@agent-relay/utils:build: dist/cjs/update-checker.js 6.3kb
@agent-relay/utils:build: dist/cjs/client-helpers.js 4.4kb
@agent-relay/utils:build: dist/cjs/git-remote.js 4.0kb
@agent-relay/utils:build: dist/cjs/logger.js 3.9kb
@agent-relay/utils:build: dist/cjs/error-tracking.js 3.5kb
@agent-relay/utils:build: dist/cjs/model-commands.js 3.4kb
@agent-relay/utils:build: dist/cjs/name-generator.js 3.2kb
@agent-relay/utils:build: dist/cjs/command-resolver.js 3.2kb
@agent-relay/utils:build: dist/cjs/legacy-protocol.js 2.5kb
@agent-relay/utils:build: dist/cjs/errors.js 2.5kb
@agent-relay/utils:build: dist/cjs/index.js 2.1kb
@agent-relay/utils:build: dist/cjs/model-mapping.js 1.7kb
@agent-relay/utils:build:
@agent-relay/utils:build: ⚡ Done in 6ms
@agent-relay/utils:build:
@agent-relay/utils:build: > @agent-relay/utils@3.2.21 postbuild
@agent-relay/utils:build: > npm run build:cjs
@agent-relay/utils:build:
@agent-relay/utils:build:
@agent-relay/utils:build: > @agent-relay/utils@3.2.21 build:cjs
@agent-relay/utils:build: > node ./scripts/build-cjs.mjs
@agent-relay/utils:build:
@agent-relay/utils:build:
@agent-relay/utils:build: dist/cjs/discovery.js 11.3kb
@agent-relay/utils:build: dist/cjs/precompiled-patterns.js 10.3kb
@agent-relay/utils:build: dist/cjs/relay-pty-path.js 9.3kb
@agent-relay/utils:build: dist/cjs/update-checker.js 6.3kb
@agent-relay/utils:build: dist/cjs/client-helpers.js 4.4kb
@agent-relay/utils:build: dist/cjs/git-remote.js 4.0kb
@agent-relay/utils:build: dist/cjs/logger.js 3.9kb
@agent-relay/utils:build: dist/cjs/error-tracking.js 3.5kb
@agent-relay/utils:build: dist/cjs/model-commands.js 3.4kb
@agent-relay/utils:build: dist/cjs/name-generator.js 3.2kb
@agent-relay/utils:build: dist/cjs/command-resolver.js 3.2kb
@agent-relay/utils:build: dist/cjs/legacy-protocol.js 2.5kb
@agent-relay/utils:build: dist/cjs/errors.js 2.5kb
@agent-relay/utils:build: dist/cjs/index.js 2.1kb
@agent-relay/utils:build: dist/cjs/model-mapping.js 1.7kb
@agent-relay/utils:build:
@agent-relay/utils:build: ⚡ Done in 5ms
@agent-relay/user-directory:build: cache miss, executing 321c11764e122558
@agent-relay/user-directory:build:
@agent-relay/user-directory:build: > @agent-relay/user-directory@3.2.21 build
@agent-relay/user-directory:build: > tsc
@agent-relay/user-directory:build:
@agent-relay/acp-bridge:build: cache miss, executing 722c86a457f24337
@agent-relay/hooks:build: cache miss, executing dab166194b943cef
@agent-relay/openclaw:build: cache miss, executing fd0ae6bde08a445d
@agent-relay/acp-bridge:build:
@agent-relay/acp-bridge:build: > @agent-relay/acp-bridge@3.2.21 build
@agent-relay/acp-bridge:build: > tsc
@agent-relay/acp-bridge:build:
@agent-relay/hooks:build:
@agent-relay/hooks:build: > @agent-relay/hooks@3.2.21 build
@agent-relay/hooks:build: > npm run clean && tsc
@agent-relay/hooks:build:
@agent-relay/openclaw:build:
@agent-relay/openclaw:build: > @agent-relay/openclaw@3.2.21 build
@agent-relay/openclaw:build: > tsc
@agent-relay/openclaw:build:
@agent-relay/hooks:build:
@agent-relay/hooks:build: > @agent-relay/hooks@3.2.21 clean
@agent-relay/hooks:build: > rm -rf dist
@agent-relay/hooks:build:
@agent-relay/memory:build: cache miss, executing 0a08437bad9ec359
@agent-relay/memory:build:
@agent-relay/memory:build: > @agent-relay/memory@3.2.21 build
@agent-relay/memory:build: > tsc
@agent-relay/memory:build:

Tasks: 12 successful, 12 total
Cached: 0 cached, 12 total
Time: 4.142s

src/cli/commands/on/services.ts(144,9): error TS1005: ',' expected.
src/cli/commands/on/services.ts(144,14): error TS1005: ',' expected.
src/cli/commands/on/services.ts(145,3): error TS1128: Declaration or statement expected. passed locally after the new tests\n- repo total coverage remains green at 63.29%\n\nLatest commit from this pass: ()

- CRITICAL: Remove hardcoded JWT signing secret, generate random per-workspace secret
- HIGH: Pass tokens via env vars instead of CLI arguments (ps-visible)
- HIGH: Fix globMatch to handle ** double-star patterns correctly
- HIGH: Fix symlink traversal in collectSeedPaths with path boundary check
- HIGH: Atomic file permissions (writeFileSync mode:0o600) for token files
- HIGH: Add SHA-256 checksum verification for downloaded binaries
- HIGH: Use homedir-based PID file path instead of CWD-relative
- HIGH: Add error handling for malformed workspace registry JSON
- MEDIUM: Write config files with 0o600 permissions (signing secrets, API keys)
- MEDIUM: Fix path traversal in syncWritableFilesBack with resolve+prefix check
- MEDIUM: Fix signal handler to store cleanup promise for close handler
- MEDIUM: Replace deps:any with typed GoOnRelayDeps interface
- MEDIUM: Close parent fd copy after spawn in spawnLogged
- MEDIUM: Add node_modules to walkProjectFiles exclusion list
- MEDIUM: Replace Math.random() with crypto.randomUUID() for JWT jti
- MEDIUM: Re-throw auth errors in provision workspace creation
- MEDIUM: Fix writeBulkWrite falsy status check with typeof
- MEDIUM: Add workspaceName deprecation notice
- LOW: Remove npx turbo build side effect from checkPrereqs

Co-Authored-By: My Senior Dev <dev@myseniordev.com>
try {
const checksumContent = await new Promise((resolve, reject) => {
const chunks = [];
const request = https.get(checksumUrl, res => {

Check warning

Code scanning / CodeQL

File data in outbound network request Medium

Outbound network request depends on
file data
.
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 3 new potential issues.

View 15 additional findings in Devin Review.

Open in Devin Review

Comment on lines +733 to +742
const escaped = pattern
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
.replace(/\\\*\\\*/g, '__DOUBLESTAR__')
.replace(/\\\*/g, '__STAR__')
.replace(/\\\?/g, '__QMARK__')
.replace(/__DOUBLESTAR__/g, '.*')
.replace(/__STAR__/g, '[^/]*')
.replace(/__QMARK__/g, '[^/]')
.replace(/__STAR__/g, '\\*')
.replace(/__QMARK__/g, '\\?');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 globMatch regex escaping omits * and ?, causing SyntaxError on any wildcard pattern

The globMatch function's first regex escape step (/[.+^${}()|[\]\\]/g at line 734) does not include * or ? in its character class. These glob wildcard characters pass through unescaped into the final regex string. For example, a pattern like docs/** produces the regex ^docs/**$, which throws SyntaxError: Nothing to repeat at runtime. This was confirmed by testing:

Reproduction showing the SyntaxError

For input pattern docs/**:

  • After escape step: docs/** (unchanged — * not in escape class)
  • After placeholder replacements: docs/** (unchanged — \*\* not found in string)
  • Resulting regex: ^docs/**$
  • new RegExp('^docs/**$')SyntaxError: Nothing to repeat

This function is called by isPathIgnored (start.ts:747-750) which controls permission enforcement in syncWritableFilesBack (start.ts:921-951). When cleanup runs after the agent exits, any readonly/ignored patterns containing wildcards (e.g., docs/**, *.js) will cause the sync-back to crash, potentially losing agent changes or failing to enforce file permissions.

Suggested change
const escaped = pattern
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
.replace(/\\\*\\\*/g, '__DOUBLESTAR__')
.replace(/\\\*/g, '__STAR__')
.replace(/\\\?/g, '__QMARK__')
.replace(/__DOUBLESTAR__/g, '.*')
.replace(/__STAR__/g, '[^/]*')
.replace(/__QMARK__/g, '[^/]')
.replace(/__STAR__/g, '\\*')
.replace(/__QMARK__/g, '\\?');
const escaped = pattern
.replace(/[.+^${}()|[\]\\*?]/g, '\\$&')
.replace(/\\\*\\\*/g, '__DOUBLESTAR__')
.replace(/\\\*/g, '__STAR__')
.replace(/\\\?/g, '__QMARK__')
.replace(/__DOUBLESTAR__/g, '.*')
.replace(/__STAR__/g, '[^/]*')
.replace(/__QMARK__/g, '[^/]');
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

private writeWorkspaceRegistry(registry: WorkspaceRegistry): void {
const registryPath = this.getWorkspaceRegistryPath();
mkdirSync(path.dirname(registryPath), { recursive: true });
writeFileSync(registryPath, `${JSON.stringify(registry, null, 2)}\n`, 'utf8');
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 SDK writes workspaces.json with world-readable permissions, exposing API keys

The SDK's writeWorkspaceRegistry at packages/sdk/src/relay.ts:479 writes workspaces.json using writeFileSync(registryPath, ..., 'utf8') which defaults to mode 0o644 (world-readable after umask). This file contains relaycastApiKey values — API credentials that should be protected. The CLI counterpart at src/cli/commands/on/start.ts:238 correctly uses { encoding: 'utf8', mode: 0o600 } to restrict access to the file owner only.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

return {};
}

const parsed = JSON.parse(raw) as unknown;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 readWorkspaceRegistry in start.ts crashes on corrupted JSON instead of returning empty registry

In start.ts:222, JSON.parse(raw) is called without a try-catch, so a corrupted workspaces.json file (e.g., from a prior crash during write) will throw an unhandled SyntaxError and crash the agent-relay on command. The SDK's equivalent at packages/sdk/src/relay.ts:455-462 correctly wraps the parse in try-catch and returns an empty registry on failure, allowing the caller to re-create the file.

Suggested change
const parsed = JSON.parse(raw) as unknown;
let parsed: unknown;
try {
parsed = JSON.parse(raw) as unknown;
} catch {
return {};
}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

khaliqgant and others added 3 commits March 27, 2026 23:41
Resolved package.json version conflict (bumped to 3.2.22) and fixed
misplaced parenthesis in resolveServiceConfig that caused TS1005/TS1128
compile errors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
start.ts: CRITICAL - reject empty JWT signing_secret with clear error
start.ts: HIGH - remove dead globMatch .replace() calls
start.ts: MEDIUM - harden syncWritableFilesBack with realpathSync for symlink traversal
start.ts: MEDIUM - add sandbox bypass warning log
postinstall.js: HIGH - validate needle count before patching, backup before write
relay.ts: MEDIUM - deprecation warning for workspaceName without workspaceId

Co-Authored-By: My Senior Dev <dev@myseniordev.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants