Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
fix(dashboard): name every field GET /api/agents ships, and mask what…
… it cannot show

api_kirocrew_agents built each roster row with
{"name": name, "scope": ..., **dataclasses.asdict(agent_cfg)}, so the
endpoint's response contract was "every field KiroCrewAgentConfig has now,
plus every field anyone adds later", automatically. handlers/members.py
made the opposite call for GET /api/members and documented why: the
response is a network-boundary contract, and a spread ships a future
field -- internal bookkeeping, a filesystem path, a credential-shaped one --
to the browser by omission.

KEY half: both row sources (the cfg.agents rows and the project-scope
rows) now go through one _agent_roster_row allowlist naming 11 keys, so
the two cannot drift into different key sets either. Three record fields
are withheld, each with no consumer anywhere in website/src: the two
per-agent watchdog windows and the deprecated, inert telegram_account.

VALUE half, two co-operating halves. Every record value is agent- or
package-writable -- an agent can edit config.json, and _do_agents_sync
copies description straight off a discovered agent spec -- so a value the
redactors would alter, or a non-string the loader let through, is replaced
WHOLESALE by _SENSITIVE_MASK, the sentinel _masked_config_dict already
uses for the same job on GET /api/config/kirocrew. Benign content is
byte-identical.

api_kirocrew_agent_update then drops any body field carrying that mask,
treating it as unchanged -- the remedy _masked_config_dict's docstring
prescribes verbatim. Without it the read half would destroy stored config:
the agents page seeds its edit sheet from a roster row and sends every
field on every save so that "" can clear a pin.

A FIXED sentinel rather than redacting in place is the load-bearing
choice. Recomputing the redaction to recognise the view breaks two ways a
sentinel does not: a second redaction chain over the same response (#8465)
produces a view the predicate no longer matches, and a stored value that
changes between the GET and the PUT makes the stale view read as a genuine
edit, writing redaction markers into the config. The sentinel depends on
neither. Cost, named: a value containing one credential-shaped token is
masked entirely rather than partially, the same trade the config endpoint
already makes.

Masking a non-string rather than coercing it to "" is what lets the write
rule PRESERVE it; an echoed "" would read as a genuine edit.

name is the single exemption, and only for the owner: it travels in the URL
rather than the body so the write rule cannot protect it, and masking it
would make /api/agents/{name} unaddressable. An app token cannot reach
those owner-gated routes, so name is masked there too. dict[str, str] is
now true rather than aspirational.

test_agents_roster_contract.py pins the exact key set at the endpoint for
both row sources, ratchets the allowlist against dataclasses.fields, and
covers both halves over HTTP including the stale-view case and that a
genuine edit still writes through.

Fixes #8454
  • Loading branch information
chenmingwei23 committed Sep 5, 2026
commit 2ab8bd64572e8615c05eb7c21210690ddd02ff50
Loading
Loading