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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,10 @@ AgentSpace/

## Documentation

- [Sandbox current-state audit](docs/security/sandbox-current-state-audit.md)
- [Sandbox threat model](docs/security/threat-model.md)
- [Sandbox policy contract](docs/security/sandbox-policy.md)
- [Security PR readiness](docs/security/pr-readiness.md)
- [Remote daemon deployment test guide](deploy/REMOTE_DAEMON_TEST.md)
- [Founder execution showcase](deploy/FOUNDER_EXECUTION_SHOWCASE.md)
- [Remote daemon installer](deploy/install-remote-daemon.sh)
Expand Down
37 changes: 37 additions & 0 deletions docs/security/pr-readiness.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# PR Readiness

## PR 1: Sandbox Policy Contract and LocalSandbox Baseline

- Branch: `security/sandbox-policy-baseline`
- Title: `feat(sandbox): add policy contract and harden trusted local execution`
- Scope:
- Add the sandbox policy contract.
- Harden LocalSandbox path validation, environment construction, output bounds, and cleanup status.
- Add adversarial LocalSandbox tests.
- Document current state, threat model, and policy semantics.
- Platform support:
- Linux/macOS: process-group termination is attempted.
- Windows: direct-child termination is used and documented as a downgrade.
- Known limitations:
- LocalSandbox is still trusted local execution.
- Network isolation is modeled but not enforced in this PR.
- Credential broker, attachment isolation, signed URLs, and runtime tool manifests are later PRs.
- Merge recommendation: draft until the listed validation commands are run in the target CI environment.

## Validation Log

Commands run locally on Windows:

```bash
npm.cmd run test:sandbox
npm.cmd run test:security
git diff --check
git status --short
```

- `npm.cmd run test:sandbox`: passed, 19 passed, 1 skipped. The skipped test is the symlink escape test because Windows symlink creation permissions vary by host policy.
- `npm.cmd run test:security`: passed, same sandbox suite coverage.
- `git diff --check`: passed. Git emitted line-ending warnings on Windows but no whitespace errors.
- `npm.cmd run typecheck:deps`: not completed in this checkout. The root script invokes POSIX-style `./apps/web/node_modules/.bin/tsc`, which `cmd.exe` cannot execute, and this fresh checkout does not have `node_modules` installed.

This PR should remain draft until maintainers or CI run the Linux typecheck and symlink test path.
74 changes: 74 additions & 0 deletions docs/security/sandbox-current-state-audit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Sandbox Current State Audit

This audit covers the sandbox and daemon execution state before the full multi-PR sandbox hardening roadmap is complete. It is intentionally scoped to concrete code paths in this repository.

## Current Execution Architecture

- `packages/daemon/src/provider-runtime.ts` calls `connectSandbox()` for direct provider executions and then calls `sandbox.exec()` with provider CLI commands, task work directories, timeout settings, and provider-specific environment values.
- `packages/sandbox/src/factory.ts` resolves the provider and currently returns either `CubeSandbox.connect(options)` or `new LocalSandbox(options.workDir, options.runtimeId, options.policy)`.
- `packages/sandbox/src/local/local-sandbox.ts` provides trusted local execution using `node:child_process.spawn`.
- AgentRouter executions in `packages/daemon/src/provider-runtime.ts` route Claude, Codex, OpenClaw, and Hermes through `runAgentRouter()` before returning normalized provider output.

## Current Sandbox Providers

- `LocalSandbox` is the only provider that executes commands today.
- `CubeSandbox` in `packages/sandbox/src/cube/cube-sandbox.ts` can create, pause, snapshot, and delete Cube sandboxes, but `CubeSandbox.exec()` still throws `CUBE_EXEC_NOT_READY_MESSAGE`. It must not be described as remote isolated execution yet.

## Current Trust Boundaries

- `LocalSandbox` is now explicit trusted local execution with path, environment, process, and output safeguards.
- `LocalSandbox` is not a security boundary against malicious code. It still runs provider CLIs on the daemon host.
- Strong isolation is out of scope for this PR and belongs to the later isolated container provider work.

## Directory and Attachment Boundaries

- Sandbox file access is rooted at `workDir` and validated in `LocalSandbox.resolveExistingInsideSandbox()` and `LocalSandbox.resolveWritableInsideSandbox()`.
- Attachment storage and download authorization are not changed by this PR. Attachment isolation remains a later PR scope.
- Runtime output artifact collection is not changed by this PR. The new filesystem policy reserves artifact allow patterns for later enforcement.

## Credential Paths

- Provider task credentials enter daemon execution through `ProviderTaskOptions.contextEnv` and provider env builders in `packages/daemon/src/provider-runtime.ts`.
- `LocalSandbox.buildSandboxEnv()` no longer inherits the daemon host environment by default. It only keeps a minimal allowlist and explicit credential keys from the sandbox policy.
- This PR does not add a full credential broker. It establishes the policy shape and default local behavior needed for that follow-up.

## Network Capability

- `LocalSandbox` does not implement network isolation and does not claim to.
- The policy model records a network policy, but enforcement for `none` and `allowlist` modes is deferred to the isolated container/network PRs.

## Process Cleanup

- `LocalSandbox.exec()` starts child processes in a separate process group on non-Windows platforms and terminates the group on timeout or output limit.
- `stop()` and `destroy()` are idempotent.
- Windows keeps a predictable fallback that terminates the direct child process. It does not claim full process-tree cleanup.

## Audit Capability

- Structured audit event storage is not implemented in this PR.
- `ExecResult` now carries structured execution outcome fields such as `terminationReason`, `stdoutTruncated`, `stderrTruncated`, and `outputLimitExceeded`, which later audit events can consume.

## Security Gaps

- No rootless container provider exists yet.
- No daemon-wide credential broker exists yet.
- Local execution still uses the host kernel, filesystem namespace, and network stack.
- Network policy is modeled but not enforced.
- Attachment signed URL authorization and tenant isolation are unchanged.
- Runtime tool manifests and adapter security manifests are unchanged.

## Scope of This PR

- Add `SandboxPolicy` and related policy types.
- Harden trusted local filesystem access, environment construction, output bounds, timeout status, and cleanup behavior.
- Add adversarial LocalSandbox tests.
- Add security documentation for the current state and threat model.

## Out of Scope

- Production-grade isolated container execution.
- Network egress proxying.
- Credential broker issuance and revocation.
- Attachment signed URL lifecycle.
- Runtime tool marketplace UI.
- Full audit event persistence.
31 changes: 31 additions & 0 deletions docs/security/sandbox-policy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Sandbox Policy

`packages/sandbox/src/policy.ts` defines the policy contract used by sandbox providers.

## Trust Levels

- `trusted-local`: executes on the daemon host with safeguards. This is compatible with the current LocalSandbox behavior and is not a security boundary against malicious code.
- `isolated`: reserved for providers that enforce OS/container-level isolation.

## LocalSandbox Baseline

`createTrustedLocalSandboxPolicy(workDir)` sets conservative defaults for PR1:

- absolute sandbox paths are rejected;
- symlinks are rejected by default;
- reads and writes are constrained to the sandbox work directory;
- single-file and total write limits are enforced;
- host environment inheritance is disabled by default;
- `HOME`, `USERPROFILE`, `TMPDIR`, `TMP`, and `TEMP` are rewritten inside the sandbox work directory;
- stdout, stderr, and combined output are bounded;
- timeout and output-limit termination are reported in `ExecResult`.

## Compatibility

The public sandbox API remains additive. Existing callers may keep constructing `new LocalSandbox(workDir, runtimeId)` or using `connectSandbox(options)`.

Callers that need explicit credentials must provide a policy with `credentials.credentialEnvKeys`. Silent inheritance of secrets from the daemon host is intentionally not part of the baseline.

## Limitations

The policy model includes network and audit sections, but PR1 only enforces the LocalSandbox filesystem, environment, process timeout, and output controls. Network egress control belongs to the isolated provider and network-isolation PRs.
77 changes: 77 additions & 0 deletions docs/security/threat-model.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# AgentSpace Sandbox Threat Model

## Attackers

- Malicious workspace member.
- Agent controlled by prompt injection.
- Malicious or compromised skill.
- Malicious runtime tool.
- Malicious package install script.
- Compromised provider CLI.
- Agent attempting cross-workspace data access.
- Task attempting to read host credentials.
- Task attempting cloud metadata endpoint access.
- Task attempting CPU, memory, PID, disk, or log exhaustion.
- Task attempting symlink, hardlink, path traversal, or race-condition escape.
- Task attempting to keep background processes alive after completion.
- User attempting attachment access by guessing an attachment identifier.

## Protected Assets

- Daemon API tokens.
- Database credentials.
- Google OAuth tokens.
- Provider API keys.
- Workspace documents.
- Attachments.
- Runtime output.
- Agent knowledge.
- Other workspace task directories.
- Host files and host network.
- Internal services.
- Cloud metadata endpoints.
- Sensitive data in logs.

## Trust Boundaries

- Browser and API user identity boundary.
- Workspace membership and role boundary.
- Agent/runtime/task boundary.
- Daemon host boundary.
- Sandbox provider boundary.
- Attachment storage boundary.
- Credential injection boundary.
- Provider CLI boundary.

## Assumptions

- Maintainers can run daemon hosts with appropriate OS and container runtime controls.
- `LocalSandbox` is for trusted local execution only.
- Strong isolation requires an isolated provider such as a rootless container provider.
- Provider CLIs may read local configuration unless explicitly isolated by policy and runtime setup.

## Security Goals

- Make sandbox policy explicit and reviewable.
- Fail closed for path escapes and symlink escapes.
- Avoid default host environment inheritance for LocalSandbox.
- Bound stdout and stderr memory growth.
- Report timeout and output-limit termination explicitly.
- Preserve compatibility for existing callers through additive types and constructor defaults.
- Avoid claiming stronger isolation than the implementation provides.

## Non-Goals

- Proving LocalSandbox is safe for malicious code.
- Implementing complete network isolation in PR1.
- Implementing complete credential brokerage in PR1.
- Implementing attachment signed URLs in PR1.
- Implementing commercial marketplace UI in PR1.

## Residual Risks

- LocalSandbox still executes on the daemon host.
- Windows cannot guarantee the same process-group cleanup semantics as Linux.
- Existing provider CLIs can still have their own behavior and local config reads.
- Full prevention of TOCTOU attacks requires stronger OS/container primitives.
- Network and attachment isolation require later PRs.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
"typecheck": "npm run typecheck:deps && npm run typecheck:web:only && npm run typecheck:cli && npm run typecheck:daemon",
"lint:web": "npm --prefix apps/web run lint",
"test:web": "npm --prefix apps/web run test",
"test:sandbox": "node --experimental-strip-types --test packages/sandbox/src/**/*.test.ts",
"test:security": "npm run test:sandbox",
"test:e2e:web": "npm --prefix apps/web run test:e2e",
"quality:web": "npm --prefix apps/web run typecheck && npm --prefix apps/web run lint && npm --prefix apps/web run test"
}
Expand Down
9 changes: 7 additions & 2 deletions packages/sandbox/src/cube/cube-config.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import assert from "node:assert/strict";
import { resolve } from "node:path";
import test from "node:test";
import {
CUBE_MOUNT_WORKDIR_ENV,
Expand All @@ -21,6 +22,10 @@ function buildOptions(env: NodeJS.ProcessEnv = {}): SandboxConnectOptions {
};
}

function expectedResolvedWorkDir(): string {
return resolve(buildOptions().workDir);
}

test("resolveSandboxProvider falls back to local and honors legacy provider env", () => {
assert.equal(resolveSandboxProvider(buildOptions()), "local");
assert.equal(
Expand Down Expand Up @@ -62,7 +67,7 @@ test("resolveCubeSandboxConfig reads E2B-compatible env vars and derives timeout
assert.equal(config.mountWorkDir, false);
assert.deepEqual(config.metadata, {
"agent-space.runtime-id": "runtime-cube-test",
"agent-space.work-dir": "/tmp/agent-space-cube-test",
"agent-space.work-dir": expectedResolvedWorkDir(),
});
});

Expand Down Expand Up @@ -94,7 +99,7 @@ test("resolveCubeSandboxConfig can mount the daemon workDir into the Cube sandbo
assert.equal(config.metadata["agent-space.mount-path"], "/workspace");
assert.equal(
config.metadata["host-mount"],
JSON.stringify([{ hostPath: "/tmp/agent-space-cube-test", mountPath: "/workspace", readOnly: false }]),
JSON.stringify([{ hostPath: expectedResolvedWorkDir(), mountPath: "/workspace", readOnly: false }]),
);
});

Expand Down
2 changes: 1 addition & 1 deletion packages/sandbox/src/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@ export async function connectSandbox(options: SandboxConnectOptions): Promise<Sa
return CubeSandbox.connect(options);
}

return new LocalSandbox(options.workDir, options.runtimeId);
return new LocalSandbox(options.workDir, options.runtimeId, options.policy);
}
16 changes: 16 additions & 0 deletions packages/sandbox/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,22 @@ export type {
SandboxProvider,
SandboxStatus,
} from "./types.ts";
export {
createTrustedLocalSandboxPolicy,
isSensitiveEnvKey,
} from "./policy.ts";
export type {
SandboxAuditPolicy,
SandboxCredentialPolicy,
SandboxEnvironmentPolicy,
SandboxFilesystemPolicy,
SandboxNetworkPolicy,
SandboxOutputPolicy,
SandboxPolicy,
SandboxProcessPolicy,
SandboxTerminationReason,
SandboxTrustLevel,
} from "./policy.ts";
export { connectSandbox } from "./factory.ts";
export {
CUBE_API_KEY_ENV,
Expand Down
Loading