Skip to content

feat(desktop): permission policy config + actionable Allow/Deny card (#4938) - #5107

Closed
wpfleger96 wants to merge 2 commits into
mainfrom
hayt/permission-policy
Closed

feat(desktop): permission policy config + actionable Allow/Deny card (#4938)#5107
wpfleger96 wants to merge 2 commits into
mainfrom
hayt/permission-policy

Conversation

@wpfleger96

@wpfleger96 wpfleger96 commented Aug 6, 2026

Copy link
Copy Markdown
Member

What

Implements the desktop side of #4938 — permission policy configuration and the actionable Allow/Deny card for ACP session/request_permission requests.

Config

Rust structs

  • New permission_policy module: PermissionPolicy enum (ask | allow | reject, lowercase serde), PermissionPolicySource (agent | global_default | built_in), and resolve_effective_permission_policy (precedence: per-agent > global > built-in ask).
  • ManagedAgentRecord.permission_policy: Option<PermissionPolicy> — per-agent override, None = inherit.
  • GlobalAgentConfig.permission_policy: Option<PermissionPolicy> — fleet default, follows the existing spawn-time read + picked-up-on-next-restart pattern.
  • Resolved value injected as BUZZ_ACP_PERMISSION_POLICY at spawn; key added to RESERVED_ENV_KEYS.
  • SpawnSnapshot includes permission_policy; a per-agent edit surfaces in the existing needsRestart flow.
  • ManagedAgentSummary exposes the resolved permission_policy + permission_policy_source.
  • UpdateManagedAgentRequest uses double-Option (None = unchanged, Some(None) = clear, Some(Some(v)) = set); server rejects edits to remotely deployed agents.
  • Remote agents: policy is read at deploy time, baked into policy_env, read-only while deployed.

TypeScript types

  • PermissionPolicy = "ask" | "allow" | "reject" and PermissionPolicySource = "agent" | "global_default" | "built_in" in types.ts.
  • ManagedAgent gains permissionPolicy + permissionPolicySource (resolved values).
  • CreateManagedAgentInput and UpdateManagedAgentInput get optional permissionPolicy (null = clear).
  • RawManagedAgent mapped in tauri.ts with safe defaults.

Actionable card

  • ObserverEvent.authorization?: { requestNonce, actionable, reason? } added to agentSessionTypes.ts.
  • agentSessionTranscript.ts: pendingPermissionsByNonce map indexes actionable cards by nonce; authorization envelope fields attached to lifecycle items.
  • control_result/permission_decision is treated as a delivery confirmation only (statuses: sent | no_active_turn | channel_full | channel_closed | no_channel) — the card stays actionable on any non-sent status so the user can retry.
  • Terminal outcomes will be wired via enveloped acp_write frames correlated by requestNonce once Duncan's contract is pinned in NIP-AO and Thufir's review lands.
  • agentControl.ts: sendPermissionDecision(pubkey, nonce, optionId) — fire-and-forget; PermissionDecisionButtons resets button pending-state on error.
  • LifecycleActivity.tsx: PermissionDecisionButtons renders per-option buttons (reject_* = destructive styling); local pending state; resets on error so the user can retry. Shown when actionable && !outcome.

Settings UI

AgentInstanceEditDialog.tsx:

  • Shows effective policy and its source (agent / global default / built-in).
  • Local agents: select with Inherit / Ask / Allow / Reject options.
  • Remote-deployed agents: read-only field with shutdown + redeploy remediation hint.

Tests

All Rust and TS unit test suites pass (2249 Rust, 4392 TS).

File size ratchet

Several pre-existing large files grow by the minimum required to add new fields/logic. The ratchet CI step will flag these; the growth is intentional for this feature:

File +lines Notes
types.rs +4 New fields on ManagedAgentRecord + ManagedAgentSummary
agent_models.rs +12 Update handler for new permission_policy input field
agents.rs +1 Default field in create path
discovery/tests.rs, readiness.rs +1 each New required struct field in test fixtures
AgentInstanceEditDialog.tsx +78 Per-agent policy select + remote read-only UI
agentSessionTranscript.ts +70 Nonce map, authorization parsing, control_result handler
tauri.ts +7 RawManagedAgent mapping
types.ts +38 New types + field additions

New file: permission_policy.rs (82 lines) — extracted to keep types.rs as close to the limit as possible.

…4938)

Add per-agent and fleet-wide permission policy configuration with
an actionable Allow/Deny card for the ask policy.

**Rust (desktop/src-tauri)**
- Add `permission_policy` module: `PermissionPolicy` enum (ask | allow |
  reject, lowercase serde), `PermissionPolicySource` (agent | global_default |
  built_in), and `resolve_effective_permission_policy` (precedence: per-agent
  > global > built-in ask)
- Add `permission_policy: Option<PermissionPolicy>` to `ManagedAgentRecord`
  (per-agent override) and `GlobalAgentConfig` (fleet default)
- Inject resolved policy as `BUZZ_ACP_PERMISSION_POLICY` env var at spawn;
  add to `RESERVED_ENV_KEYS` so users cannot override via env-vars UI
- Include `permission_policy` in `SpawnSnapshot` / restart-diff so edits
  surface in the existing `needsRestart` flow
- Expose `permission_policy` + `permission_policy_source` on
  `ManagedAgentSummary` (resolved values)
- Extend `UpdateManagedAgentRequest` with double-Option `permission_policy`
  (None = unchanged, Some(None) = clear, Some(Some(v)) = set); reject edits
  to remotely deployed agents with a clear error message
- Add remote-deployed agent path in `agents_deploy.rs`: read per-record
  policy, fall back to desktop default, inject into `policy_env`

**TypeScript (desktop/src)**
- `PermissionPolicy = "ask" | "allow" | "reject"` and
  `PermissionPolicySource = "agent" | "global_default" | "built_in"` in
  `types.ts`; add to `ManagedAgent`, `CreateManagedAgentInput`, and
  `UpdateManagedAgentInput` (null = clear per-agent override)
- `tauri.ts`: add `permission_policy` / `permission_policy_source` to
  `RawManagedAgent` with safe defaults; map in `fromRawManagedAgent`
- `agentSessionTypes.ts`: add `authorization?: { requestNonce, actionable,
  reason? }` to `ObserverEvent`; extend `lifecycle` `TranscriptItem` with
  `requestNonce`, `actionable`, `authorizationReason`, `options`
- `agentSessionTranscript.ts`: add `pendingPermissionsByNonce` map; parse
  `authorization` envelope from `session/request_permission` events;
  handle `control_result/permission_decision` to retire cards on terminal
  outcomes, including the pinned uncertain message
- `agentControl.ts`: add `sendPermissionDecision(pubkey, nonce, optionId)`
  fire-and-forget control API
- `LifecycleActivity.tsx`: `PermissionDecisionButtons` component renders
  per-option buttons styled by kind (reject_* = destructive); local pending
  state with retry on error; rendered when `actionable && !outcome`
- `AgentInstanceEditDialog.tsx`: permission policy select (Inherit / Ask /
  Allow / Reject) for local agents; read-only for remote-deployed agents with
  a shutdown+redeploy hint; shows effective value and source

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
@wpfleger96
wpfleger96 requested a review from a team as a code owner August 6, 2026 20:36
…utcome

Per interface note from Paul (2026-08-06): control_result statuses
(sent | no_active_turn | channel_full | channel_closed | no_channel)
confirm whether the permission_decision click was delivered to the
harness, not whether the permission was applied/denied.

Terminal outcomes arrive as enveloped acp_write frames correlated by
requestNonce. The card retirement matrix will be wired once Thufir's
review of Duncan's buzz-acp contract lands and NIP-AO is pinned.

Updated the control_result handler to preserve card actionability on
delivery — the PermissionDecisionButtons component already handles
button-level pending-state reset via its own catch handler if the
fire-and-forget send fails.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
@wpfleger96

Copy link
Copy Markdown
Member Author

Consolidated into #5106 per Will's request that this ships as one PR.

@wpfleger96 wpfleger96 closed this Aug 6, 2026
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