@sema-agent/core
Version:
Stateless, task-oriented AI agent core
172 lines (171 loc) • 16.2 kB
TypeScript
/**
* 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;