Skip to content

Commit 452d82a

Browse files
authored
perf(tools): read tool metadata instead of the registry on client paths (#6155)
* perf(tools): read tool metadata instead of the registry on client paths Cuts the last four edges that pulled `@/tools/registry` into the workspace shell. Every workspace route drops ~4,700 modules: route before after /w (canvas) 6,592 1,908 -71% /logs 6,227 1,543 -75% /tables 5,903 1,217 -79% /files 5,996 1,310 -78% workspace layout 5,751 1,063 -82% Dev cold compile of the canvas, n=3, cache cleared between runs: before 32.3s / 31.4s / 30.1s RSS 9.0-12.5 GB after 22.4s / 22.2s / 21.6s RSS 7.8-9.2 GB That lands where the `dev:minimal` escape hatch measured (20.0s / 6.7 GB) without its downside — `dev:minimal` swaps in curated registries that drop ~250 services, whereas this keeps every tool working. Rewired: - `block-outputs` -> `getToolOutputsMetadata` (needed `outputs`) - `serializer` -> `getToolParams` (needed `params`) - `validation` -> `hasToolId` (needed existence only) - `tools/params` -> `getToolMetadata` (needed `params`, `oauth`, `name`) `tools/params.ts` was the stubborn one: `mcp-dynamic-args.tsx` imports only `formatParameterLabel` from it, so the whole registry rode in behind a string helper — the same shape as the `mergeToolParameters` edge cut earlier. Adds a third generated artifact, `tool-ids.ts` (~110 KB). Resolution needs only the key set, so `@/tools/metadata` and `@/tools/metadata-outputs` both resolve through it and stay independent of each other, and an existence check costs ~110 KB instead of ~4 MB. Behaviour preservation was the risk here: `getTool` resolves an unversioned name onto its newest version, and a plain key lookup would have silently reported 246 versioned tools as missing. `resolveToolId` is reproduced against the id set and differentially tested — 4,404 probes (every id, every stripped base name, and an unknown) comparing old vs new resolution and existence: 0 mismatches. `ToolWithParameters.toolConfig` and `SubBlocksForToolInput.toolConfig` narrow from `ToolConfig` to `ToolMetadata`. The only external reader is `tool-input.tsx`, which uses `.name`. * docs(tools): point the boundary skill at the three metadata modules The skill still routed `hasToolMetadata` and `getToolIds` to `@/tools/metadata`, but this PR moved id resolution into `@/tools/tool-ids`. Left as-is it would send the next caller to the 4 MB module for an existence check that costs 110 KB — the exact mistake the skill exists to prevent. Also records the two properties a caller can silently get wrong: lookups guard with `Object.hasOwn` (a bare bracket lookup returns inherited prototype members), and they resolve unversioned names (246 tools are versioned, and a plain lookup reports them missing rather than crashing). * fix(tools): cut the settings-route registry edge and fix serializer test mocks Two findings from review, both real. The settings route still reached the registry: settings/[section]/page.tsx -> settings.tsx -> (dynamic import) ee/access-control/components/access-control.tsx -> group-detail.tsx -> tools/utils.ts -> tools/registry.ts It reads `getTool(id)?.name` — metadata — so it moves to `getToolMetadata`. The earlier audit missed it because it walked only from the canvas route, and the edge hides behind a dynamic `import()` that a static walk skips. Serializer tests mocked the wrong module. `Serializer` now reads params via `getToolParams` from `@/tools/metadata`, but the tests still only mocked `@/tools/utils`, so they controlled nothing and passed because the real generated artifacts happen to agree with the fixtures. Adds `toolsMetadataMock` to `@sim/testing/mocks`, backed by the same `mockToolConfigs` as `toolsUtilsMock` so a test mocking both sees one consistent tool universe, and mocks it in the three serializer suites. Verified the mock is now load-bearing: pointing it at a sentinel param makes the three user-only-required validation tests fail, and restoring it returns all 110 serializer tests to green. Before this they passed either way. * fix(tools): freeze the tool id array handed out by getToolIds `getToolIds()` returned the module's internal array by reference, so a caller doing `getToolIds().sort()` would reorder it in place and silently corrupt every later lookup — the in-place-mutation footgun `.claude/rules/sim-react-performance.md` calls out. Frozen rather than copied: the array is consumed in loops, so copying would allocate on every call. Freezing makes the mutation throw instead of corrupt, and `[...getToolIds()].sort()` still works. Return type is now `readonly string[]`, so the mistake is a compile error rather than a runtime surprise. No caller mutates it today; this is closing the hole, not fixing a live bug. * test(tools): enforce that the two tool-id resolvers never diverge `resolveToolId` now exists twice on purpose — `@/tools/utils` resolves against the live registry (so a tool added before regeneration still resolves at runtime), `@/tools/tool-ids` against the generated id list (so client code resolves without importing 4,300 tools). Nothing structurally kept them in step; a change to versioning logic in one would silently drift from the other. `tool-metadata:check` now asserts they agree across every id, every stripped base name, and an unknown — 4,404 probes — and only after the staleness check passes, so a missing regeneration reports as staleness rather than as drift. Verified it fails: breaking resolution for `gmail*` exits 1; restoring it passes. It cannot live in a vitest suite. `vitest.setup.ts` globally mocks `@/tools/registry` to an empty map, so `getTool` resolves nothing there — a parity test written as a spec passes or fails for the wrong reason. Both facts are recorded where the code is. Both resolvers stay exported. An earlier pass here un-exported the `@/tools/utils` one as dead; `tools/utils.server.ts` imports it through a multi-line import that a grep missed, and `tsc` caught it. Its doc now says which resolver a caller should reach for instead of leaving two identically-named functions unexplained.
1 parent d6e08d3 commit 452d82a

23 files changed

Lines changed: 713 additions & 73 deletions

File tree

.agents/skills/tool-registry-boundary/SKILL.md

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,20 @@ You keep the 4,300-tool executable registry out of module graphs that don't exec
1919

2020
| you need | import | notes |
2121
| --- | --- | --- |
22-
| whether a tool id exists | `hasToolMetadata` from `@/tools/metadata` | |
23-
| a tool's params | `getToolParams` / `getToolMetadata` from `@/tools/metadata` | |
24-
| a tool's declared outputs | `getToolOutputsMetadata` from `@/tools/metadata-outputs` | separate module on purpose — see below |
25-
| every tool id | `getToolIds` from `@/tools/metadata` | |
22+
| whether a tool id exists | `hasToolId` from `@/tools/tool-ids` | ~110 KB — the cheapest module |
23+
| to resolve an unversioned name | `resolveToolId` from `@/tools/tool-ids` | |
24+
| every tool id | `getToolIds` from `@/tools/tool-ids` | |
25+
| a tool's params | `getToolParams` / `getToolMetadata` from `@/tools/metadata` | ~4 MB |
26+
| a tool's declared outputs | `getToolOutputsMetadata` from `@/tools/metadata-outputs` | ~4 MB, separate on purpose |
2627
| to **execute** a tool | `getTool` from `@/tools/utils`, or `@/tools/utils.server` | server paths only |
2728

28-
Outputs live in their own module because they are roughly two thirds of the generated data and have a single consumer. Importing `@/tools/metadata` must never pull them — do not "helpfully" re-export one from the other.
29+
Three modules, cheapest first. Ids are their own artifact because resolution and existence checks need only the key set; outputs are their own because they are the larger half of the data with a single consumer. `@/tools/metadata` and `@/tools/metadata-outputs` both resolve ids through `@/tools/tool-ids`, which is what keeps them independent of each other — do not "helpfully" re-export one from another, or every caller pays for all three.
30+
31+
All lookups guard with `Object.hasOwn`. `JSON.parse` yields an object with the normal prototype, so a bare bracket lookup returns inherited members: `getToolMetadata('constructor')` returned a *function* typed as tool metadata before that was fixed.
2932

3033
## The generated artifacts
3134

32-
`apps/sim/tools/generated/tool-metadata.ts` and `tool-outputs.ts` are produced by `scripts/sync-tool-metadata.ts`:
35+
`apps/sim/tools/generated/tool-ids.ts`, `tool-metadata.ts` and `tool-outputs.ts` are produced by `scripts/sync-tool-metadata.ts`:
3336

3437
```bash
3538
bun run tool-metadata:generate # after adding/changing a tool
@@ -43,6 +46,20 @@ Three non-obvious properties, each of which was measured and is easy to undo by
4346
- **The data is a JSON string parsed at runtime, not an imported `.json` and not an object literal.** With `resolveJsonModule` (which this repo enables), a `.json` import makes TypeScript infer a literal type for all 4,300+ entries and takes `tsc --noEmit` from **12.6s to 8m07s** — a 38x regression. An ambient `declare module` does *not* short-circuit it, and an object literal costs the same. A single string literal is one cheap token for both the compiler and the bundler, and `JSON.parse` beats evaluating the equivalent literal at runtime. Do not "clean this up" into a `.json` import.
4447
- **The generator refuses to emit function values.** If you add a field to `METADATA_FIELDS` that contains a closure, generation fails loudly rather than shipping executable config to the client. `hosting` and `schemaEnrichment` are excluded for exactly this reason (`hosting.enabled`, `pricing`, and `enrichSchema` are functions) — they are server-only.
4548
- **Empty param entries are stripped.** The registry contains one (`stt_deepgram_v2`), which crashes callers that read `param.type` while iterating.
49+
- **Lookups resolve versions.** `getTool` maps an unversioned name onto the newest version, and 246 tools are versioned. A plain key lookup would silently report them missing — a quiet correctness bug, not a crash. `resolveToolId` reproduces that against the id set and is differentially tested against the original.
50+
51+
## Testing code that reads tool metadata
52+
53+
Mock the module the code under test actually reads. `vi.mock('@/tools/utils', () => toolsUtilsMock)` only controls `getTool`; code that reads `params`/`outputs`/`name` goes through `@/tools/metadata`, so mocking `tools/utils` there is a **no-op that still passes** — because the real generated artifacts happen to agree with the mock fixtures. The test looks green while controlling nothing.
54+
55+
```ts
56+
import { blocksMock, toolsMetadataMock, toolsUtilsMock } from '@sim/testing/mocks'
57+
58+
vi.mock('@/tools/utils', () => toolsUtilsMock) // executable lookup
59+
vi.mock('@/tools/metadata', () => toolsMetadataMock) // params / outputs / name
60+
```
61+
62+
Both are backed by the same `mockToolConfigs`, so mocking both gives one consistent tool universe. If you are unsure whether a mock is load-bearing, change a fixture value to a sentinel and confirm the test fails.
4663

4764
## How to verify an edge actually got cut
4865

@@ -59,9 +76,9 @@ Reference points measured on this repo:
5976
| `tools/registry.ts` reachable | ~4,900 |
6077
| `tools/merge-params.ts` (leaf) | 2 |
6178
| `providers/utils.ts` after cutting its `params` edge | 22 |
62-
| `app/workspace/[workspaceId]/w/page.tsx` (canvas) | 6,591, of which 4,689 are the registry |
79+
| `app/workspace/[workspaceId]/w/page.tsx` (canvas) | 6,592 before, 1,908 after |
6380

64-
The canvas route reaches the registry through **four** redundant edges — `providers/utils` (via `tools/params`), `lib/workflows/blocks/block-outputs`, `lib/workflows/sanitization/validation`, and `serializer/index`. Cutting any one alone moves the module count by ~1. They must all be cut before anything improves; measure the route, not the file you edited.
81+
The canvas route reached the registry through **four** redundant edges — `providers/utils` (via `tools/params`), `lib/workflows/blocks/block-outputs`, `lib/workflows/sanitization/validation`, and `serializer/index`. Cutting any one alone moved the module count by ~1. They all had to go before anything improved; measure the route, not the file you edited.
6582

6683
## When adding a new caller
6784

.claude/commands/tool-registry-boundary.md

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,20 @@ You keep the 4,300-tool executable registry out of module graphs that don't exec
1818

1919
| you need | import | notes |
2020
| --- | --- | --- |
21-
| whether a tool id exists | `hasToolMetadata` from `@/tools/metadata` | |
22-
| a tool's params | `getToolParams` / `getToolMetadata` from `@/tools/metadata` | |
23-
| a tool's declared outputs | `getToolOutputsMetadata` from `@/tools/metadata-outputs` | separate module on purpose — see below |
24-
| every tool id | `getToolIds` from `@/tools/metadata` | |
21+
| whether a tool id exists | `hasToolId` from `@/tools/tool-ids` | ~110 KB — the cheapest module |
22+
| to resolve an unversioned name | `resolveToolId` from `@/tools/tool-ids` | |
23+
| every tool id | `getToolIds` from `@/tools/tool-ids` | |
24+
| a tool's params | `getToolParams` / `getToolMetadata` from `@/tools/metadata` | ~4 MB |
25+
| a tool's declared outputs | `getToolOutputsMetadata` from `@/tools/metadata-outputs` | ~4 MB, separate on purpose |
2526
| to **execute** a tool | `getTool` from `@/tools/utils`, or `@/tools/utils.server` | server paths only |
2627

27-
Outputs live in their own module because they are roughly two thirds of the generated data and have a single consumer. Importing `@/tools/metadata` must never pull them — do not "helpfully" re-export one from the other.
28+
Three modules, cheapest first. Ids are their own artifact because resolution and existence checks need only the key set; outputs are their own because they are the larger half of the data with a single consumer. `@/tools/metadata` and `@/tools/metadata-outputs` both resolve ids through `@/tools/tool-ids`, which is what keeps them independent of each other — do not "helpfully" re-export one from another, or every caller pays for all three.
29+
30+
All lookups guard with `Object.hasOwn`. `JSON.parse` yields an object with the normal prototype, so a bare bracket lookup returns inherited members: `getToolMetadata('constructor')` returned a *function* typed as tool metadata before that was fixed.
2831

2932
## The generated artifacts
3033

31-
`apps/sim/tools/generated/tool-metadata.ts` and `tool-outputs.ts` are produced by `scripts/sync-tool-metadata.ts`:
34+
`apps/sim/tools/generated/tool-ids.ts`, `tool-metadata.ts` and `tool-outputs.ts` are produced by `scripts/sync-tool-metadata.ts`:
3235

3336
```bash
3437
bun run tool-metadata:generate # after adding/changing a tool
@@ -42,6 +45,20 @@ Three non-obvious properties, each of which was measured and is easy to undo by
4245
- **The data is a JSON string parsed at runtime, not an imported `.json` and not an object literal.** With `resolveJsonModule` (which this repo enables), a `.json` import makes TypeScript infer a literal type for all 4,300+ entries and takes `tsc --noEmit` from **12.6s to 8m07s** — a 38x regression. An ambient `declare module` does *not* short-circuit it, and an object literal costs the same. A single string literal is one cheap token for both the compiler and the bundler, and `JSON.parse` beats evaluating the equivalent literal at runtime. Do not "clean this up" into a `.json` import.
4346
- **The generator refuses to emit function values.** If you add a field to `METADATA_FIELDS` that contains a closure, generation fails loudly rather than shipping executable config to the client. `hosting` and `schemaEnrichment` are excluded for exactly this reason (`hosting.enabled`, `pricing`, and `enrichSchema` are functions) — they are server-only.
4447
- **Empty param entries are stripped.** The registry contains one (`stt_deepgram_v2`), which crashes callers that read `param.type` while iterating.
48+
- **Lookups resolve versions.** `getTool` maps an unversioned name onto the newest version, and 246 tools are versioned. A plain key lookup would silently report them missing — a quiet correctness bug, not a crash. `resolveToolId` reproduces that against the id set and is differentially tested against the original.
49+
50+
## Testing code that reads tool metadata
51+
52+
Mock the module the code under test actually reads. `vi.mock('@/tools/utils', () => toolsUtilsMock)` only controls `getTool`; code that reads `params`/`outputs`/`name` goes through `@/tools/metadata`, so mocking `tools/utils` there is a **no-op that still passes** — because the real generated artifacts happen to agree with the mock fixtures. The test looks green while controlling nothing.
53+
54+
```ts
55+
import { blocksMock, toolsMetadataMock, toolsUtilsMock } from '@sim/testing/mocks'
56+
57+
vi.mock('@/tools/utils', () => toolsUtilsMock) // executable lookup
58+
vi.mock('@/tools/metadata', () => toolsMetadataMock) // params / outputs / name
59+
```
60+
61+
Both are backed by the same `mockToolConfigs`, so mocking both gives one consistent tool universe. If you are unsure whether a mock is load-bearing, change a fixture value to a sentinel and confirm the test fails.
4562

4663
## How to verify an edge actually got cut
4764

@@ -58,9 +75,9 @@ Reference points measured on this repo:
5875
| `tools/registry.ts` reachable | ~4,900 |
5976
| `tools/merge-params.ts` (leaf) | 2 |
6077
| `providers/utils.ts` after cutting its `params` edge | 22 |
61-
| `app/workspace/[workspaceId]/w/page.tsx` (canvas) | 6,591, of which 4,689 are the registry |
78+
| `app/workspace/[workspaceId]/w/page.tsx` (canvas) | 6,592 before, 1,908 after |
6279

63-
The canvas route reaches the registry through **four** redundant edges — `providers/utils` (via `tools/params`), `lib/workflows/blocks/block-outputs`, `lib/workflows/sanitization/validation`, and `serializer/index`. Cutting any one alone moves the module count by ~1. They must all be cut before anything improves; measure the route, not the file you edited.
80+
The canvas route reached the registry through **four** redundant edges — `providers/utils` (via `tools/params`), `lib/workflows/blocks/block-outputs`, `lib/workflows/sanitization/validation`, and `serializer/index`. Cutting any one alone moved the module count by ~1. They all had to go before anything improved; measure the route, not the file you edited.
6481

6582
## When adding a new caller
6683

.cursor/commands/tool-registry-boundary.md

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,20 @@ You keep the 4,300-tool executable registry out of module graphs that don't exec
1414

1515
| you need | import | notes |
1616
| --- | --- | --- |
17-
| whether a tool id exists | `hasToolMetadata` from `@/tools/metadata` | |
18-
| a tool's params | `getToolParams` / `getToolMetadata` from `@/tools/metadata` | |
19-
| a tool's declared outputs | `getToolOutputsMetadata` from `@/tools/metadata-outputs` | separate module on purpose — see below |
20-
| every tool id | `getToolIds` from `@/tools/metadata` | |
17+
| whether a tool id exists | `hasToolId` from `@/tools/tool-ids` | ~110 KB — the cheapest module |
18+
| to resolve an unversioned name | `resolveToolId` from `@/tools/tool-ids` | |
19+
| every tool id | `getToolIds` from `@/tools/tool-ids` | |
20+
| a tool's params | `getToolParams` / `getToolMetadata` from `@/tools/metadata` | ~4 MB |
21+
| a tool's declared outputs | `getToolOutputsMetadata` from `@/tools/metadata-outputs` | ~4 MB, separate on purpose |
2122
| to **execute** a tool | `getTool` from `@/tools/utils`, or `@/tools/utils.server` | server paths only |
2223

23-
Outputs live in their own module because they are roughly two thirds of the generated data and have a single consumer. Importing `@/tools/metadata` must never pull them — do not "helpfully" re-export one from the other.
24+
Three modules, cheapest first. Ids are their own artifact because resolution and existence checks need only the key set; outputs are their own because they are the larger half of the data with a single consumer. `@/tools/metadata` and `@/tools/metadata-outputs` both resolve ids through `@/tools/tool-ids`, which is what keeps them independent of each other — do not "helpfully" re-export one from another, or every caller pays for all three.
25+
26+
All lookups guard with `Object.hasOwn`. `JSON.parse` yields an object with the normal prototype, so a bare bracket lookup returns inherited members: `getToolMetadata('constructor')` returned a *function* typed as tool metadata before that was fixed.
2427

2528
## The generated artifacts
2629

27-
`apps/sim/tools/generated/tool-metadata.ts` and `tool-outputs.ts` are produced by `scripts/sync-tool-metadata.ts`:
30+
`apps/sim/tools/generated/tool-ids.ts`, `tool-metadata.ts` and `tool-outputs.ts` are produced by `scripts/sync-tool-metadata.ts`:
2831

2932
```bash
3033
bun run tool-metadata:generate # after adding/changing a tool
@@ -38,6 +41,20 @@ Three non-obvious properties, each of which was measured and is easy to undo by
3841
- **The data is a JSON string parsed at runtime, not an imported `.json` and not an object literal.** With `resolveJsonModule` (which this repo enables), a `.json` import makes TypeScript infer a literal type for all 4,300+ entries and takes `tsc --noEmit` from **12.6s to 8m07s** — a 38x regression. An ambient `declare module` does *not* short-circuit it, and an object literal costs the same. A single string literal is one cheap token for both the compiler and the bundler, and `JSON.parse` beats evaluating the equivalent literal at runtime. Do not "clean this up" into a `.json` import.
3942
- **The generator refuses to emit function values.** If you add a field to `METADATA_FIELDS` that contains a closure, generation fails loudly rather than shipping executable config to the client. `hosting` and `schemaEnrichment` are excluded for exactly this reason (`hosting.enabled`, `pricing`, and `enrichSchema` are functions) — they are server-only.
4043
- **Empty param entries are stripped.** The registry contains one (`stt_deepgram_v2`), which crashes callers that read `param.type` while iterating.
44+
- **Lookups resolve versions.** `getTool` maps an unversioned name onto the newest version, and 246 tools are versioned. A plain key lookup would silently report them missing — a quiet correctness bug, not a crash. `resolveToolId` reproduces that against the id set and is differentially tested against the original.
45+
46+
## Testing code that reads tool metadata
47+
48+
Mock the module the code under test actually reads. `vi.mock('@/tools/utils', () => toolsUtilsMock)` only controls `getTool`; code that reads `params`/`outputs`/`name` goes through `@/tools/metadata`, so mocking `tools/utils` there is a **no-op that still passes** — because the real generated artifacts happen to agree with the mock fixtures. The test looks green while controlling nothing.
49+
50+
```ts
51+
import { blocksMock, toolsMetadataMock, toolsUtilsMock } from '@sim/testing/mocks'
52+
53+
vi.mock('@/tools/utils', () => toolsUtilsMock) // executable lookup
54+
vi.mock('@/tools/metadata', () => toolsMetadataMock) // params / outputs / name
55+
```
56+
57+
Both are backed by the same `mockToolConfigs`, so mocking both gives one consistent tool universe. If you are unsure whether a mock is load-bearing, change a fixture value to a sentinel and confirm the test fails.
4158

4259
## How to verify an edge actually got cut
4360

@@ -54,9 +71,9 @@ Reference points measured on this repo:
5471
| `tools/registry.ts` reachable | ~4,900 |
5572
| `tools/merge-params.ts` (leaf) | 2 |
5673
| `providers/utils.ts` after cutting its `params` edge | 22 |
57-
| `app/workspace/[workspaceId]/w/page.tsx` (canvas) | 6,591, of which 4,689 are the registry |
74+
| `app/workspace/[workspaceId]/w/page.tsx` (canvas) | 6,592 before, 1,908 after |
5875

59-
The canvas route reaches the registry through **four** redundant edges — `providers/utils` (via `tools/params`), `lib/workflows/blocks/block-outputs`, `lib/workflows/sanitization/validation`, and `serializer/index`. Cutting any one alone moves the module count by ~1. They must all be cut before anything improves; measure the route, not the file you edited.
76+
The canvas route reached the registry through **four** redundant edges — `providers/utils` (via `tools/params`), `lib/workflows/blocks/block-outputs`, `lib/workflows/sanitization/validation`, and `serializer/index`. Cutting any one alone moved the module count by ~1. They all had to go before anything improved; measure the route, not the file you edited.
6077

6178
## When adding a new caller
6279

apps/sim/ee/access-control/components/group-detail.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ import {
7676
import type { ProviderId } from '@/providers/types'
7777
import { getAllProviderIds, getProviderFromModel } from '@/providers/utils'
7878
import type { ProviderName } from '@/stores/providers'
79-
import { getTool } from '@/tools/utils'
79+
import { getToolMetadata } from '@/tools/metadata'
8080

8181
const logger = createLogger('AccessControlGroupDetail')
8282

@@ -733,7 +733,7 @@ function BlockToolRow({
733733
const checkboxId = `block-${block.type}`
734734

735735
const toolItems = useMemo<DenylistGridItem[]>(
736-
() => (block.tools?.access ?? []).map((id) => ({ id, label: getTool(id)?.name ?? id })),
736+
() => (block.tools?.access ?? []).map((id) => ({ id, label: getToolMetadata(id)?.name ?? id })),
737737
[block.tools?.access]
738738
)
739739
const isExpandable = toolItems.length > 1

apps/sim/lib/workflows/blocks/block-outputs.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import {
2222
type OutputCondition,
2323
type OutputFieldDefinition,
2424
} from '@/blocks/types'
25-
import { getTool } from '@/tools/utils'
25+
import { getToolOutputsMetadata } from '@/tools/metadata-outputs'
2626
import { getTrigger, isTriggerValid } from '@/triggers'
2727

2828
const logger = createLogger('BlockOutputs')
@@ -681,13 +681,13 @@ export function getToolOutputs(
681681
const toolId = blockConfig.tools.config.tool(params)
682682
if (!toolId) return {}
683683

684-
const toolConfig = getTool(toolId)
685-
if (!toolConfig?.outputs) return {}
684+
const toolOutputs = getToolOutputsMetadata(toolId)
685+
if (!toolOutputs) return {}
686686
if (includeHidden) {
687-
return toolConfig.outputs
687+
return toolOutputs
688688
}
689689
return Object.fromEntries(
690-
Object.entries(toolConfig.outputs).filter(([_, def]) => !isHiddenFromDisplay(def))
690+
Object.entries(toolOutputs).filter(([_, def]) => !isHiddenFromDisplay(def))
691691
)
692692
} catch (error) {
693693
logger.warn('Failed to get tool outputs', { error })

apps/sim/lib/workflows/sanitization/validation.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { isRecordLike } from '@sim/utils/object'
44
import { getBlock } from '@/blocks/registry'
55
import { isCustomTool, isMcpTool } from '@/executor/constants'
66
import type { BlockState, WorkflowState } from '@/stores/workflows/workflow/types'
7-
import { getTool } from '@/tools/utils'
7+
import { hasToolId } from '@/tools/tool-ids'
88

99
const logger = createLogger('WorkflowValidation')
1010

@@ -305,8 +305,7 @@ export function validateToolReference(
305305

306306
if (!isCustomTool(toolId) && !isMcpTool(toolId)) {
307307
// For built-in tools, verify they exist
308-
const tool = getTool(toolId)
309-
if (!tool) {
308+
if (!hasToolId(toolId)) {
310309
return `Block ${blockName || 'unknown'} (${blockType}): references non-existent tool '${toolId}'`
311310
}
312311
}

0 commit comments

Comments
 (0)