Skip to content

fix(access-control): default every operation and model picker to one the permission group allows - #6720

Merged
waleedlatif1 merged 6 commits into
stagingfrom
fix/permission-aware-defaults
Aug 15, 2026
Merged

fix(access-control): default every operation and model picker to one the permission group allows#6720
waleedlatif1 merged 6 commits into
stagingfrom
fix/permission-aware-defaults

Conversation

@waleedlatif1

@waleedlatif1 waleedlatif1 commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Fixes the reported bug: a block's operation placeholder should be the first operation you have access to in your permission group. The dropdown already hid denied operations, but it seeded its default before the permission config query resolved — usePermissionConfig reads as "nothing denied" while in flight, so a freshly dropped block persisted the static first operation, and nothing revisits a field that already holds a value. A group denying slack_message still got a Slack block sitting on Send Message.
  • The same bug existed on the model axis: agent, router and evaluator declare defaultValue: 'claude-sonnet-5', prepareBlockState wrote it unconditionally, and the model combobox only fills an empty field — so a group denying that model (or the Anthropic provider) got an Agent block pre-filled with a model it can't run.
  • Consolidates the rule behind lib/permission-groups/operation-access + useOperationAccess, which resolves an operation to its tool without guessing (unresolvable stays visible; the server gate stays authoritative).

The governing rule

A one-shot persisting write must never read a predicate that answers "nothing denied" while the permission config is loading. Display filtering stays optimistic and self-corrects; anything persisted is withheld until the config is known.

That rule was originally re-implemented per callsite and got missed twice, so it now lives in one place:

  • useOperationAccess.resolveSeedGate owns the creation-time veto for both restricted fields — workflow.tsx states no policy of its own, it asks for a gate and passes it to prepareBlockState.
  • The agent tool picker and both operation selectors close while the config is unknown. Every list they offer — blocks via filterBlocks, operations, MCP and custom tools — reads as unrestricted for that beat, and each pick writes in one shot.
  • A preset operation goes through the same gate as a declared default: it comes from the search index, which is itself unfiltered while loading, so it is not the informed pick it appears to be.
  • isPermissionLoading is exposed from one hook so every surface reads the same symbol.

Surfaces covered

  • block editor dropdown — default waits for the config
  • agent block tool list — operations were not gated at all; denied ones are now hidden, blocks whose every operation is denied leave the picker, and the default is the first allowed
  • canvas search / connection picker — the tool-operation index was filtered only by the block allowlist, so denied operations were still offered as one-click block drops
  • block creation — a denied or not-yet-knowable declared default seeds empty rather than pre-filled

Notes

  • Denied operations are hidden, not removed, so a workflow already saved on one keeps resolving its label. No stored value is ever rewritten — only never-chosen defaults are affected.
  • Users with no permission group get identical behavior, except the operation default is seeded after the config query resolves rather than on first render.
  • Single-tool blocks with an operation subblock (dropcontact, enrichment, sqs) are now gated where they weren't. Because they're single-tool, denying the tool denies every operation, so the picker empties rather than pre-filling.
  • usePermissionConfig gains isModelUsable (model denylist AND provider allowlist) and drops isModelAllowed/isProviderAllowed, which had no remaining external consumer once the combobox consolidated onto it. Both denylists are now indexed — the gate calls them once per option of every block offered, so a linear scan made a check's cost scale with denylist length (3.2ms → 0.30ms per search-index build at 500 denied tools).
  • Copilot's edit-workflow validation checks operations against the block's option list but not against deniedTools, so it can still author a denied operation. Execution blocks it server-side. Left as a follow-up — it needs userId/workspaceId plumbed into the server validator.

Type of Change

  • Bug fix

Testing

  • Unit tests on the operation gate and the creation-time seed veto, including the branches where an empty-string or non-string declared default must bypass the gate; verified each fails when the guard it covers is removed
  • bun run type-check clean, bun run check:audits 26/26, 16,596 tests passing
  • Enumerated the block registry to check the widened gate: every multi-operation block has a tool selector, no block has an empty operation list, and only 3 of 326 blocks declare an operation default at all

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

@vercel

vercel Bot commented Aug 15, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Aug 15, 2026 2:07am

Request Review

@cursor

cursor Bot commented Aug 15, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Touches permission-gated UX across block creation, agent tools, and search; logic is centralized and tested, but wrong gating could hide operations or block picks until config loads.

Overview
Fixes a bug where operation and model fields were seeded from static defaults before the permission group config finished loading—values written in that window were never revisited, so users could get a denied Slack operation or a blocked model on a new block.

New shared layer: lib/permission-groups/operation-access maps operations to tools without guessing, plus useOperationAccess (getDeniedOperations, resolveDefaultOperation, resolveSeedGate). Surfaces that persist a choice wait on isPermissionLoading or veto seeds until config is known; display-only filtering stays optimistic.

UI: Block editor operation dropdown and agent tool picker hide denied operations, default to the first allowed option, and disable while loading. Model combobox uses consolidated isModelUsable (model denylist + provider allowlist). Canvas search filters tool-operation results by operation access, not just block allowlist.

Creation: prepareBlockState accepts isSeededValueAllowed; workflow.tsx applies resolveSeedGate to declared defaults and strips denied preset operations from search/connection-picker drops.

usePermissionConfig: Exposes isModelUsable; indexed tool/model denylists. findProviderFromModel returns null for non-chat ids so embedding/speech models aren’t misclassified as Ollama for provider gating.

Reviewed by Cursor Bugbot for commit 32b8c89. Configure here.

@greptile-apps

greptile-apps Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR centralizes permission-aware operation and model defaulting so one-shot writes wait for permission configuration and select only usable defaults.

  • Adds shared operation-to-tool resolution and creation-time seed gating.
  • Filters editor, agent-tool, canvas-search, and connection-picker choices by operation access.
  • Consolidates model denylist and provider-allowlist checks behind isModelUsable.
  • Adds tests for operation resolution, default selection, preset handling, and seeded-value vetoes.

Confidence Score: 5/5

The PR appears safe to merge because the previously reported loading-time model and preset-operation bypasses are corrected and no blocking failure remains.

No blocking failure remains.

Important Files Changed

Filename Overview
apps/sim/hooks/use-operation-access.ts Centralizes loading-aware operation filtering, default resolution, and operation/model seed vetoes; the previously reported model-loading issue is addressed.
apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx Applies the centralized gate to declared defaults and operation presets during every block-creation path, addressing both prior bypass findings.
apps/sim/lib/permission-groups/operation-access.ts Adds non-guessing operation-to-tool resolution and first-allowed default selection with permissive handling for unresolvable operations.
apps/sim/hooks/use-permission-config.ts Consolidates model and provider checks into an indexed isModelUsable predicate.
apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/dropdown/dropdown.tsx Defers operation defaulting and disables persisted operation selection while permission configuration is loading.
apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx Filters denied agent-tool operations, chooses the first permitted operation, and closes one-shot tool selection while permissions are unknown.
apps/sim/stores/modals/search/store.ts Removes operation search entries whose resolved tools are denied before they can seed newly created blocks.
apps/sim/stores/workflows/utils.ts Allows block preparation to veto nonempty string defaults through the centralized permission seed gate.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Create block or choose preset] --> B{Permission config ready?}
  B -- No --> C[Leave operation/model empty]
  B -- Yes --> D{Requested value allowed?}
  D -- Yes --> E[Persist requested value]
  D -- No --> F[Choose first allowed option]
  C --> G[Mounted picker reconciles after load]
  F --> H[Persist allowed default]
  G --> H
Loading

Reviews (6): Last reviewed commit: "fix(access-control): only gate a model f..." | Re-trigger Greptile

Comment thread apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx Outdated
Comment thread apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit f42bda6. Configure here.

Comment thread apps/sim/hooks/use-operation-access.ts
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

…allows

The block editor's operation dropdown already hid operations whose tool the
caller's permission group denies, but it seeded its default without waiting for
that config. `usePermissionConfig` resolves as "nothing denied" while its query
is in flight, so a freshly dropped block persisted the static first operation —
and nothing revisits a field that already holds a value, so the correction that
arrived with the config never applied. A user whose group denies `slack_message`
still got a Slack block sitting on Send Message. The model combobox already had
this guard; the dropdown did not.

Consolidates the rule behind `lib/permission-groups/operation-access` and
`useOperationAccess`, which resolves an operation to its tool without guessing
(an unresolvable one stays visible; the server gate stays authoritative) and
withholds a default until the config has loaded, so seeding on a defined value
is the whole guard.

Applies it to every surface that offers or seeds an operation:

- block editor dropdown — default now waits for the config
- agent block tool list — operations were not gated at all; the picker now hides
  denied ones, drops blocks whose every operation is denied, and defaults to the
  first allowed
- canvas search / connection picker — the tool-operation index was filtered only
  by the block allowlist, so denied operations were still offered as one-click
  block drops
- block creation — a declared default operation the group denies is replaced
  with the first allowed one, and a denied preset operation is discarded
… rule

Block creation seeds `model` the same way it seeds `operation`: `agent`,
`router` and `evaluator` all declare `defaultValue: 'claude-sonnet-5'`, and
`prepareBlockState` wrote it unconditionally. The model combobox only fills a
field that is empty, so a group denying that model (or the Anthropic provider)
got an Agent block pre-filled with a model it cannot run — the same bug as the
operation one, on the other axis the permission group governs.

Rather than a second bespoke gate, `prepareBlockState` now takes one veto,
`isSeededValueAllowed(subBlockId, value)`, and seeds nothing when a declared
default is denied. Nothing substitutes a replacement there any more: the
editor's own permission-aware pickers already resolve the right one and only
fill an empty field, and substituting in the store would drift from
`getDefaultBlockName`, which names a block after its *declared* default. That
also deletes `firstAllowedOperation` and its copy of subblock-option
enumeration.

Review follow-ups:

- `usePermissionConfig` gains `isModelUsable` (denylist AND provider allowlist);
  the combobox's two hand-rolled copies of that pair now call it
- `isToolAllowed`/`isModelAllowed` index their denylists — the gate calls them
  once per option of every block offered, so a linear scan made a check's cost
  scale with denylist length (measured 3.2ms -> 0.30ms per search-index build
  at 500 denied tools)
- `useOperationAccess` had three members with three different loading
  semantics, one documented as unsafe alone; it now exposes one withholding
  `resolveOperationGate`
- the agent tool picker derived its option list twice with the empty-id filter
  on only one path; both callers now share one `{ options, denied }` result
- `OPERATION_SUBBLOCK_ID` was a verbatim copy of the private constant in
  `canvas-sentence.ts`, doc comment included; that file now imports it
Cleanup-pass findings on this branch's own lines: comments that restated the
code they sat on, a four-line note whose sibling said the same in one, and a
`?? undefined` on a non-nullable value. The comment on the tool-picker filter
now explains the clause that actually needed it (an empty option list is not a
denied one) instead of narrating the filter.

Left alone as pre-existing and out of scope: the inline `staleTime` literal in
`useAllowedIntegrationsFromEnv`, and the Operation selector's raw label /
plain `Combobox` — both byte-identical to staging and matching the convention
of every sibling field in that panel.
Block creation is one-shot, so the withholding pattern the editor's pickers use
does not transfer: withholding the predicate there meant `prepareBlockState`
seeded the declared `operation`/`model` defaults unchecked, and nothing
revisits a field that already holds a value — so a block added before the
permission config resolved kept a model the group may deny.

Both restricted fields now seed empty until the config is known; the pickers
fill them the moment it resolves. A preset operation is still honoured in that
window: unlike a declared default it is the user's explicit pick, and the
server gates the run.
Two review bots found the same class of bug in two more places, which is the
real finding: "never persist from a predicate that reads as unrestricted while
the config loads" was a rule each callsite re-implemented, and the rule had
already been forgotten twice.

Closes both reported instances and moves the rule somewhere it cannot be
forgotten again:

- `useOperationAccess.resolveSeedGate` now owns the creation-time veto for both
  restricted fields, so `workflow.tsx` states no policy of its own — it asks for
  a gate and passes it on. Previously the model half of the invariant was
  carried by an operation-shaped object that merely happened to be absent
  during the same window.
- The agent tool picker and both operation selectors close while the config is
  unknown. Every list they offer — blocks, operations, MCP and custom tools —
  reads as unrestricted for that beat, and each pick is a one-shot write.
- A preset operation goes through the same gate as a declared default. It comes
  from the search index, which is itself unfiltered while loading, so it is not
  the informed pick it looks like.
- `isPermissionLoading` is exposed from one hook, so all four surfaces read the
  same symbol instead of four spellings of the same condition.

Also from the review passes: dropped `isModelAllowed`/`isProviderAllowed` from
the public interface (consolidating onto `isModelUsable` left them with no
external consumer), un-exported `resolveOperationToolId` (no non-test caller),
and corrected the `isSeededValueAllowed` TSDoc, which still described the
contract the previous commit replaced.

Tests: replaced a case that asserted its own fixture rather than the code with
coverage of the two guard branches that were genuinely untested — an empty-string
and a non-string declared default must bypass the gate, since both mean "nothing
was declared" rather than a value to authorize.
@waleedlatif1
waleedlatif1 force-pushed the fix/permission-aware-defaults branch from f42bda6 to 75face5 Compare August 15, 2026 01:55
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/hooks/use-operation-access.ts
…s about

The seed gate ran `isModelUsable` on every subblock named `model`, but
`getProviderFromModel` resolves chat models and falls back to `ollama` for
everything else. 28 of the 44 seeded model defaults in the registry are
embedding, speech, image, video or search ids — so for any group with a
provider allowlist that omits Ollama, those blocks were created with an empty
model.

Adds `findProviderFromModel`, the non-guessing half of `getProviderFromModel`,
which returns `null` where the registry declares nothing. `isModelUsable` now
treats an unresolved id as not-a-provider-choice and leaves it alone, matching
the rule the operation gate already follows: never guess, and let the server
stay authoritative. `getProviderFromModel` delegates to it, so there is one
resolution path and its ollama fallback is unchanged.

This also repairs the same misjudgement where it predates the branch: the model
combobox filtered its options through the identical provider check, so those 28
defaults were already being hidden from their own pickers for allowlisted
groups.

The dead `try/catch` around the old call went with it — `getProviderFromModel`
returns a fallback rather than throwing for an unknown id.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 32b8c89. Configure here.

@waleedlatif1
waleedlatif1 merged commit 96f7b23 into staging Aug 15, 2026
30 checks passed
@waleedlatif1
waleedlatif1 deleted the fix/permission-aware-defaults branch August 15, 2026 02:20
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