feat!: make value providers consumer-declared, drop the AWS SDK #286

Merged
coilysiren merged 1 commit from feat/pluggable-value-providers into main 2026-08-14 04:23:04 +00:00
Member

Closes #283.

specgen baked one cloud vendor into the generator: a HasSSM branch emitting aws-sdk-go-v2 imports, a GetParameter call and a ~/.aws/credentials SSO workaround, plus a HasTailscale twin. Every generated binary inherited that dependency whether or not it resolved anything from a store.

pkg/valuesource already had the right shape - Provider, Builtins, Merge, fail-closed Resolve - and its own package doc already said consumers register store-backed resolvers "so neither engine imports a store SDK". Only codegen disagreed. This makes the doc true.

The mechanism

A store-backed provider is declared in the guardfile as a subprocess contract, in either dialect:

provider ssm {
    exec aws ssm get-parameter --with-decryption --output text --query "Parameter.Value" --name
}

The address is appended as the final argument. Only stdout is read and trimmed, so a resolved value never reaches argv, the audit row, or an error message - a non-zero exit surfaces the exit status alone.

BREAKING

A guardfile naming a non-builtin provider must now declare it. env, file and literal stay built in.

The failure mode is loud, which was the design constraint. An undeclared name yields an empty generated registry, so valuesource.Resolve returns no provider registered for "ssm" - never an empty token silently used as a credential:

// undeclared                          // declared
return map[string]valuesource.Provider{}   return map[string]valuesource.Provider{
                                             "ssm": execProvider("ssm", []string{"aws", "ssm", }),
                                           }

AOS needs a companion change: it names ssm in actions.kdl and forgejo.kdl. That lands separately once this releases, since it has to lock against a published version.

The trade, stated plainly

Credential resolution becomes whatever the declared binary does. The SDK's profile-precedence and SSO-fallback behaviour is replaced by the aws CLI's, and that CLI must exist wherever the generated binary runs. That is a real behaviour change for live token minting, chosen deliberately over keeping a vendor SDK in a policy-free engine. docs/value-providers.md says so in the doc rather than burying it.

A latent bug the new grammar exposed

kdl.Value.String() debug-formats non-string kinds, so a bare -4 in exec tailscale ip -4 came through as the literal text "<kdl.Int -4>" and would have been passed to the subprocess verbatim. argText now renders the token a shell would actually receive. Covered by a test that pins the -4 case specifically.

Worth noting the same .String() pattern is used for arguments elsewhere in both grammars, so unquoted numeric-looking tokens may misrender in other nodes too. Out of scope here; the existing convention is to quote (docs/execverb.md already says a * glob needs quoting).

Verification

Full gate green (build, vet, tidy, test, lint 0 issues), plus end to end against a scratch copy of AOS's real guardfiles:

  • with the declaration added, aosguard locks, builds and runs
  • the generated dependency graph drops from 42 aws-sdk entries to zero
  • grep -riE '\baws\b' cli/ http/ pkg/ cmd/ is now clean in shipped non-test code

AOS's guardfiles were copied to a scratch dir for that test; the agentic-os repo is untouched by this PR.

Not included

The valueFlags table in cli/execverb/argv.go is still one vendor's global-flag list, marked TODO(#282). Dropping it silently weakens a live guard (deny-when arg0 matches *tfstate* fails open once --region us-east-1 stops being stripped), so it wants the same treatment: declared in the guardfile, by the spec that knows its own binary. Left for a follow-up.

Closes #283. specgen baked one cloud vendor into the generator: a `HasSSM` branch emitting `aws-sdk-go-v2` imports, a `GetParameter` call and a `~/.aws/credentials` SSO workaround, plus a `HasTailscale` twin. Every generated binary inherited that dependency whether or not it resolved anything from a store. `pkg/valuesource` already had the right shape - `Provider`, `Builtins`, `Merge`, fail-closed `Resolve` - and its own package doc already said consumers register store-backed resolvers *"so neither engine imports a store SDK"*. Only codegen disagreed. This makes the doc true. ## The mechanism A store-backed provider is declared in the guardfile as a subprocess contract, in **either dialect**: ```kdl provider ssm { exec aws ssm get-parameter --with-decryption --output text --query "Parameter.Value" --name } ``` The address is appended as the final argument. Only stdout is read and trimmed, so a resolved value never reaches argv, the audit row, or an error message - a non-zero exit surfaces the exit status alone. ## BREAKING A guardfile naming a non-builtin provider must now declare it. `env`, `file` and `literal` stay built in. The failure mode is loud, which was the design constraint. An undeclared name yields an **empty** generated registry, so `valuesource.Resolve` returns `no provider registered for "ssm"` - never an empty token silently used as a credential: ```go // undeclared // declared return map[string]valuesource.Provider{} return map[string]valuesource.Provider{ "ssm": execProvider("ssm", []string{"aws", "ssm", …}), } ``` **AOS needs a companion change**: it names `ssm` in `actions.kdl` and `forgejo.kdl`. That lands separately once this releases, since it has to lock against a published version. ## The trade, stated plainly Credential resolution becomes whatever the declared binary does. The SDK's profile-precedence and SSO-fallback behaviour is replaced by the aws CLI's, and that CLI must exist wherever the generated binary runs. That is a real behaviour change for live token minting, chosen deliberately over keeping a vendor SDK in a policy-free engine. `docs/value-providers.md` says so in the doc rather than burying it. ## A latent bug the new grammar exposed `kdl.Value.String()` debug-formats non-string kinds, so a bare `-4` in `exec tailscale ip -4` came through as the literal text `"<kdl.Int -4>"` and would have been passed to the subprocess verbatim. `argText` now renders the token a shell would actually receive. Covered by a test that pins the `-4` case specifically. Worth noting the same `.String()` pattern is used for arguments elsewhere in both grammars, so unquoted numeric-looking tokens may misrender in other nodes too. Out of scope here; the existing convention is to quote (`docs/execverb.md` already says a `*` glob needs quoting). ## Verification Full gate green (`build`, `vet`, `tidy`, `test`, `lint` 0 issues), plus end to end against a scratch copy of AOS's real guardfiles: - with the declaration added, aosguard **locks, builds and runs** - the generated dependency graph drops from **42 `aws-sdk` entries to zero** - `grep -riE '\baws\b' cli/ http/ pkg/ cmd/` is now clean in shipped non-test code AOS's guardfiles were copied to a scratch dir for that test; the agentic-os repo is untouched by this PR. ## Not included The `valueFlags` table in `cli/execverb/argv.go` is still one vendor's global-flag list, marked `TODO(#282)`. Dropping it silently weakens a live guard (`deny-when arg0 matches *tfstate*` fails open once `--region us-east-1` stops being stripped), so it wants the same treatment: declared in the guardfile, by the spec that knows its own binary. Left for a follow-up.
feat!: make value providers consumer-declared, drop the AWS SDK
All checks were successful
ci / test (pull_request) Successful in 42s
ci / lint (pull_request) Successful in 25s
ci / secrets (pull_request) Successful in 5s
b475101fde
closes #283

specgen baked one cloud vendor into the generator: a `HasSSM` branch
emitting aws-sdk-go-v2 imports, a GetParameter call, and a
~/.aws/credentials SSO workaround, plus a `HasTailscale` twin. Every
generated binary inherited that dependency whether or not it resolved
anything from a store.

pkg/valuesource already had the right shape - Provider, Builtins,
Merge, fail-closed Resolve - and its own doc said consumers register
store-backed resolvers "so neither engine imports a store SDK". Only
codegen disagreed. This makes that true.

A store-backed provider is now declared in the guardfile as a
subprocess contract, in either dialect:

    provider ssm {
        exec aws ssm get-parameter --with-decryption --output text \
             --query "Parameter.Value" --name
    }

The address is appended as the final argument. Only stdout is read and
trimmed, so a resolved value never reaches argv, the audit row, or an
error message.

BREAKING: a guardfile naming a non-builtin provider must now declare it.
env, file and literal stay built in. An undeclared name fails closed at
resolve time ("no provider registered for ..."), never an empty token -
the generated registry is simply empty. AOS names `ssm` in actions.kdl
and forgejo.kdl and needs the declaration added; that lands separately,
after this releases.

Also fixes a latent bug the new grammar exposed: kdl.Value.String()
debug-formats non-string kinds, so a bare `-4` in `exec tailscale ip -4`
became the literal text "<kdl.Int -4>". argText renders the token a
shell would actually receive.

Verified end to end against a scratch copy of AOS's guardfiles: with the
declaration, aosguard locks, builds and runs, and the generated
dependency graph drops from 42 aws-sdk entries to zero.

Co-authored-by: Kai Siren <coilysiren@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
coilyco-flight-deck/umbra!286
No description provided.