Skip to content

feat: async gray-zone approval system + Approvals TUI - #4

Merged
Kritoooo merged 5 commits into
mainfrom
feat/approval-system
Jul 1, 2026
Merged

Kritoooo merged 5 commits into
mainfrom
feat/approval-system

Conversation

@Kritoooo

@Kritoooo Kritoooo commented Jul 1, 2026

Copy link
Copy Markdown
Member

Summary

Adds the async gray-zone approval system: when a command lands in the default-deny gray zone, run returns exit 7 and writes a pending request; an operator adjudicates it out-of-band via the CLI (agentssh approval grant/deny/ls/status/wait) or the new Approvals tab in the TUI. Grant scopes are once / session / host.

Backend

  • File-based coordination under ~/.agentssh/approvals/ (pending / responses / sessions), O_EXCL atomic claims, SHA-256 request/matcher digests, session-file flock.
  • Command matcher generalization (exact / safe-prefix / prefix host-grant modes) with anchored RE2 matchers; safe-prefix hardened so identity/side-effecting flags force exact.
  • Audit hash-chain extended with approval events (requested / granted / denied), chain stays intact.

TUI — Approvals tab

  • Consequence-first queue: each row shows id / host / command / KIND, and the focused row states exactly what an [h] host-allow would free.
  • Enter-driven verdict menu: a selectable once / session / host / deny chooser (←/→ move, enter apply, esc cancel); host is omitted for privileged requests; the menu pins to its request across background polls.
  • Responsive, aligned columns via the shared table renderer (fills the frame, host no longer truncated).

Review + polish

  • Resolved 15 code-review findings (safe-prefix escalation hardening, decision atomicity, disabled-runtime authorization, corrupt-response handling, dedupe/reap, etc.) — see docs/plans/approval-review-fixes.md.
  • e2e demo script seeds a pending approval queue so the tab can be exercised live.
  • README install version bumped to v0.7.0.

Testing

go build ./... and go test ./... pass. New tests cover the safe-prefix escalations, decision atomicity, corrupt-response handling, and the Enter chooser (open/cancel/nav, host-omitted-on-privileged, apply-resolves-request).

🤖 Generated with Claude Code

Kritoooo and others added 5 commits June 30, 2026 21:19
Add an opt-in, async (non-blocking) approval path for gray-zone
(default-deny) commands. Explicit deny rules stay unoverridable hard
blocks; only Rule=="default-deny" becomes approvable.

Backend (internal/approval): injection-safe Generalize (fully \A..\z
anchored, [ \t] never \s, byte-level ASCII tokenizer, interpreter/priv/
destructive denylists, safe-prefix default), split-engine Authorize
(grants consulted only in the default-deny branch so they never shadow
an explicit allow/deny), session grant store (flock+atomic, host-bound,
TTL, once consumed under lock), pending/responses with >=96-bit ids,
O_EXCL and req_digest (resolution is notification-only; run re-derives
via Authorize on re-run).

Wiring: run returns exit 7 on NeedsApproval (default-off => unchanged
exit 6); multi-host preflights before executing; new operator CLI
approval ls/grant/deny + session end, agent-readable approval status/wait.
policy exports RuleDefaultDeny + reserves __agentssh_approval group.
audit gains approval_requested/granted/denied + 4 omitempty fields
mirrored into Record and canonicalRecord (old chains still verify).

Verified locally: gofmt/vet/build clean, go test -race ./... all green.

Docs: docs/plans/approval-system.md (spec, v1.1 with Codex review fixes),
approval-system-codex-review.md, TUI approvals prototypes. TUI tab and
overview.md update land in a follow-up commit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add an Approvals top-level tab to `agentssh tui` so the operator can
adjudicate gray-zone approval requests without leaving the console
(previously CLI-only via `agentssh approval`).

Design (docs/plans/tui-approvals-final.html): one line per request, no
per-row key column, and a single "consequence-first" footer that — for
the focused request — states in plain English what an [h] host-allow
would free (`systemctl status *`), or why host is unavailable (sudo) /
exact-only (pipe). The list polls pending/ on a 1.5s tick so the queue
and a status-bar "N pending" badge stay live from any tab. Keys
o/s/h/d decide via the same approval.ApplyDecision wiring as the CLI
(reloads inventory+policy from disk; the TUI is the operator console so
no extra master/TTY gate). Host scope on a privileged request is refused
in place without writing. English UI throughout.

internal/tui/app.go gains the third entry tab (tab/1-3 cycle, %3),
routing, polling, help, and the badge. New ChannelTUI constant.

overview.md §5/§10 updated: the optional async approval is shipped
(default-off, exit 7, TUI adjudicates); TUI is no longer audit-only.

Tests: render + cursor-driven consequence + privileged-host refusal +
disabled-off-note. gofmt/vet clean, go test -race ./... all green.

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

Address correctness and safety issues found in the async gray-zone
approval review (git diff main...HEAD).

Security-critical:
- F1-F3: harden safe-prefix host generalization. Remove journalctl
  safe-prefix case; force exact for git --output/-o and kubectl
  identity options (--as, --as-group, --token, --kubeconfig, --server)
  and other side-effecting flags; pin git/kubectl first positional so
  grants do not widen across resources/revisions.
- F4: make the O_EXCL response resolve the first mutating step in
  ApplyDecision; only the winner applies grants and audit side effects.

Correctness:
- F5: abort TUI approval decisions on inventory/policy load errors
  instead of saving an empty policy.
- F6: gate approval TUI verdict keys, queue polling, and pending badge
  on runtime.Enabled.
- F7: session end <base> removes derived base@host approval files.
- F8: when approval is disabled, evaluate the full policy including
  persisted __agentssh_approval host rules (do not strip them).
- F9: include Matcher.Promotable in the matcher SHA-256 digest.
- F10: corrupt/empty/ID-mismatch response files return
  ErrCorruptResolution instead of wedging status/adjudication.
- F11: drop single-host session-file binding; grants carry Host.
- F12: dedupe pending on SessionID+Host+CmdSHA256; reap resolved
  pending/response files past TTL.
- F13: invalid optional approval runtime config warns (English) and
  degrades to disabled instead of exiting.
- F14: policy test is a static policy-engine check, no session reads.
- F15: approval-pending human output is English.

Mechanical:
- audit record for not_run preflight-blocked targets
- add cmd to runResponse and populate it (incl. approval-pending)
- add approval.ChannelExit constant for exit-channel audit records

All F1-F15 plus mechanical items covered by tests; go build ./... and
go test ./... pass. See docs/plans/approval-review-fixes.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Approvals tab polish:

- Replace the hand-rolled fixed-width row renderer with the shared
  responsive renderTable: aligned ID/HOST/COMMAND/KIND columns that span
  the frame (COMMAND takes the slack, HOST is no longer truncated),
  a header row, and full-row selection highlight — matching the other
  tabs.
- Add an Enter-driven verdict chooser that replaces the o/s/h/d footer
  hints with a selectable once/session/host/deny menu: ←/→ (or tab)
  move, enter/space apply, esc cancel. While open the tab captures the
  keyboard so esc/enter aren't treated as quit/tab-nav.
- The menu omits host for non-promotable (privileged) requests, and
  pins to the targeted request across background polls so a reload can't
  misapply the verdict. The direct o/s/h/d shortcuts still work.

Also seed approval data in the e2e script: enable approval in the demo
policy and produce a six-request pending queue via `run` (exit 7 in the
gray zone, no SSH), covering every KIND/consequence, and refresh the
manual checklist for the current 3-tab layout.

Tests: chooser open/cancel/nav, host omitted on privileged, and apply
resolving a real pending request. go build ./... and go test ./... pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
First release to include the async gray-zone approval system (run exit 7
+ operator adjudication via CLI and the TUI Approvals tab).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Kritoooo
Kritoooo merged commit cb78386 into main Jul 1, 2026
2 checks passed
@Kritoooo
Kritoooo deleted the feat/approval-system branch July 1, 2026 09:12
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