Skip to content
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ pnpm-debug.log*
# Relay runtime artifacts
*.sock
*.pid
.agent-relay/

# Local test artifacts
.agent-relay-test-*/
Expand Down
77 changes: 77 additions & 0 deletions .trajectories/completed/2026-02/traj_v5w0nh10we24.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
{
"id": "traj_v5w0nh10we24",
"version": 1,
"task": {
"title": "Add file permission guardrails as OS-level sandbox"
},
"status": "completed",
"startedAt": "2026-02-01T07:32:12.261Z",
"agents": [
{
"name": "default",
"role": "lead",
"joinedAt": "2026-02-01T07:32:27.146Z"
}
],
"chapters": [
{
"id": "chap_vjkd4nblwnxd",
"title": "Work",
"agentName": "default",
"startedAt": "2026-02-01T07:32:27.146Z",
"events": [
{
"ts": 1769931147148,
"type": "decision",
"content": "Used sandbox-exec on macOS for kernel-level file enforcement: Used sandbox-exec on macOS for kernel-level file enforcement",
"raw": {
"question": "Used sandbox-exec on macOS for kernel-level file enforcement",
"chosen": "Used sandbox-exec on macOS for kernel-level file enforcement",
"alternatives": [],
"reasoning": "Built-in, kernel-enforced, works without additional deps"
},
"significance": "high"
},
{
"ts": 1769931153746,
"type": "decision",
"content": "Used bubblewrap on Linux for filesystem isolation: Used bubblewrap on Linux for filesystem isolation",
"raw": {
"question": "Used bubblewrap on Linux for filesystem isolation",
"chosen": "Used bubblewrap on Linux for filesystem isolation",
"alternatives": [],
"reasoning": "Widely available, mature, flexible read-only bind mounts"
},
"significance": "high"
},
{
"ts": 1769931160596,
"type": "decision",
"content": "CLI-agnostic wrapper approach over CLI-specific flags: CLI-agnostic wrapper approach over CLI-specific flags",
"raw": {
"question": "CLI-agnostic wrapper approach over CLI-specific flags",
"chosen": "CLI-agnostic wrapper approach over CLI-specific flags",
"alternatives": [],
"reasoning": "User wanted consistent behavior across all CLIs, not just Claude"
},
"significance": "high"
}
],
"endedAt": "2026-02-01T07:32:53.018Z"
}
],
"commits": [],
"filesChanged": [],
"projectId": "/home/user/relay",
"tags": [],
"_trace": {
"startRef": "c07da61bf2eeb8bc56c5fac602a8f64150893b6b",
"endRef": "c07da61bf2eeb8bc56c5fac602a8f64150893b6b"
},
"completedAt": "2026-02-01T07:32:53.018Z",
"retrospective": {
"summary": "Implemented cross-platform file permission guardrails using OS-level sandboxing (sandbox-exec on macOS, bwrap on Linux). Works with any CLI.",
"approach": "Standard approach",
"confidence": 0.85
}
}
41 changes: 41 additions & 0 deletions .trajectories/completed/2026-02/traj_v5w0nh10we24.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Trajectory: Add file permission guardrails as OS-level sandbox

> **Status:** ✅ Completed
> **Confidence:** 85%
> **Started:** February 1, 2026 at 07:32 AM
> **Completed:** February 1, 2026 at 07:32 AM

---

## Summary

Implemented cross-platform file permission guardrails using OS-level sandboxing (sandbox-exec on macOS, bwrap on Linux). Works with any CLI.

**Approach:** Standard approach

---

## Key Decisions

### Used sandbox-exec on macOS for kernel-level file enforcement
- **Chose:** Used sandbox-exec on macOS for kernel-level file enforcement
- **Reasoning:** Built-in, kernel-enforced, works without additional deps

### Used bubblewrap on Linux for filesystem isolation
- **Chose:** Used bubblewrap on Linux for filesystem isolation
- **Reasoning:** Widely available, mature, flexible read-only bind mounts

### CLI-agnostic wrapper approach over CLI-specific flags
- **Chose:** CLI-agnostic wrapper approach over CLI-specific flags
- **Reasoning:** User wanted consistent behavior across all CLIs, not just Claude

---

## Chapters

### 1. Work
*Agent: default*

- Used sandbox-exec on macOS for kernel-level file enforcement: Used sandbox-exec on macOS for kernel-level file enforcement
- Used bubblewrap on Linux for filesystem isolation: Used bubblewrap on Linux for filesystem isolation
- CLI-agnostic wrapper approach over CLI-specific flags: CLI-agnostic wrapper approach over CLI-specific flags
9 changes: 8 additions & 1 deletion .trajectories/index.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"version": 1,
"lastUpdated": "2026-01-30T20:39:23.152Z",
"lastUpdated": "2026-02-01T07:32:53.138Z",
"trajectories": {
"traj_1b1dj40sl6jl": {
"title": "Revert aggressive retry logic in relay-pty-orchestrator",
Expand Down Expand Up @@ -211,6 +211,13 @@
"startedAt": "2026-01-30T20:38:53.393Z",
"completedAt": "2026-01-30T20:39:23.085Z",
"path": "/Users/khaliqgant/Projects/agent-workforce/relay/.trajectories/completed/2026-01/traj_tyavljk80fna.json"
},
"traj_v5w0nh10we24": {
"title": "Add file permission guardrails as OS-level sandbox",
"status": "completed",
"startedAt": "2026-02-01T07:32:12.261Z",
"completedAt": "2026-02-01T07:32:53.018Z",
"path": "/home/user/relay/.trajectories/completed/2026-02/traj_v5w0nh10we24.json"
}
}
}
50 changes: 46 additions & 4 deletions packages/bridge/src/spawner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ import type {
SpawnWithShadowResult,
SpeakOnTrigger,
} from './types.js';
import {
applySandbox,
mergePermissions,
cleanupSandboxProfile,
detectCapabilities,
type SandboxResult,
} from '@agent-relay/spawner';

// Logger instance for spawner (uses daemon log system instead of console)
const log = createLogger('spawner');
Expand Down Expand Up @@ -1048,10 +1055,18 @@ export class AgentSpawner {
};
}

// Track sandbox result for cleanup (set later if sandbox is applied)
let sandboxResultForCleanup: SandboxResult | undefined;

// Common exit handler for both wrapper types
const onExitHandler = (code: number) => {
if (debug) log.debug(`Worker ${name} exited with code ${code}`);

// Clean up sandbox profile file if one was created
if (sandboxResultForCleanup) {
cleanupSandboxProfile(sandboxResultForCleanup);
}

// Get the agentId and clean up listeners before removing from active workers
const worker = this.activeWorkers.get(name);
const agentId = worker?.pty?.getAgentId?.();
Expand Down Expand Up @@ -1103,6 +1118,33 @@ export class AgentSpawner {
await this.release(workerName);
};

// Apply file permission sandbox if configured in SpawnRequest
// This wraps the CLI command with platform-specific sandboxing (sandbox-exec on macOS, bwrap on Linux)
const filePermissions = mergePermissions(
request.filePermissionPreset,
request.filePermissions
);

let sandboxedCommand = command;
let sandboxedArgs = args;

if (filePermissions && Object.keys(filePermissions).length > 0) {
const sandboxResult = applySandbox(command, args, filePermissions, agentCwd);
sandboxedCommand = sandboxResult.command;
sandboxedArgs = sandboxResult.args;
// Store for cleanup on exit
sandboxResultForCleanup = sandboxResult;

if (sandboxResult.sandboxed) {
log.info(`Applied ${sandboxResult.method} sandbox for ${name}`);
if (debug) {
log.debug(`Sandbox permissions: ${JSON.stringify(filePermissions)}`);
}
} else if (debug) {
log.debug(`No sandbox available for ${name} (platform: ${detectCapabilities().platform})`);
}
}

// Check if we should use OpenCodeWrapper with HTTP API mode
if (isOpenCodeCli) {
// OpenCodeApi reads OPENCODE_API_URL or OPENCODE_PORT from env, defaults to localhost:4096
Expand All @@ -1119,8 +1161,8 @@ export class AgentSpawner {

const openCodeConfig: OpenCodeWrapperConfig = {
name,
command,
args,
command: sandboxedCommand,
args: sandboxedArgs,
socketPath: this.socketPath,
cwd: agentCwd,
dashboardPort: this.dashboardPort,
Expand Down Expand Up @@ -1264,8 +1306,8 @@ export class AgentSpawner {
// Create RelayPtyOrchestrator (relay-pty Rust binary)
const ptyConfig: RelayPtyOrchestratorConfig = {
name,
command,
args,
command: sandboxedCommand,
args: sandboxedArgs,
socketPath: this.socketPath,
cwd: agentCwd,
dashboardPort: this.dashboardPort,
Expand Down
12 changes: 12 additions & 0 deletions packages/bridge/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
* Types for multi-project orchestration
*/

import type { FilePermissions, FilePermissionPresetType } from '@agent-relay/spawner';

export interface ProjectConfig {
/** Absolute path to project root */
path: string;
Expand Down Expand Up @@ -61,6 +63,16 @@ export interface SpawnRequest {
userId?: string;
/** Include ACK/DONE workflow conventions in agent instructions (default: false) */
includeWorkflowConventions?: boolean;
/**
* File permission guardrails (OS-level sandbox enforcement).
* Wraps the CLI command with platform-specific sandboxing.
*/
filePermissions?: FilePermissions;
/**
* Use a predefined file permission preset instead of custom config.
* Presets: 'block-secrets', 'source-only', 'read-only', 'docs-only'
*/
filePermissionPreset?: FilePermissionPresetType;
}

/** Policy decision details */
Expand Down
1 change: 1 addition & 0 deletions packages/spawner/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
*/

export * from './types.js';
export * from './sandbox.js';
Loading
Loading