Soul pass 7, S7-1. The exploration always called loadConfig with no
arguments, and the candidate socket alone is refused (it needs the base
URL), so neither the Idunn candidate path nor --state-root was ever run: a
secret read only there (Y6, Y7) went unseen.
The recipe's [service] arguments after the entry script, each binding
given a probe path, are now the argv of an Idunn-shaped run, together with
a loopback candidate socket and its base URL. That run is added on its own
and to every combined run. A test pins the parsed argv, that both
environment names are declared in the recipe, and that loadConfig accepts
the shape.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Soul pass 6, F5 and F6. The all-at-once run set every name to one probe
value and died in loadConfig's first lines, so reads gated on a specific
value (Z4) or on two values late in the function (Z5) were missed, and a
copy of process.env taken at module load (Z6) was never the recorder.
Each name now gets each probe value alone, and the probe values include
every string literal in config.ts; the combined runs set every name at once
to values loadConfig accepted for it alone. config.ts is imported afresh
while the recorder is process.env, so a module-scope capture records too.
The remaining limit is stated in the test.
failureOf in the app tests now prints the error with util.inspect as well,
so a non-enumerable cause carrying the body (Z3) shows.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Soul pass 5, S5-7. The recording environment saw only the reads an empty
environment makes, so a secret read on a branch a value opens (X6) was
missed, and a read from a spread copy of the environment (X7) was
invisible.
loadConfig now runs over many environments until no run reads a new
name: every secret found so far set in plaintext, then each name read so
far set to each of five probe values, then all of them set to one probe
value at once; a run that throws keeps the reads it made first. Copying or
enumerating the environment (the Proxy's ownKeys and
getOwnPropertyDescriptor traps) is recorded, and a test refuses it, since
reads from a copy cannot be seen. HMAC joins the secret-shaped backstop
pattern.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Soul pass 4, F3: the recipe test found Heimdall's secrets by parsing
readSecretInput calls out of the source, and an alias, a parenthesised
first argument or a colliding local name hid a new secret from it (S1-S3).
The test now runs the real loadConfig against an environment that records
every name looked up, standing in for process.env as well, and takes each
`_FILE` lookup as a secret: readSecretInput makes one per secret, however
the call is spelled and whatever list computes the name. Each must be
declared as `_FILE` in the recipe and never under its plaintext name. The
source scan and its table of computed names are deleted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Soul pass 3 (R3): the recipe test's list of secret names was written by
hand, and its secret-shaped pattern missed TOKEN, API_KEY, PRIVATE_KEY,
SALT, CREDENTIAL and DSN. A new secret could be read in the source and
declared in plaintext in the recipe without a failure.
The test now takes the names from every readSecretInput call under src/: a
literal as written, the two computed names over the lists that compute
them, and a failure for any argument it cannot expand. Every one of those
must be declared as its _FILE and never by its plaintext name. The pattern
is wider, with two named exemptions: the signing key's path and its
bootstrap flag.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Soul S2: no test read deployment/idunn/recipe.toml, so a plaintext secret
name could be declared again (mutant ME) or a published _FILE name dropped
(mutant MF) with the suite green.
tests/recipe-secrets.test.ts reads the service's declared names and asserts
that no secret-shaped name is declared except as _FILE, that none of the
secrets Heimdall reads is declared under its plaintext name, and that every
provider client-secret file the witness publishes, every published app
profile's shared-secret file, and the database URL, token key and patron
secret files are declared.
The app-profile rule found two gaps: the witness publishes the repixelizer
and streampixels profiles and Heimdall reads their shared-secret files, but
the recipe declared neither, so an Idunn binding could not configure them.
Both are declared as optional, as GitHub and Spotify were for Soul F5.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>