Skip to content

Commit f608761

Browse files
waleedlatif1claude
andcommitted
refactor(serializer): drop tools.config.params invocation; enforce canonical-id contract via audit
The serializer's pre-execution validator no longer runs the block's `tools.config.params` mapper to discover renamed tool param ids. Instead it relies on the contract that every required+user-only tool param is backed by a subBlock whose `id` or `canonicalParamId` equals the tool param id, and a new audit (`bun run check:block-canonical`) enforces this. Migrates posthog (`personalApiKey` → canonical `apiKey`) so the audit passes cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 7584f25 commit f608761

4 files changed

Lines changed: 90 additions & 25 deletions

File tree

apps/sim/blocks/blocks/posthog.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ export const PostHogBlock: BlockConfig<PostHogResponse> = {
156156
id: 'personalApiKey',
157157
title: 'Personal API Key',
158158
type: 'short-input',
159+
canonicalParamId: 'apiKey',
159160
placeholder: 'Enter your PostHog personal API key',
160161
password: true,
161162
condition: {
@@ -1192,9 +1193,6 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
11921193
if (params.operation === 'posthog_get_project' && params.projectIdParam) {
11931194
params.projectId = params.projectIdParam
11941195
}
1195-
if (params.personalApiKey) {
1196-
params.apiKey = params.personalApiKey
1197-
}
11981196

11991197
const flagOps = [
12001198
'posthog_get_feature_flag',
@@ -1276,7 +1274,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
12761274
operation: { type: 'string', description: 'Operation to perform' },
12771275
region: { type: 'string', description: 'PostHog region (us or eu)' },
12781276
projectApiKey: { type: 'string', description: 'Project API key for public endpoints' },
1279-
personalApiKey: { type: 'string', description: 'Personal API key for private endpoints' },
1277+
apiKey: { type: 'string', description: 'Personal API key for private endpoints' },
12801278
projectId: { type: 'string', description: 'PostHog project ID' },
12811279
// Core Data
12821280
event: { type: 'string', description: 'Event name' },
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
#!/usr/bin/env bun
2+
3+
/**
4+
* CI check: enforce the canonical-id contract that the serializer's
5+
* pre-execution validator relies on.
6+
*
7+
* For every (block, tool) pair where the tool param is `required: true` and
8+
* `visibility: 'user-only'`, the block must expose a subBlock whose `id` or
9+
* `canonicalParamId` equals the tool param id. Otherwise the validator
10+
* resolves the value under the wrong key and false-flags the field as missing
11+
* at submit time.
12+
*
13+
* Historically this was patched at runtime by invoking `tools.config.params`
14+
* inside try/catch during validation. That swallowed mapper throws on
15+
* placeholder values and degraded validation accuracy. This audit lifts the
16+
* contract into config so the validator can stay pure.
17+
*
18+
* Usage:
19+
* bun run apps/sim/scripts/check-block-canonical.ts
20+
*/
21+
22+
import { getAllBlocks } from '@/blocks/registry'
23+
import { tools as toolRegistry } from '@/tools/registry'
24+
25+
type Violation = {
26+
blockType: string
27+
toolId: string
28+
paramId: string
29+
}
30+
31+
const violations: Violation[] = []
32+
33+
for (const block of getAllBlocks()) {
34+
const access: string[] = block.tools?.access ?? []
35+
if (access.length === 0) continue
36+
37+
const subBlockKeys = new Set<string>()
38+
for (const sb of block.subBlocks ?? []) {
39+
if (sb.id) subBlockKeys.add(sb.id)
40+
const canonical = (sb as { canonicalParamId?: string }).canonicalParamId
41+
if (canonical) subBlockKeys.add(canonical)
42+
}
43+
44+
for (const toolId of access) {
45+
const tool = toolRegistry[toolId]
46+
if (!tool) continue
47+
48+
for (const [paramId, paramConfig] of Object.entries(tool.params ?? {})) {
49+
if (!paramConfig || typeof paramConfig !== 'object') continue
50+
const required = (paramConfig as { required?: boolean }).required === true
51+
const userOnly = (paramConfig as { visibility?: string }).visibility === 'user-only'
52+
if (!required || !userOnly) continue
53+
54+
if (!subBlockKeys.has(paramId)) {
55+
violations.push({ blockType: block.type, toolId, paramId })
56+
}
57+
}
58+
}
59+
}
60+
61+
if (violations.length > 0) {
62+
console.error(`\nFound ${violations.length} block/tool canonical-id mismatch(es):\n`)
63+
for (const v of violations) {
64+
console.error(
65+
` - block '${v.blockType}' → tool '${v.toolId}': required user-only param '${v.paramId}' has no subBlock with id or canonicalParamId === '${v.paramId}'`
66+
)
67+
}
68+
console.error(
69+
"\nFix: rename the relevant subBlock id or canonicalParamId to match the tool param id,\n and update the block's inputs + tools.config.params mapper to read from that key.\n"
70+
)
71+
process.exit(1)
72+
}
73+
74+
console.log('check:block-canonical — no violations.')

apps/sim/serializer/index.ts

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -433,29 +433,18 @@ export class Serializer {
433433
const currentToolId = toolAccess?.length > 0 ? this.selectToolId(blockConfig, params) : null
434434
const currentTool = currentToolId ? getTool(currentToolId) : null
435435

436-
// Validate tool parameters (for blocks with tools)
436+
// Validate tool parameters (for blocks with tools).
437+
// Lookup contract: a tool param's value lives under its own paramId in `params`.
438+
// Block subBlocks must align via either `id === paramId` or `canonicalParamId === paramId`
439+
// (enforced by `bun run check:block-canonical`), so this validator never has to invoke
440+
// the block's `tools.config.params` mapper.
437441
if (currentTool) {
438-
// Apply the block's tools.config.params transform to mirror execution-time
439-
// canonical → tool-param renaming (e.g. canonical `document` → tool param `file`).
440-
// Without this, validation looks up the pre-rename name and incorrectly reports
441-
// a missing field for blocks that rename inputs in tools.config.params.
442-
let mappedParams: Record<string, any> = params
443-
try {
444-
const paramsMapper = blockConfig.tools?.config?.params
445-
if (typeof paramsMapper === 'function') {
446-
const result = paramsMapper({ ...params })
447-
if (result && typeof result === 'object' && !Array.isArray(result)) {
448-
mappedParams = { ...params, ...result }
449-
}
450-
}
451-
} catch {
452-
// Mapper may throw on placeholder/runtime values — fall back to raw params.
453-
}
454-
455442
Object.entries(currentTool.params || {}).forEach(([paramId, paramConfig]) => {
456443
if (paramConfig.required && paramConfig.visibility === 'user-only') {
457444
const matchingConfigs =
458-
blockConfig.subBlocks?.filter((sb: any) => sb.id === paramId) || []
445+
blockConfig.subBlocks?.filter(
446+
(sb: any) => sb.id === paramId || sb.canonicalParamId === paramId
447+
) || []
459448

460449
let shouldValidateParam = true
461450

@@ -485,7 +474,7 @@ export class Serializer {
485474
return
486475
}
487476

488-
const fieldValue = mappedParams[paramId]
477+
const fieldValue = params[paramId]
489478
if (fieldValue === undefined || fieldValue === null || fieldValue === '') {
490479
const activeConfig = matchingConfigs.find((config: any) =>
491480
shouldSerializeSubBlock(
@@ -509,10 +498,13 @@ export class Serializer {
509498
const validatedByTool = new Set(currentTool ? Object.keys(currentTool.params || {}) : [])
510499

511500
blockConfig.subBlocks?.forEach((subBlockConfig: SubBlockConfig) => {
512-
// Skip if already validated via tool params
501+
// Skip if already validated via tool params (either by id or canonical bridge)
513502
if (validatedByTool.has(subBlockConfig.id)) {
514503
return
515504
}
505+
if (subBlockConfig.canonicalParamId && validatedByTool.has(subBlockConfig.canonicalParamId)) {
506+
return
507+
}
516508

517509
// Check if subBlock is visible
518510
const isVisible = shouldSerializeSubBlock(

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"check:boundaries": "bun run scripts/check-monorepo-boundaries.ts",
2525
"check:api-validation": "bun run scripts/check-api-validation-contracts.ts --check",
2626
"check:api-validation:strict": "bun run scripts/check-api-validation-contracts.ts --check --enforce-boundary-baseline",
27+
"check:block-canonical": "bun run apps/sim/scripts/check-block-canonical.ts",
2728
"check:realtime-prune": "bun run scripts/check-realtime-prune-graph.ts",
2829
"mship-contracts:generate": "bun run scripts/sync-mothership-stream-contract.ts",
2930
"mship-contracts:check": "bun run scripts/sync-mothership-stream-contract.ts --check",

0 commit comments

Comments
 (0)