Skip to content

Commit 0b8f632

Browse files
authored
fix(release): reuse only full-access delegation keys and treat a blank crawl depth as unset (#9857)
* fix(release): measure Firecrawl crawl depth from the start URL and reuse only full-access delegation keys * fix(release): leave Firecrawl crawl depth to #9864 * fix(firecrawl): treat a blank crawl maxDepth as unset * test(release): prove delegation refuses read-only keys and type a blank crawl depth
1 parent 411cc9c commit 0b8f632

5 files changed

Lines changed: 48 additions & 8 deletions

File tree

‎apps/sim/lib/mothership/chat/delegation.test.ts‎

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ interface KeyRow {
2929
name: string
3030
key: string
3131
expiresAt: Date
32+
access?: 'read' | 'write'
3233
}
3334

3435
/** Models a transaction-scoped advisory lock and commits only after the entire callback succeeds. */
@@ -64,12 +65,18 @@ function database() {
6465
if (!locked) throw new Error('Read before acquiring the user lock')
6566
return {
6667
from: () => ({
67-
where: () => ({
68-
limit: async () => {
69-
const row = rows.get(name)
70-
return row && row.expiresAt.getTime() > Date.now() + 30 * 60_000 ? [row] : []
71-
},
72-
}),
68+
where: (condition: SQL) => {
69+
const query = dialect.sqlToQuery(condition)
70+
// Only the access filter binds 'write'; the other predicates bind ids, names and dates.
71+
const writeOnly = query.params.includes('write')
72+
return {
73+
limit: async () => {
74+
const row = rows.get(name)
75+
if (!row || row.expiresAt.getTime() <= Date.now() + 30 * 60_000) return []
76+
return writeOnly && row.access === 'read' ? [] : [row]
77+
},
78+
}
79+
},
7380
}),
7481
}
7582
},
@@ -154,6 +161,22 @@ describe('delegation key concurrency', () => {
154161
expect(db.rows.get('mothership-delegation:alice')).toBe(old)
155162
})
156163

164+
it('replaces a read-only key instead of handing it to the CLI', async () => {
165+
const db = database()
166+
db.rows.set('mothership-delegation:actor', {
167+
id: 'read-key',
168+
name: 'mothership-delegation:actor',
169+
key: 'encrypted-read-only',
170+
expiresAt: new Date(Date.now() + 6 * 60 * 60_000),
171+
access: 'read',
172+
})
173+
expect(await mintDelegationToken({ workspaceId: 'shared', userId: 'actor' })).toBe('plain-1')
174+
expect(db.rows.get('mothership-delegation:actor')).toMatchObject({
175+
key: 'encrypted-1',
176+
access: 'write',
177+
})
178+
})
179+
157180
it('does not log bound credential values from database errors', async () => {
158181
mocks.transaction.mockRejectedValue(
159182
new Error('Failed query params: plaintext-key, encrypted-key')

‎apps/sim/lib/mothership/chat/delegation.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export async function mintDelegationToken(params: {
4949
eq(apiKey.userId, params.userId),
5050
eq(apiKey.name, name),
5151
eq(apiKey.type, 'personal'),
52+
eq(apiKey.access, 'write'),
5253
gt(apiKey.expiresAt, new Date(Date.now() + MIN_REMAINING_MS))
5354
)
5455
)
@@ -83,6 +84,7 @@ export async function mintDelegationToken(params: {
8384
key: stored,
8485
keyHash: hashApiKey(plainKey),
8586
type: 'personal',
87+
access: 'write',
8688
createdAt: now,
8789
updatedAt: now,
8890
expiresAt,

‎apps/sim/tools/firecrawl/crawl.test.ts‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { isPlainRecord } from '@sim/utils/object'
22
import { describe, expect, it } from 'vitest'
33
import { crawlTool } from '@/tools/firecrawl/crawl'
4+
import type { FirecrawlCrawlParams } from '@/tools/firecrawl/types'
45

56
function crawlBody(options: {
67
maxDepth: number
@@ -98,4 +99,17 @@ describe('Firecrawl legacy crawl depth', () => {
9899
expect(() => crawlBody({ maxDepth })).toThrow(/maxDepth/)
99100
}
100101
)
102+
103+
it('treats a blank depth as unset instead of a zero-depth crawl', () => {
104+
if (typeof crawlTool.request.body !== 'function') throw new Error('Missing request formatter')
105+
const blankDepth: FirecrawlCrawlParams = {
106+
apiKey: 'test',
107+
url: 'https://example.com/docs',
108+
maxDepth: '',
109+
}
110+
const body = crawlTool.request.body(blankDepth)
111+
if (!isPlainRecord(body)) throw new Error('Expected a JSON request body')
112+
expect(body.includePaths).toBeUndefined()
113+
expect(body.regexOnFullURL).toBeUndefined()
114+
})
101115
})

‎apps/sim/tools/firecrawl/crawl.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ export const crawlTool: ToolConfig<FirecrawlCrawlParams, FirecrawlCrawlResponse>
168168
if (params.maxConcurrency) body.maxConcurrency = Number(params.maxConcurrency)
169169
if (params.excludePaths) body.excludePaths = params.excludePaths
170170
if (params.includePaths) body.includePaths = params.includePaths
171-
if (params.maxDepth != null) {
171+
if (params.maxDepth != null && String(params.maxDepth).trim() !== '') {
172172
const requestedDepth = Number(params.maxDepth)
173173
if (!Number.isSafeInteger(requestedDepth) || requestedDepth < 0) {
174174
throw new Error('maxDepth must be a non-negative integer')

‎apps/sim/tools/firecrawl/types.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,8 @@ export interface FirecrawlCrawlParams {
285285
apiKey: string
286286
url: string
287287
limit?: number
288-
maxDepth?: number
288+
/** Saved tool params can carry the raw field text, so a cleared field arrives as `''`. */
289+
maxDepth?: number | string
289290
formats?: FirecrawlFormat[]
290291
onlyMainContent?: boolean
291292
prompt?: string

0 commit comments

Comments
 (0)