clawclamp 0.1.3 → 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 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 { CedarEngine } from "./src/cedar-engine.js";
3
- import { AuditStore } from "./src/audit-store.js";
4
- import { createServer } from "./src/server.js";
5
- import type { OpenClawPluginApi, PluginHookBeforeToolCallEvent, PluginHookToolContext } from "../../src/plugins/types.js";
6
-
7
- export const plugin = {
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: "ClawClamp Policy Audit",
10
- description: "Cedarling-based permission control with audit logging and dry-run mode.",
10
+ name: "Clawclamp",
11
+ description: "Cedar-based authorization guard with tool audit logging.",
12
+ configSchema: clawClampConfigSchema,
11
13
  register(api: OpenClawPluginApi) {
12
- const engine = new CedarEngine({
13
- mode: (api.pluginConfig?.mode as string) || "monitor",
14
- policyStoreUrl: api.pluginConfig?.policyStoreUrl as string,
15
- });
16
-
17
- const auditStore = new AuditStore(api.config.workspaceDir || process.cwd());
18
-
19
- // Register Hook: before_tool_call
20
- api.on("before_tool_call", async (event: PluginHookBeforeToolCallEvent, ctx: PluginHookToolContext) => {
21
- const { toolName, params, runId } = event;
22
- const { agentId, sessionId } = ctx;
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: "plugin",
27
+ path: "/plugins/clawclamp",
28
+ auth: "gateway",
68
29
  match: "prefix",
69
- handler: async (req, res) => {
70
- // Mock router logic for demonstration
71
- if (req.method === "GET" && (req.url === "/" || req.url === "")) {
72
- const app = createServer(engine, auditStore);
73
- const response = await app.request("/");
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;
@@ -1,29 +1,10 @@
1
1
  {
2
2
  "id": "clawclamp",
3
- "name": "ClawClamp Policy Audit",
4
- "description": "Cedarling-based permission control with audit logging and dry-run mode.",
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
- "properties": {
8
- "mode": {
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
  }