fix(connections): name rejected OAuth endpoint on mint card (#7765) - #9128
fix(connections): name rejected OAuth endpoint on mint card (#7765)#9128chenmingwei23 wants to merge 1 commit into
Conversation
Design Review (Fable 5) — ✅ PASSDesign-level review of Design-Verdict: PASS Right root-cause shape: the copy-ready rule is centralized in one security-owned helper, transported through the handler allowlist, and pinned by wire-arrival tests. Watch
[DESIGN-REVIEWED] 24511e9 |
UX Review (Fable 5) — 🟡 CONCERNSUX-level review of UX-Verdict: CONCERNS The remediation the PR exists to deliver dead-ends at an unlinked "guide", and the two variants of one failure speak two vocabularies. Watch
Suggestions
[UX-REVIEWED] 24511e9 |
GPT 5.6 Review — ✅ no blocking findingsGPT 5.6 completed its review of This comment is updated in place on each push. Review detailsFINDING -- src/kiro_crew/security.py:16812 -- [GPT-REVIEWED] 24511e9 False positive or not applicable? A repository writer can comment: |
Opus 4.8 Review — ✅ no blocking findingsReviewed Review detailsNo findings that block; one advisory gap in the new copy-ready contract. FINDING — src/kiro_crew/security.py:16810 — [OPUS-REVIEWED] 24511e9 Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
First Principles Review (Fable 5) — 🟡 CONCERNSPremise-level review of All repository checks are done. The findings: the guide the new copy points to exists at base; First-Principles-Verdict: CONCERNS The fix earns its place end-to-end, but three copy/behavior changes to the #7739 banner and the fallback string ride along undeclared. What this change shipsIntent: let a user whose OAuth mint was rejected see which endpoint to add to
Watch
Subtractions
[FIRST-PRINCIPLES-REVIEWED] 24511e9 |
248621c to
c7b7f69
Compare
c7b7f69 to
4554df6
Compare
4554df6 to
5df08cd
Compare
5df08cd to
d387b4e
Compare
…ion card (#7765) The MCP OAuth banner (PR #7739) names the sanitized endpoint of a URL the credential gate rejects, but the two sibling connection-flow rejection surfaces still reported opaquely: the cold mint set only reason=mint_url_rejected, and warm's blocked slugs carried no endpoint identity. Without the host+path the user cannot know what to write into oauth_endpoints.json, so the failure reads as unfixable. The mint variant was attempted in PR #7739 round 1 and removed after Semgrep (python-logger-credential-disclosure) and CodeQL (py/clear-text-logging- sensitive-data, taint from oauthUrl into the logger) flagged the logger path. This surfaces the endpoint on a NON-LOG channel instead: mint.py computes security.sanitized_oauth_endpoint(oauth_url) at the terminal rejection and stores it as MintState[rejected_endpoint], which rides the card-facing view into ConnectionMintState.rejected_endpoint; the Connections card renders a new mint_failure_url_rejected_endpoint string when present, falling back to the existing unnamed message when the helper returns None (credential-bearing host, userinfo, or unparseable URL). The logger/audit lines stay slug-only, so no inline suppression or CodeQL barrier is needed -- the card view is not a sink either scanner models. warm.py keeps its codified release semantics for a credential-bearing slug (test_a_url_carrying_a_credential_is_refused_rather_than_stored): the claim is released, and the ensuing cold mint re-hits the same URL and names the endpoint through the mint surface above, so the warm rejection gains the endpoint identity without converting a screened premint into a terminal failure. docs/system-specs/modules/security.md records the per-site scanner decision. Refs #7739, #7578
d387b4e to
24511e9
Compare
Standing down (closing unmerged, issue stays open)Under a scope change to obvious-defect-only fixes, this item no longer has a review round available, so I am closing this PR unmerged and leaving #7765 open. The branch is kept. Recording the state so whoever resumes has the full picture. What is landed and verified (this work was not wrong)
Open finding 1 -- three banner/fallback changes ride along undeclaredThese are in the diff but were not named in the description. Recording them so the branch's full shape is visible:
Open finding 2 -- the remediation dead-ends, and the two variants speak two vocabularies
HousekeepingThe screenshot committed at |
What is the problem?
PR #7739 taught the MCP OAuth rejection banner to name the sanitized endpoint of a URL the credential gate refuses. Two sibling rejection surfaces in the connection flows were left opaque:
connections/mint.py(cold mint) set onlyreason=mint_url_rejected, andconnections/warm.py(_credential_bearing_slugs) released a blocked slug with no endpoint identity. So a user connecting an unlisted OAuth MCP server on the Connections card could not tell which endpoint to add tooauth_endpoints.json.Why it matters to the user
The remedy for a legitimate-but-unlisted identity provider (Okta org, Auth0, self-hosted OIDC) is to add its
(host, path)tooauth_endpoints.json. Without the endpoint on the failure surface the user cannot know what to write, so the rejection reads as unfixable -- the gap #7739 closed for the banner, still open on the mint/warm cards.How the fix solves it (symptom -> root cause)
Symptom: the card names no endpoint. Root cause: the rejection stored only a coarse reason code and never computed the sanitized identity.
sanitized_oauth_endpointmay by contract return a path that is NOT copy-ready -- the shared redaction tag when the path carried a credential, or a truncated path when it was pathologically long -- so joininghost + pathinline can renderhost[REDACTED: credential]or a chopped path as if a user could type it. Newsecurity.sanitized_oauth_endpoint_display(url)is the single owner of the copy-ready rule: it returns a joinedhost/pathstring only when pasteable, elseNone. Both consumers (the mint card AND the chat banner inchat_runner.py) now call it and neither re-derives the helper's internal sentinels, so a future third consumer inherits the guarantee by default. (This resolves the First Principles concern that the earlier revision put the copy-ready judgment at one call site while the banner sibling still rendered the redacted pair -- the two surfaces now obey one rule enforced by the owner.)MintStatethrough an explicit key allowlist, so a view field is invisible to the client unless the handler names it.api_connections_mint_statenow addsrejected_endpoint-- without this line the field is dropped at the wire and every layer below is dead code. (This was the First Principles BLOCK on an earlier revision; it was correct.)ConnectionMintState.rejected_endpoint+ i18n keymint_failure_url_rejected_endpoint(12 locales + pseudolocale) rendered when present, unnamed fallback otherwise.oauth_endpoints.jsonin the{{productName}}data home) in prose and points to theConnecting a remote OAuth MCP serverguide for the exact entry to add. The banner wording is updated in step to avoid drift, and the unnamed fallback gains a next step.warm.pykeeps its codified release semantics (test_a_url_carrying_a_credential_is_refused_rather_than_stored); the ensuing cold mint re-hits the URL and names the endpoint through the mint surface.Security: the surfaced value is host+path only; the display helper suppresses a credential-bearing host (
None), a redacted path, and a truncated path. The mint/warm log and audit lines stay slug-only, so no inline suppression or CodeQL barrier is needed.docs/system-specs/modules/security.mdrecords the contract.What tests we did
test/test_security.py::TestSanitizedOAuthEndpointDisplay): pasteable endpoint joined; empty path ->host/; credential-in-path returnsNone(not a redacted join); truncated path returnsNone; unnameable host / userinfo / unparseable ->None. The rawTestSanitizedOAuthEndpoint(23) is unchanged and still passes.test/test_connections_handoff.py): the endpoint survives serialization throughapi_connections_mint_stateto the JSON the browser receives; mutation-verified (reverting the handler line ->KeyError), plus a control for the omitted case.test/test_connections_mint.py): names the endpoint while the query credential never reaches log/audit; two controls, one per unnameable branch -- credential-in-HOST (None) and credential-in-PATH (redacted -> suppressed). Full file 126 pass (two failures are pre-existing/environmental: subprocess imports the package, not pip-installed here; identical on the pristine base).test/test_mcp_oauth_banner.py): updated in step -- names the file + guide instead of the JSON shape; the redacted-path case now falls back to the unnamed banner. 135 pass.i18n:checkgates pass; 16 style/QA/parity/productName files (196 tests) pass, including the Korean particle and zh-CN ellipsis/tone rules and the{{productName}}no-hardcode rule.tsc0 errors, eslint clean,ConnectionsPage.coverage.test.tsx107 pass, frontend shards 1/4 and 2/4 green.scripts/check_black_formatting.pypasses with the baseline unchanged (security.pyandtest_mcp_oauth_banner.pyare baselined -- edited by hand, not reformatted); isort/flake8/mypy clean on touched files.Screenshot
Committed under
temp-screenshots/7765-endpoint-name/-- the repo's conventional PR-evidence location (.gitignorekeeps it tracked;screenshot-evidence.ymlinstructs authors to commit there andcleanup-temp-screenshots.ymlreaps it later). The Connections card in both states -- endpoint named, and the unnamed fallback:Pattern harvest
Rule candidate: a value that is computed, stored and rendered can still be invisible if a transport allowlist is not updated -- so the test that matters asserts ARRIVAL at the client boundary, not storage. A first revision here computed, stored, threaded, typed and translated the field into twelve locales with every test green, yet the handler's key allowlist dropped it at the wire. When a handler re-projects a domain object through a named-key allowlist, a feature is only alive once a test drives the real handler and reads the field back out of the serialized payload. Corollary from this round: when a sanitizer's output can be non-pasteable, the "is it safe to surface" rule belongs INSIDE the sanitizer (one owner), not re-derived at each call site, or a second consumer renders the unsafe form under the opposite judgment.
Any other suggestions
The warm path delivers the endpoint transitively (via the cold-mint retry) to respect the codified release contract. If a future change wants warm to name the endpoint directly, that is a deliberate release->fail UX decision and should update
test_a_url_carrying_a_credential_is_refused_rather_than_storedin the same commit.Refs #7739, #7578