clawclamp 0.1.7 → 0.1.8

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 CHANGED
@@ -20,7 +20,7 @@ plugins:
20
20
  clawclamp:
21
21
  enabled: true
22
22
  config:
23
- mode: enforce
23
+ mode: gray
24
24
  principalId: openclaw
25
25
  policyStoreUri: file:///path/to/policy-store.json
26
26
  policyFailOpen: false
@@ -55,3 +55,4 @@ Policy management:
55
55
 
56
56
  - The UI includes a Cedar policy panel for CRUD.
57
57
  - If `policyStoreUri` is set, policies are read-only.
58
+ - Default policy set is empty, so all tool calls are denied unless you add permit policies.
package/assets/app.js CHANGED
@@ -120,8 +120,17 @@ function renderAudit(entries) {
120
120
  row.className = "table-row";
121
121
  const badgeClass = decisionBadge(entry.decision);
122
122
  const canAllow = entry.decision === "deny" || entry.decision === "allow_grayed";
123
- const isDenied = deniedTools.has(entry.toolName) || deniedTools.has("*");
124
- const canDeny = entry.decision === "allow" && !isDenied;
123
+ const canDeny = entry.decision === "allow";
124
+ const paramsText =
125
+ entry.params && typeof entry.params === "object"
126
+ ? JSON.stringify(entry.params, null, 2)
127
+ : entry.params || "";
128
+ const meta = [
129
+ entry.toolCallId ? `toolCallId: ${entry.toolCallId}` : null,
130
+ entry.runId ? `runId: ${entry.runId}` : null,
131
+ entry.sessionKey ? `sessionKey: ${entry.sessionKey}` : null,
132
+ entry.agentId ? `agentId: ${entry.agentId}` : null,
133
+ ].filter(Boolean);
125
134
  row.innerHTML = `
126
135
  <div>${formatTime(entry.timestamp)}</div>
127
136
  <div class="mono">${entry.toolName}</div>
@@ -132,24 +141,22 @@ function renderAudit(entries) {
132
141
  ${canAllow ? "<button class=\"btn mini\" data-action=\"allow\">一键允许</button>" : ""}
133
142
  ${canDeny ? "<button class=\"btn mini warn\" data-action=\"deny\">一键拒绝</button>" : ""}
134
143
  </div>
144
+ <div class="audit-detail">
145
+ <div class="audit-meta">${meta.join(" · ")}</div>
146
+ ${paramsText ? `<pre>${paramsText}</pre>` : ""}
147
+ </div>
135
148
  `;
136
149
  const allowBtn = row.querySelector("button[data-action=\"allow\"]");
137
150
  if (allowBtn) {
138
151
  allowBtn.addEventListener("click", async () => {
139
- await fetchJson(`${API_BASE}/grants`, {
140
- method: "POST",
141
- body: JSON.stringify({ toolName: entry.toolName }),
142
- });
152
+ await applyPolicyChange("permit", entry.toolName);
143
153
  await refreshAll();
144
154
  });
145
155
  }
146
156
  const denyBtn = row.querySelector("button[data-action=\"deny\"]");
147
157
  if (denyBtn) {
148
158
  denyBtn.addEventListener("click", async () => {
149
- await fetchJson(`${API_BASE}/denies`, {
150
- method: "POST",
151
- body: JSON.stringify({ toolName: entry.toolName }),
152
- });
159
+ await applyPolicyChange("forbid", entry.toolName);
153
160
  await refreshAll();
154
161
  });
155
162
  }
@@ -195,8 +202,6 @@ async function refreshAll() {
195
202
  renderMode(state);
196
203
  const grants = await fetchJson(`${API_BASE}/grants`);
197
204
  renderGrants(grants.grants || []);
198
- const denies = await fetchJson(`${API_BASE}/denies`);
199
- deniedTools = new Set(denies.tools || []);
200
205
  const logs = await fetchJson(`${API_BASE}/logs`);
201
206
  renderAudit(logs.entries || []);
202
207
  await refreshPolicies();
@@ -240,6 +245,39 @@ grantForm.addEventListener("submit", async (event) => {
240
245
  refreshAll();
241
246
  setInterval(refreshAll, 10_000);
242
247
 
248
+ function sanitizeIdPart(value) {
249
+ return value.toLowerCase().replace(/[^a-z0-9_-]+/g, "_").slice(0, 64);
250
+ }
251
+
252
+ function policyIdPrefix(effect, toolName) {
253
+ return `ui-${effect}:${sanitizeIdPart(toolName)}:`;
254
+ }
255
+
256
+ function policyBody(effect, toolName) {
257
+ const keyword = effect === "forbid" ? "forbid" : "permit";
258
+ return `${keyword}(principal, action, resource)\nwhen {\n action == Action::\"Invoke\" && resource.name == \"${toolName}\"\n};`;
259
+ }
260
+
261
+ async function removePoliciesByPrefix(prefix) {
262
+ const targets = policies.filter((policy) => policy.id.startsWith(prefix));
263
+ for (const policy of targets) {
264
+ await fetchJson(`${API_BASE}/policies/${encodeURIComponent(policy.id)}`, {
265
+ method: "DELETE",
266
+ });
267
+ }
268
+ }
269
+
270
+ async function applyPolicyChange(effect, toolName) {
271
+ const opposite = effect === "permit" ? "forbid" : "permit";
272
+ await removePoliciesByPrefix(policyIdPrefix(opposite, toolName));
273
+ const id = `${policyIdPrefix(effect, toolName)}${Date.now()}`;
274
+ const content = policyBody(effect, toolName);
275
+ await fetchJson(`${API_BASE}/policies`, {
276
+ method: "POST",
277
+ body: JSON.stringify({ id, content }),
278
+ });
279
+ }
280
+
243
281
  policyCreateBtn.addEventListener("click", async () => {
244
282
  const id = policyIdInput.value.trim();
245
283
  const content = policyContentInput.value.trim();
package/assets/styles.css CHANGED
@@ -232,6 +232,26 @@ input {
232
232
  flex-wrap: wrap;
233
233
  }
234
234
 
235
+ .audit-detail {
236
+ grid-column: 1 / -1;
237
+ border-top: 1px dashed var(--border);
238
+ margin-top: 6px;
239
+ padding-top: 6px;
240
+ font-size: 0.72rem;
241
+ color: var(--muted);
242
+ }
243
+
244
+ .audit-detail pre {
245
+ margin: 6px 0 0;
246
+ padding: 8px;
247
+ border-radius: 6px;
248
+ border: 1px solid var(--border);
249
+ background: #f3f5f9;
250
+ font-size: 0.72rem;
251
+ line-height: 1.35;
252
+ overflow: auto;
253
+ }
254
+
235
255
  .policy-grid {
236
256
  display: grid;
237
257
  grid-template-columns: 220px 1fr;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawclamp",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "OpenClaw Cedar authorization guard with audit UI",
5
5
  "type": "module",
6
6
  "dependencies": {
package/src/config.ts CHANGED
@@ -31,7 +31,7 @@ const DEFAULT_RISK_OVERRIDES: Record<string, RiskLevel> = {
31
31
 
32
32
  export const DEFAULT_CLAWCLAMP_CONFIG: ClawClampConfig = {
33
33
  enabled: true,
34
- mode: "enforce",
34
+ mode: "gray",
35
35
  principalId: "openclaw",
36
36
  policyFailOpen: false,
37
37
  risk: {
package/src/guard.ts CHANGED
@@ -9,7 +9,6 @@ import { getCedarling } from "./cedarling.js";
9
9
  import { appendAuditEntry, createAuditEntryId } from "./audit.js";
10
10
  import { listGrants, findActiveGrant } from "./grants.js";
11
11
  import { getModeOverride } from "./mode.js";
12
- import { isToolDenied, listDeniedTools } from "./denies.js";
13
12
  import { buildDefaultPolicyStore } from "./policy.js";
14
13
  import { ensurePolicyStore } from "./policy-store.js";
15
14
  import type { AuditEntry, ClawClampConfig, ClawClampMode, RiskLevel } from "./types.js";
@@ -173,13 +172,9 @@ export class ClawClampService {
173
172
  const auditId = createAuditEntryId();
174
173
  const mode = await this.getEffectiveMode();
175
174
  const grant = await this.resolveGrant(toolName);
176
- const deniedTools = await listDeniedTools(this.stateDir);
177
- const explicitlyDenied = isToolDenied(deniedTools, toolName);
178
175
 
179
176
  let cedarDecision: CedarEvaluation;
180
- if (explicitlyDenied) {
181
- cedarDecision = { decision: "deny", reason: "Denied by operator" };
182
- } else if (this.config.enabled) {
177
+ if (this.config.enabled) {
183
178
  const cedarling = await this.getCedarlingInstance();
184
179
  cedarDecision = await evaluateCedar({
185
180
  cedarling,
package/src/http.ts CHANGED
@@ -4,7 +4,6 @@ import type { IncomingMessage, ServerResponse } from "node:http";
4
4
  import { listGrants, createGrant, revokeGrant } from "./grants.js";
5
5
  import { readAuditEntries } from "./audit.js";
6
6
  import { getModeOverride, setModeOverride } from "./mode.js";
7
- import { addDeniedTool, listDeniedTools, removeDeniedTool } from "./denies.js";
8
7
  import { createPolicy, deletePolicy, listPolicies, updatePolicy } from "./policy-store.js";
9
8
  import type { ClawClampConfig, ClawClampMode } from "./types.js";
10
9
 
@@ -290,40 +289,6 @@ export function createClawClampHttpHandler(params: {
290
289
  return true;
291
290
  }
292
291
 
293
- if (apiPath === "denies" && req.method === "GET") {
294
- const tools = await listDeniedTools(params.stateDir);
295
- sendJson(res, 200, { tools });
296
- return true;
297
- }
298
-
299
- if (apiPath === "denies" && req.method === "POST") {
300
- try {
301
- const body = (await readJsonBody(req)) as Record<string, unknown>;
302
- const toolName = typeof body?.toolName === "string" ? body.toolName.trim() : "";
303
- if (!toolName) {
304
- sendJson(res, 400, { error: "toolName is required" });
305
- return true;
306
- }
307
- const tools = await addDeniedTool(params.stateDir, toolName);
308
- sendJson(res, 200, { tools });
309
- return true;
310
- } catch (error) {
311
- sendJson(res, 400, { error: error instanceof Error ? error.message : String(error) });
312
- return true;
313
- }
314
- }
315
-
316
- if (apiPath.startsWith("denies/") && req.method === "DELETE") {
317
- const toolName = apiPath.slice("denies/".length);
318
- if (!toolName) {
319
- sendJson(res, 400, { error: "toolName is required" });
320
- return true;
321
- }
322
- const tools = await removeDeniedTool(params.stateDir, toolName);
323
- sendJson(res, 200, { tools });
324
- return true;
325
- }
326
-
327
292
  if (apiPath === "policies" && req.method === "GET") {
328
293
  if (params.config.policyStoreUri) {
329
294
  sendJson(res, 200, { readOnly: true, policies: [], schema: "" });
package/src/policy.ts CHANGED
@@ -24,11 +24,7 @@ action "Invoke" appliesTo {
24
24
  };
25
25
  `;
26
26
 
27
- const DEFAULT_POLICIES = [
28
- `permit(principal, action, resource)
29
- when {
30
- action == Action::"Invoke" && resource.risk == "low"
31
- };`,
27
+ const DEFAULT_POLICIES: string[] = [
32
28
  `permit(principal, action, resource)
33
29
  when {
34
30
  action == Action::"Invoke" && context.grant_active == true
package/src/denies.ts DELETED
@@ -1,75 +0,0 @@
1
- import fs from "node:fs/promises";
2
- import path from "node:path";
3
- import { readJsonFileWithFallback, writeJsonFileAtomically } from "openclaw/plugin-sdk";
4
- import { withStateFileLock } from "./storage.js";
5
-
6
- const DENIES_FILE = "denies.json";
7
-
8
- type DenyState = {
9
- tools: string[];
10
- };
11
-
12
- const EMPTY_STATE: DenyState = { tools: [] };
13
-
14
- function resolveDenyPath(stateDir: string): string {
15
- return path.join(stateDir, "clawclamp", DENIES_FILE);
16
- }
17
-
18
- async function readDenyState(stateDir: string): Promise<DenyState> {
19
- const filePath = resolveDenyPath(stateDir);
20
- const { value } = await readJsonFileWithFallback(filePath, EMPTY_STATE);
21
- if (!value || typeof value !== "object" || Array.isArray(value)) {
22
- return { tools: [] };
23
- }
24
- const tools = Array.isArray((value as DenyState).tools) ? (value as DenyState).tools : [];
25
- return { tools };
26
- }
27
-
28
- async function writeDenyState(stateDir: string, state: DenyState): Promise<void> {
29
- const filePath = resolveDenyPath(stateDir);
30
- await fs.mkdir(path.dirname(filePath), { recursive: true });
31
- await writeJsonFileAtomically(filePath, state);
32
- }
33
-
34
- function normalizeToolName(toolName: string): string {
35
- return toolName.trim();
36
- }
37
-
38
- export async function listDeniedTools(stateDir: string): Promise<string[]> {
39
- return withStateFileLock(stateDir, "denies", async () => {
40
- const state = await readDenyState(stateDir);
41
- return state.tools;
42
- });
43
- }
44
-
45
- export async function addDeniedTool(stateDir: string, toolName: string): Promise<string[]> {
46
- return withStateFileLock(stateDir, "denies", async () => {
47
- const state = await readDenyState(stateDir);
48
- const normalized = normalizeToolName(toolName);
49
- if (!normalized) {
50
- return state.tools;
51
- }
52
- if (!state.tools.includes(normalized)) {
53
- state.tools.push(normalized);
54
- await writeDenyState(stateDir, state);
55
- }
56
- return state.tools;
57
- });
58
- }
59
-
60
- export async function removeDeniedTool(stateDir: string, toolName: string): Promise<string[]> {
61
- return withStateFileLock(stateDir, "denies", async () => {
62
- const state = await readDenyState(stateDir);
63
- const normalized = normalizeToolName(toolName);
64
- const next = state.tools.filter((tool) => tool !== normalized);
65
- if (next.length !== state.tools.length) {
66
- await writeDenyState(stateDir, { tools: next });
67
- return next;
68
- }
69
- return state.tools;
70
- });
71
- }
72
-
73
- export function isToolDenied(deniedTools: string[], toolName: string): boolean {
74
- return deniedTools.includes(toolName) || deniedTools.includes("*");
75
- }