feat: human-in-the-loop tool approvals + network-capable shell - #52
Merged
Conversation
…sitories Persistence layer for tool-call approvals: the ApprovalRequest (one row per non-memory gate hit) and UserToolApproval (standing always-scope allow/deny) models, the TOOL_APPROVAL notification type, and the MCP annotation/approval-override columns. Adds repositories for both tables and registers them in DbModule.
Core of the tool-approval system: - ToolApprovalService rendezvous gate with resolve/revoke/timeout/abort and a restart sweep; precedence lookup over standing, session, and per-run memory. - Domain types (offered-scope/downgrade rules), params redaction, and metrics. - REST controllers for resolve/revoke/pending and standing approvals. - Pub/sub approval-frame delivery keyed off cache.constants. - Tool interface gains requiresApproval/approvalScope/resourceKey fields; adds the run-activity module the gate reads.
…er image Adds an ephemeral, network-capable shell tool gated by policy.allowShellNet: - shell_net tool and a dedicated ShellNetConcurrencyLimiter, with per-policy memory/timeout/cpu/concurrency caps (maxShellNet* columns). - shell tool sensitive-command classification so risky invocations can be gated. - shell-net-runner image (curl/wget/git/ssh + writable HOME). - Unit, concurrency, classification, and live integration coverage.
Boots the network-capable runners behind an L3 egress filter: - egress-firewall.sh blocks private/metadata/gateway ranges (SSRF) and drops to an unprivileged user via gosu at container start; python-runner self-firewalls on boot with a dual-mode entrypoint. - container-runner gains an egressFirewall start flag and exec-user option; the no-network path is unchanged. ExecOptions/egress types live in shared. - python_run_net now runs behind the firewall; compose files, .env.example, and the image build scripts updated for the new build context. Integration test proves private/metadata/gateway blocked while public + DNS + allowlist work.
Enforces approvals where every tool call funnels through: - ToolRegistry gates execute() behind ToolApprovalService with registration-time scope validation; fails closed when an approval-requiring tool runs without an approval context. Per-action predicates keep read-only actions exempt (cron gates only mutating actions). - Privileged built-ins (cron, wiki share/unshare/delete, browser) annotated with approval scopes; MCP tools derive ask-by-default from captured annotations plus per-tool overrides. - Approval context wired into agent runs. Full integration coverage.
Routes approval frames to the surface the user is on: - Telegram inline-keyboard prompts with callback resolution; the tracked prompt is edited when tapped, and updates are processed concurrently (@grammyjs/runner) so approval taps arrive promptly. - Web WS approval frames over the channel protocol. - Channel-manager routing plus module wiring.
Delivers approvals across the dashboard UI: - In-chat approval card with honest offered-scope buttons, wired through use-chat frame handling and the conversations page. - Dashboard-wide pending-approval banner (counts only still-pending) and inline resolve from the notification bell. - Standing-approvals management page. - Approval frame parsing/labels helpers, reduced-motion support, and the notifications API client.
- Session reset revokes pending approvals and clears per-session approval memory; ApprovalsModule wired into the app module. Reset-command and e2e coverage. - Make the destructive seed reset opt-in via SEED_RESET so a normal seed no longer wipes existing data.
Lets operators mark MCP tools as ask-before-use per connection:
- mcp.service/controller expose and persist sparse approvalOverrides
({ [toolName]: 'ask' | 'allow' }); tier-utils and mcp-client capture tool
annotations at discovery. Schema and validation updated in shared.
- Tiers tab gains an ask-before-use column; web mcp client wired through.
Gate agent providers against the owner's Policy.allowedProviders: - agents.service rejects creating/updating an agent that names an out-of-policy provider, and gates sub-agent spawns against the spawning user's policy. - agent-definition.repository resolves a policy-permitted default worker (falling back to the first allowed, configured provider) so anonymous spawns keep working without forcing the org default into every policy. - spawn tool honours the policy path. Unit coverage across all three.
cch-gh
approved these changes
Jul 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds two related capabilities to Clawix:
shell_net) — a shell runner with an outbound egress firewall, so agents can run network-dependent commands under a per-container allow/deny policy.Details
Approvals
ApprovalRequest/UserToolApprovalmodels + repositories.ToolRegistrychokepoint so nothing bypasses it.Network shell + egress
shell_nettool with policy caps and a dedicated runner image.Agent provider policy