clawclamp 0.1.4 → 0.1.5
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 +45 -0
- package/assets/app.js +152 -0
- package/assets/index.html +75 -0
- package/assets/styles.css +252 -0
- package/index.ts +31 -95
- package/openclaw.plugin.json +4 -23
- package/package.json +6 -18
- package/src/audit.ts +55 -0
- package/src/cedarling.ts +27 -0
- package/src/config.ts +244 -0
- package/src/grants.ts +102 -0
- package/src/guard.ts +293 -0
- package/src/http.ts +241 -0
- package/src/mode.ts +48 -0
- package/src/policy.ts +57 -0
- package/src/storage.ts +23 -0
- package/src/types.ts +62 -0
- package/clawclamp-0.1.2.tgz +0 -0
- package/clawclamp-0.1.3.tgz +0 -0
- package/dist/index.js +0 -224
- package/index.js +0 -224
- package/src/audit-store.ts +0 -66
- package/src/cedar-engine.ts +0 -59
- package/src/server.ts +0 -92
- package/tsdown.config.ts +0 -11
package/README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Clawclamp
|
|
2
|
+
|
|
3
|
+
Clawclamp adds Cedar-based authorization to OpenClaw tool calls. It evaluates every tool invocation via a Cedar policy, records allow/deny decisions to an audit log, and exposes a gateway UI for reviewing logs and granting short-term approvals.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Cedar policy enforcement for `before_tool_call`.
|
|
8
|
+
- Long-term authorization via policy (defaults allow low-risk tools).
|
|
9
|
+
- Short-term authorization via expiring grants.
|
|
10
|
+
- Audit UI for allowed/denied/gray-mode tool calls.
|
|
11
|
+
- Gray mode: denied calls are still executed but logged as overrides.
|
|
12
|
+
|
|
13
|
+
## Configuration
|
|
14
|
+
|
|
15
|
+
Configure under `plugins.entries.clawclamp.config`:
|
|
16
|
+
|
|
17
|
+
```yaml
|
|
18
|
+
plugins:
|
|
19
|
+
entries:
|
|
20
|
+
clawclamp:
|
|
21
|
+
enabled: true
|
|
22
|
+
config:
|
|
23
|
+
mode: enforce
|
|
24
|
+
principalId: openclaw
|
|
25
|
+
policyStoreUri: file:///path/to/policy-store.json
|
|
26
|
+
policyFailOpen: false
|
|
27
|
+
risk:
|
|
28
|
+
default: high
|
|
29
|
+
overrides:
|
|
30
|
+
read: low
|
|
31
|
+
web_search: medium
|
|
32
|
+
exec: high
|
|
33
|
+
grants:
|
|
34
|
+
defaultTtlSeconds: 900
|
|
35
|
+
maxTtlSeconds: 3600
|
|
36
|
+
audit:
|
|
37
|
+
maxEntries: 500
|
|
38
|
+
includeParams: true
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`policyStoreUri` points to a Cedar policy store JSON (file:// or https://). `policyStoreLocal` can be set to a raw JSON string for the policy store. If omitted, the plugin uses a built-in policy store that permits low-risk tools and requires a grant for other risks.
|
|
42
|
+
|
|
43
|
+
## UI
|
|
44
|
+
|
|
45
|
+
Open the gateway path `/plugins/clawclamp` to view audit logs, toggle gray mode, and create short-term grants.
|
package/assets/app.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
const API_BASE = "/plugins/clawclamp/api";
|
|
2
|
+
|
|
3
|
+
const qs = (id) => document.getElementById(id);
|
|
4
|
+
|
|
5
|
+
const modeEl = qs("mode");
|
|
6
|
+
const modeNoteEl = qs("mode-note");
|
|
7
|
+
const enforceBtn = qs("mode-enforce");
|
|
8
|
+
const grayBtn = qs("mode-gray");
|
|
9
|
+
const refreshBtn = qs("refresh");
|
|
10
|
+
const grantForm = qs("grant-form");
|
|
11
|
+
const grantToolInput = qs("grant-tool");
|
|
12
|
+
const grantTtlInput = qs("grant-ttl");
|
|
13
|
+
const grantNoteInput = qs("grant-note");
|
|
14
|
+
const grantHint = qs("grant-hint");
|
|
15
|
+
const grantsEl = qs("grants");
|
|
16
|
+
const auditEl = qs("audit");
|
|
17
|
+
|
|
18
|
+
async function fetchJson(path, opts = {}) {
|
|
19
|
+
const res = await fetch(path, {
|
|
20
|
+
headers: { "content-type": "application/json" },
|
|
21
|
+
...opts,
|
|
22
|
+
});
|
|
23
|
+
const payload = await res.json().catch(() => ({}));
|
|
24
|
+
if (!res.ok) {
|
|
25
|
+
const message = payload.error || `Request failed (${res.status})`;
|
|
26
|
+
throw new Error(message);
|
|
27
|
+
}
|
|
28
|
+
return payload;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function formatTime(iso) {
|
|
32
|
+
if (!iso) return "-";
|
|
33
|
+
const date = new Date(iso);
|
|
34
|
+
return date.toLocaleTimeString();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function renderMode(state) {
|
|
38
|
+
modeEl.textContent = state.mode === "gray" ? "Gray Mode" : "Enforce";
|
|
39
|
+
modeNoteEl.textContent =
|
|
40
|
+
state.mode === "gray"
|
|
41
|
+
? "Denied tool calls are still executed but marked in audit logs."
|
|
42
|
+
: "Denied tool calls are blocked.";
|
|
43
|
+
enforceBtn.disabled = state.mode === "enforce";
|
|
44
|
+
grayBtn.disabled = state.mode === "gray";
|
|
45
|
+
grantHint.textContent =
|
|
46
|
+
`Default TTL ${state.grants.defaultTtlSeconds}s, max ${state.grants.maxTtlSeconds}s.`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function renderGrants(grants) {
|
|
50
|
+
if (!grants.length) {
|
|
51
|
+
grantsEl.innerHTML = "<div class=\"note\">No active grants.</div>";
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
grantsEl.innerHTML = "";
|
|
55
|
+
grants.forEach((grant) => {
|
|
56
|
+
const item = document.createElement("div");
|
|
57
|
+
item.className = "list-item";
|
|
58
|
+
item.innerHTML = `
|
|
59
|
+
<div>
|
|
60
|
+
<strong>${grant.toolName}</strong><br />
|
|
61
|
+
<span class="note">Expires ${new Date(grant.expiresAt).toLocaleString()}</span>
|
|
62
|
+
</div>
|
|
63
|
+
<button class="btn ghost" data-id="${grant.id}">Revoke</button>
|
|
64
|
+
`;
|
|
65
|
+
item.querySelector("button").addEventListener("click", async () => {
|
|
66
|
+
await fetchJson(`${API_BASE}/grants/${grant.id}`, { method: "DELETE" });
|
|
67
|
+
await refreshAll();
|
|
68
|
+
});
|
|
69
|
+
grantsEl.appendChild(item);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function decisionBadge(decision) {
|
|
74
|
+
if (decision === "allow_grayed") return "gray";
|
|
75
|
+
if (decision === "deny") return "deny";
|
|
76
|
+
return "allow";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function renderAudit(entries) {
|
|
80
|
+
if (!entries.length) {
|
|
81
|
+
auditEl.innerHTML = "<div class=\"note\">No audit entries yet.</div>";
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const header = document.createElement("div");
|
|
86
|
+
header.className = "table-row header";
|
|
87
|
+
header.innerHTML = "<div>Time</div><div>Tool</div><div>Decision</div><div>Risk</div><div>Details</div>";
|
|
88
|
+
|
|
89
|
+
auditEl.innerHTML = "";
|
|
90
|
+
auditEl.appendChild(header);
|
|
91
|
+
|
|
92
|
+
entries.forEach((entry) => {
|
|
93
|
+
const row = document.createElement("div");
|
|
94
|
+
row.className = "table-row";
|
|
95
|
+
const badgeClass = decisionBadge(entry.decision);
|
|
96
|
+
row.innerHTML = `
|
|
97
|
+
<div>${formatTime(entry.timestamp)}</div>
|
|
98
|
+
<div class="mono">${entry.toolName}</div>
|
|
99
|
+
<div class="badge ${badgeClass}">${entry.decision}</div>
|
|
100
|
+
<div>${entry.risk}</div>
|
|
101
|
+
<div>${entry.reason || entry.error || ""}</div>
|
|
102
|
+
`;
|
|
103
|
+
auditEl.appendChild(row);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function refreshAll() {
|
|
108
|
+
const state = await fetchJson(`${API_BASE}/state`);
|
|
109
|
+
renderMode(state);
|
|
110
|
+
const grants = await fetchJson(`${API_BASE}/grants`);
|
|
111
|
+
renderGrants(grants.grants || []);
|
|
112
|
+
const logs = await fetchJson(`${API_BASE}/logs`);
|
|
113
|
+
renderAudit(logs.entries || []);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
refreshBtn.addEventListener("click", () => {
|
|
117
|
+
refreshAll();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
enforceBtn.addEventListener("click", async () => {
|
|
121
|
+
await fetchJson(`${API_BASE}/mode`, {
|
|
122
|
+
method: "POST",
|
|
123
|
+
body: JSON.stringify({ mode: "enforce" }),
|
|
124
|
+
});
|
|
125
|
+
await refreshAll();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
grayBtn.addEventListener("click", async () => {
|
|
129
|
+
await fetchJson(`${API_BASE}/mode`, {
|
|
130
|
+
method: "POST",
|
|
131
|
+
body: JSON.stringify({ mode: "gray" }),
|
|
132
|
+
});
|
|
133
|
+
await refreshAll();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
grantForm.addEventListener("submit", async (event) => {
|
|
137
|
+
event.preventDefault();
|
|
138
|
+
const toolName = grantToolInput.value.trim();
|
|
139
|
+
const ttlSeconds = grantTtlInput.value ? Number(grantTtlInput.value) : undefined;
|
|
140
|
+
const note = grantNoteInput.value.trim();
|
|
141
|
+
await fetchJson(`${API_BASE}/grants`, {
|
|
142
|
+
method: "POST",
|
|
143
|
+
body: JSON.stringify({ toolName, ttlSeconds, note: note || undefined }),
|
|
144
|
+
});
|
|
145
|
+
grantToolInput.value = "";
|
|
146
|
+
grantTtlInput.value = "";
|
|
147
|
+
grantNoteInput.value = "";
|
|
148
|
+
await refreshAll();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
refreshAll();
|
|
152
|
+
setInterval(refreshAll, 10_000);
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>OpenClaw Clawclamp</title>
|
|
7
|
+
<link rel="stylesheet" href="/plugins/clawclamp/assets/styles.css" />
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div class="page">
|
|
11
|
+
<header class="header">
|
|
12
|
+
<div>
|
|
13
|
+
<p class="eyebrow">OpenClaw - Clawclamp</p>
|
|
14
|
+
<h1>Tool Authorization & Audit</h1>
|
|
15
|
+
<p class="subhead">
|
|
16
|
+
Cedar-backed policy decisions with short-term grants and gray-mode overrides.
|
|
17
|
+
</p>
|
|
18
|
+
</div>
|
|
19
|
+
<div class="header-actions">
|
|
20
|
+
<button id="refresh" class="btn ghost">Refresh</button>
|
|
21
|
+
</div>
|
|
22
|
+
</header>
|
|
23
|
+
|
|
24
|
+
<section class="grid">
|
|
25
|
+
<div class="card">
|
|
26
|
+
<h2>Mode</h2>
|
|
27
|
+
<div class="mode-row">
|
|
28
|
+
<div>
|
|
29
|
+
<div class="label">Current mode</div>
|
|
30
|
+
<div id="mode" class="value">Loading...</div>
|
|
31
|
+
<div id="mode-note" class="note"></div>
|
|
32
|
+
</div>
|
|
33
|
+
<div class="mode-actions">
|
|
34
|
+
<button id="mode-enforce" class="btn">Enforce</button>
|
|
35
|
+
<button id="mode-gray" class="btn warn">Gray</button>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<div class="card">
|
|
41
|
+
<h2>Short-Term Grant</h2>
|
|
42
|
+
<form id="grant-form" class="grant-form">
|
|
43
|
+
<label>
|
|
44
|
+
Tool name
|
|
45
|
+
<input id="grant-tool" placeholder="exec (or *)" required />
|
|
46
|
+
</label>
|
|
47
|
+
<label>
|
|
48
|
+
TTL (seconds)
|
|
49
|
+
<input id="grant-ttl" type="number" min="60" step="60" />
|
|
50
|
+
</label>
|
|
51
|
+
<label>
|
|
52
|
+
Note
|
|
53
|
+
<input id="grant-note" placeholder="Reason / ticket" />
|
|
54
|
+
</label>
|
|
55
|
+
<button class="btn primary" type="submit">Create Grant</button>
|
|
56
|
+
<div id="grant-hint" class="note"></div>
|
|
57
|
+
</form>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<div class="card">
|
|
61
|
+
<h2>Active Grants</h2>
|
|
62
|
+
<div id="grants" class="list"></div>
|
|
63
|
+
</div>
|
|
64
|
+
</section>
|
|
65
|
+
|
|
66
|
+
<section class="card">
|
|
67
|
+
<h2>Audit Log</h2>
|
|
68
|
+
<div class="note">Most recent tool calls (allowed, denied, and gray-mode overrides).</div>
|
|
69
|
+
<div class="table" id="audit"></div>
|
|
70
|
+
</section>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<script type="module" src="/plugins/clawclamp/assets/app.js"></script>
|
|
74
|
+
</body>
|
|
75
|
+
</html>
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
color-scheme: light;
|
|
3
|
+
--bg: #f5f1ea;
|
|
4
|
+
--panel: #ffffff;
|
|
5
|
+
--ink: #1b1b1b;
|
|
6
|
+
--muted: #5b5b5b;
|
|
7
|
+
--accent: #1a4dff;
|
|
8
|
+
--accent-2: #ff8b2c;
|
|
9
|
+
--warn: #d64b31;
|
|
10
|
+
--border: #e5ddd0;
|
|
11
|
+
--shadow: 0 12px 40px rgba(0, 0, 0, 0.08);
|
|
12
|
+
--radius: 16px;
|
|
13
|
+
--mono: "IBM Plex Mono", "SFMono-Regular", "Menlo", "Consolas", monospace;
|
|
14
|
+
--sans: "Space Grotesk", "Avenir Next", "Segoe UI", sans-serif;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
* {
|
|
18
|
+
box-sizing: border-box;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
body {
|
|
22
|
+
margin: 0;
|
|
23
|
+
font-family: var(--sans);
|
|
24
|
+
background: radial-gradient(circle at top, #fdfbf7 0%, var(--bg) 50%, #efe7db 100%);
|
|
25
|
+
color: var(--ink);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.page {
|
|
29
|
+
max-width: 1180px;
|
|
30
|
+
margin: 0 auto;
|
|
31
|
+
padding: 32px 24px 48px;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.header {
|
|
35
|
+
display: flex;
|
|
36
|
+
align-items: flex-start;
|
|
37
|
+
justify-content: space-between;
|
|
38
|
+
gap: 24px;
|
|
39
|
+
padding: 24px;
|
|
40
|
+
background: var(--panel);
|
|
41
|
+
border-radius: var(--radius);
|
|
42
|
+
box-shadow: var(--shadow);
|
|
43
|
+
border: 1px solid var(--border);
|
|
44
|
+
margin-bottom: 24px;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.eyebrow {
|
|
48
|
+
margin: 0 0 8px;
|
|
49
|
+
text-transform: uppercase;
|
|
50
|
+
letter-spacing: 0.2em;
|
|
51
|
+
font-size: 0.7rem;
|
|
52
|
+
color: var(--muted);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
h1 {
|
|
56
|
+
margin: 0 0 6px;
|
|
57
|
+
font-size: 2.2rem;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.subhead {
|
|
61
|
+
margin: 0;
|
|
62
|
+
color: var(--muted);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.header-actions {
|
|
66
|
+
display: flex;
|
|
67
|
+
align-items: center;
|
|
68
|
+
gap: 12px;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.grid {
|
|
72
|
+
display: grid;
|
|
73
|
+
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
|
74
|
+
gap: 20px;
|
|
75
|
+
margin-bottom: 24px;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.card {
|
|
79
|
+
background: var(--panel);
|
|
80
|
+
border-radius: var(--radius);
|
|
81
|
+
padding: 20px;
|
|
82
|
+
border: 1px solid var(--border);
|
|
83
|
+
box-shadow: var(--shadow);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
h2 {
|
|
87
|
+
margin: 0 0 12px;
|
|
88
|
+
font-size: 1.2rem;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.mode-row {
|
|
92
|
+
display: flex;
|
|
93
|
+
align-items: center;
|
|
94
|
+
justify-content: space-between;
|
|
95
|
+
gap: 16px;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.mode-actions {
|
|
99
|
+
display: flex;
|
|
100
|
+
flex-direction: column;
|
|
101
|
+
gap: 8px;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.label {
|
|
105
|
+
font-size: 0.8rem;
|
|
106
|
+
color: var(--muted);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.value {
|
|
110
|
+
font-size: 1.4rem;
|
|
111
|
+
font-weight: 600;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.note {
|
|
115
|
+
font-size: 0.85rem;
|
|
116
|
+
color: var(--muted);
|
|
117
|
+
margin-top: 6px;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.btn {
|
|
121
|
+
padding: 10px 16px;
|
|
122
|
+
border-radius: 999px;
|
|
123
|
+
border: 1px solid var(--border);
|
|
124
|
+
background: #fff;
|
|
125
|
+
cursor: pointer;
|
|
126
|
+
font-weight: 600;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.btn.primary {
|
|
130
|
+
background: var(--accent);
|
|
131
|
+
color: #fff;
|
|
132
|
+
border-color: var(--accent);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.btn.warn {
|
|
136
|
+
background: var(--warn);
|
|
137
|
+
color: #fff;
|
|
138
|
+
border-color: var(--warn);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.btn.ghost {
|
|
142
|
+
background: transparent;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.btn:disabled {
|
|
146
|
+
opacity: 0.5;
|
|
147
|
+
cursor: not-allowed;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.grant-form {
|
|
151
|
+
display: flex;
|
|
152
|
+
flex-direction: column;
|
|
153
|
+
gap: 12px;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.grant-form label {
|
|
157
|
+
display: flex;
|
|
158
|
+
flex-direction: column;
|
|
159
|
+
gap: 6px;
|
|
160
|
+
font-size: 0.85rem;
|
|
161
|
+
color: var(--muted);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
input {
|
|
165
|
+
border: 1px solid var(--border);
|
|
166
|
+
border-radius: 12px;
|
|
167
|
+
padding: 10px 12px;
|
|
168
|
+
font-size: 0.95rem;
|
|
169
|
+
font-family: var(--sans);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.list {
|
|
173
|
+
display: grid;
|
|
174
|
+
gap: 10px;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.list-item {
|
|
178
|
+
display: flex;
|
|
179
|
+
justify-content: space-between;
|
|
180
|
+
align-items: center;
|
|
181
|
+
padding: 12px;
|
|
182
|
+
border-radius: 12px;
|
|
183
|
+
border: 1px solid var(--border);
|
|
184
|
+
background: #faf7f1;
|
|
185
|
+
font-size: 0.9rem;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.list-item strong {
|
|
189
|
+
font-family: var(--mono);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.table {
|
|
193
|
+
display: grid;
|
|
194
|
+
gap: 8px;
|
|
195
|
+
margin-top: 12px;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.table-row {
|
|
199
|
+
display: grid;
|
|
200
|
+
grid-template-columns: 140px 120px 1fr 100px 120px;
|
|
201
|
+
gap: 12px;
|
|
202
|
+
align-items: center;
|
|
203
|
+
padding: 10px 12px;
|
|
204
|
+
border-radius: 12px;
|
|
205
|
+
border: 1px solid var(--border);
|
|
206
|
+
background: #fcfbf8;
|
|
207
|
+
font-size: 0.85rem;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.table-row.header {
|
|
211
|
+
background: #f0e8db;
|
|
212
|
+
font-weight: 700;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.table-row .mono {
|
|
216
|
+
font-family: var(--mono);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.badge {
|
|
220
|
+
padding: 4px 8px;
|
|
221
|
+
border-radius: 999px;
|
|
222
|
+
font-size: 0.75rem;
|
|
223
|
+
text-transform: uppercase;
|
|
224
|
+
font-weight: 700;
|
|
225
|
+
justify-self: start;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.badge.allow {
|
|
229
|
+
background: rgba(26, 77, 255, 0.15);
|
|
230
|
+
color: #1a4dff;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.badge.deny {
|
|
234
|
+
background: rgba(214, 75, 49, 0.15);
|
|
235
|
+
color: #d64b31;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.badge.gray {
|
|
239
|
+
background: rgba(255, 139, 44, 0.2);
|
|
240
|
+
color: #c15c00;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
@media (max-width: 900px) {
|
|
244
|
+
.table-row {
|
|
245
|
+
grid-template-columns: 120px 100px 1fr;
|
|
246
|
+
grid-template-rows: auto auto;
|
|
247
|
+
}
|
|
248
|
+
.table-row > :nth-child(4),
|
|
249
|
+
.table-row > :nth-child(5) {
|
|
250
|
+
grid-column: 1 / -1;
|
|
251
|
+
}
|
|
252
|
+
}
|
package/index.ts
CHANGED
|
@@ -1,103 +1,39 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
4
|
+
import { resolveClawClampConfig, clawClampConfigSchema } from "./src/config.js";
|
|
5
|
+
import { createClawClampHttpHandler } from "./src/http.js";
|
|
6
|
+
import { createClawClampService } from "./src/guard.js";
|
|
7
|
+
|
|
8
|
+
const plugin = {
|
|
8
9
|
id: "clawclamp",
|
|
9
|
-
name: "
|
|
10
|
-
description: "
|
|
10
|
+
name: "Clawclamp",
|
|
11
|
+
description: "Cedar-based authorization guard with tool audit logging.",
|
|
12
|
+
configSchema: clawClampConfigSchema,
|
|
11
13
|
register(api: OpenClawPluginApi) {
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const principal = `User::"${agentId || "unknown"}"`;
|
|
25
|
-
const action = `Action::"tool:${toolName}"`;
|
|
26
|
-
const resource = `Resource::"global"`;
|
|
27
|
-
|
|
28
|
-
const result = await engine.authorize({
|
|
29
|
-
principal,
|
|
30
|
-
action,
|
|
31
|
-
resource,
|
|
32
|
-
context: {
|
|
33
|
-
sessionId,
|
|
34
|
-
runId,
|
|
35
|
-
params,
|
|
36
|
-
timestamp: Date.now(),
|
|
37
|
-
},
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
const shouldBlock =
|
|
41
|
-
result.decision === "Deny" && engine.getMode() === "enforce";
|
|
42
|
-
|
|
43
|
-
// Log the event
|
|
44
|
-
auditStore.append({
|
|
45
|
-
timestamp: new Date().toISOString(),
|
|
46
|
-
runId,
|
|
47
|
-
principal,
|
|
48
|
-
action,
|
|
49
|
-
resource,
|
|
50
|
-
decision: result.decision,
|
|
51
|
-
mode: engine.getMode(),
|
|
52
|
-
diagnostics: result.diagnostics,
|
|
53
|
-
blocked: shouldBlock,
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
if (shouldBlock) {
|
|
57
|
-
return {
|
|
58
|
-
block: true,
|
|
59
|
-
blockReason: `Cedar Policy Denied: ${result.diagnostics.join("; ")}`,
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
// Register HTTP Route for Audit Dashboard
|
|
14
|
+
const config = resolveClawClampConfig(api.pluginConfig);
|
|
15
|
+
const stateDir = api.runtime.state.resolveStateDir();
|
|
16
|
+
const service = createClawClampService({ api, config, stateDir });
|
|
17
|
+
|
|
18
|
+
api.on(
|
|
19
|
+
"before_tool_call",
|
|
20
|
+
async (event, ctx) => service.handleBeforeToolCall(event, ctx),
|
|
21
|
+
{ priority: -10 },
|
|
22
|
+
);
|
|
23
|
+
api.on("after_tool_call", async (event, ctx) => service.handleAfterToolCall(event, ctx));
|
|
24
|
+
|
|
25
|
+
const assetsDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "assets");
|
|
65
26
|
api.registerHttpRoute({
|
|
66
|
-
path: "/",
|
|
67
|
-
auth: "
|
|
27
|
+
path: "/plugins/clawclamp",
|
|
28
|
+
auth: "gateway",
|
|
68
29
|
match: "prefix",
|
|
69
|
-
handler:
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const html = await response.text();
|
|
75
|
-
res.writeHead(200, { "Content-Type": "text/html" });
|
|
76
|
-
res.end(html);
|
|
77
|
-
return true;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (req.method === "POST" && req.url === "/mode") {
|
|
81
|
-
let body = '';
|
|
82
|
-
req.on('data', (chunk: any) => body += chunk);
|
|
83
|
-
req.on('end', () => {
|
|
84
|
-
const params = new URLSearchParams(body);
|
|
85
|
-
const mode = params.get('mode');
|
|
86
|
-
if (mode === 'enforce' || mode === 'monitor') {
|
|
87
|
-
engine.setMode(mode);
|
|
88
|
-
}
|
|
89
|
-
res.writeHead(302, { 'Location': './' });
|
|
90
|
-
res.end();
|
|
91
|
-
});
|
|
92
|
-
return true;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return false;
|
|
96
|
-
},
|
|
30
|
+
handler: createClawClampHttpHandler({
|
|
31
|
+
stateDir,
|
|
32
|
+
config,
|
|
33
|
+
assetsDir,
|
|
34
|
+
}),
|
|
97
35
|
});
|
|
98
|
-
|
|
99
|
-
api.logger.info("Cedar Audit Plugin Activated");
|
|
100
|
-
}
|
|
36
|
+
},
|
|
101
37
|
};
|
|
102
38
|
|
|
103
39
|
export default plugin;
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,29 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "clawclamp",
|
|
3
|
-
"name": "
|
|
4
|
-
"description": "
|
|
3
|
+
"name": "Clawclamp",
|
|
4
|
+
"description": "Cedar-based authorization guard with tool audit logging and gray-mode override.",
|
|
5
5
|
"configSchema": {
|
|
6
6
|
"type": "object",
|
|
7
|
-
"
|
|
8
|
-
|
|
9
|
-
"type": "string",
|
|
10
|
-
"enum": ["enforce", "monitor"],
|
|
11
|
-
"default": "monitor"
|
|
12
|
-
},
|
|
13
|
-
"policyStoreUrl": {
|
|
14
|
-
"type": "string",
|
|
15
|
-
"default": "https://example.com/policy-store.json"
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
},
|
|
19
|
-
"uiHints": {
|
|
20
|
-
"mode": {
|
|
21
|
-
"label": "Mode",
|
|
22
|
-
"help": "Set to 'enforce' to block unauthorized tools, or 'monitor' to only log them."
|
|
23
|
-
},
|
|
24
|
-
"policyStoreUrl": {
|
|
25
|
-
"label": "Policy Store URL",
|
|
26
|
-
"help": "URL to the Cedarling policy store JSON."
|
|
27
|
-
}
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"properties": {}
|
|
28
9
|
}
|
|
29
10
|
}
|