Skip to content

feat(fleetdash): approve button + mutation-verified E2E for answering a govd push_back - #233

Merged
rhCat merged 2 commits into
mainfrom
feat/fleetdash-approve
Jul 26, 2026
Merged

feat(fleetdash): approve button + mutation-verified E2E for answering a govd push_back#233
rhCat merged 2 commits into
mainfrom
feat/fleetdash-approve

Conversation

@rhCat

@rhCat rhCat commented Jul 26, 2026

Copy link
Copy Markdown
Owner

The fleet monitor showed the approval queue and could not answer it: needs_approve rendered as a value-free token list, and the only way to act was to re-submit the claim by hand.

Meanwhile the agent-side human gate is unreachable in a caged deployment — every caller of resolve_gateway_approval() is a chat-platform adapter (Discord/Telegram/Feishu/Teams) or a TTY, and a caged agent can reach neither. Measured across the fleet: 45 push_backs on maria-dev-mac, 9 approvals, all from a test harness. The human gate has never once been answered by a person.

The write is gated deliberately — every branch fail-closed

This dashboard has no app-auth and its monitor tokens are read-only by contract, so:

  • a separate credentialapprove_token_file per node (or GOVD_APPROVE_TOKEN_<NAME>), a principal token the operator provisions on purpose. The monitor token is never reused. Absent (the default) ⇒ no button rendered and the route 403s, so the read-only posture is unchanged
  • CSRF — requires X-Fleetdash-Approve, a custom header a cross-origin page cannot set without a preflight this server never answers
  • never approve blind — the run must exist in the mirror and actually be a push_back; the claim's own skill/perk/var_keys are replayed from the mirrored record, so the UI cannot widen a claim into something the agent never made
  • --no-mirror refuses outright (no record to verify against), fixing a crash where mirror_dir was None
  • the loopback-only default and its FLEETDASH_ALLOW_OPEN acknowledgement are untouched

Honest limits

This authorizes the claim, not the tool call: the agent's gate fails closed in ~0.3s and is not resumed by a later approval, so pressing approve creates the audit record and the superseded marker without delivering the action. And the monitor token is one shared secret per node, not a person, so the chain still cannot name who approved.

Both wait on per-person principals + an acl.approve axis. This is a stopgap that makes the ask answerable at all.

Tests — mutation-verified

Real Chromium against a real fleetdash, asserting on what the stub node was handed, not on what the page says. Hermetic: own fleetdash on a free loopback port, stub govd, mirror fixture in tmp_path.

Each guard was individually disabled and the suite had to go red:

guard caught by
CSRF header check test_csrf_header_required
push_back guard test_cannot_approve_a_non_pushback
run_id regex test_path_traversal_run_id_refused
button render gate test_no_button_without_operator_credential
credential gate test_credential_gate_is_enforced_SERVER_side

Two defects only mutation exposed, both of which would have shipped a green-but-hollow suite:

  1. test_cannot_approve_a_non_pushback passed for the wrong reason — the fixture's allow-run had an empty needs_approve, so deleting the push_back guard still produced a 409 from the next check down.
  2. There was no server-side coverage of the credential gate — only "the button is not rendered". Withholding a button is cosmetic; anyone can POST /approve directly. That check holds the entire read-only posture and was untested.

README documents the Playwright setup: the two independent install steps, the shared browser cache outside the venv, that the suite importorskips (so pytest tests and CI are unaffected — these currently skip in CI), and the mutation discipline plus its traps.

Deploy

Host-side (launchd) — no image rebuild, just launchctl kickstart -k gui/$(id -u)/com.cyberware.fleetdash. The button additionally needs approve_token_file in fleet.json.

🤖 Generated with Claude Code

rhCat and others added 2 commits July 25, 2026 19:21
…_back

The fleet monitor showed the approval queue and could not answer it: `needs_approve` was rendered as a
value-free token list, and the only way to act was to re-submit the claim by hand. Meanwhile the agent-side
human gate is unreachable in a caged deployment — every caller of resolve_gateway_approval() is a
chat-platform adapter (Discord/Telegram/Feishu/Teams) or a TTY, and a caged agent can reach neither. Measured
across the fleet: 45 push_backs on maria-dev-mac, 9 approvals, ALL from a test harness. The human gate has
never once been answered by a person.

This dashboard has NO app-auth and its monitor tokens are read-only by contract, so the write is gated
deliberately, every branch fail-closed:

- a SEPARATE credential — `approve_token_file` per node (or GOVD_APPROVE_TOKEN_<NAME>), a principal token the
  operator provisions on purpose. The monitor token is never reused. Absent (the default) => no button is
  rendered AND the route 403s, so the read-only posture is unchanged;
- CSRF — requires X-Fleetdash-Approve, a custom header a cross-origin page cannot set without a preflight
  this server never answers, so a hostile site cannot drive-by POST at 127.0.0.1;
- never approve blind — the run must exist in the mirror and actually be a push_back, and the claim's own
  skill/perk/var_keys are REPLAYED from the mirrored record so the UI cannot widen a claim into something the
  agent never made;
- --no-mirror refuses outright (no record to verify against), fixing a crash where mirror_dir was None;
- the loopback-only default and its FLEETDASH_ALLOW_OPEN acknowledgement are untouched.

HONEST LIMITS. This authorizes the CLAIM, not the tool call: the agent's gate fails closed in ~0.3s and is
not resumed by a later approval, so pressing approve creates the audit record and the `superseded` marker
without delivering the action. And the monitor token is one shared secret per node, not a person, so the
chain still cannot name WHO approved. Both wait on per-person principals + an acl.approve axis; this is a
stopgap that makes the ask answerable at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… testing docs

The render-level checks prove the button APPEARS under the right conditions; they cannot prove the
integrated path works, because the interesting behaviour is in the browser — the confirm() gate, the custom
CSRF header the fetch must set, and the claim the node actually receives. This drives real Chromium against
a real fleetdash and asserts on what the STUB NODE was handed, not on what the page says.

Hermetic: own fleetdash on a free loopback port, a stub govd that records the claim it is given, and an
on-disk mirror fixture in tmp_path. Never the live fleet; no credential outside the temp dir.

Every guard is mutation-verified — each was individually disabled and the suite had to go red:

  CSRF header check ....... caught (test_csrf_header_required)
  push_back guard ......... caught (test_cannot_approve_a_non_pushback)
  run_id regex ............ caught (test_path_traversal_run_id_refused)
  button render gate ...... caught (test_no_button_without_operator_credential)
  credential gate ......... caught (test_credential_gate_is_enforced_SERVER_side)

Two defects only mutation exposed, both of which would have shipped a green-but-hollow suite:

1. test_cannot_approve_a_non_pushback passed for the WRONG REASON — the fixture's allow-run had an empty
   needs_approve, so deleting the push_back guard still produced a 409 from the next check down. The fixture
   now carries approve tokens, making the guard the only thing that can refuse it. (Its perk also had to
   differ from the push_back's: the same skill/perk tuple makes mark_superseded hide the push_back's button —
   correct behaviour that would have silently gutted the happy-path test.)

2. There was NO server-side coverage of the credential gate — only "the button is not rendered". Withholding
   a button is cosmetic; anyone can POST /approve directly. That check holds the entire read-only posture and
   was untested.

README documents the setup, because two INDEPENDENT steps are needed (`pip install playwright`, then
`playwright install chromium`) and the failure when only the first has run — "Executable doesn't exist" —
does not say so. Records the shared browser cache paths (outside the venv), the env overrides, that the suite
importorskips (so `pytest tests` and CI are unaffected — these currently SKIP in CI), and the mutation
discipline plus its two traps: a str.replace(pattern, repl, 1) mutation can patch an EARLIER identical line
and wrongly report a test gap (mutate by line number, assert the line's content), and a fixture change can
neuter an unrelated test (re-run the whole file). Both bit during this work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@rhCat
rhCat merged commit 0b23b26 into main Jul 26, 2026
2 checks passed
@rhCat
rhCat deleted the feat/fleetdash-approve branch July 26, 2026 01:00
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.

1 participant