@clawdreyhepburn/carapace 1.0.4 → 1.0.6

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.
@@ -2,7 +2,7 @@
2
2
  "id": "carapace",
3
3
  "name": "Carapace",
4
4
  "description": "Cedar policy enforcement for agent tool access via before_tool_call hook. Your agent's exoskeleton.",
5
- "version": "1.0.4",
5
+ "version": "1.0.5",
6
6
  "configSchema": {
7
7
  "type": "object",
8
8
  "additionalProperties": true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawdreyhepburn/carapace",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
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
@@ -36,6 +36,14 @@ interface OpenClawPluginApi {
36
36
  opts?: { optional?: boolean },
37
37
  ): void;
38
38
  registerHook?(hookName: string | string[], handler: (event: any) => Promise<any> | any, opts?: { name: string; description?: string; priority?: number }): void;
39
+ /**
40
+ * Typed hook registration. Exposed by the real OpenClaw runtime in full
41
+ * registration mode (see loader `registerTypedHook`). This is the API this
42
+ * plugin actually uses to install the `before_tool_call` hook — declared
43
+ * optional here because some test harnesses construct a stripped-down api
44
+ * shim without it.
45
+ */
46
+ on?(hookName: string, handler: (event: any, ctx?: any) => Promise<any> | any, opts?: { name?: string; description?: string; priority?: number }): void;
39
47
  registerCli?(fn: (ctx: { program: any }) => void, opts?: { commands: string[] }): void;
40
48
  registerGatewayMethod?(name: string, handler: (ctx: { respond: (ok: boolean, data: any) => void }) => void): void;
41
49
  }
@@ -107,12 +115,9 @@ export default function register(api: OpenClawPluginApi) {
107
115
  // --- Register before_tool_call hook ---
108
116
  if (api.on) {
109
117
  api.on("before_tool_call", async (event: any) => {
110
- logger.info(`[Carapace] HOOK FIRED: keys=${JSON.stringify(Object.keys(event))}`);
111
- logger.info(`[Carapace] before_tool_call fired: ${JSON.stringify(Object.keys(event))}`);
112
118
  const toolName: string = event.toolName ?? event.tool ?? event.name ?? "";
113
119
  const params: Record<string, unknown> = event.params ?? event.arguments ?? event.input ?? {};
114
120
 
115
- logger.info(`[Carapace] tool=${toolName} defaultPolicy=${config.defaultPolicy ?? "allow-all"}`);
116
121
  if (!toolName) return {};
117
122
 
118
123
  stats.toolCallsEvaluated++;
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
  }