@agent-wall/core 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/.turbo/turbo-build.log +17 -0
- package/.turbo/turbo-test.log +30 -0
- package/LICENSE +21 -0
- package/README.md +80 -0
- package/dist/index.d.ts +1297 -0
- package/dist/index.js +3067 -0
- package/dist/index.js.map +1 -0
- package/package.json +48 -0
- package/src/audit-logger-security.test.ts +225 -0
- package/src/audit-logger.test.ts +93 -0
- package/src/audit-logger.ts +458 -0
- package/src/chain-detector.test.ts +100 -0
- package/src/chain-detector.ts +269 -0
- package/src/dashboard-server.test.ts +362 -0
- package/src/dashboard-server.ts +454 -0
- package/src/egress-control.test.ts +177 -0
- package/src/egress-control.ts +274 -0
- package/src/index.ts +137 -0
- package/src/injection-detector.test.ts +207 -0
- package/src/injection-detector.ts +397 -0
- package/src/kill-switch.test.ts +119 -0
- package/src/kill-switch.ts +198 -0
- package/src/policy-engine-security.test.ts +227 -0
- package/src/policy-engine.test.ts +453 -0
- package/src/policy-engine.ts +414 -0
- package/src/policy-loader.test.ts +202 -0
- package/src/policy-loader.ts +485 -0
- package/src/proxy.ts +786 -0
- package/src/read-buffer-security.test.ts +59 -0
- package/src/read-buffer.test.ts +135 -0
- package/src/read-buffer.ts +126 -0
- package/src/response-scanner.test.ts +464 -0
- package/src/response-scanner.ts +587 -0
- package/src/types.test.ts +152 -0
- package/src/types.ts +146 -0
- package/tsconfig.json +8 -0
- package/tsup.config.ts +9 -0
- package/vitest.config.ts +12 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for types module — JSON-RPC message helpers.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from "vitest";
|
|
6
|
+
import {
|
|
7
|
+
isRequest,
|
|
8
|
+
isResponse,
|
|
9
|
+
isNotification,
|
|
10
|
+
isToolCall,
|
|
11
|
+
isToolList,
|
|
12
|
+
getToolCallParams,
|
|
13
|
+
createDenyResponse,
|
|
14
|
+
createPromptResponse,
|
|
15
|
+
type JsonRpcMessage,
|
|
16
|
+
type JsonRpcRequest,
|
|
17
|
+
} from "./types.js";
|
|
18
|
+
|
|
19
|
+
describe("message type guards", () => {
|
|
20
|
+
const request: JsonRpcMessage = {
|
|
21
|
+
jsonrpc: "2.0",
|
|
22
|
+
id: 1,
|
|
23
|
+
method: "tools/call",
|
|
24
|
+
params: { name: "read_file", arguments: { path: "/test" } },
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const response: JsonRpcMessage = {
|
|
28
|
+
jsonrpc: "2.0",
|
|
29
|
+
id: 1,
|
|
30
|
+
result: { content: [{ type: "text", text: "hello" }] },
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const notification: JsonRpcMessage = {
|
|
34
|
+
jsonrpc: "2.0",
|
|
35
|
+
method: "notifications/initialized",
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
it("isRequest should identify requests", () => {
|
|
39
|
+
expect(isRequest(request)).toBe(true);
|
|
40
|
+
expect(isRequest(response)).toBe(false);
|
|
41
|
+
expect(isRequest(notification)).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("isResponse should identify responses", () => {
|
|
45
|
+
expect(isResponse(response)).toBe(true);
|
|
46
|
+
expect(isResponse(request)).toBe(false);
|
|
47
|
+
expect(isResponse(notification)).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("isNotification should identify notifications", () => {
|
|
51
|
+
expect(isNotification(notification)).toBe(true);
|
|
52
|
+
expect(isNotification(request)).toBe(false);
|
|
53
|
+
expect(isNotification(response)).toBe(false);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("isToolCall should identify tools/call requests", () => {
|
|
57
|
+
expect(isToolCall(request)).toBe(true);
|
|
58
|
+
expect(
|
|
59
|
+
isToolCall({ jsonrpc: "2.0", id: 1, method: "tools/list" })
|
|
60
|
+
).toBe(false);
|
|
61
|
+
expect(isToolCall(response)).toBe(false);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("isToolList should identify tools/list requests", () => {
|
|
65
|
+
expect(
|
|
66
|
+
isToolList({ jsonrpc: "2.0", id: 1, method: "tools/list" })
|
|
67
|
+
).toBe(true);
|
|
68
|
+
expect(isToolList(request)).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe("getToolCallParams", () => {
|
|
73
|
+
it("should extract tool name and arguments", () => {
|
|
74
|
+
const req: JsonRpcRequest = {
|
|
75
|
+
jsonrpc: "2.0",
|
|
76
|
+
id: 1,
|
|
77
|
+
method: "tools/call",
|
|
78
|
+
params: { name: "read_file", arguments: { path: "/test" } },
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const result = getToolCallParams(req);
|
|
82
|
+
expect(result).toEqual({
|
|
83
|
+
name: "read_file",
|
|
84
|
+
arguments: { path: "/test" },
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("should return null for non-tools/call methods", () => {
|
|
89
|
+
const req: JsonRpcRequest = {
|
|
90
|
+
jsonrpc: "2.0",
|
|
91
|
+
id: 1,
|
|
92
|
+
method: "tools/list",
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
expect(getToolCallParams(req)).toBeNull();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("should return null when params missing", () => {
|
|
99
|
+
const req: JsonRpcRequest = {
|
|
100
|
+
jsonrpc: "2.0",
|
|
101
|
+
id: 1,
|
|
102
|
+
method: "tools/call",
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
expect(getToolCallParams(req)).toBeNull();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("should default arguments to empty object", () => {
|
|
109
|
+
const req: JsonRpcRequest = {
|
|
110
|
+
jsonrpc: "2.0",
|
|
111
|
+
id: 1,
|
|
112
|
+
method: "tools/call",
|
|
113
|
+
params: { name: "some_tool" },
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const result = getToolCallParams(req);
|
|
117
|
+
expect(result).toEqual({ name: "some_tool", arguments: {} });
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe("createDenyResponse", () => {
|
|
122
|
+
it("should create a JSON-RPC error with custom code and message", () => {
|
|
123
|
+
const resp = createDenyResponse(42, "Access denied");
|
|
124
|
+
expect(resp).toEqual({
|
|
125
|
+
jsonrpc: "2.0",
|
|
126
|
+
id: 42,
|
|
127
|
+
error: {
|
|
128
|
+
code: -32001,
|
|
129
|
+
message: "Agent Wall: Access denied",
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("should work with string IDs", () => {
|
|
135
|
+
const resp = createDenyResponse("req-abc", "Blocked");
|
|
136
|
+
expect(resp.id).toBe("req-abc");
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe("createPromptResponse", () => {
|
|
141
|
+
it("should create a prompt-awaiting error response", () => {
|
|
142
|
+
const resp = createPromptResponse(7, "Requires approval");
|
|
143
|
+
expect(resp).toEqual({
|
|
144
|
+
jsonrpc: "2.0",
|
|
145
|
+
id: 7,
|
|
146
|
+
error: {
|
|
147
|
+
code: -32002,
|
|
148
|
+
message: "Agent Wall: Awaiting approval — Requires approval",
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
});
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Wall JSON-RPC Types
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the MCP protocol's JSON-RPC 2.0 message format.
|
|
5
|
+
* We define our own types instead of depending on the MCP SDK
|
|
6
|
+
* so Agent Wall has zero coupling to any specific MCP version.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
|
|
11
|
+
// ── JSON-RPC 2.0 Base ──────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
export const JsonRpcRequestSchema = z.object({
|
|
14
|
+
jsonrpc: z.literal("2.0"),
|
|
15
|
+
id: z.union([z.string(), z.number()]),
|
|
16
|
+
method: z.string(),
|
|
17
|
+
params: z.record(z.unknown()).optional(),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export const JsonRpcNotificationSchema = z.object({
|
|
21
|
+
jsonrpc: z.literal("2.0"),
|
|
22
|
+
method: z.string(),
|
|
23
|
+
params: z.record(z.unknown()).optional(),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export const JsonRpcResponseSchema = z.object({
|
|
27
|
+
jsonrpc: z.literal("2.0"),
|
|
28
|
+
id: z.union([z.string(), z.number()]),
|
|
29
|
+
result: z.unknown().optional(),
|
|
30
|
+
error: z
|
|
31
|
+
.object({
|
|
32
|
+
code: z.number(),
|
|
33
|
+
message: z.string(),
|
|
34
|
+
data: z.unknown().optional(),
|
|
35
|
+
})
|
|
36
|
+
.optional(),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
export const JsonRpcMessageSchema = z.union([
|
|
40
|
+
JsonRpcRequestSchema,
|
|
41
|
+
JsonRpcNotificationSchema,
|
|
42
|
+
JsonRpcResponseSchema,
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
export type JsonRpcRequest = z.infer<typeof JsonRpcRequestSchema>;
|
|
46
|
+
export type JsonRpcNotification = z.infer<typeof JsonRpcNotificationSchema>;
|
|
47
|
+
export type JsonRpcResponse = z.infer<typeof JsonRpcResponseSchema>;
|
|
48
|
+
export type JsonRpcMessage = z.infer<typeof JsonRpcMessageSchema>;
|
|
49
|
+
|
|
50
|
+
// ── MCP-specific message types ──────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
/** MCP tools/call request params */
|
|
53
|
+
export interface ToolCallParams {
|
|
54
|
+
name: string;
|
|
55
|
+
arguments?: Record<string, unknown>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** MCP tools/list result */
|
|
59
|
+
export interface ToolListResult {
|
|
60
|
+
tools: Array<{
|
|
61
|
+
name: string;
|
|
62
|
+
description?: string;
|
|
63
|
+
inputSchema?: Record<string, unknown>;
|
|
64
|
+
annotations?: Record<string, unknown>;
|
|
65
|
+
}>;
|
|
66
|
+
nextCursor?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** MCP response content block (text, image, resource) */
|
|
70
|
+
export interface McpContentBlock {
|
|
71
|
+
type: string;
|
|
72
|
+
text?: string;
|
|
73
|
+
data?: string;
|
|
74
|
+
mimeType?: string;
|
|
75
|
+
[key: string]: unknown;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── Helpers ─────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
export function isRequest(msg: JsonRpcMessage): msg is JsonRpcRequest {
|
|
81
|
+
return "id" in msg && "method" in msg;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function isNotification(
|
|
85
|
+
msg: JsonRpcMessage
|
|
86
|
+
): msg is JsonRpcNotification {
|
|
87
|
+
return !("id" in msg) && "method" in msg;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function isResponse(msg: JsonRpcMessage): msg is JsonRpcResponse {
|
|
91
|
+
return "id" in msg && !("method" in msg);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function isToolCall(msg: JsonRpcMessage): boolean {
|
|
95
|
+
return isRequest(msg) && msg.method === "tools/call";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function isToolList(msg: JsonRpcMessage): boolean {
|
|
99
|
+
return isRequest(msg) && msg.method === "tools/list";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function getToolCallParams(
|
|
103
|
+
msg: JsonRpcRequest
|
|
104
|
+
): ToolCallParams | null {
|
|
105
|
+
if (msg.method !== "tools/call" || !msg.params) return null;
|
|
106
|
+
const params = msg.params as Record<string, unknown>;
|
|
107
|
+
if (typeof params.name !== "string") return null;
|
|
108
|
+
return {
|
|
109
|
+
name: params.name,
|
|
110
|
+
arguments: (params.arguments as Record<string, unknown>) ?? {},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Create a JSON-RPC error response for a denied tool call.
|
|
116
|
+
*/
|
|
117
|
+
export function createDenyResponse(
|
|
118
|
+
id: string | number,
|
|
119
|
+
message: string
|
|
120
|
+
): JsonRpcResponse {
|
|
121
|
+
return {
|
|
122
|
+
jsonrpc: "2.0",
|
|
123
|
+
id,
|
|
124
|
+
error: {
|
|
125
|
+
code: -32001, // Custom: policy denied
|
|
126
|
+
message: `Agent Wall: ${message}`,
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Create a JSON-RPC error response for a tool call requiring approval.
|
|
133
|
+
*/
|
|
134
|
+
export function createPromptResponse(
|
|
135
|
+
id: string | number,
|
|
136
|
+
message: string
|
|
137
|
+
): JsonRpcResponse {
|
|
138
|
+
return {
|
|
139
|
+
jsonrpc: "2.0",
|
|
140
|
+
id,
|
|
141
|
+
error: {
|
|
142
|
+
code: -32002, // Custom: awaiting approval
|
|
143
|
+
message: `Agent Wall: Awaiting approval — ${message}`,
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
package/tsconfig.json
ADDED
package/tsup.config.ts
ADDED
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { defineConfig } from "vitest/config";
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
test: {
|
|
5
|
+
globals: true,
|
|
6
|
+
environment: "node",
|
|
7
|
+
// Live integration tests spawn real MCP servers and share filesystem
|
|
8
|
+
// state (kill switch file at cwd). Must run sequentially to prevent
|
|
9
|
+
// cross-contamination between test files.
|
|
10
|
+
fileParallelism: false,
|
|
11
|
+
},
|
|
12
|
+
});
|