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
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ vault (standalone) sandbox (standalone)
### Builtin Tools
Located in `packages/core/src/tools/builtin/`:
- `read.ts`, `write.ts`, `edit.ts` -- File operations
- `bash.ts` -- Shell command execution
- `bash.ts` -- Shell command execution; `createBashTool(options?)` factory accepts `allowedCommandPaths` for advisory command validation (warns when a binary is not under an allowed directory; Landlock is the real enforcement)
- `web-fetch.ts` -- HTTP fetching
- `web-search.ts` -- Web search via Brave Search API (conditionally included when `brave_api_key` exists in vault)
- `process.ts` -- Background process management (start/status/log/kill/list)
Expand All @@ -84,6 +84,7 @@ Each tool declares `requiredCapabilities` and implements a `ToolHandler` interfa

### Sandbox
- `packages/sandbox/src/sandbox.ts` -- Spawns child process with `unshare` + native helper
- `packages/sandbox/src/policy-builder.ts` -- `PolicyBuilder` class with fluent API; `PolicyBuilder.forDevelopment(cwd, options?)` creates a development-ready policy with allowlisted system paths, compiler toolchains (JVM, GCC), an expanded ~120 syscall allowlist, and support for `extraExecutePaths`/`extraReadWritePaths` via `DevelopmentPolicyOptions`
- `native/src/main.c` -- C helper binary that applies: Landlock filesystem rules, seccomp-BPF syscall filtering, capability dropping, `PR_SET_NO_NEW_PRIVS`
- Policy sent to helper via fd 3 as JSON

Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ After install, run `safeclaw onboard` for first-time setup.
### Security

- Zero-trust security model with mandatory OS-level sandboxing (Landlock + seccomp-BPF + Linux namespaces)
- Development-ready sandbox policy via `PolicyBuilder.forDevelopment()` — allows compilers (GCC, JVM), package managers, and standard dev tools while enforcing kernel-level access control
- AES-256-GCM encrypted secrets vault with OS keyring or passphrase-derived keys
- Ed25519-signed skill manifests with capability declarations and runtime enforcement
- Capability-based access control with path/host/executable constraints
Expand All @@ -36,6 +37,7 @@ After install, run `safeclaw onboard` for first-time setup.
### Tools

- Built-in tools: file read/write/edit, bash execution, web fetch, web search, background process management, multi-file patch application — all capability-gated
- Advisory command validation in bash tool warns when binaries are outside allowed paths (Landlock enforces the real boundary)
- Web search via Brave Search API (conditionally included when API key is in vault)
- Background process management with ring buffer output capture (1MB max, 8 concurrent, 1h auto-cleanup)
- Multi-file patch tool with unified diff parsing, fuzzy hunk matching, and atomic writes
Expand Down Expand Up @@ -63,7 +65,7 @@ Planned features in implementation order:

| # | Feature | Plan | Priority |
|---|---------|------|----------|
| 1 | Sandbox command execution & CWD permissions | [plan](docs/plans/2026-03-05-sandbox-permissions.md) | High |
| 1 | Sandbox command execution & CWD permissions | [plan](docs/plans/2026-03-05-sandbox-permissions.md) | **Done** |
| 2 | Automatic context compaction | [plan](docs/plans/2026-03-05-context-compaction.md) | High |
| 3 | Streaming UX (Phase 1 — readline) | [plan](docs/plans/2026-03-05-streaming-ux.md) | High |
| 4 | Better CLI/TUI (Ink-based) | [plan](docs/plans/2026-03-05-tui.md) | High |
Expand All @@ -89,7 +91,7 @@ Planned features in implementation order:
Monorepo structure:

- `@safeclaw/vault` — Encrypted secrets storage
- `@safeclaw/sandbox` — OS-level process sandboxing
- `@safeclaw/sandbox` — OS-level process sandboxing with `PolicyBuilder` for development-ready policies
- `@safeclaw/core` — Capabilities, agent runtime, sessions, tools, skills, model providers, copilot client
- `@safeclaw/gateway` — HTTP server with auth and rate limiting
- `@safeclaw/cli` — Command-line interface
Expand Down
2 changes: 2 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ No dependencies on other SafeClaw packages.
OS-level process isolation using Linux kernel features.

- `SandboxPolicy` / `DEFAULT_POLICY`: policy types with maximally restrictive defaults
- `PolicyBuilder`: fluent API for constructing sandbox policies; `PolicyBuilder.forDevelopment(cwd)` creates a ready-made policy for software development with allowlisted system paths, compiler toolchains, and an expanded syscall set
- `detectKernelCapabilities`: probes `/proc` for Landlock, seccomp, namespace support
- `assertSandboxSupported`: throws if required kernel features are missing
- `Sandbox` class: executes commands under policy (stub in v1; types and policies are real)
Expand Down Expand Up @@ -114,6 +115,7 @@ Central package containing the agent runtime and security infrastructure.
- `SimpleToolRegistry`: in-memory tool handler storage
- `AuditLog`: records tool executions (request + result + timestamp)
- Built-in tools: `read`, `write`, `edit`, `bash`, `web_fetch`, `apply_patch`, `process` (plus optional `web_search` when `brave_api_key` is in vault)
- `createBashTool(options?)`: factory function for the bash tool; accepts `allowedCommandPaths` for advisory command validation that warns when a binary is not under an allowed directory (Landlock is the real enforcement layer)
- `ProcessManager`: tracks spawned child processes by UUID with ring buffer output capture (1MB max per process), automatic cleanup after 1 hour, and maximum 8 concurrent processes
- `PatchParser` / `PatchApplier`: unified diff parser and hunk applier with fuzzy line-offset matching; used by the `apply_patch` tool for atomic multi-file patching

Expand Down
37 changes: 35 additions & 2 deletions docs/security-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ Landlock (kernel >= 5.13) restricts filesystem access at the kernel level. The s

### seccomp-BPF

seccomp-BPF filters system calls. The default policy uses `defaultDeny: true` and allows only a minimal set of syscalls required for basic process operation:
seccomp-BPF filters system calls. All policies use `defaultDeny: true` and allow only explicitly listed syscalls. Any syscall not in the allow list is blocked.

**DEFAULT_POLICY** allows a minimal set for basic process operation (~28 syscalls):

```
read, write, exit, exit_group, brk, mmap, close, fstat, mprotect,
Expand All @@ -28,7 +30,20 @@ execve, wait4, uname, fcntl, getcwd, arch_prctl, set_tid_address,
set_robust_list, rseq, prlimit64, getrandom
```

Any syscall not in the allow list is blocked.
**Development policy** (`PolicyBuilder.forDevelopment()`) expands this to ~120 syscalls organized by category:

- **Core I/O**: read, write, open, openat, close, lseek, pread64, pwrite64, readv, writev, sendfile
- **Memory**: mmap, mprotect, munmap, brk, mremap, madvise, msync
- **File metadata**: stat, fstat, lstat, newfstatat, fstatfs, statfs, statx
- **Directory**: getdents64, mkdir, rmdir, chdir, fchdir, getcwd
- **File operations**: rename, renameat2, unlink, unlinkat, link, linkat, symlink, symlinkat, readlink, readlinkat
- **File descriptors**: dup, dup2, dup3, fcntl, pipe, pipe2, eventfd2, epoll_create1, epoll_ctl, epoll_wait, epoll_pwait, select, pselect6, poll, ppoll
- **Permissions**: chmod, fchmod, fchmodat, chown, fchown, access, faccessat, faccessat2, umask
- **Process**: fork, vfork, clone, clone3, execve, execveat, wait4, waitid, exit, exit_group, getpid, getppid, gettid, getuid, getgid, geteuid, getegid, getgroups, setsid, setpgid, getpgid, getpgrp
- **Signals**: rt_sigaction, rt_sigprocmask, rt_sigreturn, kill, tgkill, rt_sigsuspend, sigaltstack
- **Time/clock**: clock_gettime, clock_getres, gettimeofday, nanosleep, clock_nanosleep, timer_create, timer_settime, timer_delete, timerfd_create, timerfd_settime, timerfd_gettime
- **Networking**: socket, connect, bind, listen, accept, accept4, recvfrom, sendto, recvmsg, sendmsg, shutdown, setsockopt, getsockopt, getpeername, getsockname, socketpair
- **Misc**: ioctl, prctl, arch_prctl, set_tid_address, set_robust_list, futex, sched_yield, sched_getaffinity, uname, prlimit64, getrandom, rseq, memfd_create, copy_file_range, fadvise64, fallocate, ftruncate, truncate, mlock, munlock, mincore

### Linux namespaces

Expand Down Expand Up @@ -57,6 +72,24 @@ const DEFAULT_POLICY: SandboxPolicy = {

Skills that need filesystem or network access must declare capabilities, and those capabilities must be granted before the tool orchestrator will allow execution.

### Development policy (PolicyBuilder)

For practical software development, `PolicyBuilder.forDevelopment(cwd)` creates a policy that allows compilers, package managers, and standard dev tools to run while maintaining security:

```typescript
const policy = PolicyBuilder.forDevelopment(process.cwd());
const sandbox = new Sandbox(policy);
```

The development policy grants:
- **Execute access**: `/usr/bin`, `/usr/local/bin`, `/bin`, `/usr/sbin`, `/sbin`, `/usr/lib/jvm`, `/usr/lib/gcc`, `/usr/libexec`, plus the Node.js install prefix
- **Read access**: `/usr/include`, `/usr/share`, shared library paths (`/lib`, `/usr/lib`, `/lib64`, `/usr/lib64`), `/etc`, `/proc`, `/dev/null`, `/dev/urandom`, `/dev/zero`, `/dev/random`
- **Read-write access**: CWD, `/tmp`, `~/.safeclaw`
- **Extra paths**: `DevelopmentPolicyOptions` supports `extraExecutePaths` (e.g., `~/.cargo/bin`, `~/.rustup`) and `extraReadWritePaths` for user-local toolchains
- **Expanded syscalls**: ~120 syscalls (see seccomp-BPF section above)
- **Network**: `"none"` (unchanged from default)
- **Namespaces**: all enabled (PID, net, mount, user)

For a detailed explanation of enforcement layers, the helper binary architecture, policy format, and security guarantees, see [Sandboxing Deep Dive](sandboxing.md).

## Capability system
Expand Down
3 changes: 2 additions & 1 deletion native/src/landlock.c
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,8 @@ static uint64_t policy_access_to_landlock(int access_level, int abi)

if (access_level == ACCESS_EXECUTE) {
rights |= LANDLOCK_ACCESS_FS_EXECUTE |
LANDLOCK_ACCESS_FS_READ_FILE;
LANDLOCK_ACCESS_FS_READ_FILE |
LANDLOCK_ACCESS_FS_READ_DIR;
}

return rights;
Expand Down
42 changes: 34 additions & 8 deletions packages/cli/src/commands/bootstrap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,23 @@ import {
SessionManager,
} from "@safeclaw/core";
const MockSandbox = vi.fn();
const mockForDevelopment = vi.fn().mockReturnValue({
filesystem: {
allow: [
{ path: "/bin", access: "execute" },
{ path: "/usr/bin", access: "execute" },
],
deny: [],
},
syscalls: { allow: ["read", "write"], defaultDeny: true },
network: "none",
namespaces: { pid: true, net: true, mnt: true, user: true },
timeoutMs: 30_000,
});
vi.mock("@safeclaw/sandbox", () => ({
Sandbox: MockSandbox,
DEFAULT_POLICY: {
namespaces: { pid: true, net: true, mnt: true, user: true },
timeoutMs: 30_000,
PolicyBuilder: {
forDevelopment: mockForDevelopment,
},
}));

Expand Down Expand Up @@ -72,6 +84,20 @@ function createMockDeps(
describe("bootstrapAgent", () => {
beforeEach(() => {
MockSandbox.mockReset();
mockForDevelopment.mockClear();
mockForDevelopment.mockReturnValue({
filesystem: {
allow: [
{ path: "/bin", access: "execute" },
{ path: "/usr/bin", access: "execute" },
],
deny: [],
},
syscalls: { allow: ["read", "write"], defaultDeny: true },
network: "none",
namespaces: { pid: true, net: true, mnt: true, user: true },
timeoutMs: 30_000,
});
});

it("returns agent and sessionManager when vault exists with passphrase key", async () => {
Expand Down Expand Up @@ -253,15 +279,15 @@ describe("bootstrapAgent", () => {
);
});

it("constructs Sandbox with DEFAULT_POLICY", async () => {
it("constructs Sandbox with PolicyBuilder.forDevelopment()", async () => {
const deps = createMockDeps();

await bootstrapAgent(deps);

expect(MockSandbox).toHaveBeenCalledWith({
namespaces: { pid: true, net: true, mnt: true, user: true },
timeoutMs: 30_000,
});
expect(mockForDevelopment).toHaveBeenCalledWith(process.cwd());
expect(MockSandbox).toHaveBeenCalledWith(
mockForDevelopment.mock.results[0]!.value,
);
});

it("falls back gracefully when Sandbox constructor throws", async () => {
Expand Down
12 changes: 9 additions & 3 deletions packages/cli/src/commands/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import {
KeyringProvider as DefaultKeyringProvider,
deriveKeyFromPassphrase as defaultDeriveKey,
} from "@safeclaw/vault";
import { Sandbox, DEFAULT_POLICY } from "@safeclaw/sandbox";
import { Sandbox, PolicyBuilder } from "@safeclaw/sandbox";
import { readPassphrase as defaultReadPassphrase } from "../readPassphrase.js";

export interface BootstrapDeps {
Expand Down Expand Up @@ -169,16 +169,22 @@ export async function bootstrapAgent(
}
const enforcer = new CapabilityEnforcer(capabilityRegistry);

// Build sandbox policy first — we extract allowed paths for bash tool validation
const sandboxPolicy = PolicyBuilder.forDevelopment(process.cwd());
const allowedCommandPaths = sandboxPolicy.filesystem.allow
.filter((r) => r.access === "execute" || r.access === "readwrite")
.map((r) => r.path);

const braveApiKey = vault.get("brave_api_key");
const processManager = new ProcessManager();
const toolRegistry = new SimpleToolRegistry();
for (const tool of createBuiltinTools({ braveApiKey, processManager })) {
for (const tool of createBuiltinTools({ braveApiKey, processManager, allowedCommandPaths })) {
toolRegistry.register(tool);
}

let sandbox: Sandbox | undefined;
try {
sandbox = new Sandbox(DEFAULT_POLICY);
sandbox = new Sandbox(sandboxPolicy);
} catch (err: unknown) {
// Sandbox not supported on this system — fall back to unsandboxed
const detail = err instanceof Error ? err.message : String(err);
Expand Down
Loading