feat(mint): return broker-verified App slug in mint response (lr-dbe5d4) - #27
Conversation
…r-dbe5d4)
internal/mint.Service.Mint already reads the App's actual slug from the
broker at AppSlugPath and verifies it against the role's configured
AppSlug before minting (the App-slug gate, lr-116b57). It discarded that
verified value once the gate passed. Populate githubapp.Token.AppSlug
with actualSlug -- the broker-read value already proven equal to the
configured expectation -- not the configured expectation itself, so a
consumer inherits a verified fact instead of a config echo.
Backward compatible: AppSlug is an additive field on Token; a consumer
reading only .Value/.ExpiresAt is unaffected. The gate itself is
untouched -- a mismatch or half-configured binding still returns
githubapp.Token{} and an error before this line is ever reached.
…ug (lr-dbe5d4)
gatekeeper mint has no structured output today -- only a bare token
string on stdout. Add an opt-in --json flag that emits
{token, expires_at, app_slug}, where app_slug is the broker-verified
value carried on githubapp.Token (see prior commit), empty when the
role has no App-slug binding configured.
Backward compatible by construction: the default (no --json) path is
untouched -- fmt.Println(token.Value), exactly as before. A consumer
that never opts into --json cannot observe this change at all.
…onfig echo (lr-dbe5d4) Adds the required non-vacuity pair (operator directive: a happy-path-only assertion is necessary but not sufficient, since actualSlug and binding.AppSlug are equal on every successful mint): - TestMintReturnsBrokerVerifiedAppSlug: asserts the returned Token.AppSlug equals the broker-read value on a passing mint. Verified RED by temporarily reverting the `token.AppSlug = actualSlug` assignment in mint.go and confirming this test fails with Token.AppSlug == "" before restoring the fix (recorded in the PR body). - TestMintAppSlugMismatchReturnsNoTokenAndNoSlug: the required broker/config-mismatch counterpart. Proves the mint still fails closed and returns a zero-value Token -- Value and AppSlug both empty, MintFunc never called -- when the broker-resolved slug disagrees with the configured expectation.
TestRunMintJSON_IncludesBrokerVerifiedAppSlug exercises runMint through a temp config.yaml + stub GitHub API + env broker, asserting: - --json output is a JSON object whose app_slug field is the broker-verified value read via the env broker at app_slug_path. - the default (no --json) path is unchanged: bare token string on stdout, no App slug present -- the backward-compat contract this task requires.
…ponse (lr-dbe5d4) Document both surfaces that now carry the broker-verified App slug (Go API: githubapp.Token.AppSlug; CLI: gatekeeper mint --json), why the value is verified rather than a config echo, and the explicit backward- compat statement for a consumer that does not opt in. Also documents what is deliberately NOT built here: a numeric App/bot user id alongside the slug is out of scope pending an explicit decision on whether it would replace or coexist with an existing declaration elsewhere -- see the PR body for the coupling this leaves for a future task. README.md gets a matching "Structured output" section under Usage so a first-time reader discovers --json without needing docs/ROLES.md.
fe3fa57 to
dd8c79c
Compare
|
PEACHES clean
|
|
Security audit of PR #27 (clagentic-gatekeeper). Reviewed the mint-response App-slug provenance change end to end: internal/githubapp/githubapp.go (additive Token.AppSlug field), internal/mint/mint.go (assigns token.AppSlug = actualSlug, the broker-read value, never binding.AppSlug), cmd/gatekeeper/main.go (new --json flag emitting {token, expires_at, app_slug}), plus tests and docs. Findings: (1) token leakage via --json: no new logging call introduced anywhere in the diff; JSON and default paths both write only to stdout via the same sink used before this PR; a marshal error surfaces only the encoder error object, never the token, to stderr. (2) fail-closed gate integrity: mint.go entitlement and App-slug verification gate (lines ~183-211) run in the same order as before this PR; every failure path returns a zero-value Token before the new AppSlug assignment at line 249 is ever reached; mint_test.go covers the mismatch case explicitly and asserts both Token.Value and Token.AppSlug are empty on failure. (3) identity provenance: token.AppSlug is assigned from actualSlug (broker-read), not binding.AppSlug (config echo) -- confirmed directly in source and by the non-vacuous mismatch test. (4) error-message disclosure: mismatch/half-configuration errors surface only role name and slug strings, never paths, private key material, app id, installation id, or token fragments. (5) secret-scan: gitleaks (0 leaks) and manual review of test fixtures found only synthetic values (ghs_json_test_token, numeric placeholder ids, generated test PEM, env-var-backed fake broker). (6) public-surface hygiene: README.md and docs/ROLES.md additions use generic placeholder strings only; no private repo/tooling names, internal hostnames, or internal task-tracker ids in user-facing prose. (7) go.mod/go.sum untouched -- no new dependency introduced; osv-scanner findings against the Go stdlib toolchain are pre-existing and not attributable to this diff. semgrep (131 rules, Go + multilang): 0 findings. No .yml workflow or .crew/.yaml files touched by this PR. Zero findings overall; review status clean. |
|
Merged via clagentic-loadout v0.2.0
|
What changed
Item 1 of lr-dbe5d4 (SCOPE B of the gatekeeper/loadout identity reconciliation series): the mint response now carries the broker-VERIFIED App slug alongside the token, instead of discarding it once the existing App-slug gate (mint.go, gate 2) has checked it.
Why
internal/mint/mint.go already resolves and verifies the App slug at mint time (requires both AppSlug+AppSlugPath; fails closed on half-configuration; the broker-read comparison fails closed on mismatch) but threw the verified value away once the gate passed. A separate consumer project (clagentic-loadout) independently declares the same fact in its own config, operator-typed and unverified, creating a silent two-sources-of-truth risk: a re-registered App or changed slug can produce a mis-attributed commit or a stale verdict-gate login with no reconciliation. Returning the value gatekeeper already verified lets a consumer inherit it instead of re-declaring it.
Scope note: item 2 (numeric bot user id) deferred, not built
This PR builds ONLY item 1 (the App slug). Item 2 of the parent task -- whether the numeric App bot user id should also travel with the mint response -- is explicitly out of scope for this PR per dispatch instructions, and is NOT a drive-by omission.
The task record establishes the numeric id is not an orphan value -- it is already declared in a separate project's config for two of five roles and guarded by a live consistency test there. Whoever builds item 2 must decide explicitly whether a mint-returned id REPLACES that existing declaration (needing a migration path for the existing guard) or COEXISTS with it (needing a stated reconciliation), and must not simply add a second source of truth for the exact fact this series exists to eliminate. That decision was not settled at dispatch time, so it is left for a follow-up task rather than bundled in here.
Coupling note, as requested: the mintResult/JSON shape added here (token, expires_at, app_slug) is additive -- a future app_bot_user_id (or similar) field could be added the same way (another optional, omitempty JSON key plus an additive githubapp.Token field) without breaking this shape. I do not see a structural reason item 2 could not be cleanly deferred; the two are independent additive fields on the same envelope, not a single field whose shape depends on both decisions at once.
Verification (non-vacuity)
Per the operator directive that a happy-path-only test is necessary but not sufficient, the mismatch case is the required counterpart:
Backward compatibility
Branch base correction
An earlier revision of this PR body incorrectly asserted that origin/main was at dc5f73c and that 3294d64 (the actual origin/main tip, the merge commit for the lr-2a8653 per-spawn fail-closed change) was an unmerged sibling branch tip. That claim was wrong: it was concluded from a stale local remote-tracking ref instead of a fresh git ls-remote / fetch of refs/heads/main. This branch has since been rebased onto the correct, verified origin/main tip (3294d64) and the conflict introduced by that rebase (both branches adding a new test block at the same location in internal/mint/mint_test.go) has been resolved by keeping both blocks. All commits, tests, and the non-vacuity revert/confirm-RED/restore cycle have been re-verified against the corrected base.
Test status
go build ./..., go vet ./..., and go test ./... -count=1 all pass locally (fresh run, no cache) against the rebased branch. Full suite: cmd/gatekeeper, internal/a2apolicy, internal/attestation, internal/broker, internal/config, internal/githubapp, internal/mint, internal/roles -- all green.
Task: lr-dbe5d4 (item 1 only; item 2 deferred per dispatch instructions).