@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/dist/pulse.js ADDED
@@ -0,0 +1,401 @@
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.ALLOWED_SESSION_KEYS = exports.ALLOWED_KEYS = exports.TIMEOUT_MS = exports.DEBOUNCE_MS = exports.DEFAULT_ENDPOINT = exports.INTEGRATION = void 0;
37
+ exports.stateFile = stateFile;
38
+ exports.internalMarker = internalMarker;
39
+ exports.loadState = loadState;
40
+ exports.saveState = saveState;
41
+ exports.telemetryEnabled = telemetryEnabled;
42
+ exports.isDefaultOn = isDefaultOn;
43
+ exports.isAutomationContext = isAutomationContext;
44
+ exports.mode = mode;
45
+ exports.endpoint = endpoint;
46
+ exports.sdkVersion = sdkVersion;
47
+ exports.osName = osName;
48
+ exports.buildPayload = buildPayload;
49
+ exports.shouldSend = shouldSend;
50
+ exports.post = post;
51
+ exports.maybeSend = maybeSend;
52
+ exports.showNotice = showNotice;
53
+ exports.onSessionEnd = onSessionEnd;
54
+ exports.queuePending = queuePending;
55
+ exports.markAuditReportRun = markAuditReportRun;
56
+ /**
57
+ * Anonymous usage pulse. ON by default, one-line opt-out (`AGENTX_TELEMETRY=off`).
58
+ *
59
+ * The same pulse the Python SDK sends, to the same receiver (`/api/pulse` on the control
60
+ * plane), with the same field list. `integration` is `"ts"`, which is how the funnel tells
61
+ * this package's installs from the Python decorator's and the MCP proxy's.
62
+ *
63
+ * Hard rules, asserted by test/pulse.test.ts and pinned against the Python allowlist by
64
+ * sdk_tests/test_ts_sdk_matches_python.py:
65
+ * * Never sends arguments, tool names, paths, hostnames, usernames or keys. COUNTS only.
66
+ * * Only the keys in ALLOWED_KEYS / ALLOWED_SESSION_KEYS ever leave, and exactly those.
67
+ * * Fire-and-forget: never throws, bounded by a 1s timeout, fails open offline.
68
+ * * Test and CI runs are excluded entirely, even when on: automation is not adoption.
69
+ *
70
+ * One difference from the Python pulse, stated so the two doors' columns are read right: the
71
+ * session counts here are the SUM of every session since the last pulse (a run that ends
72
+ * through process.exit() queues its counts; a run inside the day's debounce keeps them
73
+ * queued), so one pulse can carry a day's worth of sessions. The Python SDK sends one
74
+ * session's counts and drops a debounced session's.
75
+ *
76
+ * THIS IS THE ONLY FILE IN THE PACKAGE THAT REACHES THE NETWORK, and test/network-boundary.test.ts
77
+ * fails if a second one appears. `@agentx-core/scan` never phones home at all; this package
78
+ * does, once a day, with counts. Two packages, two promises, and each README says which.
79
+ */
80
+ const fs = __importStar(require("fs"));
81
+ const os = __importStar(require("os"));
82
+ const path = __importStar(require("path"));
83
+ const crypto_1 = require("crypto");
84
+ exports.INTEGRATION = "ts";
85
+ // The receiver: the control plane's POST /api/pulse. MUST be the canonical www host: the
86
+ // bare apex answers a 307 to www, and a POST that is not re-sent on redirect is a pulse
87
+ // silently dropped.
88
+ exports.DEFAULT_ENDPOINT = "https://www.agentx-core.com/api/pulse";
89
+ exports.DEBOUNCE_MS = 24 * 60 * 60 * 1000; // at most one pulse per install per day
90
+ exports.TIMEOUT_MS = 1000;
91
+ /** The complete set of top-level keys that may leave the machine. KEEP IN SYNC with agentx_sdk/pulse.py _ALLOWED_KEYS. */
92
+ exports.ALLOWED_KEYS = [
93
+ "install_id", "sdk_version", "python", "os", "first_seen", "ts",
94
+ "mode", "gateway_present", "reasoning_enabled", "contributed",
95
+ "block_category", "integration", "session", "ran_audit_report",
96
+ ];
97
+ /** The session COUNT keys. KEEP IN SYNC with agentx_sdk/pulse.py _ALLOWED_SESSION_KEYS. */
98
+ exports.ALLOWED_SESSION_KEYS = [
99
+ "tools_monitored", "intercepts", "critical_blocks",
100
+ "human_escalations", "self_corrections", "would_blocks",
101
+ "had_block", "first_block_ever", "shield_failopens",
102
+ "own_agent_block", "audit_calls", "audit_tools",
103
+ ];
104
+ // A separate file from the Python SDK's pulse.json: two SDKs on one machine are two installs,
105
+ // and sharing a file would couple this writer to that one's atomic-write format.
106
+ function stateFile() {
107
+ return path.join(os.homedir(), ".agentx", "ts-pulse.json");
108
+ }
109
+ /** The machine-level "this machine is ours" marker the Python SDK writes. READ here, never written. */
110
+ function internalMarker() {
111
+ return path.join(os.homedir(), ".agentx", "internal");
112
+ }
113
+ function loadState(file = stateFile()) {
114
+ try {
115
+ const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
116
+ return parsed && typeof parsed === "object" ? parsed : {};
117
+ }
118
+ catch {
119
+ return {};
120
+ }
121
+ }
122
+ function saveState(state, file = stateFile()) {
123
+ try {
124
+ fs.mkdirSync(path.dirname(file), { recursive: true });
125
+ const tmp = `${file}.${process.pid}.tmp`;
126
+ fs.writeFileSync(tmp, JSON.stringify(state), "utf8");
127
+ fs.renameSync(tmp, file);
128
+ }
129
+ catch {
130
+ /* telemetry must never break a run */
131
+ }
132
+ }
133
+ function ensureIdentity(state) {
134
+ if (!state.install_id)
135
+ state.install_id = (0, crypto_1.randomUUID)();
136
+ if (!state.first_seen)
137
+ state.first_seen = new Date().toISOString().slice(0, 10);
138
+ return state;
139
+ }
140
+ function truthy(v) {
141
+ return ["1", "true", "on", "yes"].includes(String(v ?? "").trim().toLowerCase());
142
+ }
143
+ /** True unless the developer explicitly opted out. `AGENTX_TELEMETRY` always wins when present. */
144
+ function telemetryEnabled(env = process.env) {
145
+ const explicit = env.AGENTX_TELEMETRY;
146
+ if (explicit !== undefined && explicit !== "")
147
+ return truthy(explicit);
148
+ return true;
149
+ }
150
+ /** True when telemetry is on purely by default, i.e. nobody set AGENTX_TELEMETRY. Only these installs get the notice. */
151
+ function isDefaultOn(env = process.env) {
152
+ return env.AGENTX_TELEMETRY === undefined || env.AGENTX_TELEMETRY === "";
153
+ }
154
+ const CI_ENV_VARS = [
155
+ "CI", "CONTINUOUS_INTEGRATION", "GITHUB_ACTIONS", "GITLAB_CI", "JENKINS_URL",
156
+ "CIRCLECI", "TRAVIS", "BUILDKITE", "TF_BUILD", "TEAMCITY_VERSION",
157
+ "BITBUCKET_BUILD_NUMBER", "APPVEYOR", "DRONE", "CODEBUILD_BUILD_ID",
158
+ ];
159
+ // Test runners set these on every worker. A developer running THEIR suite over an agent that
160
+ // imports us is exactly the run that must not count as adoption.
161
+ const TEST_RUNNER_VARS = ["VITEST", "JEST_WORKER_ID", "NODE_TEST_CONTEXT"];
162
+ /**
163
+ * True when this run is a test/CI invocation, an explicitly flagged dev environment
164
+ * (`AGENTX_ENV=development|dev|test`), or a machine the Python SDK has marked as ours. Such
165
+ * runs send nothing and climb no rung. Never throws.
166
+ *
167
+ * Unlike the Python side, this never WRITES the machine marker: a wrong write deletes a real
168
+ * user from the funnel permanently, and the Python SDK already owns that decision.
169
+ */
170
+ function isAutomationContext(env = process.env, markerExists = () => fs.existsSync(internalMarker())) {
171
+ try {
172
+ if (["development", "dev", "test"].includes(String(env.AGENTX_ENV ?? "").trim().toLowerCase()))
173
+ return true;
174
+ if (CI_ENV_VARS.some((v) => env[v]))
175
+ return true;
176
+ if (TEST_RUNNER_VARS.some((v) => env[v]))
177
+ return true;
178
+ return markerExists();
179
+ }
180
+ catch {
181
+ return false;
182
+ }
183
+ }
184
+ /** The coarse data-plane mode, resolved the way the Python SDK resolves it. */
185
+ function mode(env = process.env) {
186
+ const m = String(env.AGENTX_MODE ?? "").trim().toLowerCase();
187
+ if (m === "local" || m === "linked" || m === "cloud")
188
+ return m;
189
+ if (truthy(env.AGENTX_ALLOW_PAYLOAD_SYNC))
190
+ return "cloud";
191
+ return String(env.CONTROL_PLANE_URL ?? "").trim() ? "linked" : "local";
192
+ }
193
+ function isHttp(url) {
194
+ return /^https?:\/\//i.test(url);
195
+ }
196
+ /** Where the pulse goes: the configured control plane's /api/pulse, else the public default. */
197
+ function endpoint(env = process.env) {
198
+ const plane = String(env.CONTROL_PLANE_URL ?? "").trim();
199
+ if (isHttp(plane)) {
200
+ const base = plane.replace(/\/+$/, "");
201
+ return base.endsWith("/api/pulse") ? base : base + "/api/pulse";
202
+ }
203
+ return exports.DEFAULT_ENDPOINT;
204
+ }
205
+ /** This build's version, from package.json. "unknown" rather than a guess. */
206
+ function sdkVersion() {
207
+ try {
208
+ const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8"));
209
+ return typeof pkg.version === "string" ? pkg.version : "unknown";
210
+ }
211
+ catch {
212
+ return "unknown";
213
+ }
214
+ }
215
+ /** The Python SDK sends `platform.system().lower()`; map Node's names onto the same words. */
216
+ function osName(platform = process.platform) {
217
+ if (platform === "win32")
218
+ return "windows";
219
+ return platform || "unknown";
220
+ }
221
+ /**
222
+ * Assemble the pulse. ALLOWLIST ONLY: it reads two counters off `stats` and never a name.
223
+ * Every key in ALLOWED_KEYS is present, so the receiver sees one shape from every SDK; the
224
+ * fields this door has no way to observe are null or false, never omitted.
225
+ */
226
+ function buildPayload(stats, state, opts = {}) {
227
+ const env = opts.env ?? process.env;
228
+ const n = (v) => (typeof v === "number" && Number.isFinite(v) ? Math.max(0, Math.trunc(v)) : 0);
229
+ return {
230
+ install_id: state.install_id,
231
+ // The bare package version. The receiver stores every SDK's version in one column beside
232
+ // `integration`, and scripts/funnel.py keys its version bar on the pair, so "0.1.0" here
233
+ // and "0.1.0" from the Python SDK never share a bucket.
234
+ sdk_version: opts.version ?? sdkVersion(),
235
+ // A Node runtime has no Python version. Null, not a Node version in a column named
236
+ // python: the receiver's column carries a meaning and this door cannot supply it.
237
+ python: null,
238
+ os: osName(opts.platform),
239
+ first_seen: state.first_seen,
240
+ ts: (opts.now ?? new Date()).toISOString(),
241
+ mode: mode(env),
242
+ // No gateway leg on this door, so these are the honest constants. Not omitted: absent
243
+ // reads as "pre-signal" on the receiver, and this install is not pre-anything.
244
+ gateway_present: false,
245
+ reasoning_enabled: null,
246
+ contributed: false,
247
+ block_category: null,
248
+ integration: exports.INTEGRATION,
249
+ session: {
250
+ tools_monitored: n(stats.totalCalls),
251
+ intercepts: 0,
252
+ critical_blocks: 0,
253
+ human_escalations: 0,
254
+ self_corrections: 0,
255
+ would_blocks: 0,
256
+ had_block: false,
257
+ first_block_ever: false,
258
+ shield_failopens: 0,
259
+ own_agent_block: false,
260
+ // The audit rung, same meaning as the Python counters: calls the record saw and
261
+ // DISTINCT tools it saw. Counts; the names never leave.
262
+ audit_calls: n(stats.totalCalls),
263
+ audit_tools: n(stats.distinctTools),
264
+ },
265
+ ran_audit_report: Boolean(state.ran_audit_report),
266
+ };
267
+ }
268
+ /** Pure debounce decision. */
269
+ function shouldSend(state, nowMs) {
270
+ const last = state.last_pulse ?? 0;
271
+ return nowMs - last >= exports.DEBOUNCE_MS;
272
+ }
273
+ /** Fire the pulse. Swallows every error and is bounded by TIMEOUT_MS. */
274
+ async function post(url, payload, timeoutMs = exports.TIMEOUT_MS) {
275
+ const controller = new AbortController();
276
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
277
+ try {
278
+ await fetch(url, {
279
+ method: "POST",
280
+ headers: {
281
+ "Content-Type": "application/json",
282
+ "User-Agent": `agentx-ts-sdk-pulse/${String(payload.sdk_version ?? "?")}`,
283
+ },
284
+ body: JSON.stringify(payload),
285
+ signal: controller.signal,
286
+ });
287
+ }
288
+ catch {
289
+ /* offline, blocked, timed out: all fine */
290
+ }
291
+ finally {
292
+ clearTimeout(timer);
293
+ }
294
+ }
295
+ /**
296
+ * Send one pulse if telemetry is on and the day's window is open. Persists BEFORE dispatch so
297
+ * a crash mid-send cannot double-count tomorrow. Returns the payload sent, or null.
298
+ */
299
+ async function maybeSend(stats, state, file = stateFile(), opts = {}) {
300
+ try {
301
+ const env = opts.env ?? process.env;
302
+ if (!telemetryEnabled(env))
303
+ return null;
304
+ ensureIdentity(state);
305
+ const now = opts.now ?? Date.now();
306
+ if (!shouldSend(state, now)) {
307
+ saveState(state, file); // persist identity even when we skip
308
+ return null;
309
+ }
310
+ state.last_pulse = now;
311
+ // A send is being attempted, so the queued counts are spent here whether or not the
312
+ // network answers: post() swallows failure and the day's debounce stands either way.
313
+ delete state.pending;
314
+ saveState(state, file);
315
+ const payload = buildPayload(stats, state, { now: new Date(now), env });
316
+ await post(opts.url ?? endpoint(env), payload);
317
+ return payload;
318
+ }
319
+ catch {
320
+ return null;
321
+ }
322
+ }
323
+ /** The one-time transparency notice, printed BEFORE the first pulse leaves. To stderr: this runs inside someone else's program. */
324
+ function showNotice(state, file = stateFile(), write = (s) => process.stderr.write(s)) {
325
+ try {
326
+ const rule = "─".repeat(60);
327
+ write(`\n${rule}\n` +
328
+ " AgentX shares ANONYMOUS usage by default: SDK version, OS, and call\n" +
329
+ " COUNTS only. Never your code, your arguments, or your data.\n" +
330
+ " Opt out anytime: AGENTX_TELEMETRY=off\n" +
331
+ " Questions or feedback: founders@agentx-core.com\n" +
332
+ `${rule}\n`);
333
+ state.notice_shown = true;
334
+ ensureIdentity(state);
335
+ saveState(state, file);
336
+ }
337
+ catch {
338
+ /* never break a run */
339
+ }
340
+ }
341
+ function sum(a, b) {
342
+ if (!a)
343
+ return { ...b };
344
+ return { totalCalls: a.totalCalls + b.totalCalls, distinctTools: Math.max(a.distinctTools, b.distinctTools) };
345
+ }
346
+ async function onSessionEnd(stats, file = stateFile(), deps = {}) {
347
+ try {
348
+ const env = deps.env ?? process.env;
349
+ if (!telemetryEnabled(env))
350
+ return;
351
+ if (isAutomationContext(env, deps.markerExists))
352
+ return;
353
+ const state = loadState(file);
354
+ // Everything not yet carried by a pulse is `pending`, this session included. maybeSend
355
+ // clears it on its send path only, right before its own write, so each path writes the
356
+ // state file once and the file never holds a version with the counts missing.
357
+ state.pending = sum(state.pending, stats);
358
+ if (isDefaultOn(env) && !state.notice_shown)
359
+ showNotice(state, file, deps.notice);
360
+ await maybeSend(state.pending, state, file, { env, url: deps.url, now: deps.now });
361
+ }
362
+ catch {
363
+ /* never break a run */
364
+ }
365
+ }
366
+ /**
367
+ * The synchronous fallback for `process.on("exit")`, where nothing async can run: write the
368
+ * counts down so the NEXT run sends them. Never throws.
369
+ */
370
+ function queuePending(stats, file = stateFile(), deps = {}) {
371
+ try {
372
+ const env = deps.env ?? process.env;
373
+ if (!telemetryEnabled(env))
374
+ return;
375
+ if (isAutomationContext(env, deps.markerExists))
376
+ return;
377
+ const state = loadState(file);
378
+ state.pending = sum(state.pending, stats);
379
+ saveState(state, file);
380
+ }
381
+ catch {
382
+ /* never break a run */
383
+ }
384
+ }
385
+ /**
386
+ * A human ran `audit`. STICKY, and excluded from automation for the same reason the Python
387
+ * side is: this is the conversion event the funnel is built around and it cannot be
388
+ * un-climbed, so one CI job must not mark an install converted forever.
389
+ */
390
+ function markAuditReportRun(file = stateFile(), env = process.env) {
391
+ try {
392
+ if (isAutomationContext(env))
393
+ return;
394
+ const state = ensureIdentity(loadState(file));
395
+ state.ran_audit_report = true;
396
+ saveState(state, file);
397
+ }
398
+ catch {
399
+ /* a reader command must never fail on bookkeeping */
400
+ }
401
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * The `audit` screens. The design is `agentx audit` in agentx_sdk/cli.py, copied rather than
3
+ * reinvented: the same header, the same TOOL / CALLS / SURFACE / ARGUMENTS table, the same
4
+ * "what was KEPT" disclosure when retention has trimmed the file, and the same rule about
5
+ * naming the ledger path on an empty screen (the likeliest reason a screen is empty is the
6
+ * reader standing in a different folder).
7
+ *
8
+ * Every line keeps to 75 columns, with one documented exception carried over from the Python
9
+ * screen: a single argument name longer than the ARGUMENTS budget is kept whole up to
10
+ * MAX_WHOLE_NAME (40) characters, because a reader greps for a name and cannot grep half of
11
+ * one. So a --calls row can reach 48 + 40 columns plus a " +N more" suffix (96 with one name
12
+ * dropped), and a grouped row 46 + 40 plus the same suffix.
13
+ */
14
+ import { type TargetClass } from "./shape";
15
+ import { type CallRow, type LedgerRead } from "./ledger";
16
+ /** The wire-format version of `--json`. Its own namespace: this package versions independently of the Python `agentx.audit/1`. */
17
+ export declare const AUDIT_SCHEMA = "agentx.ts-audit/1";
18
+ /**
19
+ * The call to action, with its OWN tag. The two TypeScript calls to action we can already
20
+ * measure (scan's, and the /docs handoff) both read about one click each, so this one carries
21
+ * `utm_source=ts-sdk` rather than borrowing either, and the /gateway view beacon can finally
22
+ * say which of the three, if any, moves anyone.
23
+ */
24
+ export declare const GATEWAY_URL = "https://agentx-core.com/gateway?utm_source=ts-sdk";
25
+ export declare const PACKAGE = "@agentx-core/security-sdk";
26
+ export declare const AUDIT_COMMAND = "npx @agentx-core/security-sdk audit";
27
+ export declare const DOCS_URL = "https://agentx-core.com/docs";
28
+ /** The one snippet every empty screen shows. Written once so two screens cannot teach two spellings. */
29
+ export declare const WRAP_SNIPPET: readonly string[];
30
+ export interface ViewOptions {
31
+ /** Explicit --limit. */
32
+ limit?: number;
33
+ /** --all. */
34
+ all?: boolean;
35
+ }
36
+ export declare function plural(n: number, singular: string, pluralForm?: string): string;
37
+ /** Clip with a visible ellipsis, never silently: a truncated name that still looks like a name is worse. */
38
+ export declare function fit(text: string, width: number): string;
39
+ /**
40
+ * Join argument names to fit `width`, dropping WHOLE names rather than cutting one, and saying
41
+ * "+N more" for what was dropped. A half name matches nothing when the reader greps for it.
42
+ * Port of cli.py `_fit_names`, including its one deliberate overrun: a single name longer than
43
+ * the budget is kept whole up to MAX_WHOLE_NAME.
44
+ */
45
+ export declare function fitNames(namesIn: readonly unknown[], width: number): string;
46
+ /** A magnitude BUCKET rendered as the ">= floor" it means, never as a figure the ledger does not hold. */
47
+ export declare function formatBucket(amount: number): string;
48
+ /** " since <local date time>" for a window, or "". */
49
+ export declare function sincePhrase(windowStart: number | null): string;
50
+ export interface ToolSummary {
51
+ tool: string;
52
+ calls: number;
53
+ classes: TargetClass[];
54
+ argNames: string[];
55
+ maxAmount: number;
56
+ maxQuantity: number;
57
+ agents: string[];
58
+ firstTs: number;
59
+ lastTs: number;
60
+ }
61
+ /** Group the rows by tool, busiest first. */
62
+ export declare function summariseTools(rows: readonly CallRow[]): ToolSummary[];
63
+ /** The grouped screen. Returns the text; the caller prints it. */
64
+ export declare function renderTools(read: LedgerRead, opts?: ViewOptions): string;
65
+ /** The `--calls` screen: one line per call, newest first. The ORDER is the point. */
66
+ export declare function renderCalls(read: LedgerRead, opts?: ViewOptions): string;
67
+ /**
68
+ * The `--json` document. ALWAYS COMPLETE unless the caller passed `--limit`: a program reading
69
+ * a silently truncated list would report a smaller agent than the one that ran. Carries a
70
+ * `schema` and a `view` so a file read in isolation says which flag produced it, and a
71
+ * `produced_by` block so a reader who has never heard of us knows what made it, which build,
72
+ * and where to look. Field names follow the Python `agentx audit --json` wherever the two
73
+ * describe the same fact (`ledger.*`, `time`, `status_label`, `arg_names`, `amount_bucket`,
74
+ * `surface`), so one consumer can read both files.
75
+ */
76
+ export declare function jsonPayload(read: LedgerRead, view: "tools" | "calls", opts?: ViewOptions): Record<string, unknown>;