@plusplus7/clawclamp 0.1.0 → 0.1.1

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/assets/app.js CHANGED
@@ -44,6 +44,31 @@ async function fetchJson(path, opts = {}) {
44
44
  return payload;
45
45
  }
46
46
 
47
+ function showAlert(message) {
48
+ window.alert(message);
49
+ }
50
+
51
+ function assertOperationOk(payload, fallbackMessage) {
52
+ if (payload && Object.prototype.hasOwnProperty.call(payload, "ok") && payload.ok === false) {
53
+ throw new Error(fallbackMessage);
54
+ }
55
+ return payload;
56
+ }
57
+
58
+ async function runAction(action, successMessage, failurePrefix) {
59
+ try {
60
+ const result = await action();
61
+ if (successMessage) {
62
+ showAlert(successMessage);
63
+ }
64
+ return result;
65
+ } catch (error) {
66
+ const message = error instanceof Error ? error.message : String(error);
67
+ showAlert(`${failurePrefix}${message}`);
68
+ throw error;
69
+ }
70
+ }
71
+
47
72
  function formatTime(iso) {
48
73
  if (!iso) return "-";
49
74
  const date = new Date(iso);
@@ -79,8 +104,15 @@ function renderGrants(grants) {
79
104
  <button class="btn ghost" data-id="${grant.id}">撤销</button>
80
105
  `;
81
106
  item.querySelector("button").addEventListener("click", async () => {
82
- await fetchJson(`${API_BASE}/grants/${grant.id}`, { method: "DELETE" });
83
- await refreshAll();
107
+ try {
108
+ await runAction(async () => {
109
+ const result = await fetchJson(`${API_BASE}/grants/${encodeURIComponent(grant.id)}`, {
110
+ method: "DELETE",
111
+ });
112
+ assertOperationOk(result, "撤销授权失败");
113
+ await refreshAll();
114
+ }, "已撤销短期授权。", "撤销短期授权失败:");
115
+ } catch {}
84
116
  });
85
117
  grantsEl.appendChild(item);
86
118
  });
@@ -154,15 +186,23 @@ function renderAudit(entries) {
154
186
  const allowBtn = row.querySelector("button[data-action=\"allow\"]");
155
187
  if (allowBtn) {
156
188
  allowBtn.addEventListener("click", async () => {
157
- await applyPolicyChange("permit", entry.toolName);
158
- await refreshAll();
189
+ try {
190
+ await runAction(async () => {
191
+ await applyPolicyChange("permit", entry.toolName);
192
+ await refreshAll();
193
+ }, "已添加允许策略。", "添加允许策略失败:");
194
+ } catch {}
159
195
  });
160
196
  }
161
197
  const denyBtn = row.querySelector("button[data-action=\"deny\"]");
162
198
  if (denyBtn) {
163
199
  denyBtn.addEventListener("click", async () => {
164
- await applyPolicyChange("forbid", entry.toolName);
165
- await refreshAll();
200
+ try {
201
+ await runAction(async () => {
202
+ await applyPolicyChange("forbid", entry.toolName);
203
+ await refreshAll();
204
+ }, "已添加拒绝策略。", "添加拒绝策略失败:");
205
+ } catch {}
166
206
  });
167
207
  }
168
208
  auditEl.appendChild(row);
@@ -226,19 +266,27 @@ refreshBtn.addEventListener("click", () => {
226
266
  });
227
267
 
228
268
  enforceBtn.addEventListener("click", async () => {
229
- await fetchJson(`${API_BASE}/mode`, {
230
- method: "POST",
231
- body: JSON.stringify({ mode: "enforce" }),
232
- });
233
- await refreshAll();
269
+ try {
270
+ await runAction(async () => {
271
+ await fetchJson(`${API_BASE}/mode`, {
272
+ method: "POST",
273
+ body: JSON.stringify({ mode: "enforce" }),
274
+ });
275
+ await refreshAll();
276
+ }, "已切换到强制模式。", "切换模式失败:");
277
+ } catch {}
234
278
  });
235
279
 
236
280
  grayBtn.addEventListener("click", async () => {
237
- await fetchJson(`${API_BASE}/mode`, {
238
- method: "POST",
239
- body: JSON.stringify({ mode: "gray" }),
240
- });
241
- await refreshAll();
281
+ try {
282
+ await runAction(async () => {
283
+ await fetchJson(`${API_BASE}/mode`, {
284
+ method: "POST",
285
+ body: JSON.stringify({ mode: "gray" }),
286
+ });
287
+ await refreshAll();
288
+ }, "已切换到灰度模式。", "切换模式失败:");
289
+ } catch {}
242
290
  });
243
291
 
244
292
  grantForm.addEventListener("submit", async (event) => {
@@ -246,14 +294,18 @@ grantForm.addEventListener("submit", async (event) => {
246
294
  const toolName = grantToolInput.value.trim();
247
295
  const ttlSeconds = grantTtlInput.value ? Number(grantTtlInput.value) : undefined;
248
296
  const note = grantNoteInput.value.trim();
249
- await fetchJson(`${API_BASE}/grants`, {
250
- method: "POST",
251
- body: JSON.stringify({ toolName, ttlSeconds, note: note || undefined }),
252
- });
253
- grantToolInput.value = "";
254
- grantTtlInput.value = "";
255
- grantNoteInput.value = "";
256
- await refreshAll();
297
+ try {
298
+ await runAction(async () => {
299
+ await fetchJson(`${API_BASE}/grants`, {
300
+ method: "POST",
301
+ body: JSON.stringify({ toolName, ttlSeconds, note: note || undefined }),
302
+ });
303
+ grantToolInput.value = "";
304
+ grantTtlInput.value = "";
305
+ grantNoteInput.value = "";
306
+ await refreshAll();
307
+ }, "已创建短期授权。", "创建短期授权失败:");
308
+ } catch {}
257
309
  });
258
310
 
259
311
  auditPageSizeEl.addEventListener("change", async () => {
@@ -294,9 +346,10 @@ function policyBody(effect, toolName) {
294
346
  async function removePoliciesByPrefix(prefix) {
295
347
  const targets = policies.filter((policy) => policy.id.startsWith(prefix));
296
348
  for (const policy of targets) {
297
- await fetchJson(`${API_BASE}/policies/${encodeURIComponent(policy.id)}`, {
349
+ const result = await fetchJson(`${API_BASE}/policies/${encodeURIComponent(policy.id)}`, {
298
350
  method: "DELETE",
299
351
  });
352
+ assertOperationOk(result, `删除策略失败: ${policy.id}`);
300
353
  }
301
354
  }
302
355
 
@@ -319,12 +372,16 @@ policyCreateBtn.addEventListener("click", async () => {
319
372
  return;
320
373
  }
321
374
  const body = { content, ...(id ? { id } : {}) };
322
- await fetchJson(`${API_BASE}/policies`, {
323
- method: "POST",
324
- body: JSON.stringify(body),
325
- });
326
- policyStatusEl.textContent = "已新增策略。";
327
- await refreshAll();
375
+ try {
376
+ await runAction(async () => {
377
+ await fetchJson(`${API_BASE}/policies`, {
378
+ method: "POST",
379
+ body: JSON.stringify(body),
380
+ });
381
+ policyStatusEl.textContent = "已新增策略。";
382
+ await refreshAll();
383
+ }, "已新增策略。", "新增策略失败:");
384
+ } catch {}
328
385
  });
329
386
 
330
387
  policyUpdateBtn.addEventListener("click", async () => {
@@ -338,12 +395,16 @@ policyUpdateBtn.addEventListener("click", async () => {
338
395
  policyStatusEl.textContent = "请输入 Policy 内容。";
339
396
  return;
340
397
  }
341
- await fetchJson(`${API_BASE}/policies/${encodeURIComponent(id)}`, {
342
- method: "PUT",
343
- body: JSON.stringify({ content }),
344
- });
345
- policyStatusEl.textContent = "已保存策略。";
346
- await refreshAll();
398
+ try {
399
+ await runAction(async () => {
400
+ await fetchJson(`${API_BASE}/policies/${encodeURIComponent(id)}`, {
401
+ method: "PUT",
402
+ body: JSON.stringify({ content }),
403
+ });
404
+ policyStatusEl.textContent = "已保存策略。";
405
+ await refreshAll();
406
+ }, "已保存策略。", "保存策略失败:");
407
+ } catch {}
347
408
  });
348
409
 
349
410
  policyDeleteBtn.addEventListener("click", async () => {
@@ -352,11 +413,16 @@ policyDeleteBtn.addEventListener("click", async () => {
352
413
  policyStatusEl.textContent = "请选择或填写 Policy ID。";
353
414
  return;
354
415
  }
355
- await fetchJson(`${API_BASE}/policies/${encodeURIComponent(id)}`, {
356
- method: "DELETE",
357
- });
358
- policyStatusEl.textContent = "已删除策略。";
359
- policyIdInput.value = "";
360
- policyContentInput.value = "";
361
- await refreshAll();
416
+ try {
417
+ await runAction(async () => {
418
+ const result = await fetchJson(`${API_BASE}/policies/${encodeURIComponent(id)}`, {
419
+ method: "DELETE",
420
+ });
421
+ assertOperationOk(result, "删除策略失败");
422
+ policyStatusEl.textContent = "已删除策略。";
423
+ policyIdInput.value = "";
424
+ policyContentInput.value = "";
425
+ await refreshAll();
426
+ }, "已删除策略。", "删除策略失败:");
427
+ } catch {}
362
428
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plusplus7/clawclamp",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "OpenClaw Cedar authorization guard with audit UI",
5
5
  "type": "module",
6
6
  "dependencies": {
package/src/grants.ts CHANGED
@@ -42,12 +42,37 @@ function decodeBase64(value: string): string {
42
42
  return Buffer.from(value, "base64").toString("utf8");
43
43
  }
44
44
 
45
+ function normalizePolicyRecord(
46
+ id: string,
47
+ record: PolicyRecord,
48
+ cedarVersion: string,
49
+ fallbackDescription: string,
50
+ ): PolicyRecord {
51
+ return {
52
+ cedar_version: record.cedar_version ?? cedarVersion,
53
+ name: record.name ?? id,
54
+ description: record.description ?? fallbackDescription,
55
+ policy_content: record.policy_content,
56
+ };
57
+ }
58
+
45
59
  async function readPolicyStore(stateDir: string): Promise<PolicyStoreSnapshot> {
46
60
  const filePath = resolvePolicyPath(stateDir);
47
61
  const { value } = await readJsonFileWithFallback<PolicyStoreSnapshot>(
48
62
  filePath,
49
63
  buildDefaultPolicyStore() as PolicyStoreSnapshot,
50
64
  );
65
+ const policyStore = value.policy_stores?.[POLICY_STORE_ID];
66
+ if (policyStore?.policies) {
67
+ for (const [id, record] of Object.entries(policyStore.policies)) {
68
+ policyStore.policies[id] = normalizePolicyRecord(
69
+ id,
70
+ record,
71
+ value.cedar_version,
72
+ "Managed by Clawclamp.",
73
+ );
74
+ }
75
+ }
51
76
  return value;
52
77
  }
53
78
 
@@ -152,12 +177,16 @@ export async function createGrant(params: {
152
177
  );
153
178
  const expiresAtMs = nowMs + ttlSeconds * 1000;
154
179
  const id = grantPolicyId(nowMs);
155
- policyStore.policies![id] = {
156
- cedar_version: store.cedar_version,
157
- name: `Grant ${params.toolName}`,
158
- description: params.note?.trim() || undefined,
159
- policy_content: encodeBase64(buildGrantPolicy(params.toolName, expiresAtMs)),
160
- };
180
+ policyStore.policies![id] = normalizePolicyRecord(
181
+ id,
182
+ {
183
+ name: `Grant ${params.toolName}`,
184
+ description: params.note?.trim() || "Temporary grant policy.",
185
+ policy_content: encodeBase64(buildGrantPolicy(params.toolName, expiresAtMs)),
186
+ },
187
+ store.cedar_version,
188
+ "Temporary grant policy.",
189
+ );
161
190
  await writePolicyStore(params.stateDir, store);
162
191
  return {
163
192
  id,
package/src/guard.ts CHANGED
@@ -151,7 +151,7 @@ async function evaluateCedar(params: {
151
151
  const request = {
152
152
  principals: [
153
153
  {
154
- type: "User",
154
+ type: "Jans::User",
155
155
  id: params.config.principalId,
156
156
  role: "operator",
157
157
  },
@@ -180,7 +180,7 @@ async function evaluateCedar(params: {
180
180
  `[clawclamp] cedar response ${JSON.stringify({ request, result: jsonString })}`,
181
181
  );
182
182
  const parsed = JSON.parse(jsonString) as unknown;
183
- return parseCedarDecision(parsed, "User", params.config.principalId);
183
+ return parseCedarDecision(parsed, "Jans::User", params.config.principalId);
184
184
  } catch (error) {
185
185
  return { decision: "error", reason: error instanceof Error ? error.message : String(error) };
186
186
  }
package/src/http.ts CHANGED
@@ -48,6 +48,14 @@ function parseUrl(rawUrl?: string): URL | null {
48
48
  }
49
49
  }
50
50
 
51
+ function decodePathComponent(value: string): string {
52
+ try {
53
+ return decodeURIComponent(value);
54
+ } catch {
55
+ return value;
56
+ }
57
+ }
58
+
51
59
  function getHeader(req: IncomingMessage, name: string): string | undefined {
52
60
  const raw = req.headers[name.toLowerCase()];
53
61
  if (typeof raw === "string") {
@@ -337,7 +345,7 @@ export function createClawClampHttpHandler(params: {
337
345
  return true;
338
346
  }
339
347
  try {
340
- const id = apiPath.slice("policies/".length);
348
+ const id = decodePathComponent(apiPath.slice("policies/".length));
341
349
  if (!id) {
342
350
  sendJson(res, 400, { error: "policy id required" });
343
351
  return true;
@@ -365,7 +373,7 @@ export function createClawClampHttpHandler(params: {
365
373
  sendJson(res, 400, { error: "policyStoreUri is read-only" });
366
374
  return true;
367
375
  }
368
- const id = apiPath.slice("policies/".length);
376
+ const id = decodePathComponent(apiPath.slice("policies/".length));
369
377
  if (!id) {
370
378
  sendJson(res, 400, { error: "policy id required" });
371
379
  return true;
@@ -414,7 +422,7 @@ export function createClawClampHttpHandler(params: {
414
422
  }
415
423
 
416
424
  if (apiPath.startsWith("grants/") && req.method === "DELETE") {
417
- const grantId = apiPath.slice("grants/".length);
425
+ const grantId = decodePathComponent(apiPath.slice("grants/".length));
418
426
  if (!grantId) {
419
427
  sendJson(res, 400, { error: "grant id required" });
420
428
  return true;
@@ -50,12 +50,37 @@ function encodeBase64(value: string): string {
50
50
  return Buffer.from(value, "utf8").toString("base64");
51
51
  }
52
52
 
53
+ function normalizePolicyRecord(
54
+ id: string,
55
+ record: PolicyRecord,
56
+ cedarVersion: string,
57
+ fallbackDescription: string,
58
+ ): PolicyRecord {
59
+ return {
60
+ cedar_version: record.cedar_version ?? cedarVersion,
61
+ name: record.name ?? id,
62
+ description: record.description ?? fallbackDescription,
63
+ policy_content: record.policy_content,
64
+ };
65
+ }
66
+
53
67
  async function readPolicyStore(stateDir: string): Promise<PolicyStoreSnapshot> {
54
68
  const filePath = resolvePolicyPath(stateDir);
55
69
  const { value } = await readJsonFileWithFallback<PolicyStoreSnapshot>(
56
70
  filePath,
57
71
  buildDefaultPolicyStore() as PolicyStoreSnapshot,
58
72
  );
73
+ const policyStore = value.policy_stores?.[POLICY_STORE_ID];
74
+ if (policyStore?.policies) {
75
+ for (const [id, record] of Object.entries(policyStore.policies)) {
76
+ policyStore.policies[id] = normalizePolicyRecord(
77
+ id,
78
+ record,
79
+ value.cedar_version,
80
+ "Managed by Clawclamp.",
81
+ );
82
+ }
83
+ }
59
84
  return value;
60
85
  }
61
86
 
@@ -140,10 +165,12 @@ export async function createPolicy(params: {
140
165
  throw new Error("policy id already exists");
141
166
  }
142
167
  policyStore.policies[id] = {
143
- cedar_version: store.cedar_version,
144
- name: id,
145
- description: "Created from Clawclamp UI.",
146
- policy_content: encodeBase64(params.content),
168
+ ...normalizePolicyRecord(
169
+ id,
170
+ { policy_content: encodeBase64(params.content) },
171
+ store.cedar_version,
172
+ "Created from Clawclamp UI.",
173
+ ),
147
174
  };
148
175
  await writePolicyStore(params.stateDir, store);
149
176
  return { id, content: params.content };
@@ -162,10 +189,12 @@ export async function updatePolicy(params: {
162
189
  throw new Error("policy id not found");
163
190
  }
164
191
  policyStore.policies[params.id] = {
165
- cedar_version: store.cedar_version,
166
- name: params.id,
167
- description: "Updated from Clawclamp UI.",
168
- policy_content: encodeBase64(params.content),
192
+ ...normalizePolicyRecord(
193
+ params.id,
194
+ { policy_content: encodeBase64(params.content) },
195
+ store.cedar_version,
196
+ "Updated from Clawclamp UI.",
197
+ ),
169
198
  };
170
199
  await writePolicyStore(params.stateDir, store);
171
200
  return { id: params.id, content: params.content };
package/src/policy.ts CHANGED
@@ -1,6 +1,10 @@
1
- const DEFAULT_SCHEMA = `entity User = {
1
+ const DEFAULT_SCHEMA = `namespace Jans {
2
+ entity Role;
3
+
4
+ entity User in [Role] = {
2
5
  role: String
3
6
  };
7
+ }
4
8
 
5
9
  entity Tool = {
6
10
  name: String,
@@ -8,7 +12,7 @@ entity Tool = {
8
12
  };
9
13
 
10
14
  action "Invoke" appliesTo {
11
- principal: [User],
15
+ principal: [Jans::User],
12
16
  resource: [Tool],
13
17
  context: {
14
18
  now: Long,