Skip to content

Commit 0fe163d

Browse files
committed
fix(aws): keep stale and empty subBlock values out of tool input
The executor merges raw block inputs under the mapped params, so new params named like an existing subBlock picked up values from other operations or empty table rows. Always assign those keys in the block mappers (DynamoDB, SES, CloudFormation, IAM, STS), tolerate string booleans and blank values in the affected tools, add the remaining SDK partition SQS hosts, split long non-ASCII SES From names into RFC 2047 words, and address review findings.
1 parent e536556 commit 0fe163d

28 files changed

Lines changed: 293 additions & 87 deletions

‎apps/docs/content/docs/integrations/ses.mdx‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ Send an email via AWS SES using simple or HTML content
4040
| `replyToAddresses` | string | No | Comma-separated list of reply-to email addresses |
4141
| `configurationSetName` | string | No | SES configuration set name for tracking |
4242
| `fromName` | string | No | Friendly sender name shown with the From address \(e.g., Acme Support\) |
43-
| `emailTags` | json | No | Message tags for filtering sending events, as a JSON array of \{"name", "value"\} objects \(letters, digits, _, -, . and @ only\) |
43+
| `emailTags` | json | No | Message tags for filtering sending events, as a JSON array of \{"name", "value"\} objects \(letters, digits, _ and - only, up to 256 characters\) |
4444
| `attachments` | file[] | No | Files to attach to the email \(up to 28MB in total\) |
4545

4646
#### Output
@@ -71,7 +71,7 @@ Send an email using an SES email template with dynamic template data
7171
| `replyToAddresses` | string | No | Comma-separated list of reply-to email addresses |
7272
| `configurationSetName` | string | No | SES configuration set name for tracking |
7373
| `fromName` | string | No | Friendly sender name shown with the From address \(e.g., Acme Support\) |
74-
| `emailTags` | json | No | Message tags for filtering sending events, as a JSON array of \{"name", "value"\} objects \(letters, digits, _, -, . and @ only\) |
74+
| `emailTags` | json | No | Message tags for filtering sending events, as a JSON array of \{"name", "value"\} objects \(letters, digits, _ and - only, up to 256 characters\) |
7575
| `attachments` | file[] | No | Files to attach to the email \(up to 28MB in total\) |
7676

7777
#### Output
@@ -100,7 +100,7 @@ Send emails to multiple recipients using an SES template with per-recipient data
100100
| `replyToAddresses` | string | No | Comma-separated list of reply-to email addresses |
101101
| `configurationSetName` | string | No | SES configuration set name for tracking |
102102
| `fromName` | string | No | Friendly sender name shown with the From address \(e.g., Acme Support\) |
103-
| `emailTags` | json | No | Default message tags applied to every destination for filtering sending events, as a JSON array of \{"name", "value"\} objects \(letters, digits, _, -, . and @ only\) |
103+
| `emailTags` | json | No | Default message tags applied to every destination for filtering sending events, as a JSON array of \{"name", "value"\} objects \(letters, digits, _ and - only, up to 256 characters\) |
104104

105105
#### Output
106106

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/**
2+
* The executor hands a tool `{ ...rawInputs, ...blockParams }`, so a subBlock value the
3+
* block mapper leaves unassigned reaches the tool raw. These cases keep stored values
4+
* from another operation, and empty table rows, out of requests.
5+
*/
6+
import { describe, expect, it } from 'vitest'
7+
import { awsCloudformationCreateChangeSetContract } from '@/lib/api/contracts/tools/aws/cloudformation-create-change-set'
8+
import { awsDynamodbScanContract } from '@/lib/api/contracts/tools/aws/dynamodb-scan'
9+
import { awsIamCreateUserContract } from '@/lib/api/contracts/tools/aws/iam-create-user'
10+
import { awsSesSendTemplatedEmailContract } from '@/lib/api/contracts/tools/aws/ses-send-templated-email'
11+
import { CloudFormationBlock } from '@/blocks/blocks/cloudformation'
12+
import { DynamoDBBlock } from '@/blocks/blocks/dynamodb'
13+
import { IAMBlock } from '@/blocks/blocks/iam'
14+
import { SESBlock } from '@/blocks/blocks/ses'
15+
import type { BlockConfig } from '@/blocks/types'
16+
import { createChangeSetTool } from '@/tools/cloudformation/create_change_set'
17+
import { scanTool } from '@/tools/dynamodb/scan'
18+
import { createUserTool } from '@/tools/iam/create_user'
19+
import { sendTemplatedEmailTool } from '@/tools/ses/send_templated_email'
20+
21+
const AWS = { region: 'us-east-1', accessKeyId: 'AKIAEXAMPLE', secretAccessKey: 'secret' }
22+
23+
function executorParams(block: BlockConfig, inputs: Record<string, unknown>) {
24+
const mapped = block.tools.config?.params?.(inputs) ?? {}
25+
return { ...inputs, ...mapped } as Record<string, unknown>
26+
}
27+
28+
describe('AWS blocks keep stale subBlock values out of tool input', () => {
29+
it('scan ignores the get consistentRead and query indexName values', () => {
30+
const params = executorParams(DynamoDBBlock, {
31+
...AWS,
32+
operation: 'scan',
33+
tableName: 'orders',
34+
consistentRead: 'false',
35+
indexName: 'by-customer',
36+
})
37+
const body = scanTool.operation.input(params as never) as Record<string, unknown>
38+
expect(body.indexName).toBeUndefined()
39+
expect(awsDynamodbScanContract.body?.safeParse(body).success).toBe(true)
40+
})
41+
42+
it('templated send ignores the send_email Reply-To', () => {
43+
const params = executorParams(SESBlock, {
44+
...AWS,
45+
operation: 'send_templated_email',
46+
fromAddress: 'from@example.com',
47+
toAddresses: 'to@example.com',
48+
templateName: 'welcome',
49+
templateData: '{}',
50+
replyToAddresses: 'stale@example.com',
51+
})
52+
const body = sendTemplatedEmailTool.operation.input(params as never) as Record<string, unknown>
53+
expect(body.replyToAddresses).toBeUndefined()
54+
expect(awsSesSendTemplatedEmailContract.body?.safeParse(body).success).toBe(true)
55+
})
56+
57+
it('create change set ignores the stack tags', () => {
58+
const params = executorParams(CloudFormationBlock, {
59+
awsRegion: 'us-east-1',
60+
awsAccessKeyId: 'AKIAEXAMPLE',
61+
awsSecretAccessKey: 'secret',
62+
operation: 'create_change_set',
63+
stackName: 'app',
64+
changeSetName: 'cs-1',
65+
templateBody: '{}',
66+
tags: '[{"key":"team","value":"old"}]',
67+
})
68+
const body = createChangeSetTool.operation.input(params as never) as Record<string, unknown>
69+
expect(body.tags).toBeUndefined()
70+
expect(awsCloudformationCreateChangeSetContract.body?.safeParse(body).success).toBe(true)
71+
})
72+
73+
it('create user accepts an untouched Tags table', () => {
74+
const params = executorParams(IAMBlock, {
75+
...AWS,
76+
operation: 'create_user',
77+
userName: 'alice',
78+
tags: [{ id: 'row-1', cells: { key: '', value: '' } }],
79+
})
80+
const body = createUserTool.operation.input(params as never)
81+
expect(awsIamCreateUserContract.body?.safeParse(body).success).toBe(true)
82+
})
83+
})

‎apps/sim/blocks/blocks/cloudformation.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -549,7 +549,7 @@ Return ONLY valid JSON - no explanations, no markdown code blocks.`,
549549
...(rest.capabilities && { capabilities: rest.capabilities }),
550550
...(rest.changeSetType && { changeSetType: rest.changeSetType }),
551551
...(rest.changeSetDescription && { description: rest.changeSetDescription }),
552-
...(rest.changeSetTags && { tags: parseJson(rest.changeSetTags, 'tags') }),
552+
tags: rest.changeSetTags ? parseJson(rest.changeSetTags, 'tags') : undefined,
553553
...(rest.roleArn && { roleArn: rest.roleArn }),
554554
}
555555
}

‎apps/sim/blocks/blocks/dynamodb.ts‎

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -772,9 +772,7 @@ Return ONLY the expression - no explanations.`,
772772
if (params.consistentRead === 'true' || params.consistentRead === true) {
773773
result.consistentRead = true
774774
}
775-
if (params.getProjectionExpression) {
776-
result.projectionExpression = params.getProjectionExpression
777-
}
775+
result.projectionExpression = params.getProjectionExpression || undefined
778776
const getNames = parseJson(params.getExpressionAttributeNames, 'expressionAttributeNames')
779777
if (getNames !== undefined) result.expressionAttributeNames = getNames
780778
}
@@ -811,19 +809,19 @@ Return ONLY the expression - no explanations.`,
811809
'expressionAttributeValues'
812810
)
813811
if (values !== undefined) result.expressionAttributeValues = values
814-
if (params.indexName) result.indexName = params.indexName
812+
result.indexName = params.indexName || undefined
815813
if (params.queryLimit) result.limit = Number.parseInt(String(params.queryLimit), 10)
816814
const esk = parseJson(params.queryExclusiveStartKey, 'exclusiveStartKey')
817815
if (esk !== undefined) result.exclusiveStartKey = esk
818-
if (params.scanIndexForward === 'false' || params.scanIndexForward === false) {
819-
result.scanIndexForward = false
820-
}
821-
if (params.queryProjectionExpression) {
822-
result.projectionExpression = params.queryProjectionExpression
823-
}
824-
if (params.queryConsistentRead === 'true' || params.queryConsistentRead === true) {
825-
result.consistentRead = true
826-
}
816+
result.scanIndexForward =
817+
params.scanIndexForward === 'false' || params.scanIndexForward === false
818+
? false
819+
: undefined
820+
result.projectionExpression = params.queryProjectionExpression || undefined
821+
result.consistentRead =
822+
params.queryConsistentRead === 'true' || params.queryConsistentRead === true
823+
? true
824+
: undefined
827825
}
828826

829827
if (op === 'scan') {
@@ -848,10 +846,11 @@ Return ONLY the expression - no explanations.`,
848846
if (lim) result.limit = Number.parseInt(String(lim), 10)
849847
const esk = parseJson(params.scanExclusiveStartKey, 'exclusiveStartKey')
850848
if (esk !== undefined) result.exclusiveStartKey = esk
851-
if (params.scanIndexName) result.indexName = params.scanIndexName
852-
if (params.scanConsistentRead === 'true' || params.scanConsistentRead === true) {
853-
result.consistentRead = true
854-
}
849+
result.indexName = params.scanIndexName || undefined
850+
result.consistentRead =
851+
params.scanConsistentRead === 'true' || params.scanConsistentRead === true
852+
? true
853+
: undefined
855854
if (
856855
params.scanSegment !== undefined &&
857856
params.scanSegment !== null &&

‎apps/sim/blocks/blocks/iam.ts‎

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -650,9 +650,8 @@ export const IAMBlock: BlockConfig = {
650650
case 'create_user': {
651651
result.userName = rest.userName
652652
if (rest.path) result.path = rest.path
653-
if (rest.permissionsBoundary) result.permissionsBoundary = rest.permissionsBoundary
654-
const userTags = serializeTags(rest.tags)
655-
if (userTags) result.tags = userTags
653+
result.permissionsBoundary = rest.permissionsBoundary || undefined
654+
result.tags = serializeTags(rest.tags)
656655
break
657656
}
658657
case 'get_role':
@@ -668,9 +667,8 @@ export const IAMBlock: BlockConfig = {
668667
const parsed = Number.parseInt(String(maxSessionDuration), 10)
669668
if (!Number.isNaN(parsed)) result.maxSessionDuration = parsed
670669
}
671-
if (rest.permissionsBoundary) result.permissionsBoundary = rest.permissionsBoundary
672-
const roleTags = serializeTags(rest.tags)
673-
if (roleTags) result.tags = roleTags
670+
result.permissionsBoundary = rest.permissionsBoundary || undefined
671+
result.tags = serializeTags(rest.tags)
674672
break
675673
}
676674
case 'attach_user_policy':

‎apps/sim/blocks/blocks/rds.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ Output:
271271
"is_premium": true
272272
}
273273
274-
Values are bound as literal parameters, so SQL expressions such as NOW() are stored as text. Leave out columns that should take a database default.`,
274+
Values are bound as literal parameters, so SQL expressions such as NOW() are not evaluated: they are stored as text in text columns and fail on other column types. Leave out columns that should take a database default.`,
275275
placeholder: 'Describe the data you want to insert...',
276276
generationType: 'json-object',
277277
},
@@ -399,7 +399,7 @@ To cast a string to a specific type, use an array of entries with a typeHint of
399399
400400
Return ONLY valid JSON - no explanations, no markdown code blocks.`,
401401
placeholder: 'Describe the values for each :placeholder...',
402-
generationType: 'json-object',
402+
generationType: 'json-value',
403403
},
404404
},
405405
{

‎apps/sim/blocks/blocks/ses.ts‎

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,7 @@ export const SESBlock: BlockConfig = {
381381
wandConfig: {
382382
enabled: true,
383383
prompt:
384-
'Generate an Amazon SES message tag list as a JSON array of objects, each with a "name" and a "value". Names and values may contain only letters, digits, underscores, hyphens, periods, and @. Return ONLY the JSON array.',
384+
'Generate an Amazon SES message tag list as a JSON array of objects, each with a "name" and a "value". Names and values may contain only letters, digits, underscores, and hyphens, up to 256 characters. Return ONLY the JSON array.',
385385
generationType: 'json-array',
386386
},
387387
},
@@ -709,10 +709,9 @@ export const SESBlock: BlockConfig = {
709709
const sendAttachments = normalizeFileInput(attachments)
710710

711711
const applySendOptions = (target: Record<string, unknown>, replyToAddresses: unknown) => {
712-
if (rest.fromName) target.fromName = rest.fromName
713-
if (replyToAddresses) target.replyToAddresses = replyToAddresses
714-
const emailTags = parseEmailTags(rest.emailTags)
715-
if (emailTags !== undefined) target.emailTags = emailTags
712+
target.fromName = rest.fromName || undefined
713+
target.replyToAddresses = replyToAddresses || undefined
714+
target.emailTags = parseEmailTags(rest.emailTags)
716715
}
717716

718717
const connectionConfig = {
@@ -788,7 +787,7 @@ export const SESBlock: BlockConfig = {
788787
if (rest.bccAddresses) result.bccAddresses = rest.bccAddresses
789788
if (rest.configurationSetName) result.configurationSetName = rest.configurationSetName
790789
applySendOptions(result, rest.replyToAddresses)
791-
if (sendAttachments) result.attachments = sendAttachments
790+
result.attachments = sendAttachments
792791
break
793792
case 'send_templated_email':
794793
result.fromAddress = rest.fromAddress
@@ -799,7 +798,7 @@ export const SESBlock: BlockConfig = {
799798
if (rest.bccAddresses) result.bccAddresses = rest.bccAddresses
800799
if (rest.configurationSetName) result.configurationSetName = rest.configurationSetName
801800
applySendOptions(result, rest.templateReplyToAddresses)
802-
if (sendAttachments) result.attachments = sendAttachments
801+
result.attachments = sendAttachments
803802
break
804803
case 'send_bulk_email':
805804
result.fromAddress = rest.fromAddress

‎apps/sim/blocks/blocks/sts.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,7 @@ export const STSBlock: BlockConfig = {
317317
if (rest.tokenCode) result.tokenCode = rest.tokenCode
318318
if (rest.policyArns) result.policyArns = rest.policyArns
319319
if (rest.transitiveTagKeys) result.transitiveTagKeys = rest.transitiveTagKeys
320-
if (rest.sourceIdentity) result.sourceIdentity = rest.sourceIdentity
320+
result.sourceIdentity = rest.sourceIdentity || undefined
321321
if (rest.tags) {
322322
const rows = rest.tags
323323
if (typeof rows === 'string') {

‎apps/sim/blocks/blocks/textract.ts‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -271,10 +271,12 @@ function parseTextractQueries(value: unknown): TextractQueryParam[] {
271271
const text = entry.trim()
272272
return text ? [{ Text: text }] : []
273273
}
274-
if (typeof entry !== 'object' || entry === null) return []
275-
const record = entry as Record<string, unknown>
274+
const record =
275+
typeof entry === 'object' && entry !== null ? (entry as Record<string, unknown>) : {}
276276
const text = typeof record.Text === 'string' ? record.Text.trim() : ''
277-
if (!text) return []
277+
if (!text) {
278+
throw new Error('Each query must be a question string or an object with a non-empty "Text"')
279+
}
278280
return [
279281
{
280282
Text: text,

‎apps/sim/lib/api/contracts/tools/aws/dynamodb-query.ts‎

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,22 @@ const QuerySchema = z.object({
2424
indexName: z.string().optional(),
2525
limit: z.number().positive().optional(),
2626
exclusiveStartKey: z.record(z.string(), z.unknown()).optional(),
27-
scanIndexForward: z.boolean().optional(),
27+
scanIndexForward: z
28+
.union([z.boolean(), z.string()])
29+
.optional()
30+
.transform((val) => {
31+
if (val === false || val === 'false') return false
32+
if (val === true || val === 'true') return true
33+
return undefined
34+
}),
2835
projectionExpression: z.string().optional(),
29-
consistentRead: z.boolean().optional(),
36+
consistentRead: z
37+
.union([z.boolean(), z.string()])
38+
.optional()
39+
.transform((val) => {
40+
if (val === true || val === 'true') return true
41+
return undefined
42+
}),
3043
})
3144

3245
const QueryResponseSchema = z.object({

0 commit comments

Comments
 (0)