Skip to content

Commit 8a61cc6

Browse files
committed
Align scoped discovery, original reads, and workflow output contracts
1 parent 2287484 commit 8a61cc6

62 files changed

Lines changed: 1550 additions & 262 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/sim/app/api/table/utils.ts‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { createLogger } from '@sim/logger'
22
import { permissionSatisfies } from '@sim/platform-authz/workspace'
33
import { toError } from '@sim/utils/errors'
44
import { NextResponse } from 'next/server'
5-
import { isFeatureEnabled } from '@/lib/core/config/feature-flags'
65
import {
76
asOrchestrationError,
87
messageForOrchestrationError,
@@ -20,6 +19,10 @@ import type { ColumnDefinition, Filter, TableDefinition, TablePredicate } from '
2019
import { buildFilterClause, getTableById, TableQueryValidationError } from '@/lib/table'
2120
import { USER_TABLE_ROWS_SQL_NAME } from '@/lib/table/constants'
2221
import { TableLockedError } from '@/lib/table/mutation-locks'
22+
import {
23+
getTableQueryAvailability,
24+
TABLE_QUERY_UNAVAILABLE_REASON,
25+
} from '@/lib/table/query-availability'
2326
import { isTablePredicate } from '@/lib/table/query-builder/converters'
2427
import { validateStoragePredicate } from '@/lib/table/query-builder/validate'
2528
import type { TableLockKind } from '@/lib/table/types'
@@ -40,10 +43,10 @@ export async function tablesV2GateError(
4043
workspaceId: string
4144
): Promise<NextResponse | null> {
4245
const orgId = await getWorkspaceOrganizationId(workspaceId)
43-
if (await isFeatureEnabled('tables-v2-api', { userId, orgId })) return null
46+
if ((await getTableQueryAvailability({ userId, orgId })).enabled) return null
4447
return NextResponse.json(
4548
{
46-
error: 'The v2 table query API is not enabled for this workspace',
49+
error: TABLE_QUERY_UNAVAILABLE_REASON,
4750
code: 'tables_v2_disabled',
4851
},
4952
{ status: 403 }

‎apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/starter/input-format.tsx‎

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
isFileFieldType,
2525
parseInputFormatFiles,
2626
} from '@/lib/workflows/input-format'
27+
import type { InputFormatFieldState } from '@/lib/workflows/input-format-schema'
2728
import { FileUpload } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/file-upload/file-upload'
2829
import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text'
2930
import {
@@ -39,14 +40,8 @@ import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/c
3940
import { useActiveSearchTarget } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/providers/active-search-target-provider'
4041
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
4142

42-
interface Field {
43-
id: string
44-
name: string
45-
type?: 'string' | 'number' | 'boolean' | 'object' | 'array' | 'file[]'
46-
value?: string
47-
description?: string
48-
collapsed?: boolean
49-
}
43+
type Field = Pick<InputFormatFieldState, 'id' | 'name'> &
44+
Partial<Omit<InputFormatFieldState, 'id' | 'name'>>
5045

5146
interface FieldFormatProps {
5247
blockId: string

‎apps/sim/blocks/blocks/table.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { TABLE_LIMITS } from '@/lib/table/constants'
44
import { filterRulesToFilter, sortRulesToSort } from '@/lib/table/query-builder/converters'
55
import type { BlockConfig } from '@/blocks/types'
66
import { parseOptionalNumberInput } from '@/blocks/utils'
7+
import { TABLE_ID_PARAM } from '@/tools/table/params'
78
import type { TableQueryResponse } from '@/tools/table/types'
89
import { getTrigger } from '@/triggers'
910

@@ -735,7 +736,7 @@ Return ONLY the sort JSON:`,
735736

736737
inputs: {
737738
operation: { type: 'string', description: 'Table operation to perform' },
738-
tableId: { type: 'string', description: 'Table identifier' },
739+
tableId: { type: 'string', description: TABLE_ID_PARAM.description },
739740
data: { type: 'json', description: 'Row data for insert/update' },
740741
rows: { type: 'array', description: 'Array of row data for batch insert' },
741742
rowId: { type: 'string', description: 'Row identifier for ID-based operations' },

‎apps/sim/blocks/blocks/table_v2.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type {
1212
TablePredicateInput,
1313
} from '@/lib/table/types'
1414
import type { BlockConfig } from '@/blocks/types'
15+
import { TABLE_ID_PARAM } from '@/tools/table/params'
1516
import type { TableQueryV2Response } from '@/tools/table/types'
1617
import { getTrigger } from '@/triggers'
1718

@@ -586,7 +587,7 @@ Return ONLY the JSON object:`,
586587

587588
inputs: {
588589
operation: { type: 'string', description: 'Table operation to perform' },
589-
tableId: { type: 'string', description: 'Table identifier' },
590+
tableId: { type: 'string', description: TABLE_ID_PARAM.description },
590591
data: { type: 'json', description: 'Row data for insert/update' },
591592
rows: { type: 'array', description: 'Array of row data for batch insert' },
592593
rowId: { type: 'string', description: 'Row identifier for ID-based operations' },

‎apps/sim/executor/handlers/generic/generic-handler.test.ts‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@ import { getBlock } from '@/blocks/index'
88
import { BlockType } from '@/executor/constants'
99
import { GenericBlockHandler } from '@/executor/handlers/generic/generic-handler'
1010
import type { ExecutionContext } from '@/executor/types'
11+
import { resolveBlockReference } from '@/executor/utils/block-reference'
1112
import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry'
1213
import type { SerializedBlock } from '@/serializer/types'
1314
import { executeTool } from '@/tools'
1415
import { selectKnowledgeDocumentWriteSecretProvenance } from '@/tools/knowledge/secret-provenance'
1516
import { mcpRunOperationTool } from '@/tools/mcp/run-operation'
17+
import { tableQueryRowsV2Tool } from '@/tools/table/query_rows_v2'
1618
import type { ToolConfig } from '@/tools/types'
1719
import { getTool } from '@/tools/utils'
1820

@@ -107,6 +109,33 @@ describe('GenericBlockHandler', () => {
107109
expect(result).toEqual(expectedOutput)
108110
})
109111

112+
it('exposes the Table success field used by downstream workflow references', async () => {
113+
const rows = [{ id: 'row-1', data: { amount: 17 }, executions: {} }]
114+
const tableResponse = await tableQueryRowsV2Tool.transformResponse!(
115+
Response.json({ data: { rows, rowCount: 1, totalCount: 1, limit: 10, nextCursor: null } })
116+
)
117+
mockGetTool.mockReturnValue(tableQueryRowsV2Tool)
118+
mockExecuteTool.mockResolvedValue(tableResponse)
119+
const block: SerializedBlock = {
120+
...mockBlock,
121+
metadata: { id: 'table_v2', name: 'Read open items' },
122+
config: { tool: tableQueryRowsV2Tool.id, params: {} },
123+
}
124+
const output = await handler.execute(mockContext, block, { tableId: 'table-1' })
125+
const resolved = resolveBlockReference('readopenitems', ['success'], {
126+
blockNameMapping: { readopenitems: block.id },
127+
blockData: { [block.id]: output },
128+
blockOutputSchemas: { [block.id]: tableQueryRowsV2Tool.outputs ?? {} },
129+
})
130+
expect(resolved?.value).toBe(true)
131+
expect(output.rows).toEqual(rows)
132+
133+
mockExecuteTool.mockResolvedValue({ success: false, error: 'Table access denied', output: {} })
134+
await expect(handler.execute(mockContext, block, { tableId: 'table-1' })).rejects.toThrow(
135+
'Table access denied'
136+
)
137+
})
138+
110139
it('executes the standalone stable MCP action after argument resolution with trusted block scope', async () => {
111140
mockGetBlock.mockReturnValue(McpBlock)
112141
mockGetTool.mockReturnValue(mcpRunOperationTool)

‎apps/sim/lib/api/contracts/mothership-catalog.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { v2BlockDetailSchema, v2BlockFieldSchema } from '@/lib/api/contracts/v2/
33

44
/** Internal discovery adds compact catalog hints without changing the public v2 response. */
55
const mothershipBlockFieldSchema = v2BlockFieldSchema.extend({
6+
valueSchema: z.record(z.string(), z.unknown()).optional(),
67
optionsAvailability: z.string().optional(),
78
options: z
89
.array(
@@ -17,5 +18,8 @@ const mothershipBlockFieldSchema = v2BlockFieldSchema.extend({
1718

1819
export const mothershipBlockDetailSchema = v2BlockDetailSchema.extend({
1920
inputSchema: z.array(mothershipBlockFieldSchema),
21+
operationAvailability: z
22+
.record(z.string(), z.object({ enabled: z.boolean(), reason: z.string().optional() }))
23+
.optional(),
2024
})
2125
export type MothershipBlockDetail = z.infer<typeof mothershipBlockDetailSchema>

‎apps/sim/lib/catalog/application/list-blocks.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,10 @@ function matchesFilters(block: CatalogBlockSummary, input: ListCatalogBlocksInpu
6464
* blocks that can start a workflow rather than needing a second endpoint.
6565
*
6666
* A sunset block (`legacy` or `deprecated`) is left out unless `includeSunset`
67-
* is set: it stays readable by id and keeps executing where it is already
68-
* placed, but a list of "what may I place?" must not offer a superseded block
69-
* alongside its replacement.
67+
* is set. Existing placements keep executing, but a toolbar-hidden sunset
68+
* block is not returned by the default detail read. The explicit sunset list
69+
* exposes accessible lifecycle and replacement metadata without offering a
70+
* superseded block for new authoring.
7071
*
7172
* No audit is projected — reading a catalog is not a semantic event, and no
7273
* shipped v2 read records one.

‎apps/sim/lib/embeddings/client.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
ollamaEmbeddingModelName,
3232
resolveDimensions,
3333
} from '@/lib/embeddings/catalog'
34+
import { EmbeddingConfigurationError } from '@/lib/embeddings/configuration-error'
3435
import { resolveProviderKey } from '@/lib/embeddings/keys'
3536
import { isOllamaServerConfigured } from '@/lib/embeddings/ollama-model-catalog.server'
3637
import { DEFAULT_OPENROUTER_EMBEDDING_MODEL } from '@/lib/embeddings/openrouter-models'
@@ -384,7 +385,7 @@ async function resolveProvider(
384385
throw new Error(`OpenRouter transport does not support catalog provider: ${info.provider}`)
385386
}
386387
if (!options.apiKey) {
387-
throw new Error('OPENROUTER_API_KEY is not configured')
388+
throw new EmbeddingConfigurationError()
388389
}
389390
return {
390391
adapter: getAdapterFactory('openrouter')({
@@ -413,7 +414,7 @@ async function resolveProvider(
413414
*/
414415
if (info.provider === 'ollama') {
415416
if (!isOllamaServerConfigured()) {
416-
throw new Error('OLLAMA_URL must be configured for Ollama embeddings')
417+
throw new EmbeddingConfigurationError()
417418
}
418419
const baseUrl = getOllamaUrl().replace(/\/+$/, '')
419420
const modelName = ollamaEmbeddingModelName(model)
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/** @vitest-environment node */
2+
import { beforeEach, describe, expect, it, vi } from 'vitest'
3+
4+
const mocks = vi.hoisted(() => ({ byok: vi.fn(), rotation: vi.fn() }))
5+
vi.mock('@/lib/api-key/byok', () => ({ getBYOKKey: mocks.byok }))
6+
vi.mock('@/lib/core/config/api-keys', () => ({ getRotatingApiKey: mocks.rotation }))
7+
vi.mock('@/lib/core/config/env', () => ({ env: {} }))
8+
9+
import { EmbeddingConfigurationError } from '@/lib/embeddings/configuration-error'
10+
import { resolveProviderKey } from '@/lib/embeddings/keys'
11+
import { messageForCopilotApplicationError } from '@/lib/mothership/application/error'
12+
13+
describe('embedding configuration capability failure', () => {
14+
beforeEach(() => {
15+
mocks.byok.mockReset().mockResolvedValue(null)
16+
mocks.rotation.mockReset().mockImplementation(() => {
17+
throw new Error('private provider setup details')
18+
})
19+
})
20+
it.each(['openai', 'gemini', 'cohere', 'mistral'] as const)(
21+
'classifies missing %s configuration without returning provider internals',
22+
async (provider) => {
23+
const error = await resolveProviderKey(provider, 'workspace').catch((error: unknown) => error)
24+
expect(error).toBeInstanceOf(EmbeddingConfigurationError)
25+
expect(error).toMatchObject({
26+
code: 'conflict',
27+
capability: 'semantic_retrieval',
28+
reason: 'provider_not_configured',
29+
retryable: false,
30+
})
31+
expect(messageForCopilotApplicationError(error, 'generic')).toContain(
32+
'embedding provider is not configured'
33+
)
34+
expect(JSON.stringify(error)).not.toContain('private provider')
35+
}
36+
)
37+
it('preserves usable BYOK and does not probe platform rotation', async () => {
38+
mocks.byok.mockResolvedValue({ apiKey: 'test-provider-key', scope: 'workspace' })
39+
expect(await resolveProviderKey('openai', 'workspace')).toEqual({
40+
apiKey: 'test-provider-key',
41+
isBYOK: true,
42+
})
43+
expect(mocks.rotation).not.toHaveBeenCalled()
44+
})
45+
it('does not relabel authorization or infrastructure failures as missing configuration', async () => {
46+
const error = new Error('private database details')
47+
mocks.byok.mockRejectedValue(error)
48+
await expect(resolveProviderKey('openai', 'workspace')).rejects.toBe(error)
49+
expect(messageForCopilotApplicationError(error, 'generic')).toBe('generic')
50+
})
51+
})
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { OrchestrationError } from '@/lib/core/orchestration/types'
2+
3+
/** Safe deployment capability failure, separate from absent results or caller permissions. */
4+
export class EmbeddingConfigurationError extends OrchestrationError {
5+
readonly capability = 'semantic_retrieval'
6+
readonly reason = 'provider_not_configured'
7+
readonly retryable = false
8+
readonly recovery =
9+
'Configure an embedding provider, or read authorized original documents directly.'
10+
11+
constructor() {
12+
super(
13+
'conflict',
14+
'Semantic retrieval is unavailable because its embedding provider is not configured.'
15+
)
16+
this.name = 'EmbeddingConfigurationError'
17+
}
18+
}

0 commit comments

Comments
 (0)