Skip to content

feat(approvals): once, this chat and always tool approval scopes - #273

Merged
sambitcreate merged 7 commits into
mainfrom
feature/approval-scopes
Sep 30, 2026
Merged

sambitcreate merged 7 commits into
mainfrom
feature/approval-scopes

Conversation

@sambitcreate

Copy link
Copy Markdown
Owner

Summary

Adds three approval scopes for the parent agent's plain workspace tools (run_command, write_file, edit_file) in Ask workspaces:

  • Allow once: the default.
  • Allow for this chat: kept in memory, bound to the chat.
  • Always allow: persisted and revocable in Settings → Tool approvals.

A rule is keyed by [workspaceId, toolName, exact pattern]. The pattern is the exact trimmed command, or the normalized workspace-relative path. There are no prefixes or globs, and absolute, ~ and .. paths are only ever offered once. So a rule is never broader than what the card showed.

Source: the wave's Tier 1/2 item "Tool approval scopes: once, this chat, always".

Changes per surface

  • Main
    • ToolApprovalRuleBook (main/services/tool-approval-rules.ts) holds per-chat rules in memory (max 100 per chat, oldest evicted) and persists always rules to tool-approval-rules.json (max 200).
    • Normalization is strict, so hand-widened, non-canonical or malformed rules are dropped.
    • ToolApprovalCoordinator honors a scope only if it was offered, it is an allow, and it is a known name.
    • llm-client skips the prompt when a remembered rule matches, and grants the chosen scope after an allow.
    • New IPC: chat:listApprovalRules, chat:revokeApprovalRule, chat:revokeAllApprovalRules. chat:approve also accepts scope.
  • Desktop UI
    • The approval card gets transparent Allow for this chat and Always allow buttons next to the accent Allow once.
    • New Settings section Tool approvals in the Agent group. It uses grouped cards, a per-row Revoke and a destructive Revoke all, following docs/settings-design-system.md.
  • Aiden Remote: contract revision 16
    • PendingApproval.scopes is optional, has at least 2 entries, starts with once, and is present only when canAllow.
    • POST /approvals/{id}/respond accepts an optional scope, but only with allow and only if it was offered. Otherwise it returns 400 invalid_request. The response echoes the scope, and the scope is part of the idempotency fingerprint.
    • OpenAPI, the shared fixture and its Android byte copy, and the TS, iOS and Android revision assertions are all updated. docs/aiden-remote-api-v1.md is updated too.
  • iOS / Android
    • Scopes decode tolerantly.
    • The approval card gets a "More allow options" menu, sending a scope only when offered and never for once or deny.
  • Bot / Telegram
    • Bot-bound turns are never offered scopes; Bot authority stays policy-owned.
    • Telegram runs with full permission and never prompts, so nothing changes there.
    • Privileged or structured details, subagent run grants, MCP mutations, Form Fill, schedule/automation and browser approvals also stay once-only.

Tests

  • New main/services/tool-approval-rules.test.ts covers:
    • exact targets
    • chat isolation and the cap
    • persistence across restart, revoke and revoke all
    • the persisted bound
    • hand-edit rejection
  • New renderer/components/settings/tool-approval-settings.test.tsx renders the rows, revoke labels, busy, empty, loading and error states, plus the scope helpers.
  • Extended:
    • tool-approval.test.ts: scope fencing.
    • aiden-remote-streams.test.ts: offered, allowed and chosen; idempotency conflict; host path.
    • aiden-remote-router.test.ts: body validation and echo.
    • aiden-remote-protocol.test.ts: Ajv against the fixture and request schema.
    • remote-approval.test.ts and settings-section.test.ts.
  • New files are appended to the root test chain and scripts/ci-test-registry.json.
  • Local runs, all passing:
    • tsc --noEmit and eslint on the touched files.
    • test:aiden-remote, test:settings-design, test:ci-policy and test:agents-instructions.
    • The focused tool-approval and IPC contract suites.
    • iOS XCTest (AidenChatTests, AidenRemoteClientTests, AidenRemotePhase0Tests).
    • Android AidenChatTest (56) and AidenBotContractTest (15).

Notes

  • Protocol revision collision: other PRs in this wave may also claim contract revision 16. Whichever merges second should renumber, following the AGENTS.md hotspot rule (both fixtures, TS/iOS/Android assertions, docs).

Follow-ups

  • Scope UI for the Aiden Live assistant and for subagent/Bot approvals, if wanted. That needs a separate authority review.
  • Call ToolApprovalRuleBook.forgetChat on chat deletion. Chat rules are in memory and bounded today.
  • Live-refresh Settings → Tool approvals via onChange. Today it reloads on open and after each revoke.

🤖 Generated with Claude Code

Plain parent workspace commands and file writes in Ask workspaces can now
be allowed once, for this chat, or always. Rules match only the same tool
with the exact trimmed command or exact normalized workspace-relative path
in the same workspace, so a rule is never broader than the approval card.

- Main: ToolApprovalRuleBook (in-memory chat rules, persisted always rules,
  strict normalization), coordinator scope fencing, llm-client matching and
  granting, and IPC to list and revoke rules.
- Desktop: approval card scope buttons and Settings -> Tool approvals.
- Aiden Remote contract revision 16: PendingApproval.scopes and an optional
  allow-only scope on approval responses, echoed back.
- iOS and Android: "More allow options" menus that send an offered scope.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

The new path normalization lets a remembered approval match a different file; please fix this before merging.

Reviewed changes This review covers the approval-scope implementation across the desktop main process and UI, Aiden Remote, and the iOS and Android clients.

  • Remembered approval scopes. Adds once, chat, and always choices for the parent workspace command and file-mutation tools, with local persistence and revocation in Settings.
  • Remote approval contract. Adds optional offered scopes to revision 16, validates and fingerprints scoped responses, and exposes the choices in both native clients.
  • Approval fencing. Limits remembered rules to exact workspace/tool/target keys and keeps specialized, Bot, and structured approvals once-only.

⚠️ Deleted chats retain remembered targets

Deleting a chat never clears its chat-scoped rules, leaving up to 100 exact command or path strings per deleted chat resident until the app quits. The per-chat limit therefore does not bound accumulation across deleted chats in a long-running session.

Technical details
# Clear chat-scoped approvals on deletion

## Affected sites
- [main/services/tool-approval-rules.ts](https://github.com/sambitcreate/aiden-agent/blob/baaa33c5059a3bb26b1e251e4852452d812c724a/main/services/tool-approval-rules.ts#L276-L279) — `forgetChat` is added but has no production caller.
- [main/services/chat-application-service.ts](https://github.com/sambitcreate/aiden-agent/blob/baaa33c5059a3bb26b1e251e4852452d812c724a/main/services/chat-application-service.ts#L315-L320) — successful deletion commits at `chatStore.remove` before completion.

## Required outcome
- Clear a chat's in-memory approval rules after deletion succeeds, while retaining them if deletion fails or rolls forward.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using GPT Luna | 𝕏

Comment thread main/services/tool-approval-rules.ts Outdated
@very-hermes-bot

very-hermes-bot commented Sep 27, 2026 •

Copy link
Copy Markdown
Collaborator

Hermes Review Bot

Confidence: 4

Engine: agy/gemini-3.8-flash-high
Review mode: full
Head: 35cde6404b5632c3726870be87618cf4217af900
Generated: 2026-09-30T21:35:13+00:00
Reviews: 1

Summary

This change introduces scoped tool approvals (once, chat, and always) for plain workspace tool executions (run_command, write_file, and edit_file) in Ask workspaces, allowing users to remember exact command strings or posix-normalized relative file paths per chat (in memory) or permanently per workspace (persisted in tool-approval-rules.json). It updates the Electron coordinator, LLM tool dispatch gate, desktop approval card, desktop Settings UI with a new Tool approvals section, and bumps the Aiden Remote contract to revision 17 with coordinated updates across OpenAPI schemas, protocol fixtures, and native iOS/Android clients. Maintainers should double-check the revoke error-handling path in Settings, where a failed revocation can have its error state immediately cleared by the subsequent reload.

Confidence Score: 4/5

4/5: Traced end-to-end across main process, IPC, remote protocol revision 17, mobile clients, and settings view; identified error-state clearing bug in settings revoke action.

📁 Important Files Changed
  • main/services/tool-approval-rules.ts: Implements target extraction, command and path normalization, persistence bounds (max 200), in-memory chat bounds (max 100), and ToolApprovalRuleBook.
  • main/services/llm-client.ts: Checks remembered rules before prompting for workspace tools, attaches offered scopes to approval requests, and grants the selected scope upon allow.
  • main/services/tool-approval.ts: Extends ToolApprovalCoordinator to track offered scopes and fence granted scopes against what was explicitly offered.
  • main/services/aiden-remote-streams.ts & main/services/aiden-remote-router.ts: Updates Remote streams and router for contract revision 17, validating requested scopes and incorporating them into idempotency keys.
  • renderer/components/settings/tool-approval-settings.tsx: Renders the new Tool approvals section in Settings, providing individual rule revocation and bulk revocation.
  • renderer/main/chat-pane.tsx: Adds "Allow for this chat" and "Always allow" actions to the workspace approval card between Deny and Allow once.
  • ios/AidenOnTheGo/Features/Remote/AidenChatFeature.swift & android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/chat/AidenChatDetailScreen.kt: Adds the "More allow options" dropdown menu to the mobile approval interfaces.
  • protocol/aiden-remote/v1/openapi.json & fixtures: Advances contract revision to 17 and defines the schema constraints for approval scopes.

Findings

  • [P2 · logic] renderer/components/settings/tool-approval-settings.tsx:126 — Revocation error is immediately erased by finally load() → commented inline
📊 Sequence Diagram
sequenceDiagram
    participant UI as ChatPane / Mobile Client
    participant Main as llmClient (beforeToolCall)
    participant Coord as ToolApprovalCoordinator
    participant Book as ToolApprovalRuleBook (DataStore / Memory)

    Main->>Book: match(chatId, target)
    alt Rule remembered (chat or always)
        Book-->>Main: "chat" | "always"
        Main-->>Main: Skip prompt & run tool
    else No rule remembered
        Main->>Coord: request(prompt with scopes: ["once", "chat", "always"])
        Coord->>UI: chat:approval (prompt with scopes)
        UI->>Coord: decide(approvalId, allow, scope)
        Coord-->>Main: approvalOutcome = "allowed"
        Main->>Coord: takeDecisionPayload(streamId, toolCallId)
        Coord-->>Main: { scope: "chat" | "always" }
        Main->>Book: grant(scope, chatId, target, workspaceLabel)
        Main-->>Main: Run tool
    end
Loading

Machine-Readable Findings

[
  {
    "file": "renderer/components/settings/tool-approval-settings.tsx",
    "start_line": 126,
    "end_line": 134,
    "severity": "P2",
    "comment_type": "logic",
    "confidence": 0.95,
    "title": "Revocation error is immediately erased by finally load()",
    "mechanism": "When an action fails, catch sets the error message, but finally immediately executes await load(), which unconditionally calls setError(null) upon successfully reading the rules list, preventing error feedback from displaying to the user.",
    "repair": "Move await load() inside the try block so reloading only occurs after successful mutations and does not overwrite the caught error.",
    "id": "aab5c5b3fad688c5"
  }
]

Last reviewed commit: 35cde6404b56
Reviews (1) · Comment /hermes review to trigger a new review · /hermes review full for full re-review

@pullfrog

pullfrog Bot commented Sep 27, 2026

Copy link
Copy Markdown
Contributor

Run failed. View the logs →

Pullfrog  | Rerun failed job ➔ | View workflow run | via Pullfrog | Using GLM Flash | 𝕏

@very-hermes-bot very-hermes-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inline review — 2 finding(s) anchored to the diff. See the pinned summary comment for the overview.

Comment thread main/services/tool-approval-rules.ts
Comment thread main/services/tool-approval-rules.ts Outdated

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes This re-review covers the delta since the prior Pullfrog review (baaa33c5 → ac210eb4): one commit fixing the path-trimming concern raised in that review, plus its regression test and a memory-file note.

  • Preserved whitespace in remembered file targets. normalizePathPattern no longer trims, so distinct filesystem names (" notes.md" vs "notes.md") can no longer share an approval key. The regression test covers persisted grant/match behavior and would have failed under the old trimming; all seven rule-book tests pass on this head, and the touched files lint and typecheck cleanly.
  • Verified equivalence on both sides of the rule. llm-client passes raw tool args to the remembered-rule lookup and to the post-allow grant, so the target computed at prompt time and the target granted after an allow are built by the same normalization from the same raw input — no caller reintroduces the collapse, and persisted documents still reject non-canonical patterns.

The remaining open thread (deleted chats retaining in-memory chat rules until quit) is unchanged by this commit, author-recorded as a follow-up in the PR body, and bounded per chat in memory — not a merge blocker.

Pullfrog  | View workflow run | Using GLM Flash | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes This re-review covers the delta since the prior Pullfrog review (ac210eb4 → a7b4f34): one commit hardening the remembered-rule normalizers in response to the open inline review findings, plus its regression test and a memory-file note.

  • Rejected parent segments in remembered file targets. normalizePathPattern now rejects any path containing a literal .. segment before normalization, so traversal-shaped arguments stay once-only instead of being collapsed to a canonical pattern that could merge distinct targets (the P1 finding). The check runs on the raw input, and persisted documents with such patterns are dropped at load through the same normalizer.
  • Closed the control-character bypass in command rule targets. normalizeCommandPattern now inspects the raw, untrimmed value and permits only tab, newline, and carriage return among control characters — previously, embedded ANSI escapes or bells passed and trim() silently stripped edge controls, so the old check could be defeated in both positions (the P2 finding).
  • Added regression coverage for both hardenings. The new test asserts .. paths in interior, doubled, and leading positions and seven control characters in embedded and leading positions all yield no rule target, while tab/newline commands still qualify — the assertions fail against the previous code. All 8 rule-book tests pass on this head, the touched files lint cleanly, and both new test files remain registered in the root test chain and the CI registry. Match-side consistency was re-verified: llm-client computes the target once and reuses it for the remembered-rule lookup and the post-allow grant, so an unsafe path can neither offer scopes nor match a persisted rule.

The remaining open thread (deleted chats retaining in-memory chat rules until quit) is unchanged by this commit, author-recorded as a follow-up in the PR body, and bounded per chat in memory — not a merge blocker.

Pullfrog  | View workflow run | Using GLM Flash | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes This re-review covers the delta since the prior Pullfrog review (a7b4f34 → 647df59): one commit extending the parent-segment rejection in remembered file targets to Windows and mixed separators, plus regression coverage.

  • Closed the backslash gap in parent-segment rejection. The pre-normalization check in normalizePathPattern now splits on both / and \, so ..\outside.txt, src\..\outside.txt, and mixed forms like src/..\outside.txt are rejected as rule targets and stay once-only, matching the documented contract that .. paths are never remembered regardless of separator style.
  • Extended the regression test. The three new path assertions fail against the previous split("/") check, which passed raw backslash traversal through to a stored pattern. All 8 rule-book tests pass on this head and the touched files lint cleanly. As before, the target is computed once in llm-client and reused for both the remembered-rule lookup and the post-allow grant, so a rejected path can neither offer scopes nor match a persisted rule.

The previously noted follow-ups (calling forgetChat on chat deletion, live-refreshing Settings) remain author-documented and unchanged.

Pullfrog  | View workflow run | Using GLM Flash | 𝕏

sambitcreate and others added 2 commits September 29, 2026 21:39
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	docs/aiden-remote-api-v1.md
#	package.json
#	scripts/ci-test-registry.json

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No critical issues — one minor PR-description mismatch below.

Reviewed changes This re-review covers the merge delta from 0ce43e5b to a6b16f77.

  • Renumbered the Remote contract. Moved approval scopes to revision 17 after main's #275 agent-interrupt contract claimed revision 16, keeping the OpenAPI schema, fixtures, docs, and iOS/Android assertions in sync.
  • Preserved test registration through the merge. Checked the root test-chain changes; subagent UI suites remain reached through pretest:subagents, and the CI policy inventory passes.

ℹ️ Nitpicks

  • The PR description still says the approval-scope contract is revision 16; this head uses revision 17 after syncing main. Please update the summary to match.

Pullfrog  | Fix it ➔ | View workflow run | Using GPT Luna | 𝕏

@very-hermes-bot very-hermes-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inline review — 1 finding(s) anchored to the diff. See the pinned summary comment for the overview.

Comment on lines +130 to +134
setError(actionError instanceof Error ? actionError.message : "Couldn’t revoke that rule.");
} finally {
setPending(null);
await load();
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2 · logic] Revoke failure error state is immediately cleared by post-action reload
Confidence: 0.95

When revoking an approval rule fails, the catch block sets the error state, but the finally block immediately runs load() which resets setError(null) upon fetching rules, erasing user feedback.

Repair: Use toast.error to display the failure notification or pass a flag to load() to avoid resetting error when an action fails.

@very-hermes-bot very-hermes-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inline review — 1 finding(s) anchored to the diff. See the pinned summary comment for the overview.

Comment on lines +126 to +134
try {
await action();
toast.success(success);
} catch (actionError) {
setError(actionError instanceof Error ? actionError.message : "Couldn’t revoke that rule.");
} finally {
setPending(null);
await load();
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2 · logic] Revocation error is immediately erased by finally load()
Confidence: 0.95

When an action fails, catch sets the error message, but finally immediately executes await load(), which unconditionally calls setError(null) upon successfully reading the rules list, preventing error feedback from displaying to the user.

Repair: Move await load() inside the try block so reloading only occurs after successful mutations and does not overwrite the caught error.

@sambitcreate
sambitcreate merged commit cdc5594 into main Sep 30, 2026
24 checks passed
@sambitcreate
sambitcreate deleted the feature/approval-scopes branch September 30, 2026 22:16
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.

2 participants