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