@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 ADDED
@@ -0,0 +1,23 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AgentX
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ Contact: founders@agentx-core.com | https://agentx-core.com
package/README.md ADDED
@@ -0,0 +1,139 @@
1
+ # `@agentx-core/security-sdk`
2
+
3
+ **See what your TypeScript AI agent actually did.**
4
+
5
+ Wrap a tool in one line. Every call it makes is written down locally, and one command prints
6
+ the report. It watches and records; it blocks nothing, so it cannot break an agent that works
7
+ today.
8
+
9
+ ```bash
10
+ npm install @agentx-core/security-sdk
11
+ ```
12
+
13
+ ```ts
14
+ import { agentxWatchAll } from "@agentx-core/security-sdk";
15
+
16
+ const result = await generateText({
17
+ model,
18
+ tools: agentxWatchAll({ runSql, sendEmail, writeFile }), // was: tools: { runSql, ... }
19
+ prompt,
20
+ });
21
+ ```
22
+
23
+ Then, from the folder your agent ran in:
24
+
25
+ ```bash
26
+ npx @agentx-core/security-sdk audit
27
+ ```
28
+
29
+ ```
30
+ 🔎 WHAT YOUR AGENT DID (local to this ledger)
31
+ ===========================================================================
32
+ 412 calls across 3 tools since 2026-09-01 09:14
33
+
34
+ TOOL CALLS SURFACE ARGUMENTS
35
+ -----------------------------------------------------------------------
36
+ runSql 388 DB limit, query
37
+ writeFile 19 FS content, path
38
+ sendEmail 5 - body, subject, to
39
+
40
+ A dash under SURFACE means we could not tell what that tool touches.
41
+ We go by tool and argument names, and only match words we are sure of.
42
+
43
+ AgentX watches and writes down what your agent did, and blocks nothing,
44
+ so it cannot break a working agent.
45
+ ▶ To stop the dangerous ones before they run, a gateway:
46
+ https://agentx-core.com/gateway?utm_source=ts-sdk
47
+ ===========================================================================
48
+ ```
49
+
50
+ `audit --calls` lists every call, newest first. `audit --json` is the same data for a program,
51
+ always complete.
52
+
53
+ ## What it writes down, and what it never does
54
+
55
+ Each call becomes one line in `.agentx-calls.jsonl` in the current folder:
56
+
57
+ - the tool's name,
58
+ - the **names** of the arguments it was called with,
59
+ - a size band for an argument the tool itself named as an `amount` (only when a `currency`
60
+ sits beside it) or a `count`: a power of ten, so 1,042.55 is recorded as 1,000,
61
+ - a coarse surface such as DB, FS, HTTP, SHELL or CLOUD, read from the names only,
62
+ - when, and which agent.
63
+
64
+ **Never an argument value, a query, a payload, or a result.** There is no code path from a
65
+ value to that file: the wrapper reduces a call to its shape before anything is written, and
66
+ `test/wrap.test.ts` asserts the file holds no value from a call carrying a SQL statement, an
67
+ email address and a dollar figure. This is exactly what the Python SDK's `agentx audit` keeps.
68
+
69
+ The file is yours. Add it to `.gitignore`; the wrapper reminds you once when it creates it. It
70
+ keeps the last 30 days or 10,000 rows, and when it has trimmed, the report says the counts
71
+ describe what was **kept**.
72
+
73
+ ## Nothing is blocked
74
+
75
+ There are no rules in this package and it makes no decision about a call. Every wrapped call
76
+ runs exactly as it would have, with the same arguments and the same result, and a throw
77
+ propagates unchanged. The record answers "what did my agent do" from the calls themselves.
78
+
79
+ To stop the dangerous ones before they run, add the gateway:
80
+ https://agentx-core.com/gateway?utm_source=ts-sdk
81
+
82
+ ## This package phones home. `@agentx-core/scan` never does.
83
+
84
+ Our two TypeScript packages make different promises:
85
+
86
+ - **`@agentx-core/scan`** reads your code and makes **no network calls, ever**. Its own test
87
+ suite enforces that.
88
+ - **This package** sends one anonymous usage pulse a day: an install id (a random token, not
89
+ derived from you or your machine), the package version, your OS, and **counts** of calls and
90
+ tools. Never tool names, arguments, paths, hostnames or keys. `test/pulse.test.ts` pins the
91
+ field list exactly, and a test on the Python side pins it to the Python SDK's list, so the
92
+ two SDKs cannot drift into sending different things.
93
+
94
+ It tells you this on first run. Turn it off with one line:
95
+
96
+ ```bash
97
+ AGENTX_TELEMETRY=off
98
+ ```
99
+
100
+ Test runners and CI are excluded automatically (`VITEST`, `JEST_WORKER_ID`, `CI`,
101
+ `GITHUB_ACTIONS` and the usual others), so your own test suite never counts as usage.
102
+ `test/network-boundary.test.ts` fails if any file other than the pulse reaches the network.
103
+
104
+ ## Shapes it wraps
105
+
106
+ Anything with an `execute` function: a Vercel AI SDK `tool()`, or your own object.
107
+ `agentxWatch(tool, { name })` wraps one; `agentxWatchAll({ ... })` wraps a map and takes the
108
+ names from its keys. A Vercel `tool()` carries no name of its own (the name is the key in your
109
+ `tools` object), so prefer `agentxWatchAll` there. A tool with no `execute` (a client-side
110
+ tool) is returned unchanged. LangChain.js tools run through `invoke` and are not covered in
111
+ this version.
112
+
113
+ Options: `name`, `agentId` (shown when more than one agent is on the report), and `action`,
114
+ the closed vocabulary the rest of AgentX uses (`execute_database_query`, `fetch_url`,
115
+ `execute_shell`, `send_message`, `write_file`, `other`). `action` fills the SURFACE column only
116
+ when the tool and argument names leave it blank; it never overrides them.
117
+
118
+ Node 18 or newer. The wrapper does nothing on a runtime without a filesystem.
119
+
120
+ ## Usage
121
+
122
+ ```bash
123
+ npx @agentx-core/security-sdk audit # grouped by tool
124
+ npx @agentx-core/security-sdk audit --calls # one line per call, newest first
125
+ npx @agentx-core/security-sdk audit --json # machine-readable, always complete
126
+ npx @agentx-core/security-sdk audit --all # every row on the human screen
127
+ npx @agentx-core/security-sdk audit --limit 10
128
+ ```
129
+
130
+ Errors go to stderr, so `audit --json > out.json` is always a JSON file. An unreadable ledger
131
+ exits 2 and says so; it is never reported as "no calls".
132
+
133
+ ## The Python SDK
134
+
135
+ Same product, other language: `pip install agentx-security-sdk`, `@agentx_protect` over a tool,
136
+ `agentx audit`. The Python one also carries a keyless local shield and can block; this one, in
137
+ this version, watches.
138
+
139
+ MIT licensed.
package/dist/cli.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ interface Io {
3
+ out: (s: string) => void;
4
+ err: (s: string) => void;
5
+ env?: NodeJS.ProcessEnv;
6
+ }
7
+ export declare function main(argv: string[], io?: Io): number;
8
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.main = main;
5
+ /**
6
+ * `npx @agentx-core/security-sdk audit [--calls] [--json] [--limit N | --all]`
7
+ *
8
+ * The reader for the local record `agentxWatch` writes. Flags, errors and stream discipline
9
+ * follow `agentx audit` in agentx_sdk/cli.py: errors go to STDERR (so `--json > out.json` is
10
+ * always a JSON file), `--json` returns before any human line reaches stdout, and an
11
+ * unreadable ledger exits 2 rather than printing "no calls".
12
+ */
13
+ const ledger_1 = require("./ledger");
14
+ const report_1 = require("./report");
15
+ const pulse_1 = require("./pulse");
16
+ const USAGE = `
17
+ 🔎 agentx audit — what your agent actually did, from the local record.
18
+
19
+ ${report_1.AUDIT_COMMAND} grouped by tool
20
+ ${report_1.AUDIT_COMMAND} --calls one line per call, newest first
21
+ --json machine-readable output (always complete)
22
+ --limit N | --all how many rows the human screen shows
23
+ --help this message
24
+
25
+ The record is written by agentxWatch() into .agentx-calls.jsonl in the folder your agent ran
26
+ in; run this from that folder. It holds tool and argument NAMES, never values.
27
+ `;
28
+ const defaultIo = {
29
+ out: (s) => console.log(s),
30
+ err: (s) => console.error(s),
31
+ };
32
+ function main(argv, io = defaultIo) {
33
+ const args = [...argv];
34
+ const wantsJson = args.includes("--json");
35
+ const usageTo = wantsJson ? io.err : io.out;
36
+ if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
37
+ usageTo(USAGE.trim());
38
+ return 0;
39
+ }
40
+ const sub = args.shift();
41
+ if (sub !== "audit") {
42
+ io.err(`\n❌ Unknown command '${sub}'.`);
43
+ io.err(USAGE.trim());
44
+ return 1;
45
+ }
46
+ const opts = { calls: false, json: false };
47
+ const die = (message) => {
48
+ io.err(`\n❌ ${message}`);
49
+ io.err(` Usage: ${report_1.AUDIT_COMMAND} [--calls] [--json] [--limit N | --all]`);
50
+ return 1;
51
+ };
52
+ for (let i = 0; i < args.length; i++) {
53
+ const tok = args[i];
54
+ if (tok === "--calls")
55
+ opts.calls = true;
56
+ else if (tok === "--json")
57
+ opts.json = true;
58
+ else if (tok === "--all")
59
+ opts.all = true;
60
+ else if (tok === "--limit") {
61
+ const raw = args[i + 1];
62
+ if (raw === undefined)
63
+ return die("--limit needs a number.");
64
+ const value = Number(raw);
65
+ if (!Number.isInteger(value))
66
+ return die(`--limit needs a number, not '${raw}'.`);
67
+ if (value < 1)
68
+ return die("--limit must be at least 1.");
69
+ opts.limit = value;
70
+ i += 1;
71
+ }
72
+ else
73
+ return die(`Unknown option '${tok}' for \`${report_1.AUDIT_COMMAND}\`.`);
74
+ }
75
+ if (opts.all && opts.limit !== undefined)
76
+ return die("--all and --limit ask for different things; pass one.");
77
+ const env = io.env ?? process.env;
78
+ const file = (0, ledger_1.ledgerPath)(env);
79
+ (0, ledger_1.trimLedger)(file);
80
+ const read = (0, ledger_1.readLedger)(file);
81
+ if (opts.json) {
82
+ io.out(JSON.stringify((0, report_1.jsonPayload)(read, opts.calls ? "calls" : "tools", opts), null, 2));
83
+ return read.readable ? 0 : 2;
84
+ }
85
+ // A human looked. The rung this package exists to make visible, and the one thing `--json`
86
+ // must never climb: a cron job polling it is not a person.
87
+ (0, pulse_1.markAuditReportRun)(undefined, env);
88
+ if (!read.readable) {
89
+ io.err("\n❌ The ledger is on disk but could not be read, so this is not a statement that");
90
+ io.err(` your agent did nothing. ${file}`);
91
+ return 2;
92
+ }
93
+ io.out(opts.calls ? (0, report_1.renderCalls)(read, opts) : (0, report_1.renderTools)(read, opts));
94
+ return 0;
95
+ }
96
+ // Only auto-run as the real entry point (the published `bin`), never when a test imports `main`.
97
+ if (require.main === module && !process.env.VITEST) {
98
+ process.exitCode = main(process.argv.slice(2));
99
+ }
@@ -0,0 +1,3 @@
1
+ export { agentxWatch, agentxWatchAll } from "./wrap";
2
+ export type { WatchableTool, WatchOptions } from "./wrap";
3
+ export type { Action, TargetClass } from "./shape";
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.agentxWatchAll = exports.agentxWatch = void 0;
4
+ var wrap_1 = require("./wrap");
5
+ Object.defineProperty(exports, "agentxWatch", { enumerable: true, get: function () { return wrap_1.agentxWatch; } });
6
+ Object.defineProperty(exports, "agentxWatchAll", { enumerable: true, get: function () { return wrap_1.agentxWatchAll; } });
@@ -0,0 +1,46 @@
1
+ import type { TargetClass } from "./shape";
2
+ export declare const LEDGER_FILE = ".agentx-calls.jsonl";
3
+ export declare const RETENTION_DAYS = 30;
4
+ export declare const RETENTION_MAX_ROWS = 10000;
5
+ export interface CallRow {
6
+ k: "call";
7
+ /** Epoch milliseconds. */
8
+ ts: number;
9
+ tool: string;
10
+ args: string[];
11
+ surface: TargetClass;
12
+ amount: number;
13
+ quantity: number;
14
+ agent: string;
15
+ }
16
+ /** Where the ledger lives. `AGENTX_TS_LEDGER_PATH` moves it (tests, and a shared folder). */
17
+ export declare function ledgerPath(env?: NodeJS.ProcessEnv): string;
18
+ /** Append one call. Returns whether this write CREATED the file (the first-run notice keys on it). */
19
+ export declare function appendCall(row: CallRow, file?: string): {
20
+ created: boolean;
21
+ };
22
+ export interface LedgerRead {
23
+ path: string;
24
+ exists: boolean;
25
+ /** False when the file is there and could not be read. Never confused with empty. */
26
+ readable: boolean;
27
+ /** Oldest first. */
28
+ rows: CallRow[];
29
+ /** Cumulative rows dropped by retention. 0 means the rows are everything ever recorded. */
30
+ dropped: number;
31
+ coversAll: boolean;
32
+ /** Epoch ms of the oldest kept row, or null. */
33
+ windowStart: number | null;
34
+ /** Bytes the read covered, and whether they ended on a line break. trimLedger's carry-over keys on both. */
35
+ bytes: number;
36
+ cleanEnd: boolean;
37
+ }
38
+ export declare function readLedger(file?: string): LedgerRead;
39
+ /**
40
+ * Apply retention: drop rows older than RETENTION_DAYS, then the oldest past RETENTION_MAX_ROWS.
41
+ * Rewrites atomically (temp file + rename) only when something was dropped, and records the
42
+ * cumulative drop count so `readLedger` can say the ledger was trimmed. Never throws.
43
+ */
44
+ export declare function trimLedger(file?: string, now?: number, maxAgeDays?: number, maxRows?: number): {
45
+ dropped: number;
46
+ };
package/dist/ledger.js ADDED
@@ -0,0 +1,183 @@
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.RETENTION_MAX_ROWS = exports.RETENTION_DAYS = exports.LEDGER_FILE = void 0;
37
+ exports.ledgerPath = ledgerPath;
38
+ exports.appendCall = appendCall;
39
+ exports.readLedger = readLedger;
40
+ exports.trimLedger = trimLedger;
41
+ /**
42
+ * The local record: one JSON line per wrapped call, in the current directory.
43
+ *
44
+ * Kept per FOLDER, like the Python SDK's `.agentx.db`, so an agent run in ~/proj and an
45
+ * `audit` run from ~ read two different files. The screens name the path for exactly that
46
+ * reason. Retention is the Python SDK's: the last 30 days or 10,000 rows, and a trim is
47
+ * written down (a `trim` line carrying the cumulative count) so a reader can say "what was
48
+ * KEPT" instead of "everything your agent has ever done".
49
+ *
50
+ * Rows hold what `shape.ts` produced and nothing else. There is no code path from a raw
51
+ * argument value to this file; `wrap.ts` reduces before it records.
52
+ */
53
+ const fs = __importStar(require("fs"));
54
+ const path = __importStar(require("path"));
55
+ exports.LEDGER_FILE = ".agentx-calls.jsonl";
56
+ exports.RETENTION_DAYS = 30;
57
+ exports.RETENTION_MAX_ROWS = 10000;
58
+ /** Where the ledger lives. `AGENTX_TS_LEDGER_PATH` moves it (tests, and a shared folder). */
59
+ function ledgerPath(env = process.env) {
60
+ return path.resolve(env.AGENTX_TS_LEDGER_PATH || exports.LEDGER_FILE);
61
+ }
62
+ /** Append one call. Returns whether this write CREATED the file (the first-run notice keys on it). */
63
+ function appendCall(row, file = ledgerPath()) {
64
+ const created = !fs.existsSync(file);
65
+ fs.appendFileSync(file, JSON.stringify(row) + "\n", "utf8");
66
+ return { created };
67
+ }
68
+ function parseLines(text) {
69
+ const rows = [];
70
+ let dropped = 0;
71
+ for (const line of text.split("\n")) {
72
+ if (!line.trim())
73
+ continue;
74
+ let parsed;
75
+ try {
76
+ parsed = JSON.parse(line);
77
+ }
78
+ catch {
79
+ continue; // a torn line from a crash mid-write is skipped, not fatal
80
+ }
81
+ const r = parsed;
82
+ if (r && r.k === "trim" && typeof r.dropped === "number") {
83
+ dropped = Math.max(dropped, r.dropped);
84
+ }
85
+ else if (r && r.k === "call" && typeof r.tool === "string" && typeof r.ts === "number") {
86
+ rows.push({
87
+ k: "call",
88
+ ts: r.ts,
89
+ tool: r.tool,
90
+ args: Array.isArray(r.args) ? r.args.map(String) : [],
91
+ surface: r.surface || "other",
92
+ amount: typeof r.amount === "number" ? r.amount : 0,
93
+ quantity: typeof r.quantity === "number" ? r.quantity : 0,
94
+ agent: typeof r.agent === "string" ? r.agent : "",
95
+ });
96
+ }
97
+ }
98
+ return { rows, dropped };
99
+ }
100
+ function readLedger(file = ledgerPath()) {
101
+ const base = {
102
+ path: file, exists: false, readable: true, rows: [], dropped: 0, coversAll: true, windowStart: null,
103
+ bytes: 0, cleanEnd: true,
104
+ };
105
+ if (!fs.existsSync(file))
106
+ return base;
107
+ base.exists = true;
108
+ let buf;
109
+ try {
110
+ buf = fs.readFileSync(file);
111
+ }
112
+ catch {
113
+ // UNREADABLE IS NOT EMPTY. A reader that renders this as "no calls" makes a false
114
+ // statement about the developer's own data.
115
+ return { ...base, readable: false };
116
+ }
117
+ const { rows, dropped } = parseLines(buf.toString("utf8"));
118
+ rows.sort((a, b) => a.ts - b.ts);
119
+ return {
120
+ ...base,
121
+ rows,
122
+ dropped,
123
+ coversAll: dropped === 0,
124
+ windowStart: rows.length ? rows[0].ts : null,
125
+ bytes: buf.length,
126
+ cleanEnd: buf.length === 0 || buf[buf.length - 1] === 0x0a,
127
+ };
128
+ }
129
+ /**
130
+ * Apply retention: drop rows older than RETENTION_DAYS, then the oldest past RETENTION_MAX_ROWS.
131
+ * Rewrites atomically (temp file + rename) only when something was dropped, and records the
132
+ * cumulative drop count so `readLedger` can say the ledger was trimmed. Never throws.
133
+ */
134
+ function trimLedger(file = ledgerPath(), now = Date.now(), maxAgeDays = exports.RETENTION_DAYS, maxRows = exports.RETENTION_MAX_ROWS) {
135
+ const tmp = `${file}.${process.pid}.tmp`;
136
+ try {
137
+ const read = readLedger(file);
138
+ if (!read.exists || !read.readable)
139
+ return { dropped: 0 };
140
+ const cutoff = now - maxAgeDays * 86400 * 1000;
141
+ let kept = read.rows.filter((r) => r.ts >= cutoff);
142
+ if (kept.length > maxRows)
143
+ kept = kept.slice(kept.length - maxRows);
144
+ const droppedNow = read.rows.length - kept.length;
145
+ if (droppedNow === 0)
146
+ return { dropped: 0 };
147
+ const trim = { k: "trim", ts: now, dropped: read.dropped + droppedNow };
148
+ const lines = [JSON.stringify(trim), ...kept.map((r) => JSON.stringify(r))].join("\n") + "\n";
149
+ fs.writeFileSync(tmp, lines, "utf8");
150
+ // Other processes write this file too: an agent appending in another terminal, or a second
151
+ // exiting agent running its own trim. Rows appended after the read (the file grew, and what
152
+ // was read ended on a line break) are copied onto the end of the rewrite. If the file
153
+ // shrank, someone else rewrote it and this pass no longer describes it; the rewrite is
154
+ // abandoned rather than renamed over a file this process did not read.
155
+ const nowBytes = fs.statSync(file).size;
156
+ if (nowBytes < read.bytes)
157
+ throw new Error("ledger rewritten during trim");
158
+ if (nowBytes > read.bytes) {
159
+ if (!read.cleanEnd)
160
+ throw new Error("ledger tail is mid-line; skip this trim");
161
+ const fd = fs.openSync(file, "r");
162
+ try {
163
+ const tail = Buffer.alloc(nowBytes - read.bytes);
164
+ const got = fs.readSync(fd, tail, 0, tail.length, read.bytes);
165
+ fs.appendFileSync(tmp, tail.subarray(0, got));
166
+ }
167
+ finally {
168
+ fs.closeSync(fd);
169
+ }
170
+ }
171
+ fs.renameSync(tmp, file);
172
+ return { dropped: droppedNow };
173
+ }
174
+ catch {
175
+ try {
176
+ fs.unlinkSync(tmp);
177
+ }
178
+ catch {
179
+ /* nothing to remove */
180
+ }
181
+ return { dropped: 0 };
182
+ }
183
+ }
@@ -0,0 +1,102 @@
1
+ export declare const INTEGRATION = "ts";
2
+ export declare const DEFAULT_ENDPOINT = "https://www.agentx-core.com/api/pulse";
3
+ export declare const DEBOUNCE_MS: number;
4
+ export declare const TIMEOUT_MS = 1000;
5
+ /** The complete set of top-level keys that may leave the machine. KEEP IN SYNC with agentx_sdk/pulse.py _ALLOWED_KEYS. */
6
+ export declare const ALLOWED_KEYS: readonly ["install_id", "sdk_version", "python", "os", "first_seen", "ts", "mode", "gateway_present", "reasoning_enabled", "contributed", "block_category", "integration", "session", "ran_audit_report"];
7
+ /** The session COUNT keys. KEEP IN SYNC with agentx_sdk/pulse.py _ALLOWED_SESSION_KEYS. */
8
+ export declare const ALLOWED_SESSION_KEYS: readonly ["tools_monitored", "intercepts", "critical_blocks", "human_escalations", "self_corrections", "would_blocks", "had_block", "first_block_ever", "shield_failopens", "own_agent_block", "audit_calls", "audit_tools"];
9
+ export interface SessionStats {
10
+ /** Every wrapped call this session. */
11
+ totalCalls: number;
12
+ /** Distinct wrapped tools this session. A count; the names stay on disk. */
13
+ distinctTools: number;
14
+ }
15
+ export interface PulseState {
16
+ install_id?: string;
17
+ first_seen?: string;
18
+ last_pulse?: number;
19
+ notice_shown?: boolean;
20
+ ran_audit_report?: boolean;
21
+ /** Counts from a run that ended through process.exit() before the pulse could be sent. */
22
+ pending?: SessionStats;
23
+ }
24
+ type Env = NodeJS.ProcessEnv;
25
+ export declare function stateFile(): string;
26
+ /** The machine-level "this machine is ours" marker the Python SDK writes. READ here, never written. */
27
+ export declare function internalMarker(): string;
28
+ export declare function loadState(file?: string): PulseState;
29
+ export declare function saveState(state: PulseState, file?: string): void;
30
+ /** True unless the developer explicitly opted out. `AGENTX_TELEMETRY` always wins when present. */
31
+ export declare function telemetryEnabled(env?: Env): boolean;
32
+ /** True when telemetry is on purely by default, i.e. nobody set AGENTX_TELEMETRY. Only these installs get the notice. */
33
+ export declare function isDefaultOn(env?: Env): boolean;
34
+ /**
35
+ * True when this run is a test/CI invocation, an explicitly flagged dev environment
36
+ * (`AGENTX_ENV=development|dev|test`), or a machine the Python SDK has marked as ours. Such
37
+ * runs send nothing and climb no rung. Never throws.
38
+ *
39
+ * Unlike the Python side, this never WRITES the machine marker: a wrong write deletes a real
40
+ * user from the funnel permanently, and the Python SDK already owns that decision.
41
+ */
42
+ export declare function isAutomationContext(env?: Env, markerExists?: () => boolean): boolean;
43
+ /** The coarse data-plane mode, resolved the way the Python SDK resolves it. */
44
+ export declare function mode(env?: Env): "local" | "linked" | "cloud";
45
+ /** Where the pulse goes: the configured control plane's /api/pulse, else the public default. */
46
+ export declare function endpoint(env?: Env): string;
47
+ /** This build's version, from package.json. "unknown" rather than a guess. */
48
+ export declare function sdkVersion(): string;
49
+ /** The Python SDK sends `platform.system().lower()`; map Node's names onto the same words. */
50
+ export declare function osName(platform?: string): string;
51
+ /**
52
+ * Assemble the pulse. ALLOWLIST ONLY: it reads two counters off `stats` and never a name.
53
+ * Every key in ALLOWED_KEYS is present, so the receiver sees one shape from every SDK; the
54
+ * fields this door has no way to observe are null or false, never omitted.
55
+ */
56
+ export declare function buildPayload(stats: SessionStats, state: PulseState, opts?: {
57
+ now?: Date;
58
+ version?: string;
59
+ platform?: string;
60
+ env?: Env;
61
+ }): Record<string, unknown>;
62
+ /** Pure debounce decision. */
63
+ export declare function shouldSend(state: PulseState, nowMs: number): boolean;
64
+ /** Fire the pulse. Swallows every error and is bounded by TIMEOUT_MS. */
65
+ export declare function post(url: string, payload: Record<string, unknown>, timeoutMs?: number): Promise<void>;
66
+ /**
67
+ * Send one pulse if telemetry is on and the day's window is open. Persists BEFORE dispatch so
68
+ * a crash mid-send cannot double-count tomorrow. Returns the payload sent, or null.
69
+ */
70
+ export declare function maybeSend(stats: SessionStats, state: PulseState, file?: string, opts?: {
71
+ now?: number;
72
+ env?: Env;
73
+ url?: string;
74
+ }): Promise<Record<string, unknown> | null>;
75
+ /** The one-time transparency notice, printed BEFORE the first pulse leaves. To stderr: this runs inside someone else's program. */
76
+ export declare function showNotice(state: PulseState, file?: string, write?: (s: string) => void): void;
77
+ /**
78
+ * The session-end entry point. Fires on every run that wrapped a call. Merges in counts a
79
+ * previous run queued (see queuePending) so a script that always ends with process.exit()
80
+ * still reports, one run late rather than never.
81
+ */
82
+ export interface SessionEndDeps {
83
+ env?: Env;
84
+ url?: string;
85
+ now?: number;
86
+ /** Injectable so a test can drive this path on a machine the Python SDK has marked as ours. */
87
+ markerExists?: () => boolean;
88
+ notice?: (s: string) => void;
89
+ }
90
+ export declare function onSessionEnd(stats: SessionStats, file?: string, deps?: SessionEndDeps): Promise<void>;
91
+ /**
92
+ * The synchronous fallback for `process.on("exit")`, where nothing async can run: write the
93
+ * counts down so the NEXT run sends them. Never throws.
94
+ */
95
+ export declare function queuePending(stats: SessionStats, file?: string, deps?: SessionEndDeps): void;
96
+ /**
97
+ * A human ran `audit`. STICKY, and excluded from automation for the same reason the Python
98
+ * side is: this is the conversion event the funnel is built around and it cannot be
99
+ * un-climbed, so one CI job must not mark an install converted forever.
100
+ */
101
+ export declare function markAuditReportRun(file?: string, env?: Env): void;
102
+ export {};