Skip to content

Commit dc11784

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
Merge remote-tracking branch 'origin/staging' into investigate/oci-object-storage-integration
# Conflicts: # apps/sim/tools/generated/tool-metadata.ts
2 parents dc1ab88 + a9245b4 commit dc11784

355 files changed

Lines changed: 48600 additions & 4701 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/docs/content/docs/integrations/knowledge.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ Integrate Knowledge into the workflow. Perform full CRUD operations on documents
3333

3434
### Knowledge Search
3535

36-
Search for similar content in a knowledge base using vector similarity
36+
Search for similar content in a knowledge base by relevance
3737

3838
#### Input
3939

@@ -43,7 +43,7 @@ Search for similar content in a knowledge base using vector similarity
4343
| `query` | string | No | Search query text \(optional when using tag filters\) |
4444
| `topK` | number | No | Number of most similar results to return \(1-100\) |
4545
| `tagFilters` | array | No | Array of tag filters with tagName and tagValue properties |
46-
| `searchMode` | string | No | Retrieval mode: 'vector' \(default\) uses semantic similarity only, 'hybrid' also runs a full-text leg and fuses both |
46+
| `searchMode` | string | No | Retrieval mode: 'hybrid' fuses a full-text leg with semantic similarity, 'vector' uses semantic similarity only; omit for the workspace's default |
4747
| `rerankerEnabled` | boolean | No | Whether to apply Cohere reranking to vector search results |
4848
| `rerankerModel` | string | No | Cohere rerank model to use \(one of: rerank-v4.0-pro, rerank-v4.0-fast, rerank-v3.5\) |
4949
| `rerankerInputCount` | number | No | Number of vector results sent to the Cohere reranker \(1–100\). Defaults to topK × 4 capped at 100. |

apps/docs/content/docs/workflows/blocks/agent.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ Live tool-call chips stream for **OpenAI, Anthropic, Azure Anthropic, Google, Ve
117117
| DeepSeek | Full thinking deltas | `deepseek-v4-pro`, `deepseek-v4-flash`, `deepseek-reasoner` |
118118
| xAI | Full thinking deltas | `grok-4.6`, `grok-4.5`, `grok-4.3`, `grok-4.20-multi-agent-0309` |
119119
| Groq | Full thinking deltas | `groq/openai/gpt-oss-120b`, `groq/openai/gpt-oss-20b`, `groq/openai/gpt-oss-safeguard-20b`, `groq/qwen/qwen3.6-27b` |
120-
| Meta | Not streamed | `muse-spark-1.1` |
120+
| Meta | Not streamed | `muse-spark-1.3`, `muse-spark-1.1` |
121121
| Kimi | Full thinking deltas | `kimi-k2.6` |
122122
| Z.ai | Full thinking deltas | `glm-5.3`, `glm-5.2`, `glm-5.1`, `glm-5`, `glm-5-turbo`, `glm-4.7`, `glm-4.6`, `glm-4.5`, `glm-4.5-air` |
123123

apps/docs/openapi-v2-knowledge.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6144,7 +6144,6 @@
61446144
},
61456145
"searchMode": {
61466146
"description": "Retrieval strategy: vector is semantic-only, while hybrid also runs full-text search.",
6147-
"default": "vector",
61486147
"anyOf": [
61496148
{
61506149
"type": "string",

apps/sim/.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ CRON_SECRET=your_cron_secret # Use `openssl rand -hex 32` to generate. Authentic
203203
# FORKING_ENABLED= # Workspace forks
204204
# CREDENTIAL_GROUPS= # Enterprise managed OAuth collections
205205
# TABLE_ROW_TTL= # Table TTL columns and expired-row cleanup
206+
# KNOWLEDGE_MEMBER_ACCESS= # Per-member knowledge connectors and hybrid-by-default retrieval
206207
# ORGANIZATIONS_ENABLED= / NEXT_PUBLIC_ORGANIZATIONS_ENABLED= # Organizations only
207208

208209
# Instance organization (Optional). Most enterprise features read their settings from the

apps/sim/app/api/files/authorization.ts

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ import { createLogger } from '@sim/logger'
44
import { permissionSatisfies } from '@sim/platform-authz/workspace'
55
import { and, eq, isNull } from 'drizzle-orm'
66
import { NextResponse } from 'next/server'
7+
import { knowledgeAccessCondition } from '@/lib/knowledge/access/predicate'
8+
import {
9+
resolveUserKnowledgeAccessScope,
10+
WORKSPACE_ACCESS_SCOPE,
11+
} from '@/lib/knowledge/access/scope'
12+
import type { KnowledgeAccessScope, SystemAccessScope } from '@/lib/knowledge/access/types'
713
import { getFileMetadata } from '@/lib/uploads'
814
import type { StorageContext } from '@/lib/uploads/config'
915
import type { StorageConfig } from '@/lib/uploads/core/storage-client'
@@ -141,7 +147,7 @@ export async function verifyFileAccess(
141147
customConfig?: StorageConfig,
142148
context?: StorageContext | 'general',
143149
isLocal?: boolean,
144-
options?: { requireWrite?: boolean }
150+
options?: { requireWrite?: boolean; knowledgeAccess?: KnowledgeFileAccess }
145151
): Promise<boolean> {
146152
const requireWrite = options?.requireWrite ?? false
147153
try {
@@ -182,7 +188,7 @@ export async function verifyFileAccess(
182188

183189
// 4. KB files: kb/filename
184190
if (inferredContext === 'knowledge-base') {
185-
return await verifyKBFileAccess(cloudKey, userId, customConfig)
191+
return await verifyKBFileAccess(cloudKey, userId, customConfig, options?.knowledgeAccess)
186192
}
187193

188194
// 5. Chat files: chat/filename
@@ -485,7 +491,14 @@ async function verifyCopilotFileAccess(
485491
* signal only: it reflects whether the file is still part of a live KB, not who
486492
* owns it (ownership comes from the binding).
487493
*/
488-
async function hasActiveKbDocumentForKey(cloudKey: string, workspaceId: string): Promise<boolean> {
494+
/** A reader once resolved: a person's or the workspace's tokens, or the system reading its own rows. */
495+
type ResolvedKnowledgeFileAccess = KnowledgeAccessScope | SystemAccessScope
496+
497+
async function hasActiveKbDocumentForKey(
498+
cloudKey: string,
499+
workspaceId: string,
500+
access: ResolvedKnowledgeFileAccess
501+
): Promise<boolean> {
489502
const rows = await db
490503
.select({ id: document.id })
491504
.from(document)
@@ -497,14 +510,35 @@ async function hasActiveKbDocumentForKey(cloudKey: string, workspaceId: string):
497510
eq(document.userExcluded, false),
498511
isNull(document.archivedAt),
499512
isNull(document.deletedAt),
500-
isNull(knowledgeBase.deletedAt)
513+
isNull(knowledgeBase.deletedAt),
514+
knowledgeAccessCondition(access)
501515
)
502516
)
503517
.limit(1)
504518

505519
return rows.length > 0
506520
}
507521

522+
/**
523+
* How a KB file read identifies the reader for document access. `'user'` is
524+
* for a session-authenticated person; a resolved scope is for a caller that
525+
* already holds one (an execution with a principal). The system scope is for
526+
* a background job reading a connector-owned row it is processing, which in
527+
* members mode is hidden until the sync materializes its readers. Anything
528+
* else — an internal token, a tool running with the workflow owner's id —
529+
* reads as the workspace, never as the person whose id it happens to carry.
530+
*/
531+
export type KnowledgeFileAccess = 'user' | ResolvedKnowledgeFileAccess
532+
533+
async function resolveKnowledgeFileAccess(
534+
knowledgeAccess: KnowledgeFileAccess | undefined,
535+
userId: string,
536+
workspaceId: string
537+
): Promise<ResolvedKnowledgeFileAccess> {
538+
if (knowledgeAccess === 'user') return resolveUserKnowledgeAccessScope(userId, workspaceId)
539+
return knowledgeAccess ?? WORKSPACE_ACCESS_SCOPE
540+
}
541+
508542
/**
509543
* Verify access to KB files (`kb/<key>`).
510544
*
@@ -522,7 +556,8 @@ async function hasActiveKbDocumentForKey(cloudKey: string, workspaceId: string):
522556
async function verifyKBFileAccess(
523557
cloudKey: string,
524558
userId: string,
525-
customConfig?: StorageConfig
559+
customConfig?: StorageConfig,
560+
knowledgeAccess?: KnowledgeFileAccess
526561
): Promise<boolean> {
527562
try {
528563
const binding = await getFileMetadataByKey(cloudKey, 'knowledge-base', {
@@ -552,10 +587,12 @@ async function verifyKBFileAccess(
552587
return false
553588
}
554589

555-
if (!(await hasActiveKbDocumentForKey(cloudKey, binding.workspaceId))) {
556-
logger.warn('KB file access denied: no active document references the file', {
590+
const access = await resolveKnowledgeFileAccess(knowledgeAccess, userId, binding.workspaceId)
591+
if (!(await hasActiveKbDocumentForKey(cloudKey, binding.workspaceId, access))) {
592+
logger.warn('KB file access denied: no readable document references the file', {
557593
userId,
558594
cloudKey,
595+
accessScopeKind: access.kind,
559596
})
560597
return false
561598
}

apps/sim/app/api/files/export/[id]/route.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@ function embeds(...ids: string[]) {
2525
mockExtractEmbeddedFileRefs.mockReturnValue({ keys: [], ids })
2626
}
2727

28-
vi.mock('@/lib/auth/hybrid', () => ({ checkSessionOrInternalAuth: mockCheckAuth }))
28+
vi.mock('@/lib/auth/hybrid', () => ({
29+
AuthType: { SESSION: 'session', API_KEY: 'api_key', INTERNAL_JWT: 'internal_jwt' },
30+
checkSessionOrInternalAuth: mockCheckAuth,
31+
}))
2932
vi.mock('@/lib/uploads/server/metadata', () => ({
3033
getFileMetadataById: mockGetFileMetadataById,
3134
}))

apps/sim/app/api/files/export/[id]/route.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import type { NextRequest } from 'next/server'
77
import { NextResponse } from 'next/server'
88
import { fileExportContract } from '@/lib/api/contracts/storage-transfer'
99
import { parseRequest } from '@/lib/api/server'
10-
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
10+
import { AuthType, checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
1111
import { MATERIALIZE_CONCURRENCY, mapWithConcurrency } from '@/lib/core/utils/concurrency'
1212
import { isPayloadSizeLimitError } from '@/lib/core/utils/stream-limits'
1313
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
@@ -81,7 +81,10 @@ export const GET = withRouteHandler(
8181
return NextResponse.json({ error: 'Not found' }, { status: 404 })
8282
}
8383

84-
const hasAccess = await verifyFileAccess(record.key, userId)
84+
const knowledgeAccess = authResult.authType === AuthType.SESSION ? 'user' : undefined
85+
const hasAccess = await verifyFileAccess(record.key, userId, undefined, undefined, undefined, {
86+
knowledgeAccess,
87+
})
8588
if (!hasAccess) {
8689
logger.warn('Unauthorized file export attempt', { id, userId })
8790
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
@@ -164,7 +167,13 @@ export const GET = withRouteHandler(
164167
try {
165168
const imgRecord = await getFileMetadataById(storedFileId(imageId))
166169
if (!imgRecord) return null
167-
if (!(await verifyFileAccess(imgRecord.key, userId))) return null
170+
if (
171+
!(await verifyFileAccess(imgRecord.key, userId, undefined, undefined, undefined, {
172+
knowledgeAccess,
173+
}))
174+
) {
175+
return null
176+
}
168177
return { imageId, record: imgRecord, size: getWorkspaceFileSize(imgRecord) }
169178
} catch (error) {
170179
logger.warn('Failed to resolve asset for export', {

apps/sim/app/api/files/serve/[...path]/route.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -397,7 +397,8 @@ describe('File Serve API Route', () => {
397397
'test-user-id',
398398
undefined,
399399
'mothership',
400-
false
400+
false,
401+
{ knowledgeAccess: undefined }
401402
)
402403
expect(storageServiceMockFns.mockDownloadFile).toHaveBeenCalledWith({
403404
key: 'workspace/test-workspace-id/1234567890-photo.png',

apps/sim/app/api/files/serve/[...path]/route.ts

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
concealCrossTenantResourceError,
99
InternalUnauthenticatedError,
1010
} from '@/lib/api/server/routes'
11-
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
11+
import { AuthType, checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
1212
import { resolveServableDocBytes } from '@/lib/copilot/tools/server/files/doc-compile'
1313
import { DocCompileUserError } from '@/lib/copilot/tools/server/files/doc-compile-error'
1414
import { asOrchestrationError } from '@/lib/core/orchestration/types'
@@ -26,7 +26,7 @@ import { internalWorkspaceFileServeAuth } from '@/lib/workspace-files/api'
2626
import { readWorkspaceFileContentByKey } from '@/lib/workspace-files/application/read-workspace-file-content-by-key'
2727
import { isSimPageSource, SIM_PAGE_CONTENT_TYPE } from '@/lib/workspace-files/page-compile'
2828
import { renderSimPageDocumentWithAssets } from '@/lib/workspace-files/page-document.server'
29-
import { verifyFileAccess } from '@/app/api/files/authorization'
29+
import { type KnowledgeFileAccess, verifyFileAccess } from '@/app/api/files/authorization'
3030
import {
3131
createErrorResponse,
3232
createFileResponse,
@@ -271,12 +271,29 @@ export const GET = withRouteHandler(
271271

272272
const userId = legacyAuthResult?.userId
273273
if (!userId) throw new Error('Authenticated file serve request is missing a user ID')
274+
/** Only a session identifies a person; an internal token's user id reads as the workspace. */
275+
const knowledgeAccess =
276+
legacyAuthResult?.authType === AuthType.SESSION ? ('user' as const) : undefined
274277

275278
if (isUsingCloudStorage()) {
276-
return await handleCloudProxy(cloudKey, userId, options, request.signal, storageContext)
279+
return await handleCloudProxy(
280+
cloudKey,
281+
userId,
282+
options,
283+
request.signal,
284+
storageContext,
285+
knowledgeAccess
286+
)
277287
}
278288

279-
return await handleLocalFile(cloudKey, userId, options, request.signal, storageContext)
289+
return await handleLocalFile(
290+
cloudKey,
291+
userId,
292+
options,
293+
request.signal,
294+
storageContext,
295+
knowledgeAccess
296+
)
280297
} catch (error) {
281298
if (error instanceof InternalUnauthenticatedError) {
282299
logger.warn('Unauthorized file access attempt', { error: error.message })
@@ -359,7 +376,8 @@ async function handleLocalFile(
359376
userId: string,
360377
options: ServeOptions,
361378
signal: AbortSignal | undefined,
362-
context: StorageContext
379+
context: StorageContext,
380+
knowledgeAccess: KnowledgeFileAccess | undefined
363381
): Promise<NextResponse> {
364382
const ownerKey = `user:${userId}`
365383
try {
@@ -368,7 +386,8 @@ async function handleLocalFile(
368386
userId,
369387
undefined, // customConfig
370388
context,
371-
true // isLocal
389+
true, // isLocal
390+
{ knowledgeAccess }
372391
)
373392

374393
if (!hasAccess) {
@@ -419,7 +438,8 @@ async function handleCloudProxy(
419438
userId: string,
420439
options: ServeOptions,
421440
signal: AbortSignal | undefined,
422-
context: StorageContext
441+
context: StorageContext,
442+
knowledgeAccess: KnowledgeFileAccess | undefined
423443
): Promise<NextResponse> {
424444
const ownerKey = `user:${userId}`
425445
try {
@@ -430,7 +450,8 @@ async function handleCloudProxy(
430450
userId,
431451
undefined, // customConfig
432452
context, // context
433-
false // isLocal
453+
false, // isLocal
454+
{ knowledgeAccess }
434455
)
435456

436457
if (!hasAccess) {

apps/sim/app/api/files/view/[id]/route.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { NextRequest } from 'next/server'
33
import { NextResponse } from 'next/server'
44
import { fileViewContract } from '@/lib/api/contracts/storage-transfer'
55
import { parseRequest } from '@/lib/api/server'
6-
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
6+
import { AuthType, checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
77
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
88
import { getServeStoragePrefix, type StorageContext } from '@/lib/uploads/config'
99
import { getFileMetadataById } from '@/lib/uploads/server/metadata'
@@ -37,7 +37,9 @@ export const GET = withRouteHandler(
3737
record.key,
3838
authResult.userId,
3939
undefined,
40-
record.context as StorageContext | 'general'
40+
record.context as StorageContext | 'general',
41+
undefined,
42+
{ knowledgeAccess: authResult.authType === AuthType.SESSION ? 'user' : undefined }
4143
)
4244
if (!hasAccess) {
4345
logger.warn('Unauthorized file view attempt', { id, userId: authResult.userId })

0 commit comments

Comments
 (0)