@deployfoundation/foundation-deploy 0.2.51 → 0.2.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/dist/bin/app.js +6 -3
- package/dist/bin/foundation-deploy.js +141 -305
- package/dist/bin/release-account.js +1 -0
- package/dist/chunk-1pww63wd.js +301 -0
- package/dist/chunk-3614v9rr.js +907 -0
- package/dist/chunk-5wgwscd0.js +4 -0
- package/dist/{chunk-gnz747z2.js → chunk-96fjaysn.js} +204 -884
- package/dist/chunk-fjdgxw8h.js +516 -0
- package/dist/chunk-jf5rw6h4.js +1117 -0
- package/dist/chunk-m76jt5rc.js +376 -0
- package/dist/chunk-mh1206ne.js +1764 -0
- package/dist/chunk-nvsj7bbw.js +259 -0
- package/dist/chunk-p7zkm4pp.js +151 -0
- package/dist/chunk-pxhftba1.js +336 -0
- package/dist/{chunk-efqdhd2e.js → chunk-spq5kr1t.js} +333 -90
- package/dist/chunk-tey0gh86.js +38 -0
- package/dist/src/index.js +17 -13
- package/package.json +3 -1
- package/src/deploy/cli.ts +71 -0
- package/src/deploy/crm.ts +491 -0
- package/src/deploy/runtime-secret.ts +9 -0
- package/src/stacks/agent-stack.ts +68 -11
- package/src/stacks/api-stack.ts +340 -87
- package/src/stacks/data-stack.ts +27 -1
- package/src/stacks/secret-bearing-reads.ts +131 -0
|
@@ -1,36 +1,104 @@
|
|
|
1
|
+
import {
|
|
2
|
+
agentImage,
|
|
3
|
+
agentImageTagRequired,
|
|
4
|
+
lambdaCode,
|
|
5
|
+
releaseAccess,
|
|
6
|
+
releaseImageRepositoryArn,
|
|
7
|
+
releaseSource
|
|
8
|
+
} from "./chunk-pxhftba1.js";
|
|
1
9
|
import {
|
|
2
10
|
CUSTOMIZATION_ARTIFACT_NAME,
|
|
3
11
|
FOUNDATION_VERSION_RE,
|
|
4
12
|
LIVE_ENDPOINT_NAME,
|
|
5
13
|
WEB_SEARCH_TARGET,
|
|
6
|
-
agentImage,
|
|
7
|
-
agentImageTagRequired,
|
|
8
14
|
capabilityEnabled,
|
|
15
|
+
instanceNames,
|
|
16
|
+
newsletterManifestFor,
|
|
17
|
+
provisionsIntegration,
|
|
18
|
+
requiresAuthenticatedQueue,
|
|
19
|
+
slackCommandPrefixes
|
|
20
|
+
} from "./chunk-fjdgxw8h.js";
|
|
21
|
+
import {
|
|
9
22
|
capabilityScheduledJobs,
|
|
10
23
|
crmOutreachConfigured,
|
|
11
24
|
crmPolicyFingerprint,
|
|
12
25
|
encodeProactiveChannels,
|
|
13
|
-
instanceNames,
|
|
14
26
|
isScheduledJob,
|
|
15
|
-
lambdaCode,
|
|
16
|
-
newsletterManifestFor,
|
|
17
27
|
parseInstanceConfig,
|
|
18
28
|
parseSchedule,
|
|
19
|
-
provisionsIntegration,
|
|
20
|
-
releaseAccess,
|
|
21
|
-
releaseImageRepositoryArn,
|
|
22
|
-
releaseSource,
|
|
23
|
-
requiresAuthenticatedQueue,
|
|
24
29
|
scheduledJobBindings,
|
|
25
|
-
slackCommandPrefixes,
|
|
26
30
|
webhookJobBindings
|
|
27
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-96fjaysn.js";
|
|
28
32
|
|
|
29
33
|
// src/stacks/agent-stack.ts
|
|
30
34
|
import * as cdk from "aws-cdk-lib";
|
|
31
35
|
import * as ecr from "aws-cdk-lib/aws-ecr";
|
|
32
36
|
import * as iam from "aws-cdk-lib/aws-iam";
|
|
33
37
|
import * as logs from "aws-cdk-lib/aws-logs";
|
|
38
|
+
|
|
39
|
+
// src/stacks/secret-bearing-reads.ts
|
|
40
|
+
var SECRET_BEARING_READ_ACTIONS = [
|
|
41
|
+
"wafv2:GetSampledRequests",
|
|
42
|
+
"waf:GetSampledRequests",
|
|
43
|
+
"waf-regional:GetSampledRequests",
|
|
44
|
+
"lambda:GetFunction",
|
|
45
|
+
"lambda:GetFunctionConfiguration",
|
|
46
|
+
"lambda:ListFunctions",
|
|
47
|
+
"lambda:ListVersionsByFunction",
|
|
48
|
+
"lambda:GetLayerVersion",
|
|
49
|
+
"lambda:GetLayerVersionByArn",
|
|
50
|
+
"ecs:DescribeTaskDefinition",
|
|
51
|
+
"apprunner:DescribeService",
|
|
52
|
+
"batch:DescribeJobDefinitions",
|
|
53
|
+
"batch:DescribeJobs",
|
|
54
|
+
"codebuild:BatchGetProjects",
|
|
55
|
+
"codebuild:BatchGetBuilds",
|
|
56
|
+
"codebuild:BatchGetBuildBatches",
|
|
57
|
+
"amplify:GetApp",
|
|
58
|
+
"amplify:ListApps",
|
|
59
|
+
"amplify:GetBranch",
|
|
60
|
+
"amplify:ListBranches",
|
|
61
|
+
"amplify:GetBackendEnvironment",
|
|
62
|
+
"elasticbeanstalk:DescribeConfigurationSettings",
|
|
63
|
+
"ec2:DescribeLaunchTemplateVersions",
|
|
64
|
+
"ec2:DescribeInstanceAttribute",
|
|
65
|
+
"ssm:GetParameter",
|
|
66
|
+
"ssm:GetParameters",
|
|
67
|
+
"ssm:GetParametersByPath",
|
|
68
|
+
"ssm:GetParameterHistory",
|
|
69
|
+
"cloudformation:GetTemplate",
|
|
70
|
+
"appconfig:GetConfiguration",
|
|
71
|
+
"appconfig:GetHostedConfigurationVersion",
|
|
72
|
+
"glue:GetJob",
|
|
73
|
+
"glue:GetJobs",
|
|
74
|
+
"glue:BatchGetJobs",
|
|
75
|
+
"glue:GetConnection",
|
|
76
|
+
"glue:GetConnections",
|
|
77
|
+
"glue:GetDevEndpoint",
|
|
78
|
+
"glue:GetDevEndpoints",
|
|
79
|
+
"glue:BatchGetDevEndpoints",
|
|
80
|
+
"sagemaker:DescribeModel",
|
|
81
|
+
"sagemaker:DescribeTrainingJob",
|
|
82
|
+
"sagemaker:DescribeProcessingJob",
|
|
83
|
+
"sagemaker:DescribeTransformJob",
|
|
84
|
+
"datapipeline:GetPipelineDefinition",
|
|
85
|
+
"appsync:ListApiKeys",
|
|
86
|
+
"appsync:GetDataSource",
|
|
87
|
+
"appsync:ListDataSources",
|
|
88
|
+
"kinesisanalytics:DescribeApplication",
|
|
89
|
+
"lightsail:GetBucketAccessKeys",
|
|
90
|
+
"lightsail:GetContainerServices",
|
|
91
|
+
"lightsail:GetContainerServiceDeployments",
|
|
92
|
+
"opsworks:DescribeApps",
|
|
93
|
+
"opsworks:DescribeStacks",
|
|
94
|
+
"cognito-identity:GetCredentialsForIdentity",
|
|
95
|
+
"cognito-identity:GetOpenIdToken",
|
|
96
|
+
"cognito-identity:GetOpenIdTokenForDeveloperIdentity",
|
|
97
|
+
"connect:GetFederationToken",
|
|
98
|
+
"codeartifact:GetAuthorizationToken"
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
// src/stacks/agent-stack.ts
|
|
34
102
|
class FoundationAgent extends cdk.Stack {
|
|
35
103
|
repository;
|
|
36
104
|
executionRole;
|
|
@@ -73,6 +141,7 @@ class FoundationAgent extends cdk.Stack {
|
|
|
73
141
|
this.accessPoint = accessPoint;
|
|
74
142
|
this.executionRole = this.buildExecutionRole(props);
|
|
75
143
|
this.readOnlyRole = this.buildReadOnlyRole(props);
|
|
144
|
+
this.secretBearingReadsPolicy(props.instance, [this.executionRole, this.readOnlyRole]);
|
|
76
145
|
this.buildSpansIngestionPolicy();
|
|
77
146
|
const gateway = this.buildToolGateway(display);
|
|
78
147
|
this.webSearchUrl = gateway.getAtt("GatewayUrl").toString();
|
|
@@ -221,20 +290,27 @@ class FoundationAgent extends cdk.Stack {
|
|
|
221
290
|
role.addToPolicy(new iam.PolicyStatement({
|
|
222
291
|
sid: "DenyDirectCrmTableAccess",
|
|
223
292
|
effect: iam.Effect.DENY,
|
|
224
|
-
actions: [
|
|
225
|
-
"dynamodb:GetItem",
|
|
226
|
-
"dynamodb:BatchGetItem",
|
|
227
|
-
"dynamodb:TransactGetItems",
|
|
228
|
-
"dynamodb:Query",
|
|
229
|
-
"dynamodb:Scan",
|
|
230
|
-
"dynamodb:PartiQLSelect",
|
|
231
|
-
"dynamodb:ExecuteStatement",
|
|
232
|
-
"dynamodb:BatchExecuteStatement",
|
|
233
|
-
"dynamodb:ExportTableToPointInTime"
|
|
234
|
-
],
|
|
293
|
+
actions: ["dynamodb:*"],
|
|
235
294
|
resources
|
|
236
295
|
}));
|
|
237
296
|
}
|
|
297
|
+
denyDirectCrmHttpSecretAccess(role, instance) {
|
|
298
|
+
if (!provisionsIntegration(instance, "crm") || instance.crm.provider.kind !== "http")
|
|
299
|
+
return;
|
|
300
|
+
role.addToPolicy(new iam.PolicyStatement({
|
|
301
|
+
sid: "DenyCrmHttpSecret",
|
|
302
|
+
effect: iam.Effect.DENY,
|
|
303
|
+
actions: ["secretsmanager:*"],
|
|
304
|
+
resources: [
|
|
305
|
+
cdk.Stack.of(this).formatArn({
|
|
306
|
+
service: "secretsmanager",
|
|
307
|
+
resource: "secret",
|
|
308
|
+
resourceName: `${this.names.secretCrmHttp}-??????`,
|
|
309
|
+
arnFormat: cdk.ArnFormat.COLON_RESOURCE_NAME
|
|
310
|
+
})
|
|
311
|
+
]
|
|
312
|
+
}));
|
|
313
|
+
}
|
|
238
314
|
denyDirectKnockSecretAccess(role, props) {
|
|
239
315
|
if (!provisionsIntegration(props.instance, "knock"))
|
|
240
316
|
return;
|
|
@@ -385,6 +461,7 @@ class FoundationAgent extends cdk.Stack {
|
|
|
385
461
|
managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName("ReadOnlyAccess")]
|
|
386
462
|
});
|
|
387
463
|
this.denyDirectCrmTableAccess(role, props.instance, props.crmTable);
|
|
464
|
+
this.denyDirectCrmHttpSecretAccess(role, props.instance);
|
|
388
465
|
this.denyDirectKnockSecretAccess(role, props);
|
|
389
466
|
this.denyDirectEmailSecretAccess(role, props.secrets.googleEmail, instanceNames(props.instance).secretGoogleEmail);
|
|
390
467
|
this.denyDirectDocsSecretAccess(role, instanceNames(props.instance).secretGoogleDocs);
|
|
@@ -427,6 +504,20 @@ class FoundationAgent extends cdk.Stack {
|
|
|
427
504
|
}));
|
|
428
505
|
return role;
|
|
429
506
|
}
|
|
507
|
+
secretBearingReadsPolicy(instance, roles) {
|
|
508
|
+
new iam.ManagedPolicy(this, "AgentSecretBearingReadsDeny", {
|
|
509
|
+
description: `${instance.displayName} agent: no AWS read whose response carries a secret`,
|
|
510
|
+
roles,
|
|
511
|
+
statements: [
|
|
512
|
+
new iam.PolicyStatement({
|
|
513
|
+
sid: "DenySecretBearingReads",
|
|
514
|
+
effect: iam.Effect.DENY,
|
|
515
|
+
actions: [...SECRET_BEARING_READ_ACTIONS],
|
|
516
|
+
resources: ["*"]
|
|
517
|
+
})
|
|
518
|
+
]
|
|
519
|
+
});
|
|
520
|
+
}
|
|
430
521
|
buildSpansIngestionPolicy() {
|
|
431
522
|
return new logs.ResourcePolicy(this, "AgentSpansIngestion", {
|
|
432
523
|
resourcePolicyName: `${this.stackName}-AgentSpansFromXray`,
|
|
@@ -454,6 +545,7 @@ class FoundationAgent extends cdk.Stack {
|
|
|
454
545
|
description: `${props.instance.displayName} AgentCore execution role (least privilege)`
|
|
455
546
|
});
|
|
456
547
|
this.denyDirectCrmTableAccess(role, props.instance, props.crmTable);
|
|
548
|
+
this.denyDirectCrmHttpSecretAccess(role, props.instance);
|
|
457
549
|
this.denyDirectKnockSecretAccess(role, props);
|
|
458
550
|
this.denyDirectUpworkApprovalAccess(role, props.upworkApprovalTable);
|
|
459
551
|
this.denyDirectEmailSecretAccess(role, props.secrets.googleEmail, instanceNames(props.instance).secretGoogleEmail);
|
|
@@ -1550,12 +1642,14 @@ class FoundationApi extends cdk2.Stack {
|
|
|
1550
1642
|
}
|
|
1551
1643
|
if (provisionsIntegration(props.instance, "crm")) {
|
|
1552
1644
|
const crm = crmConfigForProvisioning(props.configPath);
|
|
1645
|
+
const provider = props.instance.crm.provider;
|
|
1553
1646
|
const crmTable = props.crmTable;
|
|
1554
1647
|
if (crmTable === undefined)
|
|
1555
1648
|
throw new Error(`${id}: integrations.crm is enabled but no CRM table was supplied by FoundationData`);
|
|
1649
|
+
const usesTable = provider.kind === "dynamodb" || crm.outreach !== undefined;
|
|
1556
1650
|
const crmProxyRole = new iam2.Role(this, "CrmProxyRole", {
|
|
1557
1651
|
assumedBy: new iam2.ServicePrincipal("lambda.amazonaws.com"),
|
|
1558
|
-
description: `${display} CRM proxy Lambda role (self-hosted CRM table access only)`,
|
|
1652
|
+
description: provider.kind === "dynamodb" ? `${display} CRM proxy Lambda role (self-hosted CRM table access only)` : `${display} CRM proxy Lambda role (crm/1 key and outreach ledger only)`,
|
|
1559
1653
|
managedPolicies: [
|
|
1560
1654
|
iam2.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole")
|
|
1561
1655
|
]
|
|
@@ -1565,30 +1659,83 @@ class FoundationApi extends cdk2.Stack {
|
|
|
1565
1659
|
actions: ["secretsmanager:GetSecretValue"],
|
|
1566
1660
|
resources: [secretArnPattern2(props.secrets.signing)]
|
|
1567
1661
|
}));
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1662
|
+
if (provider.kind === "dynamodb") {
|
|
1663
|
+
crmProxyRole.addToPolicy(new iam2.PolicyStatement({
|
|
1664
|
+
sid: "CrmTableData",
|
|
1665
|
+
actions: [
|
|
1666
|
+
"dynamodb:GetItem",
|
|
1667
|
+
"dynamodb:PutItem",
|
|
1668
|
+
"dynamodb:UpdateItem",
|
|
1669
|
+
"dynamodb:DeleteItem",
|
|
1670
|
+
"dynamodb:BatchGetItem",
|
|
1671
|
+
"dynamodb:BatchWriteItem",
|
|
1672
|
+
"dynamodb:TransactWriteItems",
|
|
1673
|
+
"dynamodb:ConditionCheckItem"
|
|
1674
|
+
],
|
|
1675
|
+
resources: [crmTable.tableArn]
|
|
1676
|
+
}));
|
|
1677
|
+
crmProxyRole.addToPolicy(new iam2.PolicyStatement({
|
|
1678
|
+
sid: "CrmRecordsIndex",
|
|
1679
|
+
actions: ["dynamodb:Query"],
|
|
1680
|
+
resources: [crmTable.tableArn, `${crmTable.tableArn}/index/RecordsIndex`]
|
|
1681
|
+
}));
|
|
1682
|
+
} else {
|
|
1683
|
+
crmProxyRole.addToPolicy(new iam2.PolicyStatement({
|
|
1684
|
+
sid: "CrmHttpSecret",
|
|
1685
|
+
actions: ["secretsmanager:GetSecretValue"],
|
|
1686
|
+
resources: [
|
|
1687
|
+
secretArnPattern2(secretsmanager.Secret.fromSecretNameV2(this, "CrmHttpSecret", names.secretCrmHttp))
|
|
1688
|
+
]
|
|
1689
|
+
}));
|
|
1690
|
+
if (usesTable)
|
|
1691
|
+
crmProxyRole.addToPolicy(new iam2.PolicyStatement({
|
|
1692
|
+
sid: "CrmOutreachLedger",
|
|
1693
|
+
actions: [
|
|
1694
|
+
"dynamodb:GetItem",
|
|
1695
|
+
"dynamodb:PutItem",
|
|
1696
|
+
"dynamodb:UpdateItem",
|
|
1697
|
+
"dynamodb:DeleteItem",
|
|
1698
|
+
"dynamodb:TransactWriteItems"
|
|
1699
|
+
],
|
|
1700
|
+
resources: [crmTable.tableArn],
|
|
1701
|
+
conditions: {
|
|
1702
|
+
"ForAllValues:StringEquals": {
|
|
1703
|
+
"dynamodb:LeadingKeys": [crmOutreachLedgerPartition(props.instance.name)]
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
}));
|
|
1707
|
+
}
|
|
1708
|
+
if (usesTable)
|
|
1709
|
+
crmProxyRole.addToPolicy(new iam2.PolicyStatement({
|
|
1710
|
+
sid: "CrmTableKey",
|
|
1711
|
+
actions: ["kms:Decrypt", "kms:Encrypt", "kms:GenerateDataKey", "kms:DescribeKey"],
|
|
1712
|
+
resources: [(crmTable.encryptionKey ?? props.dataKey).keyArn]
|
|
1713
|
+
}));
|
|
1714
|
+
const release = releaseVersionContext(this);
|
|
1715
|
+
const crmProxyEnvironment = {
|
|
1716
|
+
FOUNDATION_CRM_TABLE_NAME: crmTable.tableName,
|
|
1717
|
+
FOUNDATION_SLACK_SIGNING_SECRET_ID: props.secrets.signing.secretName,
|
|
1718
|
+
FOUNDATION_SLACK_TEAM_ID: props.instance.slack.teamId,
|
|
1719
|
+
FOUNDATION_CRM_TENANT_ID: props.instance.name,
|
|
1720
|
+
FOUNDATION_CRM_ALLOWED_CHANNELS: JSON.stringify(crm.channels),
|
|
1721
|
+
FOUNDATION_CRM_ALLOWED_STATUSES: JSON.stringify(crm.statuses),
|
|
1722
|
+
FOUNDATION_CRM_ACTIVITY_TYPES: JSON.stringify(crm.activityTypes),
|
|
1723
|
+
FOUNDATION_CRM_MAX_BATCH_SIZE: String(crm.maxBatchSize),
|
|
1724
|
+
FOUNDATION_CRM_STATUS_POLICY: JSON.stringify(crm.statusPolicy),
|
|
1725
|
+
FOUNDATION_CRM_DELETES: String(crm.deletes),
|
|
1726
|
+
...crm.outreach === undefined ? {} : {
|
|
1727
|
+
FOUNDATION_CRM_OUTREACH: JSON.stringify(crm.outreach),
|
|
1728
|
+
FOUNDATION_CRM_TIMEZONE: crm.timezone
|
|
1729
|
+
},
|
|
1730
|
+
...provider.kind === "http" ? {
|
|
1731
|
+
FOUNDATION_CRM_PROVIDER: "http",
|
|
1732
|
+
FOUNDATION_CRM_HTTP_BASE_URL: provider.baseUrl,
|
|
1733
|
+
FOUNDATION_CRM_HTTP_SECRET_ID: names.secretCrmHttp,
|
|
1734
|
+
FOUNDATION_CRM_HTTP_EXPECTED_TENANT: provider.expectedTenant,
|
|
1735
|
+
...release === undefined ? {} : { FOUNDATION_RELEASE: release }
|
|
1736
|
+
} : {}
|
|
1737
|
+
};
|
|
1738
|
+
assertLambdaEnvironmentBudget(crmProxyEnvironment);
|
|
1592
1739
|
this.crmProxyFunction = new lambda.Function(this, "CrmProxyFunction", {
|
|
1593
1740
|
functionName: names.crmProxyFunctionName,
|
|
1594
1741
|
runtime: lambda.Runtime.NODEJS_22_X,
|
|
@@ -1597,20 +1744,7 @@ class FoundationApi extends cdk2.Stack {
|
|
|
1597
1744
|
role: crmProxyRole,
|
|
1598
1745
|
timeout: cdk2.Duration.seconds(30),
|
|
1599
1746
|
memorySize: 256,
|
|
1600
|
-
environment:
|
|
1601
|
-
FOUNDATION_CRM_TABLE_NAME: crmTable.tableName,
|
|
1602
|
-
FOUNDATION_SLACK_SIGNING_SECRET_ID: props.secrets.signing.secretName,
|
|
1603
|
-
FOUNDATION_SLACK_TEAM_ID: props.instance.slack.teamId,
|
|
1604
|
-
FOUNDATION_CRM_TENANT_ID: props.instance.name,
|
|
1605
|
-
FOUNDATION_CRM_ALLOWED_CHANNELS: JSON.stringify(crm.channels),
|
|
1606
|
-
FOUNDATION_CRM_ALLOWED_STATUSES: JSON.stringify(crm.statuses),
|
|
1607
|
-
FOUNDATION_CRM_ACTIVITY_TYPES: JSON.stringify(crm.activityTypes),
|
|
1608
|
-
FOUNDATION_CRM_MAX_BATCH_SIZE: String(crm.maxBatchSize),
|
|
1609
|
-
...crm.outreach === undefined ? {} : {
|
|
1610
|
-
FOUNDATION_CRM_OUTREACH: JSON.stringify(crm.outreach),
|
|
1611
|
-
FOUNDATION_CRM_TIMEZONE: crm.timezone
|
|
1612
|
-
}
|
|
1613
|
-
}
|
|
1747
|
+
environment: crmProxyEnvironment
|
|
1614
1748
|
});
|
|
1615
1749
|
new cdk2.CfnOutput(this, "CrmProxyFunctionArn", {
|
|
1616
1750
|
value: this.crmProxyFunction.functionArn
|
|
@@ -1660,6 +1794,12 @@ class FoundationApi extends cdk2.Stack {
|
|
|
1660
1794
|
agentRuntimeArn: props.agentRuntimeArn,
|
|
1661
1795
|
alarmAction
|
|
1662
1796
|
});
|
|
1797
|
+
if (this.crmProxyFunction !== undefined && props.instance.crm.provider.kind === "http")
|
|
1798
|
+
addCrmProviderAlarms(this, {
|
|
1799
|
+
instance: props.instance,
|
|
1800
|
+
functionName: names.crmProxyFunctionName,
|
|
1801
|
+
alarmAction
|
|
1802
|
+
});
|
|
1663
1803
|
let dispatcherFunctionName;
|
|
1664
1804
|
if (configuredScheduledJobs.length > 0) {
|
|
1665
1805
|
const dispatchRole = new iam2.Role(this, "ScheduledJobDispatcherRole", {
|
|
@@ -1782,34 +1922,13 @@ function addConfigLoadAlarm(stack, args) {
|
|
|
1782
1922
|
return;
|
|
1783
1923
|
const runtimeId = cdk2.Fn.select(1, cdk2.Fn.split("/", args.agentRuntimeArn));
|
|
1784
1924
|
const logGroupName = `/aws/bedrock-agentcore/runtimes/${runtimeId}-${LIVE_ENDPOINT_NAME}`;
|
|
1785
|
-
const
|
|
1786
|
-
|
|
1787
|
-
description: `${args.instance.displayName} creates the runtime's live log group when missing`,
|
|
1788
|
-
managedPolicies: [
|
|
1789
|
-
iam2.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole")
|
|
1790
|
-
]
|
|
1791
|
-
});
|
|
1792
|
-
role.addToPolicy(new iam2.PolicyStatement({
|
|
1925
|
+
const ensureLogGroup = ensureLogGroupExists(stack, {
|
|
1926
|
+
id: "AgentLiveLogGroup",
|
|
1793
1927
|
sid: "CreateRuntimeLiveLogGroup",
|
|
1794
|
-
actions: ["logs:CreateLogGroup"],
|
|
1795
|
-
resources: [
|
|
1796
|
-
`arn:${stack.partition}:logs:${stack.region}:${stack.account}:log-group:${logGroupName}`
|
|
1797
|
-
]
|
|
1798
|
-
}));
|
|
1799
|
-
const ensureFunction = new lambda.Function(stack, "AgentLiveLogGroupFunction", {
|
|
1800
|
-
runtime: lambda.Runtime.NODEJS_22_X,
|
|
1801
|
-
handler: "index.runtimeLogGroupHandler",
|
|
1802
|
-
code: lambdaCode(stack, "gateway"),
|
|
1803
|
-
role,
|
|
1804
|
-
timeout: cdk2.Duration.seconds(30),
|
|
1805
|
-
memorySize: 128
|
|
1806
|
-
});
|
|
1807
|
-
const ensureLogGroup = new cdk2.CustomResource(stack, "AgentLiveLogGroup", {
|
|
1808
|
-
serviceToken: ensureFunction.functionArn,
|
|
1809
1928
|
resourceType: "Custom::FoundationRuntimeLogGroup",
|
|
1810
|
-
|
|
1929
|
+
description: `${args.instance.displayName} creates the runtime's live log group when missing`,
|
|
1930
|
+
logGroupName
|
|
1811
1931
|
});
|
|
1812
|
-
ensureLogGroup.node.addDependency(role);
|
|
1813
1932
|
const filter = new logs2.MetricFilter(stack, "ConfigLoadFailureMetricFilter", {
|
|
1814
1933
|
logGroup: logs2.LogGroup.fromLogGroupName(stack, "AgentLiveLogs", logGroupName),
|
|
1815
1934
|
filterPattern: logs2.FilterPattern.any(...CONFIG_LOAD_FAILURE_EVENTS.map((event) => logs2.FilterPattern.stringValue("$.event", "=", event))),
|
|
@@ -1835,22 +1954,136 @@ function addConfigLoadAlarm(stack, args) {
|
|
|
1835
1954
|
alarm.addAlarmAction(args.alarmAction);
|
|
1836
1955
|
return alarm;
|
|
1837
1956
|
}
|
|
1957
|
+
function ensureLogGroupExists(stack, args) {
|
|
1958
|
+
const role = new iam2.Role(stack, `${args.id}Role`, {
|
|
1959
|
+
assumedBy: new iam2.ServicePrincipal("lambda.amazonaws.com"),
|
|
1960
|
+
description: args.description,
|
|
1961
|
+
managedPolicies: [
|
|
1962
|
+
iam2.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole")
|
|
1963
|
+
]
|
|
1964
|
+
});
|
|
1965
|
+
role.addToPolicy(new iam2.PolicyStatement({
|
|
1966
|
+
sid: args.sid,
|
|
1967
|
+
actions: ["logs:CreateLogGroup"],
|
|
1968
|
+
resources: [
|
|
1969
|
+
`arn:${stack.partition}:logs:${stack.region}:${stack.account}:log-group:${args.logGroupName}`
|
|
1970
|
+
]
|
|
1971
|
+
}));
|
|
1972
|
+
const ensureFunction = new lambda.Function(stack, `${args.id}Function`, {
|
|
1973
|
+
runtime: lambda.Runtime.NODEJS_22_X,
|
|
1974
|
+
handler: "index.runtimeLogGroupHandler",
|
|
1975
|
+
code: lambdaCode(stack, "gateway"),
|
|
1976
|
+
role,
|
|
1977
|
+
timeout: cdk2.Duration.seconds(30),
|
|
1978
|
+
memorySize: 128
|
|
1979
|
+
});
|
|
1980
|
+
const ensureLogGroup = new cdk2.CustomResource(stack, args.id, {
|
|
1981
|
+
serviceToken: ensureFunction.functionArn,
|
|
1982
|
+
resourceType: args.resourceType,
|
|
1983
|
+
properties: { LogGroupName: args.logGroupName }
|
|
1984
|
+
});
|
|
1985
|
+
ensureLogGroup.node.addDependency(role);
|
|
1986
|
+
return ensureLogGroup;
|
|
1987
|
+
}
|
|
1988
|
+
var CRM_PROVIDER_ALARMS = [
|
|
1989
|
+
{
|
|
1990
|
+
event: "crm_proxy",
|
|
1991
|
+
code: "tenant_mismatch",
|
|
1992
|
+
name: "TenantMismatch",
|
|
1993
|
+
description: "The CRM answered as a different tenant from crm.provider.expectedTenant, so the CRM proxy refused the reply and the CRM tools fail closed. Check that the key written with crm-key was issued for that tenant, and that expectedTenant is right."
|
|
1994
|
+
},
|
|
1995
|
+
{
|
|
1996
|
+
event: "crm_proxy",
|
|
1997
|
+
code: "upstream_unauthorized",
|
|
1998
|
+
name: "UpstreamUnauthorized",
|
|
1999
|
+
description: "The CRM refused the CRM proxy's key (401), even after the proxy re-read it: revoked, expired or wrong. Issue a new key in the CRM and run foundation-deploy crm-key; the proxy picks it up on its next call."
|
|
2000
|
+
},
|
|
2001
|
+
{
|
|
2002
|
+
event: "crm_proxy",
|
|
2003
|
+
code: "upstream_forbidden",
|
|
2004
|
+
name: "UpstreamForbidden",
|
|
2005
|
+
description: "The CRM answered 403: a scope the key lacks, the CRM's kill switch, or a WAF in front of it blocking the proxy. Check the key's scopes, the switch and the WAF's logs."
|
|
2006
|
+
},
|
|
2007
|
+
{
|
|
2008
|
+
event: "crm_proxy",
|
|
2009
|
+
code: "unconfigured",
|
|
2010
|
+
name: "Unconfigured",
|
|
2011
|
+
description: "The CRM proxy is unconfigured, so every CRM tool fails closed: no key written yet (run foundation-deploy crm-key), or the CRM's /meta does not match the deployment. The log line's check field names which: contract, statuses, activity_types, batch_size or operations."
|
|
2012
|
+
},
|
|
2013
|
+
{
|
|
2014
|
+
event: "crm_proxy_warning",
|
|
2015
|
+
code: "ledger_kept",
|
|
2016
|
+
name: "LedgerKept",
|
|
2017
|
+
description: "A draft's outreach reservation was kept because the CRM's answer was unknown (a timeout, a 5xx, a 429 or a reply without the contract header). The weekly cap counts it, failing closed. Check in the CRM whether that draft was logged."
|
|
2018
|
+
},
|
|
2019
|
+
{
|
|
2020
|
+
event: "crm_proxy_warning",
|
|
2021
|
+
code: "policy_mismatch",
|
|
2022
|
+
name: "PolicyMismatch",
|
|
2023
|
+
description: "The CRM's own policy in /meta differs from the deployment's capabilities.crm (the log line's fields name which). Each side enforces its own, so the stricter wins; align the two."
|
|
2024
|
+
}
|
|
2025
|
+
];
|
|
2026
|
+
function addCrmProviderAlarms(stack, args) {
|
|
2027
|
+
const logGroupName = `/aws/lambda/${args.functionName}`;
|
|
2028
|
+
const ensureLogGroup = ensureLogGroupExists(stack, {
|
|
2029
|
+
id: "CrmProxyLogGroup",
|
|
2030
|
+
sid: "CreateCrmProxyLogGroup",
|
|
2031
|
+
resourceType: "Custom::FoundationLambdaLogGroup",
|
|
2032
|
+
description: `${args.instance.displayName} creates the CRM proxy's log group when missing`,
|
|
2033
|
+
logGroupName
|
|
2034
|
+
});
|
|
2035
|
+
const logGroup = logs2.LogGroup.fromLogGroupName(stack, "CrmProxyLogs", logGroupName);
|
|
2036
|
+
return CRM_PROVIDER_ALARMS.map((entry) => {
|
|
2037
|
+
const filter = new logs2.MetricFilter(stack, `CrmProxy${entry.name}MetricFilter`, {
|
|
2038
|
+
logGroup,
|
|
2039
|
+
filterPattern: logs2.FilterPattern.all(logs2.FilterPattern.stringValue("$.event", "=", entry.event), logs2.FilterPattern.stringValue("$.code", "=", entry.code)),
|
|
2040
|
+
metricNamespace: `Foundation/${args.instance.naming.prefix}`,
|
|
2041
|
+
metricName: `CrmProxy${entry.name}`,
|
|
2042
|
+
metricValue: "1"
|
|
2043
|
+
});
|
|
2044
|
+
filter.node.addDependency(ensureLogGroup);
|
|
2045
|
+
const alarm = new cloudwatch.Alarm(stack, `CrmProxy${entry.name}Alarm`, {
|
|
2046
|
+
alarmDescription: `${args.instance.displayName} CRM proxy logged ${entry.code}. ${entry.description}`,
|
|
2047
|
+
metric: filter.metric({
|
|
2048
|
+
statistic: "Sum",
|
|
2049
|
+
period: cdk2.Duration.minutes(entry.code === "policy_mismatch" ? 60 : 5)
|
|
2050
|
+
}),
|
|
2051
|
+
threshold: 1,
|
|
2052
|
+
evaluationPeriods: 1,
|
|
2053
|
+
comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
|
|
2054
|
+
treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING
|
|
2055
|
+
});
|
|
2056
|
+
alarm.addAlarmAction(args.alarmAction);
|
|
2057
|
+
return alarm;
|
|
2058
|
+
});
|
|
2059
|
+
}
|
|
2060
|
+
function crmOutreachLedgerPartition(tenantId) {
|
|
2061
|
+
return `TENANT#${tenantId}#OUTREACH`;
|
|
2062
|
+
}
|
|
2063
|
+
function releaseVersionContext(scope) {
|
|
2064
|
+
const explicit = String(scope.node.tryGetContext("foundationVersion") ?? "");
|
|
2065
|
+
const version = explicit !== "" ? explicit : releaseSource(scope)?.version;
|
|
2066
|
+
return version !== undefined && FOUNDATION_VERSION_RE.test(version) ? version : undefined;
|
|
2067
|
+
}
|
|
1838
2068
|
function crmConfigForProvisioning(configPath) {
|
|
1839
2069
|
const config = parseInstanceConfig(readFileSync(configPath, "utf8"));
|
|
1840
2070
|
const crm = config.capabilities.crm;
|
|
1841
2071
|
const missing = [
|
|
1842
2072
|
...crm.enabled ? [] : ["capabilities.crm.enabled"],
|
|
1843
2073
|
...crm.channels.length > 0 ? [] : ["capabilities.crm.channels"],
|
|
1844
|
-
...crm.statuses.length > 0 ? [] : ["capabilities.crm.statuses"],
|
|
2074
|
+
...crm.statuses.company.length > 0 ? [] : ["capabilities.crm.statuses.company"],
|
|
2075
|
+
...crm.statuses.contact.length > 0 ? [] : ["capabilities.crm.statuses.contact"],
|
|
1845
2076
|
...crm.activityTypes.length > 0 ? [] : ["capabilities.crm.activityTypes"]
|
|
1846
2077
|
];
|
|
1847
2078
|
if (missing.length > 0)
|
|
1848
2079
|
throw new Error(`${configPath}: integrations.crm requires a configured CRM capability (${missing.join(", ")})`);
|
|
1849
2080
|
return {
|
|
1850
2081
|
channels: [...crm.channels],
|
|
1851
|
-
statuses: [...crm.statuses],
|
|
2082
|
+
statuses: { company: [...crm.statuses.company], contact: [...crm.statuses.contact] },
|
|
1852
2083
|
activityTypes: [...crm.activityTypes],
|
|
1853
2084
|
maxBatchSize: crm.maxBatchSize,
|
|
2085
|
+
statusPolicy: crm.statusPolicy,
|
|
2086
|
+
deletes: crm.deletes,
|
|
1854
2087
|
policyFingerprint: crmPolicyFingerprint(crm),
|
|
1855
2088
|
...crmOutreachConfigured(crm.outreach) ? { outreach: crm.outreach } : {},
|
|
1856
2089
|
timezone: config.timezone
|
|
@@ -1929,7 +2162,7 @@ function assertLambdaEnvironmentBudget(values) {
|
|
|
1929
2162
|
bytes += cdk2.Token.isUnresolved(value) ? LAMBDA_ENVIRONMENT_TOKEN_BYTES : Buffer.byteLength(value);
|
|
1930
2163
|
}
|
|
1931
2164
|
if (bytes > LAMBDA_ENVIRONMENT_MAX_BYTES) {
|
|
1932
|
-
throw new Error(`
|
|
2165
|
+
throw new Error(`A Lambda environment needs an estimated ${bytes} bytes; the AWS limit is ${LAMBDA_ENVIRONMENT_MAX_BYTES}. Shorten job prompts or reduce configured values.`);
|
|
1933
2166
|
}
|
|
1934
2167
|
}
|
|
1935
2168
|
function scheduledJobsFor(configPath) {
|
|
@@ -2120,6 +2353,7 @@ class FoundationData extends cdk4.Stack {
|
|
|
2120
2353
|
crmTable;
|
|
2121
2354
|
upworkApprovalTable;
|
|
2122
2355
|
etradePendingTable;
|
|
2356
|
+
crmHttpSecret;
|
|
2123
2357
|
secrets;
|
|
2124
2358
|
constructor(scope, id, props) {
|
|
2125
2359
|
super(scope, id, props);
|
|
@@ -2238,6 +2472,15 @@ class FoundationData extends cdk4.Stack {
|
|
|
2238
2472
|
generateStringKey: "placeholder"
|
|
2239
2473
|
}
|
|
2240
2474
|
}) : undefined;
|
|
2475
|
+
this.crmHttpSecret = provisionsIntegration(props.instance, "crm") && props.instance.crm.provider.kind === "http" ? new secretsmanager2.Secret(this, "CrmHttpSecret", {
|
|
2476
|
+
secretName: names.secretCrmHttp,
|
|
2477
|
+
description: `crm/1 key for ${display}'s CRM ({ api_key }); written by foundation-deploy crm-key, read only by the CRM proxy`,
|
|
2478
|
+
removalPolicy: cdk4.RemovalPolicy.DESTROY,
|
|
2479
|
+
generateSecretString: {
|
|
2480
|
+
secretStringTemplate: "{}",
|
|
2481
|
+
generateStringKey: "placeholder"
|
|
2482
|
+
}
|
|
2483
|
+
}) : undefined;
|
|
2241
2484
|
const upwork = provisionsIntegration(props.instance, "upwork") ? secretShell("UpworkSecret", names.secretUpwork, `Upwork OAuth client and token store for ${display} ({ client_id, client_secret, access_token?, refresh_token?, expires_at?, tenant_id? })`) : undefined;
|
|
2242
2485
|
const etradeClient = provisionsIntegration(props.instance, "etrade") ? new secretsmanager2.Secret(this, "EtradeClientSecret", {
|
|
2243
2486
|
secretName: names.secretEtradeClient,
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// ../capability-crm/src/proxy/errors.ts
|
|
2
|
+
class CrmOutreachUnconfiguredError extends Error {
|
|
3
|
+
constructor() {
|
|
4
|
+
super("CRM outreach is not configured");
|
|
5
|
+
this.name = "CrmOutreachUnconfiguredError";
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
class CrmRecordNotFoundError extends Error {
|
|
10
|
+
constructor() {
|
|
11
|
+
super("CRM record does not exist");
|
|
12
|
+
this.name = "CrmRecordNotFoundError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
class CrmIdempotencyConflictError extends Error {
|
|
17
|
+
constructor() {
|
|
18
|
+
super("CRM idempotency key conflicts with an earlier request");
|
|
19
|
+
this.name = "CrmIdempotencyConflictError";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
class CrmStorageError extends Error {
|
|
24
|
+
constructor() {
|
|
25
|
+
super("CRM storage operation failed");
|
|
26
|
+
this.name = "CrmStorageError";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
class CrmStaleWriteError extends Error {
|
|
30
|
+
code;
|
|
31
|
+
constructor(code) {
|
|
32
|
+
super(`CRM record changed since it was read: ${code}`);
|
|
33
|
+
this.code = code;
|
|
34
|
+
this.name = "CrmStaleWriteError";
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export { CrmOutreachUnconfiguredError, CrmRecordNotFoundError, CrmIdempotencyConflictError, CrmStorageError, CrmStaleWriteError };
|
package/dist/src/index.js
CHANGED
|
@@ -10,42 +10,46 @@ import {
|
|
|
10
10
|
FoundationPipeline,
|
|
11
11
|
NewsletterStack,
|
|
12
12
|
deployStatements
|
|
13
|
-
} from "../chunk-
|
|
13
|
+
} from "../chunk-spq5kr1t.js";
|
|
14
14
|
import {
|
|
15
15
|
BUN_VERSION,
|
|
16
16
|
LAMBDA_ENTRY_POINTS,
|
|
17
|
-
LIVE_ENDPOINT_NAME,
|
|
18
17
|
RELEASE_BUCKET_ENV,
|
|
19
18
|
RELEASE_DIR_ENV,
|
|
20
|
-
WEB_SEARCH_CONNECTOR_VERSION,
|
|
21
|
-
WEB_SEARCH_TARGET,
|
|
22
|
-
adminsFor,
|
|
23
19
|
agentImage,
|
|
24
20
|
agentImageTagRequired,
|
|
25
21
|
buildManifest,
|
|
26
|
-
capabilityEnabled,
|
|
27
|
-
configPathFor,
|
|
28
22
|
ecrRepositoryArn,
|
|
29
|
-
enabledIntegrations,
|
|
30
|
-
instanceNames,
|
|
31
23
|
lambdaBundleContext,
|
|
32
24
|
lambdaCode,
|
|
33
25
|
lambdaKey,
|
|
34
|
-
loadInstanceFile,
|
|
35
26
|
manifestKey,
|
|
36
27
|
manifestSigningPayload,
|
|
37
28
|
parseManifest,
|
|
38
|
-
provisionsIntegration,
|
|
39
29
|
releaseAccess,
|
|
40
30
|
releaseCacheDir,
|
|
41
31
|
releaseImageRepositoryArn,
|
|
42
32
|
releaseSource,
|
|
43
|
-
requiresAuthenticatedQueue,
|
|
44
33
|
skillsKey,
|
|
45
34
|
skipBundle,
|
|
46
35
|
verifyManifest
|
|
47
|
-
} from "../chunk-
|
|
36
|
+
} from "../chunk-pxhftba1.js";
|
|
48
37
|
import"../chunk-dz91avrb.js";
|
|
38
|
+
import {
|
|
39
|
+
LIVE_ENDPOINT_NAME,
|
|
40
|
+
WEB_SEARCH_CONNECTOR_VERSION,
|
|
41
|
+
WEB_SEARCH_TARGET,
|
|
42
|
+
adminsFor,
|
|
43
|
+
capabilityEnabled,
|
|
44
|
+
configPathFor,
|
|
45
|
+
enabledIntegrations,
|
|
46
|
+
instanceNames,
|
|
47
|
+
loadInstanceFile,
|
|
48
|
+
provisionsIntegration,
|
|
49
|
+
requiresAuthenticatedQueue
|
|
50
|
+
} from "../chunk-fjdgxw8h.js";
|
|
51
|
+
import"../chunk-96fjaysn.js";
|
|
52
|
+
import"../chunk-5wgwscd0.js";
|
|
49
53
|
export {
|
|
50
54
|
BUILD_TIMEOUT,
|
|
51
55
|
BUN_VERSION,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deployfoundation/foundation-deploy",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.53",
|
|
4
4
|
"description": "Deploys one Foundation instance from a published, signed release: the CDK app and the deploy tool.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"private": false,
|
|
@@ -28,7 +28,9 @@
|
|
|
28
28
|
"synth": "cdk synth --quiet"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
+
"@aws-sdk/client-dynamodb": "3.1111.0",
|
|
31
32
|
"@aws-sdk/client-kms": "3.1111.0",
|
|
33
|
+
"@aws-sdk/lib-dynamodb": "3.1111.0",
|
|
32
34
|
"aws-cdk": "2.1142.0",
|
|
33
35
|
"aws-cdk-lib": "2.270.0",
|
|
34
36
|
"constructs": "10.8.1",
|