clawclamp 0.1.0

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/index.ts ADDED
@@ -0,0 +1,103 @@
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 "openclaw/plugin-sdk/core";
6
+
7
+ export const plugin = {
8
+ id: "clawclamp",
9
+ name: "ClawClamp Policy Audit",
10
+ description: "Cedarling-based permission control with audit logging and dry-run mode.",
11
+ 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
65
+ api.registerHttpRoute({
66
+ path: "/",
67
+ auth: "plugin",
68
+ 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
+ },
97
+ });
98
+
99
+ api.logger.info("Cedar Audit Plugin Activated");
100
+ }
101
+ };
102
+
103
+ export default plugin;
@@ -0,0 +1,29 @@
1
+ {
2
+ "id": "clawclamp",
3
+ "name": "ClawClamp Policy Audit",
4
+ "description": "Cedarling-based permission control with audit logging and dry-run mode.",
5
+ "configSchema": {
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
+ }
28
+ }
29
+ }
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "id": "clawclamp",
3
+ "name": "clawclamp",
4
+ "description": "Cedarling-based permission control with audit logging and dry-run mode.",
5
+ "version": "0.1.0",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "license": "MIT",
9
+ "scripts": {
10
+ "build": "tsdown",
11
+ "dev": "tsdown --watch",
12
+ "typecheck": "tsc --noEmit"
13
+ },
14
+ "dependencies": {
15
+ "hono": "4.12.7"
16
+ },
17
+ "devDependencies": {
18
+ "tsdown": "0.21.0",
19
+ "typescript": "^5.9.3",
20
+ "@types/node": "^22.10.2"
21
+ }
22
+ }
@@ -0,0 +1,66 @@
1
+
2
+ import { join } from 'path';
3
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
4
+
5
+ export type AuditEntry = {
6
+ timestamp: string;
7
+ runId?: string;
8
+ principal: string;
9
+ action: string;
10
+ resource: string;
11
+ decision: "Allow" | "Deny";
12
+ mode: "enforce" | "monitor";
13
+ diagnostics?: string[];
14
+ blocked: boolean; // Was the tool execution actually blocked?
15
+ };
16
+
17
+ export class AuditStore {
18
+ private logFile: string;
19
+ private entries: AuditEntry[] = [];
20
+
21
+ constructor(workspaceDir: string) {
22
+ const auditDir = join(workspaceDir, '.openclaw', 'audit');
23
+ if (!existsSync(auditDir)) {
24
+ mkdirSync(auditDir, { recursive: true });
25
+ }
26
+ this.logFile = join(auditDir, 'cedar-audit.jsonl');
27
+ this.load();
28
+ }
29
+
30
+ private load() {
31
+ if (existsSync(this.logFile)) {
32
+ try {
33
+ const content = readFileSync(this.logFile, 'utf-8');
34
+ this.entries = content
35
+ .split('\n')
36
+ .filter(line => line.trim())
37
+ .map(line => JSON.parse(line));
38
+ } catch (e) {
39
+ console.error('Failed to load audit logs:', e);
40
+ }
41
+ }
42
+ }
43
+
44
+ append(entry: AuditEntry) {
45
+ this.entries.push(entry);
46
+ try {
47
+ // Append to file
48
+ // In a real implementation, use appendFileSync or a stream
49
+ const line = JSON.stringify(entry) + '\n';
50
+ // Simulating append by re-reading full file is inefficient but okay for MVP mock
51
+ // Better:
52
+ const fs = require('fs');
53
+ fs.appendFileSync(this.logFile, line);
54
+ } catch (e) {
55
+ console.error('Failed to write audit log:', e);
56
+ }
57
+ }
58
+
59
+ getAll(): AuditEntry[] {
60
+ return this.entries;
61
+ }
62
+
63
+ getRecent(limit: number = 50): AuditEntry[] {
64
+ return this.entries.slice(-limit);
65
+ }
66
+ }
@@ -0,0 +1,59 @@
1
+
2
+ // This is a mock implementation because we cannot compile WASM in this environment.
3
+ // In a real scenario, this would import the WASM module.
4
+
5
+ export type CedarContext = Record<string, any>;
6
+ export type CedarDecision = "Allow" | "Deny";
7
+ export type CedarResult = {
8
+ decision: CedarDecision;
9
+ diagnostics: string[];
10
+ };
11
+
12
+ export class CedarEngine {
13
+ private policyStore: any;
14
+ private mode: "enforce" | "monitor" = "monitor";
15
+
16
+ constructor(config: { mode?: string; policyStoreUrl?: string }) {
17
+ this.mode = (config.mode as any) || "monitor";
18
+ console.log(`[CedarEngine] Initialized in ${this.mode} mode`);
19
+ }
20
+
21
+ setMode(mode: "enforce" | "monitor") {
22
+ this.mode = mode;
23
+ console.log(`[CedarEngine] Switched to ${this.mode} mode`);
24
+ }
25
+
26
+ getMode() {
27
+ return this.mode;
28
+ }
29
+
30
+ // Mock authorize function based on Cedarling docs
31
+ async authorize(params: {
32
+ principal: string;
33
+ action: string;
34
+ resource: string;
35
+ context: CedarContext;
36
+ }): Promise<CedarResult> {
37
+ console.log(`[CedarEngine] Evaluating: ${params.principal} -> ${params.action} on ${params.resource}`);
38
+
39
+ // Simulate some logic:
40
+ // Deny 'rm -rf' or dangerous commands unless principal is 'admin'
41
+ // Allow everything else for now in this mock
42
+
43
+ let decision: CedarDecision = "Allow";
44
+ const diagnostics: string[] = [];
45
+
46
+ if (params.action.includes("exec") && params.resource.includes("rm")) {
47
+ if (params.principal !== 'User::"admin"') {
48
+ decision = "Deny";
49
+ diagnostics.push("Dangerous command 'rm' is restricted to admin.");
50
+ }
51
+ }
52
+
53
+ // In Monitor mode, we log the *would be* decision but return Allow to the caller (effectively).
54
+ // However, the caller (plugin hook) needs to know the *policy decision* to log it correctly.
55
+ // The hook will decide whether to block based on the mode.
56
+
57
+ return { decision, diagnostics };
58
+ }
59
+ }
package/src/server.ts ADDED
@@ -0,0 +1,92 @@
1
+
2
+ import { Hono } from "hono";
3
+ import { html } from "hono/html";
4
+ import type { AuditStore } from "./audit-store.js";
5
+ import type { CedarEngine } from "./cedar-engine.js";
6
+
7
+ export const createServer = (engine: CedarEngine, store: AuditStore) => {
8
+ const app = new Hono();
9
+
10
+ // Simple HTML Dashboard
11
+ app.get("/", (c) => {
12
+ const logs = store.getRecent(50);
13
+ const mode = engine.getMode();
14
+
15
+ return c.html(html`
16
+ <!DOCTYPE html>
17
+ <html>
18
+ <head>
19
+ <title>OpenClaw ClawClamp Audit</title>
20
+ <style>
21
+ body { font-family: sans-serif; padding: 20px; }
22
+ table { width: 100%; border-collapse: collapse; margin-top: 20px; }
23
+ th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
24
+ th { background-color: #f2f2f2; }
25
+ .allow { color: green; }
26
+ .deny { color: red; }
27
+ .blocked { font-weight: bold; color: darkred; }
28
+ .monitored { color: orange; }
29
+ </style>
30
+ </head>
31
+ <body>
32
+ <h1>ClawClamp Policy Audit</h1>
33
+ <p>Current Mode: <strong>${mode.toUpperCase()}</strong></p>
34
+ <form action="/mode" method="post">
35
+ <select name="mode">
36
+ <option value="monitor" ${mode === "monitor" ? "selected" : ""}>Monitor (Log Only)</option>
37
+ <option value="enforce" ${mode === "enforce" ? "selected" : ""}>Enforce (Block)</option>
38
+ </select>
39
+ <button type="submit">Update Mode</button>
40
+ </form>
41
+
42
+ <h2>Recent Tool Executions</h2>
43
+ <table>
44
+ <thead>
45
+ <tr>
46
+ <th>Time</th>
47
+ <th>Run ID</th>
48
+ <th>Principal</th>
49
+ <th>Action</th>
50
+ <th>Resource</th>
51
+ <th>Decision</th>
52
+ <th>Result</th>
53
+ </tr>
54
+ </thead>
55
+ <tbody>
56
+ ${logs.map((log) => html`
57
+ <tr>
58
+ <td>${new Date(log.timestamp).toLocaleTimeString()}</td>
59
+ <td>${log.runId || "-"}</td>
60
+ <td>${log.principal}</td>
61
+ <td>${log.action}</td>
62
+ <td>${log.resource}</td>
63
+ <td class="${log.decision.toLowerCase()}">${log.decision}</td>
64
+ <td class="${log.blocked ? "blocked" : "monitored"}">
65
+ ${log.blocked ? "BLOCKED" : "ALLOWED"}
66
+ </td>
67
+ </tr>
68
+ `)}
69
+ </tbody>
70
+ </table>
71
+ </body>
72
+ </html>
73
+ `);
74
+ });
75
+
76
+ // API to update mode
77
+ app.post("/mode", async (c) => {
78
+ const body = await c.req.parseBody();
79
+ const mode = body["mode"] as "monitor" | "enforce";
80
+ if (mode === "monitor" || mode === "enforce") {
81
+ engine.setMode(mode);
82
+ }
83
+ return c.redirect("/");
84
+ });
85
+
86
+ // API to get logs JSON
87
+ app.get("/api/logs", (c) => {
88
+ return c.json(store.getAll());
89
+ });
90
+
91
+ return app;
92
+ };