@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.
@@ -0,0 +1,59 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { ReadBuffer, BufferOverflowError } from "./read-buffer.js";
3
+
4
+ describe("ReadBuffer Security", () => {
5
+ describe("overflow protection", () => {
6
+ it("should accept data within the default limit", () => {
7
+ const buf = new ReadBuffer();
8
+ const chunk = Buffer.alloc(1000, "a");
9
+ expect(() => buf.append(chunk)).not.toThrow();
10
+ });
11
+
12
+ it("should throw BufferOverflowError when exceeding limit", () => {
13
+ const buf = new ReadBuffer(100); // 100 byte limit
14
+ const chunk = Buffer.alloc(200, "a");
15
+ expect(() => buf.append(chunk)).toThrow(BufferOverflowError);
16
+ });
17
+
18
+ it("should clear buffer after overflow", () => {
19
+ const buf = new ReadBuffer(100);
20
+ try {
21
+ buf.append(Buffer.alloc(200, "a"));
22
+ } catch {
23
+ // expected
24
+ }
25
+ expect(buf.hasPendingData).toBe(false);
26
+ expect(buf.currentSize).toBe(0);
27
+ });
28
+
29
+ it("should throw when accumulated chunks exceed limit", () => {
30
+ const buf = new ReadBuffer(100);
31
+ buf.append(Buffer.alloc(60, "a"));
32
+ expect(() => buf.append(Buffer.alloc(60, "b"))).toThrow(BufferOverflowError);
33
+ });
34
+
35
+ it("should work normally after extracting messages frees space", () => {
36
+ const buf = new ReadBuffer(200);
37
+ const msg = JSON.stringify({ jsonrpc: "2.0", method: "test" }) + "\n";
38
+ buf.append(Buffer.from(msg));
39
+ const result = buf.readMessage();
40
+ expect(result).not.toBeNull();
41
+ expect(buf.currentSize).toBe(0);
42
+ });
43
+
44
+ it("should respect custom buffer size", () => {
45
+ const buf = new ReadBuffer(50);
46
+ expect(() => buf.append(Buffer.alloc(51, "x"))).toThrow(BufferOverflowError);
47
+ });
48
+
49
+ it("should include size info in error message", () => {
50
+ const buf = new ReadBuffer(100);
51
+ try {
52
+ buf.append(Buffer.alloc(200, "a"));
53
+ } catch (err) {
54
+ expect((err as Error).message).toContain("200");
55
+ expect((err as Error).message).toContain("100");
56
+ }
57
+ });
58
+ });
59
+ });
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Tests for ReadBuffer — JSON-RPC message parsing over newline-delimited streams.
3
+ */
4
+
5
+ import { describe, it, expect } from "vitest";
6
+ import { ReadBuffer, serializeMessage, deserializeMessage } from "./read-buffer.js";
7
+ import type { JsonRpcMessage } from "./types.js";
8
+
9
+ describe("ReadBuffer", () => {
10
+ it("should parse a single complete message", () => {
11
+ const buf = new ReadBuffer();
12
+ const msg: JsonRpcMessage = {
13
+ jsonrpc: "2.0",
14
+ id: 1,
15
+ method: "tools/list",
16
+ };
17
+ buf.append(Buffer.from(JSON.stringify(msg) + "\n"));
18
+
19
+ const result = buf.readMessage();
20
+ expect(result).toEqual(msg);
21
+ });
22
+
23
+ it("should return null when no complete message available", () => {
24
+ const buf = new ReadBuffer();
25
+ buf.append(Buffer.from('{"jsonrpc":"2.0"'));
26
+
27
+ const result = buf.readMessage();
28
+ expect(result).toBeNull();
29
+ });
30
+
31
+ it("should handle messages split across multiple chunks", () => {
32
+ const buf = new ReadBuffer();
33
+ const msg: JsonRpcMessage = {
34
+ jsonrpc: "2.0",
35
+ id: 1,
36
+ method: "tools/call",
37
+ params: { name: "read_file", arguments: { path: "/test" } },
38
+ };
39
+ const full = JSON.stringify(msg) + "\n";
40
+ const mid = Math.floor(full.length / 2);
41
+
42
+ buf.append(Buffer.from(full.slice(0, mid)));
43
+ expect(buf.readMessage()).toBeNull();
44
+
45
+ buf.append(Buffer.from(full.slice(mid)));
46
+ const result = buf.readMessage();
47
+ expect(result).toEqual(msg);
48
+ });
49
+
50
+ it("should parse multiple messages in one chunk", () => {
51
+ const buf = new ReadBuffer();
52
+ const msg1: JsonRpcMessage = { jsonrpc: "2.0", id: 1, method: "a" };
53
+ const msg2: JsonRpcMessage = { jsonrpc: "2.0", id: 2, method: "b" };
54
+
55
+ buf.append(
56
+ Buffer.from(JSON.stringify(msg1) + "\n" + JSON.stringify(msg2) + "\n")
57
+ );
58
+
59
+ const results = buf.readAllMessages();
60
+ expect(results).toHaveLength(2);
61
+ expect(results[0]).toEqual(msg1);
62
+ expect(results[1]).toEqual(msg2);
63
+ });
64
+
65
+ it("should handle \\r\\n line endings (Windows)", () => {
66
+ const buf = new ReadBuffer();
67
+ const msg: JsonRpcMessage = { jsonrpc: "2.0", id: 1, method: "test" };
68
+ buf.append(Buffer.from(JSON.stringify(msg) + "\r\n"));
69
+
70
+ const result = buf.readMessage();
71
+ expect(result).toEqual(msg);
72
+ });
73
+
74
+ it("should skip empty lines", () => {
75
+ const buf = new ReadBuffer();
76
+ const msg: JsonRpcMessage = { jsonrpc: "2.0", id: 1, method: "test" };
77
+ buf.append(Buffer.from("\n\n" + JSON.stringify(msg) + "\n"));
78
+
79
+ const results = buf.readAllMessages();
80
+ expect(results).toHaveLength(1);
81
+ expect(results[0]).toEqual(msg);
82
+ });
83
+
84
+ it("should clear the buffer", () => {
85
+ const buf = new ReadBuffer();
86
+ buf.append(Buffer.from("some data"));
87
+ expect(buf.hasPendingData).toBe(true);
88
+
89
+ buf.clear();
90
+ expect(buf.hasPendingData).toBe(false);
91
+ });
92
+
93
+ it("should throw on invalid JSON", () => {
94
+ const buf = new ReadBuffer();
95
+ buf.append(Buffer.from("not-json\n"));
96
+
97
+ expect(() => buf.readMessage()).toThrow();
98
+ });
99
+ });
100
+
101
+ describe("serializeMessage", () => {
102
+ it("should serialize with trailing newline", () => {
103
+ const msg: JsonRpcMessage = { jsonrpc: "2.0", id: 1, method: "test" };
104
+ const result = serializeMessage(msg);
105
+ expect(result).toBe('{"jsonrpc":"2.0","id":1,"method":"test"}\n');
106
+ expect(result.endsWith("\n")).toBe(true);
107
+ });
108
+ });
109
+
110
+ describe("deserializeMessage", () => {
111
+ it("should parse valid JSON-RPC request", () => {
112
+ const line = '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"read_file"}}';
113
+ const result = deserializeMessage(line);
114
+ expect(result).toEqual({
115
+ jsonrpc: "2.0",
116
+ id: 1,
117
+ method: "tools/call",
118
+ params: { name: "read_file" },
119
+ });
120
+ });
121
+
122
+ it("should parse valid JSON-RPC response", () => {
123
+ const line = '{"jsonrpc":"2.0","id":1,"result":{"content":"hello"}}';
124
+ const result = deserializeMessage(line);
125
+ expect(result).toEqual({
126
+ jsonrpc: "2.0",
127
+ id: 1,
128
+ result: { content: "hello" },
129
+ });
130
+ });
131
+
132
+ it("should reject non-JSON-RPC messages", () => {
133
+ expect(() => deserializeMessage('{"hello":"world"}')).toThrow();
134
+ });
135
+ });
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Agent Wall Read Buffer
3
+ *
4
+ * Accumulates raw bytes from a stream and extracts
5
+ * newline-delimited JSON-RPC messages one at a time.
6
+ *
7
+ * Directly mirrors the MCP SDK's ReadBuffer pattern:
8
+ * - Append raw chunks
9
+ * - Scan for '\n' delimiter
10
+ * - Extract line, strip '\r', JSON.parse, validate
11
+ *
12
+ * Security: Enforces maximum buffer size to prevent DOS
13
+ * via unbounded memory growth from a single large message.
14
+ */
15
+
16
+ import { JsonRpcMessage, JsonRpcMessageSchema } from "./types.js";
17
+
18
+ /** Default max buffer size: 10MB */
19
+ const DEFAULT_MAX_BUFFER_SIZE = 10 * 1024 * 1024;
20
+
21
+ export class ReadBuffer {
22
+ private buffer: Buffer | null = null;
23
+ private maxBufferSize: number;
24
+
25
+ constructor(maxBufferSize: number = DEFAULT_MAX_BUFFER_SIZE) {
26
+ this.maxBufferSize = maxBufferSize;
27
+ }
28
+
29
+ /**
30
+ * Append raw bytes from a stream chunk.
31
+ * Throws if the buffer exceeds the configured maximum size.
32
+ */
33
+ append(chunk: Buffer): void {
34
+ this.buffer = this.buffer ? Buffer.concat([this.buffer, chunk]) : chunk;
35
+ if (this.buffer.length > this.maxBufferSize) {
36
+ const size = this.buffer.length;
37
+ this.buffer = null;
38
+ throw new BufferOverflowError(
39
+ `Buffer size ${size} exceeds maximum ${this.maxBufferSize} bytes — possible DOS attack`
40
+ );
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Try to extract the next complete JSON-RPC message.
46
+ * Returns null if no complete message is available yet.
47
+ * Automatically skips empty lines.
48
+ */
49
+ readMessage(): JsonRpcMessage | null {
50
+ while (this.buffer) {
51
+ const index = this.buffer.indexOf("\n");
52
+ if (index === -1) return null;
53
+
54
+ // Extract the line (strip trailing \r for Windows compatibility)
55
+ const line = this.buffer.toString("utf8", 0, index).replace(/\r$/, "");
56
+ this.buffer = this.buffer.subarray(index + 1);
57
+
58
+ // Normalize: discard buffer reference if empty
59
+ if (this.buffer.length === 0) this.buffer = null;
60
+
61
+ // Skip empty lines
62
+ if (line.length === 0) continue;
63
+
64
+ return deserializeMessage(line);
65
+ }
66
+ return null;
67
+ }
68
+
69
+ /**
70
+ * Extract ALL available messages from the buffer.
71
+ */
72
+ readAllMessages(): JsonRpcMessage[] {
73
+ const messages: JsonRpcMessage[] = [];
74
+ let msg: JsonRpcMessage | null;
75
+ while ((msg = this.readMessage()) !== null) {
76
+ messages.push(msg);
77
+ }
78
+ return messages;
79
+ }
80
+
81
+ /**
82
+ * Clear the buffer.
83
+ */
84
+ clear(): void {
85
+ this.buffer = null;
86
+ }
87
+
88
+ /**
89
+ * Check if there's any pending data in the buffer.
90
+ */
91
+ get hasPendingData(): boolean {
92
+ return this.buffer !== null && this.buffer.length > 0;
93
+ }
94
+
95
+ /**
96
+ * Get current buffer size in bytes.
97
+ */
98
+ get currentSize(): number {
99
+ return this.buffer?.length ?? 0;
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Error thrown when buffer exceeds maximum size.
105
+ */
106
+ export class BufferOverflowError extends Error {
107
+ constructor(message: string) {
108
+ super(message);
109
+ this.name = "BufferOverflowError";
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Parse a single line of text into a validated JSON-RPC message.
115
+ */
116
+ export function deserializeMessage(line: string): JsonRpcMessage {
117
+ const parsed = JSON.parse(line);
118
+ return JsonRpcMessageSchema.parse(parsed);
119
+ }
120
+
121
+ /**
122
+ * Serialize a JSON-RPC message to a newline-delimited string.
123
+ */
124
+ export function serializeMessage(message: JsonRpcMessage): string {
125
+ return JSON.stringify(message) + "\n";
126
+ }