@signalvaultio/node 0.1.0 → 0.3.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/README.md +164 -19
- package/dist/__tests__/index.test.js +89 -39
- package/dist/__tests__/tools.test.d.ts +1 -0
- package/dist/__tests__/tools.test.js +243 -0
- package/dist/index.d.ts +114 -25
- package/dist/index.js +384 -210
- package/dist/tools.d.ts +69 -0
- package/dist/tools.js +207 -0
- package/package.json +13 -3
package/dist/tools.d.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
interface ToolContext {
|
|
2
|
+
requestId?: string;
|
|
3
|
+
}
|
|
4
|
+
export interface ToolRecordOptions {
|
|
5
|
+
/** Logical name of the tool. Required. Truncated to 200 bytes. */
|
|
6
|
+
toolName: string;
|
|
7
|
+
/** Arguments passed to the tool. Any JSON-serializable value. */
|
|
8
|
+
toolInput?: unknown;
|
|
9
|
+
/** What the tool returned. Any JSON-serializable value. */
|
|
10
|
+
toolOutput?: unknown;
|
|
11
|
+
/** How long the tool took to run, in milliseconds. */
|
|
12
|
+
durationMs: number;
|
|
13
|
+
/** Error message if the tool threw. Truncated to 1900 bytes. */
|
|
14
|
+
error?: string | null;
|
|
15
|
+
/**
|
|
16
|
+
* Optional ISO8601 timestamp of when the tool started.
|
|
17
|
+
* Falls back to server-side `inserted_at` when omitted.
|
|
18
|
+
*/
|
|
19
|
+
startedAt?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Parent ai.request correlation. If omitted, the SDK uses the requestId from
|
|
22
|
+
* the surrounding `withContext` block (if any), else null (orphan tool call).
|
|
23
|
+
*/
|
|
24
|
+
requestId?: string | null;
|
|
25
|
+
/** Per-call metadata, merged over client-level defaults. */
|
|
26
|
+
metadata?: Record<string, unknown>;
|
|
27
|
+
}
|
|
28
|
+
export interface ToolsAPI {
|
|
29
|
+
/**
|
|
30
|
+
* Records a tool call manually. Use when the wrapper isn't a fit
|
|
31
|
+
* (streaming tools, custom timing, post-hoc capture).
|
|
32
|
+
*/
|
|
33
|
+
record(opts: ToolRecordOptions): Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
/** Returns the requestId from the surrounding withContext block, or undefined. */
|
|
36
|
+
export declare function getCurrentRequestId(): string | undefined;
|
|
37
|
+
/** Runs `fn` inside an async context with the given requestId attached. */
|
|
38
|
+
export declare function runWithContext<T>(ctx: ToolContext, fn: () => Promise<T>): Promise<T>;
|
|
39
|
+
export interface ToolWrapperConfig {
|
|
40
|
+
recordFn: (opts: ToolRecordOptions) => Promise<void>;
|
|
41
|
+
debug?: boolean;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Wraps a function so every call records an `agent.tool_call` event with
|
|
45
|
+
* timing, args, result, and any error.
|
|
46
|
+
*
|
|
47
|
+
* The wrapper auto-times, JSON-serializes args/result, and captures the
|
|
48
|
+
* surrounding `withContext` requestId (if any). Errors propagate to the caller
|
|
49
|
+
* after recording — wrapping is observation, not error swallowing.
|
|
50
|
+
*/
|
|
51
|
+
export declare function wrapTool<TArgs extends readonly unknown[], TReturn>(config: ToolWrapperConfig, toolName: string, fn: (...args: TArgs) => TReturn | Promise<TReturn>, options?: {
|
|
52
|
+
metadata?: Record<string, unknown>;
|
|
53
|
+
}): (...args: TArgs) => Promise<TReturn>;
|
|
54
|
+
/** Validates and normalizes a tool name. Throws if missing or non-string. */
|
|
55
|
+
export declare function validateToolName(name: unknown): string;
|
|
56
|
+
/** Truncates an error string to the server-accepted byte limit. */
|
|
57
|
+
export declare function sanitizeError(err: unknown): string | undefined;
|
|
58
|
+
/**
|
|
59
|
+
* Returns a JSON-safe version of `value`, capped at MAX_PAYLOAD_BYTES.
|
|
60
|
+
*
|
|
61
|
+
* - Circular references, BigInt, functions, Symbols → replaced with a marker
|
|
62
|
+
* object so the audit event is still useful instead of dropped silently.
|
|
63
|
+
* - Oversized values → truncated; the marker `_signalvault_truncated` is set
|
|
64
|
+
* so the receiver can detect partial captures.
|
|
65
|
+
*
|
|
66
|
+
* Never throws — sanitization failures degrade gracefully.
|
|
67
|
+
*/
|
|
68
|
+
export declare function sanitizePayload(value: unknown): unknown;
|
|
69
|
+
export {};
|
package/dist/tools.js
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getCurrentRequestId = getCurrentRequestId;
|
|
4
|
+
exports.runWithContext = runWithContext;
|
|
5
|
+
exports.wrapTool = wrapTool;
|
|
6
|
+
exports.validateToolName = validateToolName;
|
|
7
|
+
exports.sanitizeError = sanitizeError;
|
|
8
|
+
exports.sanitizePayload = sanitizePayload;
|
|
9
|
+
const async_hooks_1 = require("async_hooks");
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Constants
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
/** Hard limit for serialized tool_input/tool_output JSON, in UTF-8 bytes. */
|
|
14
|
+
const MAX_PAYLOAD_BYTES = 256 * 1024; // 256 KB
|
|
15
|
+
/** Hard limit for tool error string, in UTF-8 bytes. */
|
|
16
|
+
const MAX_ERROR_BYTES = 1900;
|
|
17
|
+
/** Hard limit for tool_name, in UTF-8 bytes. */
|
|
18
|
+
const MAX_TOOL_NAME_BYTES = 200;
|
|
19
|
+
const toolContext = new async_hooks_1.AsyncLocalStorage();
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Internal: context-aware request_id resolution
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
/** Returns the requestId from the surrounding withContext block, or undefined. */
|
|
24
|
+
function getCurrentRequestId() {
|
|
25
|
+
return toolContext.getStore()?.requestId;
|
|
26
|
+
}
|
|
27
|
+
/** Runs `fn` inside an async context with the given requestId attached. */
|
|
28
|
+
function runWithContext(ctx, fn) {
|
|
29
|
+
return toolContext.run(ctx, fn);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Wraps a function so every call records an `agent.tool_call` event with
|
|
33
|
+
* timing, args, result, and any error.
|
|
34
|
+
*
|
|
35
|
+
* The wrapper auto-times, JSON-serializes args/result, and captures the
|
|
36
|
+
* surrounding `withContext` requestId (if any). Errors propagate to the caller
|
|
37
|
+
* after recording — wrapping is observation, not error swallowing.
|
|
38
|
+
*/
|
|
39
|
+
function wrapTool(config, toolName, fn, options) {
|
|
40
|
+
return async (...args) => {
|
|
41
|
+
const startedAt = new Date().toISOString();
|
|
42
|
+
const start = Date.now();
|
|
43
|
+
let result;
|
|
44
|
+
let error = null;
|
|
45
|
+
try {
|
|
46
|
+
result = await Promise.resolve(fn(...args));
|
|
47
|
+
const durationMs = Date.now() - start;
|
|
48
|
+
// Fire-and-forget recording so the wrapped function's latency
|
|
49
|
+
// isn't affected by SignalVault's network call.
|
|
50
|
+
void config
|
|
51
|
+
.recordFn({
|
|
52
|
+
toolName,
|
|
53
|
+
toolInput: serializeArgs(args),
|
|
54
|
+
toolOutput: result,
|
|
55
|
+
durationMs,
|
|
56
|
+
startedAt,
|
|
57
|
+
requestId: getCurrentRequestId() ?? null,
|
|
58
|
+
metadata: options?.metadata,
|
|
59
|
+
})
|
|
60
|
+
.catch((err) => {
|
|
61
|
+
if (config.debug)
|
|
62
|
+
console.error('[SignalVault] tool record failed:', err);
|
|
63
|
+
});
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
catch (e) {
|
|
67
|
+
const durationMs = Date.now() - start;
|
|
68
|
+
error = e instanceof Error ? e.message : String(e);
|
|
69
|
+
void config
|
|
70
|
+
.recordFn({
|
|
71
|
+
toolName,
|
|
72
|
+
toolInput: serializeArgs(args),
|
|
73
|
+
toolOutput: null,
|
|
74
|
+
durationMs,
|
|
75
|
+
error,
|
|
76
|
+
startedAt,
|
|
77
|
+
requestId: getCurrentRequestId() ?? null,
|
|
78
|
+
metadata: options?.metadata,
|
|
79
|
+
})
|
|
80
|
+
.catch((err) => {
|
|
81
|
+
if (config.debug)
|
|
82
|
+
console.error('[SignalVault] tool record failed:', err);
|
|
83
|
+
});
|
|
84
|
+
throw e;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
// Sanitization helpers
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
/**
|
|
92
|
+
* Returns the byte length of a string when encoded as UTF-8.
|
|
93
|
+
* Used to enforce server-side byte limits accurately on the client.
|
|
94
|
+
*/
|
|
95
|
+
function byteLength(s) {
|
|
96
|
+
return Buffer.byteLength(s, 'utf8');
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Truncates a string to `max` UTF-8 bytes, preserving multi-byte boundaries.
|
|
100
|
+
* Appends a "(…N bytes truncated)" marker so the receiver knows truncation
|
|
101
|
+
* happened rather than seeing a silently shortened string.
|
|
102
|
+
*/
|
|
103
|
+
function truncateString(s, max) {
|
|
104
|
+
if (byteLength(s) <= max)
|
|
105
|
+
return s;
|
|
106
|
+
const marker = (n) => ` …(truncated ${n} bytes)`;
|
|
107
|
+
// Reserve room for the marker — sized to fit the worst-case marker.
|
|
108
|
+
const reserve = byteLength(marker(99999999));
|
|
109
|
+
const buf = Buffer.from(s, 'utf8');
|
|
110
|
+
const truncatedBytes = buf.byteLength - (max - reserve);
|
|
111
|
+
const sliced = buf.subarray(0, max - reserve).toString('utf8');
|
|
112
|
+
// Cleanup: drop a trailing replacement char if we cut a multi-byte sequence.
|
|
113
|
+
const cleaned = sliced.endsWith('�') ? sliced.slice(0, -1) : sliced;
|
|
114
|
+
return cleaned + marker(truncatedBytes);
|
|
115
|
+
}
|
|
116
|
+
/** Validates and normalizes a tool name. Throws if missing or non-string. */
|
|
117
|
+
function validateToolName(name) {
|
|
118
|
+
if (typeof name !== 'string' || name.length === 0) {
|
|
119
|
+
throw new Error('[SignalVault] toolName must be a non-empty string');
|
|
120
|
+
}
|
|
121
|
+
return byteLength(name) > MAX_TOOL_NAME_BYTES
|
|
122
|
+
? truncateString(name, MAX_TOOL_NAME_BYTES)
|
|
123
|
+
: name;
|
|
124
|
+
}
|
|
125
|
+
/** Truncates an error string to the server-accepted byte limit. */
|
|
126
|
+
function sanitizeError(err) {
|
|
127
|
+
if (err === null || err === undefined)
|
|
128
|
+
return undefined;
|
|
129
|
+
const s = typeof err === 'string' ? err : String(err);
|
|
130
|
+
return truncateString(s, MAX_ERROR_BYTES);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Returns a JSON-safe version of `value`, capped at MAX_PAYLOAD_BYTES.
|
|
134
|
+
*
|
|
135
|
+
* - Circular references, BigInt, functions, Symbols → replaced with a marker
|
|
136
|
+
* object so the audit event is still useful instead of dropped silently.
|
|
137
|
+
* - Oversized values → truncated; the marker `_signalvault_truncated` is set
|
|
138
|
+
* so the receiver can detect partial captures.
|
|
139
|
+
*
|
|
140
|
+
* Never throws — sanitization failures degrade gracefully.
|
|
141
|
+
*/
|
|
142
|
+
function sanitizePayload(value) {
|
|
143
|
+
if (value === undefined)
|
|
144
|
+
return null;
|
|
145
|
+
let serialized;
|
|
146
|
+
try {
|
|
147
|
+
serialized = JSON.stringify(value, replacer());
|
|
148
|
+
}
|
|
149
|
+
catch (e) {
|
|
150
|
+
return {
|
|
151
|
+
_signalvault_serialization_error: e instanceof Error ? e.message : String(e),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
// JSON.stringify returns undefined for top-level functions/Symbols/undefined.
|
|
155
|
+
if (serialized === undefined) {
|
|
156
|
+
return { _signalvault_serialization_error: 'value is not JSON-serializable' };
|
|
157
|
+
}
|
|
158
|
+
if (byteLength(serialized) <= MAX_PAYLOAD_BYTES) {
|
|
159
|
+
// Re-parse so the body is sent as a structured value, not a JSON string.
|
|
160
|
+
try {
|
|
161
|
+
return JSON.parse(serialized);
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
return { _signalvault_serialization_error: 're-parse failed' };
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// Oversized: include the original byte size so debugging is possible.
|
|
168
|
+
return {
|
|
169
|
+
_signalvault_truncated: true,
|
|
170
|
+
_signalvault_original_bytes: byteLength(serialized),
|
|
171
|
+
_signalvault_max_bytes: MAX_PAYLOAD_BYTES,
|
|
172
|
+
preview: truncateString(serialized, MAX_PAYLOAD_BYTES - 200),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* JSON.stringify replacer that handles circular refs, BigInts, and other
|
|
177
|
+
* non-serializable values by substituting markers instead of throwing.
|
|
178
|
+
*/
|
|
179
|
+
function replacer() {
|
|
180
|
+
const seen = new WeakSet();
|
|
181
|
+
return function (_key, value) {
|
|
182
|
+
if (typeof value === 'bigint')
|
|
183
|
+
return `${value.toString()}n`;
|
|
184
|
+
if (typeof value === 'function')
|
|
185
|
+
return '[Function]';
|
|
186
|
+
if (typeof value === 'symbol')
|
|
187
|
+
return value.toString();
|
|
188
|
+
if (value !== null && typeof value === 'object') {
|
|
189
|
+
if (seen.has(value))
|
|
190
|
+
return '[Circular]';
|
|
191
|
+
seen.add(value);
|
|
192
|
+
}
|
|
193
|
+
return value;
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* If the wrapped function takes a single argument, store that value directly
|
|
198
|
+
* (the common case). Otherwise store the whole args array. This avoids storing
|
|
199
|
+
* `[{...}]` when callers pass a single object — better for dashboard rendering.
|
|
200
|
+
*/
|
|
201
|
+
function serializeArgs(args) {
|
|
202
|
+
if (args.length === 0)
|
|
203
|
+
return null;
|
|
204
|
+
if (args.length === 1)
|
|
205
|
+
return args[0];
|
|
206
|
+
return args;
|
|
207
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@signalvaultio/node",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "SignalVault SDK - AI audit logs and guardrails for safe production AI",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
"audit",
|
|
19
19
|
"guardrails",
|
|
20
20
|
"openai",
|
|
21
|
+
"anthropic",
|
|
22
|
+
"claude",
|
|
21
23
|
"llm",
|
|
22
24
|
"compliance",
|
|
23
25
|
"security",
|
|
@@ -25,16 +27,24 @@
|
|
|
25
27
|
],
|
|
26
28
|
"author": "SignalVault <support@signalvault.io>",
|
|
27
29
|
"license": "MIT",
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18"
|
|
32
|
+
},
|
|
28
33
|
"repository": {
|
|
29
34
|
"type": "git",
|
|
30
|
-
"url": "https://github.com/signalvaultio/signalvault-node"
|
|
35
|
+
"url": "git+https://github.com/signalvaultio/signalvault-node.git"
|
|
31
36
|
},
|
|
32
37
|
"bugs": {
|
|
33
38
|
"url": "https://github.com/signalvaultio/signalvault-node/issues"
|
|
34
39
|
},
|
|
35
40
|
"homepage": "https://signalvault.io/docs",
|
|
36
41
|
"peerDependencies": {
|
|
37
|
-
"openai": "^4.0.0"
|
|
42
|
+
"openai": "^4.0.0",
|
|
43
|
+
"@anthropic-ai/sdk": ">=0.20.0"
|
|
44
|
+
},
|
|
45
|
+
"peerDependenciesMeta": {
|
|
46
|
+
"openai": { "optional": true },
|
|
47
|
+
"@anthropic-ai/sdk": { "optional": true }
|
|
38
48
|
},
|
|
39
49
|
"dependencies": {
|
|
40
50
|
"uuid": "^9.0.0"
|