/** * @file Azure Blob Storage backing for {@link SessionStateStore} + * {@link ArtifactStore}. * * This module supports two coexisting authentication modes; the legacy * mode is preserved verbatim so the in-cluster `scripts/deploy-aks.sh` * flow, local Docker storage, and CI all keep working untouched. * * - **Connection-string (legacy, default)**: pass `AZURE_STORAGE_CONNECTION_STRING`. * `AccountName` + `AccountKey` are parsed from the conn string into a * `StorageSharedKeyCredential`, which is reused to mint short-lived * read-only SAS URLs in {@link SessionBlobStore.generateArtifactSasUrl}. * * - **Managed identity (opt-in, bicep-deploy flow)**: set * `PILOTSWARM_USE_MANAGED_IDENTITY=1` *and* * `AZURE_STORAGE_ACCOUNT_URL=https://.blob.core.windows.net`. * The factory uses {@link DefaultAzureCredential} (workload-identity in * AKS, `az login`/env creds locally). No shared key is available, so * `generateArtifactSasUrl()` throws with * `code = "NotSupportedInManagedIdentityMode"` and callers must stream * artifacts through the worker (see TUI/portal proxy paths) rather * than handing a direct SAS URL to the client. * * Selection is done by {@link createSessionBlobStore}; see that function * for the precedence rules. `useManagedIdentity` is *not* inferred from * the absence of a connection string — it is an explicit opt-in flag so * unmigrated stamps stay on the legacy path. */ import { ContainerClient, StorageSharedKeyCredential } from "@azure/storage-blob"; import { type SnapshotCommitInput, type SnapshotCommitResult, type SnapshotHydrateResult, type SnapshotProbe, type VersionedSnapshotStore } from "./snapshot-protocol.js"; import { type ArtifactDownloadResult, type ArtifactMetadata, type SessionStateStore, type ArtifactStore, type ArtifactUploadOptions, type SnapshotCodec } from "./session-store.js"; /** * Epoch-chain snapshot blob: `S.e.tar.br`, one blob per chain. The name * must NEVER end `.tar.gz` — that is the only shape shipped resource-manager * purge binaries collect as delete candidates, and this name invisibility * (not fail-closed parsing in new code) is what protects retained epochs * from an old binary. See the key-shape invariant in snapshot-protocol.ts. */ export declare function epochSnapshotBlobName(sessionId: string, epoch: number): string; /** * Snapshot-blob metadata for a CAS commit, written atomically with the * content by single-shot Put Blob. Epoch chains additionally carry * `psepoch` — the key already scopes them; the field makes listings * self-describing. */ export declare function snapshotCommitBlobMetadata(args: { version: number; turnKey: string; contentHash: string; codec: SnapshotCodec; rawSizeBytes: number; epoch?: number; }): Record; /** * Configuration for constructing a {@link SessionBlobStore} against an * already-built `ContainerClient`. Used by the managed-identity path * (where there is no connection string to parse) and by tests that want * to inject a mocked client. * * @internal */ export interface SessionBlobStoreClientConfig { containerClient: ContainerClient; containerName: string; /** * Optional `StorageSharedKeyCredential` used solely to mint * read-only SAS URLs in {@link SessionBlobStore.generateArtifactSasUrl}. * In managed-identity mode this is intentionally `null`/absent — * SAS generation will throw `NotSupportedInManagedIdentityMode` so * callers (TUI / portal) know to proxy downloads through the worker * instead of relying on shared-key SAS. */ sharedKeyCredential?: StorageSharedKeyCredential | null; sessionStateDir?: string; } /** * Manages session state in Azure Blob Storage. * * - `dehydrate()` — tar + upload session dir, remove local files * - `hydrate()` — download + untar session dir * - `checkpoint()` — tar + upload without removing local files * - `exists()` / `delete()` — blob lifecycle * * Two construction modes: * - **Connection string** (legacy / local / `scripts/deploy-aks.sh`): * `new SessionBlobStore(connectionString, containerName?, sessionStateDir?)`. * Parses `AccountName` + `AccountKey` out of the conn string for SAS URL * generation. This is what every current caller uses. * - **Managed identity** (new bicep-deploy flow when * `PILOTSWARM_USE_MANAGED_IDENTITY=1`): construct via * {@link createSessionBlobStore} or pass a {@link SessionBlobStoreClientConfig}. * No shared key is available, so SAS URL generation throws. * * @internal */ export declare class SessionBlobStore implements SessionStateStore, ArtifactStore, VersionedSnapshotStore { private containerClient; private containerName; private credential; private sessionStateDir; private snapshotSizeBySession; constructor(connectionStringOrConfig: string | SessionBlobStoreClientConfig, containerName?: string, sessionStateDir?: string); /** * Dehydrate a session: tar, upload, remove local files. * Frees the worker slot for another session. */ dehydrate(sessionId: string, meta?: Record, epoch?: number): Promise; /** * Hydrate a session: download tar from blob, extract to local disk. * No-op if local session files already exist. */ hydrate(sessionId: string, epoch?: number): Promise; /** * Checkpoint: upload current session state to blob without removing local files. * Used for crash resilience — the session stays warm in memory. */ checkpoint(sessionId: string, epoch?: number): Promise; /** Size-cache key: legacy family caches under the bare id, epoch chains under their blob name. */ private sizeCacheKey; getSnapshotSizeBytes(sessionId: string, epoch?: number): Promise; /** Check if a dehydrated session exists in blob storage. */ exists(sessionId: string, epoch?: number): Promise; /** Delete a dehydrated session from blob storage (epoch >= 1: only that epoch's chain). */ delete(sessionId: string, epoch?: number): Promise; /** * Remove the legacy family AND every epoch chain for the session (real * session deletion). Epoch objects are enumerated by prefix and deleted * ONLY when the fail-closed parser accepts the name — anything else * under the prefix is logged and left alone. */ deleteAllEpochs(sessionId: string): Promise; private static readonly COMMIT_MAX_ATTEMPTS; private static readonly SINGLE_SHOT_MAX_BYTES; private snapshotBlobName; private probeFromMetadata; private headSnapshot; probeSnapshot(sessionId: string, epoch?: number): Promise; commitSnapshot(sessionId: string, input: SnapshotCommitInput, epoch?: number): Promise; hydrateSnapshot(sessionId: string, epoch?: number): Promise; private artifactBlobPath; /** * Upload an artifact file (e.g. .md) to blob storage. * Max 1MB content. */ uploadArtifact(sessionId: string, filename: string, content: string | Buffer, contentType?: string, opts?: ArtifactUploadOptions): Promise; /** * Data-plane write: stream a worker-local file straight to blob storage. * The body never transits a buffer larger than the SDK's block size — * and, crucially, never transits a model context window. */ uploadArtifactFromFile(sessionId: string, filename: string, filePath: string, contentType?: string, opts?: ArtifactUploadOptions): Promise; /** * Server-side copy between sessions. Bytes move store-to-store through * this process; no model tokens, no worker filesystem. */ copyArtifact(fromSessionId: string, fromFilename: string, toSessionId: string, toFilename?: string, opts?: ArtifactUploadOptions): Promise; /** * Download an artifact file from blob storage. * Returns the file content as a string. */ downloadArtifact(sessionId: string, filename: string): Promise; statArtifact(sessionId: string, filename: string): Promise; setArtifactPinned(sessionId: string, filename: string, pinned: boolean): Promise; downloadArtifactText(sessionId: string, filename: string): Promise; /** * List artifact files for a session. * Returns filenames (not full blob paths). */ listArtifacts(sessionId: string): Promise; deleteArtifact(sessionId: string, filename: string): Promise; /** * Check if an artifact exists. */ artifactExists(sessionId: string, filename: string): Promise; /** * Generate a short-lived read-only SAS URL for an artifact. * The TUI uses this to download files without needing blob credentials. * * @param sessionId Session that owns the artifact * @param filename Artifact filename * @param expiryMinutes How long the URL is valid (default: 1 minute) * @returns Full SAS URL string */ generateArtifactSasUrl(sessionId: string, filename: string, expiryMinutes?: number): string; /** * Delete all artifacts for a session. Pinned artifacts survive unless * `includePinned` is set — a parent's cleanup or failure must not * destroy deliverables that were explicitly marked to outlive it. */ deleteArtifacts(sessionId: string, opts?: { includePinned?: boolean; }): Promise; } /** * Environment shape consumed by {@link createSessionBlobStore}. We accept a * loose `Record` so callers can pass either * `process.env` or a curated env map (the deploy orchestrator's * `loadEnv()` output, the worker's `options`, etc.) without juggling * types. * * @internal */ export interface SessionBlobStoreEnv { /** * Blob-specific managed-identity flag — takes precedence over * `PILOTSWARM_USE_MANAGED_IDENTITY` whenever it is set (to any * value, truthy or not). * * Deploy overlays (bicep-deploy / waldemort) set THIS name for blob * auth while reusing the unsuffixed name for database AAD auth, so * the two can legitimately disagree (blob=1, db=0). Reading only the * unsuffixed name here made the portal silently fall back to the * filesystem artifact store while workers wrote to blob — agents * could exchange artifacts, but every portal/TUI/MCP download and * listing returned "not found". */ PILOTSWARM_BLOB_USE_MANAGED_IDENTITY?: string; /** * `1` / `true` selects managed-identity mode. When set, the factory * requires `AZURE_STORAGE_ACCOUNT_URL` and ignores any * `AZURE_STORAGE_CONNECTION_STRING` value. * * Legacy/shared name: also doubles as the database AAD flag in some * deploys — prefer `PILOTSWARM_BLOB_USE_MANAGED_IDENTITY` for blob. * * Why a flag and not "MI iff conn string is absent"? Because we want * the legacy code path (connection string → shared-key credential → * shared-key SAS) to remain the default for the existing * `scripts/deploy-aks.sh` flow, local Docker storage, CI, and any * stamp that hasn't migrated. The flag is the explicit opt-in that * the bicep-deploy orchestrator sets in the worker overlay * ConfigMap. */ PILOTSWARM_USE_MANAGED_IDENTITY?: string; /** `https://.blob.core.windows.net` — required in MI mode. */ AZURE_STORAGE_ACCOUNT_URL?: string; AZURE_STORAGE_CONNECTION_STRING?: string; AZURE_STORAGE_CONTAINER?: string; } /** * Pick the right `SessionBlobStore` implementation based on env. Returns * `null` when no Azure storage backing is configured (caller falls back * to the filesystem store). * * Selection (first match wins): * 1. MI flag truthy + `AZURE_STORAGE_ACCOUNT_URL` → managed-identity * mode. The MI flag is `PILOTSWARM_BLOB_USE_MANAGED_IDENTITY` when * set (blob-specific, wins even when explicitly falsy), else the * legacy shared `PILOTSWARM_USE_MANAGED_IDENTITY`. Uses * {@link DefaultAzureCredential}, which picks up the * workload-identity token in AKS or `az login` / env-var creds * locally. SAS URL minting will throw — callers must proxy * downloads. * 2. `AZURE_STORAGE_CONNECTION_STRING` set → legacy connection-string * mode. Identical to pre-MI behaviour. Used by the existing * `scripts/deploy-aks.sh` flow, local Docker storage, CI, and any * stamp that hasn't switched the flag on. * 3. `AZURE_STORAGE_ACCOUNT_URL` set but neither credential path * enabled → throw. An account URL with no way to authenticate is a * misconfiguration; silently handing the caller `null` (→ empty * filesystem store) is how the portal served "artifact not found" * for months of blob-backed worker writes. * 4. Otherwise → `null`. * * @internal */ export declare function createSessionBlobStore(env: SessionBlobStoreEnv, opts?: { sessionStateDir?: string; }): SessionBlobStore | null; //# sourceMappingURL=blob-store.d.ts.map