Skip to content

fix(judge): stop env expansion from corrupting literal API keys - #156

Open
addyCooks wants to merge 3 commits into
Nano-Collective:mainfrom
addyCooks:fix/128-literal-api-keys-survive-env-expansion
Open

fix(judge): stop env expansion from corrupting literal API keys#156
addyCooks wants to merge 3 commits into
Nano-Collective:mainfrom
addyCooks:fix/128-literal-api-keys-survive-env-expansion

Conversation

@addyCooks

Copy link
Copy Markdown
Contributor

Description

expandEnvVar ran a global regex over every string in judge.json, and one of
its two branches matched a bare $NAME anywhere in the value. An API key pasted
verbatim from a provider console the input path judge configure asks for
was silently rewritten at load time. The file on disk stayed correct, so
inspecting it proved nothing; the corruption happened on every load.

Substitution now fires only when a value is entirely one ${VAR} or
${VAR:-default} reference — the form the docs promise and the unbraced
branch is gone. These values are credentials, so the rule is exact: a value
either is a reference, or it is a literal that round-trips byte for byte.

An unset reference is left intact rather than collapsing to '', and
loadJudgeConfig raises an error naming the variable. Leaving the placeholder
alone is not by itself enough to make the failure name the variable without
the check the literal ${VAR} just reaches the provider and returns the same
anonymous 401. apiKey: config.apiKey ?? 'dummy-key' becomes || at the one
call site where '' genuinely means "no key configured", which is what the
local-server placeholder was always for.

Closes #128

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update

Testing

Automated Tests

  • All existing tests pass (pnpm test:all completes successfully)
  • New tests added for new functionality (if applicable)

Manual Testing

  • Tested nanotune init
  • Tested nanotune data commands (add/import/list/validate)
  • Tested nanotune train
  • Tested nanotune export
  • Tested nanotune benchmark

Checklist

  • Code follows project style guidelines (pnpm format)
  • Self-review completed
  • Documentation updated (if needed)
  • No breaking changes (or clearly documented)

expandEnvVar ran a global regex over every string in judge.json, and one of
its two branches matched a bare `$NAME` anywhere in the value. An API key
pasted verbatim from a provider console — the input path `judge configure`
asks for — was silently rewritten at load time:

  sk-ant-api03-AB$CD-EF  ->  sk-ant-api03-AB-EF
  sk-live$SECRET_PART-a  ->  sk-live-a

There was no escape syntax and no way to opt out. judge.json on disk stayed
correct, so inspecting the file proved nothing; the corruption happened on
every load, and the user-visible result was a provider 401 with a
correct-looking key, which sends people to debug their provider account
rather than nanotune.

Substitute only when the value is *entirely* one `${VAR}` or
`${VAR:-default}` reference — the form the docs actually promise — and drop
the unbraced branch. These values are credentials, so the rule is now exact:
a value either is a reference, or it is a literal that round-trips byte for
byte. A `${VAR}` embedded in a longer string is a literal under that rule and
is no longer expanded.

The second half is the empty-string case. An unset variable resolved to '',
and `apiKey: config.apiKey ?? 'dummy-key'` does not catch '' — `??` only
catches null/undefined — so an empty bearer token went out and came back as
the same anonymous 401. An unset reference now stays intact and
loadJudgeConfig names the variable that is missing, so the error points at
the shell rather than the provider. `??` becomes `||` at the one call site
where '' genuinely means "no key configured", which is what the local-server
`dummy-key` placeholder was always for.

Closes Nano-Collective#128
Every key shape from the issue is asserted to round-trip, table-driven so a
new shape is one line. The variables they look like — SECRET_PART, CD, A,
LEADING — are exported to a sentinel first: with them unset a surviving
expansion produces '' and the key merely looks truncated, so the test could
pass for the wrong reason. Against the pre-fix implementation 11 of these
fail, including all four literal-key cases.

Six existing tests encoded the behaviour being removed — unbraced `$MY_VAR`,
an unset variable collapsing to '', and expansion of references embedded in a
longer string. They are rewritten to assert the new contract rather than
deleted, since that contract is the fix.

loadJudgeConfig is covered end to end for the first time: the literal key
survives a real read of judge.json, the documented `${VAR}` form still
resolves, an unset variable raises an error naming it, and a default keeps
that error from firing. Set-but-empty and an empty `${VAR:-}` default are
pinned separately — both must win over the leave-it-alone fallback, which is
why the resolution order uses `??` rather than `||`.

env-substitution.ts 97.22%; judge.ts 71.93% -> 74.53%.
The page promised `${ENV_VAR}` support without saying where the boundary
was, which is how a literal key containing `$` came to be treated as a
reference. Spell out that only a whole-value reference is substituted, show
the shapes side by side including one that is now deliberately literal, and
show the error a user gets when the variable is unset.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Env-var expansion silently corrupts literal API keys containing $

1 participant