@clawdreyhepburn/carapace 1.0.5 → 1.0.7
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/CHANGELOG.md +15 -0
- package/README.md +60 -166
- package/dist/cedar-engine-cedarling.js +19 -65
- package/dist/cedar-engine-cedarling.js.map +1 -1
- package/dist/gui/html.js +14 -0
- package/dist/gui/html.js.map +1 -1
- package/dist/gui/server.d.ts +2 -0
- package/dist/gui/server.js +6 -14
- package/dist/gui/server.js.map +1 -1
- package/dist/index.d.ts +20 -5
- package/dist/index.js +236 -337
- package/dist/index.js.map +1 -1
- package/dist/llm-proxy.d.ts +1 -2
- package/dist/llm-proxy.js +6 -10
- package/dist/llm-proxy.js.map +1 -1
- package/dist/policy-source.d.ts +13 -0
- package/dist/policy-source.js +23 -0
- package/dist/policy-source.js.map +1 -1
- package/dist/types.d.ts +4 -1
- package/docs/RECOMMENDED-POLICIES.md +2 -2
- package/docs/SECURITY.md +36 -136
- package/package.json +1 -1
- package/policies/00-schema.cedar +30 -0
- package/policies/10-shell-deny.cedar +51 -0
- package/policies/20-shell-permit.cedar +60 -0
- package/policies/30-shell-path-guards.cedar +86 -0
- package/policies/40-api-policy.cedar +70 -0
- package/policies/50-tool-policy.cedar +38 -0
- package/policies/schema.json +88 -0
- package/src/index.ts +26 -14
- package/src/llm-proxy.ts +13 -0
- package/src/policy-source.ts +22 -0
package/package.json
CHANGED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// 00-schema.cedar — Cedar schema declaration for Carapace deployment
|
|
2
|
+
//
|
|
3
|
+
// This is NOT a policy. It declares the entity types, actions, and
|
|
4
|
+
// context shapes that the policies in this directory reference.
|
|
5
|
+
//
|
|
6
|
+
// Carapace uses the Jans:: namespace by default
|
|
7
|
+
// (see cedar-engine-cedarling.ts: `namespace ?? "Jans"`).
|
|
8
|
+
//
|
|
9
|
+
// Entity types:
|
|
10
|
+
// - Workload: the principal making the request (the agent / sub-agent)
|
|
11
|
+
// - Shell: a shell binary, e.g. Jans::Shell::"rm"
|
|
12
|
+
// - Tool: an MCP tool, e.g. Jans::Tool::"filesystem/write_file"
|
|
13
|
+
// - API: an external API by hostname, e.g. Jans::API::"api.github.com"
|
|
14
|
+
//
|
|
15
|
+
// Actions:
|
|
16
|
+
// - exec_command: applies to Shell resources
|
|
17
|
+
// - call_tool: applies to Tool resources
|
|
18
|
+
// - call_api: applies to API resources
|
|
19
|
+
//
|
|
20
|
+
// Context fields (optional, populated by Carapace at evaluation time):
|
|
21
|
+
// - args: string — the full argv joined for shell/tool calls
|
|
22
|
+
// - method: string — HTTP method for API calls (GET, POST, etc.)
|
|
23
|
+
// - path: string — URL path for API calls
|
|
24
|
+
|
|
25
|
+
// NOTE: Cedar's schema is declared in a separate JSON or .cedarschema file
|
|
26
|
+
// in some deployments. Carapace synthesizes its schema at runtime in
|
|
27
|
+
// cedar-engine-cedarling.ts. This file is human-readable documentation
|
|
28
|
+
// of what that synthesized schema looks like; the real schema is built
|
|
29
|
+
// in code. We keep this file as 00-schema.cedar so policy authors can
|
|
30
|
+
// see the shape without digging into TypeScript.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// 10-shell-deny.cedar — Hard-deny shell binaries that are dangerous on this Mac mini.
|
|
2
|
+
//
|
|
3
|
+
// In Cedar, forbid wins over permit. These run unconditionally for any principal,
|
|
4
|
+
// so even if a future permit rule matches, these still deny.
|
|
5
|
+
//
|
|
6
|
+
// Categories:
|
|
7
|
+
// 1. Destructive filesystem ops — rm, rmdir, dd, mkfs, format, diskutil
|
|
8
|
+
// 2. Privilege escalation — sudo, su, launchctl, systemctl
|
|
9
|
+
// 3. Permission/ownership change — chmod, chown, chgrp
|
|
10
|
+
// 4. Credential access — security, ssh-keygen, gpg, op, pass
|
|
11
|
+
// 5. Shell wrappers (bypass risk) — bash, sh, zsh, env, xargs
|
|
12
|
+
// 6. Network recon — nmap, tcpdump, netcat, nc
|
|
13
|
+
|
|
14
|
+
// --- 1. Destructive filesystem ---
|
|
15
|
+
forbid(principal, action == Jans::Action::"exec_command", resource == Jans::Shell::"rm");
|
|
16
|
+
forbid(principal, action == Jans::Action::"exec_command", resource == Jans::Shell::"rmdir");
|
|
17
|
+
forbid(principal, action == Jans::Action::"exec_command", resource == Jans::Shell::"dd");
|
|
18
|
+
forbid(principal, action == Jans::Action::"exec_command", resource == Jans::Shell::"mkfs");
|
|
19
|
+
forbid(principal, action == Jans::Action::"exec_command", resource == Jans::Shell::"format");
|
|
20
|
+
forbid(principal, action == Jans::Action::"exec_command", resource == Jans::Shell::"diskutil");
|
|
21
|
+
|
|
22
|
+
// --- 2. Privilege escalation ---
|
|
23
|
+
forbid(principal, action == Jans::Action::"exec_command", resource == Jans::Shell::"sudo");
|
|
24
|
+
forbid(principal, action == Jans::Action::"exec_command", resource == Jans::Shell::"su");
|
|
25
|
+
forbid(principal, action == Jans::Action::"exec_command", resource == Jans::Shell::"launchctl");
|
|
26
|
+
forbid(principal, action == Jans::Action::"exec_command", resource == Jans::Shell::"systemctl");
|
|
27
|
+
|
|
28
|
+
// --- 3. Permission / ownership ---
|
|
29
|
+
forbid(principal, action == Jans::Action::"exec_command", resource == Jans::Shell::"chmod");
|
|
30
|
+
forbid(principal, action == Jans::Action::"exec_command", resource == Jans::Shell::"chown");
|
|
31
|
+
forbid(principal, action == Jans::Action::"exec_command", resource == Jans::Shell::"chgrp");
|
|
32
|
+
|
|
33
|
+
// --- 4. Credential access ---
|
|
34
|
+
forbid(principal, action == Jans::Action::"exec_command", resource == Jans::Shell::"security");
|
|
35
|
+
forbid(principal, action == Jans::Action::"exec_command", resource == Jans::Shell::"ssh-keygen");
|
|
36
|
+
forbid(principal, action == Jans::Action::"exec_command", resource == Jans::Shell::"gpg");
|
|
37
|
+
forbid(principal, action == Jans::Action::"exec_command", resource == Jans::Shell::"op");
|
|
38
|
+
forbid(principal, action == Jans::Action::"exec_command", resource == Jans::Shell::"pass");
|
|
39
|
+
|
|
40
|
+
// --- 5. Shell wrappers (close the bypass) ---
|
|
41
|
+
forbid(principal, action == Jans::Action::"exec_command", resource == Jans::Shell::"bash");
|
|
42
|
+
forbid(principal, action == Jans::Action::"exec_command", resource == Jans::Shell::"sh");
|
|
43
|
+
forbid(principal, action == Jans::Action::"exec_command", resource == Jans::Shell::"zsh");
|
|
44
|
+
forbid(principal, action == Jans::Action::"exec_command", resource == Jans::Shell::"env");
|
|
45
|
+
forbid(principal, action == Jans::Action::"exec_command", resource == Jans::Shell::"xargs");
|
|
46
|
+
|
|
47
|
+
// --- 6. Network recon ---
|
|
48
|
+
forbid(principal, action == Jans::Action::"exec_command", resource == Jans::Shell::"nmap");
|
|
49
|
+
forbid(principal, action == Jans::Action::"exec_command", resource == Jans::Shell::"tcpdump");
|
|
50
|
+
forbid(principal, action == Jans::Action::"exec_command", resource == Jans::Shell::"netcat");
|
|
51
|
+
forbid(principal, action == Jans::Action::"exec_command", resource == Jans::Shell::"nc");
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// 20-shell-permit.cedar — Allowlist of safe shell binaries for this workload.
|
|
2
|
+
//
|
|
3
|
+
// Carapace evaluates with default-deny. Without a matching `permit`, every shell
|
|
4
|
+
// call is denied. These are the binaries the Clawdrey deployment actually needs.
|
|
5
|
+
//
|
|
6
|
+
// We restrict to `principal is Jans::Workload` so only properly-typed agents
|
|
7
|
+
// can use these — humans calling shell directly bypass Carapace entirely.
|
|
8
|
+
|
|
9
|
+
// --- Read-only inspection ---
|
|
10
|
+
permit(principal is Jans::Workload, action == Jans::Action::"exec_command", resource == Jans::Shell::"cat");
|
|
11
|
+
permit(principal is Jans::Workload, action == Jans::Action::"exec_command", resource == Jans::Shell::"ls");
|
|
12
|
+
permit(principal is Jans::Workload, action == Jans::Action::"exec_command", resource == Jans::Shell::"grep");
|
|
13
|
+
permit(principal is Jans::Workload, action == Jans::Action::"exec_command", resource == Jans::Shell::"find");
|
|
14
|
+
permit(principal is Jans::Workload, action == Jans::Action::"exec_command", resource == Jans::Shell::"head");
|
|
15
|
+
permit(principal is Jans::Workload, action == Jans::Action::"exec_command", resource == Jans::Shell::"tail");
|
|
16
|
+
permit(principal is Jans::Workload, action == Jans::Action::"exec_command", resource == Jans::Shell::"wc");
|
|
17
|
+
permit(principal is Jans::Workload, action == Jans::Action::"exec_command", resource == Jans::Shell::"file");
|
|
18
|
+
permit(principal is Jans::Workload, action == Jans::Action::"exec_command", resource == Jans::Shell::"stat");
|
|
19
|
+
permit(principal is Jans::Workload, action == Jans::Action::"exec_command", resource == Jans::Shell::"which");
|
|
20
|
+
|
|
21
|
+
// --- Filesystem operations (non-destructive) ---
|
|
22
|
+
// Note: rm is forbidden in 10-shell-deny.cedar. Use `trash` instead — recoverable.
|
|
23
|
+
permit(principal is Jans::Workload, action == Jans::Action::"exec_command", resource == Jans::Shell::"mkdir");
|
|
24
|
+
permit(principal is Jans::Workload, action == Jans::Action::"exec_command", resource == Jans::Shell::"cp");
|
|
25
|
+
permit(principal is Jans::Workload, action == Jans::Action::"exec_command", resource == Jans::Shell::"mv");
|
|
26
|
+
permit(principal is Jans::Workload, action == Jans::Action::"exec_command", resource == Jans::Shell::"trash");
|
|
27
|
+
permit(principal is Jans::Workload, action == Jans::Action::"exec_command", resource == Jans::Shell::"touch");
|
|
28
|
+
permit(principal is Jans::Workload, action == Jans::Action::"exec_command", resource == Jans::Shell::"ln");
|
|
29
|
+
|
|
30
|
+
// --- Dev tools ---
|
|
31
|
+
permit(principal is Jans::Workload, action == Jans::Action::"exec_command", resource == Jans::Shell::"git");
|
|
32
|
+
permit(principal is Jans::Workload, action == Jans::Action::"exec_command", resource == Jans::Shell::"node");
|
|
33
|
+
permit(principal is Jans::Workload, action == Jans::Action::"exec_command", resource == Jans::Shell::"npm");
|
|
34
|
+
permit(principal is Jans::Workload, action == Jans::Action::"exec_command", resource == Jans::Shell::"npx");
|
|
35
|
+
permit(principal is Jans::Workload, action == Jans::Action::"exec_command", resource == Jans::Shell::"python3");
|
|
36
|
+
permit(principal is Jans::Workload, action == Jans::Action::"exec_command", resource == Jans::Shell::"pip3");
|
|
37
|
+
permit(principal is Jans::Workload, action == Jans::Action::"exec_command", resource == Jans::Shell::"cargo");
|
|
38
|
+
permit(principal is Jans::Workload, action == Jans::Action::"exec_command", resource == Jans::Shell::"rustc");
|
|
39
|
+
|
|
40
|
+
// --- macOS-specific helpers Clawdrey actually uses ---
|
|
41
|
+
permit(principal is Jans::Workload, action == Jans::Action::"exec_command", resource == Jans::Shell::"osascript");
|
|
42
|
+
permit(principal is Jans::Workload, action == Jans::Action::"exec_command", resource == Jans::Shell::"pbcopy");
|
|
43
|
+
permit(principal is Jans::Workload, action == Jans::Action::"exec_command", resource == Jans::Shell::"pbpaste");
|
|
44
|
+
permit(principal is Jans::Workload, action == Jans::Action::"exec_command", resource == Jans::Shell::"open");
|
|
45
|
+
permit(principal is Jans::Workload, action == Jans::Action::"exec_command", resource == Jans::Shell::"SwitchAudioSource");
|
|
46
|
+
permit(principal is Jans::Workload, action == Jans::Action::"exec_command", resource == Jans::Shell::"cliclick");
|
|
47
|
+
|
|
48
|
+
// --- Skills CLI tools (himalaya, gog, imsg, gh, jq, rg, ffmpeg, sox) ---
|
|
49
|
+
permit(principal is Jans::Workload, action == Jans::Action::"exec_command", resource == Jans::Shell::"himalaya");
|
|
50
|
+
permit(principal is Jans::Workload, action == Jans::Action::"exec_command", resource == Jans::Shell::"gog");
|
|
51
|
+
permit(principal is Jans::Workload, action == Jans::Action::"exec_command", resource == Jans::Shell::"imsg");
|
|
52
|
+
permit(principal is Jans::Workload, action == Jans::Action::"exec_command", resource == Jans::Shell::"gh");
|
|
53
|
+
permit(principal is Jans::Workload, action == Jans::Action::"exec_command", resource == Jans::Shell::"jq");
|
|
54
|
+
permit(principal is Jans::Workload, action == Jans::Action::"exec_command", resource == Jans::Shell::"rg");
|
|
55
|
+
permit(principal is Jans::Workload, action == Jans::Action::"exec_command", resource == Jans::Shell::"ffmpeg");
|
|
56
|
+
permit(principal is Jans::Workload, action == Jans::Action::"exec_command", resource == Jans::Shell::"ffprobe");
|
|
57
|
+
permit(principal is Jans::Workload, action == Jans::Action::"exec_command", resource == Jans::Shell::"sox");
|
|
58
|
+
permit(principal is Jans::Workload, action == Jans::Action::"exec_command", resource == Jans::Shell::"whisper");
|
|
59
|
+
permit(principal is Jans::Workload, action == Jans::Action::"exec_command", resource == Jans::Shell::"curl");
|
|
60
|
+
permit(principal is Jans::Workload, action == Jans::Action::"exec_command", resource == Jans::Shell::"wget");
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// 30-shell-path-guards.cedar — Conditional denies based on path arguments.
|
|
2
|
+
//
|
|
3
|
+
// These run AFTER 20-shell-permit.cedar in evaluation. Cedar's "forbid wins"
|
|
4
|
+
// semantics means a permit + a matching forbid still results in deny.
|
|
5
|
+
//
|
|
6
|
+
// Pattern: even when an otherwise-permitted shell binary is invoked, we
|
|
7
|
+
// forbid it if its arguments touch sensitive paths.
|
|
8
|
+
//
|
|
9
|
+
// `context.args` is set by Carapace at evaluation time (see cedar-engine
|
|
10
|
+
// for how args are flattened into a single string for `like` matching).
|
|
11
|
+
|
|
12
|
+
// --- Block reads of sensitive credential / config paths ---
|
|
13
|
+
forbid(
|
|
14
|
+
principal,
|
|
15
|
+
action == Jans::Action::"exec_command",
|
|
16
|
+
resource == Jans::Shell::"cat"
|
|
17
|
+
) when {
|
|
18
|
+
context has args &&
|
|
19
|
+
(context.args like "*/.ssh/*" ||
|
|
20
|
+
context.args like "*/.aws/*" ||
|
|
21
|
+
context.args like "*/.env*" ||
|
|
22
|
+
context.args like "*/.gnupg/*" ||
|
|
23
|
+
context.args like "*/.openclaw/credentials/*" ||
|
|
24
|
+
context.args like "*/.config/openclaw/*" ||
|
|
25
|
+
context.args like "*id_rsa*" ||
|
|
26
|
+
context.args like "*id_ed25519*")
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
forbid(
|
|
30
|
+
principal,
|
|
31
|
+
action == Jans::Action::"exec_command",
|
|
32
|
+
resource == Jans::Shell::"head"
|
|
33
|
+
) when {
|
|
34
|
+
context has args &&
|
|
35
|
+
(context.args like "*/.ssh/*" ||
|
|
36
|
+
context.args like "*/.aws/*" ||
|
|
37
|
+
context.args like "*/.openclaw/credentials/*")
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
forbid(
|
|
41
|
+
principal,
|
|
42
|
+
action == Jans::Action::"exec_command",
|
|
43
|
+
resource == Jans::Shell::"tail"
|
|
44
|
+
) when {
|
|
45
|
+
context has args &&
|
|
46
|
+
(context.args like "*/.ssh/*" ||
|
|
47
|
+
context.args like "*/.aws/*" ||
|
|
48
|
+
context.args like "*/.openclaw/credentials/*")
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// --- Block writes/copies/moves into protected directories ---
|
|
52
|
+
forbid(
|
|
53
|
+
principal,
|
|
54
|
+
action == Jans::Action::"exec_command",
|
|
55
|
+
resource == Jans::Shell::"cp"
|
|
56
|
+
) when {
|
|
57
|
+
context has args &&
|
|
58
|
+
(context.args like "*/.ssh/*" ||
|
|
59
|
+
context.args like "*/.aws/*" ||
|
|
60
|
+
context.args like "*/.openclaw/credentials/*" ||
|
|
61
|
+
context.args like "*/etc/*")
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
forbid(
|
|
65
|
+
principal,
|
|
66
|
+
action == Jans::Action::"exec_command",
|
|
67
|
+
resource == Jans::Shell::"mv"
|
|
68
|
+
) when {
|
|
69
|
+
context has args &&
|
|
70
|
+
(context.args like "*/.ssh/*" ||
|
|
71
|
+
context.args like "*/.aws/*" ||
|
|
72
|
+
context.args like "*/.openclaw/credentials/*" ||
|
|
73
|
+
context.args like "*/etc/*")
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// --- Block git config writes that could change SSH/credential paths ---
|
|
77
|
+
forbid(
|
|
78
|
+
principal,
|
|
79
|
+
action == Jans::Action::"exec_command",
|
|
80
|
+
resource == Jans::Shell::"git"
|
|
81
|
+
) when {
|
|
82
|
+
context has args &&
|
|
83
|
+
context.args like "*config*" &&
|
|
84
|
+
(context.args like "*credential.helper*" ||
|
|
85
|
+
context.args like "*core.sshCommand*")
|
|
86
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// 40-api-policy.cedar — Outbound HTTP/API gating.
|
|
2
|
+
//
|
|
3
|
+
// Carapace identifies APIs by hostname (Jans::API::"host.name") and
|
|
4
|
+
// gates them under Jans::Action::"call_api". Optional context.method
|
|
5
|
+
// can scope a permit to read-only verbs (GET/HEAD).
|
|
6
|
+
|
|
7
|
+
// --- Hard denies: known data exfiltration / paste services ---
|
|
8
|
+
forbid(principal, action == Jans::Action::"call_api", resource == Jans::API::"pastebin.com");
|
|
9
|
+
forbid(principal, action == Jans::Action::"call_api", resource == Jans::API::"hastebin.com");
|
|
10
|
+
forbid(principal, action == Jans::Action::"call_api", resource == Jans::API::"transfer.sh");
|
|
11
|
+
forbid(principal, action == Jans::Action::"call_api", resource == Jans::API::"file.io");
|
|
12
|
+
forbid(principal, action == Jans::Action::"call_api", resource == Jans::API::"webhook.site");
|
|
13
|
+
forbid(principal, action == Jans::Action::"call_api", resource == Jans::API::"requestbin.com");
|
|
14
|
+
forbid(principal, action == Jans::Action::"call_api", resource == Jans::API::"ngrok.io");
|
|
15
|
+
forbid(principal, action == Jans::Action::"call_api", resource == Jans::API::"pipedream.net");
|
|
16
|
+
|
|
17
|
+
// --- Hard denies: localhost / loopback (prevent SSRF / side-channel exfil) ---
|
|
18
|
+
forbid(principal, action == Jans::Action::"call_api", resource == Jans::API::"127.0.0.1");
|
|
19
|
+
forbid(principal, action == Jans::Action::"call_api", resource == Jans::API::"0.0.0.0");
|
|
20
|
+
forbid(principal, action == Jans::Action::"call_api", resource == Jans::API::"localhost");
|
|
21
|
+
|
|
22
|
+
// --- Permit: developer / package registries (full access) ---
|
|
23
|
+
permit(principal is Jans::Workload, action == Jans::Action::"call_api", resource == Jans::API::"api.github.com");
|
|
24
|
+
permit(principal is Jans::Workload, action == Jans::Action::"call_api", resource == Jans::API::"github.com");
|
|
25
|
+
permit(principal is Jans::Workload, action == Jans::Action::"call_api", resource == Jans::API::"raw.githubusercontent.com");
|
|
26
|
+
permit(principal is Jans::Workload, action == Jans::Action::"call_api", resource == Jans::API::"registry.npmjs.org");
|
|
27
|
+
permit(principal is Jans::Workload, action == Jans::Action::"call_api", resource == Jans::API::"pypi.org");
|
|
28
|
+
permit(principal is Jans::Workload, action == Jans::Action::"call_api", resource == Jans::API::"crates.io");
|
|
29
|
+
|
|
30
|
+
// --- Permit: model providers (Clawdrey's own brain APIs) ---
|
|
31
|
+
permit(principal is Jans::Workload, action == Jans::Action::"call_api", resource == Jans::API::"api.anthropic.com");
|
|
32
|
+
permit(principal is Jans::Workload, action == Jans::Action::"call_api", resource == Jans::API::"api.openai.com");
|
|
33
|
+
permit(principal is Jans::Workload, action == Jans::Action::"call_api", resource == Jans::API::"api.elevenlabs.io");
|
|
34
|
+
|
|
35
|
+
// --- Permit: read-only access to general web (GET/HEAD only) ---
|
|
36
|
+
// Specific safe documentation and reference sources we use a lot.
|
|
37
|
+
permit(
|
|
38
|
+
principal is Jans::Workload,
|
|
39
|
+
action == Jans::Action::"call_api",
|
|
40
|
+
resource == Jans::API::"docs.openclaw.ai"
|
|
41
|
+
) when {
|
|
42
|
+
context has method && (context.method == "GET" || context.method == "HEAD")
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
permit(
|
|
46
|
+
principal is Jans::Workload,
|
|
47
|
+
action == Jans::Action::"call_api",
|
|
48
|
+
resource == Jans::API::"learn.microsoft.com"
|
|
49
|
+
) when {
|
|
50
|
+
context has method && (context.method == "GET" || context.method == "HEAD")
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
permit(
|
|
54
|
+
principal is Jans::Workload,
|
|
55
|
+
action == Jans::Action::"call_api",
|
|
56
|
+
resource == Jans::API::"developer.mozilla.org"
|
|
57
|
+
) when {
|
|
58
|
+
context has method && (context.method == "GET" || context.method == "HEAD")
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// --- Permit: own infrastructure ---
|
|
62
|
+
permit(principal is Jans::Workload, action == Jans::Action::"call_api", resource == Jans::API::"clawdrey.com");
|
|
63
|
+
|
|
64
|
+
// --- Notes ---
|
|
65
|
+
// Posting to social platforms (api.x.com, api.linkedin.com, discord.com, slack.com)
|
|
66
|
+
// is intentionally not permit'd here — it must go through curated paths
|
|
67
|
+
// (e.g. ~/.openclaw/workspace/scripts/x-api.py for X) that have their own
|
|
68
|
+
// review surface. If you need to permit one of those for a specific scenario,
|
|
69
|
+
// add a focused permit policy with `principal is` constrained to a labeled
|
|
70
|
+
// agent type, not the broad Workload.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// 50-tool-policy.cedar — MCP tool gating.
|
|
2
|
+
//
|
|
3
|
+
// Carapace identifies MCP tools by qualified name (Jans::Tool::"server/tool")
|
|
4
|
+
// and gates them under Jans::Action::"call_tool".
|
|
5
|
+
//
|
|
6
|
+
// Strategy here: deny the destructive shapes explicitly, then permit
|
|
7
|
+
// everything else for properly-typed Workload principals. This is the
|
|
8
|
+
// inverse posture from shell (which is allowlist-only) because MCP tool
|
|
9
|
+
// surfaces vary widely between deployments and we don't want every
|
|
10
|
+
// new MCP server to require a policy update.
|
|
11
|
+
|
|
12
|
+
// --- Hard denies: bulk-destructive tool patterns ---
|
|
13
|
+
// Destructive filesystem
|
|
14
|
+
forbid(principal, action == Jans::Action::"call_tool", resource == Jans::Tool::"filesystem/delete_file");
|
|
15
|
+
forbid(principal, action == Jans::Action::"call_tool", resource == Jans::Tool::"filesystem/delete_directory");
|
|
16
|
+
forbid(principal, action == Jans::Action::"call_tool", resource == Jans::Tool::"filesystem/move_file");
|
|
17
|
+
|
|
18
|
+
// Mass email destruction / spam
|
|
19
|
+
forbid(principal, action == Jans::Action::"call_tool", resource == Jans::Tool::"email/delete_all");
|
|
20
|
+
forbid(principal, action == Jans::Action::"call_tool", resource == Jans::Tool::"email/empty_trash");
|
|
21
|
+
forbid(principal, action == Jans::Action::"call_tool", resource == Jans::Tool::"email/send_bulk");
|
|
22
|
+
|
|
23
|
+
// Database destruction / drift
|
|
24
|
+
forbid(principal, action == Jans::Action::"call_tool", resource == Jans::Tool::"database/drop_table");
|
|
25
|
+
forbid(principal, action == Jans::Action::"call_tool", resource == Jans::Tool::"database/truncate");
|
|
26
|
+
forbid(principal, action == Jans::Action::"call_tool", resource == Jans::Tool::"database/execute_sql");
|
|
27
|
+
|
|
28
|
+
// Cloud destruction
|
|
29
|
+
forbid(principal, action == Jans::Action::"call_tool", resource == Jans::Tool::"aws/delete_bucket");
|
|
30
|
+
forbid(principal, action == Jans::Action::"call_tool", resource == Jans::Tool::"aws/terminate_instance");
|
|
31
|
+
forbid(principal, action == Jans::Action::"call_tool", resource == Jans::Tool::"gcp/delete_project");
|
|
32
|
+
|
|
33
|
+
// --- Default-permit: any other MCP tool for properly-typed Workloads ---
|
|
34
|
+
// This is the catch-all. For Carapace's posture today (additive, not
|
|
35
|
+
// strict allowlist), permitting all `call_tool` for Workloads keeps
|
|
36
|
+
// daily flow unblocked while the explicit `forbid`s above carve out
|
|
37
|
+
// the dangerous shapes.
|
|
38
|
+
permit(principal is Jans::Workload, action == Jans::Action::"call_tool", resource);
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
{
|
|
2
|
+
"Jans": {
|
|
3
|
+
"entityTypes": {
|
|
4
|
+
"Workload": {
|
|
5
|
+
"shape": {
|
|
6
|
+
"type": "Record",
|
|
7
|
+
"attributes": {
|
|
8
|
+
"name": { "type": "EntityOrCommon", "name": "String", "required": false }
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"Agent": {
|
|
13
|
+
"shape": {
|
|
14
|
+
"type": "Record",
|
|
15
|
+
"attributes": {
|
|
16
|
+
"role": { "type": "EntityOrCommon", "name": "String", "required": false }
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"Shell": {
|
|
21
|
+
"shape": {
|
|
22
|
+
"type": "Record",
|
|
23
|
+
"attributes": {
|
|
24
|
+
"command": { "type": "EntityOrCommon", "name": "String", "required": false },
|
|
25
|
+
"workdir": { "type": "EntityOrCommon", "name": "String", "required": false }
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"Tool": {
|
|
30
|
+
"shape": {
|
|
31
|
+
"type": "Record",
|
|
32
|
+
"attributes": {
|
|
33
|
+
"server": { "type": "EntityOrCommon", "name": "String", "required": false },
|
|
34
|
+
"name": { "type": "EntityOrCommon", "name": "String", "required": false }
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"API": {
|
|
39
|
+
"shape": {
|
|
40
|
+
"type": "Record",
|
|
41
|
+
"attributes": {
|
|
42
|
+
"host": { "type": "EntityOrCommon", "name": "String", "required": false }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"actions": {
|
|
48
|
+
"exec_command": {
|
|
49
|
+
"appliesTo": {
|
|
50
|
+
"principalTypes": ["Workload", "Agent"],
|
|
51
|
+
"resourceTypes": ["Shell"],
|
|
52
|
+
"context": {
|
|
53
|
+
"type": "Record",
|
|
54
|
+
"attributes": {
|
|
55
|
+
"args": { "type": "EntityOrCommon", "name": "String", "required": false },
|
|
56
|
+
"workdir": { "type": "EntityOrCommon", "name": "String", "required": false }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"call_tool": {
|
|
62
|
+
"appliesTo": {
|
|
63
|
+
"principalTypes": ["Workload", "Agent"],
|
|
64
|
+
"resourceTypes": ["Tool"],
|
|
65
|
+
"context": {
|
|
66
|
+
"type": "Record",
|
|
67
|
+
"attributes": {
|
|
68
|
+
"args": { "type": "EntityOrCommon", "name": "String", "required": false }
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
"call_api": {
|
|
74
|
+
"appliesTo": {
|
|
75
|
+
"principalTypes": ["Workload", "Agent"],
|
|
76
|
+
"resourceTypes": ["API"],
|
|
77
|
+
"context": {
|
|
78
|
+
"type": "Record",
|
|
79
|
+
"attributes": {
|
|
80
|
+
"method": { "type": "EntityOrCommon", "name": "String", "required": false },
|
|
81
|
+
"path": { "type": "EntityOrCommon", "name": "String", "required": false }
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
* No proxy, no baseUrl redirect, no models.json patching.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { appendFileSync, mkdirSync, readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { homedir } from "node:os";
|
|
8
11
|
import { CedarlingEngine } from "./cedar-engine-cedarling.js";
|
|
9
12
|
import { McpAggregator } from "./mcp-aggregator.js";
|
|
10
13
|
import { ControlGui } from "./gui/server.js";
|
|
@@ -13,6 +16,18 @@ import type { PluginConfig } from "./types.js";
|
|
|
13
16
|
export const id = "carapace";
|
|
14
17
|
export const name = "Carapace";
|
|
15
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Resolve the OpenClaw home directory the same way the runtime does:
|
|
21
|
+
* honor $OPENCLAW_HOME when set, otherwise fall back to ~/.openclaw.
|
|
22
|
+
* Used by the audit log and the setup/uninstall CLI so they never
|
|
23
|
+
* read/write the wrong config for users with a custom OPENCLAW_HOME.
|
|
24
|
+
*/
|
|
25
|
+
function openclawHome(): string {
|
|
26
|
+
const override = process.env.OPENCLAW_HOME;
|
|
27
|
+
if (override && override.trim() !== "") return override;
|
|
28
|
+
return join(homedir(), ".openclaw");
|
|
29
|
+
}
|
|
30
|
+
|
|
16
31
|
/**
|
|
17
32
|
* OpenClaw plugin API shape (matches real runtime).
|
|
18
33
|
*/
|
|
@@ -36,6 +51,14 @@ interface OpenClawPluginApi {
|
|
|
36
51
|
opts?: { optional?: boolean },
|
|
37
52
|
): void;
|
|
38
53
|
registerHook?(hookName: string | string[], handler: (event: any) => Promise<any> | any, opts?: { name: string; description?: string; priority?: number }): void;
|
|
54
|
+
/**
|
|
55
|
+
* Typed hook registration. Exposed by the real OpenClaw runtime in full
|
|
56
|
+
* registration mode (see loader `registerTypedHook`). This is the API this
|
|
57
|
+
* plugin actually uses to install the `before_tool_call` hook — declared
|
|
58
|
+
* optional here because some test harnesses construct a stripped-down api
|
|
59
|
+
* shim without it.
|
|
60
|
+
*/
|
|
61
|
+
on?(hookName: string, handler: (event: any, ctx?: any) => Promise<any> | any, opts?: { name?: string; description?: string; priority?: number }): void;
|
|
39
62
|
registerCli?(fn: (ctx: { program: any }) => void, opts?: { commands: string[] }): void;
|
|
40
63
|
registerGatewayMethod?(name: string, handler: (ctx: { respond: (ok: boolean, data: any) => void }) => void): void;
|
|
41
64
|
}
|
|
@@ -64,10 +87,7 @@ function buildUpstreamConfig(proxyConfig: NonNullable<PluginConfig["proxy"]>): {
|
|
|
64
87
|
// Audit log
|
|
65
88
|
function appendAuditLog(entry: { timestamp: string; tool: string; decision: string; reasons: string[]; params?: any }): void {
|
|
66
89
|
try {
|
|
67
|
-
const
|
|
68
|
-
const { join } = require("node:path");
|
|
69
|
-
const { homedir } = require("node:os");
|
|
70
|
-
const logDir = join(homedir(), ".openclaw", "mcp-policies", "logs");
|
|
90
|
+
const logDir = join(openclawHome(), "mcp-policies", "logs");
|
|
71
91
|
mkdirSync(logDir, { recursive: true });
|
|
72
92
|
appendFileSync(join(logDir, "audit.log"), JSON.stringify(entry) + "\n", "utf-8");
|
|
73
93
|
} catch {}
|
|
@@ -442,13 +462,9 @@ export default function register(api: OpenClawPluginApi) {
|
|
|
442
462
|
cmd.command("setup")
|
|
443
463
|
.description("Enable Carapace plugin in OpenClaw config")
|
|
444
464
|
.action(async () => {
|
|
445
|
-
const { readFileSync, writeFileSync, existsSync } = require("node:fs");
|
|
446
|
-
const { join } = require("node:path");
|
|
447
|
-
const { homedir } = require("node:os");
|
|
448
|
-
|
|
449
465
|
console.log("\n🦞 Carapace Setup\n");
|
|
450
466
|
|
|
451
|
-
const configPath = join(
|
|
467
|
+
const configPath = join(openclawHome(), "openclaw.json");
|
|
452
468
|
let cfg: any = {};
|
|
453
469
|
if (existsSync(configPath)) {
|
|
454
470
|
cfg = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
@@ -483,13 +499,9 @@ export default function register(api: OpenClawPluginApi) {
|
|
|
483
499
|
cmd.command("uninstall")
|
|
484
500
|
.description("Disable Carapace plugin")
|
|
485
501
|
.action(async () => {
|
|
486
|
-
const { readFileSync, writeFileSync, existsSync } = require("node:fs");
|
|
487
|
-
const { join } = require("node:path");
|
|
488
|
-
const { homedir } = require("node:os");
|
|
489
|
-
|
|
490
502
|
console.log("\n🦞 Carapace Uninstall\n");
|
|
491
503
|
|
|
492
|
-
const configPath = join(
|
|
504
|
+
const configPath = join(openclawHome(), "openclaw.json");
|
|
493
505
|
if (!existsSync(configPath)) {
|
|
494
506
|
console.log(" No config file found. Nothing to undo.\n");
|
|
495
507
|
return;
|
package/src/llm-proxy.ts
CHANGED
|
@@ -63,6 +63,9 @@ export class LlmProxy {
|
|
|
63
63
|
toolCallsDenied: 0,
|
|
64
64
|
};
|
|
65
65
|
|
|
66
|
+
// In-memory audit log
|
|
67
|
+
private auditLog: Array<{ tool: string; decision: "allow" | "deny"; timestamp: string }> = [];
|
|
68
|
+
|
|
66
69
|
constructor(opts: LlmProxyOpts) {
|
|
67
70
|
this.port = opts.port;
|
|
68
71
|
this.upstream = opts.upstream;
|
|
@@ -70,6 +73,10 @@ export class LlmProxy {
|
|
|
70
73
|
this.logger = opts.logger;
|
|
71
74
|
}
|
|
72
75
|
|
|
76
|
+
getAuditLog() {
|
|
77
|
+
return [...this.auditLog];
|
|
78
|
+
}
|
|
79
|
+
|
|
73
80
|
async start(): Promise<void> {
|
|
74
81
|
this.server = createServer(async (req, res) => {
|
|
75
82
|
try {
|
|
@@ -600,6 +607,12 @@ export class LlmProxy {
|
|
|
600
607
|
this.stats.toolCallsDenied++;
|
|
601
608
|
}
|
|
602
609
|
|
|
610
|
+
this.auditLog.push({
|
|
611
|
+
tool: toolName,
|
|
612
|
+
decision: decision.decision,
|
|
613
|
+
timestamp: new Date().toISOString(),
|
|
614
|
+
});
|
|
615
|
+
|
|
603
616
|
return decision.decision;
|
|
604
617
|
}
|
|
605
618
|
|
package/src/policy-source.ts
CHANGED
|
@@ -41,4 +41,26 @@ export class CarapacePolicySource implements PolicySource {
|
|
|
41
41
|
const policies = files.map(f => readFileSync(join(this.policyDir, f), "utf-8"));
|
|
42
42
|
return policies.join("\n\n");
|
|
43
43
|
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Load Carapace's Cedar schema JSON from the policy directory.
|
|
47
|
+
*
|
|
48
|
+
* OVID-ME's WASM engine needs this to evaluate Carapace policies
|
|
49
|
+
* faithfully (entity types like `Shell`, `Tool`, `API`, action
|
|
50
|
+
* definitions). Pass the returned object to
|
|
51
|
+
* `evaluateMandateAsync(..., { externalSchema })` or
|
|
52
|
+
* `evaluateWithWasm(..., { externalSchema })`.
|
|
53
|
+
*
|
|
54
|
+
* Returns null when no schema.json is present; in that case OVID-ME
|
|
55
|
+
* falls back to its synthesized Agent + Resource schema.
|
|
56
|
+
*/
|
|
57
|
+
async getSchema(): Promise<Record<string, any> | null> {
|
|
58
|
+
const schemaPath = join(this.policyDir, "schema.json");
|
|
59
|
+
if (!existsSync(schemaPath)) return null;
|
|
60
|
+
try {
|
|
61
|
+
return JSON.parse(readFileSync(schemaPath, "utf-8"));
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
44
66
|
}
|