Skip to content

feat(credentialsource): add process source#38

Merged
dpup merged 2 commits into
majorcontext:mainfrom
natehardison:nate/credential-process
Jul 3, 2026
Merged

feat(credentialsource): add process source#38
dpup merged 2 commits into
majorcontext:mainfrom
natehardison:nate/credential-process

Conversation

@natehardison

Copy link
Copy Markdown
Contributor

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.

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.
@dpup

dpup commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

nice! looks good to me. Will give @andybons some time to respond since he's been working more with gatekeeper.

@andybons

andybons commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Looks great!

@dpup dpup merged commit 773d99c into majorcontext:main Jul 3, 2026
2 checks passed
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.

3 participants