feat(credentialsource): add process source#38
Merged
Conversation
Add a host-command credential source: `type: process` runs a configured command with `sh -c` and serves its trimmed stdout as the credential value. Any helper that prints a credential works — OS keychain CLIs, pass, 1Password's op, or AWS credential_process helpers — without a dedicated Go backend per tool. The source implements RefreshingSource so the existing background refresh loop drives re-execution. When the output is AWS credential_process-format JSON, TTL derives from the embedded Expiration (clamped at zero so an expired credential refreshes immediately); other output refreshes on a fixed 5-minute interval. No safety margin is subtracted in the source: the refresh loop's 75%-of-TTL schedule provides it. Hardening: header-invalid control characters (RFC 7230) are stripped with a logged count (the value is never logged); stderr is included (truncated to 256 bytes) in fetch errors so helper failures like an expired SSO session are diagnosable, while stdout never appears in errors; empty output is an error; WaitDelay ensures a grandchild process holding the stdout pipe cannot block Fetch past context cancellation. Config: SourceConfig gains a `command` field; ResolveSource wires `type: process` and every existing source type rejects the new field as extraneous, matching the established validation pattern. Testing: 13 new tests (9 source, 4 config resolution) covering the happy path, STS expiry sniffing, expired credentials, default TTL fallback, sanitization, context cancellation, command failure with stderr surfacing, empty output, and config validation both ways. go test -race ./..., go vet ./..., and gofmt are clean.
Findings from a pre-review pass on PR majorcontext#38, all test-first: Validation: the token-exchange extraneous-field check lives in gatekeeper_tokenexchange.go and was missed when the other six source types learned to reject the new fields — a stray `command:` on `type: token-exchange` was silently ignored. Both new fields are now rejected there and the test table gained the companion rows. Expiry sniffing now requires the credential_process shape: exact-case Version, AccessKeyId, and Expiration keys. encoding/json matches field names case-insensitively, so any JSON output with an unrelated `expiration` field could silently hijack the refresh schedule. Already-expired output now fails the fetch (with the timestamp in the error) instead of installing credentials that would 401 every request for a full refresh interval; failing engages the refresh loop's backoff, which previously reset on every successful-but-stale fetch, re-running the helper every 30 seconds indefinitely. New `ttl` config field wires the previously unreachable defaultTTL constructor parameter, so helpers that are interactive or rate-limited can slow the default 5m schedule (and short-lived tokens can speed it up). Rejected as extraneous on every other source type. The stripped-control-characters warning now fires only for non-whitespace control bytes: trailing newlines and pretty-printed JSON are normal helper output and previously warned on every fetch. Docs: refresh cadence claims corrected (75% of TTL, not "every 5 minutes"), ResolveCredentialSource's type enumeration gained process, the changelog PR link placeholder is filled in, and the reference page documents ttl, the format check, and the expired-output error. Testing: 7 new tests (expired-is-error, near-expiry-serves, non-credential-JSON ignored, whitespace-no-warn, garbage-warns, ttl honored, invalid ttl rejected) plus companion validation rows; go test -race ./..., go vet, and gofmt are clean.
Contributor
|
nice! looks good to me. Will give @andybons some time to respond since he's been working more with gatekeeper. |
Contributor
|
Looks great! |
andybons
approved these changes
Jul 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Add a host-command credential source:
type: processruns a configured command withsh -cand serves its trimmed stdout as the credential value. Any helper that prints a credential works — OS keychain CLIs, pass, 1Password's op, or AWS credential_process helpers — without a dedicated Go backend per tool.The source implements RefreshingSource so the existing background refresh loop drives re-execution. When the output is AWS credential_process-format JSON, TTL derives from the embedded Expiration (clamped at zero so an expired credential refreshes immediately); other output refreshes on a fixed 5-minute interval. No safety margin is subtracted in the source: the refresh loop's 75%-of-TTL schedule provides it.
Hardening: header-invalid control characters (RFC 7230) are stripped with a logged count (the value is never logged); stderr is included (truncated to 256 bytes) in fetch errors so helper failures like an expired SSO session are diagnosable, while stdout never appears in errors; empty output is an error; WaitDelay ensures a grandchild process holding the stdout pipe cannot block Fetch past context cancellation.
Config: SourceConfig gains a
commandfield; ResolveSource wirestype: processand every existing source type rejects the new field as extraneous, matching the established validation pattern.Testing: 13 new tests (9 source, 4 config resolution) covering the happy path, STS expiry sniffing, expired credentials, default TTL fallback, sanitization, context cancellation, command failure with stderr surfacing, empty output, and config validation both ways. go test -race ./..., go vet ./..., and gofmt are clean.