capgate 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -21
- package/NOTICE +4 -0
- package/README.md +290 -167
- package/dist/cli.js +30 -15
- package/dist/cli.js.map +1 -1
- package/dist/policy/adapters/bwrap.d.ts.map +1 -1
- package/dist/policy/adapters/bwrap.js +26 -11
- package/dist/policy/adapters/bwrap.js.map +1 -1
- package/dist/policy/adapters/docker.d.ts +32 -0
- package/dist/policy/adapters/docker.d.ts.map +1 -0
- package/dist/policy/adapters/docker.js +143 -0
- package/dist/policy/adapters/docker.js.map +1 -0
- package/dist/policy/adapters/egress.d.ts +24 -0
- package/dist/policy/adapters/egress.d.ts.map +1 -0
- package/dist/policy/adapters/egress.js +194 -0
- package/dist/policy/adapters/egress.js.map +1 -0
- package/dist/policy/index.d.ts +4 -0
- package/dist/policy/index.d.ts.map +1 -1
- package/dist/policy/index.js +2 -0
- package/dist/policy/index.js.map +1 -1
- package/package.json +66 -64
- package/dist/.tsbuildinfo +0 -1
package/README.md
CHANGED
|
@@ -1,167 +1,290 @@
|
|
|
1
|
-
# capgate
|
|
2
|
-
|
|
3
|
-
**Compile MCP tool manifests into sandbox policies.**
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
```
|
|
10
|
-
manifest (JSON) → Capability[]
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
fs:read:/
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
-
|
|
128
|
-
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
##
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
1
|
+
# capgate
|
|
2
|
+
|
|
3
|
+
**Compile MCP tool manifests into sandbox policies.**
|
|
4
|
+
|
|
5
|
+
MCP servers today either run with full host trust (Claude Desktop, most wrappers) or get wrapped in a one-size-fits-all container. Neither lets you say *this server needs `fs:read:/workspace/**` and `net:connect:api.github.com:443`, nothing else* — and have a sandbox policy fall out of that declaration.
|
|
6
|
+
|
|
7
|
+
`capgate` is the missing compile step. It reads a [Model Context Protocol](https://modelcontextprotocol.io) server manifest, parses capability strings, and emits a concrete sandbox policy your host can hand straight to bubblewrap or `docker run`.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
manifest (JSON) → Capability[] → NormalizedPolicy → adapter (bwrap | docker) → argv + egress + env + assertions
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
It is a **sandbox compiler for MCP servers**, not a runtime. It does not execute tools, resolve secrets, or speak MCP on the wire. Its job is to make the sandbox boundary **reviewable in a PR before the first agent call** — what the server is allowed to reach lives in the repo, not in someone's `docker run` muscle memory.
|
|
14
|
+
|
|
15
|
+
**For platform and security engineers** who can't ship MCP servers under blanket host trust and don't want to hand-write bwrap argv or `docker run` flags per server. **Not for** end-user agent UIs (this isn't a runtime), or for teams who want post-hoc tool-call auditing — that's a different lane (see [How capgate compares](#how-capgate-compares) below for the short version, or [A working map of MCP security tools](https://razukc.github.io/capgate/positioning/) for the full discussion).
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install capgate
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Requires Node.js ≥ 18.
|
|
26
|
+
|
|
27
|
+
## 30-second example
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { compile, lowerToDocker } from 'capgate';
|
|
31
|
+
|
|
32
|
+
const docker = lowerToDocker(compile({
|
|
33
|
+
name: 'my-server',
|
|
34
|
+
version: '0.1.0',
|
|
35
|
+
tools: [{ name: 'read_file', capabilities: ['fs:read:/workspace/**'] }],
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
console.log(docker.argv.join(' '));
|
|
39
|
+
// → --rm --cap-drop ALL --security-opt no-new-privileges --read-only
|
|
40
|
+
// --tmpfs /tmp --network none --volume /workspace:/workspace:ro
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
One capability in, one container policy out. No declared network → `--network none`. Read-only declared → `:ro` mount. No env declared → no env crosses the boundary. The CLI prints the same artifact for `bwrap`.
|
|
44
|
+
|
|
45
|
+
## CLI
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
capgate compile manifest.json --target bwrap --pretty
|
|
49
|
+
capgate compile manifest.json --target docker --pretty
|
|
50
|
+
capgate compile manifest.json --target egress --egress-target squid --pretty
|
|
51
|
+
capgate compile manifest.json --target egress --egress-target nftables --pretty
|
|
52
|
+
cat manifest.json | capgate compile - --target docker
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`--target egress` emits a static proxy config for a host-run proxy (`--egress-target squid|nftables`, default `squid`). Exits non-zero on parse errors (3), unknown arguments (2), or `CompilationError` (4). See `capgate --help`.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Worked example: `github` server with PAT
|
|
60
|
+
|
|
61
|
+
The 30-second example is a single tool with a single capability. A realistic MCP server has several tools, several capability kinds, and a threat model that motivates the sandbox in the first place.
|
|
62
|
+
|
|
63
|
+
**The threat.** An MCP `github` server runs with a personal access token in its environment. A tool description carrying adversarial text triggers an outbound request to attacker-controlled infrastructure, exfiltrating the PAT. A default container won't stop this — it inherits the host environment and reaches any host on the internet.
|
|
64
|
+
|
|
65
|
+
**The verdict.** capgate compiles the manifest below into a policy whose egress allowlist contains exactly one entry: `api.github.com:443`. An egress proxy honoring that allowlist refuses any outbound request that isn't api.github.com, blocking PAT exfiltration to a third party. No host env is inherited; only `GITHUB_PERSONAL_ACCESS_TOKEN` is named for the host's secret store to inject at exec time.
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
import { compile, lowerToBwrap, lowerToDocker } from 'capgate';
|
|
69
|
+
|
|
70
|
+
const manifest = {
|
|
71
|
+
name: '@modelcontextprotocol/server-github',
|
|
72
|
+
version: '0.6.2',
|
|
73
|
+
tools: [
|
|
74
|
+
{
|
|
75
|
+
name: 'create_issue',
|
|
76
|
+
description: 'Create an issue on a GitHub repository',
|
|
77
|
+
capabilities: [
|
|
78
|
+
'net:connect:api.github.com:443',
|
|
79
|
+
'env:inject:GITHUB_PERSONAL_ACCESS_TOKEN',
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: 'search_code',
|
|
84
|
+
description: 'Search code in a local checkout',
|
|
85
|
+
capabilities: [
|
|
86
|
+
'fs:read:/workspace/**',
|
|
87
|
+
'net:connect:api.github.com:443',
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: 'apply_patch',
|
|
92
|
+
description: 'Apply a code patch to the local checkout',
|
|
93
|
+
capabilities: ['fs:read,write:/workspace/**'],
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const policy = compile(manifest);
|
|
99
|
+
const bwrap = lowerToBwrap(policy);
|
|
100
|
+
const docker = lowerToDocker(policy, { readOnlyRootfs: true });
|
|
101
|
+
|
|
102
|
+
// Both artifacts share the same shape:
|
|
103
|
+
// .argv — flags ready for execFile()
|
|
104
|
+
// .egress — host egress-proxy allowlist (compiler-emitted, host-enforced)
|
|
105
|
+
// .envInjections — env var names the host must inject from a secret store
|
|
106
|
+
// .assertions — declared guarantees the sandbox cannot enforce; host verifies
|
|
107
|
+
// .notes — audit-friendly diagnostics (drift, edge cases, host decisions)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The compiler unions per-tool capabilities into a server-level policy: `apply_patch` widens `/workspace` from `:ro` to `:rw`, and only one env name survives the merge.
|
|
111
|
+
|
|
112
|
+
```jsonc
|
|
113
|
+
// docker.egress === bwrap.egress
|
|
114
|
+
[{ "host": "api.github.com", "port": 443, "blockPrivate": true }]
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Adapter `argv` (docker shown in full; bwrap abridged):
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
# docker (full)
|
|
121
|
+
--rm --cap-drop ALL --security-opt no-new-privileges --read-only
|
|
122
|
+
--tmpfs /tmp
|
|
123
|
+
--volume /workspace:/workspace:rw
|
|
124
|
+
--env GITHUB_PERSONAL_ACCESS_TOKEN
|
|
125
|
+
|
|
126
|
+
# bwrap (abridged — see fixture for full output)
|
|
127
|
+
--unshare-uts --unshare-cgroup-try --unshare-user-try --unshare-pid --unshare-ipc
|
|
128
|
+
--die-with-parent --new-session
|
|
129
|
+
--ro-bind-try /usr /usr --ro-bind-try /lib /lib --ro-bind-try /etc/ssl /etc/ssl
|
|
130
|
+
--proc /proc --tmpfs /tmp
|
|
131
|
+
--bind /workspace /workspace
|
|
132
|
+
--clearenv --setenv PATH /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
|
133
|
+
--setenv HOME /tmp
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Note what's *missing* from the docker `argv`: no inherited host env, no host network, no extra capabilities, no writable rootfs. capgate emits the policy; enforcement is the host's job.
|
|
137
|
+
|
|
138
|
+
Full golden outputs: [`bwrap/github.json`](tests/fixtures/policy/policies/bwrap/github.json), [`docker/github.json`](tests/fixtures/policy/policies/docker/github.json). Worked examples for `filesystem`, `fetch` (egress + assertions), and `puppeteer` (nested-sandbox edge case) live alongside them.
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## What's stable, what's evolving
|
|
143
|
+
|
|
144
|
+
`v0.0.x` is published for adopters who want to pin against capgate today. Stability commitments:
|
|
145
|
+
|
|
146
|
+
| Surface | Status in v0.0.x |
|
|
147
|
+
|---|---|
|
|
148
|
+
| Capability string grammar (`fs`, `net`, `env`, `assert` kinds) | **Stable.** String form will not change; new refinements are additive. |
|
|
149
|
+
| Adapter output shape (`argv`, `egress`, `envInjections`, `assertions`, `notes`) | **Stable.** Fields are additive; existing fields keep their semantics. |
|
|
150
|
+
| `compile()` and `lowerToBwrap` / `lowerToDocker` exports | **Stable.** |
|
|
151
|
+
| `lowerToEgress(policy, { target })` (`squid` / `nftables`) | **Usable.** Artifact shape (`config`, `filename`, `unenforceable`, `notes`) is additive; more `EgressTarget` values land without breaking existing ones. |
|
|
152
|
+
| `exec`, `ipc`, `clock` capability kinds | **Usable.** May gain refinements (like `exec:?nestedSandbox=true` did); existing forms keep working. |
|
|
153
|
+
| Adapter option objects (e.g. `lowerToDocker(policy, { readOnlyRootfs })`) | **Evolving.** Will expand in v0.1 as more adapters land. |
|
|
154
|
+
| `assert:` validator hook | **Metadata-only in v0.0.x.** Runtime hook lands in v0.2. |
|
|
155
|
+
|
|
156
|
+
Pin a minor range against `v0.0.x` for production review pipelines. Grammar additions land in `v0.1`; existing strings keep parsing.
|
|
157
|
+
|
|
158
|
+
## How capgate compares
|
|
159
|
+
|
|
160
|
+
MCP-server security splits across three lanes. capgate is in the static-technical lane. The other two aren't competitors — most teams running MCP servers in production end up wanting more than one.
|
|
161
|
+
|
|
162
|
+
| Lane | What it does | When you'd use it |
|
|
163
|
+
|---|---|---|
|
|
164
|
+
| **Static technical** (capgate) | Reads a manifest, emits sandbox argv + egress allowlist. Static artifact, no runtime. | You want the sandbox policy reviewable in PR before the server ever runs. |
|
|
165
|
+
| **Static governance** | Reads a manifest, emits a compliance report against a threat catalog or policy framework. | You want a procedural sign-off on a server before it's adopted. |
|
|
166
|
+
| **Dynamic attestation** | Logs and cryptographically signs each tool call at runtime. | You need a tamper-evident record of what the server actually did. |
|
|
167
|
+
|
|
168
|
+
Two adjacent concerns are *not* on this map: per-request authentication (gateways / OAuth — about *who* is calling) and runtime threat detection (pattern-watching, anomaly alerts — about flagging in real time). Both are real categories; both are different conversations. See [A working map of MCP security tools](https://razukc.github.io/capgate/positioning/) for the full discussion.
|
|
169
|
+
|
|
170
|
+
If you arrived here from a comparison post and you wanted dynamic attestation or static governance, capgate isn't that — but the artifact it emits can be the input to either.
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Validated servers
|
|
175
|
+
|
|
176
|
+
Before committing to the capability-grammar abstraction, we ran a [go/no-go exercise](tests/fixtures/policy/GO_NO_GO.md) against 10 real MCP servers. The full inventory (capability strings, source links, lowering notes) lives in [`GO_NO_GO.md`](tests/fixtures/policy/GO_NO_GO.md); the summary:
|
|
177
|
+
|
|
178
|
+
| Server | Capabilities (excerpt) | Status | Manifest | bwrap | docker |
|
|
179
|
+
|---|---|---|---|---|---|
|
|
180
|
+
| [filesystem](https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem) | `fs:read,write:<roots>` | mechanical | [filesystem.json](tests/fixtures/policy/manifests/filesystem.json) | [✓](tests/fixtures/policy/policies/bwrap/filesystem.json) | [✓](tests/fixtures/policy/policies/docker/filesystem.json) |
|
|
181
|
+
| [fetch](https://github.com/modelcontextprotocol/servers/tree/main/src/fetch) | `net:connect:*`, `assert:fetch.block_rfc1918` | mechanical (assert) | [fetch.json](tests/fixtures/policy/manifests/fetch.json) | [✓](tests/fixtures/policy/policies/bwrap/fetch.json) | [✓](tests/fixtures/policy/policies/docker/fetch.json) |
|
|
182
|
+
| [git](https://github.com/modelcontextprotocol/servers/tree/main/src/git) | `fs:read,write:<repo>`, `exec:spawn:git`, `net:connect:*` | mechanical | — | — | — |
|
|
183
|
+
| [memory](https://github.com/modelcontextprotocol/servers/tree/main/src/memory) | `fs:read,write:$MEMORY_FILE_PATH` | mechanical | — | — | — |
|
|
184
|
+
| [time](https://github.com/modelcontextprotocol/servers/tree/main/src/time) | `fs:read:/usr/share/zoneinfo`, `clock:tzdata` | mechanical | — | — | — |
|
|
185
|
+
| [github](https://github.com/modelcontextprotocol/servers-archived/tree/main/src/github) | `net:connect:api.github.com:443`, `env:inject:GITHUB_PERSONAL_ACCESS_TOKEN` | mechanical | [github.json](tests/fixtures/policy/manifests/github.json) | [✓](tests/fixtures/policy/policies/bwrap/github.json) | [✓](tests/fixtures/policy/policies/docker/github.json) |
|
|
186
|
+
| [postgres](https://github.com/modelcontextprotocol/servers-archived/tree/main/src/postgres) | `net:connect:<db>:<port>`, `assert:postgres.read_only_txn` | mechanical (assert) | — | — | — |
|
|
187
|
+
| [sqlite](https://github.com/modelcontextprotocol/servers-archived/tree/main/src/sqlite) | `fs:read,write:<db_path>` | mechanical | — | — | — |
|
|
188
|
+
| [brave-search](https://github.com/modelcontextprotocol/servers-archived/tree/main/src/brave-search) | `net:connect:api.search.brave.com:443`, `env:inject:BRAVE_API_KEY` | mechanical | — | — | — |
|
|
189
|
+
| [puppeteer](https://github.com/modelcontextprotocol/servers-archived/tree/main/src/puppeteer) | `exec:spawn:chromium?nestedSandbox=true`, `ipc:connect:x11` | nested-sandbox | [puppeteer.json](tests/fixtures/policy/manifests/puppeteer.json) | [✓](tests/fixtures/policy/policies/bwrap/puppeteer.json) | [✓](tests/fixtures/policy/policies/docker/puppeteer.json) |
|
|
190
|
+
|
|
191
|
+
Four of the ten ship as golden-file fixtures for both adapters; the rest follow one of the four shapes and are tracked in `GO_NO_GO.md` for the next grammar review. **MCP server author?** If your server isn't listed and you'd like a fixture review, [open an issue](https://github.com/razukc/capgate/issues/new) with a link to the manifest.
|
|
192
|
+
|
|
193
|
+
## Capability grammar
|
|
194
|
+
|
|
195
|
+
Capabilities are strings of the form `<kind>:<actions>:<scope>[?refinement=value&...]`.
|
|
196
|
+
|
|
197
|
+
```
|
|
198
|
+
fs:read,write:/workspace/**
|
|
199
|
+
fs:read:/usr/share/zoneinfo
|
|
200
|
+
net:connect:api.github.com:443
|
|
201
|
+
net:connect:* # any host, any port; implicit blockPrivate=true
|
|
202
|
+
exec:spawn:git
|
|
203
|
+
exec:spawn:chromium?nestedSandbox=true
|
|
204
|
+
env:inject:GITHUB_PAT
|
|
205
|
+
ipc:connect:x11
|
|
206
|
+
clock:tzdata
|
|
207
|
+
assert:postgres.read_only_txn:"all queries run in READ ONLY TRANSACTION"
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
The grammar rejects ambiguity (relative paths, bad ports, non-UPPER_SNAKE env vars) at parse time — fail-closed, always.
|
|
211
|
+
|
|
212
|
+
## Scope for v0.1
|
|
213
|
+
|
|
214
|
+
**In scope:**
|
|
215
|
+
- Capability grammar covering `fs`, `net`, `exec`, `env`, `ipc`, `clock`, `assert`.
|
|
216
|
+
- Lowering to `bwrap` (Linux namespace sandbox), `docker` (`docker run` argv), and `egress` (proxy config — `squid` / `nftables`, **shipped**). Worker `resourceLimits` (in-process JS isolation) is the next target.
|
|
217
|
+
- Golden-file tests from real MCP server manifests.
|
|
218
|
+
|
|
219
|
+
**Out of scope (deferred):**
|
|
220
|
+
- Firecracker / microVM adapter — needed for production but not for proving the abstraction.
|
|
221
|
+
- E2B / Daytona / Blaxel adapters — API stability varies; wait for a design partner.
|
|
222
|
+
- seccomp-bpf syscall filters — requires a separate IR; out of the capability model.
|
|
223
|
+
- MCP client/server implementation — this library consumes manifests, it does not speak MCP on the wire.
|
|
224
|
+
|
|
225
|
+
## Design notes
|
|
226
|
+
|
|
227
|
+
### Why capabilities are typed and discriminated
|
|
228
|
+
|
|
229
|
+
Early drafts used `{ resource: string; actions: string[]; scope: object }`. That failed the go/no-go test: every adapter had to re-parse `resource` to know what kind of capability it was looking at. The current discriminated union (`kind: 'fs' | 'net' | ...`) makes adapter code a flat switch; no string parsing past the grammar layer.
|
|
230
|
+
|
|
231
|
+
### Why enforceable vs declared
|
|
232
|
+
|
|
233
|
+
The go/no-go exercise revealed two capabilities that cannot be enforced at the sandbox layer: `fetch`'s RFC1918 block (sandbox can only toggle net on/off) and `postgres`'s read-only transaction guarantee (lives inside the MCP server). Silently dropping them would be a security lie. Promoting them to a first-class `assert:` capability keeps them in the audit trail: adapters emit them as metadata, the host is expected to verify them out-of-band, and the compiler fails compilation if an assertion is unrecognized by any configured validator (deferred to v0.2).
|
|
234
|
+
|
|
235
|
+
### Why the grammar is string-based
|
|
236
|
+
|
|
237
|
+
JSON-object capabilities are verbose and bury the kind under keys. The string form `fs:read,write:/workspace/**` is one line in a manifest, greps cleanly, and round-trips losslessly through the grammar.
|
|
238
|
+
|
|
239
|
+
### Why `nestedSandbox` is a refinement, not a kind
|
|
240
|
+
|
|
241
|
+
Chromium carries its own sandbox that fights namespace isolation. Every production sandbox tool has a special case for this. Rather than a new capability kind, `nestedSandbox=true` is a refinement on an existing `exec:` capability — the adapter sees it during lowering and emits a different bwrap profile (user/pid/ipc namespaces kept for inner-sandbox compatibility). The IR stays small; the edge case is explicit and documented.
|
|
242
|
+
|
|
243
|
+
### Why egress lowers to a proxy config, not a proxy
|
|
244
|
+
|
|
245
|
+
`egress[]` is emitted by the bwrap and docker adapters but enforced by neither — the host is told to wire a proxy. The neither-adapter-can-actually-do-net property is real: bwrap's `--unshare-net` is all-or-nothing (it always isolates now; see the net posture in `bwrap.ts`), and Docker's default bridge NATs but doesn't allowlist. Closing that gap by *running* a proxy would turn capgate into a gateway and drag a long-running, root-adjacent process into a library whose whole value is being a static compiler.
|
|
246
|
+
|
|
247
|
+
The move keeps capgate a compiler: a third lowering target, `lowerToEgress(policy, { target })`, compiles `policy.net` into a config blob for a proxy the host *already runs* — `squid` (allowlist by hostname via CONNECT, no TLS interception) and `nftables` (allowlist by IP+port in-kernel, bypass-proof, plus the `blockPrivate` drops) ship today. Both configs are fail-closed: squid ends in an unconditional `http_access deny all`, nftables defaults to `policy drop`, and an empty `policy.net` compiles to a deny-ALL config. The artifact carries an `unenforceable[]` field naming every declared rule the chosen target *cannot* honor (e.g. nftables can't express `api.github.com` — it filters IPs, not rotating hostnames), so "portable" stays honest: it compiles everywhere and tells you what each backend loses. Envoy/Cloudflare/microVM targets are later entries behind the same `EgressTarget` switch; the IR does not change. This keeps Docker MCP Gateway and Cloudflare as *targets you compile to*, not competitors — the manifest stays the single reviewable source of truth, and enforcement is borrowed, not built.
|
|
248
|
+
|
|
249
|
+
## Non-goals that matter
|
|
250
|
+
|
|
251
|
+
- **The compiler does not decide trust.** Capability declarations come from the manifest; the compiler does not infer them from tool descriptions. Inference belongs in a separate auditing tool. A manifest that under-declares is a bug in the manifest.
|
|
252
|
+
- **The compiler does not execute.** It emits policy artifacts. Running bwrap, wiring proxies, and spawning Workers is the host's job.
|
|
253
|
+
- **The compiler does not resolve secrets.** `env:inject:GITHUB_PAT` carries the name only. A secret store resolves the value at runtime, outside this library.
|
|
254
|
+
|
|
255
|
+
## Failure modes
|
|
256
|
+
|
|
257
|
+
- Unknown capability kind → `CompilationError('CAP_UNKNOWN_KIND')`.
|
|
258
|
+
- Capability a configured adapter cannot lower → `CompilationError('ADAPTER_UNSUPPORTED')` *(impl. pending)*.
|
|
259
|
+
- Manifest missing required fields → `CompilationError('MANIFEST_SHAPE')`.
|
|
260
|
+
|
|
261
|
+
All compilation errors are fatal. There is no warning mode.
|
|
262
|
+
|
|
263
|
+
## Test strategy
|
|
264
|
+
|
|
265
|
+
Golden files. One fixture manifest → one expected policy per adapter. Every PR that changes grammar, IR, or an adapter must update the golden files in the same commit. Reviewers read the diff. This is the primary correctness mechanism; unit tests on the grammar are secondary.
|
|
266
|
+
|
|
267
|
+
```bash
|
|
268
|
+
npm test # run all tests
|
|
269
|
+
npm run test:update-goldens # regenerate golden files after intentional changes
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
## Open questions before v0.1
|
|
273
|
+
|
|
274
|
+
1. **Egress proxy choice.** mitmproxy (great DX, slow, not prod-grade) vs nftables (hard to author, prod-grade, Linux-only) vs Envoy (prod-grade, ops-heavy). Direction settled: capgate stays a compiler and emits a config blob per target rather than running a proxy — see [Why egress lowers to a proxy config, not a proxy](#why-egress-lowers-to-a-proxy-config-not-a-proxy). Both `squid` and `nftables` now ship as reference bindings (they cover complementary cases — hostname-CONNECT vs in-kernel IP allowlist). Remaining sub-question is whether a dev-only mitmproxy emitter is worth carrying.
|
|
275
|
+
2. **Path glob semantics.** bwrap binds directories, not globs. A `fs:read:/workspace/**` capability lowers to `--ro-bind /workspace /workspace`, which is a *superset* of the declared scope. Runtime enforcement of globs is an MCP-server concern.
|
|
276
|
+
3. **Server-level vs tool-level capabilities.** v0.0 unions them. Finer-grained per-tool sandboxing (one sandbox per invocation) is possible but expensive — deferred until a user asks for it.
|
|
277
|
+
|
|
278
|
+
## Contributing
|
|
279
|
+
|
|
280
|
+
If you have a concrete manifest + unexpected compiler output, file an issue with both. See [CONTRIBUTING.md](CONTRIBUTING.md) for what else is useful.
|
|
281
|
+
|
|
282
|
+
Design-partner stage: if you're already reviewing MCP servers in production and willing to share how your review process works, [issue #1](https://github.com/razukc/capgate/issues/1) has a question for you.
|
|
283
|
+
|
|
284
|
+
## Security
|
|
285
|
+
|
|
286
|
+
capgate compiles declarations into sandbox policies downstream hosts trust — bugs here can silently over-grant. Please report privately per [SECURITY.md](SECURITY.md).
|
|
287
|
+
|
|
288
|
+
## License
|
|
289
|
+
|
|
290
|
+
Apache License 2.0 — see [LICENSE](LICENSE) and [NOTICE](NOTICE).
|
package/dist/cli.js
CHANGED
|
@@ -10,12 +10,13 @@
|
|
|
10
10
|
// This is deliberately minimal. Anything fancier (watch mode, multi-target,
|
|
11
11
|
// YAML output) belongs behind a feature flag, not here.
|
|
12
12
|
import { readFileSync } from 'node:fs';
|
|
13
|
-
import { compile, lowerToBwrap, CompilationError } from './policy/index.js';
|
|
13
|
+
import { compile, lowerToBwrap, lowerToDocker, lowerToEgress, CompilationError } from './policy/index.js';
|
|
14
14
|
function parseArgs(argv) {
|
|
15
15
|
const args = {
|
|
16
16
|
command: undefined,
|
|
17
17
|
input: undefined,
|
|
18
18
|
target: 'bwrap',
|
|
19
|
+
egressTarget: 'squid',
|
|
19
20
|
pretty: false,
|
|
20
21
|
help: false,
|
|
21
22
|
};
|
|
@@ -28,6 +29,8 @@ function parseArgs(argv) {
|
|
|
28
29
|
args.pretty = true;
|
|
29
30
|
else if (a === '--target')
|
|
30
31
|
args.target = rest[++i];
|
|
32
|
+
else if (a === '--egress-target')
|
|
33
|
+
args.egressTarget = rest[++i];
|
|
31
34
|
else if (!args.command)
|
|
32
35
|
args.command = a;
|
|
33
36
|
else if (!args.input)
|
|
@@ -35,19 +38,21 @@ function parseArgs(argv) {
|
|
|
35
38
|
}
|
|
36
39
|
return args;
|
|
37
40
|
}
|
|
38
|
-
const USAGE = `capgate — compile MCP manifests into sandbox policies
|
|
39
|
-
|
|
40
|
-
Usage:
|
|
41
|
-
capgate compile <manifest.json|-> [--target bwrap] [--pretty]
|
|
42
|
-
|
|
43
|
-
Options:
|
|
44
|
-
--target <name>
|
|
45
|
-
--
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
41
|
+
const USAGE = `capgate — compile MCP manifests into sandbox policies
|
|
42
|
+
|
|
43
|
+
Usage:
|
|
44
|
+
capgate compile <manifest.json|-> [--target bwrap] [--pretty]
|
|
45
|
+
|
|
46
|
+
Options:
|
|
47
|
+
--target <name> Adapter to lower to. Default: bwrap. Supported: bwrap, docker, egress.
|
|
48
|
+
--egress-target <name> Egress backend when --target egress. Default: squid. Supported: squid, nftables.
|
|
49
|
+
--pretty Indent JSON output with 2 spaces.
|
|
50
|
+
-h, --help Show this message.
|
|
51
|
+
|
|
52
|
+
Examples:
|
|
53
|
+
capgate compile manifests/filesystem.json --pretty
|
|
54
|
+
cat manifest.json | capgate compile - --target bwrap
|
|
55
|
+
capgate compile manifests/github.json --target egress --egress-target nftables --pretty
|
|
51
56
|
`;
|
|
52
57
|
function readInput(path) {
|
|
53
58
|
if (path === '-')
|
|
@@ -83,8 +88,18 @@ function main() {
|
|
|
83
88
|
case 'bwrap':
|
|
84
89
|
output = lowerToBwrap(policy);
|
|
85
90
|
break;
|
|
91
|
+
case 'docker':
|
|
92
|
+
output = lowerToDocker(policy);
|
|
93
|
+
break;
|
|
94
|
+
case 'egress':
|
|
95
|
+
if (args.egressTarget !== 'squid' && args.egressTarget !== 'nftables') {
|
|
96
|
+
process.stderr.write(`capgate: unsupported --egress-target "${args.egressTarget}" (supported: squid, nftables)\n`);
|
|
97
|
+
process.exit(2);
|
|
98
|
+
}
|
|
99
|
+
output = lowerToEgress(policy, { target: args.egressTarget });
|
|
100
|
+
break;
|
|
86
101
|
default:
|
|
87
|
-
process.stderr.write(`capgate: unsupported --target "${args.target}" (supported: bwrap)\n`);
|
|
102
|
+
process.stderr.write(`capgate: unsupported --target "${args.target}" (supported: bwrap, docker, egress)\n`);
|
|
88
103
|
process.exit(2);
|
|
89
104
|
}
|
|
90
105
|
const indent = args.pretty ? 2 : 0;
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,wDAAwD;AACxD,EAAE;AACF,SAAS;AACT,gEAAgE;AAChE,EAAE;AACF,0EAA0E;AAC1E,2EAA2E;AAC3E,EAAE;AACF,4EAA4E;AAC5E,wDAAwD;AAExD,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bwrap.d.ts","sourceRoot":"","sources":["../../../src/policy/adapters/bwrap.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"bwrap.d.ts","sourceRoot":"","sources":["../../../src/policy/adapters/bwrap.ts"],"names":[],"mappings":"AA4BA,OAAO,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAE5C,MAAM,WAAW,aAAa;IAC5B,0FAA0F;IAC1F,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,oDAAoD;IACpD,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,+EAA+E;IAC/E,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,oEAAoE;IACpE,UAAU,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAClD,6DAA6D;IAC7D,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,YAAY,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,YAAY;IAC3B,+EAA+E;IAC/E,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,yEAAyE;IACzE,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAID,wBAAgB,YAAY,CAAC,MAAM,EAAE,gBAAgB,EAAE,IAAI,GAAE,YAAiB,GAAG,aAAa,CAwH7F"}
|