@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawdreyhepburn/carapace",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "Cedar policy enforcement for agent tool access via OpenClaw's before_tool_call hook.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -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 { appendFileSync, mkdirSync } = require("node:fs");
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(homedir(), ".openclaw", "openclaw.json");
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(homedir(), ".openclaw", "openclaw.json");
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
 
@@ -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
  }