@aporthq/aport-agent-guardrails 1.0.21 → 1.0.22
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 +8 -3
- package/bin/openclaw +14 -14
- package/docs/PROVIDER.md +6 -6
- package/docs/QUICKSTART_OPENCLAW_PLUGIN.md +42 -408
- package/docs/RELEASE.md +3 -2
- package/docs/frameworks/openclaw.md +123 -39
- package/extensions/openclaw-aport/CHANGELOG.md +8 -2
- package/extensions/openclaw-aport/MIGRATION.md +22 -375
- package/extensions/openclaw-aport/README.md +72 -362
- package/extensions/openclaw-aport/api-client.js +22 -0
- package/extensions/openclaw-aport/audit.js +32 -0
- package/extensions/openclaw-aport/decision.js +21 -0
- package/extensions/openclaw-aport/index.js +169 -591
- package/extensions/openclaw-aport/local-evaluator.js +303 -0
- package/extensions/openclaw-aport/openclaw.plugin.json +5 -5
- package/extensions/openclaw-aport/package-lock.json +2 -2
- package/extensions/openclaw-aport/package.json +27 -6
- package/extensions/openclaw-aport/tool-mapping.js +89 -0
- package/package.json +1 -1
- package/extensions/openclaw-aport/index.ts +0 -547
- package/extensions/openclaw-aport/test.js +0 -356
|
@@ -1,547 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* APort OpenClaw Plugin
|
|
3
|
-
*
|
|
4
|
-
* Registers before_tool_call hook for deterministic policy enforcement.
|
|
5
|
-
* Calls APort guardrail (local or API) before every tool execution.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
|
9
|
-
import { spawn } from "child_process";
|
|
10
|
-
import { createHash, randomUUID } from "crypto";
|
|
11
|
-
import { readFile, mkdir, appendFile } from "fs/promises";
|
|
12
|
-
import { appendFileSync, mkdirSync, existsSync } from "fs";
|
|
13
|
-
import { join, dirname } from "path";
|
|
14
|
-
import { homedir } from "os";
|
|
15
|
-
|
|
16
|
-
// Re-export utility functions for testing
|
|
17
|
-
export { mapToolToPolicy, canonicalize, verifyDecisionIntegrity };
|
|
18
|
-
|
|
19
|
-
interface APortPluginConfig {
|
|
20
|
-
mode?: "local" | "api";
|
|
21
|
-
agentId?: string;
|
|
22
|
-
passportFile?: string;
|
|
23
|
-
guardrailScript?: string;
|
|
24
|
-
apiUrl?: string;
|
|
25
|
-
apiKey?: string;
|
|
26
|
-
failClosed?: boolean;
|
|
27
|
-
allowUnmappedTools?: boolean;
|
|
28
|
-
alwaysVerifyEachToolCall?: boolean;
|
|
29
|
-
mapExecToPolicy?: boolean;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export default definePluginEntry({
|
|
33
|
-
id: "openclaw-aport",
|
|
34
|
-
name: "APort Guardrails",
|
|
35
|
-
description:
|
|
36
|
-
"Deterministic pre-action authorization via APort policy enforcement. Registers before_tool_call to block disallowed tools.",
|
|
37
|
-
|
|
38
|
-
register(api) {
|
|
39
|
-
// Get plugin config
|
|
40
|
-
const config = (api.pluginConfig || {}) as APortPluginConfig;
|
|
41
|
-
const mode = config.mode || "local";
|
|
42
|
-
const agentId = config.agentId || null;
|
|
43
|
-
const passportFile = expandPath(
|
|
44
|
-
config.passportFile || "~/.openclaw/aport/passport.json"
|
|
45
|
-
);
|
|
46
|
-
const guardrailScript = expandPath(
|
|
47
|
-
config.guardrailScript || "~/.openclaw/.skills/aport-guardrail-bash.sh"
|
|
48
|
-
);
|
|
49
|
-
const apiUrl =
|
|
50
|
-
config.apiUrl || process.env.APORT_API_URL || "https://api.aport.io";
|
|
51
|
-
const apiKey = process.env.APORT_API_KEY || config.apiKey;
|
|
52
|
-
|
|
53
|
-
const failClosed = config.failClosed !== false;
|
|
54
|
-
const allowUnmappedTools = config.allowUnmappedTools !== false; // Default true for backward compatibility
|
|
55
|
-
const mapExecToPolicy = config.mapExecToPolicy !== false;
|
|
56
|
-
|
|
57
|
-
const log = (msg: string) => api.logger?.info?.(msg);
|
|
58
|
-
const warn = (msg: string) => api.logger?.warn?.(msg);
|
|
59
|
-
const err = (msg: string) => api.logger?.error?.(msg);
|
|
60
|
-
|
|
61
|
-
log(
|
|
62
|
-
`[APort] Loaded: mode=${mode}, ${
|
|
63
|
-
agentId ? `agentId=${agentId}` : `passportFile=${passportFile}`
|
|
64
|
-
}, unmapped=${
|
|
65
|
-
allowUnmappedTools ? "allow" : "block"
|
|
66
|
-
}, mapExec=${mapExecToPolicy}`
|
|
67
|
-
);
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* before_tool_call hook - Runs before EVERY tool execution
|
|
71
|
-
*/
|
|
72
|
-
api.on("before_tool_call", async (event: any, _ctx: any) => {
|
|
73
|
-
const { toolName, params } = event;
|
|
74
|
-
|
|
75
|
-
try {
|
|
76
|
-
// Map OpenClaw tool names to APort policy names
|
|
77
|
-
const policyName =
|
|
78
|
-
toolName === "exec" && !mapExecToPolicy
|
|
79
|
-
? null
|
|
80
|
-
: mapToolToPolicy(toolName);
|
|
81
|
-
|
|
82
|
-
if (!policyName) {
|
|
83
|
-
if (allowUnmappedTools) {
|
|
84
|
-
log(`[APort] ALLOW: ${toolName} - (unmapped, no policy)`);
|
|
85
|
-
return {};
|
|
86
|
-
}
|
|
87
|
-
log(
|
|
88
|
-
`[APort] BLOCKED: ${toolName} - no policy mapping (allowUnmappedTools=false)`
|
|
89
|
-
);
|
|
90
|
-
return {
|
|
91
|
-
block: true,
|
|
92
|
-
blockReason: `🛡️ APort: Tool "${toolName}" has no policy mapping. Unmapped tools are blocked (allowUnmappedTools: false). Set allowUnmappedTools: true in config to allow unmapped custom skills and ClawHub tools.`,
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
log(`[APort] Checking tool: ${toolName} → policy: ${policyName}`);
|
|
97
|
-
|
|
98
|
-
// Normalize context
|
|
99
|
-
let effectivePolicyName = policyName;
|
|
100
|
-
let effectiveToolName = toolName;
|
|
101
|
-
let context =
|
|
102
|
-
policyName === "system.command.execute.v1"
|
|
103
|
-
? normalizeExecContext(params, event)
|
|
104
|
-
: params;
|
|
105
|
-
|
|
106
|
-
// Allow exec with no command
|
|
107
|
-
if (effectivePolicyName === "system.command.execute.v1") {
|
|
108
|
-
const cmdStr =
|
|
109
|
-
typeof context.command === "string" ? context.command.trim() : "";
|
|
110
|
-
if (!cmdStr) {
|
|
111
|
-
log(`[APort] ALLOW: exec - (empty command, skip)`);
|
|
112
|
-
return {};
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Verify via API or script
|
|
117
|
-
const scriptToolName = effectivePolicyName.replace(/\.v\d+$/, "");
|
|
118
|
-
let decision: any;
|
|
119
|
-
if (mode === "api") {
|
|
120
|
-
decision = await verifyViaAPI(effectivePolicyName, context, {
|
|
121
|
-
apiUrl,
|
|
122
|
-
apiKey,
|
|
123
|
-
passportFile: agentId ? null : passportFile,
|
|
124
|
-
agentId,
|
|
125
|
-
});
|
|
126
|
-
// Audit log for API mode (local mode is logged by the bash script via OPENCLAW_AUDIT_LOG)
|
|
127
|
-
const configDir = dirname(passportFile);
|
|
128
|
-
const auditLogPath = join(configDir, "audit.log");
|
|
129
|
-
const ctxSummary =
|
|
130
|
-
typeof context.command === "string"
|
|
131
|
-
? context.command
|
|
132
|
-
: typeof context.file_path === "string"
|
|
133
|
-
? context.file_path
|
|
134
|
-
: typeof context.recipient === "string"
|
|
135
|
-
? context.recipient
|
|
136
|
-
: undefined;
|
|
137
|
-
logAuditEntry(auditLogPath, {
|
|
138
|
-
tool: effectiveToolName,
|
|
139
|
-
allow: Boolean(decision.allow),
|
|
140
|
-
policy: effectivePolicyName,
|
|
141
|
-
code: decision.reasons?.[0]?.code,
|
|
142
|
-
agentId: agentId || undefined,
|
|
143
|
-
context: ctxSummary,
|
|
144
|
-
});
|
|
145
|
-
} else {
|
|
146
|
-
decision = await verifyViaScript(scriptToolName, context, {
|
|
147
|
-
guardrailScript,
|
|
148
|
-
passportFile,
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Verify decision integrity (prevent tampering)
|
|
153
|
-
if (!verifyDecisionIntegrity(decision)) {
|
|
154
|
-
err(
|
|
155
|
-
`[APort] Decision integrity check failed for ${effectiveToolName} - content_hash mismatch`
|
|
156
|
-
);
|
|
157
|
-
return {
|
|
158
|
-
block: true,
|
|
159
|
-
blockReason:
|
|
160
|
-
"🛡️ APort: Decision integrity verification failed (content_hash mismatch). Possible tampering detected.",
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// Check decision
|
|
165
|
-
if (!decision.allow) {
|
|
166
|
-
const { reasons, primaryMessage } = formatReasons(decision);
|
|
167
|
-
const message = primaryMessage || "Policy denied";
|
|
168
|
-
log(`[APort] BLOCKED: ${effectiveToolName} - ${message}`);
|
|
169
|
-
|
|
170
|
-
const reasonLines = reasons
|
|
171
|
-
.map(
|
|
172
|
-
(r: any) => ` • ${r.code || "oap.unknown"}: ${r.message || ""}`
|
|
173
|
-
)
|
|
174
|
-
.join("\n");
|
|
175
|
-
|
|
176
|
-
const blockReason = [
|
|
177
|
-
"🛡️ APort Policy Denied",
|
|
178
|
-
"",
|
|
179
|
-
`Policy: ${effectivePolicyName}`,
|
|
180
|
-
"",
|
|
181
|
-
"Reasons (OAP codes):",
|
|
182
|
-
reasonLines || ` • ${message}`,
|
|
183
|
-
"",
|
|
184
|
-
agentId
|
|
185
|
-
? `To allow this action, update limits at aport.io (hosted passport: ${agentId})`
|
|
186
|
-
: `To allow this action, update limits in your passport: ${passportFile}`,
|
|
187
|
-
].join("\n");
|
|
188
|
-
|
|
189
|
-
return {
|
|
190
|
-
block: true,
|
|
191
|
-
blockReason,
|
|
192
|
-
reasons,
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
log(`[APort] ALLOW: ${effectiveToolName}`);
|
|
197
|
-
return {
|
|
198
|
-
reasons: decision.reasons?.length ? decision.reasons : undefined,
|
|
199
|
-
};
|
|
200
|
-
} catch (error: any) {
|
|
201
|
-
err(`[APort] Error evaluating policy: ${error.message}`);
|
|
202
|
-
|
|
203
|
-
if (failClosed) {
|
|
204
|
-
return {
|
|
205
|
-
block: true,
|
|
206
|
-
blockReason: `🛡️ APort Policy Error (fail-closed)\n\nError: ${error.message}\n\nCheck configuration at plugins.entries.openclaw-aport.config`,
|
|
207
|
-
};
|
|
208
|
-
} else {
|
|
209
|
-
warn(`[APort] Allowing tool despite error (failClosed=false)`);
|
|
210
|
-
return {};
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
log(`[APort] Registered hooks: before_tool_call`);
|
|
216
|
-
},
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
// Helper functions
|
|
220
|
-
|
|
221
|
-
function formatReasons(decision: any) {
|
|
222
|
-
const reasons = decision.reasons || [];
|
|
223
|
-
const primaryMessage = reasons[0]?.message || decision.reason || "";
|
|
224
|
-
return { reasons, primaryMessage };
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function normalizeExecContext(params: any, event: any) {
|
|
228
|
-
const src =
|
|
229
|
-
event && typeof event === "object" ? { ...event, ...params } : params || {};
|
|
230
|
-
if (typeof src !== "object") return { command: "" };
|
|
231
|
-
|
|
232
|
-
const raw =
|
|
233
|
-
src.command ??
|
|
234
|
-
src.cmd ??
|
|
235
|
-
(src.arguments &&
|
|
236
|
-
typeof src.arguments === "object" &&
|
|
237
|
-
src.arguments.command) ??
|
|
238
|
-
(src.input && typeof src.input === "object" && src.input.command) ??
|
|
239
|
-
(typeof src.input === "string" && src.input.trim().length > 0
|
|
240
|
-
? src.input
|
|
241
|
-
: null) ??
|
|
242
|
-
(src.args && typeof src.args === "object" && src.args.command) ??
|
|
243
|
-
(src.invocation &&
|
|
244
|
-
typeof src.invocation === "object" &&
|
|
245
|
-
src.invocation.command) ??
|
|
246
|
-
(src.payload && typeof src.payload === "object" && src.payload.command) ??
|
|
247
|
-
(Array.isArray(src.args) && src.args.length > 0
|
|
248
|
-
? src.args.join(" ")
|
|
249
|
-
: src.args?.[0]);
|
|
250
|
-
|
|
251
|
-
const full = typeof raw === "string" ? raw : raw != null ? String(raw) : "";
|
|
252
|
-
|
|
253
|
-
const out = { ...params, command: full, full_command: full };
|
|
254
|
-
if (params && params.workdir !== undefined && out.cwd === undefined)
|
|
255
|
-
out.cwd = params.workdir;
|
|
256
|
-
return out;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function mapToolToPolicy(toolName: string): string | null {
|
|
260
|
-
const tool = toolName.toLowerCase();
|
|
261
|
-
|
|
262
|
-
// Git/Code operations
|
|
263
|
-
if (tool.match(/git\.(create_pr|merge|push|commit)/))
|
|
264
|
-
return "code.repository.merge.v1";
|
|
265
|
-
if (tool.startsWith("git.")) return "code.repository.merge.v1";
|
|
266
|
-
|
|
267
|
-
// System commands / exec
|
|
268
|
-
if (tool === "exec") return "system.command.execute.v1";
|
|
269
|
-
if (tool.match(/exec\.(run|shell)/)) return "system.command.execute.v1";
|
|
270
|
-
if (tool.startsWith("exec.")) return "system.command.execute.v1";
|
|
271
|
-
if (tool.startsWith("system.command.")) return "system.command.execute.v1";
|
|
272
|
-
if (tool === "bash" || tool === "shell" || tool === "command")
|
|
273
|
-
return "system.command.execute.v1";
|
|
274
|
-
|
|
275
|
-
// Messaging
|
|
276
|
-
if (tool.startsWith("message.")) return "messaging.message.send.v1";
|
|
277
|
-
if (tool.startsWith("messaging.")) return "messaging.message.send.v1";
|
|
278
|
-
if (tool.match(/sms|whatsapp|slack|email/))
|
|
279
|
-
return "messaging.message.send.v1";
|
|
280
|
-
|
|
281
|
-
// File operations
|
|
282
|
-
if (tool === "read") return "data.file.read.v1";
|
|
283
|
-
if (tool.startsWith("file.read")) return "data.file.read.v1";
|
|
284
|
-
if (tool.startsWith("data.file.read")) return "data.file.read.v1";
|
|
285
|
-
if (tool === "write") return "data.file.write.v1";
|
|
286
|
-
if (tool === "edit") return "data.file.write.v1";
|
|
287
|
-
// Claude Code tool names
|
|
288
|
-
if (tool === "multiedit" || tool === "notebookedit")
|
|
289
|
-
return "data.file.write.v1";
|
|
290
|
-
if (
|
|
291
|
-
tool === "glob" ||
|
|
292
|
-
tool === "ls" ||
|
|
293
|
-
tool === "grep" ||
|
|
294
|
-
tool === "toolsearch"
|
|
295
|
-
)
|
|
296
|
-
return "data.file.read.v1";
|
|
297
|
-
if (tool === "todoread") return "data.file.read.v1";
|
|
298
|
-
if (tool === "todowrite") return "data.file.write.v1";
|
|
299
|
-
if (
|
|
300
|
-
tool === "task" ||
|
|
301
|
-
tool === "taskcreate" ||
|
|
302
|
-
tool === "taskupdate" ||
|
|
303
|
-
tool === "taskstop"
|
|
304
|
-
)
|
|
305
|
-
return "agent.session.create.v1";
|
|
306
|
-
if (tool === "taskget" || tool === "tasklist" || tool === "taskoutput")
|
|
307
|
-
return "data.file.read.v1";
|
|
308
|
-
if (tool === "agent" || tool === "skill" || tool === "enterworktree")
|
|
309
|
-
return "agent.session.create.v1";
|
|
310
|
-
if (
|
|
311
|
-
tool === "askuserquestion" ||
|
|
312
|
-
tool === "enterplanmode" ||
|
|
313
|
-
tool === "exitplanmode"
|
|
314
|
-
)
|
|
315
|
-
return null; // allow
|
|
316
|
-
if (tool === "croncreate" || tool === "crondelete")
|
|
317
|
-
return "agent.session.create.v1";
|
|
318
|
-
if (tool === "cronlist") return "data.file.read.v1";
|
|
319
|
-
if (tool.startsWith("file.write")) return "data.file.write.v1";
|
|
320
|
-
if (tool.startsWith("file.edit")) return "data.file.write.v1";
|
|
321
|
-
if (tool.startsWith("data.file.write")) return "data.file.write.v1";
|
|
322
|
-
|
|
323
|
-
// Web operations
|
|
324
|
-
if (tool === "web_fetch" || tool === "webfetch") return "web.fetch.v1";
|
|
325
|
-
if (tool === "web_search" || tool === "websearch") return "web.fetch.v1";
|
|
326
|
-
if (tool.startsWith("web.fetch")) return "web.fetch.v1";
|
|
327
|
-
if (tool.startsWith("web.search")) return "web.fetch.v1";
|
|
328
|
-
if (tool === "browser") return "web.browser.v1";
|
|
329
|
-
if (tool.startsWith("web.browser")) return "web.browser.v1";
|
|
330
|
-
if (tool.startsWith("browser.")) return "web.browser.v1";
|
|
331
|
-
|
|
332
|
-
// MCP tools
|
|
333
|
-
if (tool.startsWith("mcp.")) return "mcp.tool.execute.v1";
|
|
334
|
-
// Claude Code MCP tools use mcp__ prefix (double underscore, not mcp.)
|
|
335
|
-
if (tool.startsWith("mcp__")) return "mcp.tool.execute.v1";
|
|
336
|
-
|
|
337
|
-
// Agent sessions and spawning
|
|
338
|
-
if (tool.match(/agent\.session|session\.create/))
|
|
339
|
-
return "agent.session.create.v1";
|
|
340
|
-
if (tool === "sessions_spawn" || tool === "sessions_send")
|
|
341
|
-
return "agent.session.create.v1";
|
|
342
|
-
if (tool.startsWith("session.") || tool.startsWith("sessions."))
|
|
343
|
-
return "agent.session.create.v1";
|
|
344
|
-
|
|
345
|
-
// Scheduled tasks (cron)
|
|
346
|
-
if (tool === "cron" || tool.startsWith("cron."))
|
|
347
|
-
return "agent.session.create.v1";
|
|
348
|
-
|
|
349
|
-
// Gateway operations (high risk - treat as command execution)
|
|
350
|
-
if (tool === "gateway" || tool.startsWith("gateway."))
|
|
351
|
-
return "system.command.execute.v1";
|
|
352
|
-
|
|
353
|
-
// Process operations
|
|
354
|
-
if (tool === "process" || tool.startsWith("process."))
|
|
355
|
-
return "system.command.execute.v1";
|
|
356
|
-
|
|
357
|
-
// Tool registration
|
|
358
|
-
if (tool.match(/agent\.tool|tool\.register/)) return "agent.tool.register.v1";
|
|
359
|
-
|
|
360
|
-
// Financial operations
|
|
361
|
-
if (tool.match(/payment\.refund|refund/)) return "finance.payment.refund.v1";
|
|
362
|
-
if (tool.match(/payment\.charge|charge/)) return "finance.payment.charge.v1";
|
|
363
|
-
if (tool.startsWith("finance.")) return "finance.payment.refund.v1";
|
|
364
|
-
|
|
365
|
-
// Data operations
|
|
366
|
-
if (tool.match(/database\.(write|insert|update|delete)/))
|
|
367
|
-
return "data.export.create.v1";
|
|
368
|
-
if (tool.match(/data\.export|export/)) return "data.export.create.v1";
|
|
369
|
-
|
|
370
|
-
return null;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
function canonicalize(obj: any): string {
|
|
374
|
-
if (obj === null || typeof obj !== "object") return JSON.stringify(obj);
|
|
375
|
-
if (Array.isArray(obj)) return "[" + obj.map(canonicalize).join(",") + "]";
|
|
376
|
-
const keys = Object.keys(obj).sort();
|
|
377
|
-
const parts = keys.map((k) => JSON.stringify(k) + ":" + canonicalize(obj[k]));
|
|
378
|
-
return "{" + parts.join(",") + "}";
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
function verifyDecisionIntegrity(decision: any): boolean {
|
|
382
|
-
if (!decision || !decision.content_hash) return true;
|
|
383
|
-
const { content_hash, ...rest } = decision;
|
|
384
|
-
const canonical = canonicalize(rest);
|
|
385
|
-
const computed =
|
|
386
|
-
"sha256:" + createHash("sha256").update(canonical, "utf8").digest("hex");
|
|
387
|
-
return computed === content_hash;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
async function verifyViaScript(
|
|
391
|
-
toolName: string,
|
|
392
|
-
params: any,
|
|
393
|
-
{ guardrailScript, passportFile }: any
|
|
394
|
-
): Promise<any> {
|
|
395
|
-
const contextJson = JSON.stringify(params);
|
|
396
|
-
const configDir = dirname(passportFile);
|
|
397
|
-
const decisionsDir = join(configDir, "decisions");
|
|
398
|
-
await mkdir(decisionsDir, { recursive: true });
|
|
399
|
-
const decisionFile = join(decisionsDir, `${randomUUID()}.json`);
|
|
400
|
-
|
|
401
|
-
return new Promise((resolve, reject) => {
|
|
402
|
-
const proc = spawn(guardrailScript, [toolName, contextJson], {
|
|
403
|
-
env: {
|
|
404
|
-
...process.env,
|
|
405
|
-
OPENCLAW_PASSPORT_FILE: passportFile,
|
|
406
|
-
OPENCLAW_DECISION_FILE: decisionFile,
|
|
407
|
-
OPENCLAW_AUDIT_LOG: join(configDir, "audit.log"),
|
|
408
|
-
},
|
|
409
|
-
});
|
|
410
|
-
|
|
411
|
-
let stdout = "";
|
|
412
|
-
let stderr = "";
|
|
413
|
-
|
|
414
|
-
proc.stdout.on("data", (data) => (stdout += data));
|
|
415
|
-
proc.stderr.on("data", (data) => (stderr += data));
|
|
416
|
-
|
|
417
|
-
proc.on("close", async (code) => {
|
|
418
|
-
try {
|
|
419
|
-
const decisionData = await readFile(decisionFile, "utf8");
|
|
420
|
-
const decision = JSON.parse(decisionData);
|
|
421
|
-
resolve(decision);
|
|
422
|
-
} catch (err) {
|
|
423
|
-
if (code === 0) {
|
|
424
|
-
resolve({ allow: true });
|
|
425
|
-
} else {
|
|
426
|
-
resolve({
|
|
427
|
-
allow: false,
|
|
428
|
-
reasons: [
|
|
429
|
-
{ message: stderr || `Tool ${toolName} denied (exit ${code})` },
|
|
430
|
-
],
|
|
431
|
-
});
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
});
|
|
435
|
-
|
|
436
|
-
proc.on("error", (error) => {
|
|
437
|
-
reject(new Error(`Failed to run guardrail script: ${error.message}`));
|
|
438
|
-
});
|
|
439
|
-
});
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
function ensureIdempotencyKey(context: any) {
|
|
443
|
-
if (context && context.idempotency_key) return context;
|
|
444
|
-
const ts = Date.now().toString(36);
|
|
445
|
-
const r = Math.random().toString(36).slice(2, 10);
|
|
446
|
-
const key = `idem_${ts}_${r}`.slice(0, 64);
|
|
447
|
-
return { ...context, idempotency_key: key };
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
async function verifyViaAPI(
|
|
451
|
-
policyName: string,
|
|
452
|
-
params: any,
|
|
453
|
-
{ apiUrl, apiKey, passportFile, agentId }: any
|
|
454
|
-
): Promise<any> {
|
|
455
|
-
try {
|
|
456
|
-
const context = ensureIdempotencyKey(params);
|
|
457
|
-
|
|
458
|
-
const url = `${apiUrl}/api/verify/policy/${policyName}`;
|
|
459
|
-
const headers: any = {
|
|
460
|
-
"Content-Type": "application/json",
|
|
461
|
-
};
|
|
462
|
-
if (apiKey) {
|
|
463
|
-
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
let body;
|
|
467
|
-
if (agentId) {
|
|
468
|
-
body = JSON.stringify({
|
|
469
|
-
context: { agent_id: agentId, ...context },
|
|
470
|
-
});
|
|
471
|
-
} else {
|
|
472
|
-
const passportData = await readFile(passportFile, "utf8");
|
|
473
|
-
const passport = JSON.parse(passportData);
|
|
474
|
-
body = JSON.stringify({
|
|
475
|
-
passport,
|
|
476
|
-
context,
|
|
477
|
-
});
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
const response = await fetch(url, {
|
|
481
|
-
method: "POST",
|
|
482
|
-
headers,
|
|
483
|
-
body,
|
|
484
|
-
});
|
|
485
|
-
|
|
486
|
-
if (!response.ok) {
|
|
487
|
-
throw new Error(
|
|
488
|
-
`API request failed: ${response.status} ${response.statusText}`
|
|
489
|
-
);
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
const data = (await response.json()) as { decision?: any };
|
|
493
|
-
return data.decision || data;
|
|
494
|
-
} catch (error: any) {
|
|
495
|
-
throw new Error(`API verification failed: ${error.message}`);
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
function expandPath(path: string): string {
|
|
500
|
-
if (path.startsWith("~/")) {
|
|
501
|
-
return join(homedir(), path.slice(2));
|
|
502
|
-
}
|
|
503
|
-
return path;
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
/**
|
|
507
|
-
* Write one-line audit entry matching bash guardrail format.
|
|
508
|
-
* Deny: sync (blocking). Allow: async (non-blocking). Best-effort: never throws.
|
|
509
|
-
*/
|
|
510
|
-
function logAuditEntry(
|
|
511
|
-
auditLogPath: string,
|
|
512
|
-
entry: {
|
|
513
|
-
tool: string;
|
|
514
|
-
allow: boolean;
|
|
515
|
-
policy: string;
|
|
516
|
-
code?: string;
|
|
517
|
-
agentId?: string;
|
|
518
|
-
context?: string;
|
|
519
|
-
}
|
|
520
|
-
): void {
|
|
521
|
-
try {
|
|
522
|
-
const ts = new Date()
|
|
523
|
-
.toISOString()
|
|
524
|
-
.replace("T", " ")
|
|
525
|
-
.replace(/\.\d+Z$/, "");
|
|
526
|
-
const code = entry.code || (entry.allow ? "oap.allowed" : "oap.denied");
|
|
527
|
-
let line = `[${ts}] tool=${entry.tool} allow=${entry.allow} policy=${entry.policy} code=${code}`;
|
|
528
|
-
if (entry.agentId) line += ` agent_id=${entry.agentId}`;
|
|
529
|
-
if (entry.context) {
|
|
530
|
-
const sanitized = entry.context
|
|
531
|
-
.replace(/[\r\n]+/g, " ")
|
|
532
|
-
.replace(/"/g, '\\"')
|
|
533
|
-
.slice(0, 120);
|
|
534
|
-
line += ` context="${sanitized}"`;
|
|
535
|
-
}
|
|
536
|
-
line += "\n";
|
|
537
|
-
const dir = dirname(auditLogPath);
|
|
538
|
-
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
539
|
-
if (!entry.allow) {
|
|
540
|
-
appendFileSync(auditLogPath, line, "utf8");
|
|
541
|
-
} else {
|
|
542
|
-
appendFile(auditLogPath, line, "utf8").catch(() => {});
|
|
543
|
-
}
|
|
544
|
-
} catch {
|
|
545
|
-
/* best-effort */
|
|
546
|
-
}
|
|
547
|
-
}
|