@agentx-core/security-sdk 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/LICENSE +23 -0
- package/README.md +139 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.js +99 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +6 -0
- package/dist/ledger.d.ts +46 -0
- package/dist/ledger.js +183 -0
- package/dist/pulse.d.ts +102 -0
- package/dist/pulse.js +401 -0
- package/dist/report.d.ts +76 -0
- package/dist/report.js +359 -0
- package/dist/shape.d.ts +79 -0
- package/dist/shape.js +197 -0
- package/dist/wrap.d.ts +52 -0
- package/dist/wrap.js +200 -0
- package/package.json +47 -0
package/dist/wrap.d.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agentxWatch(tool)`: wrap a tool so every call is written down. Watches and records; blocks
|
|
3
|
+
* nothing. The free, keyless rung for TypeScript, and the TypeScript analogue of the Python
|
|
4
|
+
* `@agentx_protect` decorator in its watching posture.
|
|
5
|
+
*
|
|
6
|
+
* NO IMPACT ON THE WRAPPED CODE. The original `execute` is called with the same `this`, the
|
|
7
|
+
* same arguments and the same context, and whatever it returns or throws passes straight
|
|
8
|
+
* through. Recording happens first, inside its own try/catch, so an unwritable folder or a
|
|
9
|
+
* full disk costs a ledger row and never a tool call. The wrapper does not make a sync tool
|
|
10
|
+
* async and does not touch the arguments object.
|
|
11
|
+
*
|
|
12
|
+
* THE PART THAT RECORDS AND REPORTS IS SEPARATE FROM THE PART THAT WRAPS, deliberately
|
|
13
|
+
* (shape.ts / ledger.ts / report.ts against this file). If blocking ever arrives it is a
|
|
14
|
+
* different wrapper over the same record, not a rewrite.
|
|
15
|
+
*/
|
|
16
|
+
import { type Action } from "./shape";
|
|
17
|
+
import * as pulse from "./pulse";
|
|
18
|
+
/**
|
|
19
|
+
* Any object with an `execute` function: a Vercel AI SDK `tool()`, or your own. A LangChain.js
|
|
20
|
+
* tool is a class instance driven through `invoke`, and this wrapper does not cover it.
|
|
21
|
+
*/
|
|
22
|
+
export interface WatchableTool {
|
|
23
|
+
description?: string;
|
|
24
|
+
execute: (...args: any[]) => any;
|
|
25
|
+
[k: string]: unknown;
|
|
26
|
+
}
|
|
27
|
+
export interface WatchOptions {
|
|
28
|
+
/**
|
|
29
|
+
* The tool's name as it appears on the report. A Vercel AI SDK `tool()` carries none (its
|
|
30
|
+
* name is the key in your `tools` object), so pass it here or use `agentxWatchAll`, which
|
|
31
|
+
* takes the keys. Unnamed tools report as "(unnamed)".
|
|
32
|
+
*/
|
|
33
|
+
name?: string;
|
|
34
|
+
/** Which agent made the call. Shown on the report only when more than one agent is on it. */
|
|
35
|
+
agentId?: string;
|
|
36
|
+
/** The closed action vocabulary the rest of the product uses. Fills the SURFACE column when the names leave it blank. */
|
|
37
|
+
action?: Action;
|
|
38
|
+
}
|
|
39
|
+
/** TEST ONLY. */
|
|
40
|
+
export declare function __resetSessionForTests(): void;
|
|
41
|
+
/** TEST ONLY. The counters as the pulse will see them. */
|
|
42
|
+
export declare function __sessionStatsForTests(): pulse.SessionStats;
|
|
43
|
+
/**
|
|
44
|
+
* Wrap one tool. Returns a new object with the same shape; the original is not mutated.
|
|
45
|
+
*/
|
|
46
|
+
export declare function agentxWatch<T extends WatchableTool>(tool: T, opts?: WatchOptions): T;
|
|
47
|
+
/**
|
|
48
|
+
* Wrap a whole `tools` map, naming each by its key. The Vercel AI SDK shape:
|
|
49
|
+
*
|
|
50
|
+
* tools: agentxWatchAll({ runSql, sendEmail })
|
|
51
|
+
*/
|
|
52
|
+
export declare function agentxWatchAll<T extends Record<string, WatchableTool>>(tools: T, opts?: Omit<WatchOptions, "name">): T;
|
package/dist/wrap.js
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.__resetSessionForTests = __resetSessionForTests;
|
|
37
|
+
exports.__sessionStatsForTests = __sessionStatsForTests;
|
|
38
|
+
exports.agentxWatch = agentxWatch;
|
|
39
|
+
exports.agentxWatchAll = agentxWatchAll;
|
|
40
|
+
/**
|
|
41
|
+
* `agentxWatch(tool)`: wrap a tool so every call is written down. Watches and records; blocks
|
|
42
|
+
* nothing. The free, keyless rung for TypeScript, and the TypeScript analogue of the Python
|
|
43
|
+
* `@agentx_protect` decorator in its watching posture.
|
|
44
|
+
*
|
|
45
|
+
* NO IMPACT ON THE WRAPPED CODE. The original `execute` is called with the same `this`, the
|
|
46
|
+
* same arguments and the same context, and whatever it returns or throws passes straight
|
|
47
|
+
* through. Recording happens first, inside its own try/catch, so an unwritable folder or a
|
|
48
|
+
* full disk costs a ledger row and never a tool call. The wrapper does not make a sync tool
|
|
49
|
+
* async and does not touch the arguments object.
|
|
50
|
+
*
|
|
51
|
+
* THE PART THAT RECORDS AND REPORTS IS SEPARATE FROM THE PART THAT WRAPS, deliberately
|
|
52
|
+
* (shape.ts / ledger.ts / report.ts against this file). If blocking ever arrives it is a
|
|
53
|
+
* different wrapper over the same record, not a rewrite.
|
|
54
|
+
*/
|
|
55
|
+
const shape_1 = require("./shape");
|
|
56
|
+
const ledger_1 = require("./ledger");
|
|
57
|
+
const pulse = __importStar(require("./pulse"));
|
|
58
|
+
const report_1 = require("./report");
|
|
59
|
+
const session = {
|
|
60
|
+
totalCalls: 0,
|
|
61
|
+
toolNames: new Set(),
|
|
62
|
+
hooked: false,
|
|
63
|
+
reported: false,
|
|
64
|
+
ending: false,
|
|
65
|
+
ledgerNoticed: false,
|
|
66
|
+
};
|
|
67
|
+
/** TEST ONLY. */
|
|
68
|
+
function __resetSessionForTests() {
|
|
69
|
+
session.totalCalls = 0;
|
|
70
|
+
session.toolNames.clear();
|
|
71
|
+
session.reported = false;
|
|
72
|
+
session.ending = false;
|
|
73
|
+
session.ledgerNoticed = false;
|
|
74
|
+
}
|
|
75
|
+
/** TEST ONLY. The counters as the pulse will see them. */
|
|
76
|
+
function __sessionStatsForTests() {
|
|
77
|
+
return stats();
|
|
78
|
+
}
|
|
79
|
+
function stats() {
|
|
80
|
+
return { totalCalls: session.totalCalls, distinctTools: session.toolNames.size };
|
|
81
|
+
}
|
|
82
|
+
function stderr(line) {
|
|
83
|
+
try {
|
|
84
|
+
process.stderr.write(line + "\n");
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
/* a closed stderr is not our problem to raise */
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The session-end line, to stderr, only when something was wrapped and called. Says what it
|
|
92
|
+
* counted ("tool calls", every one of them) and that nothing was blocked, so the count cannot
|
|
93
|
+
* be read as protection.
|
|
94
|
+
*/
|
|
95
|
+
function printReport() {
|
|
96
|
+
if (session.reported || session.totalCalls === 0)
|
|
97
|
+
return;
|
|
98
|
+
session.reported = true;
|
|
99
|
+
const rule = "─".repeat(60);
|
|
100
|
+
stderr(rule);
|
|
101
|
+
stderr(` AgentX watched ${(0, report_1.plural)(session.totalCalls, "tool call")} across ${(0, report_1.plural)(session.toolNames.size, "tool")} this run.`);
|
|
102
|
+
stderr(" Watching records and blocks nothing. What your agent did:");
|
|
103
|
+
stderr(` ${report_1.AUDIT_COMMAND}`);
|
|
104
|
+
stderr(rule);
|
|
105
|
+
}
|
|
106
|
+
async function onBeforeExit() {
|
|
107
|
+
// `beforeExit` fires again once the awaited send below drains the loop; the flag stops the
|
|
108
|
+
// second pass from sending a second pulse.
|
|
109
|
+
if (session.ending)
|
|
110
|
+
return;
|
|
111
|
+
session.ending = true;
|
|
112
|
+
printReport();
|
|
113
|
+
(0, ledger_1.trimLedger)();
|
|
114
|
+
await pulse.onSessionEnd(stats());
|
|
115
|
+
}
|
|
116
|
+
function onExit() {
|
|
117
|
+
printReport();
|
|
118
|
+
if (!session.ending) {
|
|
119
|
+
// process.exit() skipped beforeExit, so nothing async can run now: queue the counts and
|
|
120
|
+
// the next run sends them. One run late is honest; never is a lost install.
|
|
121
|
+
session.ending = true;
|
|
122
|
+
pulse.queuePending(stats());
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function installHooks() {
|
|
126
|
+
if (session.hooked)
|
|
127
|
+
return;
|
|
128
|
+
if (typeof process === "undefined" || typeof process.on !== "function")
|
|
129
|
+
return;
|
|
130
|
+
session.hooked = true;
|
|
131
|
+
process.once("beforeExit", () => {
|
|
132
|
+
void onBeforeExit();
|
|
133
|
+
});
|
|
134
|
+
process.on("exit", onExit);
|
|
135
|
+
}
|
|
136
|
+
function record(name, args, description, opts) {
|
|
137
|
+
// REDUCE FIRST. Only the shape exists past this line; there is no variable holding a value
|
|
138
|
+
// for the ledger writer to reach.
|
|
139
|
+
const shape = (0, shape_1.callShape)(name, args, description, opts.action);
|
|
140
|
+
const row = {
|
|
141
|
+
k: "call",
|
|
142
|
+
ts: Date.now(),
|
|
143
|
+
tool: name,
|
|
144
|
+
args: shape.argNames,
|
|
145
|
+
surface: shape.targetClass,
|
|
146
|
+
amount: shape.amount,
|
|
147
|
+
quantity: shape.quantity,
|
|
148
|
+
agent: opts.agentId ?? "",
|
|
149
|
+
};
|
|
150
|
+
const file = (0, ledger_1.ledgerPath)();
|
|
151
|
+
const { created } = (0, ledger_1.appendCall)(row, file);
|
|
152
|
+
if (created && !session.ledgerNoticed) {
|
|
153
|
+
session.ledgerNoticed = true;
|
|
154
|
+
stderr(`[agentx] recording wrapped tool calls to ${file} (tool and argument NAMES, never values). Add it to .gitignore if this folder is a repository.`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Wrap one tool. Returns a new object with the same shape; the original is not mutated.
|
|
159
|
+
*/
|
|
160
|
+
function agentxWatch(tool, opts = {}) {
|
|
161
|
+
// Anything without an `execute` function comes back untouched: a null or undefined entry
|
|
162
|
+
// in a tools map, a Vercel AI SDK client-side tool, an untyped caller's object. Manufacturing
|
|
163
|
+
// an `execute` for it would make the SDK run the tool server-side and throw inside this
|
|
164
|
+
// wrapper.
|
|
165
|
+
if (!tool || typeof tool.execute !== "function")
|
|
166
|
+
return tool;
|
|
167
|
+
const original = tool.execute;
|
|
168
|
+
const name = opts.name ?? (typeof tool.name === "string" && tool.name ? tool.name : "(unnamed)");
|
|
169
|
+
const description = typeof tool.description === "string" ? tool.description : undefined;
|
|
170
|
+
const execute = function (...callArgs) {
|
|
171
|
+
try {
|
|
172
|
+
installHooks();
|
|
173
|
+
session.totalCalls += 1;
|
|
174
|
+
session.toolNames.add(name);
|
|
175
|
+
record(name, callArgs[0], description, opts);
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
/* a ledger problem is never a tool problem */
|
|
179
|
+
}
|
|
180
|
+
return original.apply(this, callArgs);
|
|
181
|
+
};
|
|
182
|
+
// The copy keeps the tool's prototype and every own property, so a class-based tool whose
|
|
183
|
+
// `execute` calls `this.query(...)` still finds it: the SDK invokes `wrapped.execute`, so
|
|
184
|
+
// `this` inside the original is this copy.
|
|
185
|
+
const copy = Object.create(Object.getPrototypeOf(tool), Object.getOwnPropertyDescriptors(tool));
|
|
186
|
+
Object.defineProperty(copy, "execute", { value: execute, writable: true, configurable: true, enumerable: true });
|
|
187
|
+
return copy;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Wrap a whole `tools` map, naming each by its key. The Vercel AI SDK shape:
|
|
191
|
+
*
|
|
192
|
+
* tools: agentxWatchAll({ runSql, sendEmail })
|
|
193
|
+
*/
|
|
194
|
+
function agentxWatchAll(tools, opts = {}) {
|
|
195
|
+
const out = {};
|
|
196
|
+
for (const [key, tool] of Object.entries(tools)) {
|
|
197
|
+
out[key] = agentxWatch(tool, { ...opts, name: key });
|
|
198
|
+
}
|
|
199
|
+
return out;
|
|
200
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agentx-core/security-sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "See what your TypeScript AI agent actually did. Wrap a tool in one line; every call is recorded locally (tool name, argument names, never values) and `audit` prints the report. Watches and records; blocks nothing.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "AgentX <founders@agentx-core.com> (https://agentx-core.com)",
|
|
7
|
+
"homepage": "https://agentx-core.com",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"ai",
|
|
10
|
+
"agent",
|
|
11
|
+
"agents",
|
|
12
|
+
"ai-agent",
|
|
13
|
+
"tools",
|
|
14
|
+
"tool-calling",
|
|
15
|
+
"security",
|
|
16
|
+
"audit",
|
|
17
|
+
"observability",
|
|
18
|
+
"vercel-ai-sdk",
|
|
19
|
+
"llm",
|
|
20
|
+
"typescript"
|
|
21
|
+
],
|
|
22
|
+
"bin": {
|
|
23
|
+
"agentx-security-sdk": "dist/cli.js"
|
|
24
|
+
},
|
|
25
|
+
"main": "dist/index.js",
|
|
26
|
+
"types": "dist/index.d.ts",
|
|
27
|
+
"files": [
|
|
28
|
+
"dist"
|
|
29
|
+
],
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsc -p tsconfig.json",
|
|
35
|
+
"test": "vitest run",
|
|
36
|
+
"test:watch": "vitest",
|
|
37
|
+
"prepublishOnly": "npm run build && npm test"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/node": "^20.19.30",
|
|
44
|
+
"typescript": "^5.9.3",
|
|
45
|
+
"vitest": "^3.2.4"
|
|
46
|
+
}
|
|
47
|
+
}
|