Skip to content
Documentation GitHub
Agent

Agent Permissions

Agent Permissions

Covers the full lifecycle of agent permission requests: dialog appearance, AllowOnce and AlwaysAllow grants, Deny, banner behavior when the conversation panel is closed, banner dismissal without granting, capability-based scoping, and the state of the respond_permission_request backend command. This spec is P0 because permission enforcement is a critical security boundary: a bug that silently bypasses capability gating allows the agent to mutate workspace content (create, delete, or reorganize pages) without user consent.

The permission system is event-driven. When the agent loop requires a capability that has not been pre-granted, it emits an agent:permission-required Tauri event. The React frontend (via agentStore.pendingPermission) renders a PermissionRequestDialog card inline in the message thread. When the conversation panel is closed, a PermissionRequestBanner appears at the bottom of the screen as a non-blocking nudge. The user’s response is sent to respond_permission_request on the backend, which logs the decision. Full runtime capability gating is tracked under INK-404; the current implementation logs responses and is designed for that enforcement layer to be wired in without a contract change.

Preconditions

  • HTTP bridge running on port 9990
  • A workspace initialized via initialize_workspace before each scenario
  • Agent harness started via start_agent before scenarios that require a running agent
  • Bridge shim injected via playwright.config.ts

Scenarios

Seed: seed.spec.ts

1. Permission request dialog appears in the conversation panel

When the agent emits agent:permission-required, the PermissionRequestDialog is rendered in the message thread with the correct tool name, capability, and explanation.

Steps:

  1. Start the agent harness via start_agent.
  2. Send a message that causes the agent to request the pages_write capability (e.g., “Create a new page called Test”).
  3. Open the conversation panel if it is not already visible.
  4. Observe the message thread.

Expected: A [data-testid="permission-request-dialog"] card appears in the message thread. The card shows: (a) the tool name in monospace (e.g., create_page), (b) the required capability in bold (e.g., pages_write), and (c) an explanation message from the agent. Three buttons are visible: “Allow Once”, “Always Allow”, and “Deny”. The agent has stopped streaming and is waiting for a response.

2. AllowOnce grants permission for a single operation

Choosing “Allow Once” allows the current tool call to proceed but does not persist the grant.

Steps:

  1. Start the agent and trigger a pages_write permission request (as in scenario 1).
  2. Click the “Allow Once” button ([data-testid="permission-allow-once"]) in the dialog.
  3. Observe the conversation panel and the backend logs.

Expected: The PermissionRequestDialog card disappears. The respond_permission_request command is called with response: "AllowOnce". The backend logs permission_request_responded with response=AllowOnce. The agent resumes processing. After the session ends, a subsequent action requiring pages_write would trigger a new permission request (the grant was one-time only).

3. AlwaysAllow grants persistent permission for the session

Choosing “Always Allow” records a persistent grant so the same capability is not requested again during the session.

Steps:

  1. Start the agent and trigger a pages_write permission request.
  2. Click the “Always Allow” button ([data-testid="permission-always-allow"]) in the dialog.
  3. Observe the backend log entry.
  4. Without restarting the agent, trigger another action that would require pages_write.

Expected: After step 2, respond_permission_request is called with response: "AlwaysAllow". The backend logs permission_request_responded with response=AlwaysAllow. In step 4, no new permission dialog appears for pages_write — the agent proceeds without interrupting the user.

4. Deny blocks the operation and the agent receives denial

Choosing “Deny” prevents the tool from executing and delivers a denial signal to the agent loop.

Steps:

  1. Start the agent and trigger a pages_write permission request.
  2. Click the “Deny” button ([data-testid="permission-deny"]) in the dialog.
  3. Observe the conversation panel.

Expected: The PermissionRequestDialog card disappears. respond_permission_request is called with response: "Deny". The backend logs permission_request_responded with response=Deny. The agent resumes without executing the denied tool — it should respond to the user explaining it could not complete the requested action due to a permission denial.

5. Permission request dialog is disabled while a response is pending

Clicking a response button while the command is in-flight must be idempotent — buttons are disabled after the first click.

Steps:

  1. Trigger a permission request dialog.
  2. Click “Allow Once” and, before the command completes (simulate latency or observe UI), immediately attempt to click “Always Allow” or “Deny”.

Expected: Once any button is clicked, all three buttons receive disabled attribute and show opacity-50 cursor-not-allowed styling. Duplicate respond_permission_request calls are not made. The isPending guard in PermissionRequestDialog prevents double submission.

6. Permission request banner appears when conversation panel is closed

When agent:permission-required fires and the conversation panel is not open, a bottom banner appears as a non-blocking notification.

Steps:

  1. Start the agent and make sure the conversation panel is closed.
  2. Trigger a pages_write permission request.
  3. Observe the bottom of the screen.

Expected: A [data-testid="permission-request-banner"] element appears at the bottom center of the screen. The banner text shows the capability name (e.g., pages_write). A “View” button is present. A dismiss (X) button is present. The banner does not block the workspace UI.

7. Permission banner — View button opens the conversation panel

Clicking “View” in the banner navigates the user to the conversation panel where the full dialog is shown.

Steps:

  1. Trigger a permission request with the conversation panel closed.
  2. Observe the banner at the bottom of the screen.
  3. Click the “View” button ([data-testid="permission-banner-view"]).

Expected: The conversation panel opens. The PermissionRequestDialog card is visible in the message thread, showing the tool name, capability, and the three response buttons. The banner disappears (because isAgentPanelOpen is now true).

8. Permission banner dismissal does not deny the permission

Clicking the dismiss (X) button in the banner hides it visually without recording a denial.

Steps:

  1. Trigger a permission request with the conversation panel closed.
  2. Observe the banner.
  3. Click the dismiss button ([data-testid="permission-banner-dismiss"]).

Expected: The banner disappears. respond_permission_request is NOT called — no denial is logged. If the user subsequently opens the conversation panel, the PermissionRequestDialog is still visible and actionable. The permission request is still pending.

9. Permission dialog shows tool name and rationale clearly

The dialog content accurately reflects the requesting tool and the reason stated by the agent.

Steps:

  1. Trigger a permission request where the agent calls a specific tool (e.g., delete_page).
  2. Observe the PermissionRequestDialog in the message thread.

Expected: The tool name is rendered in a font-mono styled element matching the tool name exactly (e.g., delete_page). The capability name is displayed in bold (e.g., pages_delete). If an explanation string was emitted in the agent:permission-required event, it is shown in the [id="permission-dialog-description"] paragraph.

10. Multiple capability scopes require separate dialogs

Different capabilities (e.g., pages_write and tags_write) are independent — granting one does not grant the other.

Steps:

  1. Start the agent and trigger an action requiring pages_write (e.g., creating a page).
  2. Click “Allow Once” to grant it.
  3. Trigger an action requiring tags_write (e.g., tagging the page).
  4. Observe whether a new permission dialog appears for tags_write.

Expected: After step 3, a new PermissionRequestDialog appears for tags_write. The prior AllowOnce grant for pages_write does not carry over to tags_write. The dialog accurately shows the new capability name. The previous capability grant is not visible or applicable to the new request.

11. Agent cannot be started without a workspace open

start_agent requires an open workspace. Without one, it returns an Internal error immediately.

Steps:

  1. Issue a start_agent command without first calling initialize_workspace or open_workspace.

Expected: The command returns a CommandError::Internal with a message containing “not initialized” or equivalent. No agent harness is created. The agent status remains Stopped.

12. respond_permission_request command accepts valid response variants

All three PermissionResponse variants (AllowOnce, AlwaysAllow, Deny) are accepted by the backend without error.

Steps:

  1. Send respond_permission_request with request_id: "req-1", capability: "pages_write", response: "AllowOnce".
  2. Send respond_permission_request with request_id: "req-2", capability: "pages_write", response: "AlwaysAllow".
  3. Send respond_permission_request with request_id: "req-3", capability: "pages_write", response: "Deny".

Expected: All three calls return Ok(()) (status: “ok”). No error is returned for any valid variant. Backend logs show permission_request_responded with the correct response field in each case.

Test Data

KeyValueNotes
write_capabilitypages_writeCapability required to create or update pages
delete_capabilitypages_deleteCapability required to delete pages
tags_capabilitytags_writeSecond capability scope for multi-capability isolation test
allow_once_responseAllowOnceSingle-use grant variant
always_allow_responseAlwaysAllowPersistent grant variant
deny_responseDenyDenial variant
dialog_testidpermission-request-dialogdata-testid for the inline dialog card
banner_testidpermission-request-bannerdata-testid for the bottom-of-screen banner
allow_once_btn_testidpermission-allow-oncedata-testid for the Allow Once button
always_allow_btn_testidpermission-always-allowdata-testid for the Always Allow button
deny_btn_testidpermission-denydata-testid for the Deny button
banner_view_testidpermission-banner-viewdata-testid for the View button in the banner
banner_dismiss_testidpermission-banner-dismissdata-testid for the dismiss button in the banner

Notes

  • The respond_permission_request Tauri command (INK-404) currently logs the response and clears the pending request. Full runtime capability gating — where the agent loop actually blocks until it receives a user decision — is tracked under INK-404 for a future wave. Tests should verify the command contract (correct parameters accepted, correct log output) and the UI contract (dialog appears/disappears correctly).
  • The HTTP bridge exposes start_agent, send_agent_message, and respond_permission_request routes. All scenarios in this spec are exercisable via the bridge.
  • The agent:permission-required Tauri event carries { toolName, capability, message }. The frontend maps this to a PermissionRequest object with an auto-generated id (UUID), stored in agentStore.pendingPermission.
  • Dismissing the banner (permission-banner-dismiss) only sets local component state (dismissed = true). It does NOT call setPendingPermission(null) in the store — the permission request remains accessible when the conversation panel is opened. This is the intended UX: banner hides non-destructively.
  • The PermissionResponse enum variants are AllowOnce, AlwaysAllow, and Deny — PascalCase, not snake_case. The frontend sends exactly these string values to respondPermissionRequest. Tests that invoke the command directly must use these exact casing values.
  • AgentSettings.capabilities (stored in settings.json) represents pre-configured, persistent capability grants set during agent setup — not the same as runtime AllowOnce/AlwaysAllow decisions. The settings capabilities map is edited in the AgentSettings UI panel, not via the permission dialog.

Was this page helpful?