Skip to content

fix(copilot): close fail-open write gates in agent tools - #6132

Merged
TheodoreSpeaks merged 7 commits into
stagingfrom
fix/copilot-tool-authz
Aug 2, 2026
Merged

fix(copilot): close fail-open write gates in agent tools#6132
TheodoreSpeaks merged 7 commits into
stagingfrom
fix/copilot-tool-authz

Conversation

@TheodoreSpeaks

@TheodoreSpeaks TheodoreSpeaks commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Five authorization/accounting gaps on the copilot tool paths. In each case the agent is a weaker route to the same write than the UI — a workspace member gets more through Sim than the product intends to give them. Found while auditing which resources have business logic outside lib/[resource]/orchestration; these are the authz half of that audit, and the orchestration migrations stack separately on top of #5273.

1. Write gates failed open on absent permission

manage_custom_tool, manage_mcp_tool, manage_skill, and the server-tool router each hand-rolled the same ladder:

if (context.userPermission && perm !== 'write' && perm !== 'admin') return denied

userPermission is optional on the execution context, so undefined, null, and '' skipped the check entirely and the write went through unguarded. Replaced with a shared copilotToolCanWrite() built on permissionSatisfies from @sim/platform-authz/workspace, which never satisfies on a null permission. Both copilot execution paths (server-tool router and handler map) now share one definition of "write access" and one denial message.

manage_custom_tool also read params.workspaceId || context.workspaceId, letting a caller-supplied workspace id override the authenticated one — the permission was resolved for the context workspace but the write landed in whichever workspace the params named. It now uses the context workspace only.

2. materialize_file had no permission check at all

All three of its operations write: save and extract create workspace files, import creates a workflow. Unlike the server-tool router, the handler-map path has no central gate, so nothing stopped a read-only member from creating resources. Added ensureWorkspaceAccess(..., 'write') after param validation (so a bogus operation still fails cheaply without a DB read).

3. Deploy orchestration didn't enforce the mutation lock

assertWorkflowMutable lived in the deploy routes, so performRevertToVersion was the only orchestration entry point that couldn't be called past a lock. The copilot deploy tools call performFullDeploy / performFullUndeploy / performActivateVersion directly and could therefore deploy, undeploy, and activate versions of a locked workflow. The assert moves into all three functions; routes may still assert first to render their own 423, and this is the backstop that makes the guarantee hold regardless of caller.

4. Workspace secrets could be overwritten without secret-admin

upsertWorkspaceEnvVars was a weakened copy of the environment route's write path: no per-key secret-admin check, no advisory lock, no audit row. Its only caller is the set_environment_variables copilot tool — which is not in the router's WRITE_ACTIONS, so its only gate is workspace write.

Net effect: any write-level member could ask the agent to overwrite a workspace secret they are not an admin of, leaving no audit record, racing the route's locked transaction on a read-modify-write of the same jsonb column. The route (app/api/workspaces/[id]/environment/route.ts) enforces all three.

The gate now lives inside upsertWorkspaceEnvVars so every caller inherits it, reusing the route's own getWorkspaceEnvKeyAdminAccess helper rather than restating its logic. Denials throw WorkspaceEnvAccessError.

5. Knowledge-base indexing skipped the usage gate

knowledge_base's add_file computed a billing attribution and then went straight to createSingleDocument without calling checkAttributedUsageLimits — the gate every upload route applies before accepting indexing work. An over-quota workspace could index without limit through the agent. The query operation in the same file already gated correctly, so this was an inconsistency within one tool rather than a missing helper.

Tests

  • permissions.test.ts pins the fail-closed contract for undefined/null/''/read/unrecognized values.
  • materialize-file.test.ts asserts each operation is refused without write access, and that no upload is read before the gate.
  • environment/utils.test.ts covers the four gate decisions (non-admin overwrite denied, new key without write denied, key admin allowed, workspace admin allowed) plus the empty-update no-op.
  • knowledge-base.test.ts asserts an over-quota payer is refused before any file is resolved or document created.

bunx vitest run lib/copilot lib/environment lib/workflows/orchestration → 1043 pass. Full bun run lint:check and bun run type-check clean.

🤖 Generated with Claude Code

TheodoreSpeaks and others added 2 commits July 31, 2026 13:01
The three handler-map management tools (manage_custom_tool,
manage_mcp_tool, manage_skill) gated writes with
`context.userPermission && perm !== 'write' && perm !== 'admin'`.
userPermission is optional on the execution context, so an absent value
skipped the check entirely and the write proceeded unguarded — while
the server-tool router's equivalent gate is fail-closed. Both paths now
share copilotToolCanWrite, built on the canonical permissionSatisfies
(null/undefined never satisfies).

manage_custom_tool additionally resolved its target as
`params.workspaceId || context.workspaceId`, so a model-supplied
workspace id won while the permission check was resolved for the
context workspace — and upsertCustomTools performs no authz of its own.
It now uses the server-set context only, matching its two siblings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CiHhAk2R1NryaS3R8n2yFz
…ation lock

materialize_file's save/extract/import all create workspace resources, but
the handler-map path has no central permission check, so a read-only member
could create files and workflows through the agent. Gate on write access
after param validation.

assertWorkflowMutable moves into performFullDeploy / performFullUndeploy /
performActivateVersion, where performRevertToVersion already had it. The
check previously lived only in the deploy routes, so the copilot deploy
tools — which call the orchestration functions directly — could deploy,
undeploy, and activate versions of a locked workflow.

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

Copy link
Copy Markdown
Collaborator Author

@greptile review

@vercel

vercel Bot commented Jul 31, 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 2, 2026 12:39am

Request Review

@greptile-apps

greptile-apps Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR closes authorization and accounting gaps in Copilot write paths.

  • Centralizes fail-closed workspace write checks and prevents caller-supplied workspace overrides.
  • Adds write authorization to file materialization and usage-limit enforcement to knowledge-base indexing.
  • Enforces workflow mutation locks at deployment orchestration entry points.
  • Adds per-secret authorization, serialization, credential creation, cache invalidation, and auditing to workspace environment updates.
  • Adds regression tests for the tightened permission, billing, environment, and deployment behavior.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
apps/sim/lib/copilot/tools/permissions.ts Introduces a shared fail-closed workspace-write predicate and denial-message formatter.
apps/sim/lib/copilot/tools/handlers/materialize-file.ts Requires workspace write access before resolving uploads or performing any materialization operation.
apps/sim/lib/copilot/tools/server/knowledge/knowledge-base.ts Applies attributed usage limits before accepting knowledge-base indexing work.
apps/sim/lib/environment/utils.ts Adds per-key authorization, advisory-lock serialization, credential provisioning, cache invalidation, and auditing to environment updates.
apps/sim/lib/workflows/orchestration/deploy.ts Adds mutation-lock checks to deploy, undeploy, and version-activation orchestration entry points without introducing a distinct follow-up defect.
apps/sim/lib/copilot/tools/server/router.ts Reuses the shared fail-closed write predicate for server-tool routing.

Reviews (6): Last reviewed commit: "fix(env): give the workspace env denial ..." | Re-trigger Greptile

Comment thread apps/sim/lib/workflows/orchestration/deploy.ts
… on usage

Two more paths where the agent is a weaker route to the same write than the UI.

upsertWorkspaceEnvVars was a weakened copy of the environment route's write:
no per-key secret-admin check, no advisory lock, no audit row. Its only caller
is the set_environment_variables copilot tool, which gates on workspace 'write'
alone — so any write-level member could have the agent overwrite a workspace
secret they do not administer, with nothing in the audit log and a lost-update
race against the route's locked transaction. The gate now lives in the function
so every caller inherits it, reusing getWorkspaceEnvKeyAdminAccess rather than
restating the route's logic.

knowledge_base add_file computed a billing attribution and then indexed without
calling checkAttributedUsageLimits, which every upload route applies before
accepting indexing work — an over-quota workspace could index without limit
through the agent. The same file's query operation already gated correctly.

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

Copy link
Copy Markdown
Collaborator Author

@greptile review

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@greptile

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor

cursor Bot commented Jul 31, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Touches authentication/authorization on agent write paths, workspace secret ACLs, and workflow deploy gates—incorrect logic could deny legitimate users or leave bypasses; changes are security-focused and warrant careful review.

Overview
Closes several authorization and billing gaps where copilot/agent tool paths were weaker than the UI routes—members could write or consume resources they should not.

Copilot write gates now use shared copilotToolCanWrite() (fail-closed via permissionSatisfies) and consistent denial messages on the server-tool router and handler-map tools (manage_custom_tool, manage_mcp_tool, manage_skill). manage_custom_tool no longer honors model-supplied workspaceId; writes always target the authenticated context workspace.

materialize_file requires workspace write via ensureWorkspaceAccess before save, import, or extract (handler-map path had no central gate).

Deploy orchestration (performFullDeploy, performFullUndeploy, performActivateVersion) enforces the workflow mutation lock inside orchestration and returns { success: false } on lock denial instead of throwing.

upsertWorkspaceEnvVars aligns with the workspace environment route: per-key secret-admin for overwrites, write access for new keys, advisory-locked jsonb merge, audit rows, and safer newKeys derivation for legacy secrets without credential rows.

Knowledge add_file calls checkAttributedUsageLimits on the KB workspace payer before resolving files or creating documents, matching upload routes.

Tests cover permission fail-closed behavior, materialize write gate, env upsert denials, KB usage gate, and deploy lock handling.

Reviewed by Cursor Bugbot for commit aec5b31. Bugbot is set up for automated code reviews on this repo. Configure here.

Comment thread apps/sim/lib/workflows/orchestration/deploy.ts Outdated
Comment thread apps/sim/lib/environment/utils.ts Outdated
…ret's ACL

Two defects in the previous commits, both found by review.

assertWorkflowMutable throws, and the three orchestration entry points awaited
it outside any try/catch, so a locked workflow escaped as an exception instead
of the { success: false } result the callers consume — performChatDeploy and
the copilot deploy tools would have surfaced a generic 500 rather than a lock
denial. performRevertToVersion already converted it; the three now do too,
through a shared helper.

upsertWorkspaceEnvVars derived newKeys for createWorkspaceEnvCredentials from
the credential rows rather than the stored variables. A secret written before
credential rows existed has no ACL, so overwriting it looked like adding a new
key: it minted a credential and made the caller that secret's admin. The
environment route derives newKeys from the locked jsonb read for exactly this
reason, and now so does this.

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

Copy link
Copy Markdown
Collaborator Author

@greptile

@TheodoreSpeaks

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 b1103e3. Configure here.

TheodoreSpeaks and others added 2 commits August 1, 2026 17:30
Resolves the env-utils conflicts: staging renamed the effective-environment
cache constants while this branch added the workspace env write gate. Keeps
both, and merges the two add/add test suites into one file on the shared
@sim/testing db + encryption mocks.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PCXbU36FwJBmtJKaH83BUm
@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@greptile

Comment thread apps/sim/lib/environment/utils.ts
WorkspaceEnvAccessError reported "must be an admin of these secrets" for
both denials, so a caller lacking workspace write to ADD a key was told to
get secret-admin on a key that does not exist yet. Carry the reason and
mirror the route's two messages.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PCXbU36FwJBmtJKaH83BUm
@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@greptile

@TheodoreSpeaks

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 aec5b31. Configure here.

@TheodoreSpeaks
TheodoreSpeaks merged commit 8348592 into staging Aug 2, 2026
27 checks passed
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