UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

172 lines (171 loc) • 16.2 kB
/** * The environment a spawned child inherits: which of this process's variables it must not see. * * A child the engine spawns may run what the MODEL chose (a tool shell, a gate over model-written code, a language * server). Under the `"scrub"` policy (the default) it inherits every variable but those the judgment below names — * deleted whole, never rewritten; `"all"` and a `string[]` allow list are the caller's explicit alternatives * ({@link inheritedHostEnv}). Whatever the policy, an entry that is this deployment's own credential is never inherited: * its name is one the deployment declared, or its value equals a credential value the deployment declared or the * engine holds ({@link declareWithheldEnv}, {@link holdOwnCredentialValues}). One judgment per variable ({@link classifySecretEnvEntry}), the first rule that answers: * 1. exempt — git's environment config and the proxy variables, handed on as written ({@link isExemptEnvName}); * 2. `exact-name` — a known credential name (the vocabulary's exact env names); * 3. `prefix-rule` — the OpenTelemetry settings ({@link OTEL_ENV_RE}); * 4. `namespace-rule` — a credential word as any segment of a name in the engine's own `SEMA_` namespace; * 5. `name-rule` — a credential-shaped name ({@link nameIsCredentialShaped}) off the vocabulary's non-secret exact names; * 6. `value-rule` — a value that carries a credential ({@link looksLikeCredentialValue}). * {@link isSecretEnvKey} and {@link scrubSecretEnv} both answer with this one judgment; rules 2–5 are its name half * ({@link isSensitiveName}), rule 6 its value half, and rule 1 the env-entry exemption read before both. The exempt families, the * credential names, the name shape and the value predicate follow the reference implementation's subprocess-env scrub * as written, three ways stricter: plural `TOKENS` is a credential word unless a count word touches it, the GitHub * token names are not held back from the name shape, and the name shape always runs. */ import { type RedactionConfidence, type RedactionSource } from "./credential-vocabulary.js"; import { type EngineNotice } from "./engine-notice.js"; /** Does a value carry a credential? A URL userinfo (after every `scheme://`, and a leading `//`) with a password or a * token-like user; a scheme-less `user:password@host`; a credential assignment; a webhook URL; a Sonar login; a * private-key header; an `Authorization` header, `token …`, `Bearer …` or `Basic …` with credential-shaped content. * Reads the first {@link VALUE_SCAN_LIMIT} code units. The value half of the judgment, exported apart from the env * entry's exemptions ({@link isExemptEnvName}): a caller holding a value that is no environment variable asks this. */ export declare function looksLikeCredentialValue(value: string): boolean; /** Which rule withheld an entry: one of the scrub's rules, or `own-credential` — the entry is this deployment's own * credential (its name is declared, or its value equals a declared or engine-held credential value: * {@link declareWithheldEnv}), which every policy withholds. */ export type SecretEnvFindingKind = "exact-name" | "name-rule" | "namespace-rule" | "prefix-rule" | "value-rule" | "own-credential"; /** One env entry withheld (deleted) from a child's environment: its name — never its value, so the record is * loggable —, the rule that withheld it, how strong that claim is, and the minting module. */ export interface SecretEnvFinding { key: string; kind: SecretEnvFindingKind; confidence: RedactionConfidence; source: RedactionSource; } /** Does `key` name a credential? The name half of the judgment ({@link sensitiveNameKind}), exported apart from the env * entry's exemptions ({@link isExemptEnvName}): a caller holding a key that is no environment variable (a member of a * record) asks this, and an exempt name is no exception there. */ export declare function isSensitiveName(key: string): boolean; /** Does the `"scrub"` policy withhold `key`? With `value`, exactly the answer {@link scrubSecretEnv} gives for that * entry; without it, the answer for its name alone (the value rule then has nothing to read). */ export declare function isSecretEnvKey(key: string, value?: string): boolean; /** The `"scrub"` policy: `env` without every entry {@link isSecretEnvKey} withholds, the rest as written. `findings` is * an optional caller-owned collector: each withheld entry is pushed onto it (name, rule, confidence), none of them * present in the result. `env` itself is not modified. */ export declare function scrubSecretEnv(env: NodeJS.ProcessEnv, findings?: SecretEnvFinding[]): NodeJS.ProcessEnv; /** How much of this process's environment a child spawned on this host inherits — the `inheritEnv` value. */ export type InheritEnvPolicy = "all" | "scrub" | string[]; /** What {@link declareWithheldEnv} takes: this deployment's own credentials in this process's environment. */ export interface WithheldEnvDeclaration { /** Names of variables that are this deployment's own credentials: never inherited, and an allow list naming one is refused. */ readonly names: readonly string[]; /** Credential values this deployment holds: an inherited entry whose value equals one — the same string, compared * whole, never as a part — is never inherited, whatever its name. A value must be a credential: not white space only, * a flag or placeholder word, one character repeated, or fewer than eight characters. */ readonly values: readonly string[]; } /** * Declare this deployment's OWN credentials in this process's environment — the engine's or its host's: an approval * bearer, a service token — by name and by value. From then on no child spawned on this host inherits one, whatever the * policy: every spawn site reads its environment through {@link inheritedHostEnv}, which subtracts from `"all"`, from the * scrub's result and from an allow list's every entry that is this deployment's own credential (each one present is a * finding of kind `own-credential`), and {@link inheritEnvPolicyProblem} refuses an allow list naming a declared name. * A NAME is compared as the platform compares variable names: exactly, but case-blind on Windows. A VALUE is compared * whole, as the same string: an entry holding it as a part (`Bearer <value>`, a URL, an encoding of it) is not its value. * Besides the declared values, every value the engine took in itself ({@link holdOwnCredentialValues}) is withheld the * same way. A variable a host hands a child itself (a shell's `shellEnv`, a per-exec `env`, a language server's `env`, * an MCP server entry's `env`) is its explicit grant and is not read against the table. * * Declared once, at start-up, before anything spawns; a later call REPLACES the declaration whole (a configuration * reload), read from the next spawn on — a spawn whose environment is already read (one announcing its findings) keeps * it; the values the engine took in stay. The lists are copied, and a value is kept only as a digest (no credential is * held by the table). A malformed table — not a plain object `{ names, values }` (the names-only list of earlier releases * included), a member besides those two, a list that is a Proxy or whose element throws while it is read, a name that * is no variable name or that the host hands every child anyway ({@link runtimeSuppliedEnv}), a value that is not a * string a variable can hold or is no credential ({@link notACredentialReason}: white space only, a flag or placeholder * word, one character repeated, fewer than {@link CREDENTIAL_MIN_CHARS} characters — withholding it would take a setting * such as `CI=true` from every child) or that is, in this process's environment now, the value of such a variable — * throws and leaves the declaration as it was. A value listed twice is the one value (the declaration is a set). No * refusal sentence repeats a value. * * Two Windows readings, measured on a real machine: a withheld name that the child's own shell or runtime re-creates * with a DEFAULT of its own (`ComSpec`, `PATHEXT`, `HOME`, `CommonProgramFiles`, …) shows up in the child's table with * that default, never with this process's value — withheld, not leaked; and the disclosure (`detail.ownCredentials`) * spells each withheld name as this process's environment spells it, not as the declaration spelled it. */ export declare function declareWithheldEnv(table: WithheldEnvDeclaration): void; /** * The engine takes credential values in — each where the engine receives it: a first-party brain's construction * credential (`brainConfigSnapshot`), a model entry's own credential headers (`resolveRouteCredential`), a protocol * peer's credential headers (`resolveProtocolHttpHeaders`), the memory distiller's chat seat key (`openAiCompatChatSeat`, * and `RunnerDeps.memoryConsolidationDriver.chat` at `new Runner`, before that seat is built). From then on, for the * life of the process, no child spawned on this host inherits an entry whose value equals one: the same table, the same * subtraction, finding and disclosure as a declared value ({@link declareWithheldEnv}). A credential the engine is handed * per request (a per-model auth hook's answer) is not taken in: the host that resolves it owns it and declares it. * An absent or empty value adds nothing. A value that is no credential ({@link notACredentialReason}: white space, a * flag or placeholder word, one character repeated, fewer than {@link CREDENTIAL_MIN_CHARS} characters — the key a local * gateway is told to be handed) is not taken in: its holder uses it as given (it is no bad value), and the variables * that hold the same string stay with every child. Said once per process per value, never the value, as the notice * `config.credential_not_withheld` through the engine's one notice throat ({@link deliverEngineNotice}): to `onNotice` * when the intake has the structured seat in hand (the `RunnerDeps` receipt at `new Runner`), else as that notice's * line on `console.warn` (the throat's own arm for an absent seat — a brain's construction, a request's route reading, * a peer's dial, a directly built distiller seat). A value that is, in this process's * environment now, the value of a variable every child is handed whatever the spawn passes cannot be withheld: the intake * throws (`config.invalid_argument`, the variable named, never the value) and takes nothing in — the brain is not built, * the peer is not dialed. */ export declare function holdOwnCredentialValues(values: Iterable<string | undefined>, onNotice?: (notice: EngineNotice) => void): void; /** THE one reading of which HTTP header carries a credential by its NAME — the credential vocabulary's header-name * reading ({@link isSensitiveHeaderName}: `Authorization`, `Proxy-Authorization`, `X-Api-Key`, Azure's `api-key`, * `x-goog-api-key`, `Cookie`, any name holding a credential word; not a gateway's own spelling of a name the * vocabulary declares non-secret, `cf-aig-cache-key` / `X-Idempotency-Key`). The reading of a record MEMBER's name * (`isSensitiveRecordKey`), which decides what a record shows, takes those declared names whole only. Every face that * asks "which header is a credential, by its name" reads this one: the withheld-value intake below * ({@link credentialHeaderValues}) and the route law — which bag carries a credential, which header values a per-model * credential's identity is read from, which headers the per-model winner strips, which the user-header merge leaves to * that strip, which the run-static stream options refuse. The NAME is the shared half: the intake adds a half of its * own, a value that looks like a credential under any name ({@link looksLikeCredentialValue}) — a guess it may make, * because withholding a value from a child costs little; the route law does not, because a header it reads as a * credential replaces the deployment's key and is bound to an origin, and a guess that is wrong drops a key that should * have gone out. */ export declare function isCredentialHeaderName(name: string): boolean; /** Whether a credential header's value is `<auth-scheme> <credentials>` by definition (RFC 9110 §11.6.2 / §11.7.2: * `Authorization`, `Proxy-Authorization`) — the scheme word is public and the credentials part is the secret. Any other * credential header's value is opaque, read whole. */ export declare function isSchemedCredentialHeader(name: string): boolean; /** The credential values a header bag carries: each header whose name is a credential header ({@link isCredentialHeaderName}) * or whose value carries one ({@link looksLikeCredentialValue}), as written and, when it reads `<scheme> <credentials>`, * its credentials part too — the spelling a variable copied from it would carry. A schemed header's * ({@link isSchemedCredentialHeader}) value over a credentials part that is no credential (`Bearer 1`) gives that part * alone — the engine's intake then does not take it in — and not the whole value, which carries nothing else. Any other * header (`User-Agent`, `Accept`, a region) is no credential. */ export declare function credentialHeaderValues(headers: Readonly<Record<string, string>> | undefined): string[]; /** Test seam: never called by production code (a host re-declares `{ names: [], values: [] }`; what the engine took in stays). */ export declare function __resetWithheldEnv(): void; /** * THE environment a child spawned ON THIS HOST inherits: the policy's arm, less this deployment's own credentials — a * declared name, or a value equal to a declared or engine-held credential value ({@link declareWithheldEnv}, * {@link holdOwnCredentialValues}). The arms: `"scrub"` (and absent) is this process's environment through * {@link scrubSecretEnv}; `"all"` is this process's environment as written; a `string[]` is exactly the named * variables that are set, as written — a strict allow list, so a name it leaves out (`PATH` included) is not * inherited. `findings` collects every entry withheld: the scrub's, and each own credential the arm held. * * A policy {@link inheritEnvPolicyProblem} refuses throws that reason HERE, at every spawn, read against the * declaration as it stands: a value that is no policy at all (never read as the scrub), and an allow list naming a * declared own credential — whenever the declaration was made, so the refusal at spawn time is the invariant. The * spawn sites that take a policy (the tool shell's `NodeExecutionEnv` at construction, the oracle gate per gate) also * ask first, so a host that declared before building them hears it at start-up. */ export declare function inheritedHostEnv(policy: InheritEnvPolicy | undefined, findings?: SecretEnvFinding[]): NodeJS.ProcessEnv; /** Why `value` is not an {@link InheritEnvPolicy} this deployment can read (absent counts as one — the scrub), or * `undefined` when it is: a malformed policy, or an allow list naming one of the declared own credentials * ({@link declareWithheldEnv}, compared as it says). */ export declare function inheritEnvPolicyProblem(value: unknown): string | undefined; /** Test seam: never called by production code. */ export declare function __resetSecretEnvScrubNoticeAnnouncement(): void; /** * The ONE disclosure point for a scrub's withheld keys. Routed through {@link deliverEngineNotice} — the * engine's one notice throat — so a wired `onNotice` seat REPLACES the console line exactly as every * other `EngineNotice` does, and an absent seat prints the same words `console.warn` always printed here. * `detail.names` carries every withheld key; `detail.ownCredentials`, present only when one was withheld, the * subset withheld as the deployment's declared own credentials (kind `own-credential`) rather than by the scrub. * Once per PROCESS, never per call or per site; an EMPTY findings array is a no-op. */ export declare function announceSecretEnvScrub(onNotice: ((notice: EngineNotice) => void) | undefined, findings: readonly SecretEnvFinding[]): void;