clawclamp 0.1.3 → 0.1.4
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/clawclamp-0.1.3.tgz +0 -0
- package/index.js +224 -0
- package/package.json +5 -5
- package/tsdown.config.ts +2 -1
|
Binary file
|
package/index.js
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync } from "fs";
|
|
4
|
+
import { Hono } from "hono";
|
|
5
|
+
import { html } from "hono/html";
|
|
6
|
+
//#region \0rolldown/runtime.js
|
|
7
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
8
|
+
//#endregion
|
|
9
|
+
//#region src/cedar-engine.ts
|
|
10
|
+
var CedarEngine = class {
|
|
11
|
+
constructor(config) {
|
|
12
|
+
this.mode = "monitor";
|
|
13
|
+
this.mode = config.mode || "monitor";
|
|
14
|
+
console.log(`[CedarEngine] Initialized in ${this.mode} mode`);
|
|
15
|
+
}
|
|
16
|
+
setMode(mode) {
|
|
17
|
+
this.mode = mode;
|
|
18
|
+
console.log(`[CedarEngine] Switched to ${this.mode} mode`);
|
|
19
|
+
}
|
|
20
|
+
getMode() {
|
|
21
|
+
return this.mode;
|
|
22
|
+
}
|
|
23
|
+
async authorize(params) {
|
|
24
|
+
console.log(`[CedarEngine] Evaluating: ${params.principal} -> ${params.action} on ${params.resource}`);
|
|
25
|
+
let decision = "Allow";
|
|
26
|
+
const diagnostics = [];
|
|
27
|
+
if (params.action.includes("exec") && params.resource.includes("rm")) {
|
|
28
|
+
if (params.principal !== "User::\"admin\"") {
|
|
29
|
+
decision = "Deny";
|
|
30
|
+
diagnostics.push("Dangerous command 'rm' is restricted to admin.");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
decision,
|
|
35
|
+
diagnostics
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
//#endregion
|
|
40
|
+
//#region src/audit-store.ts
|
|
41
|
+
var AuditStore = class {
|
|
42
|
+
constructor(workspaceDir) {
|
|
43
|
+
this.entries = [];
|
|
44
|
+
const auditDir = join(workspaceDir, ".openclaw", "audit");
|
|
45
|
+
if (!existsSync(auditDir)) mkdirSync(auditDir, { recursive: true });
|
|
46
|
+
this.logFile = join(auditDir, "cedar-audit.jsonl");
|
|
47
|
+
this.load();
|
|
48
|
+
}
|
|
49
|
+
load() {
|
|
50
|
+
if (existsSync(this.logFile)) try {
|
|
51
|
+
this.entries = readFileSync(this.logFile, "utf-8").split("\n").filter((line) => line.trim()).map((line) => JSON.parse(line));
|
|
52
|
+
} catch (e) {
|
|
53
|
+
console.error("Failed to load audit logs:", e);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
append(entry) {
|
|
57
|
+
this.entries.push(entry);
|
|
58
|
+
try {
|
|
59
|
+
const line = JSON.stringify(entry) + "\n";
|
|
60
|
+
__require("fs").appendFileSync(this.logFile, line);
|
|
61
|
+
} catch (e) {
|
|
62
|
+
console.error("Failed to write audit log:", e);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
getAll() {
|
|
66
|
+
return this.entries;
|
|
67
|
+
}
|
|
68
|
+
getRecent(limit = 50) {
|
|
69
|
+
return this.entries.slice(-limit);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
//#endregion
|
|
73
|
+
//#region src/server.ts
|
|
74
|
+
const createServer = (engine, store) => {
|
|
75
|
+
const app = new Hono();
|
|
76
|
+
app.get("/", (c) => {
|
|
77
|
+
const logs = store.getRecent(50);
|
|
78
|
+
const mode = engine.getMode();
|
|
79
|
+
return c.html(html`
|
|
80
|
+
<!DOCTYPE html>
|
|
81
|
+
<html>
|
|
82
|
+
<head>
|
|
83
|
+
<title>OpenClaw ClawClamp Audit</title>
|
|
84
|
+
<style>
|
|
85
|
+
body { font-family: sans-serif; padding: 20px; }
|
|
86
|
+
table { width: 100%; border-collapse: collapse; margin-top: 20px; }
|
|
87
|
+
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
|
|
88
|
+
th { background-color: #f2f2f2; }
|
|
89
|
+
.allow { color: green; }
|
|
90
|
+
.deny { color: red; }
|
|
91
|
+
.blocked { font-weight: bold; color: darkred; }
|
|
92
|
+
.monitored { color: orange; }
|
|
93
|
+
</style>
|
|
94
|
+
</head>
|
|
95
|
+
<body>
|
|
96
|
+
<h1>ClawClamp Policy Audit</h1>
|
|
97
|
+
<p>Current Mode: <strong>${mode.toUpperCase()}</strong></p>
|
|
98
|
+
<form action="/mode" method="post">
|
|
99
|
+
<select name="mode">
|
|
100
|
+
<option value="monitor" ${mode === "monitor" ? "selected" : ""}>Monitor (Log Only)</option>
|
|
101
|
+
<option value="enforce" ${mode === "enforce" ? "selected" : ""}>Enforce (Block)</option>
|
|
102
|
+
</select>
|
|
103
|
+
<button type="submit">Update Mode</button>
|
|
104
|
+
</form>
|
|
105
|
+
|
|
106
|
+
<h2>Recent Tool Executions</h2>
|
|
107
|
+
<table>
|
|
108
|
+
<thead>
|
|
109
|
+
<tr>
|
|
110
|
+
<th>Time</th>
|
|
111
|
+
<th>Run ID</th>
|
|
112
|
+
<th>Principal</th>
|
|
113
|
+
<th>Action</th>
|
|
114
|
+
<th>Resource</th>
|
|
115
|
+
<th>Decision</th>
|
|
116
|
+
<th>Result</th>
|
|
117
|
+
</tr>
|
|
118
|
+
</thead>
|
|
119
|
+
<tbody>
|
|
120
|
+
${logs.map((log) => html`
|
|
121
|
+
<tr>
|
|
122
|
+
<td>${new Date(log.timestamp).toLocaleTimeString()}</td>
|
|
123
|
+
<td>${log.runId || "-"}</td>
|
|
124
|
+
<td>${log.principal}</td>
|
|
125
|
+
<td>${log.action}</td>
|
|
126
|
+
<td>${log.resource}</td>
|
|
127
|
+
<td class="${log.decision.toLowerCase()}">${log.decision}</td>
|
|
128
|
+
<td class="${log.blocked ? "blocked" : "monitored"}">
|
|
129
|
+
${log.blocked ? "BLOCKED" : "ALLOWED"}
|
|
130
|
+
</td>
|
|
131
|
+
</tr>
|
|
132
|
+
`)}
|
|
133
|
+
</tbody>
|
|
134
|
+
</table>
|
|
135
|
+
</body>
|
|
136
|
+
</html>
|
|
137
|
+
`);
|
|
138
|
+
});
|
|
139
|
+
app.post("/mode", async (c) => {
|
|
140
|
+
const mode = (await c.req.parseBody())["mode"];
|
|
141
|
+
if (mode === "monitor" || mode === "enforce") engine.setMode(mode);
|
|
142
|
+
return c.redirect("/");
|
|
143
|
+
});
|
|
144
|
+
app.get("/api/logs", (c) => {
|
|
145
|
+
return c.json(store.getAll());
|
|
146
|
+
});
|
|
147
|
+
return app;
|
|
148
|
+
};
|
|
149
|
+
//#endregion
|
|
150
|
+
//#region index.ts
|
|
151
|
+
const plugin = {
|
|
152
|
+
id: "clawclamp",
|
|
153
|
+
name: "ClawClamp Policy Audit",
|
|
154
|
+
description: "Cedarling-based permission control with audit logging and dry-run mode.",
|
|
155
|
+
register(api) {
|
|
156
|
+
const engine = new CedarEngine({
|
|
157
|
+
mode: api.pluginConfig?.mode || "monitor",
|
|
158
|
+
policyStoreUrl: api.pluginConfig?.policyStoreUrl
|
|
159
|
+
});
|
|
160
|
+
const auditStore = new AuditStore(api.config.workspaceDir || process.cwd());
|
|
161
|
+
api.on("before_tool_call", async (event, ctx) => {
|
|
162
|
+
const { toolName, params, runId } = event;
|
|
163
|
+
const { agentId, sessionId } = ctx;
|
|
164
|
+
const principal = `User::"${agentId || "unknown"}"`;
|
|
165
|
+
const action = `Action::"tool:${toolName}"`;
|
|
166
|
+
const resource = `Resource::"global"`;
|
|
167
|
+
const result = await engine.authorize({
|
|
168
|
+
principal,
|
|
169
|
+
action,
|
|
170
|
+
resource,
|
|
171
|
+
context: {
|
|
172
|
+
sessionId,
|
|
173
|
+
runId,
|
|
174
|
+
params,
|
|
175
|
+
timestamp: Date.now()
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
const shouldBlock = result.decision === "Deny" && engine.getMode() === "enforce";
|
|
179
|
+
auditStore.append({
|
|
180
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
181
|
+
runId,
|
|
182
|
+
principal,
|
|
183
|
+
action,
|
|
184
|
+
resource,
|
|
185
|
+
decision: result.decision,
|
|
186
|
+
mode: engine.getMode(),
|
|
187
|
+
diagnostics: result.diagnostics,
|
|
188
|
+
blocked: shouldBlock
|
|
189
|
+
});
|
|
190
|
+
if (shouldBlock) return {
|
|
191
|
+
block: true,
|
|
192
|
+
blockReason: `Cedar Policy Denied: ${result.diagnostics.join("; ")}`
|
|
193
|
+
};
|
|
194
|
+
});
|
|
195
|
+
api.registerHttpRoute({
|
|
196
|
+
path: "/",
|
|
197
|
+
auth: "plugin",
|
|
198
|
+
match: "prefix",
|
|
199
|
+
handler: async (req, res) => {
|
|
200
|
+
if (req.method === "GET" && (req.url === "/" || req.url === "")) {
|
|
201
|
+
const html = await (await createServer(engine, auditStore).request("/")).text();
|
|
202
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
203
|
+
res.end(html);
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
if (req.method === "POST" && req.url === "/mode") {
|
|
207
|
+
let body = "";
|
|
208
|
+
req.on("data", (chunk) => body += chunk);
|
|
209
|
+
req.on("end", () => {
|
|
210
|
+
const mode = new URLSearchParams(body).get("mode");
|
|
211
|
+
if (mode === "enforce" || mode === "monitor") engine.setMode(mode);
|
|
212
|
+
res.writeHead(302, { "Location": "./" });
|
|
213
|
+
res.end();
|
|
214
|
+
});
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
api.logger.info("Cedar Audit Plugin Activated");
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
//#endregion
|
|
224
|
+
export { plugin as default, plugin };
|
package/package.json
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
"id": "clawclamp",
|
|
3
3
|
"name": "clawclamp",
|
|
4
4
|
"description": "Cedarling-based permission control with audit logging and dry-run mode.",
|
|
5
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.4",
|
|
6
6
|
"type": "module",
|
|
7
|
-
"main": "
|
|
8
|
-
"types": "
|
|
7
|
+
"main": "index.js",
|
|
8
|
+
"types": "index.d.ts",
|
|
9
9
|
"license": "MIT",
|
|
10
10
|
"scripts": {
|
|
11
|
-
"build": "tsdown && mv
|
|
11
|
+
"build": "tsdown && mv index.mjs index.js",
|
|
12
12
|
"dev": "tsdown --watch",
|
|
13
13
|
"typecheck": "tsc --noEmit"
|
|
14
14
|
},
|
|
@@ -21,6 +21,6 @@
|
|
|
21
21
|
"@types/node": "^22.10.2"
|
|
22
22
|
},
|
|
23
23
|
"openclaw": {
|
|
24
|
-
"extensions": ["
|
|
24
|
+
"extensions": ["index.js"]
|
|
25
25
|
}
|
|
26
26
|
}
|