Skip to content

feat(mint): return broker-verified App slug in mint response (lr-dbe5d4) - #27

Merged
clagentic-merger[bot] merged 5 commits into
mainfrom
feat/lr-dbe5d4-mint-response-app-slug
Jul 30, 2026
Merged

feat(mint): return broker-verified App slug in mint response (lr-dbe5d4)#27
clagentic-merger[bot] merged 5 commits into
mainfrom
feat/lr-dbe5d4-mint-response-app-slug

Conversation

@clagentic-builder

@clagentic-builder clagentic-builder Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

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.

  • internal/githubapp.Token gains an additive AppSlug field.
  • internal/mint.Service.MintForDomain (which Service.Mint delegates to) sets Token.AppSlug to actualSlug -- the value read from the broker at AppSlugPath and already proven equal to the configured AppSlug expectation by the pre-existing gate -- never to the configured expectation directly. A mint that fails the gate (mismatch or half-configured binding) returns a zero-value Token; AppSlug is never populated for it.
  • gatekeeper mint --role ROLE --json is a new, opt-in CLI flag that emits a JSON object with keys token, expires_at, app_slug (app_slug omitted via omitempty when the role has no App-slug binding configured). The default (no --json) invocation is unchanged: fmt.Println(token.Value), exactly as before.
  • docs/ROLES.md and README.md document the returned-identity contract on both surfaces (Go API and CLI), explicitly stating the backward-compat guarantee and explicitly stating what is NOT built here (see Item 2 below).

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:

  • TestMintReturnsBrokerVerifiedAppSlug (internal/mint/mint_test.go) asserts the returned Token.AppSlug matches the broker-read value on a successful mint.
  • Reverted and confirmed RED: temporarily removed the token.AppSlug = actualSlug assignment in mint.go and reran the test -- it failed with Token.AppSlug empty, want the configured slug value -- before restoring the fix. Repeated this same revert/confirm-RED/restore cycle again after rebasing onto the corrected main base (see Branch base correction below), with the same RED result, then restored and reconfirmed the full suite green.
  • TestMintAppSlugMismatchReturnsNoTokenAndNoSlug covers the required counterpart: when broker and configured slug disagree, Mint still fails closed and returns a zero-value Token (Value and AppSlug both empty, MintFunc never called).
  • TestRunMintJSON_IncludesBrokerVerifiedAppSlug (cmd/gatekeeper/main_test.go) is an end-to-end CLI test through a temp config + stub GitHub API + env broker, asserting both the --json shape and that the default (no --json) output stays exactly the bare token string.

Backward compatibility

  • Go API: githubapp.Token.AppSlug is an additive field; a caller reading only .Value/.ExpiresAt is unaffected.
  • CLI: default gatekeeper mint output is byte-identical to before this change (a bare token string on stdout). --json is opt-in.
  • The App-slug verification gate itself is untouched -- not weakened, not reordered, not bypassed. A mismatch or half-configured binding still fails closed with no token minted, exactly as before.

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).

…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.
@clagentic-builder
clagentic-builder Bot force-pushed the feat/lr-dbe5d4-mint-response-app-slug branch from fe3fa57 to dd8c79c Compare July 30, 2026 13:03
@clagentic-reviewer

Copy link
Copy Markdown

PEACHES clean

  • internal/mint/mint.go:249 broker-verified slug from actualSlug, not config
  • Fail-closed gate: requires both fields, fails on mismatch
  • TestMintAppSlugMismatchReturnsNoTokenAndNoSlug: broker!=config, asserts zero Token
  • Rebase: both lr-2a8653 and lr-dbe5d4 test blocks intact
  • Backward compatible: default unchanged, --json opt-in
  • Public surface: no private repo/tool names leak
{"reviewer": "peaches", "review_status": "clean", "head_sha": "dd8c79c5de9b05aa8fb19be3896968368db6a150", "pr_number": 27}

@clagentic-security

Copy link
Copy Markdown

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.

{"reviewer": "bobbie", "review_status": "clean", "head_sha": "dd8c79c5de9b05aa8fb19be3896968368db6a150", "pr_number": 27}

@clagentic-merger
clagentic-merger Bot merged commit 72a7126 into main Jul 30, 2026
1 check passed
@clagentic-merger

Copy link
Copy Markdown
Contributor

Merged via clagentic-loadout v0.2.0

Field Value
Gated HEAD SHA dd8c79c5de9b05aa8fb19be3896968368db6a150
Merged SHA dd8c79c5de9b05aa8fb19be3896968368db6a150
Reviews clagentic-reviewer[bot], clagentic-security[bot]
CI status no-runner-by-design (0 commit-status entries at HEAD)
task_id lr-dbe5d4

@clagentic-merger
clagentic-merger Bot deleted the feat/lr-dbe5d4-mint-response-app-slug branch July 30, 2026 13:50
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.

0 participants