@clawdreyhepburn/carapace 0.1.0 → 0.2.1
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/README.md +79 -15
- package/docs/RECOMMENDED-POLICIES.md +519 -0
- package/package.json +1 -1
- package/src/cedar-engine-cedarling.ts +135 -31
- package/src/index.ts +287 -0
- package/src/types.ts +26 -0
package/README.md
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
<a href="#installation">Installation</a> •
|
|
11
11
|
<a href="#quick-start">Quick Start</a> •
|
|
12
12
|
<a href="#how-it-works">How It Works</a> •
|
|
13
|
+
<a href="docs/RECOMMENDED-POLICIES.md">Recommended Policies</a> •
|
|
13
14
|
<a href="#gui">Control GUI</a> •
|
|
14
15
|
<a href="#security">Security</a> •
|
|
15
16
|
<a href="#attribution">Attribution</a>
|
|
@@ -18,11 +19,11 @@
|
|
|
18
19
|
|
|
19
20
|
---
|
|
20
21
|
|
|
21
|
-
Carapace is an [OpenClaw](https://github.com/openclaw/openclaw) plugin that
|
|
22
|
+
Carapace is an [OpenClaw](https://github.com/openclaw/openclaw) plugin that puts Cedar authorization between your AI agent and everything it can do — MCP tools, shell commands, and outbound API calls. It aggregates multiple MCP servers, discovers their tools, gates shell execution by binary name, controls outbound HTTP by domain, and enforces [Cedar](https://www.cedarpolicy.com/) policies on every operation — with a local GUI where humans can see and control everything.
|
|
22
23
|
|
|
23
|
-
**The problem:**
|
|
24
|
+
**The problem:** Agents have access to tools, a shell, and the network. But who decides what they can actually *do*? Today the answer is "whatever's in the config file" — a static, all-or-nothing list with no audit trail, no formal guarantees, and no human oversight.
|
|
24
25
|
|
|
25
|
-
**The solution:** Carapace puts Cedar between your agent and its
|
|
26
|
+
**The solution:** Carapace puts Cedar between your agent and its capabilities. Cedar policies are declarative, auditable, and formally verifiable. The local GUI makes it accessible to humans who don't want to write policy files by hand. Toggle a switch, and the Cedar policy updates. It's that simple.
|
|
26
27
|
|
|
27
28
|
## Design Philosophy
|
|
28
29
|
|
|
@@ -42,12 +43,17 @@ The progression:
|
|
|
42
43
|
| OpenClaw |---->| |---->| (filesystem) |
|
|
43
44
|
| Agent | | +----------------------+ | +-----------------+
|
|
44
45
|
| | | | Cedarling WASM | | | MCP Server B |
|
|
45
|
-
|
|
|
46
|
+
| mcp_call |---->| | (Cedar 4.4.2) | |---->| (GitHub) |
|
|
46
47
|
| | | +----------------------+ | +-----------------+
|
|
47
|
-
|
|
|
48
|
-
|
|
|
48
|
+
| carapace | | | +-----------------+
|
|
49
|
+
| _exec --|---->| Cedar: exec_command |---->| Shell (local) |
|
|
50
|
+
| | | | +-----------------+
|
|
51
|
+
| carapace | | | +-----------------+
|
|
52
|
+
| _fetch --|---->| Cedar: call_api |---->| HTTP (remote) |
|
|
49
53
|
| | | +----------------------+ | +-----------------+
|
|
50
|
-
|
|
54
|
+
| | | | Local Control GUI | |
|
|
55
|
+
+-------------+ | +----------------------+ |
|
|
56
|
+
+--------------+--------------+
|
|
51
57
|
|
|
|
52
58
|
+------+------+
|
|
53
59
|
| Human |
|
|
@@ -55,7 +61,7 @@ The progression:
|
|
|
55
61
|
+-------------+
|
|
56
62
|
```
|
|
57
63
|
|
|
58
|
-
**Every
|
|
64
|
+
**Every operation flows through Cedar evaluation.** MCP tool calls, shell commands, and outbound API requests are all authorized by Cedar policies before execution. If the policy says deny, the operation never happens. The agent gets a clear denial message with the reason.
|
|
59
65
|
|
|
60
66
|
## Screenshots
|
|
61
67
|
|
|
@@ -149,18 +155,36 @@ In your OpenClaw config, add the servers you want Carapace to manage:
|
|
|
149
155
|
}
|
|
150
156
|
```
|
|
151
157
|
|
|
152
|
-
### 2.
|
|
158
|
+
### 2. Close the bypass gap
|
|
159
|
+
|
|
160
|
+
By default, agents can still use OpenClaw's built-in `exec` and `web_fetch` tools, which bypass Cedar entirely. Run setup to close this:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
openclaw carapace setup
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
This adds `exec`, `web_fetch`, and `web_search` to `tools.deny` in your OpenClaw config, forcing agents to use `carapace_exec` and `carapace_fetch` instead — which go through Cedar.
|
|
167
|
+
|
|
168
|
+
You can check for bypasses anytime:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
openclaw carapace check
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
> ⚠️ **Without this step, Carapace policies are advisory, not enforced.** The agent can simply choose to use the built-in tools instead. Always run `carapace setup` for real security.
|
|
175
|
+
|
|
176
|
+
### 3. Open the control GUI
|
|
153
177
|
|
|
154
178
|
Navigate to [http://localhost:19820](http://localhost:19820) in your browser. You'll see all discovered tools from all connected servers.
|
|
155
179
|
|
|
156
|
-
###
|
|
180
|
+
### 4. Enable tools
|
|
157
181
|
|
|
158
182
|
Toggle individual tools on/off. Each toggle writes a Cedar policy:
|
|
159
183
|
|
|
160
184
|
- **Toggle ON** → creates a `permit` policy for that tool
|
|
161
185
|
- **Toggle OFF** → creates a `forbid` policy for that tool
|
|
162
186
|
|
|
163
|
-
###
|
|
187
|
+
### 5. Create custom policies
|
|
164
188
|
|
|
165
189
|
Click **"+ New Policy"** to open the visual builder, or edit policies directly in the Policies tab. Examples:
|
|
166
190
|
|
|
@@ -179,6 +203,32 @@ forbid(
|
|
|
179
203
|
resource == Jans::Tool::"filesystem/write_file"
|
|
180
204
|
);
|
|
181
205
|
|
|
206
|
+
// Allow git and npm commands, block everything else
|
|
207
|
+
permit(
|
|
208
|
+
principal is Jans::Workload,
|
|
209
|
+
action == Jans::Action::"exec_command",
|
|
210
|
+
resource == Jans::Shell::"git"
|
|
211
|
+
);
|
|
212
|
+
permit(
|
|
213
|
+
principal is Jans::Workload,
|
|
214
|
+
action == Jans::Action::"exec_command",
|
|
215
|
+
resource == Jans::Shell::"npm"
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
// Allow API calls to GitHub, block all other domains
|
|
219
|
+
permit(
|
|
220
|
+
principal is Jans::Workload,
|
|
221
|
+
action == Jans::Action::"call_api",
|
|
222
|
+
resource == Jans::API::"api.github.com"
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
// Block a specific domain
|
|
226
|
+
forbid(
|
|
227
|
+
principal,
|
|
228
|
+
action == Jans::Action::"call_api",
|
|
229
|
+
resource == Jans::API::"evil.example.com"
|
|
230
|
+
);
|
|
231
|
+
|
|
182
232
|
// Allow everything (use with caution)
|
|
183
233
|
permit(
|
|
184
234
|
principal is Jans::Workload,
|
|
@@ -187,7 +237,9 @@ permit(
|
|
|
187
237
|
);
|
|
188
238
|
```
|
|
189
239
|
|
|
190
|
-
|
|
240
|
+
> 📖 **Want more?** See [Recommended Policies](docs/RECOMMENDED-POLICIES.md) for real-world policies covering destructive commands, credential theft, data exfiltration, email deletion, and complete starter configurations.
|
|
241
|
+
|
|
242
|
+
### 6. Verify policies
|
|
191
243
|
|
|
192
244
|
Click **⚡ Verify** to validate that all policies are syntactically correct and consistent.
|
|
193
245
|
|
|
@@ -198,10 +250,21 @@ Click **⚡ Verify** to validate that all policies are syntactically correct and
|
|
|
198
250
|
Carapace uses [Cedarling](https://github.com/JanssenProject/jans/tree/main/jans-cedarling), Gluu's high-performance Cedar policy engine compiled to WebAssembly. This means:
|
|
199
251
|
|
|
200
252
|
- **Real Cedar evaluation** — not a simplified subset. Full Cedar 4.4.2 with the official Rust SDK.
|
|
253
|
+
- **Three resource types** — `Tool` (MCP tools), `Shell` (commands by binary name), `API` (outbound HTTP by domain). All go through the same Cedar engine.
|
|
201
254
|
- **Forbid always wins** — if any policy says `forbid`, the request is denied regardless of any `permit` policies. This is core Cedar semantics and prevents privilege escalation.
|
|
202
|
-
- **Allow-all by default** — installing Carapace doesn't break anything. All
|
|
255
|
+
- **Allow-all by default** — installing Carapace doesn't break anything. All operations work until you add `forbid` policies. Switch to `deny-all` when you're ready for least-privilege.
|
|
203
256
|
- **Sub-millisecond evaluation** — WASM runs at near-native speed. Typical authorization decisions take <6ms.
|
|
204
257
|
|
|
258
|
+
### Resource Types
|
|
259
|
+
|
|
260
|
+
| Type | Cedar Entity | Action | Gates | Example |
|
|
261
|
+
|------|-------------|--------|-------|---------|
|
|
262
|
+
| MCP Tool | `Jans::Tool` | `call_tool` | Upstream MCP server calls | `Tool::"filesystem/write_file"` |
|
|
263
|
+
| Shell | `Jans::Shell` | `exec_command` | Local command execution | `Shell::"rm"`, `Shell::"git"` |
|
|
264
|
+
| API | `Jans::API` | `call_api` | Outbound HTTP requests | `API::"api.github.com"` |
|
|
265
|
+
|
|
266
|
+
Shell commands are matched by **binary name** (the first token of the command). API calls are matched by **domain name**. This keeps policies readable and auditable — you can see at a glance "this agent can run `git` and `npm` but not `rm` or `curl`."
|
|
267
|
+
|
|
205
268
|
### Policy Store Format
|
|
206
269
|
|
|
207
270
|
Policies are stored as individual `.cedar` files in the policy directory (default: `~/.openclaw/mcp-policies/`). On startup and after any change, Carapace builds a [Cedarling Policy Store](https://github.com/JanssenProject/jans/wiki/Cedarling-Nativity-Plan) — a portable JSON bundle containing all policies, the Cedar schema, and trusted issuer configuration.
|
|
@@ -227,7 +290,7 @@ The GUI communicates with Carapace through a local REST API:
|
|
|
227
290
|
|----------|--------|-------------|
|
|
228
291
|
| `/api/status` | GET | Server status, all tools, all policies |
|
|
229
292
|
| `/api/tools` | GET | List tools (optional `?server=` filter) |
|
|
230
|
-
| `/api/toggle` | POST | Enable/disable a
|
|
293
|
+
| `/api/toggle` | POST | Enable/disable a resource `{"tool": "...", "enabled": true, "type": "tool\|shell\|api"}` |
|
|
231
294
|
| `/api/policy` | POST | Create/update a policy `{"id": "...", "raw": "..."}` |
|
|
232
295
|
| `/api/policy` | DELETE | Delete a policy `{"id": "..."}` |
|
|
233
296
|
| `/api/policies` | GET | List all policies |
|
|
@@ -250,7 +313,8 @@ Carapace is designed to protect against:
|
|
|
250
313
|
### What Carapace Does NOT Protect Against
|
|
251
314
|
|
|
252
315
|
- **Malicious MCP servers** — Carapace trusts the upstream MCP servers to behave as described. It does not sandbox server execution.
|
|
253
|
-
- **
|
|
316
|
+
- **Argument-level validation** — Carapace authorizes *which* operation can be performed (which tool, which binary, which domain), not the specific arguments. Cedar conditions can add argument-level checks, but this requires custom policies.
|
|
317
|
+
- **Shell argument injection** — Carapace gates by binary name (`git`, `npm`), not by the full command line. An agent permitted to run `git` could run `git push --force`. Use Cedar `when` conditions on `context.args` for finer control.
|
|
254
318
|
- **Network-level attacks** — The GUI runs on localhost without authentication. See [GUI Security](#gui-security) below.
|
|
255
319
|
|
|
256
320
|
### GUI Security
|
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
# Recommended Policies
|
|
2
|
+
|
|
3
|
+
Real-world Cedar policies for common "oh no" scenarios. These are starting points — adapt them to your agent's actual needs.
|
|
4
|
+
|
|
5
|
+
## Before You Write Policies: Close the Bypass Gap
|
|
6
|
+
|
|
7
|
+
**This is the most important step.** If you skip it, every policy in this document is advisory — the agent can just use OpenClaw's built-in `exec` tool instead of `carapace_exec` and bypass Cedar entirely.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
openclaw carapace setup
|
|
11
|
+
openclaw gateway restart
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
This denies the built-in `exec`, `web_fetch`, and `web_search` tools, forcing agents to use the Cedar-gated `carapace_exec` and `carapace_fetch` instead. Verify with:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
openclaw carapace check
|
|
18
|
+
# Should show: ✅ No bypass vulnerabilities found.
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**Without this, an agent can:**
|
|
22
|
+
- Call `exec` directly with `rm -rf /` — Carapace never sees it
|
|
23
|
+
- Call `web_fetch` to exfiltrate data — Carapace never sees it
|
|
24
|
+
- Call `exec` with `curl` to hit any API — Carapace never sees it
|
|
25
|
+
|
|
26
|
+
Run setup first. Then write policies.
|
|
27
|
+
|
|
28
|
+
## The Basics
|
|
29
|
+
|
|
30
|
+
Carapace defaults to **allow-all** so installing it never breaks anything. The recommended path:
|
|
31
|
+
|
|
32
|
+
1. **Run `carapace setup`** → close the bypass gap
|
|
33
|
+
2. **Add forbids** → block the scary stuff (this doc)
|
|
34
|
+
3. **Switch to deny-all** → explicitly permit only what's needed (advanced)
|
|
35
|
+
|
|
36
|
+
Most people should start with step 2 and stay there until they're comfortable.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Shell Policies
|
|
41
|
+
|
|
42
|
+
### Block destructive file operations
|
|
43
|
+
|
|
44
|
+
The classics. An agent with shell access can `rm -rf /` before you blink.
|
|
45
|
+
|
|
46
|
+
```cedar
|
|
47
|
+
// Block rm entirely — use trash instead
|
|
48
|
+
forbid(
|
|
49
|
+
principal,
|
|
50
|
+
action == Jans::Action::"exec_command",
|
|
51
|
+
resource == Jans::Shell::"rm"
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
// Block destructive disk tools
|
|
55
|
+
forbid(
|
|
56
|
+
principal,
|
|
57
|
+
action == Jans::Action::"exec_command",
|
|
58
|
+
resource == Jans::Shell::"rmdir"
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
forbid(
|
|
62
|
+
principal,
|
|
63
|
+
action == Jans::Action::"exec_command",
|
|
64
|
+
resource == Jans::Shell::"mkfs"
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
forbid(
|
|
68
|
+
principal,
|
|
69
|
+
action == Jans::Action::"exec_command",
|
|
70
|
+
resource == Jans::Shell::"dd"
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// Block format/partition tools
|
|
74
|
+
forbid(
|
|
75
|
+
principal,
|
|
76
|
+
action == Jans::Action::"exec_command",
|
|
77
|
+
resource == Jans::Shell::"diskutil"
|
|
78
|
+
);
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**Why:** An agent that can delete files can delete *all* files. `rm` is the single most dangerous command you can give an agent. Use `trash` (recoverable) instead and permit that.
|
|
82
|
+
|
|
83
|
+
### Block credential and secret access
|
|
84
|
+
|
|
85
|
+
Agents don't need to read your SSH keys or browser passwords.
|
|
86
|
+
|
|
87
|
+
```cedar
|
|
88
|
+
// Block direct credential access tools
|
|
89
|
+
forbid(
|
|
90
|
+
principal,
|
|
91
|
+
action == Jans::Action::"exec_command",
|
|
92
|
+
resource == Jans::Shell::"security"
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
forbid(
|
|
96
|
+
principal,
|
|
97
|
+
action == Jans::Action::"exec_command",
|
|
98
|
+
resource == Jans::Shell::"ssh-keygen"
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
forbid(
|
|
102
|
+
principal,
|
|
103
|
+
action == Jans::Action::"exec_command",
|
|
104
|
+
resource == Jans::Shell::"gpg"
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
// Block password managers
|
|
108
|
+
forbid(
|
|
109
|
+
principal,
|
|
110
|
+
action == Jans::Action::"exec_command",
|
|
111
|
+
resource == Jans::Shell::"op"
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
forbid(
|
|
115
|
+
principal,
|
|
116
|
+
action == Jans::Action::"exec_command",
|
|
117
|
+
resource == Jans::Shell::"pass"
|
|
118
|
+
);
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**Why:** `security` (macOS Keychain CLI) can dump stored passwords. `ssh-keygen` can overwrite your keys. An agent doesn't need either of these to do useful work.
|
|
122
|
+
|
|
123
|
+
### Block system administration
|
|
124
|
+
|
|
125
|
+
Unless your agent is explicitly managing infrastructure, it shouldn't touch system config.
|
|
126
|
+
|
|
127
|
+
```cedar
|
|
128
|
+
forbid(
|
|
129
|
+
principal,
|
|
130
|
+
action == Jans::Action::"exec_command",
|
|
131
|
+
resource == Jans::Shell::"sudo"
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
forbid(
|
|
135
|
+
principal,
|
|
136
|
+
action == Jans::Action::"exec_command",
|
|
137
|
+
resource == Jans::Shell::"su"
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
forbid(
|
|
141
|
+
principal,
|
|
142
|
+
action == Jans::Action::"exec_command",
|
|
143
|
+
resource == Jans::Shell::"chmod"
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
forbid(
|
|
147
|
+
principal,
|
|
148
|
+
action == Jans::Action::"exec_command",
|
|
149
|
+
resource == Jans::Shell::"chown"
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
forbid(
|
|
153
|
+
principal,
|
|
154
|
+
action == Jans::Action::"exec_command",
|
|
155
|
+
resource == Jans::Shell::"launchctl"
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
forbid(
|
|
159
|
+
principal,
|
|
160
|
+
action == Jans::Action::"exec_command",
|
|
161
|
+
resource == Jans::Shell::"systemctl"
|
|
162
|
+
);
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**Why:** Privilege escalation is the nightmare scenario. If your agent can `sudo`, it can do *anything*. Even `chmod` can weaken file permissions enough to enable other attacks.
|
|
166
|
+
|
|
167
|
+
### Block network reconnaissance
|
|
168
|
+
|
|
169
|
+
Agents don't need to scan your network.
|
|
170
|
+
|
|
171
|
+
```cedar
|
|
172
|
+
forbid(
|
|
173
|
+
principal,
|
|
174
|
+
action == Jans::Action::"exec_command",
|
|
175
|
+
resource == Jans::Shell::"nmap"
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
forbid(
|
|
179
|
+
principal,
|
|
180
|
+
action == Jans::Action::"exec_command",
|
|
181
|
+
resource == Jans::Shell::"tcpdump"
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
forbid(
|
|
185
|
+
principal,
|
|
186
|
+
action == Jans::Action::"exec_command",
|
|
187
|
+
resource == Jans::Shell::"netcat"
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
forbid(
|
|
191
|
+
principal,
|
|
192
|
+
action == Jans::Action::"exec_command",
|
|
193
|
+
resource == Jans::Shell::"nc"
|
|
194
|
+
);
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Allow a safe set of dev tools
|
|
198
|
+
|
|
199
|
+
If your agent does development work, permit the tools it actually needs:
|
|
200
|
+
|
|
201
|
+
```cedar
|
|
202
|
+
// Version control
|
|
203
|
+
permit(
|
|
204
|
+
principal is Jans::Workload,
|
|
205
|
+
action == Jans::Action::"exec_command",
|
|
206
|
+
resource == Jans::Shell::"git"
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
// Package managers
|
|
210
|
+
permit(
|
|
211
|
+
principal is Jans::Workload,
|
|
212
|
+
action == Jans::Action::"exec_command",
|
|
213
|
+
resource == Jans::Shell::"npm"
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
permit(
|
|
217
|
+
principal is Jans::Workload,
|
|
218
|
+
action == Jans::Action::"exec_command",
|
|
219
|
+
resource == Jans::Shell::"npx"
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
// Safe file operations
|
|
223
|
+
permit(
|
|
224
|
+
principal is Jans::Workload,
|
|
225
|
+
action == Jans::Action::"exec_command",
|
|
226
|
+
resource == Jans::Shell::"cat"
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
permit(
|
|
230
|
+
principal is Jans::Workload,
|
|
231
|
+
action == Jans::Action::"exec_command",
|
|
232
|
+
resource == Jans::Shell::"ls"
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
permit(
|
|
236
|
+
principal is Jans::Workload,
|
|
237
|
+
action == Jans::Action::"exec_command",
|
|
238
|
+
resource == Jans::Shell::"grep"
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
permit(
|
|
242
|
+
principal is Jans::Workload,
|
|
243
|
+
action == Jans::Action::"exec_command",
|
|
244
|
+
resource == Jans::Shell::"find"
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
permit(
|
|
248
|
+
principal is Jans::Workload,
|
|
249
|
+
action == Jans::Action::"exec_command",
|
|
250
|
+
resource == Jans::Shell::"trash"
|
|
251
|
+
);
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## API Policies
|
|
257
|
+
|
|
258
|
+
### Block data exfiltration
|
|
259
|
+
|
|
260
|
+
An agent that can POST to any URL can send your files, credentials, and chat history anywhere.
|
|
261
|
+
|
|
262
|
+
```cedar
|
|
263
|
+
// Block known paste/upload services
|
|
264
|
+
forbid(
|
|
265
|
+
principal,
|
|
266
|
+
action == Jans::Action::"call_api",
|
|
267
|
+
resource == Jans::API::"pastebin.com"
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
forbid(
|
|
271
|
+
principal,
|
|
272
|
+
action == Jans::Action::"call_api",
|
|
273
|
+
resource == Jans::API::"hastebin.com"
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
forbid(
|
|
277
|
+
principal,
|
|
278
|
+
action == Jans::Action::"call_api",
|
|
279
|
+
resource == Jans::API::"transfer.sh"
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
forbid(
|
|
283
|
+
principal,
|
|
284
|
+
action == Jans::Action::"call_api",
|
|
285
|
+
resource == Jans::API::"file.io"
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
forbid(
|
|
289
|
+
principal,
|
|
290
|
+
action == Jans::Action::"call_api",
|
|
291
|
+
resource == Jans::API::"webhook.site"
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
forbid(
|
|
295
|
+
principal,
|
|
296
|
+
action == Jans::Action::"call_api",
|
|
297
|
+
resource == Jans::API::"requestbin.com"
|
|
298
|
+
);
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
**Why:** Prompt injection attacks can instruct an agent to exfiltrate data by posting it to an attacker-controlled URL. Blocking common exfil endpoints is a basic hygiene measure.
|
|
302
|
+
|
|
303
|
+
### Allow specific APIs your agent needs
|
|
304
|
+
|
|
305
|
+
Better than blocking bad domains: only allow the domains your agent actually uses.
|
|
306
|
+
|
|
307
|
+
```cedar
|
|
308
|
+
// GitHub API
|
|
309
|
+
permit(
|
|
310
|
+
principal is Jans::Workload,
|
|
311
|
+
action == Jans::Action::"call_api",
|
|
312
|
+
resource == Jans::API::"api.github.com"
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
// npm registry
|
|
316
|
+
permit(
|
|
317
|
+
principal is Jans::Workload,
|
|
318
|
+
action == Jans::Action::"call_api",
|
|
319
|
+
resource == Jans::API::"registry.npmjs.org"
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
// Your own services
|
|
323
|
+
permit(
|
|
324
|
+
principal is Jans::Workload,
|
|
325
|
+
action == Jans::Action::"call_api",
|
|
326
|
+
resource == Jans::API::"api.yourcompany.com"
|
|
327
|
+
);
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### Block social media posting
|
|
331
|
+
|
|
332
|
+
If your agent has social media access, you might want to prevent it from posting without oversight, or block it from leaking info to random accounts.
|
|
333
|
+
|
|
334
|
+
```cedar
|
|
335
|
+
// Block direct API access to social platforms
|
|
336
|
+
forbid(
|
|
337
|
+
principal,
|
|
338
|
+
action == Jans::Action::"call_api",
|
|
339
|
+
resource == Jans::API::"api.twitter.com"
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
forbid(
|
|
343
|
+
principal,
|
|
344
|
+
action == Jans::Action::"call_api",
|
|
345
|
+
resource == Jans::API::"api.x.com"
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
forbid(
|
|
349
|
+
principal,
|
|
350
|
+
action == Jans::Action::"call_api",
|
|
351
|
+
resource == Jans::API::"graph.facebook.com"
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
forbid(
|
|
355
|
+
principal,
|
|
356
|
+
action == Jans::Action::"call_api",
|
|
357
|
+
resource == Jans::API::"api.linkedin.com"
|
|
358
|
+
);
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
**Why:** An agent that can post to social media can damage your reputation in seconds. Even if your agent "should" post, you probably want that gated through an MCP tool with its own policy rather than raw API access.
|
|
362
|
+
|
|
363
|
+
---
|
|
364
|
+
|
|
365
|
+
## MCP Tool Policies
|
|
366
|
+
|
|
367
|
+
### Block destructive MCP tools
|
|
368
|
+
|
|
369
|
+
If you're using the filesystem MCP server, the write tools are the dangerous ones:
|
|
370
|
+
|
|
371
|
+
```cedar
|
|
372
|
+
forbid(
|
|
373
|
+
principal,
|
|
374
|
+
action == Jans::Action::"call_tool",
|
|
375
|
+
resource == Jans::Tool::"filesystem/write_file"
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
forbid(
|
|
379
|
+
principal,
|
|
380
|
+
action == Jans::Action::"call_tool",
|
|
381
|
+
resource == Jans::Tool::"filesystem/move_file"
|
|
382
|
+
);
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### Block email mass operations
|
|
386
|
+
|
|
387
|
+
If your agent has email access via MCP:
|
|
388
|
+
|
|
389
|
+
```cedar
|
|
390
|
+
// Prevent bulk deletion
|
|
391
|
+
forbid(
|
|
392
|
+
principal,
|
|
393
|
+
action == Jans::Action::"call_tool",
|
|
394
|
+
resource == Jans::Tool::"email/delete_all"
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
forbid(
|
|
398
|
+
principal,
|
|
399
|
+
action == Jans::Action::"call_tool",
|
|
400
|
+
resource == Jans::Tool::"email/empty_trash"
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
// Prevent mass sending (spam)
|
|
404
|
+
forbid(
|
|
405
|
+
principal,
|
|
406
|
+
action == Jans::Action::"call_tool",
|
|
407
|
+
resource == Jans::Tool::"email/send_bulk"
|
|
408
|
+
);
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
**Why:** An agent that can delete emails can delete your *entire inbox*. An agent that can send emails can spam your contacts. These are catastrophic, irreversible actions.
|
|
412
|
+
|
|
413
|
+
### Block database mutations
|
|
414
|
+
|
|
415
|
+
If your agent has database access:
|
|
416
|
+
|
|
417
|
+
```cedar
|
|
418
|
+
forbid(
|
|
419
|
+
principal,
|
|
420
|
+
action == Jans::Action::"call_tool",
|
|
421
|
+
resource == Jans::Tool::"database/execute_sql"
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
// Allow reads only
|
|
425
|
+
permit(
|
|
426
|
+
principal is Jans::Workload,
|
|
427
|
+
action == Jans::Action::"call_tool",
|
|
428
|
+
resource == Jans::Tool::"database/query"
|
|
429
|
+
);
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
---
|
|
433
|
+
|
|
434
|
+
## Complete Starter Policies
|
|
435
|
+
|
|
436
|
+
### "Cautious developer" — safe for a coding agent
|
|
437
|
+
|
|
438
|
+
```cedar
|
|
439
|
+
// Shell: allow common dev tools, block everything dangerous
|
|
440
|
+
permit(principal is Jans::Workload, action == Jans::Action::"exec_command", resource == Jans::Shell::"git");
|
|
441
|
+
permit(principal is Jans::Workload, action == Jans::Action::"exec_command", resource == Jans::Shell::"npm");
|
|
442
|
+
permit(principal is Jans::Workload, action == Jans::Action::"exec_command", resource == Jans::Shell::"npx");
|
|
443
|
+
permit(principal is Jans::Workload, action == Jans::Action::"exec_command", resource == Jans::Shell::"node");
|
|
444
|
+
permit(principal is Jans::Workload, action == Jans::Action::"exec_command", resource == Jans::Shell::"cat");
|
|
445
|
+
permit(principal is Jans::Workload, action == Jans::Action::"exec_command", resource == Jans::Shell::"ls");
|
|
446
|
+
permit(principal is Jans::Workload, action == Jans::Action::"exec_command", resource == Jans::Shell::"grep");
|
|
447
|
+
permit(principal is Jans::Workload, action == Jans::Action::"exec_command", resource == Jans::Shell::"find");
|
|
448
|
+
permit(principal is Jans::Workload, action == Jans::Action::"exec_command", resource == Jans::Shell::"trash");
|
|
449
|
+
permit(principal is Jans::Workload, action == Jans::Action::"exec_command", resource == Jans::Shell::"mkdir");
|
|
450
|
+
permit(principal is Jans::Workload, action == Jans::Action::"exec_command", resource == Jans::Shell::"cp");
|
|
451
|
+
permit(principal is Jans::Workload, action == Jans::Action::"exec_command", resource == Jans::Shell::"mv");
|
|
452
|
+
|
|
453
|
+
forbid(principal, action == Jans::Action::"exec_command", resource == Jans::Shell::"rm");
|
|
454
|
+
forbid(principal, action == Jans::Action::"exec_command", resource == Jans::Shell::"sudo");
|
|
455
|
+
forbid(principal, action == Jans::Action::"exec_command", resource == Jans::Shell::"security");
|
|
456
|
+
|
|
457
|
+
// API: allow GitHub and npm, block exfil
|
|
458
|
+
permit(principal is Jans::Workload, action == Jans::Action::"call_api", resource == Jans::API::"api.github.com");
|
|
459
|
+
permit(principal is Jans::Workload, action == Jans::Action::"call_api", resource == Jans::API::"registry.npmjs.org");
|
|
460
|
+
forbid(principal, action == Jans::Action::"call_api", resource == Jans::API::"pastebin.com");
|
|
461
|
+
forbid(principal, action == Jans::Action::"call_api", resource == Jans::API::"webhook.site");
|
|
462
|
+
|
|
463
|
+
// MCP: allow all tools (rely on shell/API policies for safety)
|
|
464
|
+
permit(principal is Jans::Workload, action == Jans::Action::"call_tool", resource);
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
### "Paranoid lockdown" — least privilege, deny-all baseline
|
|
468
|
+
|
|
469
|
+
Set `defaultPolicy: "deny-all"` in config, then only add permits:
|
|
470
|
+
|
|
471
|
+
```cedar
|
|
472
|
+
// Only the exact tools this agent needs
|
|
473
|
+
permit(principal is Jans::Workload, action == Jans::Action::"call_tool", resource == Jans::Tool::"filesystem/read_file");
|
|
474
|
+
permit(principal is Jans::Workload, action == Jans::Action::"call_tool", resource == Jans::Tool::"filesystem/list_directory");
|
|
475
|
+
|
|
476
|
+
// Only git
|
|
477
|
+
permit(principal is Jans::Workload, action == Jans::Action::"exec_command", resource == Jans::Shell::"git");
|
|
478
|
+
|
|
479
|
+
// No API access at all (omit all call_api permits)
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
Everything not explicitly permitted is denied. This is the most secure posture but requires you to know exactly what your agent needs.
|
|
483
|
+
|
|
484
|
+
---
|
|
485
|
+
|
|
486
|
+
## Policy Design Principles
|
|
487
|
+
|
|
488
|
+
1. **Forbid the catastrophic, then iterate.** Start by blocking `rm`, `sudo`, and data exfil domains. You can always add more forbids later.
|
|
489
|
+
|
|
490
|
+
2. **Forbid always wins.** In Cedar, a `forbid` policy overrides any `permit`. This means you can write broad permits and surgical forbids without worrying about order or precedence.
|
|
491
|
+
|
|
492
|
+
3. **Binary name is the gate, not the arguments.** `Shell::"git"` permits *all* git commands including `git push --force`. If you need argument-level control, use Cedar `when` conditions on `context.args`:
|
|
493
|
+
|
|
494
|
+
```cedar
|
|
495
|
+
forbid(
|
|
496
|
+
principal,
|
|
497
|
+
action == Jans::Action::"exec_command",
|
|
498
|
+
resource == Jans::Shell::"git"
|
|
499
|
+
) when {
|
|
500
|
+
context.args like "*--force*"
|
|
501
|
+
};
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
4. **Domain name is the gate, not the path.** `API::"api.github.com"` permits all endpoints on that domain. If you need path-level control, use `when` conditions on `context.url`.
|
|
505
|
+
|
|
506
|
+
5. **Deny-all is aspirational.** Most people should start with allow-all + surgical forbids. Switch to deny-all only when you understand your agent's full tool surface.
|
|
507
|
+
|
|
508
|
+
6. **Review regularly.** Your agent's needs change. Policies that made sense last month might be too loose or too tight today. The GUI makes this easy — open it, look at what's enabled, adjust.
|
|
509
|
+
|
|
510
|
+
---
|
|
511
|
+
|
|
512
|
+
## Further Reading
|
|
513
|
+
|
|
514
|
+
- [Cedar for AI Agents: Why Your AI Agent Needs a Policy Language](https://clawdrey.com/blog/cedar-for-ai-agents-part-1-why-your-ai-agent-needs-a-policy-language.html)
|
|
515
|
+
- [Cedar for AI Agents: Writing Your First Agent Policy](https://clawdrey.com/blog/cedar-for-ai-agents-part-2-writing-your-first-agent-policy.html)
|
|
516
|
+
- [Cedar for AI Agents: When Forbid Meets Permit](https://clawdrey.com/blog/cedar-for-ai-agents-part-3-when-forbid-meets-permit.html)
|
|
517
|
+
- [Cedar for AI Agents: Proving It — SMT Solvers and Why I Trust Math More Than Tests](https://clawdrey.com/blog/proving-it-smt-solvers-and-why-i-trust-math-more-than-tests.html)
|
|
518
|
+
- [Cedar Language Reference](https://docs.cedarpolicy.com/)
|
|
519
|
+
- [Carapace README](../README.md)
|
package/package.json
CHANGED
|
@@ -144,6 +144,12 @@ export class CedarlingEngine {
|
|
|
144
144
|
.replace(/.*::"/g, "")
|
|
145
145
|
.replace(/"$/, "");
|
|
146
146
|
|
|
147
|
+
// Determine resource entity type from the request string
|
|
148
|
+
// Supports Tool::"x", Shell::"x", API::"x", etc.
|
|
149
|
+
let resourceEntityType = "Tool";
|
|
150
|
+
const typeMatch = request.resource.match(/^(?:\w+::)?(\w+)::/);
|
|
151
|
+
if (typeMatch) resourceEntityType = typeMatch[1];
|
|
152
|
+
|
|
147
153
|
const result = await this.cedarling.authorize_unsigned({
|
|
148
154
|
principals: [
|
|
149
155
|
{
|
|
@@ -157,7 +163,7 @@ export class CedarlingEngine {
|
|
|
157
163
|
action: `${this.namespace}::Action::"${actionName}"`,
|
|
158
164
|
resource: {
|
|
159
165
|
cedar_entity_mapping: {
|
|
160
|
-
entity_type: `${this.namespace}
|
|
166
|
+
entity_type: `${this.namespace}::${resourceEntityType}`,
|
|
161
167
|
id: resourceId,
|
|
162
168
|
},
|
|
163
169
|
...(request.context ?? {}),
|
|
@@ -189,37 +195,47 @@ export class CedarlingEngine {
|
|
|
189
195
|
}
|
|
190
196
|
|
|
191
197
|
/**
|
|
192
|
-
* Enable a
|
|
198
|
+
* Enable a resource by adding a permit policy and rebuilding Cedarling.
|
|
199
|
+
* resourceType: "Tool" | "Shell" | "API"
|
|
200
|
+
* action: the Cedar action name (e.g., "call_tool", "exec_command", "call_api")
|
|
193
201
|
*/
|
|
194
|
-
|
|
195
|
-
const
|
|
196
|
-
const
|
|
202
|
+
enableResource(qualifiedName: string, resourceType: string = "Tool", action: string = "call_tool"): void {
|
|
203
|
+
const slug = qualifiedName.replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
204
|
+
const policyId = `${resourceType.toLowerCase()}-enable-${slug}`;
|
|
205
|
+
const raw = `permit(\n principal is ${this.namespace}::${this.agentEntityType},\n action == ${this.namespace}::Action::"${action}",\n resource == ${this.namespace}::${resourceType}::"${qualifiedName}"\n);`;
|
|
197
206
|
|
|
198
|
-
|
|
199
|
-
const disableId = `tool-disable-${qualifiedName.replace(/\//g, "-")}`;
|
|
207
|
+
const disableId = `${resourceType.toLowerCase()}-disable-${slug}`;
|
|
200
208
|
this.removePolicyFile(disableId);
|
|
201
209
|
|
|
202
210
|
this.writePolicyFile(policyId, raw);
|
|
203
211
|
this.policies.set(policyId, { effect: "permit", raw });
|
|
204
212
|
this.rebuildCedarling().catch(() => {});
|
|
205
|
-
this.logger.info(`Enabled
|
|
213
|
+
this.logger.info(`Enabled ${resourceType}: ${qualifiedName}`);
|
|
206
214
|
}
|
|
207
215
|
|
|
208
216
|
/**
|
|
209
|
-
* Disable a
|
|
217
|
+
* Disable a resource by adding a forbid policy and rebuilding Cedarling.
|
|
210
218
|
*/
|
|
211
|
-
|
|
212
|
-
const
|
|
213
|
-
const
|
|
219
|
+
disableResource(qualifiedName: string, resourceType: string = "Tool", action: string = "call_tool"): void {
|
|
220
|
+
const slug = qualifiedName.replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
221
|
+
const policyId = `${resourceType.toLowerCase()}-disable-${slug}`;
|
|
222
|
+
const raw = `forbid(\n principal,\n action == ${this.namespace}::Action::"${action}",\n resource == ${this.namespace}::${resourceType}::"${qualifiedName}"\n);`;
|
|
214
223
|
|
|
215
|
-
|
|
216
|
-
const enableId = `tool-enable-${qualifiedName.replace(/\//g, "-")}`;
|
|
224
|
+
const enableId = `${resourceType.toLowerCase()}-enable-${slug}`;
|
|
217
225
|
this.removePolicyFile(enableId);
|
|
218
226
|
|
|
219
227
|
this.writePolicyFile(policyId, raw);
|
|
220
228
|
this.policies.set(policyId, { effect: "forbid", raw });
|
|
221
229
|
this.rebuildCedarling().catch(() => {});
|
|
222
|
-
this.logger.info(`Disabled
|
|
230
|
+
this.logger.info(`Disabled ${resourceType}: ${qualifiedName}`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Backwards-compatible aliases */
|
|
234
|
+
enableTool(qualifiedName: string): void {
|
|
235
|
+
this.enableResource(qualifiedName, "Tool", "call_tool");
|
|
236
|
+
}
|
|
237
|
+
disableTool(qualifiedName: string): void {
|
|
238
|
+
this.disableResource(qualifiedName, "Tool", "call_tool");
|
|
223
239
|
}
|
|
224
240
|
|
|
225
241
|
/**
|
|
@@ -293,27 +309,29 @@ export class CedarlingEngine {
|
|
|
293
309
|
};
|
|
294
310
|
}
|
|
295
311
|
|
|
296
|
-
// Verification: try
|
|
297
|
-
// schemas and policies are valid.
|
|
312
|
+
// Verification: try dummy authorize requests for each resource type.
|
|
313
|
+
// If the policy store loaded, schemas and policies are valid.
|
|
298
314
|
try {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
315
|
+
for (const [action, resType] of [["call_tool", "Tool"], ["exec_command", "Shell"], ["call_api", "API"]]) {
|
|
316
|
+
await this.cedarling.authorize_unsigned({
|
|
317
|
+
principals: [
|
|
318
|
+
{
|
|
319
|
+
cedar_entity_mapping: {
|
|
320
|
+
entity_type: `${this.namespace}::${this.agentEntityType}`,
|
|
321
|
+
id: "__verify_probe__",
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
],
|
|
325
|
+
action: `${this.namespace}::Action::"${action}"`,
|
|
326
|
+
resource: {
|
|
302
327
|
cedar_entity_mapping: {
|
|
303
|
-
entity_type: `${this.namespace}::${
|
|
328
|
+
entity_type: `${this.namespace}::${resType}`,
|
|
304
329
|
id: "__verify_probe__",
|
|
305
330
|
},
|
|
306
331
|
},
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
cedar_entity_mapping: {
|
|
311
|
-
entity_type: `${this.namespace}::Tool`,
|
|
312
|
-
id: "__verify_probe__",
|
|
313
|
-
},
|
|
314
|
-
},
|
|
315
|
-
context: {},
|
|
316
|
-
});
|
|
332
|
+
context: {},
|
|
333
|
+
});
|
|
334
|
+
}
|
|
317
335
|
return { ok: true, issues: [], durationMs: Date.now() - start };
|
|
318
336
|
} catch (err: any) {
|
|
319
337
|
return {
|
|
@@ -491,6 +509,45 @@ export class CedarlingEngine {
|
|
|
491
509
|
},
|
|
492
510
|
},
|
|
493
511
|
},
|
|
512
|
+
Shell: {
|
|
513
|
+
shape: {
|
|
514
|
+
type: "Record",
|
|
515
|
+
attributes: {
|
|
516
|
+
command: {
|
|
517
|
+
type: "EntityOrCommon",
|
|
518
|
+
name: "String",
|
|
519
|
+
required: false,
|
|
520
|
+
},
|
|
521
|
+
workdir: {
|
|
522
|
+
type: "EntityOrCommon",
|
|
523
|
+
name: "String",
|
|
524
|
+
required: false,
|
|
525
|
+
},
|
|
526
|
+
},
|
|
527
|
+
},
|
|
528
|
+
},
|
|
529
|
+
API: {
|
|
530
|
+
shape: {
|
|
531
|
+
type: "Record",
|
|
532
|
+
attributes: {
|
|
533
|
+
url: {
|
|
534
|
+
type: "EntityOrCommon",
|
|
535
|
+
name: "String",
|
|
536
|
+
required: false,
|
|
537
|
+
},
|
|
538
|
+
method: {
|
|
539
|
+
type: "EntityOrCommon",
|
|
540
|
+
name: "String",
|
|
541
|
+
required: false,
|
|
542
|
+
},
|
|
543
|
+
domain: {
|
|
544
|
+
type: "EntityOrCommon",
|
|
545
|
+
name: "String",
|
|
546
|
+
required: false,
|
|
547
|
+
},
|
|
548
|
+
},
|
|
549
|
+
},
|
|
550
|
+
},
|
|
494
551
|
},
|
|
495
552
|
actions: {
|
|
496
553
|
call_tool: {
|
|
@@ -507,6 +564,53 @@ export class CedarlingEngine {
|
|
|
507
564
|
context: { type: "Record", attributes: {} },
|
|
508
565
|
},
|
|
509
566
|
},
|
|
567
|
+
exec_command: {
|
|
568
|
+
appliesTo: {
|
|
569
|
+
principalTypes: [this.agentEntityType],
|
|
570
|
+
resourceTypes: ["Shell"],
|
|
571
|
+
context: {
|
|
572
|
+
type: "Record",
|
|
573
|
+
attributes: {
|
|
574
|
+
args: {
|
|
575
|
+
type: "EntityOrCommon",
|
|
576
|
+
name: "String",
|
|
577
|
+
required: false,
|
|
578
|
+
},
|
|
579
|
+
workdir: {
|
|
580
|
+
type: "EntityOrCommon",
|
|
581
|
+
name: "String",
|
|
582
|
+
required: false,
|
|
583
|
+
},
|
|
584
|
+
},
|
|
585
|
+
},
|
|
586
|
+
},
|
|
587
|
+
},
|
|
588
|
+
call_api: {
|
|
589
|
+
appliesTo: {
|
|
590
|
+
principalTypes: [this.agentEntityType],
|
|
591
|
+
resourceTypes: ["API"],
|
|
592
|
+
context: {
|
|
593
|
+
type: "Record",
|
|
594
|
+
attributes: {
|
|
595
|
+
url: {
|
|
596
|
+
type: "EntityOrCommon",
|
|
597
|
+
name: "String",
|
|
598
|
+
required: false,
|
|
599
|
+
},
|
|
600
|
+
method: {
|
|
601
|
+
type: "EntityOrCommon",
|
|
602
|
+
name: "String",
|
|
603
|
+
required: false,
|
|
604
|
+
},
|
|
605
|
+
body: {
|
|
606
|
+
type: "EntityOrCommon",
|
|
607
|
+
name: "String",
|
|
608
|
+
required: false,
|
|
609
|
+
},
|
|
610
|
+
},
|
|
611
|
+
},
|
|
612
|
+
},
|
|
613
|
+
},
|
|
510
614
|
},
|
|
511
615
|
},
|
|
512
616
|
};
|
package/src/index.ts
CHANGED
|
@@ -64,6 +64,53 @@ export default function register(api: OpenClawPluginApi) {
|
|
|
64
64
|
logger,
|
|
65
65
|
});
|
|
66
66
|
|
|
67
|
+
// --- Bypass detection: warn if built-in tools aren't denied ---
|
|
68
|
+
const BYPASS_TOOLS = ["exec", "web_fetch", "web_search"];
|
|
69
|
+
|
|
70
|
+
function checkForBypasses(): string[] {
|
|
71
|
+
// Read OpenClaw config to check tools.deny
|
|
72
|
+
try {
|
|
73
|
+
const { readFileSync, existsSync } = require("node:fs");
|
|
74
|
+
const { join } = require("node:path");
|
|
75
|
+
const { homedir } = require("node:os");
|
|
76
|
+
const configPath = join(homedir(), ".openclaw", "openclaw.json");
|
|
77
|
+
if (!existsSync(configPath)) return BYPASS_TOOLS;
|
|
78
|
+
const cfg = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
79
|
+
const denied: string[] = cfg.tools?.deny ?? [];
|
|
80
|
+
return BYPASS_TOOLS.filter((t) => !denied.includes(t));
|
|
81
|
+
} catch {
|
|
82
|
+
return BYPASS_TOOLS;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function patchConfigDenyTools(): { patched: string[]; alreadyDenied: string[] } {
|
|
87
|
+
const { readFileSync, writeFileSync, existsSync } = require("node:fs");
|
|
88
|
+
const { join } = require("node:path");
|
|
89
|
+
const { homedir } = require("node:os");
|
|
90
|
+
const configPath = join(homedir(), ".openclaw", "openclaw.json");
|
|
91
|
+
|
|
92
|
+
let cfg: any = {};
|
|
93
|
+
if (existsSync(configPath)) {
|
|
94
|
+
cfg = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!cfg.tools) cfg.tools = {};
|
|
98
|
+
if (!cfg.tools.deny) cfg.tools.deny = [];
|
|
99
|
+
|
|
100
|
+
const alreadyDenied = BYPASS_TOOLS.filter((t) => cfg.tools.deny.includes(t));
|
|
101
|
+
const toAdd = BYPASS_TOOLS.filter((t) => !cfg.tools.deny.includes(t));
|
|
102
|
+
|
|
103
|
+
for (const tool of toAdd) {
|
|
104
|
+
cfg.tools.deny.push(tool);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (toAdd.length > 0) {
|
|
108
|
+
writeFileSync(configPath, JSON.stringify(cfg, null, 2) + "\n", "utf-8");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return { patched: toAdd, alreadyDenied };
|
|
112
|
+
}
|
|
113
|
+
|
|
67
114
|
// --- Background service: connect to MCP servers and serve GUI ---
|
|
68
115
|
api.registerService({
|
|
69
116
|
id: "carapace",
|
|
@@ -73,6 +120,16 @@ export default function register(api: OpenClawPluginApi) {
|
|
|
73
120
|
await aggregator.connectAll();
|
|
74
121
|
await gui.start();
|
|
75
122
|
logger.info(`Control GUI at http://localhost:${config.guiPort ?? 19820}`);
|
|
123
|
+
|
|
124
|
+
// Check for bypass vulnerabilities
|
|
125
|
+
const bypasses = checkForBypasses();
|
|
126
|
+
if (bypasses.length > 0) {
|
|
127
|
+
logger.warn(
|
|
128
|
+
`⚠️ BYPASS RISK: Built-in tools [${bypasses.join(", ")}] are NOT denied. ` +
|
|
129
|
+
`Agents can use these to bypass Carapace Cedar policies. ` +
|
|
130
|
+
`Run "openclaw carapace setup" to fix this automatically.`
|
|
131
|
+
);
|
|
132
|
+
}
|
|
76
133
|
},
|
|
77
134
|
async stop() {
|
|
78
135
|
await gui.stop();
|
|
@@ -158,6 +215,184 @@ export default function register(api: OpenClawPluginApi) {
|
|
|
158
215
|
},
|
|
159
216
|
});
|
|
160
217
|
|
|
218
|
+
// --- Agent tool: execute a shell command through Cedar authorization ---
|
|
219
|
+
api.registerTool({
|
|
220
|
+
name: "carapace_exec",
|
|
221
|
+
label: "Shell Exec (Carapace)",
|
|
222
|
+
description:
|
|
223
|
+
"Execute a shell command through the Carapace Cedar proxy. The command is authorized by Cedar policies before execution. Use this when you want Cedar-gated shell access.",
|
|
224
|
+
parameters: {
|
|
225
|
+
type: "object",
|
|
226
|
+
required: ["command"],
|
|
227
|
+
properties: {
|
|
228
|
+
command: {
|
|
229
|
+
type: "string",
|
|
230
|
+
description: "The shell command to execute (e.g., 'git status', 'npm install')",
|
|
231
|
+
},
|
|
232
|
+
workdir: {
|
|
233
|
+
type: "string",
|
|
234
|
+
description: "Working directory for the command (optional)",
|
|
235
|
+
},
|
|
236
|
+
timeout: {
|
|
237
|
+
type: "number",
|
|
238
|
+
description: "Timeout in seconds (default: 30)",
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
async execute(_toolCallId: string, params: { command: string; workdir?: string; timeout?: number }) {
|
|
243
|
+
const { command, workdir, timeout = 30 } = params;
|
|
244
|
+
|
|
245
|
+
// Extract the binary name for policy matching
|
|
246
|
+
const binary = command.trim().split(/\s+/)[0].replace(/^.*\//, "");
|
|
247
|
+
|
|
248
|
+
// Authorize via Cedar
|
|
249
|
+
const decision = await cedar.authorize({
|
|
250
|
+
principal: `Agent::"openclaw"`,
|
|
251
|
+
action: `Action::"exec_command"`,
|
|
252
|
+
resource: `Shell::"${binary}"`,
|
|
253
|
+
context: { args: command, workdir: workdir ?? "" },
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
if (decision.decision === "deny") {
|
|
257
|
+
return {
|
|
258
|
+
content: [
|
|
259
|
+
{
|
|
260
|
+
type: "text",
|
|
261
|
+
text: `DENIED by Cedar policy: shell command "${binary}"\nFull command: ${command}\nReason: ${decision.reasons.join(", ") || "default deny"}`,
|
|
262
|
+
},
|
|
263
|
+
],
|
|
264
|
+
isError: true,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Execute the command
|
|
269
|
+
try {
|
|
270
|
+
const { execSync } = await import("node:child_process");
|
|
271
|
+
const result = execSync(command, {
|
|
272
|
+
cwd: workdir,
|
|
273
|
+
timeout: timeout * 1000,
|
|
274
|
+
maxBuffer: 1024 * 1024,
|
|
275
|
+
encoding: "utf-8",
|
|
276
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
277
|
+
});
|
|
278
|
+
return {
|
|
279
|
+
content: [{ type: "text", text: result }],
|
|
280
|
+
};
|
|
281
|
+
} catch (err: any) {
|
|
282
|
+
const output = err.stdout ?? err.stderr ?? err.message;
|
|
283
|
+
return {
|
|
284
|
+
content: [{ type: "text", text: `Command failed (exit ${err.status ?? "?"}): ${output}` }],
|
|
285
|
+
isError: true,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
}, { optional: true });
|
|
290
|
+
|
|
291
|
+
// --- Agent tool: make an HTTP API call through Cedar authorization ---
|
|
292
|
+
api.registerTool({
|
|
293
|
+
name: "carapace_fetch",
|
|
294
|
+
label: "API Fetch (Carapace)",
|
|
295
|
+
description:
|
|
296
|
+
"Make an HTTP API call through the Carapace Cedar proxy. The request is authorized by Cedar policies before being sent. Use this when you want Cedar-gated outbound API access.",
|
|
297
|
+
parameters: {
|
|
298
|
+
type: "object",
|
|
299
|
+
required: ["url"],
|
|
300
|
+
properties: {
|
|
301
|
+
url: {
|
|
302
|
+
type: "string",
|
|
303
|
+
description: "The URL to fetch",
|
|
304
|
+
},
|
|
305
|
+
method: {
|
|
306
|
+
type: "string",
|
|
307
|
+
description: "HTTP method (GET, POST, PUT, DELETE, PATCH). Default: GET",
|
|
308
|
+
},
|
|
309
|
+
headers: {
|
|
310
|
+
type: "object",
|
|
311
|
+
description: "HTTP headers to include",
|
|
312
|
+
},
|
|
313
|
+
body: {
|
|
314
|
+
type: "string",
|
|
315
|
+
description: "Request body (for POST/PUT/PATCH)",
|
|
316
|
+
},
|
|
317
|
+
timeout: {
|
|
318
|
+
type: "number",
|
|
319
|
+
description: "Timeout in seconds (default: 30)",
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
async execute(_toolCallId: string, params: {
|
|
324
|
+
url: string; method?: string; headers?: Record<string, string>; body?: string; timeout?: number
|
|
325
|
+
}) {
|
|
326
|
+
const { url, method = "GET", headers = {}, body, timeout = 30 } = params;
|
|
327
|
+
|
|
328
|
+
// Extract domain for policy matching
|
|
329
|
+
let domain: string;
|
|
330
|
+
try {
|
|
331
|
+
domain = new URL(url).hostname;
|
|
332
|
+
} catch {
|
|
333
|
+
return {
|
|
334
|
+
content: [{ type: "text", text: `Invalid URL: ${url}` }],
|
|
335
|
+
isError: true,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Authorize via Cedar
|
|
340
|
+
const decision = await cedar.authorize({
|
|
341
|
+
principal: `Agent::"openclaw"`,
|
|
342
|
+
action: `Action::"call_api"`,
|
|
343
|
+
resource: `API::"${domain}"`,
|
|
344
|
+
context: { url, method, body: body ?? "" },
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
if (decision.decision === "deny") {
|
|
348
|
+
return {
|
|
349
|
+
content: [
|
|
350
|
+
{
|
|
351
|
+
type: "text",
|
|
352
|
+
text: `DENIED by Cedar policy: API call to "${domain}"\nURL: ${url}\nMethod: ${method}\nReason: ${decision.reasons.join(", ") || "default deny"}`,
|
|
353
|
+
},
|
|
354
|
+
],
|
|
355
|
+
isError: true,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Make the HTTP request
|
|
360
|
+
try {
|
|
361
|
+
const controller = new AbortController();
|
|
362
|
+
const timer = setTimeout(() => controller.abort(), timeout * 1000);
|
|
363
|
+
|
|
364
|
+
const response = await fetch(url, {
|
|
365
|
+
method,
|
|
366
|
+
headers,
|
|
367
|
+
body: body ?? undefined,
|
|
368
|
+
signal: controller.signal,
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
clearTimeout(timer);
|
|
372
|
+
|
|
373
|
+
const responseText = await response.text();
|
|
374
|
+
const truncated = responseText.length > 50000
|
|
375
|
+
? responseText.slice(0, 50000) + "\n...[truncated]"
|
|
376
|
+
: responseText;
|
|
377
|
+
|
|
378
|
+
return {
|
|
379
|
+
content: [
|
|
380
|
+
{
|
|
381
|
+
type: "text",
|
|
382
|
+
text: `HTTP ${response.status} ${response.statusText}\n\n${truncated}`,
|
|
383
|
+
},
|
|
384
|
+
],
|
|
385
|
+
isError: !response.ok,
|
|
386
|
+
};
|
|
387
|
+
} catch (err: any) {
|
|
388
|
+
return {
|
|
389
|
+
content: [{ type: "text", text: `API call failed: ${err.message}` }],
|
|
390
|
+
isError: true,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
},
|
|
394
|
+
}, { optional: true });
|
|
395
|
+
|
|
161
396
|
// --- CLI command ---
|
|
162
397
|
api.registerCli?.(
|
|
163
398
|
({ program }) => {
|
|
@@ -195,6 +430,58 @@ export default function register(api: OpenClawPluginApi) {
|
|
|
195
430
|
}
|
|
196
431
|
}
|
|
197
432
|
});
|
|
433
|
+
|
|
434
|
+
cmd.command("setup")
|
|
435
|
+
.description("Configure OpenClaw to route exec/fetch through Carapace (denies built-in bypass tools)")
|
|
436
|
+
.action(async () => {
|
|
437
|
+
console.log("\n🦞 Carapace Setup\n");
|
|
438
|
+
|
|
439
|
+
const bypasses = checkForBypasses();
|
|
440
|
+
if (bypasses.length === 0) {
|
|
441
|
+
console.log(" ✅ All bypass tools are already denied. No changes needed.\n");
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
console.log(" Built-in tools that bypass Carapace Cedar policies:");
|
|
446
|
+
for (const tool of bypasses) {
|
|
447
|
+
console.log(` ⚠️ ${tool} — agents can use this to skip Cedar authorization`);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
console.log("\n This will add the following to your OpenClaw config:");
|
|
451
|
+
console.log(` tools.deny: [${bypasses.map(t => `"${t}"`).join(", ")}]`);
|
|
452
|
+
console.log("\n After setup, agents must use carapace_exec and carapace_fetch");
|
|
453
|
+
console.log(" instead, which enforce Cedar policies on every call.\n");
|
|
454
|
+
|
|
455
|
+
const { patched, alreadyDenied } = patchConfigDenyTools();
|
|
456
|
+
|
|
457
|
+
if (alreadyDenied.length > 0) {
|
|
458
|
+
console.log(` Already denied: ${alreadyDenied.join(", ")}`);
|
|
459
|
+
}
|
|
460
|
+
if (patched.length > 0) {
|
|
461
|
+
console.log(` ✅ Added to tools.deny: ${patched.join(", ")}`);
|
|
462
|
+
console.log("\n Restart the gateway for changes to take effect:");
|
|
463
|
+
console.log(" openclaw gateway restart\n");
|
|
464
|
+
} else {
|
|
465
|
+
console.log(" ✅ No changes needed.\n");
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
cmd.command("check")
|
|
470
|
+
.description("Check for bypass vulnerabilities (built-in tools that skip Cedar)")
|
|
471
|
+
.action(async () => {
|
|
472
|
+
console.log("\n🦞 Carapace Security Check\n");
|
|
473
|
+
const bypasses = checkForBypasses();
|
|
474
|
+
if (bypasses.length === 0) {
|
|
475
|
+
console.log(" ✅ No bypass vulnerabilities found.");
|
|
476
|
+
console.log(" All agent exec/fetch operations go through Cedar.\n");
|
|
477
|
+
} else {
|
|
478
|
+
console.log(" ⚠️ Bypass vulnerabilities found:\n");
|
|
479
|
+
for (const tool of bypasses) {
|
|
480
|
+
console.log(` 🔓 ${tool} — agents can bypass Cedar policies via this tool`);
|
|
481
|
+
}
|
|
482
|
+
console.log(`\n Run "openclaw carapace setup" to fix.\n`);
|
|
483
|
+
}
|
|
484
|
+
});
|
|
198
485
|
},
|
|
199
486
|
{ commands: ["carapace"] },
|
|
200
487
|
);
|
package/src/types.ts
CHANGED
|
@@ -42,6 +42,32 @@ export interface McpTool {
|
|
|
42
42
|
enabled: boolean;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/** A gated resource — tools, shell commands, or APIs */
|
|
46
|
+
export interface GatedResource {
|
|
47
|
+
id: string; // unique identifier (e.g., "filesystem/read_file", "bash", "api.github.com")
|
|
48
|
+
type: "tool" | "shell" | "api";
|
|
49
|
+
name: string; // display name
|
|
50
|
+
description: string;
|
|
51
|
+
source: string; // server name, "local", or domain
|
|
52
|
+
enabled: boolean;
|
|
53
|
+
metadata?: Record<string, unknown>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface ShellRule {
|
|
57
|
+
id: string; // e.g., "bash", "git", "npm"
|
|
58
|
+
pattern: string; // command pattern (binary name or glob)
|
|
59
|
+
description: string;
|
|
60
|
+
enabled: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface ApiRule {
|
|
64
|
+
id: string; // e.g., "api.github.com", "registry.npmjs.org"
|
|
65
|
+
pattern: string; // URL pattern (domain or prefix)
|
|
66
|
+
method?: string; // HTTP method filter (optional)
|
|
67
|
+
description: string;
|
|
68
|
+
enabled: boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
45
71
|
export interface ServerStatus {
|
|
46
72
|
connected: boolean;
|
|
47
73
|
toolCount: number;
|