Skip to content

Commit e8776f3

Browse files
feloyclaude
andauthored
feat(policy): authorize workspace hosts in sandbox policy (#80)
* feat(policy): authorize workspace hosts in sandbox policy Reads network.hosts from workspace.json and emits a single 'workspace' NetworkPolicyRule in the baked policy.yaml. Each host becomes an endpoint; all four PATH glob paths (/bin/**, /usr/bin/**, /usr/local/bin/**, /sandbox/.local/bin/**) plus the agent binary (when present) are authorized as binaries. Closes #75 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Philippe Martin <phmartin@redhat.com> * fix(policy): fail fast on malformed workspace host entries parse_workspace_host and workspace_hosts_policy now return Result and propagate URL parse errors instead of silently falling back to (original_input, 443), which would emit invalid endpoint hosts into the sandbox policy. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Philippe Martin <phmartin@redhat.com> * chore(docs): document workspace network rules feature Adds 'workspace network rules' to the intro feature list and the sandbox policy layer breakdown, and adds a new dedicated section explaining network.hosts in workspace.json, the authorised binary globs, error behaviour, and a worked example for Rust/crates.io access. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Philippe Martin <phmartin@redhat.com> * chore(docs): update sandbox-policy skill for workspace network layer Reflects the fourth policy merge layer: description, merge code snippet, merge order note, rules table, and build_policy() test example all updated to include the workspace hosts fragment. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Philippe Martin <phmartin@redhat.com> --------- Signed-off-by: Philippe Martin <phmartin@redhat.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 36d30a2 commit e8776f3

5 files changed

Lines changed: 300 additions & 7 deletions

File tree

.agents/skills/sandbox-policy/SKILL.md

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,12 @@ How the sandbox policy is assembled, what each section means, and how to add or
1111

1212
Every built image contains `/etc/openshell/policy.yaml`. The openshell runtime reads it at startup to configure filesystem access, the user the agent runs as, and which network endpoints each binary is allowed to reach.
1313

14-
The final policy is assembled by `build_policy()` in `src/main.rs` by merging three sources:
14+
The final policy is assembled by `build_policy()` in `src/main.rs` by merging four sources:
1515

1616
1. **Base policy**`assets/policy.yaml`, committed to the repo. Contains filesystem and process config, plus baseline network rules shared by all images (git, gh CLI).
1717
2. **Inference fragment** — returned by `Inference::policy_yaml(agent_binary, base_url)`. Adds network rules for the LLM backend. Only included when both `--agent` and `--inference` are given (the method needs the agent binary path to scope the rule).
1818
3. **Agent fragment** — returned by `Agent::policy_yaml()`. Adds agent-specific network rules (e.g., Claude needs `platform.claude.com` for telemetry). Only included if the string is non-empty.
19+
4. **Workspace fragment** — constructed from `workspace.network.hosts` in `.kaiden/workspace.json`. Adds a single `workspace` rule whose endpoints are the user-declared hosts and whose binaries are the four PATH globs (`/bin/**`, `/usr/bin/**`, `/usr/local/bin/**`, `/sandbox/.local/bin/**`) plus the agent binary when present. Only included when `network.hosts` is non-empty. An invalid host entry causes the build to fail immediately.
1920

2021
## YAML schema
2122

@@ -97,11 +98,23 @@ if let Some(agent) = agent {
9798
sandbox_policy.network_policies.extend(fragment.network_policies);
9899
}
99100
}
101+
102+
if let Some(hosts) = workspace
103+
.and_then(|ws| ws.network.as_ref())
104+
.map(|net| net.hosts.as_slice())
105+
.filter(|h| !h.is_empty())
106+
{
107+
let agent_binary = agent.map(|a| a.binary_path());
108+
sandbox_policy.network_policies.insert(
109+
"workspace".to_string(),
110+
workspace_hosts_policy(hosts, agent_binary)?,
111+
);
112+
}
100113
```
101114

102115
`network_policies` is a `BTreeMap<String, NetworkPolicyRule>`. The BTreeMap key is the merge slug (e.g. `"anthropic"`, `"claude_code"`). `extend()` means: if a fragment uses a slug that already exists in the base, the base entry is silently replaced. The `name` field inside the rule is separate — it is what the runtime displays, and should match the slug by convention.
103116

104-
**Merge order**: base → inference → agent. An agent rule can therefore override a base or inference rule with the same slug.
117+
**Merge order**: base → inference → agent → workspace. A workspace rule uses `insert()`, so it can override any earlier rule with the slug `"workspace"` (none exists in base or the other fragments).
105118

106119
## Existing network rules
107120

@@ -113,6 +126,7 @@ if let Some(agent) = agent {
113126
| `vertexai` | VertexAiInference | `oauth2.googleapis.com:443`, `aiplatform.googleapis.com:443`, `*-aiplatform.googleapis.com:443` | agent binary |
114127
| `ollama` | OllamaInference | `host.openshell.internal:11434` (or custom host:port) | agent binary |
115128
| `claude_code` | ClaudeAgent | `raw.githubusercontent.com:443`, `platform.claude.com:443`, `api.github.com:443` (read-only) | `/sandbox/.local/bin/claude` |
129+
| `workspace` | `workspace.network.hosts` | all user-declared hosts (port defaults to 443) | `/bin/**`, `/usr/bin/**`, `/usr/local/bin/**`, `/sandbox/.local/bin/**`, agent binary |
116130

117131
## Where to make changes
118132

@@ -206,6 +220,7 @@ fn build_policy_with_myprovider_inference_includes_expected_host() {
206220
Some(&agent::ClaudeAgent),
207221
Some(&inference::MyProviderInference),
208222
None,
223+
None,
209224
).unwrap();
210225
assert!(yaml.contains("api.myprovider.com"));
211226
}

.kaiden/workspace.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,12 @@
22
"features": {
33
"ghcr.io/devcontainers/features/rust:1": {},
44
"./features/cargo-llvm-cov": {}
5+
},
6+
"network": {
7+
"hosts": [
8+
"index.crates.io",
9+
"static.crates.io",
10+
"static.rust-lang.org"
11+
]
512
}
613
}

README.md

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,25 @@ The tool assembles the image in layers — base image, agent installation, agent
1919
- **Base policy** — Git operations over HTTPS and the GitHub REST API.
2020
- **Agent network rules** — agent-specific endpoints are added by `--agent`.
2121
- **Inference network rules** — LLM backend endpoints are added by `--inference`.
22+
- **Workspace network rules** — user-defined hosts declared in `.kaiden/workspace.json` are added to the policy.
2223
5. **Installation of project-specific toolchains** — toolchains and utilities declared as Dev Container Features in `.kaiden/workspace.json` are installed in the image.
2324

25+
### workspace.json fields
26+
27+
`.kaiden/workspace.json` is the per-workspace configuration file. The following fields are supported:
28+
29+
| Field | Description | Details |
30+
| ----- | ----------- | ------- |
31+
| `features` | Dev Container Features to install in the image | [Dev Container Features](#dev-container-features) |
32+
| `skills` | Skill directories to copy into the agent's skills directory | [Skills](#skills) |
33+
| `network.hosts` | Hostnames (and optional ports) to allow through the sandbox network policy | [Workspace network rules](#workspace-network-rules) |
34+
| ~~`network.mode`~~ | ~~`allow` or `deny` — OpenShell always enforces deny mode; allow-all is not supported~~ | ~~not used by the image builder~~ |
35+
| ~~`environment`~~ | ~~Environment variables to inject into the workspace~~ | ~~not used by the image builder~~ |
36+
| ~~`mcp`~~ | ~~MCP server configuration (command-based and URL-based servers)~~ | ~~not used by the image builder~~ |
37+
| ~~`mounts`~~ | ~~Host directories to mount in the workspace~~ | ~~not used by the image builder~~ |
38+
| ~~`ports`~~ | ~~TCP ports to expose from the workspace~~ | ~~not used by the image builder~~ |
39+
| ~~`secrets`~~ | ~~Secret names to inject into the workspace~~ | ~~not used by the image builder~~ |
40+
2441
### Agent Supported Features
2542

2643
| Agent | User settings | Auto-onboarding | Skills |
@@ -320,11 +337,12 @@ Every image built by this tool includes `/etc/openshell/policy.yaml`. This file
320337
- **Filesystem policy** — which paths are read-only, read-write, or inaccessible to the `sandbox` user.
321338
- **Network policies** — which binaries are allowed to connect to which hosts and ports.
322339

323-
The policy is built in three layers, merged in order:
340+
The policy is built in four layers, merged in order:
324341

325342
1. **Base** ([`assets/policy.yaml`](assets/policy.yaml)) — general-purpose tooling: Git operations over HTTPS and the GitHub REST API via `gh`.
326343
2. **Inference** (added by `--inference`) — LLM backend endpoints scoped to the agent binary. For example, `--inference anthropic` adds `api.anthropic.com` and `statsig.anthropic.com`; `--inference vertexai` adds `oauth2.googleapis.com` and `aiplatform.googleapis.com` (including the `*-aiplatform.googleapis.com` wildcard); `--inference ollama` adds `host.openshell.internal:11434` for local model access; `--inference openai` adds `api.openai.com` (or the custom endpoint host when `--endpoint` is used).
327344
3. **Agent** (added by `--agent`) — agent-specific endpoints. For example, `--agent claude` adds `platform.claude.com`, `raw.githubusercontent.com`, and the GitHub REST API for Claude's coding tools; `--agent opencode` adds `opencode.ai`, `registry.npmjs.org`, and `models.dev`.
345+
4. **Workspace** (added from `network.hosts` in `.kaiden/workspace.json`) — user-defined hosts that any binary in standard PATH directories (`/bin`, `/usr/bin`, `/usr/local/bin`, `/sandbox/.local/bin`) and the agent binary (when present) may reach. See [Workspace network rules](#workspace-network-rules).
328346

329347
## Dev Container Features
330348

@@ -417,6 +435,57 @@ When `.kaiden/workspace.json` is present, the tool:
417435

418436
Features run as root so install scripts can write to system paths.
419437

438+
## Workspace network rules
439+
440+
The OpenShell sandbox enforces a **deny-by-default** network policy: all outbound connections are blocked unless explicitly listed in the policy. There is no supported way to allow all hosts — the sandbox does not implement an allow-all mode. The `network.mode` field in `workspace.json` (which some orchestrators read to switch between `deny` and `allow`) is ignored by the image builder; the policy is always assembled in deny mode with explicit allow-rules.
441+
442+
Use the `network.hosts` field in `.kaiden/workspace.json` to allow additional hosts — for example, package registries or internal APIs that your project's toolchain needs to reach.
443+
444+
```json
445+
{
446+
"network": {
447+
"hosts": [
448+
"index.crates.io",
449+
"static.crates.io",
450+
"static.rust-lang.org"
451+
]
452+
}
453+
}
454+
```
455+
456+
Each entry is a hostname, optionally followed by a port (`host:port`). When no port is given, port 443 is used.
457+
458+
The builder merges a single `workspace` network policy rule into `policy.yaml` that covers all listed hosts. The rule authorises the following binaries to connect to those hosts:
459+
460+
| Binary glob | Covers |
461+
|---|---|
462+
| `/bin/**` | Core system utilities |
463+
| `/usr/bin/**` | Standard system binaries (e.g. `curl`) |
464+
| `/usr/local/bin/**` | Locally installed tools |
465+
| `/sandbox/.local/bin/**` | User-local binaries |
466+
| agent binary | The agent binary (e.g. `/sandbox/.local/bin/claude`) when `--agent` is used |
467+
468+
An invalid or unparseable host entry (e.g. a bare space or malformed URL) causes the build to fail immediately with a descriptive error message.
469+
470+
### Example — Rust project with crates.io access
471+
472+
```json
473+
{
474+
"features": {
475+
"ghcr.io/devcontainers/features/rust:1": {}
476+
},
477+
"network": {
478+
"hosts": [
479+
"index.crates.io",
480+
"static.crates.io",
481+
"static.rust-lang.org"
482+
]
483+
}
484+
}
485+
```
486+
487+
With this configuration, `cargo build` and `cargo fetch` inside the sandbox can download crate metadata and source tarballs.
488+
420489
## Full option reference
421490

422491
```

0 commit comments

Comments
 (0)