@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 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 sits between your AI agent and its MCP tools. It aggregates multiple MCP servers, discovers their tools, and enforces [Cedar](https://www.cedarpolicy.com/) authorization policies on every tool call — with a local GUI where humans can see and control everything.
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:** MCP gives agents access to tools. But who decides *which* tools an agent can use? 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
+ **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 tools. 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
+ **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
- | | | | (Cedar 4.4.2) | |---->| (GitHub) |
46
+ | mcp_call |---->| | (Cedar 4.4.2) | |---->| (GitHub) |
46
47
  | | | +----------------------+ | +-----------------+
47
- | | | +----------------------+ | | MCP Server C |
48
- | | | | Local Control GUI | |---->| (database) |
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 tool call flows through Cedar evaluation.** If the policy says deny, the call never reaches the upstream MCP server. The agent gets a clear denial message with the reason.
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. Open the control GUI
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
- ### 3. Enable tools
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
- ### 4. Create custom policies
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
- ### 5. Verify policies
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 tools work until you add `forbid` policies. Switch to `deny-all` when you're ready for least-privilege.
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 tool `{"tool": "...", "enabled": true}` |
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
- - **Tool argument validation** — Carapace authorizes *which* tool can be called, not *what arguments* are passed. (Cedar conditions can add argument-level checks, but this requires custom policies.)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawdreyhepburn/carapace",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Immutable policy boundaries for MCP tool access. Powered by Cedar + Cedarling WASM.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -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}::Tool`,
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 tool by adding a permit policy and rebuilding Cedarling.
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
- enableTool(qualifiedName: string): void {
195
- const policyId = `tool-enable-${qualifiedName.replace(/\//g, "-")}`;
196
- const raw = `permit(\n principal is ${this.namespace}::${this.agentEntityType},\n action == ${this.namespace}::Action::"call_tool",\n resource == ${this.namespace}::Tool::"${qualifiedName}"\n);`;
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
- // Remove any disable policy
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 tool: ${qualifiedName}`);
213
+ this.logger.info(`Enabled ${resourceType}: ${qualifiedName}`);
206
214
  }
207
215
 
208
216
  /**
209
- * Disable a tool by adding a forbid policy and rebuilding Cedarling.
217
+ * Disable a resource by adding a forbid policy and rebuilding Cedarling.
210
218
  */
211
- disableTool(qualifiedName: string): void {
212
- const policyId = `tool-disable-${qualifiedName.replace(/\//g, "-")}`;
213
- const raw = `forbid(\n principal,\n action == ${this.namespace}::Action::"call_tool",\n resource == ${this.namespace}::Tool::"${qualifiedName}"\n);`;
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
- // Remove any enable policy
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 tool: ${qualifiedName}`);
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 a dummy authorize request. If the policy store loaded,
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
- await this.cedarling.authorize_unsigned({
300
- principals: [
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}::${this.agentEntityType}`,
328
+ entity_type: `${this.namespace}::${resType}`,
304
329
  id: "__verify_probe__",
305
330
  },
306
331
  },
307
- ],
308
- action: `${this.namespace}::Action::"call_tool"`,
309
- resource: {
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;