agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
179 lines • 10.3 kB
TypeScript
/**
* artifacts/gcsArtifacts — the claim-check store in a Cloud Storage bucket.
*
* The same five verbs, the same laws, a different vendor's client. Read
* `objectStore.ts` first for the laws both object adapters obey; read
* `s3Artifacts.ts` beside this one to see what is genuinely per-vendor rather
* than per-implementation. Three things really are different here, and each
* one changes what an operator pays:
*
* 1. **A listing carries the metadata.** `bucket.getFiles()` hands back File
* objects with their metadata already populated — creation time, size AND
* the custom entry holding the ticket. So `list()` is ONE call per page
* and needs no per-row read, where the S3 adapter must HEAD each row it
* returns. Same port, same promise, cheaper here; said out loud because
* "the cloud adapters behave identically" is true of the CONTRACT and not
* of the bill.
* 2. **A bigger metadata budget.** Custom metadata is capped at 8 KiB
* (keys and values together), against 2 KB on the other column. The
* ticket is checked against it at put and refused by name, so the cap is
* a stated limit and not a surprise from the service.
* 3. **The client has no command objects.** Methods hang off a chain —
* `storage.bucket(b).file(k).save(...)` — which is why this column's pin
* test asserts a METHOD CHAIN rather than a set of command names.
*
* ── The object key ──────────────────────────────────────────────────────────
* `[<prefix>/]<tenant>/<principal>/<conversation>/<ref>`, scope-partitioned by
* `scopePath.ts` — the same percent-encoding law as the directory adapter, so
* a tenant of literally `'..'` is a NAME here too. A ref alone opens nothing:
* a wrong scope computes a different object name, the service answers 404, and
* the caller reads `null`.
*
* ── What a 404 is allowed to mean here ──────────────────────────────────────
* A missing object and a missing bucket look the SAME on this column
* (`code: 404`, reason `notFound`, differing only in prose this adapter will
* not parse). So the split is made by the call instead: only a read of one
* named object may read a 404 as "not there". A 404 from a save or a listing
* is not an answer to anything the caller asked — it goes through the
* sanitizer, because nothing downstream converts it and the client's own text
* for it carries the object name.
*
* ── Retention, and the operator's bulk tool ─────────────────────────────────
* `ttlMs` stamps `expiresAt` AT MINT (stated, never sprung); expiry is
* enforced on READ and the expired object is deleted on the way past; budgets
* evict oldest-first (an object store has no cheap read-recency). A put SCANS
* the scope only when a byte/count budget is configured — with no budget there
* is nothing to plan, and a put stays a single upload.
*
* Reclaiming what nobody reads again is **Object Lifecycle Management**, the
* operator's bulk tool. This adapter does not create rules: a lifecycle rule
* is a cost and compliance decision that belongs to your infrastructure.
* Align it like this:
*
* ```jsonc
* // Delete objects 7 days after creation. Keep the rule LONGER than the
* // store's ttlMs, never shorter: `expiresAt` is the promise printed on the
* // ticket, and a lifecycle rule that deletes first makes a live ticket
* // resolve to null BEFORE the time it stated. Longer, and lifecycle is what
* // it should be — the backstop for what the store's own sweep never
* // revisited.
* { "lifecycle": { "rule": [
* { "action": { "type": "Delete" },
* "condition": { "age": 7, "matchesPrefix": ["artifacts/"] } }
* ]}}
* ```
*
* ── Lazy peer dependency ────────────────────────────────────────────────────
* `@google-cloud/storage` is an OPTIONAL peer dependency, required at
* CONSTRUCTION (the sqliteSessions law): importing the barrel costs a browser
* bundle nothing, and a missing install refuses where the config was written.
* Pass `storage` to share the client your app already built.
*
* ── What that peer's TREE carries, said out loud ────────────────────────────
* Optional means it is your dependency and not this package's — nothing here
* loads it unless you call `gcsArtifacts` — but you inherit its tree when you
* do. Two independent audits (2026-08-13, 2026-08-14) reported the same five
* MODERATE advisories, and they reproduce here:
*
* `@google-cloud/storage` → `retry-request` → `teeny-request` → `gaxios` → `uuid`
*
* rooted in `uuid` (GHSA-w5hq-g745-h8pq — a missing buffer bounds check in
* v3/v5/v6 when `buf` is provided). It is not a defect in this adapter and
* there is no line here that would fix it: the chain is entirely inside
* Google's client.
*
* **Do not take `npm audit fix --force`.** Its resolution installs
* `@google-cloud/storage@5.18.3` — a major DOWNGRADE to a client from a
* different era of the API. Trading a bounds check in a path this adapter does
* not exercise for a client several majors behind the service is a different
* outage, not a security improvement, and this package will never pin you to
* it. Pin the newest 7.x yourself and watch the upstream chain.
*
* ── Status ──────────────────────────────────────────────────────────────────
* **Field-validated** (was "awaiting field use" through 9.28.0). The method
* chain it calls is pinned against the really-installed package by
* `test/adapters/google/google-surface-pin.test.ts`, and the behaviour is
* proved offline against an emulation double that speaks the same chain — but
* the promotion rests on live evidence, not on those:
*
* An independent trial ran THIS adapter, unchanged since 9.25.0, against a real
* Cloud Storage bucket (`@google-cloud/storage@7.22.0`, uniform bucket-level
* access, public-access prevention on) and all nine checks passed: JSON
* put/head/get with a verified SHA-256, scope isolation refusing another
* tenant / principal / conversation, two distinct cursor pages, a byte-exact
* native `putStream`/`getStream`, a TTL that expired and lazy-deleted, a
* `maxCountPerScope` eviction reporting `max-count`, an oversize label refused
* BEFORE upload against the 8 KiB metadata budget, a missing bucket answering
* with the documented ambiguous `null` on read and a sanitized 404 on write
* that leaked neither key nor scope, and an idempotent repeated delete.
* (FINDINGS "Cloud Storage `gcsArtifacts()` — field PASS".)
*
* What that does NOT promote: the same evidence says nothing about a bucket
* with soft delete or object versioning enabled — the trial's bucket had both
* off, on purpose, so the evidence would be disposable.
*/
/// <reference types="node" />
import { type ArtifactRetention } from './retention.js';
import { type ArtifactStore } from './types.js';
/** One object's metadata, as this adapter reads it. */
export interface GcsFileMetadataLike {
readonly size?: string | number;
readonly timeCreated?: string;
readonly contentType?: string;
/** The custom entries — where the ticket rides. */
readonly metadata?: Record<string, unknown>;
}
/** One object handle, as this adapter calls it. */
export interface GcsFileLike {
/** Populated by a listing; a File built by `bucket.file()` may not have it. */
readonly name?: string;
readonly metadata?: GcsFileMetadataLike;
save(data: Uint8Array | string, options?: unknown): Promise<unknown>;
download(options?: unknown): Promise<[Uint8Array]>;
getMetadata(options?: unknown): Promise<[GcsFileMetadataLike, unknown?]>;
delete(options?: unknown): Promise<unknown>;
createReadStream(options?: unknown): NodeJS.ReadableStream;
createWriteStream(options?: unknown): NodeJS.WritableStream;
}
/** One bucket handle, as this adapter calls it. */
export interface GcsBucketLike {
file(name: string): GcsFileLike;
getFiles(query?: unknown): Promise<[GcsFileLike[], unknown?, unknown?]>;
}
/** The client, as this adapter calls it. */
export interface GcsStorageLike {
bucket(name: string): GcsBucketLike;
}
/** The module shape this adapter loads. */
export interface GcsSdkModule {
readonly Storage?: new (config: Record<string, unknown>) => GcsStorageLike;
}
/** Options for {@link gcsArtifacts}. */
export interface GcsArtifactsOptions {
/** The bucket. It must already exist — this library never creates one. */
readonly bucket: string;
/** Object-name prefix inside the bucket, so a bucket can be shared. */
readonly prefix?: string;
/** Project id for the client this factory builds. Ignored when `storage` is
* passed — that client's configuration is yours. */
readonly projectId?: string;
/** Your own pre-built client; configuration and credentials stay yours. */
readonly storage?: GcsStorageLike;
/** Retention dials. Budgets evict OLDEST-first (no cheap read-recency). */
readonly retention?: ArtifactRetention;
/** @internal Test seam — the SDK module, injected. */
readonly _sdk?: GcsSdkModule;
/** @internal Test seam — a client injected past the SDK entirely. */
readonly _storage?: GcsStorageLike;
/** @internal Test seam — the clock. Defaults to `Date.now`. */
readonly _now?: () => number;
}
/**
* An artifact store in a Cloud Storage bucket.
*
* @example
* const store = gcsArtifacts({ bucket: 'my-agent-artifacts', prefix: 'artifacts' });
* const agent = Agent.create({ provider, artifacts: store });
*/
export declare function gcsArtifacts(options: GcsArtifactsOptions): ArtifactStore;
//# sourceMappingURL=gcsArtifacts.d.ts.map