clawclamp 0.1.4 → 0.1.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.
- package/README.md +45 -0
- package/assets/app.js +166 -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,166 @@
|
|
|
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" ? "灰度" : "强制";
|
|
39
|
+
modeNoteEl.textContent =
|
|
40
|
+
state.mode === "gray"
|
|
41
|
+
? "被拒绝的工具调用仍会执行,但会标记为灰度放行。"
|
|
42
|
+
: "被拒绝的工具调用将被阻断。";
|
|
43
|
+
enforceBtn.disabled = state.mode === "enforce";
|
|
44
|
+
grayBtn.disabled = state.mode === "gray";
|
|
45
|
+
grantHint.textContent =
|
|
46
|
+
`默认 TTL ${state.grants.defaultTtlSeconds}s,最长 ${state.grants.maxTtlSeconds}s。`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function renderGrants(grants) {
|
|
50
|
+
if (!grants.length) {
|
|
51
|
+
grantsEl.innerHTML = "<div class=\"note\">暂无有效授权。</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">到期时间 ${new Date(grant.expiresAt).toLocaleString()}</span>
|
|
62
|
+
</div>
|
|
63
|
+
<button class="btn ghost" data-id="${grant.id}">撤销</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 decisionLabel(decision) {
|
|
80
|
+
if (decision === "allow_grayed") return "灰度放行";
|
|
81
|
+
if (decision === "deny") return "拒绝";
|
|
82
|
+
if (decision === "error") return "错误";
|
|
83
|
+
return "允许";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function riskLabel(risk) {
|
|
87
|
+
if (risk === "low") return "低";
|
|
88
|
+
if (risk === "medium") return "中";
|
|
89
|
+
if (risk === "high") return "高";
|
|
90
|
+
return risk || "-";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function renderAudit(entries) {
|
|
94
|
+
if (!entries.length) {
|
|
95
|
+
auditEl.innerHTML = "<div class=\"note\">暂无审计记录。</div>";
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const header = document.createElement("div");
|
|
100
|
+
header.className = "table-row header";
|
|
101
|
+
header.innerHTML = "<div>时间</div><div>工具</div><div>决策</div><div>风险</div><div>说明</div>";
|
|
102
|
+
|
|
103
|
+
auditEl.innerHTML = "";
|
|
104
|
+
auditEl.appendChild(header);
|
|
105
|
+
|
|
106
|
+
entries.forEach((entry) => {
|
|
107
|
+
const row = document.createElement("div");
|
|
108
|
+
row.className = "table-row";
|
|
109
|
+
const badgeClass = decisionBadge(entry.decision);
|
|
110
|
+
row.innerHTML = `
|
|
111
|
+
<div>${formatTime(entry.timestamp)}</div>
|
|
112
|
+
<div class="mono">${entry.toolName}</div>
|
|
113
|
+
<div class="badge ${badgeClass}">${decisionLabel(entry.decision)}</div>
|
|
114
|
+
<div>${riskLabel(entry.risk)}</div>
|
|
115
|
+
<div>${entry.reason || entry.error || ""}</div>
|
|
116
|
+
`;
|
|
117
|
+
auditEl.appendChild(row);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function refreshAll() {
|
|
122
|
+
const state = await fetchJson(`${API_BASE}/state`);
|
|
123
|
+
renderMode(state);
|
|
124
|
+
const grants = await fetchJson(`${API_BASE}/grants`);
|
|
125
|
+
renderGrants(grants.grants || []);
|
|
126
|
+
const logs = await fetchJson(`${API_BASE}/logs`);
|
|
127
|
+
renderAudit(logs.entries || []);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
refreshBtn.addEventListener("click", () => {
|
|
131
|
+
refreshAll();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
enforceBtn.addEventListener("click", async () => {
|
|
135
|
+
await fetchJson(`${API_BASE}/mode`, {
|
|
136
|
+
method: "POST",
|
|
137
|
+
body: JSON.stringify({ mode: "enforce" }),
|
|
138
|
+
});
|
|
139
|
+
await refreshAll();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
grayBtn.addEventListener("click", async () => {
|
|
143
|
+
await fetchJson(`${API_BASE}/mode`, {
|
|
144
|
+
method: "POST",
|
|
145
|
+
body: JSON.stringify({ mode: "gray" }),
|
|
146
|
+
});
|
|
147
|
+
await refreshAll();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
grantForm.addEventListener("submit", async (event) => {
|
|
151
|
+
event.preventDefault();
|
|
152
|
+
const toolName = grantToolInput.value.trim();
|
|
153
|
+
const ttlSeconds = grantTtlInput.value ? Number(grantTtlInput.value) : undefined;
|
|
154
|
+
const note = grantNoteInput.value.trim();
|
|
155
|
+
await fetchJson(`${API_BASE}/grants`, {
|
|
156
|
+
method: "POST",
|
|
157
|
+
body: JSON.stringify({ toolName, ttlSeconds, note: note || undefined }),
|
|
158
|
+
});
|
|
159
|
+
grantToolInput.value = "";
|
|
160
|
+
grantTtlInput.value = "";
|
|
161
|
+
grantNoteInput.value = "";
|
|
162
|
+
await refreshAll();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
refreshAll();
|
|
166
|
+
setInterval(refreshAll, 10_000);
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN">
|
|
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>工具授权与审计</h1>
|
|
15
|
+
<p class="subhead">
|
|
16
|
+
基于 Cedar 的策略决策,支持短期授权与灰度放行。
|
|
17
|
+
</p>
|
|
18
|
+
</div>
|
|
19
|
+
<div class="header-actions">
|
|
20
|
+
<button id="refresh" class="btn ghost">刷新</button>
|
|
21
|
+
</div>
|
|
22
|
+
</header>
|
|
23
|
+
|
|
24
|
+
<section class="grid">
|
|
25
|
+
<div class="card">
|
|
26
|
+
<h2>模式</h2>
|
|
27
|
+
<div class="mode-row">
|
|
28
|
+
<div>
|
|
29
|
+
<div class="label">当前模式</div>
|
|
30
|
+
<div id="mode" class="value">加载中...</div>
|
|
31
|
+
<div id="mode-note" class="note"></div>
|
|
32
|
+
</div>
|
|
33
|
+
<div class="mode-actions">
|
|
34
|
+
<button id="mode-enforce" class="btn">强制</button>
|
|
35
|
+
<button id="mode-gray" class="btn warn">灰度</button>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<div class="card">
|
|
41
|
+
<h2>短期授权</h2>
|
|
42
|
+
<form id="grant-form" class="grant-form">
|
|
43
|
+
<label>
|
|
44
|
+
工具名称
|
|
45
|
+
<input id="grant-tool" placeholder="exec(或 *)" required />
|
|
46
|
+
</label>
|
|
47
|
+
<label>
|
|
48
|
+
TTL(秒)
|
|
49
|
+
<input id="grant-ttl" type="number" min="60" step="60" />
|
|
50
|
+
</label>
|
|
51
|
+
<label>
|
|
52
|
+
备注
|
|
53
|
+
<input id="grant-note" placeholder="原因 / 工单" />
|
|
54
|
+
</label>
|
|
55
|
+
<button class="btn primary" type="submit">创建授权</button>
|
|
56
|
+
<div id="grant-hint" class="note"></div>
|
|
57
|
+
</form>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<div class="card">
|
|
61
|
+
<h2>当前授权</h2>
|
|
62
|
+
<div id="grants" class="list"></div>
|
|
63
|
+
</div>
|
|
64
|
+
</section>
|
|
65
|
+
|
|
66
|
+
<section class="card">
|
|
67
|
+
<h2>审计日志</h2>
|
|
68
|
+
<div class="note">最近的工具调用记录(允许、拒绝、灰度放行)。</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;
|