Commit 3fa2359
authored
tool policies: per-tool and per-namespace permission overrides (#437)
* add tool policies for per-tool and per-namespace permission overrides
Users can now mark tools as auto-approve, require approval, or block on a
per-tool (vercel.dns.create) or per-subtree (vercel.dns.*, vercel.*) basis.
Resolution: ordered list per scope, first match wins. No matching policy
falls through to the plugin's existing resolveAnnotations output, so plugin
defaults still drive behavior for unscoped tools.
Block hides tools from search/list and hard-fails at invoke with
ToolBlockedError carrying the matched pattern so agents can adapt.
require_approval forces the elicitation prompt; approve skips it.
Mid-invocation elicitations (mayElicit) are unaffected.
- new core_schema tool_policy table; drizzle migration 0006
- policies.ts: matchPattern, isValidPattern, resolveToolPolicy
- ToolBlockedError; ToolListFilter.includeBlocked opt-out for settings UI
- executor.policies.{list,create,update,remove,resolve} CRUD
- HTTP PoliciesApi + handlers under /scopes/:scopeId/policies
- cloud /policies route + sidebar entry; PoliciesPage in @executor/react
* show effective policy on each tool in source detail view
Source detail is a management view, so include policy-blocked tools in
the per-source list (`includeBlocked: true`) and render the effective
policy on each row:
- Color-coded dot per row in ToolTree (green=auto-approve, amber=require
approval, red=blocked) with the matched pattern in the title attribute
- Blocked rows render at reduced opacity so they read as inactive
- ToolDetail header shows a badge with action + matched pattern when a
policy is in effect
Resolution mirrors the server: walk the policy list (already pre-sorted
by the API in evaluation order) and stop at the first match.
* also surface plugin-default approval state on each tool
The dot only fired for user-authored policies, leaving tools whose
plugin default is `requiresApproval: true` (e.g. openapi POST/DELETE)
looking unannotated. Users would think "no rule applies" when actually
the plugin's default still gates the prompt.
- sources.tools handler now resolves annotations (was previously
disabled) and returns requiresApproval / approvalDescription
- ToolSummary gains defaultRequiresApproval
- ToolTree row renders a hollow amber ring when no user policy matches
but the plugin default would prompt; user policies still render as
filled dots so they're visually distinct
- ToolDetail header shows a muted "Default: Require approval" badge
when no user policy matches; auto-approve-by-default stays silent
(it's the safe state, no point cluttering every row)
* unify user policy and plugin default into single EffectivePolicy resolver
The UI was branching on "user policy vs plugin default" and rendering
different things from each path. There's only ONE effective policy per
tool — combining the layers should be the resolver's job, not the
caller's.
- new EffectivePolicy type: { action, source: 'user' | 'plugin-default', pattern? }
- resolveEffectivePolicy / effectivePolicyFromSorted in @executor/sdk
return the unified answer; callers ask once and render one thing
- ToolSummary.policy is now always present (always EffectivePolicy);
ToolTree and ToolDetail render once, parameterized by source
- tests for the new helper (4 cases: user wins, default require, default approve, user-overrides-default)
* allow bare * pattern as universal match
Patterns like "*" should "just work" for trust-everything or
deny-everything use cases. The matcher and validator both rejected it
because the existing rule was "no leading *" — but bare * is the one
case where leading * is unambiguous.
- matchPattern: bare "*" matches every tool id; existing leading-*
rejection still applies to mixed forms ("*foo", "*.foo")
- isValidPattern: accept "*" as a complete pattern
- updated tests, schema comment, error message, and the AddPolicyForm
hint to mention the universal pattern
* Trace MCP JSON-RPC request ids (#440)
* optimistic policy mutations via Atom.optimistic + UI polish
- Wire policies through Atom.optimistic / Atom.optimisticFn instead of a
custom pending-state layer. Reducer reads get(self) so racing edits
stack correctly and the post-commit refresh pulls authoritative state.
- Action selector on each policy row is now a clickable badge dropdown
(chevron + ring), backed by Select primitives so the selected item
shows a check on the right. Kebab still owns Remove.
- Drop the custom usePoliciesWithPending / usePendingPolicies helpers;
policies.tsx reads policiesOptimisticAtom and writes through
create/update/removePolicyOptimistic directly.
- Codify the pattern as a project skill under
.skills/effect-atom-optimistic-updates so future work reaches for the
effect-atom primitives instead of rebuilding the layer.
* add wrdn-effect-atom-optimistic warden skill
Codifies the "use Atom.optimistic, not hand-rolled pending state" rule
as a Warden skill. Local skill resolved from .agents/skills/. Scoped to
the React UI package since the rule is React-specific.
Detects: new useState/useRef/Map of pending values alongside an
effect-atom mutation; new entries in PendingResource or new
use<X>WithPending hooks in optimistic.tsx; reads of <thing>Atom paired
with writes through <thing>OptimisticAtom; missing Atom.family wrapper
around Atom.optimistic.
Grandfathers existing legacy helpers (sources, connections); only flags
new consumers.
* add Move to top/bottom on policy rows
Adds reorder affordances in the kebab menu. Computes min/max position
across the list and writes through updatePolicyOptimistic so the row
moves immediately. Page sorts by position so the optimistic write
reorders the list visually and converges with the server order on
refresh.
* swap Move to top/bottom for Move up/down on policy rows
Single-update reorder: target position is the midpoint between the
row's new neighbors, with -1 / +1 fallback at the ends. One mutation
call per click, no row swap needed.
* drop Position N label from policy rows; visual order conveys it
* tool_policy.position: bigint → double precision
The reorder UI uses float midpoints (e.g. 2.5 between 2 and 3) so a
single update places the row between its new neighbors without
touching the neighbors' positions. bigint rejected the float; switch
to double precision.
* tool_policy.position: switch to fractional-indexing strings (lexorank)
Drops the float-midpoint scheme for fractional-indexing keys (Jira /
Notion / Linear style). Strings store lexicographically and can always
be subdivided by lengthening — no precision ceiling on repeated
inserts between the same two rows.
- column: text, lex compare in resolver / list / page sort
- create default: generateKeyBetween(null, currentMin) — top of list
- reorder: generateKeyBetween(neighborA, neighborB) — single update
- migration 0008: TRUNCATE + ALTER (feature unshipped, dev-only data)
Adds fractional-indexing dep to @executor/sdk and @executor/react.
* address review feedback on tool_policies
- Consolidate tool_policy migrations into a single 0006 with the final
shape (text position, composite (scope_id, position) index). Drops
the bigint→double→text intermediates that never ran anywhere
meaningful. Anyone with an existing dev DB needs to wipe
apps/cloud/.dev-db before restart.
- Fix a crash where Move up/down on a row neighboring an optimistic
placeholder (position: "") would call generateKeyBetween with an
invalid key. Reorder math now runs against committed rows only;
pending placeholders aren't reorderable until the server confirms.
- Stable sort tiebreak on id when two rows share a fractional-indexing
key. Same fix in the React page, the executor's policiesList, and
resolveToolPolicy via a shared comparePolicyRow helper.
- source-detail.tsx now reads policiesOptimisticAtom so action changes
on the policies page reflect immediately in the source tool tree.
- Correct the Atom.optimisticFn reducer description in both project
and Warden skills — the runtime feeds `current` to the reducer; the
reducer doesn't call `get(self)`.
* add backend tests for policy reorder and tiebreak
- resolveToolPolicy: identical positions sort deterministically by id,
regardless of input array order. Catches the racing-insert collision
where two clients pick the same generateKeyBetween(null, min) key.
- update without position preserves the existing position (regression
for the case where partial updates would otherwise blank out the
field).
- update with a new position reorders the list — exercises the
end-to-end reorder path that the UI's Move up/down feeds.
- Consecutive creates produce strictly increasing-precedence keys with
no collisions; list order is insertion-reverse.
29 → 33 tests.
* add tool_policy table to apps/local
Mirrors the cloud sqlite-side schema added in this PR. Uses the same
fractional-indexing position column and composite (scope_id, position)
index. Dev runs read drizzle/ directly; compiled binaries inline the
SQL via embedded-migrations.gen.ts at build time.
* Surface OpenAPI tool error messages in execute (#442)1 parent ab09457 commit 3fa2359
46 files changed
Lines changed: 2935 additions & 36 deletions
File tree
- .agents/skills/wrdn-effect-atom-optimistic
- .skills/effect-atom-optimistic-updates
- apps
- cloud
- drizzle
- meta
- src
- routes
- services
- web
- local
- drizzle
- meta
- src/server
- packages
- core
- api/src
- handlers
- policies
- sources
- execution/src
- sdk
- src
- kernel
- runtime-dynamic-worker/src
- runtime-quickjs/src
- plugins
- keychain/src
- openapi/src/sdk
- react
- src
- api
- components
- pages
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
0 commit comments