Secret values stay out of ordinary output

link-secret-value holds every secret Link holds, in one of its types by where it may go, so a wrong way doesn't compile. Beside them are the one HMAC every signature is, the one way in a post from outside is checked by, a process safe to hold secrets, and the audit that keeps each secret in its type.

Its types

Each type says where its secret may go: nowhere, to a program by name, into a signature, or sealed to one recipient.

OpaqueSecret

One Link uses and nobody reads, such as an agent's proof secret or a service's signing secret. It signs and checks; there is no way to its bytes.

Secret

A value another program or service must be given, such as a model's API key, a connection's token or a password. It's read only by asking for it by name, Secret::expose, at a place this package's audit lists; it's never serialized but by bare, never copied but by Secret::duplicate.

SigningSecret

An Ed25519 key pair's private half, such as a machine's key, which signs and says its public half, and nothing else.

Sealed

Either, encrypted to one recipient, the only form that's saved or sent as it is: HPKE (RFC 9180) in its base mode, with X25519, HKDF-SHA256 and ChaCha20-Poly1305. What a sealed value is (About: whose, what kind, which version) is its additional data, so it opens only as what it was sealed as.

A recipient for one transfer

The recipient's pair is made for one transfer (Recipient) and dropped when it's done, so a stream captured on the way can never be opened later. A sealed stream (stream) seals a fresh ChaCha20-Poly1305 secret in its header; numbered records authenticate their length and order, including a required empty final record, and a failed writer never makes a successful end.

What none of them does

None prints what it holds: Debug writes <redacted>, so a log line, an error or a struct printed with {:?} carries none. None has Display: what takes one would send <redacted> as the value, so it doesn't compile, and the value is exposed there, by name. Their memory is zeroed when they're dropped, and compared in constant time (subtle).

hmac

The one HMAC (SHA-256, on ring) every signature Link makes or checks is, and the one hex a signature is written in: an agent's proof, a bridge's, a webhook's. No other workspace signs or decodes a signature itself.

inbound

Every post from outside that a secret vouches for (a connection's webhook, a payment's, a repository's, an agent's own) is checked here, built once. A receiver says how its sender signs, as a value (Scheme); it never writes verification.

What Inbound::verify gives every receiver

The body capped before anything is read (Refused::TooBig); closed by default, one without a secret refusing every post (Refused::Unconfigured), and nothing builds one that takes anyone; a constant-time compare, of the secret or of its HMAC; and a signed time checked against its window (Refused::Stale).

Replays, rolling and answers

The delivery's id, for a Seen store to refuse a replay (Inbound::take); both the current secret and the one before it tried, so any secret rolls with no restart (Secrets); and one answer for each refusal (Refused::status).

process

A process safe to hold secrets, and its children given none but on purpose.

guard

First in every program that holds a secret: no core dump, no other process of its user reading its memory, its environment or its descriptors (/proc/<pid> is root's once it isn't dumpable), and its memory locked out of swap where the machine allows it.

Secret::from_env

Marks the variable it read as taken, and takes it out of the environment while the process has one thread, since changing the environment with another thread running isn't safe.

command and ours

command starts every child: its environment the parent's but every variable taken and every one whose name says it's a secret's (names_a_secret). ours starts a child that's one of Link's own programs: the same, and every secret this one holds handed to it on a pipe, where its Secret::from_env finds them, so a key you set for lnk reaches the plugin that reads it.

The pipe

It goes to that child alone: every child started here closes it, so it reaches no program the child starts in turn, but a program of its own not built on this package (a plugin from outside Link) gets it as Link's own do.

ends_with_parent, stand_in_front and carried

ends_with_parent starts a child that ends when this process does, however it ends; stand_in_front, a child this process waits for, its stop passed on to it; and carried, a program run for the one that started this one, as a wrapper runs it, with the environment it was given for it, whole.

give

The one way to hand a child a secret in its environment, for a program that reads one nowhere else: each a place the audit lists, with why.

OwnedChild

process::OwnedChild owns a subprocess and its process group on Unix. It observes exit without reaping the leader, stops the group before reaping and refuses numeric group signals if another reaper has taken ownership. Its child borrow is for pipes and identity only, never wait or try_wait. release transfers the complete cleanup obligation to a separate owner, as a durable service controller does. Descendants must remain in the owned group for group cleanup to cover them.

The audit

Every folder runs it over its own code, from a test of its own (cargo test -p link-secret-value --test audit for this one, with each package's secrets.toml or audit.toml): a secret leaves its type only where a list says it does, so a new place fails until it's listed, which a review sees. Code under tests/, or in a #[cfg(test)] module, isn't read.

Counted against the list

exposes: Secret::expose and its kin, and Recipient::open_sealed and open_json, counted by file against [exposes]. bare: a secret written as its value (bare, OpaqueSecret::keep) against [bare]. children: a secret given in a child's environment on purpose (process::give) or its environment passed on whole (process::carried) against [children].

Never listed

copies: an exposed value copied out unsaid (clone, to_string, format! and the like). argv: an exposed value in a command's arguments. env: a variable that may hold a secret, by its setting or its name (…_TOKEN, …_KEY, …_SECRET, …_PASSWORD, …_AUTH, …_CREDENTIALS, …DATABASE_URL), read with env::var or a helper that returns no Secret.

Never listed either

old ways: zeroize named outside this package, a Debug written by hand to hide something, or a constant-time compare written by hand. children: a child started with this process's environment whole, a secret put in its environment by hand, or the environment's values read whole. A type merely ending in Command, such as AvailableCommand, starts no process. programs: a main of a package that uses this one whose first statement isn't process::guard.

Fields

A field named like a secret (a word of it token, key, secret, password, auth, credential, apikey) whose type names none of the secret types, a path, a number or a flag, against [fields], which maps "<file> <field>: <type>" to why it holds none, so a field of another type by the same name is a new one.

What it can't see

It reads code as text, so a review still sees a value exposed through more than one binding before it reaches .env or .arg; a Command made in one statement and given its environment in another (start it with process::command); a helper that returns the environment's values under its own name; and a child started by a library.