fix(dashboard): mask agent free-text fields in the config endpoint response (#8717) - #8775
Conversation
Design Review (Fable 5) — ✅ PASSDesign-level review of Design-Verdict: PASS Real boundary (agent-writable text reaching the browser), closed with the existing detector and sentinel semantics, costs named, write-back safety verified. Suggestions
[DESIGN-REVIEWED] 3fb317c |
GPT 5.6 Review — ✅ no blocking findingsGPT 5.6 completed its review of This comment is updated in place on each push. Review detailsNo findings. False positive or not applicable? A repository writer can comment: |
Opus 4.8 Review — ✅ no blocking findingsReviewed Review detailsBased on my analysis: CANDIDATE 1 claims the removal-reference masking is incomplete because Verifying the pieces:
But testing (a)/(b)/(c) at the required bar:
The candidate's own confidence is "medium," with the channel-binding link explicitly unverified. It does not clear 80. Step 2 review of the changed lines ( No findings. [OPUS-REVIEWED] 3fb317c 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 claims verified: agent sync copies First-Principles-Verdict: CONCERNS Every item is derived from the external-content boundary and lands at the choke point, but a third surface shipping four of the same fields verbatim goes uncounted. What this change shipsIntent: stop agent- and package-writable free text in agent records from reaching the browser unmasked through the config endpoint (#8717). This is a FIX.
Watch
[FIRST-PRINCIPLES-REVIEWED] 3fb317c |
…sponse (#8717) The GET /api/config/kirocrew masking is schema-driven, so agent-record free-text fields — agent- and package-writable strings that are not schema-sensitive — left both response sites unmasked. Add a second pass to _masked_config_dict that masks a credential- or exfiltration-URL-shaped value wholesale with the existing sentinel, keyed on the same detector the roster path uses so the two rules cannot drift. Covers every unguarded str field of the record (an invariant test pins the tuple against the dataclass), removes a suspicious-keyed record from the browser view (agent sync stores discovered names as map keys) and masks default_agent / session.pool_agent references to it. Benign content is byte-identical and the save() path is never masked. Closes #8717
602b53a to
3fb317c
Compare
|
Disposition of the First Principles advisory CONCERNS on
|
|
Disposition of Design Review suggestion 1 on
|
|
Disposition of Design Review suggestion 2 on
|
Summary
GET /api/config/kirocrew's_masked_config_dictis schema-driven: it masks only values the JSON schema markssensitive=True. Agent-record free-text fields (description,triggers, and siblings) are agent- and package-writable — an agent can editconfig.jsondirectly, and agent sync copiesdescriptionstraight off a discovered agent spec — but they are not schema-sensitive, so they left BOTH response sites of the endpoint (the GET body and the PATCH echo) unmasked. This is the config-endpoint half of the class whose roster half PR #8472 proposes to close; issue #8717 records the asymmetry.The fix adds a second pass to
_masked_config_dict:_AGENT_UNTRUSTED_TEXT_FIELDSnames everystrfield ofKiroCrewAgentConfigwhose load path does not pin its shape (description,triggers,kiro_agent,workspace,memory_store,model,source,telegram_account).reasoning_effort(coerce_effort) andsession_color(_safe_color) are excluded because their load guards already refuse redactable content. An invariant test enumerates the dataclass'sstrfields against the tuple plus that exception set, so a newly added free-text field fails loudly instead of shipping unmasked._mask_agent_free_textreplaces a value_redact_externalwould alter (credential- or exfiltration-URL-shaped) WHOLESALE with the existing_SENSITIVE_MASKsentinel; a non-string is masked too (descriptionhas no load-time type guard, so one genuinely reaches here). Benign content passes through byte-identical. Keyed on_redact_externalitself so this rule and the roster's cannot drift apart.Write-back safety: neither branch of the endpoint can echo the sentinel into storage — the PATCH allowlist (
_EDITABLE_CONFIG) names noagents.*path, and the PUT branch reads only the singularagentsection against a hardcoded key list. Thesave()path is never masked.Named costs: a value containing one credential-shaped token is masked entirely (the same trade
_masked_config_dictalready makes); the overview config tab renderskiro_agent/workspace/memory_storeand cross-references the latter two, so a masked value breaks that "used by" row — but only for a record whose value is already credential-shaped and therefore already meaningless as a reference. The record KEY (the agent name) is handled by REMOVAL: agent sync stores a discovered agent's name as the map key, so a credential-shaped package name would ship verbatim as a key, and masking a key would collide two suspicious records into one entry. A suspicious-keyed record is dropped from the browser view (save() still carries it) and the name-reference fields that could still spell it (default_agent,session.pool_agent) are masked when they match. Named cost: such a record is invisible in the config tab — its name was never renderable content.Out of scope:
GET /api/agentsstill ships these fields verbatim on main — that half of the class is PR #8472's, which carries the same mask plus the mask-means-unchanged write rule its route needs.Tests
test/test_config_agent_fields_redaction.py(18 tests): credential- and URL-shaped values masked wholesale; benign / empty / CJK strings byte-identical; non-strings masked; partial-credential wholesale-mask pinned; extended field set covered; structural fields untouched;save()path untouched; redactor-precondition assertions localizing detector regressions; a dataclass-enumeration invariant ratcheting the field tuple; and both response sites driven over HTTP (real GET route, real PATCH handler).triggersfrom the tuple, droppingworkspacefrom the tuple, non-string passthrough, unconditional masking, keeping a suspicious-keyed record, and skipping the name-reference masking each turn distinct tests red.Pattern harvest
Rule candidate: flag any aiohttp
json_responsehandler that serializes a config/record dataclass where somestrfields lack a load-time shape guard and the response path applies only schema-driven masking — the unguarded fields ship agent-writable free text verbatim (semgrep:web.json_response($X)where$Xflows fromto_dict()/asdict()without a per-field mask pass).Class: schema-driven redaction gives point coverage; writable free-text fields outside the schema's sensitive set need their own named-field mask pass, ratcheted by a dataclass-enumeration invariant test.
Closes #8717