limits-openclaw 0.0.6

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.
@@ -0,0 +1,299 @@
1
+ /**
2
+ * limits-openclaw: register(api) entrypoint — wires before_tool_call and after_tool_call.
3
+ */
4
+ import { loadConfig } from "./config.js";
5
+ import { callEnforce } from "./enforcer.js";
6
+ import { log } from "./logger.js";
7
+ import { extractToken } from "./token.js";
8
+ import { runConfigureWizard } from "./configure-wizard.js";
9
+ let firstPreFired = false;
10
+ function getEventAndCtx(args) {
11
+ if (args.length >= 2) {
12
+ return { event: args[0], ctx: args[1] };
13
+ }
14
+ const payload = args[0];
15
+ if (payload && typeof payload === "object" && "event" in payload) {
16
+ const p = payload;
17
+ return { event: p.event, ctx: p.ctx ?? p.context };
18
+ }
19
+ return { event: payload, ctx: undefined };
20
+ }
21
+ function getToolFromEvent(event) {
22
+ if (!event || typeof event !== "object")
23
+ return {};
24
+ const e = event;
25
+ return {
26
+ name: typeof e.toolName === "string" ? e.toolName : undefined,
27
+ args: e.toolParams ?? e.args ?? e.arguments,
28
+ toolCallId: typeof e.toolCallId === "string"
29
+ ? e.toolCallId
30
+ : typeof e.id === "string"
31
+ ? e.id
32
+ : undefined,
33
+ };
34
+ }
35
+ function buildContext(event, ctx) {
36
+ const e = event && typeof event === "object" ? event : {};
37
+ const c = ctx && typeof ctx === "object" ? ctx : {};
38
+ const requestId = (e.requestId ?? c.requestId);
39
+ const runId = (e.runId ?? c.runId);
40
+ const sessionKey = (e.sessionKey ?? c.sessionKey);
41
+ const agentId = (e.agentId ?? c.agentId);
42
+ const channel = (e.channel ?? c.channel);
43
+ const userMessageSummary = (e.userMessageSummary ?? c.userMessageSummary);
44
+ if (requestId === undefined &&
45
+ runId === undefined &&
46
+ sessionKey === undefined &&
47
+ agentId === undefined &&
48
+ channel === undefined &&
49
+ userMessageSummary === undefined) {
50
+ return undefined;
51
+ }
52
+ return {
53
+ ...(typeof requestId === "string" && { requestId }),
54
+ ...(typeof runId === "string" && { runId }),
55
+ ...(typeof sessionKey === "string" && { sessionKey }),
56
+ ...(typeof agentId === "string" && { agentId }),
57
+ ...(typeof channel === "string" && { channel }),
58
+ ...(typeof userMessageSummary === "string" && { userMessageSummary }),
59
+ };
60
+ }
61
+ function getResultFromPayload(args) {
62
+ const payload = args[0];
63
+ if (payload && typeof payload === "object" && "result" in payload) {
64
+ return payload.result;
65
+ }
66
+ if (args.length >= 2)
67
+ return args[1];
68
+ return undefined;
69
+ }
70
+ function textResult(text) {
71
+ return { content: [{ type: "text", text }] };
72
+ }
73
+ const POLICY_MODES = ["INSTRUCTIONS", "CONDITIONS", "GUARDRAIL"];
74
+ function registerPolicyTools(api) {
75
+ const config = loadConfig(api);
76
+ const baseUrl = config.baseUrl.replace(/\/$/, "");
77
+ const apiKey = config.apiToken;
78
+ if (!baseUrl || !apiKey || typeof api.registerTool !== "function") {
79
+ return;
80
+ }
81
+ api.registerTool({
82
+ name: "limits_generate_create_policy",
83
+ description: "Generate a new policy from natural language and create it on the Limits backend. Use when the user wants to add a new policy (e.g. block payment tools, add a guardrail).",
84
+ parameters: {
85
+ type: "object",
86
+ properties: {
87
+ input: {
88
+ type: "string",
89
+ description: "Natural-language description of the policy (what to block, allow, or require).",
90
+ },
91
+ mode: {
92
+ type: "string",
93
+ enum: POLICY_MODES,
94
+ description: "INSTRUCTIONS | CONDITIONS | GUARDRAIL. Default INSTRUCTIONS. Use GUARDRAIL for rules that scan tool output.",
95
+ },
96
+ tools: {
97
+ type: "array",
98
+ items: { type: "string" },
99
+ description: 'Required. Specifies which tool calls this policy applies to. Use ["*"] to apply to all tools / all requests. Use exact tool names (e.g. "read_file") or dot-star prefix patterns (e.g. "stripe_.*") for specific tools. The backend also accepts "stripe_*" and normalizes it to "stripe_.*". Examples: ["*"], ["read_file"], ["stripe_.*", "payment_.*"].',
100
+ },
101
+ },
102
+ required: ["input", "tools"],
103
+ },
104
+ async execute(_id, params) {
105
+ const input = String(params?.input ?? "").trim();
106
+ if (!input)
107
+ return textResult("Error: input is required.");
108
+ const toolsRaw = params?.tools;
109
+ if (!Array.isArray(toolsRaw) ||
110
+ toolsRaw.length === 0 ||
111
+ !toolsRaw.every((t) => typeof t === "string" && t.trim() !== "")) {
112
+ return textResult('Error: tools is required and must be a non-empty array of strings. Use ["*"] for all tools / all requests, or specific tool names / prefix patterns (e.g. ["stripe_.*"]).');
113
+ }
114
+ const tools = toolsRaw;
115
+ const mode = params?.mode && POLICY_MODES.includes(params.mode) ? params.mode : "INSTRUCTIONS";
116
+ try {
117
+ const res = await fetch(`${baseUrl}/api/policies/generatecreate`, {
118
+ method: "POST",
119
+ headers: {
120
+ Authorization: `Bearer ${apiKey}`,
121
+ "Content-Type": "application/json",
122
+ },
123
+ body: JSON.stringify({ input, mode, tools }),
124
+ });
125
+ const body = await res.text();
126
+ if (!res.ok)
127
+ return textResult(`Limits API error (${res.status}): ${body || res.statusText}`);
128
+ return textResult(body || "Policy created.");
129
+ }
130
+ catch (err) {
131
+ return textResult(`Request failed: ${String(err)}`);
132
+ }
133
+ }
134
+ }, { optional: true });
135
+ api.registerTool({
136
+ name: "limits_generate_update_policy",
137
+ description: "Generate updates from natural language and apply them to an existing policy on the Limits backend. Use when the user wants to change an existing policy.",
138
+ parameters: {
139
+ type: "object",
140
+ properties: {
141
+ policyId: {
142
+ type: "string",
143
+ description: "The ID (UUID) of the existing policy to update.",
144
+ },
145
+ input: {
146
+ type: "string",
147
+ description: "Natural-language description of the changes or additions.",
148
+ },
149
+ mode: {
150
+ type: "string",
151
+ enum: POLICY_MODES,
152
+ description: "INSTRUCTIONS | CONDITIONS | GUARDRAIL.",
153
+ },
154
+ },
155
+ required: ["policyId", "input"],
156
+ },
157
+ async execute(_id, params) {
158
+ const policyId = String(params?.policyId ?? "").trim();
159
+ const input = String(params?.input ?? "").trim();
160
+ if (!policyId || !input)
161
+ return textResult("Error: policyId and input are required.");
162
+ const mode = params?.mode && POLICY_MODES.includes(params.mode) ? params.mode : "INSTRUCTIONS";
163
+ try {
164
+ const res = await fetch(`${baseUrl}/api/policies/${encodeURIComponent(policyId)}/generateupdate`, {
165
+ method: "POST",
166
+ headers: {
167
+ Authorization: `Bearer ${apiKey}`,
168
+ "Content-Type": "application/json",
169
+ },
170
+ body: JSON.stringify({ input, mode }),
171
+ });
172
+ const body = await res.text();
173
+ if (!res.ok)
174
+ return textResult(`Limits API error (${res.status}): ${body || res.statusText}`);
175
+ return textResult(body || "Policy updated.");
176
+ }
177
+ catch (err) {
178
+ return textResult(`Request failed: ${String(err)}`);
179
+ }
180
+ }
181
+ }, { optional: true });
182
+ log("policy-generator tools registered (limits_generate_create_policy, limits_generate_update_policy)");
183
+ }
184
+ export function register(api) {
185
+ log("limits-openclaw loaded");
186
+ if (typeof api.on !== "function") {
187
+ log("api.on not available, hooks not registered");
188
+ return;
189
+ }
190
+ api.on("before_tool_call", async (...args) => {
191
+ if (!firstPreFired) {
192
+ firstPreFired = true;
193
+ log("before_tool_call observed");
194
+ }
195
+ const config = loadConfig(api);
196
+ if (!config.baseUrl) {
197
+ return config.failMode === "block"
198
+ ? { block: true, reason: "enforcement unavailable" }
199
+ : null;
200
+ }
201
+ const { event, ctx } = getEventAndCtx(args);
202
+ const apiToken = extractToken(config.tokenSource, event, ctx) ?? config.apiToken;
203
+ const tool = getToolFromEvent(event);
204
+ const context = buildContext(event, ctx);
205
+ const body = {
206
+ phase: "pre",
207
+ apiToken,
208
+ tool: { name: tool.name, args: tool.args, toolCallId: tool.toolCallId },
209
+ ...(context && Object.keys(context).length > 0 && { context }),
210
+ };
211
+ let response;
212
+ try {
213
+ response = await callEnforce(config, body);
214
+ }
215
+ catch {
216
+ response = null;
217
+ }
218
+ if (!response) {
219
+ if (config.failMode === "block") {
220
+ return { block: true, reason: "enforcement unavailable" };
221
+ }
222
+ return null;
223
+ }
224
+ if (response.action === "ALLOW")
225
+ return null;
226
+ if (response.action === "BLOCK") {
227
+ return {
228
+ block: true,
229
+ reason: typeof response.reason === "string"
230
+ ? response.reason
231
+ : "Blocked by policy",
232
+ };
233
+ }
234
+ if (response.action === "REWRITE" && "rewriteArgs" in response && response.rewriteArgs !== undefined) {
235
+ return { args: response.rewriteArgs };
236
+ }
237
+ return null;
238
+ });
239
+ api.on("after_tool_call", async (...args) => {
240
+ const config = loadConfig(api);
241
+ if (!config.baseUrl) {
242
+ if (config.failMode === "block") {
243
+ return { result: "[BLOCKED by policy — content withheld]" };
244
+ }
245
+ return null;
246
+ }
247
+ const { event, ctx } = getEventAndCtx(args);
248
+ const apiToken = extractToken(config.tokenSource, event, ctx) ?? config.apiToken;
249
+ const tool = getToolFromEvent(event);
250
+ const result = getResultFromPayload(args);
251
+ const context = buildContext(event, ctx);
252
+ const body = {
253
+ phase: "post",
254
+ apiToken,
255
+ tool: {
256
+ name: tool.name,
257
+ args: tool.args,
258
+ toolCallId: tool.toolCallId,
259
+ result,
260
+ },
261
+ ...(context && Object.keys(context).length > 0 && { context }),
262
+ };
263
+ let response;
264
+ try {
265
+ response = await callEnforce(config, body);
266
+ }
267
+ catch {
268
+ response = null;
269
+ }
270
+ if (!response) {
271
+ if (config.failMode === "block") {
272
+ return { result: "[BLOCKED by policy — content withheld]" };
273
+ }
274
+ return null;
275
+ }
276
+ if (response.action === "ALLOW")
277
+ return null;
278
+ if (response.action === "BLOCK") {
279
+ return { result: "[BLOCKED by policy — content withheld]" };
280
+ }
281
+ if (response.action === "REDACT" && response.redactedResult !== undefined) {
282
+ return { result: response.redactedResult };
283
+ }
284
+ if (response.action === "REWRITE" && response.rewrittenResult !== undefined) {
285
+ return { result: response.rewrittenResult };
286
+ }
287
+ return null;
288
+ });
289
+ registerPolicyTools(api);
290
+ if (typeof api.registerCli === "function") {
291
+ api.registerCli((opts) => {
292
+ const program = opts.program;
293
+ const root = program.command("limits").description("Limits OpenClaw plugin");
294
+ root.command("configure").description("Configure API token and base URL (wizard)").action(async () => {
295
+ await runConfigureWizard();
296
+ });
297
+ }, { commands: ["limits"] });
298
+ }
299
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Safe logger for limits-openclaw. Never logs apiToken, request body, or tool args.
3
+ */
4
+ const PREFIX = "[limits-openclaw]";
5
+ export function log(message, ...safeArgs) {
6
+ const safe = safeArgs.map((a) => typeof a === "string" || typeof a === "number" || typeof a === "boolean"
7
+ ? a
8
+ : "[object]");
9
+ console.log(PREFIX, message, ...safe);
10
+ }
11
+ export function warn(message, ...safeArgs) {
12
+ const safe = safeArgs.map((a) => typeof a === "string" || typeof a === "number" || typeof a === "boolean"
13
+ ? a
14
+ : "[object]");
15
+ console.warn(PREFIX, message, ...safe);
16
+ }
17
+ export function error(message, ...safeArgs) {
18
+ const safe = safeArgs.map((a) => typeof a === "string" || typeof a === "number" || typeof a === "boolean"
19
+ ? a
20
+ : "[object]");
21
+ console.error(PREFIX, message, ...safe);
22
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Extract API token from a dot-separated path (event, ctx, or env).
3
+ * Never throws; returns undefined if path is missing or invalid.
4
+ */
5
+ export function extractToken(tokenSource, event, ctx) {
6
+ if (typeof tokenSource !== "string" || !tokenSource.trim()) {
7
+ return undefined;
8
+ }
9
+ const path = tokenSource.trim().split(".");
10
+ if (path.length < 2) {
11
+ return undefined;
12
+ }
13
+ const [root, ...rest] = path;
14
+ let obj;
15
+ if (root === "event") {
16
+ obj = event;
17
+ }
18
+ else if (root === "ctx") {
19
+ obj = ctx;
20
+ }
21
+ else if (root === "env") {
22
+ const key = rest.join(".");
23
+ const val = process.env[key];
24
+ return typeof val === "string" ? val : undefined;
25
+ }
26
+ else {
27
+ return undefined;
28
+ }
29
+ for (const key of rest) {
30
+ if (obj === null || obj === undefined || typeof obj !== "object") {
31
+ return undefined;
32
+ }
33
+ obj = obj[key];
34
+ }
35
+ return typeof obj === "string" ? obj : undefined;
36
+ }
@@ -0,0 +1,143 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from "vitest";
2
+ import { createServer } from "http";
3
+ /**
4
+ * Integration test: real HTTP server, real enforcer client, full pre+post flow.
5
+ * SaaS returns BLOCK for stripe.* tools, ALLOW for others.
6
+ */
7
+ describe("mock-saas integration", () => {
8
+ let server;
9
+ let baseUrl;
10
+ let beforeHandler = null;
11
+ let afterHandler = null;
12
+ beforeAll(async () => {
13
+ await new Promise((resolve) => {
14
+ server = createServer((req, res) => {
15
+ if (req.method !== "POST" || req.url !== "/openclaw/enforce") {
16
+ res.writeHead(404);
17
+ res.end();
18
+ return;
19
+ }
20
+ let body = "";
21
+ req.on("data", (ch) => (body += ch));
22
+ req.on("end", () => {
23
+ try {
24
+ const data = JSON.parse(body);
25
+ const name = data.tool?.name ?? "";
26
+ const isStripe = /^stripe\./.test(name) || name.startsWith("stripe_");
27
+ if (data.phase === "pre" || data.phase === "post") {
28
+ if (isStripe) {
29
+ res.writeHead(200, { "Content-Type": "application/json" });
30
+ res.end(JSON.stringify({
31
+ action: "BLOCK",
32
+ reason: "Stripe tools are disabled in this environment",
33
+ }));
34
+ }
35
+ else {
36
+ res.writeHead(200, { "Content-Type": "application/json" });
37
+ res.end(JSON.stringify({ action: "ALLOW" }));
38
+ }
39
+ }
40
+ else {
41
+ res.writeHead(400);
42
+ res.end();
43
+ }
44
+ }
45
+ catch {
46
+ res.writeHead(400);
47
+ res.end();
48
+ }
49
+ });
50
+ });
51
+ server.listen(0, "127.0.0.1", () => {
52
+ const addr = server.address();
53
+ const port = typeof addr === "object" && addr && "port" in addr ? addr.port : 0;
54
+ baseUrl = `http://127.0.0.1:${port}`;
55
+ resolve();
56
+ });
57
+ });
58
+ });
59
+ afterAll(() => {
60
+ return new Promise((resolve) => {
61
+ server.close(() => resolve());
62
+ });
63
+ });
64
+ it("registers and runs pre-hook: stripe tool blocked, other allowed", async () => {
65
+ const api = {
66
+ config: {
67
+ plugins: {
68
+ entries: {
69
+ "limits-openclaw": {
70
+ enabled: true,
71
+ config: {
72
+ baseUrl: baseUrl,
73
+ failMode: "allow",
74
+ tokenSource: "event.metadata.apiToken",
75
+ },
76
+ },
77
+ },
78
+ },
79
+ },
80
+ on(event, handler) {
81
+ if (event === "before_tool_call")
82
+ beforeHandler = handler;
83
+ if (event === "after_tool_call")
84
+ afterHandler = handler;
85
+ },
86
+ };
87
+ const { register } = await import("../../src/index.js");
88
+ register(api);
89
+ const eventStripe = {
90
+ toolName: "stripe_charge",
91
+ toolParams: { amount: 100 },
92
+ metadata: { apiToken: "sk_test" },
93
+ };
94
+ const resultBlocked = await beforeHandler(eventStripe, undefined);
95
+ expect(resultBlocked).toEqual({
96
+ block: true,
97
+ reason: "Stripe tools are disabled in this environment",
98
+ });
99
+ const eventSafe = {
100
+ toolName: "read_file",
101
+ toolParams: { path: "/tmp/foo" },
102
+ metadata: { apiToken: "sk_test" },
103
+ };
104
+ const resultAllowed = await beforeHandler(eventSafe, undefined);
105
+ expect(resultAllowed).toBe(null);
106
+ });
107
+ it("post-hook: stripe result blocked, other allowed", async () => {
108
+ const api = {
109
+ config: {
110
+ plugins: {
111
+ entries: {
112
+ "limits-openclaw": {
113
+ enabled: true,
114
+ config: {
115
+ baseUrl: baseUrl,
116
+ failMode: "allow",
117
+ tokenSource: "event.metadata.apiToken",
118
+ },
119
+ },
120
+ },
121
+ },
122
+ },
123
+ on(event, handler) {
124
+ if (event === "after_tool_call")
125
+ afterHandler = handler;
126
+ },
127
+ };
128
+ const { register } = await import("../../src/index.js");
129
+ register(api);
130
+ const stripeResult = await afterHandler({
131
+ toolName: "stripe_charge",
132
+ metadata: { apiToken: "sk_test" },
133
+ }, { chargeId: "ch_secret" }, undefined);
134
+ expect(stripeResult).toEqual({
135
+ result: "[BLOCKED by policy — content withheld]",
136
+ });
137
+ const safeResult = await afterHandler({
138
+ toolName: "read_file",
139
+ metadata: { apiToken: "sk_test" },
140
+ }, { content: "hello" }, undefined);
141
+ expect(safeResult).toBe(null);
142
+ });
143
+ });
@@ -0,0 +1,101 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ const callEnforceMock = vi.fn();
3
+ vi.mock("../../src/enforcer.js", () => ({ callEnforce: callEnforceMock }));
4
+ describe("failmode", () => {
5
+ let beforeHandler = null;
6
+ let afterHandler = null;
7
+ function createApi(failMode) {
8
+ return {
9
+ config: {
10
+ plugins: {
11
+ entries: {
12
+ "limits-openclaw": {
13
+ enabled: true,
14
+ config: {
15
+ baseUrl: "https://api.test.com",
16
+ failMode: failMode,
17
+ tokenSource: "event.metadata.apiToken",
18
+ },
19
+ },
20
+ },
21
+ },
22
+ },
23
+ on(event, handler) {
24
+ if (event === "before_tool_call")
25
+ beforeHandler = handler;
26
+ if (event === "after_tool_call")
27
+ afterHandler = handler;
28
+ },
29
+ };
30
+ }
31
+ beforeEach(async () => {
32
+ callEnforceMock.mockReset();
33
+ beforeHandler = null;
34
+ afterHandler = null;
35
+ });
36
+ it("SaaS unreachable with failMode allow returns null (pre)", async () => {
37
+ callEnforceMock.mockResolvedValue(null);
38
+ const { register } = await import("../../src/index.js");
39
+ register(createApi("allow"));
40
+ const event = { toolName: "bash", metadata: { apiToken: "sk_ok" } };
41
+ const result = await beforeHandler(event, undefined);
42
+ expect(result).toBe(null);
43
+ });
44
+ it("SaaS unreachable with failMode block returns block (pre)", async () => {
45
+ callEnforceMock.mockResolvedValue(null);
46
+ const { register } = await import("../../src/index.js");
47
+ register(createApi("block"));
48
+ const event = { toolName: "bash", metadata: { apiToken: "sk_ok" } };
49
+ const result = await beforeHandler(event, undefined);
50
+ expect(result).toEqual({
51
+ block: true,
52
+ reason: "enforcement unavailable",
53
+ });
54
+ });
55
+ it("no baseUrl with failMode block returns block (pre)", async () => {
56
+ const api = {
57
+ config: {
58
+ plugins: {
59
+ entries: {
60
+ "limits-openclaw": {
61
+ enabled: true,
62
+ config: {
63
+ baseUrl: "",
64
+ failMode: "block",
65
+ tokenSource: "event.metadata.apiToken",
66
+ },
67
+ },
68
+ },
69
+ },
70
+ },
71
+ on(event, handler) {
72
+ if (event === "before_tool_call")
73
+ beforeHandler = handler;
74
+ },
75
+ };
76
+ const { register } = await import("../../src/index.js");
77
+ register(api);
78
+ const result = await beforeHandler({ toolName: "x" }, undefined);
79
+ expect(result).toEqual({
80
+ block: true,
81
+ reason: "enforcement unavailable",
82
+ });
83
+ });
84
+ it("SaaS unreachable with failMode block (post) returns blocked result", async () => {
85
+ callEnforceMock.mockResolvedValue(null);
86
+ const { register } = await import("../../src/index.js");
87
+ register(createApi("block"));
88
+ const result = await afterHandler({ toolName: "get", metadata: { apiToken: "sk_ok" } }, { data: "secret" }, undefined);
89
+ expect(result).toEqual({
90
+ result: "[BLOCKED by policy — content withheld]",
91
+ });
92
+ });
93
+ it("timeout / network error returns null when failMode allow", async () => {
94
+ callEnforceMock.mockRejectedValue(new Error("network error"));
95
+ const { register } = await import("../../src/index.js");
96
+ register(createApi("allow"));
97
+ const event = { toolName: "ping", metadata: { apiToken: "sk_ok" } };
98
+ const result = await beforeHandler(event, undefined);
99
+ expect(result).toBe(null);
100
+ });
101
+ });
@@ -0,0 +1,79 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ const callEnforceMock = vi.fn();
3
+ vi.mock("../../src/enforcer.js", () => ({ callEnforce: callEnforceMock }));
4
+ describe("postHook", () => {
5
+ let afterHandler = null;
6
+ beforeEach(async () => {
7
+ callEnforceMock.mockReset();
8
+ afterHandler = null;
9
+ const api = {
10
+ config: {
11
+ plugins: {
12
+ entries: {
13
+ "limits-openclaw": {
14
+ enabled: true,
15
+ config: {
16
+ baseUrl: "https://api.test.com",
17
+ failMode: "allow",
18
+ tokenSource: "event.metadata.apiToken",
19
+ },
20
+ },
21
+ },
22
+ },
23
+ },
24
+ on(event, handler) {
25
+ if (event === "after_tool_call")
26
+ afterHandler = handler;
27
+ },
28
+ };
29
+ const { register } = await import("../../src/index.js");
30
+ register(api);
31
+ });
32
+ async function runPost(event, result, ctx) {
33
+ if (!afterHandler)
34
+ throw new Error("after_tool_call not registered");
35
+ if (result !== undefined && ctx !== undefined)
36
+ return afterHandler(event, result, ctx);
37
+ if (result !== undefined)
38
+ return afterHandler(event, result);
39
+ return afterHandler({ ...event, result: undefined }, undefined);
40
+ }
41
+ it("ALLOW returns null", async () => {
42
+ callEnforceMock.mockResolvedValue({ action: "ALLOW" });
43
+ const event = {
44
+ toolName: "read_file",
45
+ metadata: { apiToken: "sk_ok" },
46
+ };
47
+ const result = await runPost(event, { content: "data" }, undefined);
48
+ expect(result).toBe(null);
49
+ });
50
+ it("BLOCK returns blocked result message", async () => {
51
+ callEnforceMock.mockResolvedValue({
52
+ action: "BLOCK",
53
+ reason: "Sensitive output",
54
+ });
55
+ const event = { toolName: "query_db", metadata: { apiToken: "sk_ok" } };
56
+ const result = await runPost(event, { rows: [] }, undefined);
57
+ expect(result).toEqual({
58
+ result: "[BLOCKED by policy — content withheld]",
59
+ });
60
+ });
61
+ it("REDACT returns { result: redactedResult }", async () => {
62
+ callEnforceMock.mockResolvedValue({
63
+ action: "REDACT",
64
+ redactedResult: "[REDACTED]",
65
+ });
66
+ const event = { toolName: "get_user", metadata: { apiToken: "sk_ok" } };
67
+ const result = await runPost(event, { email: "a@b.com" }, undefined);
68
+ expect(result).toEqual({ result: "[REDACTED]" });
69
+ });
70
+ it("REWRITE returns { result: rewrittenResult }", async () => {
71
+ callEnforceMock.mockResolvedValue({
72
+ action: "REWRITE",
73
+ rewrittenResult: { summary: "Safe summary" },
74
+ });
75
+ const event = { toolName: "run_report", metadata: { apiToken: "sk_ok" } };
76
+ const result = await runPost(event, { raw: "long..." }, undefined);
77
+ expect(result).toEqual({ result: { summary: "Safe summary" } });
78
+ });
79
+ });