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 @@
1
+ {"version":3,"file":"file.js","sourceRoot":"","sources":["../../../src/adapters/observability/file.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,146 @@
1
+ /**
2
+ * vaultCredentials — a {@link CredentialProvider} over a HashiCorp-Vault-compatible
3
+ * KV v2 secret store, spoken as plain HTTP.
4
+ *
5
+ * import { vaultCredentials } from 'agentfootprint/security';
6
+ *
7
+ * const credentials = vaultCredentials({
8
+ * address: 'https://vault.internal:8200', // https, or say `allowHttp` out loud
9
+ * mount: 'secret', // KV v2 mount, default 'secret'
10
+ * paths: { github: 'ci/github' }, // service → path INSIDE the mount
11
+ * }); // token: VAULT_TOKEN, or `token`
12
+ *
13
+ * Zero dependencies and no SDK: one `GET` per resolution through the runtime's
14
+ * own `fetch`. Vault's HTTP API is small, stable and the thing every
15
+ * Vault-compatible store (OpenBao, and the Vault-API modes of several managed
16
+ * stores) implements — so the adapter that speaks HTTP works against more
17
+ * backends than the adapter that imports one vendor's client.
18
+ *
19
+ * ## V1 is deliberately one shape, and says so by name
20
+ *
21
+ * | Axis | V1 | Anything else |
22
+ * |---|---|---|
23
+ * | 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 |
24
+ * | 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 |
25
+ * | 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 |
26
+ *
27
+ * That is not modesty, it is the honest edge: an auth method the author cannot
28
+ * exercise against a real cluster would be a guess wearing an adapter's clothes.
29
+ * Each refusal names the option it would arrive on, so "tell us your auth shape"
30
+ * is a field report rather than an issue title.
31
+ *
32
+ * ## Field → credential kind
33
+ *
34
+ * A KV v2 read returns `{ data: { data: { …your fields… }, metadata: {…} } }`.
35
+ * The inner object is mapped to a {@link Credential} by the FIRST rule that
36
+ * matches, so a secret written the ordinary way needs no configuration:
37
+ *
38
+ * | Fields present | Becomes | Header it applies |
39
+ * |---|---|---|
40
+ * | `token` | `bearer(token)` | `authorization: Bearer …` |
41
+ * | `api_key` \| `apiKey` \| `key` | `apiKey(value, header ?? 'x-api-key')` | that header |
42
+ * | `username` + `password` | `basic(username, password)` | `authorization: Basic …` |
43
+ * | `headers` (an object of strings) | `headers(map)` | all of them |
44
+ *
45
+ * A secret matching none of them is refused — naming the PATH and the four
46
+ * shapes, never the secret. `toCredential` is the seam for a shop whose fields
47
+ * are named otherwise; it sees the secret and returns a `Credential`, and
48
+ * returning `undefined` falls back to the table above.
49
+ *
50
+ * ## Secrecy (the 8.6.0 two-clause law, applied here)
51
+ *
52
+ * A thrown message reaches the model as a tool result AND rides
53
+ * `agentfootprint.credential.failed`. So every error this adapter raises names
54
+ * **the service, the path and the HTTP status, and nothing from the response
55
+ * body or the token**. Nothing here logs, and no secret value, no `X-Vault-Token`
56
+ * header and no field name from the payload appears in any message it can throw
57
+ * — pinned by a grep-shaped test over every failure path. The credential it
58
+ * returns hides its own secret fields (non-enumerable) and carries `toHeaders`,
59
+ * so `structuredClone` rejects it and it cannot enter tracked scope by accident.
60
+ *
61
+ * @example Dev → prod is the same two lines
62
+ * ```ts
63
+ * // dev
64
+ * const credentials = staticTokens({ github: 'ghp_dev_xxx' });
65
+ * // prod — the tool code does not change
66
+ * const credentials = vaultCredentials({ address: process.env.VAULT_ADDR! });
67
+ * Agent.create({ provider, model, credentials }).build();
68
+ * ```
69
+ */
70
+ import type { Credential, CredentialProvider } from '../../identity/types.js';
71
+ /** The base options every form shares. */
72
+ interface VaultCredentialsBase {
73
+ /** Vault's base URL, e.g. `https://vault.internal:8200` — **required**, and
74
+ * **https** unless {@link VaultCredentialsBase.allowHttp} says otherwise. No
75
+ * `VAULT_ADDR` fallback: an agent that silently picks up an address from the
76
+ * environment is an agent that reads a different vault when the environment
77
+ * changes under it. Name it. */
78
+ readonly address: string;
79
+ /** The Vault token. Falls back to `VAULT_TOKEN` (the variable every Vault
80
+ * tool already sets). This is a secret: it is sent as `X-Vault-Token` and
81
+ * appears in no message this adapter can throw. */
82
+ readonly token?: string;
83
+ /** Auth method. **`'token'` is the only one V1 implements.** Anything else is
84
+ * refused at construction, by name, with what it would take — see
85
+ * {@link vaultCredentials}. */
86
+ readonly auth?: 'token';
87
+ /** KV v2 mount point. Default `'secret'` (Vault's own default for the KV v2
88
+ * engine). The read URL is `<address>/v1/<mount>/data/<path>`. */
89
+ readonly mount?: string;
90
+ /** Vault Enterprise / HCP namespace, sent as `X-Vault-Namespace`. Omit for
91
+ * open-source Vault and OpenBao, which have no namespaces. */
92
+ readonly namespace?: string;
93
+ /** Map the secret's fields to a {@link Credential} yourself. Returns
94
+ * `undefined` to fall back to the built-in table (`token` / `api_key` /
95
+ * `username`+`password` / `headers`). The seam for a shop whose field names
96
+ * are its own — and the reason this adapter does not need an option per
97
+ * spelling. **Never log or return the fields from here**; they are the
98
+ * secret. */
99
+ readonly toCredential?: (secret: Readonly<Record<string, unknown>>, service: string) => Credential | undefined;
100
+ /** Header name for the `api_key` shape when the secret does not carry its own
101
+ * `header` field. Default `'x-api-key'`. */
102
+ readonly apiKeyHeader?: string;
103
+ /** Request timeout in ms. Default 5000 — a credential resolution sits in
104
+ * front of a tool call, so a hung vault must fail rather than hang a run. */
105
+ readonly timeoutMs?: number;
106
+ /** Allow a plain-`http://` address. **Refused unless you set this**, because
107
+ * the Vault token travels in a request header: over plaintext HTTP, anyone
108
+ * on the path reads a token that can usually read every secret it can reach.
109
+ * Set it only for a loopback dev server (`http://127.0.0.1:8200`). */
110
+ readonly allowHttp?: boolean;
111
+ /** Stable provider id (default `'vault'`). Shows up in "which provider vended
112
+ * this". */
113
+ readonly id?: string;
114
+ /** Test seam — inject `fetch`. Bypasses the network entirely. */
115
+ readonly _fetch?: typeof fetch;
116
+ }
117
+ /**
118
+ * How a `service` becomes a path inside the mount. Three arms, and they
119
+ * EXCLUDE each other — two spellings of one rule can disagree, so the type
120
+ * refuses the pair and so does the constructor.
121
+ */
122
+ type VaultPathMapping = {
123
+ /** `service → path inside the mount`, the {@link staticTokens} shape one
124
+ * level up: the same literal map, holding a path instead of a token. An
125
+ * unknown service is refused by name, listing the known ones. */
126
+ readonly paths: Readonly<Record<string, string>>;
127
+ readonly resolve?: never;
128
+ } | {
129
+ /** `service → path`, computed. Return `undefined` to refuse a service.
130
+ * For the convention-driven shop: ``(s) => `agents/${s}` ``. */
131
+ readonly resolve: (service: string) => string | undefined;
132
+ readonly paths?: never;
133
+ } | {
134
+ /** Neither: the **service id IS the path** under the mount, so
135
+ * `service: 'github'` reads `<mount>/data/github`. */
136
+ readonly paths?: undefined;
137
+ readonly resolve?: undefined;
138
+ };
139
+ export type VaultCredentialsOptions = VaultCredentialsBase & VaultPathMapping;
140
+ /**
141
+ * Build a {@link CredentialProvider} that reads KV v2 secrets from a
142
+ * Vault-compatible store. See {@link VaultCredentialsOptions} for the
143
+ * per-option contract and this module's docstring for the V1 boundary.
144
+ */
145
+ export declare function vaultCredentials(options: VaultCredentialsOptions): CredentialProvider;
146
+ export {};
@@ -0,0 +1,332 @@
1
+ /**
2
+ * vaultCredentials — a {@link CredentialProvider} over a HashiCorp-Vault-compatible
3
+ * KV v2 secret store, spoken as plain HTTP.
4
+ *
5
+ * import { vaultCredentials } from 'agentfootprint/security';
6
+ *
7
+ * const credentials = vaultCredentials({
8
+ * address: 'https://vault.internal:8200', // https, or say `allowHttp` out loud
9
+ * mount: 'secret', // KV v2 mount, default 'secret'
10
+ * paths: { github: 'ci/github' }, // service → path INSIDE the mount
11
+ * }); // token: VAULT_TOKEN, or `token`
12
+ *
13
+ * Zero dependencies and no SDK: one `GET` per resolution through the runtime's
14
+ * own `fetch`. Vault's HTTP API is small, stable and the thing every
15
+ * Vault-compatible store (OpenBao, and the Vault-API modes of several managed
16
+ * stores) implements — so the adapter that speaks HTTP works against more
17
+ * backends than the adapter that imports one vendor's client.
18
+ *
19
+ * ## V1 is deliberately one shape, and says so by name
20
+ *
21
+ * | Axis | V1 | Anything else |
22
+ * |---|---|---|
23
+ * | 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 |
24
+ * | 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 |
25
+ * | 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 |
26
+ *
27
+ * That is not modesty, it is the honest edge: an auth method the author cannot
28
+ * exercise against a real cluster would be a guess wearing an adapter's clothes.
29
+ * Each refusal names the option it would arrive on, so "tell us your auth shape"
30
+ * is a field report rather than an issue title.
31
+ *
32
+ * ## Field → credential kind
33
+ *
34
+ * A KV v2 read returns `{ data: { data: { …your fields… }, metadata: {…} } }`.
35
+ * The inner object is mapped to a {@link Credential} by the FIRST rule that
36
+ * matches, so a secret written the ordinary way needs no configuration:
37
+ *
38
+ * | Fields present | Becomes | Header it applies |
39
+ * |---|---|---|
40
+ * | `token` | `bearer(token)` | `authorization: Bearer …` |
41
+ * | `api_key` \| `apiKey` \| `key` | `apiKey(value, header ?? 'x-api-key')` | that header |
42
+ * | `username` + `password` | `basic(username, password)` | `authorization: Basic …` |
43
+ * | `headers` (an object of strings) | `headers(map)` | all of them |
44
+ *
45
+ * A secret matching none of them is refused — naming the PATH and the four
46
+ * shapes, never the secret. `toCredential` is the seam for a shop whose fields
47
+ * are named otherwise; it sees the secret and returns a `Credential`, and
48
+ * returning `undefined` falls back to the table above.
49
+ *
50
+ * ## Secrecy (the 8.6.0 two-clause law, applied here)
51
+ *
52
+ * A thrown message reaches the model as a tool result AND rides
53
+ * `agentfootprint.credential.failed`. So every error this adapter raises names
54
+ * **the service, the path and the HTTP status, and nothing from the response
55
+ * body or the token**. Nothing here logs, and no secret value, no `X-Vault-Token`
56
+ * header and no field name from the payload appears in any message it can throw
57
+ * — pinned by a grep-shaped test over every failure path. The credential it
58
+ * returns hides its own secret fields (non-enumerable) and carries `toHeaders`,
59
+ * so `structuredClone` rejects it and it cannot enter tracked scope by accident.
60
+ *
61
+ * @example Dev → prod is the same two lines
62
+ * ```ts
63
+ * // dev
64
+ * const credentials = staticTokens({ github: 'ghp_dev_xxx' });
65
+ * // prod — the tool code does not change
66
+ * const credentials = vaultCredentials({ address: process.env.VAULT_ADDR! });
67
+ * Agent.create({ provider, model, credentials }).build();
68
+ * ```
69
+ */
70
+ import { apiKey, basic, bearer, headers } from '../../identity/kinds.js';
71
+ /** Auth methods a caller may ask for, and the option that would carry each one
72
+ * when it is built. Naming them is the teaching: a refusal that says "not
73
+ * supported" ends the conversation; one that says what it would take starts a
74
+ * field report. */
75
+ const UNBUILT_AUTH_METHODS = {
76
+ approle: '`roleId` + `secretId` (Vault `auth/approle/login`)',
77
+ kubernetes: '`role` + the projected service-account token path (`auth/kubernetes/login`)',
78
+ jwt: '`role` + a signed JWT/OIDC assertion (`auth/jwt/login`)',
79
+ oidc: '`role` + a signed JWT/OIDC assertion (`auth/jwt/login`)',
80
+ aws: '`role` + a signed STS identity document (`auth/aws/login`)',
81
+ cert: 'a client certificate + key, which is a TLS-agent decision, not a header',
82
+ userpass: '`username` + `password` (`auth/userpass/login`)',
83
+ ldap: '`username` + `password` (`auth/ldap/login`)',
84
+ };
85
+ // ─── Public factory ──────────────────────────────────────────────────
86
+ /**
87
+ * Build a {@link CredentialProvider} that reads KV v2 secrets from a
88
+ * Vault-compatible store. See {@link VaultCredentialsOptions} for the
89
+ * per-option contract and this module's docstring for the V1 boundary.
90
+ */
91
+ export function vaultCredentials(options) {
92
+ const id = options.id ?? 'vault';
93
+ if (!options.address || typeof options.address !== 'string' || options.address.trim() === '') {
94
+ throw new TypeError(`${id}: \`address\` is required — the Vault base URL, e.g. ` +
95
+ `'https://vault.internal:8200'. It is not read from VAULT_ADDR on purpose: ` +
96
+ `an agent that picks its vault out of the environment reads a different vault ` +
97
+ `when the environment changes under it.`);
98
+ }
99
+ const address = options.address.replace(/\/+$/, '');
100
+ if (!/^https:\/\//i.test(address)) {
101
+ if (!/^http:\/\//i.test(address)) {
102
+ throw new TypeError(`${id}: \`address\` must be an http(s) URL (got '${address}').`);
103
+ }
104
+ if (!options.allowHttp) {
105
+ throw new TypeError(`${id}: refusing a plain-http address ('${address}'). The Vault token travels in ` +
106
+ `the \`X-Vault-Token\` request header, so over plaintext HTTP anyone on the path ` +
107
+ `reads a token that can usually read every secret it can reach — and a leaked ` +
108
+ `read token is not revoked by rotating one secret. Use https, or set ` +
109
+ `\`allowHttp: true\` deliberately for a loopback dev server.`);
110
+ }
111
+ }
112
+ if (options.auth !== undefined && options.auth !== 'token') {
113
+ const asked = String(options.auth).toLowerCase();
114
+ const wouldTake = UNBUILT_AUTH_METHODS[asked];
115
+ throw new TypeError(`${id}: \`auth: '${String(options.auth)}'\` is not built. V1 authenticates with a ` +
116
+ `TOKEN only — \`token\`, or the \`VAULT_TOKEN\` environment variable.` +
117
+ (wouldTake
118
+ ? ` ${asked} login would arrive on ${wouldTake}, plus a re-login when the ` +
119
+ `returned lease expires.`
120
+ : ` A login method would need its own credentials and a re-login on lease expiry.`) +
121
+ ` This is field-gated rather than guessed: tell us your auth shape (which method, ` +
122
+ `which mount path, which lease behaviour) and it gets built against a real cluster ` +
123
+ `instead of an API document. Until then, exchange it yourself and pass the ` +
124
+ `resulting token as \`token\`.`);
125
+ }
126
+ if (options.paths && options.resolve) {
127
+ throw new TypeError(`${id}: pass \`paths\` OR \`resolve\`, not both — two spellings of one rule can ` +
128
+ `disagree, and the one that loses would do so silently. Use \`paths\` for a fixed ` +
129
+ `map, \`resolve\` when the path is computed from the service id.`);
130
+ }
131
+ const token = options.token ?? readEnv('VAULT_TOKEN');
132
+ if (!token) {
133
+ throw new TypeError(`${id}: no Vault token. Pass \`token\`, or set the VAULT_TOKEN environment variable. ` +
134
+ `(V1 authenticates with a token only — see \`auth\`.)`);
135
+ }
136
+ const mount = trimSlashes(options.mount ?? 'secret');
137
+ const timeoutMs = options.timeoutMs ?? 5000;
138
+ const apiKeyHeader = options.apiKeyHeader ?? 'x-api-key';
139
+ const doFetch = options._fetch ?? ((...args) => fetch(...args));
140
+ /** service → path inside the mount, by whichever arm was configured. */
141
+ function pathFor(service) {
142
+ if (options.paths) {
143
+ const mapped = options.paths[service];
144
+ if (mapped === undefined) {
145
+ throw new Error(`${id}: no secret path configured for service '${service}'. ` +
146
+ `Known services: ${Object.keys(options.paths).join(', ') || '(none)'}.`);
147
+ }
148
+ return trimSlashes(mapped);
149
+ }
150
+ if (options.resolve) {
151
+ const mapped = options.resolve(service);
152
+ if (!mapped) {
153
+ throw new Error(`${id}: \`resolve('${service}')\` returned no path, so this service has no secret ` +
154
+ `to read. Return a path inside the '${mount}' mount, or configure the service ` +
155
+ `elsewhere.`);
156
+ }
157
+ return trimSlashes(mapped);
158
+ }
159
+ // No mapping configured: the service id IS the path.
160
+ return trimSlashes(service);
161
+ }
162
+ return {
163
+ id,
164
+ async getCredential(req) {
165
+ const secretPath = pathFor(req.service);
166
+ // KV v2's read shape. `/data/` is the v2 API segment, not part of your
167
+ // path — `secret/ci/github` in the UI is `secret/data/ci/github` here.
168
+ const url = `${address}/v1/${mount}/data/${secretPath}`;
169
+ const where = `'${mount}/${secretPath}'`;
170
+ const secret = await readSecret({
171
+ url,
172
+ where,
173
+ id,
174
+ service: req.service,
175
+ token,
176
+ namespace: options.namespace,
177
+ timeoutMs,
178
+ doFetch,
179
+ });
180
+ const custom = options.toCredential?.(secret, req.service);
181
+ if (custom)
182
+ return { status: 'issued', credential: custom };
183
+ const credential = mapFieldsToCredential(secret, apiKeyHeader);
184
+ if (!credential) {
185
+ throw new Error(`${id}: the secret at ${where} has none of the field shapes this adapter reads — ` +
186
+ `\`token\`, \`api_key\`/\`apiKey\`/\`key\`, \`username\`+\`password\`, or ` +
187
+ `\`headers\`. (The fields it DOES have are deliberately not named here: an ` +
188
+ `error message reaches the model and the credential.failed event.) Rewrite the ` +
189
+ `secret in one of those shapes, or pass \`toCredential\` to map your own.`);
190
+ }
191
+ // No `expiresAt`: a KV v2 secret has no lease, so there is no expiry to
192
+ // report and inventing one would be worse than absence. V1 re-reads on
193
+ // every call, which is the library's model since 9.7.0.
194
+ return { status: 'issued', credential };
195
+ },
196
+ };
197
+ }
198
+ /**
199
+ * GET one KV v2 secret and return its inner `data.data` object.
200
+ *
201
+ * Every throw below names the provider id, the service, the mount path and (for
202
+ * an HTTP failure) the status. None of them can name the token, a response body
203
+ * or a field of the secret — that restraint IS the contract, because this
204
+ * message becomes a tool result the model reads and a `credential.failed`
205
+ * payload every observer receives.
206
+ */
207
+ async function readSecret(args) {
208
+ const { id, where, service } = args;
209
+ let res;
210
+ try {
211
+ res = await args.doFetch(args.url, {
212
+ method: 'GET',
213
+ headers: {
214
+ 'X-Vault-Token': args.token,
215
+ accept: 'application/json',
216
+ ...(args.namespace && { 'X-Vault-Namespace': args.namespace }),
217
+ },
218
+ signal: AbortSignal.timeout(args.timeoutMs),
219
+ });
220
+ }
221
+ catch (err) {
222
+ // Transport failure: DNS, TLS, refused connection, timeout. The cause is
223
+ // safe to name — it describes the socket, never the payload — but it is
224
+ // re-wrapped rather than rethrown so no fetch implementation can smuggle
225
+ // request headers into the text.
226
+ throw new Error(`${id}: could not reach Vault for service '${service}' (${where}): ` +
227
+ `${transportReason(err)}.`);
228
+ }
229
+ if (!res.ok) {
230
+ throw new Error(`${id}: Vault returned ${res.status} reading ${where} for service '${service}'` +
231
+ `${statusHint(res.status)}`);
232
+ }
233
+ let body;
234
+ try {
235
+ body = await res.json();
236
+ }
237
+ catch {
238
+ throw new Error(`${id}: Vault's response for ${where} was not JSON (status ${res.status}). ` +
239
+ `Check that \`address\` points at Vault's API and not at a proxy or login page.`);
240
+ }
241
+ const outer = body?.data;
242
+ if (!isRecord(outer)) {
243
+ throw new Error(`${id}: no secret data at ${where} (status ${res.status}). ` +
244
+ `Check the path exists in the KV v2 mount.`);
245
+ }
246
+ const inner = outer.data;
247
+ if (!isRecord(inner)) {
248
+ // A v1 mount answers with the fields directly under `data`, with no inner
249
+ // envelope. That is the one cheap, unambiguous v1 tell, so it is named
250
+ // rather than guessed at.
251
+ throw new Error(`${id}: the response for ${where} is not KV v2 shaped (no \`data.data\` envelope). ` +
252
+ `This adapter reads KV **v2** only, which is why the URL carries the \`/data/\` ` +
253
+ `segment. If '${where.split('/')[0]?.replace(/'/g, '') || 'that mount'}' is a KV v1 ` +
254
+ `mount, upgrade it (\`vault kv enable-versioning\`) or mount v2 — or tell us, and ` +
255
+ `\`kvVersion\` is the option a v1 reader would arrive on.`);
256
+ }
257
+ return inner;
258
+ }
259
+ /** A short, payload-free reason for a transport failure. */
260
+ function transportReason(err) {
261
+ if (err instanceof Error) {
262
+ if (err.name === 'TimeoutError' || err.name === 'AbortError')
263
+ return 'the request timed out';
264
+ return err.name || 'network error';
265
+ }
266
+ return 'network error';
267
+ }
268
+ /** What a status usually means here. Static text keyed by number — it cannot
269
+ * echo a response, because it never reads one. */
270
+ function statusHint(status) {
271
+ if (status === 403) {
272
+ return '. The token is valid but its policy does not allow reading that path.';
273
+ }
274
+ if (status === 401)
275
+ return '. The token is missing, expired or revoked.';
276
+ if (status === 404) {
277
+ return ('. Either the path does not exist, or the mount is not a KV v2 mount ' +
278
+ '(the read URL carries the v2 `/data/` segment).');
279
+ }
280
+ if (status === 503)
281
+ return '. Vault is sealed or standby.';
282
+ return '.';
283
+ }
284
+ // ─── Field → kind ────────────────────────────────────────────────────
285
+ /**
286
+ * The built-in field table, first match wins. Deliberately small: four shapes
287
+ * cover what secrets actually hold, and `toCredential` covers the rest without
288
+ * this function growing an option per spelling.
289
+ */
290
+ function mapFieldsToCredential(secret, defaultApiKeyHeader) {
291
+ const token = str(secret.token);
292
+ if (token)
293
+ return bearer(token);
294
+ const key = str(secret.api_key) ?? str(secret.apiKey) ?? str(secret.key);
295
+ if (key)
296
+ return apiKey(key, str(secret.header) ?? defaultApiKeyHeader);
297
+ const username = str(secret.username);
298
+ const password = str(secret.password);
299
+ if (username && password)
300
+ return basic(username, password);
301
+ const map = secret.headers;
302
+ if (isRecord(map)) {
303
+ const flat = {};
304
+ for (const [k, v] of Object.entries(map)) {
305
+ const s = str(v);
306
+ if (s)
307
+ flat[k] = s;
308
+ }
309
+ if (Object.keys(flat).length > 0)
310
+ return headers(flat);
311
+ }
312
+ return undefined;
313
+ }
314
+ // ─── Small helpers ───────────────────────────────────────────────────
315
+ function isRecord(v) {
316
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
317
+ }
318
+ /** A non-empty string, or undefined. Numbers are NOT coerced: a secret field
319
+ * that is not a string is not a credential this adapter knows how to apply. */
320
+ function str(v) {
321
+ return typeof v === 'string' && v !== '' ? v : undefined;
322
+ }
323
+ function trimSlashes(s) {
324
+ return s.replace(/^\/+|\/+$/g, '');
325
+ }
326
+ /** Read one environment variable, in any runtime. Absent `process` (a browser,
327
+ * a worker) simply means no fallback. */
328
+ function readEnv(name) {
329
+ const p = globalThis.process;
330
+ return p?.env?.[name];
331
+ }
332
+ //# 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,145 @@
1
+ /**
2
+ * fileObservability — the typed event stream, one JSON line per event, in a
3
+ * local file.
4
+ *
5
+ * The sink for the shop that has no collector. Every other adapter in this
6
+ * folder ships somewhere: CloudWatch, X-Ray, an OTLP endpoint. A great many
7
+ * on-premises deployments have none of those — they have a directory, a log
8
+ * shipper (Filebeat, Fluent Bit, Vector, `promtail`, `journald`, or a person
9
+ * with `grep`), and a rule that nothing leaves the network. NDJSON on disk is
10
+ * the format all of those already read, so the sink is the file.
11
+ *
12
+ * Zero dependencies: `node:fs`, lazily required at construction so merely
13
+ * importing `agentfootprint/observe` stays browser-safe (this factory is
14
+ * Node-only; calling it in a browser throws by name).
15
+ *
16
+ * ## The line
17
+ *
18
+ * One `JSON.stringify(event)` per line, newline-terminated, appended in
19
+ * dispatch order — the SAME envelope `cloudwatchObservability` puts in a log
20
+ * event, so a query written against one reads the other:
21
+ *
22
+ * ```jsonl
23
+ * {"type":"agentfootprint.agent.turn_start","payload":{…},"meta":{"runId":"…","sessionId":"…"}}
24
+ * {"type":"agentfootprint.stream.tool_end","payload":{…},"meta":{…}}
25
+ * ```
26
+ *
27
+ * Nothing is summarized, bounded or redacted on the way out. **A payload that
28
+ * must not be on that disk must not reach this strategy** — narrow it with
29
+ * `eventTypes` / `tier` / `sampleRate`, or apply a footprintjs
30
+ * `RedactionPolicy` upstream, exactly as with every other sink. (For a bounded
31
+ * record by construction, `auditExport({ payloadMode: 'bounded' })` is the
32
+ * adapter that does that job.)
33
+ *
34
+ * ## Buffered, not synchronous
35
+ *
36
+ * `exportEvent` is sync and never touches the disk: it serializes, buffers, and
37
+ * returns. Batches are appended asynchronously on a size trigger
38
+ * (`maxBufferEvents` / `maxBufferBytes`), on a timer (`flushIntervalMs`), and on
39
+ * `flush()`. A hard kill therefore loses at most the buffer — the price of not
40
+ * making telemetry a term in agent-loop latency. Call `flush()` (or
41
+ * `agent.shutdown()`, which does) at process end; see the 8.12.0 lifecycle laws
42
+ * on {@link BaseStrategy.flush}.
43
+ *
44
+ * ## Rotation is ONE generation, and that is deliberate
45
+ *
46
+ * With `maxBytes` set, a batch that would push the file past the ceiling first
47
+ * renames it to `<path>.1` — **replacing any previous `.1`** — and starts a
48
+ * fresh file. That is the whole policy. There is no `.2`, no compression, no
49
+ * time-based schedule, no cross-process coordination (two processes writing one
50
+ * file each keep their own byte count and will both rotate it). It exists so an
51
+ * unattended agent cannot fill a disk, and for nothing else: **retention is a
52
+ * log-management daemon's job**, and `logrotate` with `copytruncate`, Fluent Bit,
53
+ * or a systemd timer will do it properly. Omit `maxBytes` — the default — and
54
+ * this adapter never renames anything, which is the right choice when a real
55
+ * rotator already owns the file.
56
+ *
57
+ * @example
58
+ * ```ts
59
+ * import { fileObservability } from 'agentfootprint/observe';
60
+ *
61
+ * const telemetry = agent.enable.observability({
62
+ * strategy: fileObservability({
63
+ * path: '/var/log/agentfootprint/events.ndjson',
64
+ * maxBytes: 64 * 1024 * 1024, // safety ceiling; logrotate owns retention
65
+ * }),
66
+ * });
67
+ *
68
+ * // … run …
69
+ * await agent.shutdown(); // flushes + stops everything enabled
70
+ * ```
71
+ */
72
+ import type { AgentfootprintEvent, AgentfootprintEventType } from '../../events/registry.js';
73
+ import type { ObservabilityStrategy } from '../../strategies/types.js';
74
+ export interface FileObservabilityOptions {
75
+ /** Absolute or relative path to the NDJSON file. **Required.** Its parent
76
+ * directories are created (`recursive`) and the file itself is claimed at
77
+ * construction, so an unwritable path fails where you wrote it rather than
78
+ * at the first event — the only moment a caller is still watching. */
79
+ readonly path: string;
80
+ /** Rotation ceiling in bytes. Omitted → **never rotates** (the right choice
81
+ * when `logrotate` or a shipper already owns the file). Set → a batch that
82
+ * would cross the ceiling first renames the file to `<path>.1`, replacing
83
+ * any previous `.1`, and starts fresh. ONE generation, no compression, no
84
+ * cross-process coordination — see the note in this module's docstring. */
85
+ readonly maxBytes?: number;
86
+ /** Max events buffered before a forced append. Default 100. */
87
+ readonly maxBufferEvents?: number;
88
+ /** Max buffered payload bytes (UTF-8) before a forced append. Default 65536
89
+ * (64 KB) — a local write is cheap, so this is far larger than the network
90
+ * adapters' 10 KB. */
91
+ readonly maxBufferBytes?: number;
92
+ /** Forced-append interval when traffic is sparse, in ms. Default 1000.
93
+ * `0` disables the timer — only size triggers and `flush()` write. */
94
+ readonly flushIntervalMs?: number;
95
+ /** Narrow what lands on the disk. Becomes the strategy's
96
+ * {@link ObservabilityStrategy.relevantEventTypes}, so the dispatcher does
97
+ * not even forward the rest — the filter costs nothing at the hot path.
98
+ * Omitted → every event the `tier` lets through is written. */
99
+ readonly eventTypes?: readonly AgentfootprintEventType[];
100
+ /**
101
+ * Where delivery failures go — a full disk, a revoked permission, a path
102
+ * whose directory was removed under a long-running process.
103
+ *
104
+ * Same law as the network adapters (8.11.0): **telemetry that fails
105
+ * invisibly is indistinguishable from telemetry that works.** Unhandled,
106
+ * failures reach a rate-limited `console.error`. The batch that failed is
107
+ * dropped, never requeued, so a disk that has been full for an hour cannot
108
+ * grow the buffer without bound.
109
+ */
110
+ readonly onError?: (error: Error, event?: AgentfootprintEvent) => void;
111
+ /** Test seam — inject a filesystem. Bypasses `node:fs` entirely, which is
112
+ * also what lets the rotation policy be asserted without a real disk. */
113
+ readonly _fs?: FileSinkFs;
114
+ }
115
+ /**
116
+ * The slice of the filesystem this adapter touches — five calls, named by what
117
+ * the adapter uses them FOR rather than by their `node:fs` signatures.
118
+ *
119
+ * Sync members run once, at construction; the append path is async so the agent
120
+ * loop never waits on a disk.
121
+ */
122
+ export interface FileSinkFs {
123
+ /** Create the log file's parent directory chain. */
124
+ mkdirSync(dir: string, options: {
125
+ readonly recursive: boolean;
126
+ }): void;
127
+ /** Claim the file at construction. This is the writability refusal: an
128
+ * unwritable path throws HERE, with the caller still on the stack. */
129
+ appendFileSync(file: string, data: string): void;
130
+ /** Current size, so the rotation counter starts from what is already there
131
+ * rather than from zero on every restart. */
132
+ statSync(file: string): {
133
+ readonly size: number;
134
+ };
135
+ /** Append one batch of NDJSON lines. */
136
+ appendFile(file: string, data: string): Promise<void>;
137
+ /** Rotate: `<path>` → `<path>.1`, replacing any previous `.1`. */
138
+ rename(from: string, to: string): Promise<void>;
139
+ }
140
+ /**
141
+ * NDJSON-to-a-local-file observability strategy. See
142
+ * {@link FileObservabilityOptions} for the per-option contract, and this
143
+ * module's docstring for the rotation policy and what is NOT bounded.
144
+ */
145
+ export declare function fileObservability(opts: FileObservabilityOptions): ObservabilityStrategy;