agentfootprint 9.7.0 → 9.8.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,336 @@
1
+ "use strict";
2
+ /**
3
+ * vaultCredentials — a {@link CredentialProvider} over a HashiCorp-Vault-compatible
4
+ * KV v2 secret store, spoken as plain HTTP.
5
+ *
6
+ * import { vaultCredentials } from 'agentfootprint/security';
7
+ *
8
+ * const credentials = vaultCredentials({
9
+ * address: 'https://vault.internal:8200', // https, or say `allowHttp` out loud
10
+ * mount: 'secret', // KV v2 mount, default 'secret'
11
+ * paths: { github: 'ci/github' }, // service → path INSIDE the mount
12
+ * }); // token: VAULT_TOKEN, or `token`
13
+ *
14
+ * Zero dependencies and no SDK: one `GET` per resolution through the runtime's
15
+ * own `fetch`. Vault's HTTP API is small, stable and the thing every
16
+ * Vault-compatible store (OpenBao, and the Vault-API modes of several managed
17
+ * stores) implements — so the adapter that speaks HTTP works against more
18
+ * backends than the adapter that imports one vendor's client.
19
+ *
20
+ * ## V1 is deliberately one shape, and says so by name
21
+ *
22
+ * | Axis | V1 | Anything else |
23
+ * |---|---|---|
24
+ * | Auth | a **token** (`token` option, else `VAULT_TOKEN`) | AppRole / Kubernetes / JWT / AWS IAM login are **refused by name**, naming the option that would carry them |
25
+ * | Secret engine | **KV v2** (`<mount>/data/<path>`, the `data.data` envelope) | a KV v1 mount is refused by name once the response shape gives it away |
26
+ * | Leases / renewal | **none** — every `getCredential` re-reads the secret | a lease-aware provider is a different object, and the library's model since 9.7.0 is re-resolve-per-call |
27
+ *
28
+ * That is not modesty, it is the honest edge: an auth method the author cannot
29
+ * exercise against a real cluster would be a guess wearing an adapter's clothes.
30
+ * Each refusal names the option it would arrive on, so "tell us your auth shape"
31
+ * is a field report rather than an issue title.
32
+ *
33
+ * ## Field → credential kind
34
+ *
35
+ * A KV v2 read returns `{ data: { data: { …your fields… }, metadata: {…} } }`.
36
+ * The inner object is mapped to a {@link Credential} by the FIRST rule that
37
+ * matches, so a secret written the ordinary way needs no configuration:
38
+ *
39
+ * | Fields present | Becomes | Header it applies |
40
+ * |---|---|---|
41
+ * | `token` | `bearer(token)` | `authorization: Bearer …` |
42
+ * | `api_key` \| `apiKey` \| `key` | `apiKey(value, header ?? 'x-api-key')` | that header |
43
+ * | `username` + `password` | `basic(username, password)` | `authorization: Basic …` |
44
+ * | `headers` (an object of strings) | `headers(map)` | all of them |
45
+ *
46
+ * A secret matching none of them is refused — naming the PATH and the four
47
+ * shapes, never the secret. `toCredential` is the seam for a shop whose fields
48
+ * are named otherwise; it sees the secret and returns a `Credential`, and
49
+ * returning `undefined` falls back to the table above.
50
+ *
51
+ * ## Secrecy (the 8.6.0 two-clause law, applied here)
52
+ *
53
+ * A thrown message reaches the model as a tool result AND rides
54
+ * `agentfootprint.credential.failed`. So every error this adapter raises names
55
+ * **the service, the path and the HTTP status, and nothing from the response
56
+ * body or the token**. Nothing here logs, and no secret value, no `X-Vault-Token`
57
+ * header and no field name from the payload appears in any message it can throw
58
+ * — pinned by a grep-shaped test over every failure path. The credential it
59
+ * returns hides its own secret fields (non-enumerable) and carries `toHeaders`,
60
+ * so `structuredClone` rejects it and it cannot enter tracked scope by accident.
61
+ *
62
+ * @example Dev → prod is the same two lines
63
+ * ```ts
64
+ * // dev
65
+ * const credentials = staticTokens({ github: 'ghp_dev_xxx' });
66
+ * // prod — the tool code does not change
67
+ * const credentials = vaultCredentials({ address: process.env.VAULT_ADDR! });
68
+ * Agent.create({ provider, model, credentials }).build();
69
+ * ```
70
+ */
71
+ Object.defineProperty(exports, "__esModule", { value: true });
72
+ exports.vaultCredentials = void 0;
73
+ const kinds_js_1 = require("../../identity/kinds.js");
74
+ /** Auth methods a caller may ask for, and the option that would carry each one
75
+ * when it is built. Naming them is the teaching: a refusal that says "not
76
+ * supported" ends the conversation; one that says what it would take starts a
77
+ * field report. */
78
+ const UNBUILT_AUTH_METHODS = {
79
+ approle: '`roleId` + `secretId` (Vault `auth/approle/login`)',
80
+ kubernetes: '`role` + the projected service-account token path (`auth/kubernetes/login`)',
81
+ jwt: '`role` + a signed JWT/OIDC assertion (`auth/jwt/login`)',
82
+ oidc: '`role` + a signed JWT/OIDC assertion (`auth/jwt/login`)',
83
+ aws: '`role` + a signed STS identity document (`auth/aws/login`)',
84
+ cert: 'a client certificate + key, which is a TLS-agent decision, not a header',
85
+ userpass: '`username` + `password` (`auth/userpass/login`)',
86
+ ldap: '`username` + `password` (`auth/ldap/login`)',
87
+ };
88
+ // ─── Public factory ──────────────────────────────────────────────────
89
+ /**
90
+ * Build a {@link CredentialProvider} that reads KV v2 secrets from a
91
+ * Vault-compatible store. See {@link VaultCredentialsOptions} for the
92
+ * per-option contract and this module's docstring for the V1 boundary.
93
+ */
94
+ function vaultCredentials(options) {
95
+ const id = options.id ?? 'vault';
96
+ if (!options.address || typeof options.address !== 'string' || options.address.trim() === '') {
97
+ throw new TypeError(`${id}: \`address\` is required — the Vault base URL, e.g. ` +
98
+ `'https://vault.internal:8200'. It is not read from VAULT_ADDR on purpose: ` +
99
+ `an agent that picks its vault out of the environment reads a different vault ` +
100
+ `when the environment changes under it.`);
101
+ }
102
+ const address = options.address.replace(/\/+$/, '');
103
+ if (!/^https:\/\//i.test(address)) {
104
+ if (!/^http:\/\//i.test(address)) {
105
+ throw new TypeError(`${id}: \`address\` must be an http(s) URL (got '${address}').`);
106
+ }
107
+ if (!options.allowHttp) {
108
+ throw new TypeError(`${id}: refusing a plain-http address ('${address}'). The Vault token travels in ` +
109
+ `the \`X-Vault-Token\` request header, so over plaintext HTTP anyone on the path ` +
110
+ `reads a token that can usually read every secret it can reach — and a leaked ` +
111
+ `read token is not revoked by rotating one secret. Use https, or set ` +
112
+ `\`allowHttp: true\` deliberately for a loopback dev server.`);
113
+ }
114
+ }
115
+ if (options.auth !== undefined && options.auth !== 'token') {
116
+ const asked = String(options.auth).toLowerCase();
117
+ const wouldTake = UNBUILT_AUTH_METHODS[asked];
118
+ throw new TypeError(`${id}: \`auth: '${String(options.auth)}'\` is not built. V1 authenticates with a ` +
119
+ `TOKEN only — \`token\`, or the \`VAULT_TOKEN\` environment variable.` +
120
+ (wouldTake
121
+ ? ` ${asked} login would arrive on ${wouldTake}, plus a re-login when the ` +
122
+ `returned lease expires.`
123
+ : ` A login method would need its own credentials and a re-login on lease expiry.`) +
124
+ ` This is field-gated rather than guessed: tell us your auth shape (which method, ` +
125
+ `which mount path, which lease behaviour) and it gets built against a real cluster ` +
126
+ `instead of an API document. Until then, exchange it yourself and pass the ` +
127
+ `resulting token as \`token\`.`);
128
+ }
129
+ if (options.paths && options.resolve) {
130
+ throw new TypeError(`${id}: pass \`paths\` OR \`resolve\`, not both — two spellings of one rule can ` +
131
+ `disagree, and the one that loses would do so silently. Use \`paths\` for a fixed ` +
132
+ `map, \`resolve\` when the path is computed from the service id.`);
133
+ }
134
+ const token = options.token ?? readEnv('VAULT_TOKEN');
135
+ if (!token) {
136
+ throw new TypeError(`${id}: no Vault token. Pass \`token\`, or set the VAULT_TOKEN environment variable. ` +
137
+ `(V1 authenticates with a token only — see \`auth\`.)`);
138
+ }
139
+ const mount = trimSlashes(options.mount ?? 'secret');
140
+ const timeoutMs = options.timeoutMs ?? 5000;
141
+ const apiKeyHeader = options.apiKeyHeader ?? 'x-api-key';
142
+ const doFetch = options._fetch ?? ((...args) => fetch(...args));
143
+ /** service → path inside the mount, by whichever arm was configured. */
144
+ function pathFor(service) {
145
+ if (options.paths) {
146
+ const mapped = options.paths[service];
147
+ if (mapped === undefined) {
148
+ throw new Error(`${id}: no secret path configured for service '${service}'. ` +
149
+ `Known services: ${Object.keys(options.paths).join(', ') || '(none)'}.`);
150
+ }
151
+ return trimSlashes(mapped);
152
+ }
153
+ if (options.resolve) {
154
+ const mapped = options.resolve(service);
155
+ if (!mapped) {
156
+ throw new Error(`${id}: \`resolve('${service}')\` returned no path, so this service has no secret ` +
157
+ `to read. Return a path inside the '${mount}' mount, or configure the service ` +
158
+ `elsewhere.`);
159
+ }
160
+ return trimSlashes(mapped);
161
+ }
162
+ // No mapping configured: the service id IS the path.
163
+ return trimSlashes(service);
164
+ }
165
+ return {
166
+ id,
167
+ async getCredential(req) {
168
+ const secretPath = pathFor(req.service);
169
+ // KV v2's read shape. `/data/` is the v2 API segment, not part of your
170
+ // path — `secret/ci/github` in the UI is `secret/data/ci/github` here.
171
+ const url = `${address}/v1/${mount}/data/${secretPath}`;
172
+ const where = `'${mount}/${secretPath}'`;
173
+ const secret = await readSecret({
174
+ url,
175
+ where,
176
+ id,
177
+ service: req.service,
178
+ token,
179
+ namespace: options.namespace,
180
+ timeoutMs,
181
+ doFetch,
182
+ });
183
+ const custom = options.toCredential?.(secret, req.service);
184
+ if (custom)
185
+ return { status: 'issued', credential: custom };
186
+ const credential = mapFieldsToCredential(secret, apiKeyHeader);
187
+ if (!credential) {
188
+ throw new Error(`${id}: the secret at ${where} has none of the field shapes this adapter reads — ` +
189
+ `\`token\`, \`api_key\`/\`apiKey\`/\`key\`, \`username\`+\`password\`, or ` +
190
+ `\`headers\`. (The fields it DOES have are deliberately not named here: an ` +
191
+ `error message reaches the model and the credential.failed event.) Rewrite the ` +
192
+ `secret in one of those shapes, or pass \`toCredential\` to map your own.`);
193
+ }
194
+ // No `expiresAt`: a KV v2 secret has no lease, so there is no expiry to
195
+ // report and inventing one would be worse than absence. V1 re-reads on
196
+ // every call, which is the library's model since 9.7.0.
197
+ return { status: 'issued', credential };
198
+ },
199
+ };
200
+ }
201
+ exports.vaultCredentials = vaultCredentials;
202
+ /**
203
+ * GET one KV v2 secret and return its inner `data.data` object.
204
+ *
205
+ * Every throw below names the provider id, the service, the mount path and (for
206
+ * an HTTP failure) the status. None of them can name the token, a response body
207
+ * or a field of the secret — that restraint IS the contract, because this
208
+ * message becomes a tool result the model reads and a `credential.failed`
209
+ * payload every observer receives.
210
+ */
211
+ async function readSecret(args) {
212
+ const { id, where, service } = args;
213
+ let res;
214
+ try {
215
+ res = await args.doFetch(args.url, {
216
+ method: 'GET',
217
+ headers: {
218
+ 'X-Vault-Token': args.token,
219
+ accept: 'application/json',
220
+ ...(args.namespace && { 'X-Vault-Namespace': args.namespace }),
221
+ },
222
+ signal: AbortSignal.timeout(args.timeoutMs),
223
+ });
224
+ }
225
+ catch (err) {
226
+ // Transport failure: DNS, TLS, refused connection, timeout. The cause is
227
+ // safe to name — it describes the socket, never the payload — but it is
228
+ // re-wrapped rather than rethrown so no fetch implementation can smuggle
229
+ // request headers into the text.
230
+ throw new Error(`${id}: could not reach Vault for service '${service}' (${where}): ` +
231
+ `${transportReason(err)}.`);
232
+ }
233
+ if (!res.ok) {
234
+ throw new Error(`${id}: Vault returned ${res.status} reading ${where} for service '${service}'` +
235
+ `${statusHint(res.status)}`);
236
+ }
237
+ let body;
238
+ try {
239
+ body = await res.json();
240
+ }
241
+ catch {
242
+ throw new Error(`${id}: Vault's response for ${where} was not JSON (status ${res.status}). ` +
243
+ `Check that \`address\` points at Vault's API and not at a proxy or login page.`);
244
+ }
245
+ const outer = body?.data;
246
+ if (!isRecord(outer)) {
247
+ throw new Error(`${id}: no secret data at ${where} (status ${res.status}). ` +
248
+ `Check the path exists in the KV v2 mount.`);
249
+ }
250
+ const inner = outer.data;
251
+ if (!isRecord(inner)) {
252
+ // A v1 mount answers with the fields directly under `data`, with no inner
253
+ // envelope. That is the one cheap, unambiguous v1 tell, so it is named
254
+ // rather than guessed at.
255
+ throw new Error(`${id}: the response for ${where} is not KV v2 shaped (no \`data.data\` envelope). ` +
256
+ `This adapter reads KV **v2** only, which is why the URL carries the \`/data/\` ` +
257
+ `segment. If '${where.split('/')[0]?.replace(/'/g, '') || 'that mount'}' is a KV v1 ` +
258
+ `mount, upgrade it (\`vault kv enable-versioning\`) or mount v2 — or tell us, and ` +
259
+ `\`kvVersion\` is the option a v1 reader would arrive on.`);
260
+ }
261
+ return inner;
262
+ }
263
+ /** A short, payload-free reason for a transport failure. */
264
+ function transportReason(err) {
265
+ if (err instanceof Error) {
266
+ if (err.name === 'TimeoutError' || err.name === 'AbortError')
267
+ return 'the request timed out';
268
+ return err.name || 'network error';
269
+ }
270
+ return 'network error';
271
+ }
272
+ /** What a status usually means here. Static text keyed by number — it cannot
273
+ * echo a response, because it never reads one. */
274
+ function statusHint(status) {
275
+ if (status === 403) {
276
+ return '. The token is valid but its policy does not allow reading that path.';
277
+ }
278
+ if (status === 401)
279
+ return '. The token is missing, expired or revoked.';
280
+ if (status === 404) {
281
+ return ('. Either the path does not exist, or the mount is not a KV v2 mount ' +
282
+ '(the read URL carries the v2 `/data/` segment).');
283
+ }
284
+ if (status === 503)
285
+ return '. Vault is sealed or standby.';
286
+ return '.';
287
+ }
288
+ // ─── Field → kind ────────────────────────────────────────────────────
289
+ /**
290
+ * The built-in field table, first match wins. Deliberately small: four shapes
291
+ * cover what secrets actually hold, and `toCredential` covers the rest without
292
+ * this function growing an option per spelling.
293
+ */
294
+ function mapFieldsToCredential(secret, defaultApiKeyHeader) {
295
+ const token = str(secret.token);
296
+ if (token)
297
+ return (0, kinds_js_1.bearer)(token);
298
+ const key = str(secret.api_key) ?? str(secret.apiKey) ?? str(secret.key);
299
+ if (key)
300
+ return (0, kinds_js_1.apiKey)(key, str(secret.header) ?? defaultApiKeyHeader);
301
+ const username = str(secret.username);
302
+ const password = str(secret.password);
303
+ if (username && password)
304
+ return (0, kinds_js_1.basic)(username, password);
305
+ const map = secret.headers;
306
+ if (isRecord(map)) {
307
+ const flat = {};
308
+ for (const [k, v] of Object.entries(map)) {
309
+ const s = str(v);
310
+ if (s)
311
+ flat[k] = s;
312
+ }
313
+ if (Object.keys(flat).length > 0)
314
+ return (0, kinds_js_1.headers)(flat);
315
+ }
316
+ return undefined;
317
+ }
318
+ // ─── Small helpers ───────────────────────────────────────────────────
319
+ function isRecord(v) {
320
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
321
+ }
322
+ /** A non-empty string, or undefined. Numbers are NOT coerced: a secret field
323
+ * that is not a string is not a credential this adapter knows how to apply. */
324
+ function str(v) {
325
+ return typeof v === 'string' && v !== '' ? v : undefined;
326
+ }
327
+ function trimSlashes(s) {
328
+ return s.replace(/^\/+|\/+$/g, '');
329
+ }
330
+ /** Read one environment variable, in any runtime. Absent `process` (a browser,
331
+ * a worker) simply means no fallback. */
332
+ function readEnv(name) {
333
+ const p = globalThis.process;
334
+ return p?.env?.[name];
335
+ }
336
+ //# sourceMappingURL=vault.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vault.js","sourceRoot":"","sources":["../../../src/adapters/identity/vault.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,307 @@
1
+ "use strict";
2
+ /**
3
+ * fileObservability — the typed event stream, one JSON line per event, in a
4
+ * local file.
5
+ *
6
+ * The sink for the shop that has no collector. Every other adapter in this
7
+ * folder ships somewhere: CloudWatch, X-Ray, an OTLP endpoint. A great many
8
+ * on-premises deployments have none of those — they have a directory, a log
9
+ * shipper (Filebeat, Fluent Bit, Vector, `promtail`, `journald`, or a person
10
+ * with `grep`), and a rule that nothing leaves the network. NDJSON on disk is
11
+ * the format all of those already read, so the sink is the file.
12
+ *
13
+ * Zero dependencies: `node:fs`, lazily required at construction so merely
14
+ * importing `agentfootprint/observe` stays browser-safe (this factory is
15
+ * Node-only; calling it in a browser throws by name).
16
+ *
17
+ * ## The line
18
+ *
19
+ * One `JSON.stringify(event)` per line, newline-terminated, appended in
20
+ * dispatch order — the SAME envelope `cloudwatchObservability` puts in a log
21
+ * event, so a query written against one reads the other:
22
+ *
23
+ * ```jsonl
24
+ * {"type":"agentfootprint.agent.turn_start","payload":{…},"meta":{"runId":"…","sessionId":"…"}}
25
+ * {"type":"agentfootprint.stream.tool_end","payload":{…},"meta":{…}}
26
+ * ```
27
+ *
28
+ * Nothing is summarized, bounded or redacted on the way out. **A payload that
29
+ * must not be on that disk must not reach this strategy** — narrow it with
30
+ * `eventTypes` / `tier` / `sampleRate`, or apply a footprintjs
31
+ * `RedactionPolicy` upstream, exactly as with every other sink. (For a bounded
32
+ * record by construction, `auditExport({ payloadMode: 'bounded' })` is the
33
+ * adapter that does that job.)
34
+ *
35
+ * ## Buffered, not synchronous
36
+ *
37
+ * `exportEvent` is sync and never touches the disk: it serializes, buffers, and
38
+ * returns. Batches are appended asynchronously on a size trigger
39
+ * (`maxBufferEvents` / `maxBufferBytes`), on a timer (`flushIntervalMs`), and on
40
+ * `flush()`. A hard kill therefore loses at most the buffer — the price of not
41
+ * making telemetry a term in agent-loop latency. Call `flush()` (or
42
+ * `agent.shutdown()`, which does) at process end; see the 8.12.0 lifecycle laws
43
+ * on {@link BaseStrategy.flush}.
44
+ *
45
+ * ## Rotation is ONE generation, and that is deliberate
46
+ *
47
+ * With `maxBytes` set, a batch that would push the file past the ceiling first
48
+ * renames it to `<path>.1` — **replacing any previous `.1`** — and starts a
49
+ * fresh file. That is the whole policy. There is no `.2`, no compression, no
50
+ * time-based schedule, no cross-process coordination (two processes writing one
51
+ * file each keep their own byte count and will both rotate it). It exists so an
52
+ * unattended agent cannot fill a disk, and for nothing else: **retention is a
53
+ * log-management daemon's job**, and `logrotate` with `copytruncate`, Fluent Bit,
54
+ * or a systemd timer will do it properly. Omit `maxBytes` — the default — and
55
+ * this adapter never renames anything, which is the right choice when a real
56
+ * rotator already owns the file.
57
+ *
58
+ * @example
59
+ * ```ts
60
+ * import { fileObservability } from 'agentfootprint/observe';
61
+ *
62
+ * const telemetry = agent.enable.observability({
63
+ * strategy: fileObservability({
64
+ * path: '/var/log/agentfootprint/events.ndjson',
65
+ * maxBytes: 64 * 1024 * 1024, // safety ceiling; logrotate owns retention
66
+ * }),
67
+ * });
68
+ *
69
+ * // … run …
70
+ * await agent.shutdown(); // flushes + stops everything enabled
71
+ * ```
72
+ */
73
+ Object.defineProperty(exports, "__esModule", { value: true });
74
+ exports.fileObservability = void 0;
75
+ const lazyRequire_js_1 = require("../../lib/lazyRequire.js");
76
+ const deliveryErrors_js_1 = require("./deliveryErrors.js");
77
+ // ─── Public factory ──────────────────────────────────────────────────
78
+ /**
79
+ * NDJSON-to-a-local-file observability strategy. See
80
+ * {@link FileObservabilityOptions} for the per-option contract, and this
81
+ * module's docstring for the rotation policy and what is NOT bounded.
82
+ */
83
+ function fileObservability(opts) {
84
+ const strategyName = 'file';
85
+ if (!opts.path || typeof opts.path !== 'string' || opts.path.trim() === '') {
86
+ throw new TypeError(`[${strategyName}Observability] \`path\` is required — the file this sink writes. ` +
87
+ `Pass one, e.g. '/var/log/agentfootprint/events.ndjson'. There is no default: ` +
88
+ `where a run's telemetry lands on your disk is not a decision this library makes.`);
89
+ }
90
+ if (opts.maxBytes !== undefined && !(opts.maxBytes > 0)) {
91
+ throw new TypeError(`[${strategyName}Observability] \`maxBytes\` must be a positive number of bytes ` +
92
+ `(got ${String(opts.maxBytes)}). Omit it for no rotation at all, which is the ` +
93
+ `right choice when logrotate or a log shipper already owns '${opts.path}'.`);
94
+ }
95
+ const path = opts.path;
96
+ const rotatedPath = `${path}.1`;
97
+ const maxBufferEvents = opts.maxBufferEvents ?? 100;
98
+ const maxBufferBytes = opts.maxBufferBytes ?? 65_536;
99
+ const flushIntervalMs = opts.flushIntervalMs ?? 1000;
100
+ const fs = opts._fs ?? createNodeFileSink(strategyName);
101
+ // Claim the file NOW. Two things this buys, both worth the syscall:
102
+ // 1. an unwritable path (no directory, no permission, a directory where a
103
+ // file should be, a read-only mount) is refused at construction, where
104
+ // the caller can still read the message;
105
+ // 2. `statSync` below gives the rotation counter a true starting size, so a
106
+ // restart appends to an existing file rather than believing it is empty.
107
+ let bytesOnDisk = 0;
108
+ try {
109
+ const dir = parentDir(path);
110
+ if (dir)
111
+ fs.mkdirSync(dir, { recursive: true });
112
+ fs.appendFileSync(path, '');
113
+ bytesOnDisk = fs.statSync(path).size;
114
+ }
115
+ catch (err) {
116
+ throw new Error(`[${strategyName}Observability] cannot write '${path}': ${errorMessage(err)}. ` +
117
+ `The parent directory is created for you; a failure here means the path is a ` +
118
+ `directory, the mount is read-only, or the process user lacks permission. ` +
119
+ `Fix the path or the permission — this sink refuses to start rather than ` +
120
+ `silently drop every event.`);
121
+ }
122
+ // Buffered lines — drained by `flush()` / size trigger / timer.
123
+ const buffer = [];
124
+ let bufferBytes = 0;
125
+ let lastFlushPromise = Promise.resolve();
126
+ let timer;
127
+ let stopped = false;
128
+ // The fallback when the consumer wires nothing. Rate-limited; a
129
+ // consumer-supplied sink is not.
130
+ const consoleSink = (0, deliveryErrors_js_1.rateLimitedConsoleSink)(strategyName);
131
+ function scheduleTimedFlush() {
132
+ if (timer || flushIntervalMs <= 0 || stopped)
133
+ return;
134
+ timer = setTimeout(() => {
135
+ timer = undefined;
136
+ void doFlush();
137
+ }, flushIntervalMs);
138
+ // Never hold the process open for telemetry. `unref` exists on Node's
139
+ // Timeout and not on the browser's number — feature-detected, so a
140
+ // non-Node runtime with an injected `_fs` still works.
141
+ timer.unref?.();
142
+ }
143
+ /** Route a delivery failure through whatever `_onError` IS RIGHT NOW —
144
+ * reading it at call time, so a consumer who assigns `_onError` after
145
+ * construction still receives them (the 8.11.0 fix). */
146
+ function reportFailure(err) {
147
+ strategy._onError?.(err);
148
+ }
149
+ /** ONE generation. A file already at or over the ceiling is renamed to
150
+ * `<path>.1` (replacing any previous `.1`) and the counter resets. A batch
151
+ * bigger than the whole ceiling is still written — truncating a run's
152
+ * telemetry to fit a dial nobody set for that purpose would be worse. */
153
+ async function rotateIfNeeded(incomingBytes) {
154
+ const ceiling = opts.maxBytes;
155
+ if (ceiling === undefined)
156
+ return;
157
+ if (bytesOnDisk === 0)
158
+ return;
159
+ if (bytesOnDisk + incomingBytes <= ceiling)
160
+ return;
161
+ await fs.rename(path, rotatedPath);
162
+ bytesOnDisk = 0;
163
+ }
164
+ async function doFlush() {
165
+ // `stopped` is deliberately NOT a guard here — same stance as the AWS
166
+ // adapters since 8.11.1. `stop()` stops this strategy ACCEPTING events; it
167
+ // does not authorise throwing away events already accepted, and a `flush()`
168
+ // that cannot make progress after a `stop()` is how shutdown spins forever.
169
+ if (buffer.length === 0)
170
+ return;
171
+ // Snapshot + clear so events emitted during the in-flight write accumulate
172
+ // into the next batch.
173
+ const batch = buffer.splice(0);
174
+ const batchBytes = bufferBytes;
175
+ bufferBytes = 0;
176
+ const data = `${batch.join('\n')}\n`;
177
+ try {
178
+ await rotateIfNeeded(batchBytes);
179
+ await fs.appendFile(path, data);
180
+ bytesOnDisk += byteLength(data);
181
+ }
182
+ catch (err) {
183
+ reportFailure(new Error(`${batch.length} event(s) dropped writing to '${path}': ${errorMessage(err)}`));
184
+ }
185
+ }
186
+ function enqueue(event) {
187
+ if (stopped)
188
+ return;
189
+ // The hot path must not throw (port law). `JSON.stringify` can — a cycle, a
190
+ // BigInt, a throwing getter — and an event that cannot be serialized is a
191
+ // reportable fact, not a reason to break the agent loop.
192
+ let line;
193
+ try {
194
+ line = JSON.stringify(event);
195
+ }
196
+ catch (err) {
197
+ reportFailure(new Error(`event '${event?.type ?? 'unknown'}' could not be serialized: ` + errorMessage(err)));
198
+ return;
199
+ }
200
+ // `JSON.stringify` returns undefined for an undefined input — nothing to
201
+ // write, and a bare "undefined" line would corrupt the NDJSON stream.
202
+ if (line === undefined)
203
+ return;
204
+ buffer.push(line);
205
+ bufferBytes += byteLength(line) + 1; // + the newline this line will carry
206
+ if (buffer.length >= maxBufferEvents || bufferBytes >= maxBufferBytes) {
207
+ // Size trigger. Chain onto the last write rather than racing it — the
208
+ // file's line order IS the dispatch order, and that is the only thing an
209
+ // offline reader can rely on.
210
+ lastFlushPromise = lastFlushPromise.then(doFlush, doFlush);
211
+ }
212
+ else {
213
+ scheduleTimedFlush();
214
+ }
215
+ }
216
+ const strategy = {
217
+ name: strategyName,
218
+ capabilities: { events: true, logs: true },
219
+ ...(opts.eventTypes && { relevantEventTypes: opts.eventTypes }),
220
+ exportEvent: enqueue,
221
+ /**
222
+ * Write what is buffered. Called for you on shutdown (8.12.0) — by the
223
+ * handle `enable.observability()` returns, by `agent.shutdown()`, and by a
224
+ * `standingAgent` closing. Safe at any time, including after `stop()`.
225
+ */
226
+ async flush() {
227
+ // BOUNDED BY CONSTRUCTION — every pass must remove at least one buffered
228
+ // line; a pass that removes none ends the drain instead of retrying. The
229
+ // shape the CloudWatch adapter arrived at in 8.11.1, for the same reason:
230
+ // a drain that cannot finish must return, never spin.
231
+ for (;;) {
232
+ const pending = buffer.length;
233
+ lastFlushPromise = lastFlushPromise.then(doFlush, doFlush);
234
+ await lastFlushPromise;
235
+ if (buffer.length === 0)
236
+ return;
237
+ if (buffer.length >= pending)
238
+ return;
239
+ }
240
+ },
241
+ /** Clear the timer and stop accepting events. Terminal: there is no
242
+ * restart. What it does NOT do is discard the buffer — `flush()` after
243
+ * `stop()` still writes it. */
244
+ stop() {
245
+ stopped = true;
246
+ if (timer) {
247
+ clearTimeout(timer);
248
+ timer = undefined;
249
+ }
250
+ },
251
+ /** Where failures go. Two callers: the dispatch layer (when `exportEvent`
252
+ * itself throws) and this adapter's own write path. */
253
+ _onError(err, event) {
254
+ (opts.onError ?? consoleSink)(err, event);
255
+ },
256
+ };
257
+ return strategy;
258
+ }
259
+ exports.fileObservability = fileObservability;
260
+ // ─── node:fs binding (lazy) ──────────────────────────────────────────
261
+ /**
262
+ * Bind {@link FileSinkFs} to `node:fs`. Lazily required so that importing
263
+ * `agentfootprint/observe` in a browser bundle never resolves `node:fs` — only
264
+ * CALLING this factory does, and a runtime without it is refused by name.
265
+ */
266
+ function createNodeFileSink(strategyName) {
267
+ let mod;
268
+ try {
269
+ mod = (0, lazyRequire_js_1.lazyRequire)('node:fs');
270
+ }
271
+ catch {
272
+ throw new Error(`[${strategyName}Observability] needs \`node:fs\`, and this runtime has none. ` +
273
+ `It is a Node-only sink by definition — a browser has no local file to append to. ` +
274
+ `In a browser, ship events over your own transport, or pass \`_fs\` to write ` +
275
+ `somewhere you control.`);
276
+ }
277
+ return {
278
+ mkdirSync: (dir, options) => void mod.mkdirSync(dir, options),
279
+ appendFileSync: (file, data) => mod.appendFileSync(file, data),
280
+ statSync: (file) => mod.statSync(file),
281
+ appendFile: (file, data) => mod.promises.appendFile(file, data),
282
+ rename: (from, to) => mod.promises.rename(from, to),
283
+ };
284
+ }
285
+ // ─── Small helpers ───────────────────────────────────────────────────
286
+ function errorMessage(err) {
287
+ return err instanceof Error ? err.message : String(err);
288
+ }
289
+ /** UTF-8 byte length. `Buffer` where there is one, `TextEncoder` otherwise —
290
+ * the byte count decides rotation, so a multi-byte prompt must not be counted
291
+ * as its character length. */
292
+ function byteLength(s) {
293
+ const B = globalThis.Buffer;
294
+ if (B)
295
+ return B.byteLength(s, 'utf8');
296
+ return new TextEncoder().encode(s).length;
297
+ }
298
+ /** The directory part of a path, POSIX or Windows separators, or `''` for a
299
+ * bare filename (nothing to create). Done by hand rather than through
300
+ * `node:path` so the module has exactly one Node import, behind one seam. */
301
+ function parentDir(file) {
302
+ const idx = Math.max(file.lastIndexOf('/'), file.lastIndexOf('\\'));
303
+ if (idx <= 0)
304
+ return '';
305
+ return file.slice(0, idx);
306
+ }
307
+ //# sourceMappingURL=file.js.map