Skip to content

feat(runtime): deliver credentials as environment variables, not conversation text - #747

Open
gavin09527 wants to merge 1 commit into
mainfrom
feat/runtime-secret-store
Open

feat(runtime): deliver credentials as environment variables, not conversation text#747
gavin09527 wants to merge 1 commit into
mainfrom
feat/runtime-secret-store

Conversation

@gavin09527

Copy link
Copy Markdown
Contributor

Item 2 of the agent-config-control-plan: credentials must reach the agent without their plaintext passing through the model context.

Why

Tools the agent drives in direct mode (gh, git, docker, kubectl, lark-cli) authenticate themselves, so credentials have to exist on the machine. The goal is therefore not "no credentials on disk" β€” that is unachievable β€” but "no plaintext credential in the model's context". A value that has been in the context has already been sent to the model provider, and no local deletion can recall it.

How

The control plane drops one file per credential into <ZYLOS_DIR>/security/credentials/, named exactly as the environment variable (GH_TOKEN, content = the value). At launch these become real environment variables of the agent process, so the agent only ever handles the name:

gh auth login --with-token <<< "$GH_TOKEN"   # shell substitutes; value never enters the transcript

The security/ naming is a deliberate soft wall β€” current models' safety training makes them reluctant to cat files under such a path, lowering the chance of an accidental read. It is not a hard control; the hard guarantees remain server-side revocation and the provider-side kill switch.

Injecting at launch rather than through PM2 is intentional: PM2 persists a service's env into ~/.pm2/dump.pm2 and prints it via pm2 env <id>, which would create extra plaintext copies in places nobody thinks to revoke.

Security guard

PROTECTED_NAMES refuses PATH, HOME, SHELL, LD_PRELOAD, NODE_OPTIONS and friends. Without it, whatever can write into the secret directory could redirect every command the agent runs β€” a credential drop must not become a code-execution primitive. Enforced both at load and at apply.

Failure behaviour

  • Every rejection is reported, never silently dropped. A credential that quietly fails to load looks to the agent exactly like "this tool is not configured" β€” a confusing way to discover a typo in a filename.
  • Loose permissions warn but still load: availability beats tidiness for a credential that is already on disk.
  • Exactly one trailing newline is stripped (echo secret > FILE adds one and it would otherwise become part of the credential).
  • Compat mode included β€” ZYLOS_CLEAN_ENV=false must not silently opt a host out of credential delivery, or the agent would report tools as unconfigured for no visible reason.
  • Logs list injected names only, never values.

Revocation

Delete the file and restart the runtime. Removing a variable from a live process is not reliably possible, so the restart is the mechanism.

Testing

16 new tests in cli/lib/__tests__/secret-store.test.js: protected-name refusal, invalid names, empty files, dotfiles, trailing-newline handling, permission warning, precedence over a same-named .env value, compat mode, and "a rejection is reported rather than silent".

No regression β€” verified by stashing the change and re-running: the 35 pre-existing suite failures are identical before and after. Note node --test alone fails on mock.module; use node scripts/run-node-tests.js, which passes --experimental-test-module-mocks.

Not in this commit

The skill-side rule (agent should look for the env var name and never read the secret directory) and the manifest/template docs.

πŸ€– Generated with Claude Code

…ersation text

Item 2 of the agent-config-control-plan: credentials must reach the agent
without their plaintext passing through the model context.

Tools the agent drives in direct mode (gh, git, docker, kubectl, lark-cli)
authenticate themselves, so credentials have to exist on the machine. The goal
is therefore not "no credentials on disk" β€” that is unachievable β€” but "no
plaintext credential in the model's context". A value that has been in the
context has already been sent to the model provider, and no local deletion can
recall it.

The control plane drops one file per credential into
`<ZYLOS_DIR>/security/credentials/`, named exactly as the environment variable.
At launch these become real environment variables of the agent process, so the
agent only ever handles the *name*: it runs
`gh auth login --with-token <<< "$GH_TOKEN"` and the shell substitutes the
value, which never reaches the transcript.

Injecting at launch rather than through PM2 is deliberate: PM2 persists a
service's env into `~/.pm2/dump.pm2` and prints it via `pm2 env <id>`, which
would create extra plaintext copies in places nobody thinks to revoke.

Security guard: `PROTECTED_NAMES` refuses PATH, HOME, SHELL, LD_PRELOAD,
NODE_OPTIONS and friends. Without it, whatever can write into the secret
directory could redirect every command the agent runs β€” a credential drop must
not be a code-execution primitive.

Every rejection is reported rather than silently dropped: a credential that
quietly fails to load looks to the agent exactly like "this tool is not
configured", which is a confusing way to discover a typo in a filename. Loose
file permissions warn but still load β€” availability beats tidiness for a
credential that is already on disk.

Applies in compat mode too, so `ZYLOS_CLEAN_ENV=false` cannot silently opt a
host out of credential delivery. Logs list injected names only, never values.

Revocation stays: delete the file and restart. Removing a variable from a live
process is not reliably possible, so the restart is the mechanism.

Tests: 16 new (`cli/lib/__tests__/secret-store.test.js`) covering protected-name
refusal, invalid names, empty files, trailing-newline handling, permission
warnings, precedence over .env, and compat mode. Verified no regression by
stashing: the 35 pre-existing suite failures are identical before and after.
Note `node --test` alone fails on `mock.module`; use
`node scripts/run-node-tests.js`, which passes --experimental-test-module-mocks.

Still to come (not in this commit): the skill-side rule telling the agent to
look for the env var name and never read the secret directory, plus manifest
template docs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

1 participant