/** One `/usr/bin/security` invocation result; the Keychain runner's return shape. */ export interface LoopsKeychainCommandResult { /** Exit status; null when the tool could not be started or was killed. */ status: number | null; stdout: string; stderr: string; } /** Tier-1 credential inputs and Keychain-tier controls (an injected runner in tests). */ export interface LoopsKeychainTierOptions { /** Whether the Keychain tier runs for a caller-built env (default: ambient only). */ enabled?: boolean; /** Defaults to `process.platform`; the tier exists only on `"darwin"`. */ platform?: string; /** The machine's host name, used as the account when `HASNA_STATION` is unset. */ hostname?: () => string; /** The `security` runner; defaults to spawning `/usr/bin/security` by argv. */ run?: (argv: readonly string[]) => LoopsKeychainCommandResult; } /** The credential-chain options the shared resolver applies, spelled locally. */ export interface LoopsCredentialChainOptions { /** Tier 1: an explicit key, e.g. from `--api-key`. */ apiKey?: string; /** Tier 1: an explicit profile name, e.g. from `--profile`. Beats `HASNA_PROFILE`. */ profile?: string; /** Tier 3: Keychain controls — an injected runner in tests. */ keychain?: LoopsKeychainTierOptions; } /** The shared storage client's surface, spelled structurally for the published boundary. */ export interface LoopsStorageClient { readonly baseUrl: string; readonly transport: { readonly baseUrl: string; get(path: string, options?: Record): Promise; post(path: string, body?: unknown, options?: Record): Promise; patch(path: string, body?: unknown, options?: Record): Promise; put(path: string, body?: unknown, options?: Record): Promise; request(method: string, path: string, body?: unknown, options?: Record): Promise; }; } export type CloudStorageResolution = { transport: "file"; client: null; } | { transport: "api"; client: LoopsStorageClient; baseUrl: string; }; export type Env = Record; /** The explicit local opt-in env key; `=file` selects the on-box SQLite store. */ export declare const LOOPS_CONNECTION_ENV_KEY = "HASNA_LOOPS_CONNECTION"; export interface CloudStorageOptions { /** Tier-1 credential inputs and Keychain-tier controls (an injected runner in tests). */ credentials?: LoopsCredentialChainOptions; } /** Every env name that can configure a loops authority or credential, resolver-derived. */ export declare function loopsAuthorityEnvKeys(): string[]; /** * Does the ENVIRONMENT itself configure a loops authority or credential? * * Deliberately env-only: answering it must not touch the Keychain or the * filesystem, because doing so would defeat the isolation the opt-in * short-circuit exists to provide. A DECLARED-BUT-BLANK variable counts as * absent here — blank has always been this package's spelling for "not * configured" — but it is NOT absent once we do go hosted: the resolver * refuses a declared blank loudly rather than resolving around it. */ export declare function hasLoopsEnvAuthorityIntent(env: Env): boolean; /** True when the operator spelled the explicit local opt-in (`=file`). */ export declare function isLoopsFileOptIn(env: Env): boolean; /** True when this environment should be served by the on-box SQLite store. */ export declare function selectsLoopsLocalStore(env: Env): boolean; /** The same ambient test @hasna/contracts performs, run BEFORE any normalisation. */ export declare function isAmbientLoopsEnv(env: Env): boolean; /** * The environment as the resolver should see it: every authority/credential * variable that is DECLARED BUT BLANK removed. * * A blank has always been this package's spelling for "not configured" — it is * how the CLI test harnesses and fail-closed fixtures scrub an inherited * environment. @hasna/contracts takes the opposite and, for its purposes, * correct view: a declared-but-blank credential is a misconfiguration it * refuses loudly rather than resolving around. Both are right at their own * layer. Normalising here keeps "blank means unset" true at the loops seam * while leaving the resolver's stricter rule intact for everything it does * receive: a value that is present is still policed, and two aliases that * actually disagree still refuse. */ export declare function loopsResolverEnv(env: T): T; /** The env object and credential options a loops surface hands @hasna/contracts. */ export interface LoopsResolverInputs { /** The environment with every declared-but-blank authority variable removed. */ env: T; /** The chain options, with the Keychain tier's ambient gate already decided. */ credentials: LoopsCredentialChainOptions; } /** * Build the resolver's inputs: the normalised environment AND the credential * options that keep the machine's Keychain tier reachable across it. * * WHY THIS IS NOT JUST {@link loopsResolverEnv}: blanking a variable and * deleting it are not the same operation to @hasna/contracts, because dropping * a key forces us to hand the resolver a COPY, and the resolver gates its * ambient tiers on OBJECT IDENTITY (`env === process.env`, or the registry * symbol its own snapshot carries). A copy is, by that test, a caller-built * world — the hermetic seam — so the Keychain is outside it and the tier turns * itself off. Silently. On a station whose Keychain holds * `hasna.credentials.loops.api-key`, ONE declared-but-blank authority variable * would drop the run from the Keychain identity to whatever came next. The * gate is therefore decided HERE, on the original env, and carried across the * copy as the documented `keychain.enabled` control rather than being left to * an identity test the copy cannot pass. An explicit `enabled` from the caller * still wins, and an injected `run` is left alone. */ export declare function loopsResolverInputs(env: T, credentials?: LoopsCredentialChainOptions): LoopsResolverInputs; /** * Say — once per process, on stderr — that this install is running against the * on-box file store. * * Local mode is legitimate for loops (a persistent local loop runner), but it * is still announced: "no credential resolved" and "deliberately offline" * look identical in the output otherwise, and the first one is usually a * misconfiguration the operator wants to hear about. */ export declare function noticeLocalLoopsMode(write?: (line: string) => void): void; /** Test seam: forget that the local-mode line was printed. */ export declare function resetLocalLoopsModeNotice(): void; /** * Resolve whether `name`'s data lives behind the hosted `/v1` API or in the * explicitly selected local store for the current environment. * * The hosted decision comes from `@hasna/contracts` 1.0.2's shared resolver, * fresh on every call: the CLI, the MCP server and the SDK all go through * here, so a key rotation on a machine heals without a restart, and a station * needs no inline env prefix at all. Never returns partially-built remote * state and never exposes the API key. Throws when hosted is implied but no * credential resolves — the client never falls back to the on-box file. */ export declare function resolveCloudStorage(name: string, env?: Env, options?: CloudStorageOptions): CloudStorageResolution; /** * Throw when the client connection for `name` is not explicitly configured: * neither a hosted credential nor the explicit local opt-in. Used by the * surfaces that must not silently report a file connection no data command * would use. */ export declare function requireConfiguredConnection(name: string, env?: Env, options?: CloudStorageOptions): void;