import { type Server } from 'node:net'; import { signRequest, type NodeKeyPair } from '@forgezero/runtime/identity'; import { type ManagedVaultSocketRequest, type ManagedVaultSocketResponse } from '@forgezero/runtime/managed-vault-socket'; import type { DeploymentVaultBindingsV3 } from '@forgezero/access/vault'; import type { SecretCache } from './cache'; /** * The application Vault socket — why managed code holds no remote credential. * * `@forgezero/vault` discovers `/run/forgezero/vault.sock` and prefers it over * `FORGEZERO_API_KEY`, so moving an app onto managed compute is DELETING an * environment variable. That only means anything if something is listening, and * for a long time nothing was: the client knew how to prefer the socket and the * agent was a version constant. * * ## The node identity never becomes an application signing oracle * * The Agent hybrid-signs its own outbound HTTPS requests internally. This * socket never exposes `identity`, `sign` or `attest`: otherwise any tenant * process admitted to the Vault group could ask the Agent to sign a deployment, * heartbeat or broad replication request and act as the node without ever * extracting its key. Applications receive only exact entries they request * from the already-bound, requested-entry-only memory cache. * * ## Peer credentials, not a shared secret * * Access control is the filesystem's: the socket is created 0660 under the * dedicated `forgezero-vault` service group. An application service account is * admitted by group membership; unrelated users and the deployment runner are * not. A token checked over the socket would have to live somewhere both sides * can read, which is the problem this exists to remove rather than a solution. */ export declare class AgentError extends Error { readonly code: string; constructor(code: string, message: string); } /** One line of JSON in, one line of JSON out. */ export type Request = { op: 'identity'; } | { op: 'sign'; method: string; path: string; query?: string; body?: string; } | { op: 'attest'; nonce?: string; } | ManagedVaultSocketRequest; export type Response = { ok: true; op: 'identity'; nodeKey: string; publicKeys: NodeKeyPair['ed25519'] & { mlDsa: string; }; } | { ok: true; op: 'sign'; envelope: ReturnType; } | { ok: true; op: 'attest'; report: string; source: string; } | ManagedVaultSocketResponse; /** The real Unix-socket boundary; privileged machine operations stay in-process. */ export declare function handleApplicationRequest(options: AgentOptions, request: unknown): Promise; /** * Produces a hardware attestation report for this guest. * * Injected and absent by default. A SEV-SNP report is meaningful only when the * API verifies it against the server challenge, catalog measurement, TCB floor * and AMD chain; this socket merely obtains the signed bytes. * * So with no source configured, `attest` REFUSES. It does not return an empty * report, and it does not silently succeed: a caller that believes it verified * an attestation when nothing did is worse off than one told plainly that * attestation is unavailable here. */ export interface AttestationSource { readonly name: string; report(nonce: string): Promise; } export interface AgentOptions { socketPath: string; /** PID 1-owned listener retained while the Agent drains and restarts. */ listenFd?: number; keys: NodeKeyPair; /** The node's identifier, as the platform knows it. */ nodeKey: string; attestation?: AttestationSource; /** * Secrets, cached in memory on this guest. * * Optional: an agent that only signs is a complete agent, and a box with no * Deployment bound to it has nothing to cache. When absent, `get` says so rather * than returning nothing — an application that receives an empty value for a * database URL fails somewhere far from here. */ cache?: SecretCache; /** Exact V3 bindings attached only by a fenced native release claim. */ vaultBindings?: DeploymentVaultBindingsV3; /** Every request, for the hash-chained journal. */ record?: (entry: { op: string; outcome: 'ok' | 'refused'; detail?: string; }) => void; } /** * Initialize the stable proxy route without overwriting an in-progress * candidate handover. The link target is local to the runtime directory, so it * cannot redirect the credential-bearing proxy outside its fixed socket tree. */ export declare function ensureAgentSocketRoute(routePath: string, activePath: string): void; export declare function handleRequest(options: AgentOptions, request: Request): Promise; /** * Listen, with the socket file replaced rather than reused. * * A stale socket from a killed process makes `listen` fail with EADDRINUSE, and * an agent that will not start after an unclean shutdown is an outage that needs * a human. The file is unlinked first — safe because only one agent runs per * guest, and a live one would be holding the path. */ export declare function startAgent(options: AgentOptions): Server;