Skip to content

Commit b625280

Browse files
committed
fix(secrets): simplify copilot mounting flow
1 parent 0ba34a6 commit b625280

23 files changed

Lines changed: 202 additions & 458 deletions

File tree

apps/docs/content/docs/en/platform/credentials.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ Masking is activated only when Sim successfully resolves a value from **Settings
7979

8080
Copilot's Function and code-execution tools receive a saved secret only when their code explicitly contains a valid `{{KEY}}` reference. Direct `environmentVariables.KEY` access, shell `$KEY`, dynamic names, literals, and configured-but-unused secrets do not mount a value. Code execution requires workspace write access, and the caller must also be allowed to view the raw value: your own Personal secrets, any secret for which you are a Credential Admin, and Workspace secrets when you are a workspace admin. Credential Members can continue using shared secrets through normal workflow and tool resolution, but cannot mount their plaintext into arbitrary Copilot code.
8181

82-
Interactive code calls require **Allow** for that individual call, even if the tool was previously allowed for the chat or account. Headless surfaces use their saved **Secret access** setting:
82+
Headless surfaces use their saved **Secret access** setting:
8383

8484
- **Sim Chat block** — under **Show additional fields**
8585
- **Scheduled Tasks** — in the task modal

apps/sim/app/api/copilot/tool-permission/route.test.ts

Lines changed: 0 additions & 151 deletions
This file was deleted.

apps/sim/app/api/copilot/tool-permission/route.ts

Lines changed: 8 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { createLogger } from '@sim/logger'
22
import { getErrorMessage } from '@sim/utils/errors'
3-
import { isRecordLike } from '@sim/utils/object'
43
import { type NextRequest, NextResponse } from 'next/server'
54
import { copilotToolPermissionContract } from '@/lib/api/contracts/copilot'
65
import { parseRequest, validationErrorResponse } from '@/lib/api/server'
@@ -22,14 +21,12 @@ import {
2221
} from '@/lib/copilot/persistence/tool-permission/auto-allow'
2322
import {
2423
authenticateCopilotRequestSessionOnly,
25-
createBadRequestResponse,
2624
createInternalServerErrorResponse,
2725
createNotFoundResponse,
2826
createRequestTracker,
2927
createUnauthorizedResponse,
3028
} from '@/lib/copilot/request/http'
3129
import { withIncomingGoSpan } from '@/lib/copilot/request/otel'
32-
import { getToolSecretMountNames } from '@/lib/copilot/tools/secret-mount'
3330
import { isCopilotToolPermissionsEnabled } from '@/lib/core/config/env-flags'
3431
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
3532

@@ -41,10 +38,6 @@ interface DecisionResult {
4138
applied: boolean
4239
}
4340

44-
interface RejectedDecision {
45-
rejection: 'permission-feature-disabled' | 'persistent-secret-permission'
46-
}
47-
4841
/**
4942
* Records one prompt answer and wakes the orchestrator waiting on it.
5043
*
@@ -56,7 +49,7 @@ async function applyDecision(
5649
toolCallId: string,
5750
decision: ToolPermissionDecision,
5851
userId: string
59-
): Promise<DecisionResult | RejectedDecision | null> {
52+
): Promise<DecisionResult | null> {
6053
const existing = await getAsyncToolCall(toolCallId).catch((err) => {
6154
logger.warn('Failed to fetch async tool call', { toolCallId, error: getErrorMessage(err) })
6255
return null
@@ -72,19 +65,6 @@ async function applyDecision(
7265
})
7366
if (!run || run.userId !== userId) return null
7467

75-
const args = isRecordLike(existing.args) ? existing.args : undefined
76-
const mountsSecrets = getToolSecretMountNames(existing.toolName, args).length > 0
77-
if (!isCopilotToolPermissionsEnabled && !mountsSecrets) {
78-
return { rejection: 'permission-feature-disabled' }
79-
}
80-
if (
81-
mountsSecrets &&
82-
(decision === TOOL_PERMISSION_DECISION.allow_chat ||
83-
decision === TOOL_PERMISSION_DECISION.always_allow)
84-
) {
85-
return { rejection: 'persistent-secret-permission' }
86-
}
87-
8868
const claimed = await recordToolPermissionDecision(toolCallId, decision)
8969
if (!claimed) {
9070
// Someone already answered. Report their decision rather than pretending
@@ -137,6 +117,13 @@ export const POST = withRouteHandler((req: NextRequest) => {
137117
{ [TraceAttr.RequestId]: tracker.requestId },
138118
async (span) => {
139119
try {
120+
// Nothing can legitimately be awaiting a decision while the feature is
121+
// off, so close the endpoint rather than letting it write decisions
122+
// onto rows no orchestrator is waiting on.
123+
if (!isCopilotToolPermissionsEnabled) {
124+
return createNotFoundResponse('Tool permissions are not enabled')
125+
}
126+
140127
const { userId: authenticatedUserId, isAuthenticated } =
141128
await authenticateCopilotRequestSessionOnly()
142129

@@ -167,12 +154,6 @@ export const POST = withRouteHandler((req: NextRequest) => {
167154
const results: DecisionResult[] = []
168155
for (const { toolCallId, decision } of decisions) {
169156
const result = await applyDecision(toolCallId, decision, authenticatedUserId)
170-
if (result && 'rejection' in result) {
171-
if (result.rejection === 'permission-feature-disabled') {
172-
return createNotFoundResponse('Tool permissions are not enabled')
173-
}
174-
return createBadRequestResponse('Secret-bearing code calls can only be allowed once')
175-
}
176157
if (result) results.push(result)
177158
}
178159

apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-permission-card.tsx

Lines changed: 15 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { useCallback, useEffect, useState } from 'react'
44
import {
55
ChevronDown,
66
Chip,
7-
ChipTag,
87
DropdownMenu,
98
DropdownMenuContent,
109
DropdownMenuItem,
@@ -17,7 +16,6 @@ import { createLogger } from '@sim/logger'
1716
import { useQueryClient } from '@tanstack/react-query'
1817
import { requestJson } from '@/lib/api/client/request'
1918
import { copilotToolPermissionContract } from '@/lib/api/contracts/copilot'
20-
import { getToolSecretMountNames } from '@/lib/copilot/tools/secret-mount'
2119
import { generalSettingsKeys } from '@/hooks/queries/general-settings'
2220
import { useToolPermissionStore } from '@/stores/tool-permission/store'
2321

@@ -130,8 +128,6 @@ export function ToolPermissionCard({
130128
)
131129

132130
const preview = argsPreview(params)
133-
const mountedSecretNames = getToolSecretMountNames(toolName, params)
134-
const mountsSecrets = mountedSecretNames.length > 0
135131
const busy = submitting !== null || isSubmitted
136132

137133
if (expired) {
@@ -173,39 +169,26 @@ export function ToolPermissionCard({
173169
<Chip variant='primary' disabled={busy} onClick={() => void submit('allow', [toolCallId])}>
174170
Allow
175171
</Chip>
176-
{!mountsSecrets && (
177-
<DropdownMenu>
178-
<DropdownMenuTrigger asChild>
179-
<Chip variant='border' rightIcon={ChevronDown} disabled={busy}>
180-
Don't ask again
181-
</Chip>
182-
</DropdownMenuTrigger>
183-
<DropdownMenuContent align='start'>
184-
<DropdownMenuItem onSelect={() => void submit('allow_chat', [toolCallId])}>
185-
For this chat
186-
</DropdownMenuItem>
187-
<DropdownMenuItem onSelect={() => void submit('always_allow', [toolCallId])}>
188-
For every chat
189-
</DropdownMenuItem>
190-
</DropdownMenuContent>
191-
</DropdownMenu>
192-
)}
172+
<DropdownMenu>
173+
<DropdownMenuTrigger asChild>
174+
<Chip variant='border' rightIcon={ChevronDown} disabled={busy}>
175+
Don't ask again
176+
</Chip>
177+
</DropdownMenuTrigger>
178+
<DropdownMenuContent align='start'>
179+
<DropdownMenuItem onSelect={() => void submit('allow_chat', [toolCallId])}>
180+
For this chat
181+
</DropdownMenuItem>
182+
<DropdownMenuItem onSelect={() => void submit('always_allow', [toolCallId])}>
183+
For every chat
184+
</DropdownMenuItem>
185+
</DropdownMenuContent>
186+
</DropdownMenu>
193187
<Chip disabled={busy} onClick={() => void submit('skip', [toolCallId])}>
194188
Skip
195189
</Chip>
196190
</div>
197191

198-
{mountsSecrets && (
199-
<div className='flex flex-wrap items-center gap-1 pl-[22px]'>
200-
<span className='mr-1 text-[var(--text-tertiary)] text-xs'>Secrets</span>
201-
{mountedSecretNames.map((name) => (
202-
<ChipTag key={name} variant='mono'>
203-
{`{{${name}}}`}
204-
</ChipTag>
205-
))}
206-
</div>
207-
)}
208-
209192
{showBulkActions && (
210193
<div className='flex flex-wrap items-center pl-[22px]'>
211194
<span className='mr-1 text-[var(--text-tertiary)] text-xs'>

apps/sim/lib/copilot/tools/secret-mount.test.ts renamed to apps/sim/executor/utils/code-secret-references.test.ts

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,7 @@
22
* @vitest-environment node
33
*/
44
import { describe, expect, it } from 'vitest'
5-
import { FunctionExecute, Read, RunCode } from '@/lib/copilot/generated/tool-catalog-v1'
6-
import {
7-
extractCodeSecretNames,
8-
getToolSecretMountNames,
9-
toolHasSecretMountCapability,
10-
} from '@/lib/copilot/tools/secret-mount'
5+
import { extractCodeSecretNames } from '@/executor/utils/code-secret-references'
116

127
describe('Copilot code secret declarations', () => {
138
it.each(['javascript', 'python'])(
@@ -39,12 +34,4 @@ describe('Copilot code secret declarations', () => {
3934
)
4035
).toEqual([])
4136
})
42-
43-
it('uses the generated capability as the sole tool classifier', () => {
44-
expect(toolHasSecretMountCapability(FunctionExecute.id)).toBe(true)
45-
expect(toolHasSecretMountCapability(RunCode.id)).toBe(true)
46-
expect(toolHasSecretMountCapability(Read.id)).toBe(false)
47-
expect(getToolSecretMountNames(Read.id, { code: 'return {{SECRET}}' })).toEqual([])
48-
expect(getToolSecretMountNames(RunCode.id, { code: 'return {{SECRET}}' })).toEqual(['SECRET'])
49-
})
5037
})

0 commit comments

Comments
 (0)