UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

167 lines (166 loc) • 12.2 kB
/** * Where a text holds a credential — answered by ONE table, {@link CREDENTIAL_VOCABULARY}, and the three views read * off it: * - refuse-write ({@link refuseWriteView}): the rows that carry a `strict` form, in table order. A hit fail-closed * rejects a durable write, so a strict form is narrow (vendor-distinctive prefix, long body, word boundary): a * wrongly refused ordinary note is that face's head risk. * - egress ({@link egressPassGroups}): every row's wide `match`, in table order, grouped into passes. A hit replaces * the value in text that is persisted and forwarded, so a wide form is broad: a missed credential is that face's * head risk and over-redaction is cheap. * - env names ({@link envNameView}): the exact credential names, the credential words by position, the * connection-string name shape and the token-prefix value predicate a spawned child's environment is judged by. * - record keys ({@link recordKeyView}): the same credential words, read as the name of a member of a structured record * (a header name, a config member) rather than of an environment variable. * * A family's strict and wide forms live on the SAME row, and every text the strict form matches the wide form matches * too (machine-judged over the fixture file by `scripts/verify-credential-vocabulary.mjs`): the two faces may differ in * floors, never in vocabulary. Detection is shared; each layer renders its own marker. * * Zero imports by design: this module is the detection definition and moves into its own package as written. */ /** How sure a detection is. */ export type RedactionConfidence = "high" | "medium" | "low"; /** Which rule table a finding names as its minter. A closed set: a report's entries can only have been written by * the engine's own tables, so "this finding says X" is an engine statement. */ export type RedactionSource = "arg-summary" | "untrusted-egress" | "secret-env"; /** One value family: "this run of characters is itself a credential". */ export interface ValueFamilyRow { /** Stable family name, `^[a-z0-9-]+$`; a finding's `family`. The refuse-write label is this id spelled with `_`. */ readonly id: string; /** The finding `kind` this family reports (the coarse class a consumer groups by). */ readonly kind: string; readonly source: RedactionSource; readonly confidence: RedactionConfidence; /** The wide form (egress), global. A capture group 1, when the form has one, is a label kept verbatim ahead of the * marker (`AccountKey=`, `api_key=`). */ readonly match: RegExp; /** The strict form (refuse-write), never global. Present = this family refuses a durable write; absent = it does * not (refusing is the tightening, so it is written, never implied). */ readonly strict?: RegExp; /** The egress pass this wide form runs in. Rows naming the same pass sit next to each other and join as ONE * alternation, so a token is taken at its leftmost start whichever of them it is (a nested shape is never cut by * an earlier family's pass). Absent = a pass of its own. */ readonly pass?: string; } /** Where a credential word counts in a name (env names are upper-cased words joined by `_`): * - `leading` — at the start of the name or right after `_`; * - `plural` — the word plus `S`, in the leading position; * - `counted-plural` — a plural, unless a count word (`MAX_TOKENS`) stands right in front of it; * - `separated` — only right after `_` (the shell's own `PWD` is none; a record has no such variable, so the record-key * view reads it as `leading`); * - `embedded` — anywhere (`AUTHTOKEN`, `MYSECRET`). * In every position the word must end the name or be followed by `_` or a digit (`KEYBOARD` is no word). */ export type CredentialWordPosition = "leading" | "plural" | "counted-plural" | "separated" | "embedded"; /** One credential word: "a name holding this word names a credential". */ export interface CredentialWordRow { readonly word: string; readonly position: CredentialWordPosition; /** Further words that name the same credential as a record key only, read in this row's position by the record-key * view alone (`AUTH` → the `Authorization` header). The env-name view never reads them: a variable named * `AUTHORIZATION` is not withheld by its name. */ readonly recordKeys?: readonly string[]; } /** The URL userinfo rendering every renderer follows: keep the username (an auditor can tell whose account leaked * and revoke it) and replace the password, or replace the whole userinfo. */ export interface VocabularyPolicy { readonly urlUserinfo: "keep-username" | "whole"; } export interface CredentialVocabulary { /** Value families, in egress pass order. */ readonly values: readonly ValueFamilyRow[]; readonly words: readonly CredentialWordRow[]; /** The words that turn a following counted plural into a number. */ readonly countWords: readonly string[]; /** A connection string named as such (`DB_CONN_STR`, `CONNECTION_STRINGS`), case-insensitive. */ readonly connectionStringName: RegExp; /** Credential env names, each also judged under the `INPUT_` prefix (a GitHub Actions input). */ readonly exactEnvNames: readonly string[]; /** Credential-shaped names that hold no credential: declared names, never a pattern over names (the one pattern is * {@link CredentialVocabulary.nonSecretNameShape}). Three readings of the same list: the env-name view and the * record-member reading ({@link isSensitiveRecordKey}) take each as the WHOLE name (`CACHE_KEY` holds a setting, * `SECRET_CACHE_KEY` a secret, in an environment and in a config or detail record alike); the HTTP header-name * reading ({@link isSensitiveHeaderName}) takes it as the whole name or the TAIL of a longer one whose front part names * no credential word (`cf-aig-cache-key`, `X-Idempotency-Key`: a gateway's header for the same non-secret thing; * `secret-cache-key` stays a secret). */ readonly nonSecretExactNames: readonly string[]; /** The one name shape that holds no credential whatever words it carries: git's `GIT_CONFIG_KEY_<n…>` names a * configuration key (its value sits in `GIT_CONFIG_VALUE_<n>`). Read by every name view, beside the exact names. */ readonly nonSecretNameShape: RegExp; /** A leading vendor prefix that spells a credential word without being one (`AUTH0_`): dropped from a name before the * credential words are looked for, in every name view alike. */ readonly nonCredentialPrefix: RegExp; /** A value (or a password-less URL user) opening with a known token prefix. */ readonly tokenPrefix: RegExp; readonly policy: VocabularyPolicy; } /** * The URI scheme, bounded so it cannot backtrack: RFC 3986's scheme production plus `_`, at most 64 characters. An * unbounded `\w+` made every `://` pattern quadratic in the longest unbroken word run (one long base64 paste cost * seconds of synchronous CPU); the bound makes it linear and redacts no less on any URL. */ export declare const URI_SCHEME_SOURCE: string; /** The fewest characters a string can have and still be read as a credential: the floor of a vendor-prefixed token's body * and of a bearer / authorization credential run in this table, and of the value rules of the env scrub. A shorter * string names no secret, whoever holds it. */ export declare const CREDENTIAL_MIN_CHARS = 8; export declare const CREDENTIAL_VOCABULARY: CredentialVocabulary; /** The refuse-write label of a family: its id spelled with `_` (surfaced in the refusal; never the matched value). */ export declare const refuseLabel: (id: string) => string; /** One refuse-write form: the family's label and its strict form. */ export interface RefuseWriteForm { readonly label: string; readonly re: RegExp; } /** The refuse-write view: every row carrying a strict form, in table order (the first hit names the label). */ export declare function refuseWriteView(vocabulary: CredentialVocabulary): readonly RefuseWriteForm[]; /** The egress view: the rows in table order, each run of adjacent rows naming the same `pass` as one group. */ export declare function egressPassGroups(vocabulary: CredentialVocabulary): ReadonlyArray<readonly ValueFamilyRow[]>; /** The env-name view: what {@link CREDENTIAL_VOCABULARY} says about variable names and token-prefixed values. */ export interface EnvNameView { /** Upper-cased, `-` read as `_`; each name also under `INPUT_`. */ readonly exactNames: ReadonlySet<string>; readonly nonSecretExactNames: ReadonlySet<string>; readonly nonSecretNameShape: RegExp; readonly nonCredentialPrefix: RegExp; /** A credential word in its position, case-insensitive. */ readonly credentialWord: RegExp; readonly connectionStringName: RegExp; /** A credential word that is a whole `_` segment of a name (every position but `embedded`; a counted plural no count * word touches), as a regex source. */ readonly segmentWord: string; readonly tokenPrefix: RegExp; } export declare function envNameView(vocabulary: CredentialVocabulary): EnvNameView; /** `camelCase` / `PascalCase` read as `_`-separated words, `OAuth` and `NextAuth` kept whole. */ export declare function splitCamelCase(name: string): string; /** The record-key view: what {@link CREDENTIAL_VOCABULARY} says about the names a structured record carries — the member * of a config or detail record and the HTTP header — the credential words of the env-name view, each row's `recordKeys` * spellings besides, and a `separated` word standing alone (`pwd`, `pass`: a record has no shell variable of that name). * The names the vocabulary declares non-secret stay non-secret here too, as the whole name; the header-name reading * ({@link isSensitiveHeaderName}) alone also takes one as the tail of a longer name — a gateway prefixes the header it * names the same non-secret thing with (`cf-aig-cache-key`), where a secret named `secret_cache_key` lives in a * body or a config record and is read whole. */ export interface RecordKeyView { /** A credential word in its position, case-insensitive. Applied to a key with `-` read as `_`, as written and * camel-split ({@link isSensitiveRecordKey}, {@link isSensitiveHeaderName}). */ readonly credentialWord: RegExp; readonly nonSecretExactNames: ReadonlySet<string>; readonly nonSecretNameShape: RegExp; readonly nonCredentialPrefix: RegExp; } export declare function recordKeyView(vocabulary: CredentialVocabulary): RecordKeyView; /** Does `key`, the name of a record's MEMBER (`Proxy-Authorization`, `pwd`, `X_API_KEY`, `clientSecret`), name a credential? * Case does not matter, `-` and `_` are one. A name the vocabulary declares non-secret is none as the WHOLE name only * (`partition_key`; `x_partition_key` and `secret_cache_key` are still read by their words): a * secret is named by the system that holds it, and what this reading decides is whether a value is shown or kept out of * a record. The HTTP header-name reading is {@link isSensitiveHeaderName}. `view` is the vocabulary's record-key view * unless a caller holds another. */ export declare function isSensitiveRecordKey(key: string, view?: RecordKeyView): boolean; /** Does `name`, an HTTP header's name (`Authorization`, `X-Api-Key`, `cf-aig-cache-key`), name a credential? The * record-key reading ({@link isSensitiveRecordKey}) plus one exemption: a declared non-secret name is none also as the * TAIL of a longer name, at a `_` / `-` boundary, whose front part holds no credential word (`cf-aig-cache-key`, * `X-Idempotency-Key`, `X-Partition-Key`; `secret-cache-key`, `clientAuth-cache-key` and `xcache-key` * are still read by their words). A gateway prefixes the header it names the same non-secret thing with, and the reading * that mistakes such a header for a credential drops the key that should have gone out. `view` is the vocabulary's * record-key view unless a caller holds another. */ export declare function isSensitiveHeaderName(name: string, view?: RecordKeyView): boolean;