Skip to content

fix: overlay skill-declared secrets dynamically (closes #48)#51

Open
initializ-mk wants to merge 1 commit into
mainfrom
bug/skill-env-secrets-overlay
Open

fix: overlay skill-declared secrets dynamically (closes #48)#51
initializ-mk wants to merge 1 commit into
mainfrom
bug/skill-env-secrets-overlay

Conversation

@initializ-mk
Copy link
Copy Markdown
Contributor

Summary

Fixes #48 — skill-declared env vars stored in the encrypted secrets file are now passed to downstream binaries.

The hardcoded knownKeys allowlist in Runner.overlaySecrets and OverlaySecretsToEnv is replaced with a dynamic union of the builtin keys and whatever the secrets provider exposes via Provider.List(). Custom skill env vars (e.g. MY_API_KEY) declared in SKILL.md and stored via forge secrets set now reach the OS environment so SkillCommandExecutor and CLIExecuteTool can read them via os.Getenv / os.LookupEnv.

Adding a new skill with new env var names no longer requires any edits to runner.go.

Approach

  • Extracted builtinSecretKeys to a single package-level slice. It is kept as a fallback so providers that cannot enumerate (e.g. EnvProvider.List() returns nil by design) still get LLM/channel keys overlaid via Get().
  • Added secretOverlayKeys(provider) ([]string, error) — returns builtins ∪ provider.List(), deduped. List errors are surfaced but the builtins are still returned, so a failed enumeration degrades to today's behavior rather than dropping the LLM/channel keys.
  • Cross-category secret-reuse detection stays scoped to builtinSecretKeys — only those keys have defined categories in secretCategory. Custom keys have no defined category so they are not part of the check.

Tests

  • TestSecretOverlayKeys_NonEnumerableProvider — providers returning nil from List() still yield the builtin set.
  • TestSecretOverlayKeys_UnionsBuiltinsAndListed — builtins ∪ custom keys with dedup.
  • TestSecretOverlayKeys_ListError — list errors propagate but builtins are preserved.
  • TestOverlaySecretsToEnv_CustomSkillKey — end-to-end regression: a non-builtin key (MY_SKILL_API_KEY) written to an encrypted file lands in os.Environ after OverlaySecretsToEnv. Isolates HOME to avoid touching the developer's real ~/.forge/secrets.enc.
  • All pre-existing tests continue to pass.
gofmt -l forge-cli/...                       # clean
golangci-lint run ./{forge-core,forge-cli,forge-plugins}/...  # 0 issues
cd forge-cli && go test ./...                # all pass

Acceptance criteria from #48

  • Skill env var stored in encrypted secrets reaches downstream binary.
  • No edits to runner.go required for a new skill with new env var names.
  • Existing builtin-key behavior preserved.
  • Cross-category secret-reuse detection continues to work for the builtin set.

Known follow-up (out of scope for #48)

While testing I noticed OverlaySecretsToEnv (and the runner's buildSecretProvider) unconditionally appends the global ~/.forge/secrets.enc to the chain even if that file uses a different passphrase. ChainProvider.List() propagates the resulting decrypt error and forces the new code into the builtins-only fallback for custom keys — so a stale global file can hide skill-declared keys. The same brittleness already affects Get for keys not present in the local file, so it predates this change. Worth a separate issue (gate the global path on os.Stat, or make ChainProvider.List/Get lenient to per-provider failures).

Test plan

  • On a project with secrets.providers: [encrypted-file], declare a custom env var (e.g. MY_API_KEY) under forge.env.required in a skill, store the value via forge secrets set MY_API_KEY=..., run the agent, and confirm the skill script / cli_execute sees the value.
  • Confirm existing LLM provider keys (OPENAI_API_KEY, ANTHROPIC_API_KEY, etc.) and channel keys (SLACK_BOT_TOKEN, TELEGRAM_BOT_TOKEN) still resolve the same as before.
  • Confirm cross-category reuse detection still fires when the same value is used for an LLM key and a channel key.

Replace the hardcoded knownKeys list in Runner.overlaySecrets and
OverlaySecretsToEnv with a dynamic union of the builtin keys and
whatever the secrets provider exposes via List(). Custom skill env
vars stored via `forge secrets set` now reach the OS environment so
SkillCommandExecutor / cli_execute can read them, with no edits to
runner.go required when adding a new skill.

- Extract builtinSecretKeys to a single package-level slice (used as
  the fallback when a provider cannot enumerate, e.g. EnvProvider).
- Add secretOverlayKeys helper that returns builtins ∪ provider.List(),
  deduped. List errors are surfaced but builtins are still returned so
  the builtin overlay path keeps working when enumeration fails (wrong
  passphrase on a global file, etc.).
- Keep cross-category secret-reuse detection scoped to builtinSecretKeys
  (only set with categories defined in secretCategory).
- Tests: stub-provider unit tests for union/dedup/error paths plus an
  end-to-end test that writes a non-builtin key into an encrypted file
  and verifies OverlaySecretsToEnv lands it in os.Environ.
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.

Skill-declared env vars stored as encrypted secrets are not passed to downstream binaries

1 participant