@deployfoundation/foundation-deploy 0.2.37 → 0.2.39
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/dist/bin/app.js +3 -2
- package/dist/bin/foundation-deploy.js +503 -100
- package/dist/bin/release-account.js +1 -2
- package/dist/{chunk-9yz4hnte.js → chunk-6v0ewr5b.js} +20 -5
- package/dist/{chunk-hps5vshg.js → chunk-7rd8c1th.js} +28 -2
- package/dist/chunk-dz91avrb.js +16 -0
- package/dist/src/index.js +5 -2
- package/package.json +1 -1
- package/src/artifacts.ts +33 -0
- package/src/deploy/release.ts +1 -35
- package/src/deploy-permissions.ts +37 -2
- package/src/index.ts +2 -0
- package/src/release/trust.ts +52 -0
- package/src/stacks/api-stack.ts +2 -1
- package/src/stacks/ci-stack.ts +2 -1
- package/src/stacks/pipeline-stack.ts +2 -1
- package/dist/chunk-gmdzzrc4.js +0 -442
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
capabilityScheduledJobs,
|
|
10
10
|
crmOutreachConfigured,
|
|
11
11
|
crmPolicyFingerprint,
|
|
12
|
+
encodeProactiveChannels,
|
|
12
13
|
instanceNames,
|
|
13
14
|
isScheduledJob,
|
|
14
15
|
lambdaCode,
|
|
@@ -16,13 +17,14 @@ import {
|
|
|
16
17
|
parseInstanceConfig,
|
|
17
18
|
parseSchedule,
|
|
18
19
|
provisionsIntegration,
|
|
20
|
+
releaseAccess,
|
|
19
21
|
releaseImageRepositoryArn,
|
|
20
22
|
releaseSource,
|
|
21
23
|
requiresAuthenticatedQueue,
|
|
22
24
|
scheduledJobBindings,
|
|
23
25
|
slackCommandPrefixes,
|
|
24
26
|
webhookJobBindings
|
|
25
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-7rd8c1th.js";
|
|
26
28
|
|
|
27
29
|
// src/stacks/agent-stack.ts
|
|
28
30
|
import * as cdk from "aws-cdk-lib";
|
|
@@ -1063,7 +1065,7 @@ class FoundationApi extends cdk2.Stack {
|
|
|
1063
1065
|
},
|
|
1064
1066
|
FOUNDATION_INSTANCE: props.instance.name,
|
|
1065
1067
|
FOUNDATION_COMMAND_PREFIXES: slackCommandPrefixes(props.instance).join(","),
|
|
1066
|
-
FOUNDATION_PROACTIVE_CHANNELS: props.instance.proactive.channels
|
|
1068
|
+
FOUNDATION_PROACTIVE_CHANNELS: encodeProactiveChannels(props.instance.proactive.channels),
|
|
1067
1069
|
FOUNDATION_PROACTIVE_MAX_PER_HOUR: String(props.instance.proactive.maxPerHour),
|
|
1068
1070
|
...jobsWebhookSecret === undefined ? {} : {
|
|
1069
1071
|
FOUNDATION_WEBHOOK_JOBS: JSON.stringify(webhookJobs),
|
|
@@ -1845,6 +1847,18 @@ function deployStatements(stack, names, targets, instance) {
|
|
|
1845
1847
|
const runtimeSecret = secretArn(names.secretRuntime);
|
|
1846
1848
|
const knockOauthClient = secretArn(names.secretKnockOauthClient);
|
|
1847
1849
|
return [
|
|
1850
|
+
new iam3.PolicyStatement({
|
|
1851
|
+
sid: "ReadFoundationRelease",
|
|
1852
|
+
actions: ["s3:GetObject"],
|
|
1853
|
+
resources: [`arn:${stack.partition}:s3:::${targets.release.bucket}/releases/*`]
|
|
1854
|
+
}),
|
|
1855
|
+
...targets.release.signingKeyArn === undefined ? [] : [
|
|
1856
|
+
new iam3.PolicyStatement({
|
|
1857
|
+
sid: "VerifyReleaseSignature",
|
|
1858
|
+
actions: ["kms:Verify"],
|
|
1859
|
+
resources: [targets.release.signingKeyArn]
|
|
1860
|
+
})
|
|
1861
|
+
],
|
|
1848
1862
|
new iam3.PolicyStatement({
|
|
1849
1863
|
sid: "AssumeCdkBootstrapRoles",
|
|
1850
1864
|
actions: ["sts:AssumeRole"],
|
|
@@ -1926,7 +1940,8 @@ function deployStatements(stack, names, targets, instance) {
|
|
|
1926
1940
|
"bedrock-agentcore:GetAgentRuntime",
|
|
1927
1941
|
"bedrock-agentcore:ListAgentRuntimeVersions",
|
|
1928
1942
|
"bedrock-agentcore:GetAgentRuntimeEndpoint",
|
|
1929
|
-
"bedrock-agentcore:UpdateAgentRuntimeEndpoint"
|
|
1943
|
+
"bedrock-agentcore:UpdateAgentRuntimeEndpoint",
|
|
1944
|
+
"bedrock-agentcore:CreateAgentRuntimeEndpoint"
|
|
1930
1945
|
],
|
|
1931
1946
|
resources: [
|
|
1932
1947
|
`arn:${stack.partition}:bedrock-agentcore:${stack.region}:${stack.account}:runtime/${names.runtimeName}*`,
|
|
@@ -1969,7 +1984,7 @@ class FoundationCi extends cdk3.Stack {
|
|
|
1969
1984
|
}),
|
|
1970
1985
|
inlinePolicies: {
|
|
1971
1986
|
FoundationDeploy: new iam4.PolicyDocument({
|
|
1972
|
-
statements: deployStatements(this, names, { bucket: props.bucket, dataKey: props.dataKey }, props.instance)
|
|
1987
|
+
statements: deployStatements(this, names, { bucket: props.bucket, dataKey: props.dataKey, release: releaseAccess(this) }, props.instance)
|
|
1973
1988
|
})
|
|
1974
1989
|
}
|
|
1975
1990
|
});
|
|
@@ -3452,7 +3467,7 @@ class FoundationPipeline extends cdk8.Stack {
|
|
|
3452
3467
|
assumedBy: new iam9.ServicePrincipal("codebuild.amazonaws.com"),
|
|
3453
3468
|
inlinePolicies: {
|
|
3454
3469
|
FoundationDeploy: new iam9.PolicyDocument({
|
|
3455
|
-
statements: deployStatements(this, names, { bucket: props.bucket, dataKey: props.dataKey }, props.instance)
|
|
3470
|
+
statements: deployStatements(this, names, { bucket: props.bucket, dataKey: props.dataKey, release: releaseAccess(this) }, props.instance)
|
|
3456
3471
|
})
|
|
3457
3472
|
}
|
|
3458
3473
|
});
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_RELEASE_BUCKET,
|
|
3
|
+
configuredKeyArn
|
|
4
|
+
} from "./chunk-dz91avrb.js";
|
|
5
|
+
|
|
1
6
|
// ../core/src/instance.ts
|
|
2
7
|
import { parse as parseYaml } from "yaml";
|
|
3
8
|
import { z as z2 } from "zod";
|
|
@@ -864,6 +869,12 @@ function requiredSlackCommandSuffixes(enabled) {
|
|
|
864
869
|
return [...suffixes].sort();
|
|
865
870
|
}
|
|
866
871
|
|
|
872
|
+
// ../core/src/proactive.ts
|
|
873
|
+
var PROACTIVE_CHANNEL_MODES = ["triage", "alerts"];
|
|
874
|
+
function encodeProactiveChannels(channels) {
|
|
875
|
+
return channels.map((channel) => channel.mode === "triage" ? channel.id : `${channel.id}:${channel.mode}`).join(",");
|
|
876
|
+
}
|
|
877
|
+
|
|
867
878
|
// ../core/src/instance.ts
|
|
868
879
|
var PENDING = "TBD";
|
|
869
880
|
var CUSTOMIZATION_ARTIFACT_NAME = "Customization";
|
|
@@ -915,8 +926,15 @@ var RawCustomization = z2.object({
|
|
|
915
926
|
path: z2.string().refine(normalizedRelativePosixPath, "customization.path must be a normalized relative POSIX path"),
|
|
916
927
|
required: z2.boolean().default(true)
|
|
917
928
|
}).strict();
|
|
929
|
+
var ProactiveChannelId = z2.string().regex(/^[CG][A-Z0-9]{8,}$/);
|
|
930
|
+
var RawProactiveChannel = z2.union([
|
|
931
|
+
ProactiveChannelId.transform((id) => ({ id, mode: "triage" })),
|
|
932
|
+
z2.object({ id: ProactiveChannelId, mode: z2.enum(PROACTIVE_CHANNEL_MODES).default("triage") }).strict()
|
|
933
|
+
]);
|
|
918
934
|
var RawProactive = z2.object({
|
|
919
|
-
channels: z2.array(
|
|
935
|
+
channels: z2.array(RawProactiveChannel).default([]).refine((channels) => new Set(channels.map((c) => c.id)).size === channels.length, {
|
|
936
|
+
message: "a channel may be listed only once"
|
|
937
|
+
}),
|
|
920
938
|
maxPerHour: z2.number().int().min(1).max(500).default(20)
|
|
921
939
|
}).strict().default({ channels: [] });
|
|
922
940
|
var RawRuntime = z2.object({
|
|
@@ -1803,6 +1821,14 @@ function releaseSource(scope) {
|
|
|
1803
1821
|
throw new Error(`-c releaseBucket=<name> is required when deploying a release (or set ${RELEASE_BUCKET_ENV}); it is the bucket holding ${manifest.lambda.gateway?.key ?? "the release artifacts"}`);
|
|
1804
1822
|
return { version: manifest.version, bucket, manifest };
|
|
1805
1823
|
}
|
|
1824
|
+
function releaseAccess(scope, env = process.env) {
|
|
1825
|
+
const context = scope.node.tryGetContext("releaseBucket");
|
|
1826
|
+
const fromContext = typeof context === "string" && context !== "" ? context : undefined;
|
|
1827
|
+
const fromEnv = env[RELEASE_BUCKET_ENV];
|
|
1828
|
+
const bucket = fromContext ?? (fromEnv !== undefined && fromEnv !== "" ? fromEnv : DEFAULT_RELEASE_BUCKET);
|
|
1829
|
+
const signingKeyArn = configuredKeyArn(env);
|
|
1830
|
+
return { bucket, ...signingKeyArn === undefined ? {} : { signingKeyArn } };
|
|
1831
|
+
}
|
|
1806
1832
|
function releaseBucket(scope, source) {
|
|
1807
1833
|
const stack = cdk.Stack.of(scope);
|
|
1808
1834
|
const existing = stack.node.tryFindChild("FoundationReleaseBucket");
|
|
@@ -1886,4 +1912,4 @@ function ecrRepositoryArn(repositoryUri) {
|
|
|
1886
1912
|
return `arn:aws:ecr:${region}:${account}:repository/${name}`;
|
|
1887
1913
|
}
|
|
1888
1914
|
|
|
1889
|
-
export { parseSchedule, BASE_SLACK_EVENTS, CAPABILITY_IDS, requiredGithubPermissions, requiredSlackScopes, requiredSlackCommands, CUSTOMIZATION_ARTIFACT_NAME, newsletterManifestFor, FOUNDATION_VERSION_RE, versionedConfigKey, instanceNames, slackCommandPrefix, slackCommandPrefixes, isScheduledJob, isWebhookJob, webhookJobBindings, scheduledJobBindings, jobWebhookKey, capabilityScheduledJobs, crmOutreachConfigured, crmPolicyFingerprint, parseInstanceConfig, loadInstanceFile, configPathFor, capabilityEnabled, requiresAuthenticatedQueue, enabledIntegrations, provisionsIntegration, adminsFor, WEB_SEARCH_TARGET, WEB_SEARCH_CONNECTOR_VERSION, LIVE_ENDPOINT_NAME, RELEASE_VERSION_RE, parseManifest, manifestKey, lambdaKey, skillsKey, buildManifest, manifestSigningPayload, verifyManifest, lambdaBundleContext, BUN_VERSION, LAMBDA_ENTRY_POINTS, skipBundle, RELEASE_BUCKET_ENV, RELEASE_DIR_ENV, releaseCacheDir, releaseSource, lambdaCode, agentImage, agentImageTagRequired, releaseImageRepositoryArn, ecrRepositoryArn };
|
|
1915
|
+
export { parseSchedule, BASE_SLACK_EVENTS, CAPABILITY_IDS, requiredGithubPermissions, requiredSlackScopes, requiredSlackCommands, encodeProactiveChannels, CUSTOMIZATION_ARTIFACT_NAME, newsletterManifestFor, FOUNDATION_VERSION_RE, versionedConfigKey, instanceNames, slackCommandPrefix, slackCommandPrefixes, isScheduledJob, isWebhookJob, webhookJobBindings, scheduledJobBindings, jobWebhookKey, capabilityScheduledJobs, crmOutreachConfigured, crmPolicyFingerprint, parseInstanceConfig, loadInstanceFile, configPathFor, capabilityEnabled, requiresAuthenticatedQueue, enabledIntegrations, provisionsIntegration, adminsFor, WEB_SEARCH_TARGET, WEB_SEARCH_CONNECTOR_VERSION, LIVE_ENDPOINT_NAME, RELEASE_VERSION_RE, parseManifest, manifestKey, lambdaKey, skillsKey, buildManifest, manifestSigningPayload, verifyManifest, lambdaBundleContext, BUN_VERSION, LAMBDA_ENTRY_POINTS, skipBundle, RELEASE_BUCKET_ENV, RELEASE_DIR_ENV, releaseCacheDir, releaseSource, releaseAccess, lambdaCode, agentImage, agentImageTagRequired, releaseImageRepositoryArn, ecrRepositoryArn };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// src/release/trust.ts
|
|
2
|
+
var DEFAULT_RELEASE_BUCKET = "foundry41-foundation-releases";
|
|
3
|
+
var RELEASE_KMS_KEY_ARN_ENV = "FOUNDATION_RELEASE_KMS_KEY_ARN";
|
|
4
|
+
var RELEASE_KMS_KEY_ARN = "arn:aws:kms:us-east-1:793593623536:key/fba7d9f3-e9b6-49ae-b4a5-c8c5f1429231";
|
|
5
|
+
function configuredKeyArn(env = process.env) {
|
|
6
|
+
const configured = env[RELEASE_KMS_KEY_ARN_ENV] ?? RELEASE_KMS_KEY_ARN;
|
|
7
|
+
return configured === undefined || configured === "" ? undefined : configured;
|
|
8
|
+
}
|
|
9
|
+
function expectedKeyArn(env = process.env) {
|
|
10
|
+
const configured = configuredKeyArn(env);
|
|
11
|
+
if (configured === undefined)
|
|
12
|
+
throw new Error(`no Foundation release signing key is configured; set ${RELEASE_KMS_KEY_ARN_ENV} to the key ARN published with the release`);
|
|
13
|
+
return configured;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export { DEFAULT_RELEASE_BUCKET, configuredKeyArn, expectedKeyArn };
|
package/dist/src/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
FoundationPipeline,
|
|
11
11
|
NewsletterStack,
|
|
12
12
|
deployStatements
|
|
13
|
-
} from "../chunk-
|
|
13
|
+
} from "../chunk-6v0ewr5b.js";
|
|
14
14
|
import {
|
|
15
15
|
BUN_VERSION,
|
|
16
16
|
LAMBDA_ENTRY_POINTS,
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
manifestSigningPayload,
|
|
37
37
|
parseManifest,
|
|
38
38
|
provisionsIntegration,
|
|
39
|
+
releaseAccess,
|
|
39
40
|
releaseCacheDir,
|
|
40
41
|
releaseImageRepositoryArn,
|
|
41
42
|
releaseSource,
|
|
@@ -43,7 +44,8 @@ import {
|
|
|
43
44
|
skillsKey,
|
|
44
45
|
skipBundle,
|
|
45
46
|
verifyManifest
|
|
46
|
-
} from "../chunk-
|
|
47
|
+
} from "../chunk-7rd8c1th.js";
|
|
48
|
+
import"../chunk-dz91avrb.js";
|
|
47
49
|
export {
|
|
48
50
|
BUILD_TIMEOUT,
|
|
49
51
|
BUN_VERSION,
|
|
@@ -80,6 +82,7 @@ export {
|
|
|
80
82
|
instanceNames as namesFor,
|
|
81
83
|
parseManifest,
|
|
82
84
|
provisionsIntegration,
|
|
85
|
+
releaseAccess,
|
|
83
86
|
releaseCacheDir,
|
|
84
87
|
releaseImageRepositoryArn,
|
|
85
88
|
releaseSource,
|
package/package.json
CHANGED
package/src/artifacts.ts
CHANGED
|
@@ -41,6 +41,7 @@ import * as s3 from "aws-cdk-lib/aws-s3";
|
|
|
41
41
|
import type { Construct } from "constructs";
|
|
42
42
|
import { lambdaBundleContext } from "./lambda-bundle-context.ts";
|
|
43
43
|
import { type ReleaseManifest, parseManifest } from "./release/manifest.ts";
|
|
44
|
+
import { DEFAULT_RELEASE_BUCKET, configuredKeyArn } from "./release/trust.ts";
|
|
44
45
|
|
|
45
46
|
/** Pinned Bun used to bundle every Lambda (and the docker fallback image tag). */
|
|
46
47
|
export const BUN_VERSION = "1.4.0";
|
|
@@ -174,6 +175,38 @@ export function releaseSource(scope: Construct): ReleaseSource | undefined {
|
|
|
174
175
|
return { version: manifest.version, bucket, manifest };
|
|
175
176
|
}
|
|
176
177
|
|
|
178
|
+
/**
|
|
179
|
+
* What a deployer of this app reads from Foundation's release account: the
|
|
180
|
+
* bucket the tool fetches `releases/*` from and the key whose signature it
|
|
181
|
+
* checks. The deployer roles (`deploy-permissions.ts`) are granted exactly
|
|
182
|
+
* these, cross-account, so a CodePipeline or GitHub Actions deploy can verify
|
|
183
|
+
* a release the way an admin's laptop can.
|
|
184
|
+
*
|
|
185
|
+
* Resolved the way `foundation-deploy` resolves them — `-c releaseBucket`
|
|
186
|
+
* (which the tool passes on a release deploy) or `FOUNDATION_RELEASE_BUCKET`,
|
|
187
|
+
* else the default bucket; `FOUNDATION_RELEASE_KMS_KEY_ARN`, else the pinned
|
|
188
|
+
* key — and resolved on EVERY synth, release or local build, so the grant does
|
|
189
|
+
* not come and go with how the last deploy was run.
|
|
190
|
+
*/
|
|
191
|
+
export interface ReleaseAccess {
|
|
192
|
+
bucket: string;
|
|
193
|
+
/** Absent only when no signing key is configured at all; nothing is verified then. */
|
|
194
|
+
signingKeyArn?: string;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function releaseAccess(
|
|
198
|
+
scope: Construct,
|
|
199
|
+
env: Record<string, string | undefined> = process.env,
|
|
200
|
+
): ReleaseAccess {
|
|
201
|
+
const context: unknown = scope.node.tryGetContext("releaseBucket");
|
|
202
|
+
const fromContext = typeof context === "string" && context !== "" ? context : undefined;
|
|
203
|
+
const fromEnv = env[RELEASE_BUCKET_ENV];
|
|
204
|
+
const bucket =
|
|
205
|
+
fromContext ?? (fromEnv !== undefined && fromEnv !== "" ? fromEnv : DEFAULT_RELEASE_BUCKET);
|
|
206
|
+
const signingKeyArn = configuredKeyArn(env);
|
|
207
|
+
return { bucket, ...(signingKeyArn === undefined ? {} : { signingKeyArn }) };
|
|
208
|
+
}
|
|
209
|
+
|
|
177
210
|
/**
|
|
178
211
|
* The release bucket as one imported construct per stack. Every Lambda in a
|
|
179
212
|
* stack references the same bucket, and importing it once keeps the template's
|
package/src/deploy/release.ts
CHANGED
|
@@ -29,34 +29,10 @@ import {
|
|
|
29
29
|
manifestKey,
|
|
30
30
|
verifyManifest,
|
|
31
31
|
} from "../release/manifest.ts";
|
|
32
|
+
import { DEFAULT_RELEASE_BUCKET, expectedKeyArn } from "../release/trust.ts";
|
|
32
33
|
import { type AwsCallContext, aws } from "./aws.ts";
|
|
33
34
|
import { FOUNDATION_ROOT, INFRA_ROOT } from "./paths.ts";
|
|
34
35
|
|
|
35
|
-
/**
|
|
36
|
-
* The bucket a release is fetched from when nothing says otherwise.
|
|
37
|
-
*
|
|
38
|
-
* One name, owned by Foundry 41, created by `release-account-stack.ts`. A
|
|
39
|
-
* mirror is `--release-bucket` or `FOUNDATION_RELEASE_BUCKET`, and the
|
|
40
|
-
* signature is what makes a mirror safe to use.
|
|
41
|
-
*/
|
|
42
|
-
export const DEFAULT_RELEASE_BUCKET = "foundry41-foundation-releases";
|
|
43
|
-
|
|
44
|
-
/** The env var that names the key a deployer will accept a manifest from. */
|
|
45
|
-
export const RELEASE_KMS_KEY_ARN_ENV = "FOUNDATION_RELEASE_KMS_KEY_ARN";
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* The Foundation release signing key, in the Foundry 41 release account
|
|
49
|
-
* (793593623536), created by `ReleaseAccountStack` on 2026-09-20.
|
|
50
|
-
*
|
|
51
|
-
* This constant is what makes signature verification mean anything: a
|
|
52
|
-
* deployer refuses a manifest signed by any other key. Changing it rotates
|
|
53
|
-
* the root of trust for every customer, so it changes only with a key
|
|
54
|
-
* rotation and a release that says so. {@link RELEASE_KMS_KEY_ARN_ENV}
|
|
55
|
-
* overrides it, for a private mirror or a test key.
|
|
56
|
-
*/
|
|
57
|
-
export const RELEASE_KMS_KEY_ARN: string | undefined =
|
|
58
|
-
"arn:aws:kms:us-east-1:793593623536:key/fba7d9f3-e9b6-49ae-b4a5-c8c5f1429231";
|
|
59
|
-
|
|
60
36
|
/** What the caller asked for, before anything has been fetched. */
|
|
61
37
|
export interface ReleaseRequest {
|
|
62
38
|
/** `vX.Y.Z`, when the caller named a version or one was implied. */
|
|
@@ -149,16 +125,6 @@ export function releaseRequest(
|
|
|
149
125
|
return { version, bucket };
|
|
150
126
|
}
|
|
151
127
|
|
|
152
|
-
/** The key ARN a deployer will accept a manifest from. */
|
|
153
|
-
export function expectedKeyArn(env: Record<string, string | undefined> = process.env): string {
|
|
154
|
-
const configured = env[RELEASE_KMS_KEY_ARN_ENV] ?? RELEASE_KMS_KEY_ARN;
|
|
155
|
-
if (configured === undefined || configured === "")
|
|
156
|
-
throw new Error(
|
|
157
|
-
`no Foundation release signing key is configured; set ${RELEASE_KMS_KEY_ARN_ENV} to the key ARN published with the release`,
|
|
158
|
-
);
|
|
159
|
-
return configured;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
128
|
export interface ResolveReleaseDeps {
|
|
163
129
|
/** Defaults to the SDK-backed verifier for the expected key. */
|
|
164
130
|
kms?: KmsVerifier;
|
|
@@ -17,6 +17,7 @@ import type * as cdk from "aws-cdk-lib";
|
|
|
17
17
|
import * as iam from "aws-cdk-lib/aws-iam";
|
|
18
18
|
import type * as kms from "aws-cdk-lib/aws-kms";
|
|
19
19
|
import type * as s3 from "aws-cdk-lib/aws-s3";
|
|
20
|
+
import type { ReleaseAccess } from "./artifacts.ts";
|
|
20
21
|
import { type Instance, type InstanceNames, provisionsIntegration } from "./names.ts";
|
|
21
22
|
|
|
22
23
|
export interface DeployTargets {
|
|
@@ -24,6 +25,13 @@ export interface DeployTargets {
|
|
|
24
25
|
bucket: s3.IBucket;
|
|
25
26
|
/** The CMK that bucket is encrypted with. */
|
|
26
27
|
dataKey: kms.IKey;
|
|
28
|
+
/**
|
|
29
|
+
* Foundation's release bucket and signing key, in the release account
|
|
30
|
+
* (`releaseAccess()` in `artifacts.ts`). Cross-account access needs BOTH
|
|
31
|
+
* sides to allow it: the release account's bucket and key policies name the
|
|
32
|
+
* customer account, and the statements below are this account's half.
|
|
33
|
+
*/
|
|
34
|
+
release: ReleaseAccess;
|
|
27
35
|
}
|
|
28
36
|
|
|
29
37
|
/**
|
|
@@ -49,6 +57,30 @@ export function deployStatements(
|
|
|
49
57
|
const knockOauthClient = secretArn(names.secretKnockOauthClient);
|
|
50
58
|
|
|
51
59
|
return [
|
|
60
|
+
// A release deploy, before anything else: `aws s3 cp` of
|
|
61
|
+
// `releases/<v>/manifest.json`, then of every Lambda zip and
|
|
62
|
+
// `skills.tar.gz` the manifest names (the digest check in `verifyRelease`),
|
|
63
|
+
// and of `skills.tar.gz` again for the config sync (`unpackReleaseSkills`).
|
|
64
|
+
// HeadObject is authorized by s3:GetObject. The bucket is SSE-S3, so there
|
|
65
|
+
// is no key to grant for it.
|
|
66
|
+
new iam.PolicyStatement({
|
|
67
|
+
sid: "ReadFoundationRelease",
|
|
68
|
+
actions: ["s3:GetObject"],
|
|
69
|
+
resources: [`arn:${stack.partition}:s3:::${targets.release.bucket}/releases/*`],
|
|
70
|
+
}),
|
|
71
|
+
// ...and the manifest's signature, checked with one KMS `Verify` against
|
|
72
|
+
// the pinned key (`src/release/kms.ts`); nothing calls GetPublicKey or
|
|
73
|
+
// DescribeKey. By key ARN, not alias: an identity policy cannot name
|
|
74
|
+
// another account's key by its alias.
|
|
75
|
+
...(targets.release.signingKeyArn === undefined
|
|
76
|
+
? []
|
|
77
|
+
: [
|
|
78
|
+
new iam.PolicyStatement({
|
|
79
|
+
sid: "VerifyReleaseSignature",
|
|
80
|
+
actions: ["kms:Verify"],
|
|
81
|
+
resources: [targets.release.signingKeyArn],
|
|
82
|
+
}),
|
|
83
|
+
]),
|
|
52
84
|
// How `cdk deploy` does everything else: it assumes the bootstrap roles for
|
|
53
85
|
// lookup, asset publishing and the CloudFormation call.
|
|
54
86
|
new iam.PolicyStatement({
|
|
@@ -140,8 +172,10 @@ export function deployStatements(
|
|
|
140
172
|
actions: ["kms:Encrypt", "kms:Decrypt", "kms:GenerateDataKey*"],
|
|
141
173
|
resources: [targets.dataKey.keyArn],
|
|
142
174
|
}),
|
|
143
|
-
// `foundation-deploy post-deploy
|
|
144
|
-
// `_fs_probe`) against DEFAULT, then promotion
|
|
175
|
+
// `foundation-deploy post-deploy` (and `deploy`'s inline promotion): the
|
|
176
|
+
// smoke test (`_smoke_test`, `_fs_probe`) against DEFAULT, then promotion
|
|
177
|
+
// of the new version onto `live`, which CREATES `live` when an instance
|
|
178
|
+
// brought up by `deploy` alone has none yet (`promoteEndpoint`).
|
|
145
179
|
new iam.PolicyStatement({
|
|
146
180
|
sid: "SmokeTestRuntime",
|
|
147
181
|
actions: [
|
|
@@ -150,6 +184,7 @@ export function deployStatements(
|
|
|
150
184
|
"bedrock-agentcore:ListAgentRuntimeVersions",
|
|
151
185
|
"bedrock-agentcore:GetAgentRuntimeEndpoint",
|
|
152
186
|
"bedrock-agentcore:UpdateAgentRuntimeEndpoint",
|
|
187
|
+
"bedrock-agentcore:CreateAgentRuntimeEndpoint",
|
|
153
188
|
],
|
|
154
189
|
resources: [
|
|
155
190
|
`arn:${stack.partition}:bedrock-agentcore:${stack.region}:${stack.account}:runtime/${names.runtimeName}*`,
|
package/src/index.ts
CHANGED
|
@@ -4,11 +4,13 @@ export {
|
|
|
4
4
|
RELEASE_BUCKET_ENV,
|
|
5
5
|
RELEASE_DIR_ENV,
|
|
6
6
|
type LambdaArtifactId,
|
|
7
|
+
type ReleaseAccess,
|
|
7
8
|
type ReleaseSource,
|
|
8
9
|
agentImage,
|
|
9
10
|
agentImageTagRequired,
|
|
10
11
|
ecrRepositoryArn,
|
|
11
12
|
lambdaCode,
|
|
13
|
+
releaseAccess,
|
|
12
14
|
releaseCacheDir,
|
|
13
15
|
releaseImageRepositoryArn,
|
|
14
16
|
releaseSource,
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a deployer gets a release from, and whose signature it accepts.
|
|
3
|
+
*
|
|
4
|
+
* Pure, like `manifest.ts`: the deploy tool reads these to fetch and verify a
|
|
5
|
+
* release, and the CDK app reads them to give the deployer roles
|
|
6
|
+
* (`deploy-permissions.ts`) the release-account permissions that fetch needs.
|
|
7
|
+
* Both must resolve to the same bucket and the same key, so the resolution
|
|
8
|
+
* lives here once rather than in each.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The bucket a release is fetched from when nothing says otherwise.
|
|
13
|
+
*
|
|
14
|
+
* One name, owned by Foundry 41, created by `release-account-stack.ts`. A
|
|
15
|
+
* mirror is `--release-bucket` or `FOUNDATION_RELEASE_BUCKET`, and the
|
|
16
|
+
* signature is what makes a mirror safe to use.
|
|
17
|
+
*/
|
|
18
|
+
export const DEFAULT_RELEASE_BUCKET = "foundry41-foundation-releases";
|
|
19
|
+
|
|
20
|
+
/** The env var that names the key a deployer will accept a manifest from. */
|
|
21
|
+
export const RELEASE_KMS_KEY_ARN_ENV = "FOUNDATION_RELEASE_KMS_KEY_ARN";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The Foundation release signing key, in the Foundry 41 release account
|
|
25
|
+
* (793593623536), created by `ReleaseAccountStack` on 2026-09-20.
|
|
26
|
+
*
|
|
27
|
+
* This constant is what makes signature verification mean anything: a
|
|
28
|
+
* deployer refuses a manifest signed by any other key. Changing it rotates
|
|
29
|
+
* the root of trust for every customer, so it changes only with a key
|
|
30
|
+
* rotation and a release that says so. {@link RELEASE_KMS_KEY_ARN_ENV}
|
|
31
|
+
* overrides it, for a private mirror or a test key.
|
|
32
|
+
*/
|
|
33
|
+
export const RELEASE_KMS_KEY_ARN: string | undefined =
|
|
34
|
+
"arn:aws:kms:us-east-1:793593623536:key/fba7d9f3-e9b6-49ae-b4a5-c8c5f1429231";
|
|
35
|
+
|
|
36
|
+
/** The key ARN a deployer will accept a manifest from, or `undefined` when none is configured. */
|
|
37
|
+
export function configuredKeyArn(
|
|
38
|
+
env: Record<string, string | undefined> = process.env,
|
|
39
|
+
): string | undefined {
|
|
40
|
+
const configured = env[RELEASE_KMS_KEY_ARN_ENV] ?? RELEASE_KMS_KEY_ARN;
|
|
41
|
+
return configured === undefined || configured === "" ? undefined : configured;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The key ARN a deployer will accept a manifest from. Throws when none is configured. */
|
|
45
|
+
export function expectedKeyArn(env: Record<string, string | undefined> = process.env): string {
|
|
46
|
+
const configured = configuredKeyArn(env);
|
|
47
|
+
if (configured === undefined)
|
|
48
|
+
throw new Error(
|
|
49
|
+
`no Foundation release signing key is configured; set ${RELEASE_KMS_KEY_ARN_ENV} to the key ARN published with the release`,
|
|
50
|
+
);
|
|
51
|
+
return configured;
|
|
52
|
+
}
|
package/src/stacks/api-stack.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
capabilityScheduledJobs,
|
|
10
10
|
crmOutreachConfigured,
|
|
11
11
|
crmPolicyFingerprint,
|
|
12
|
+
encodeProactiveChannels,
|
|
12
13
|
isScheduledJob,
|
|
13
14
|
parseInstanceConfig,
|
|
14
15
|
scheduledJobBindings,
|
|
@@ -479,7 +480,7 @@ export class FoundationApi extends cdk.Stack {
|
|
|
479
480
|
}),
|
|
480
481
|
FOUNDATION_INSTANCE: props.instance.name,
|
|
481
482
|
FOUNDATION_COMMAND_PREFIXES: slackCommandPrefixes(props.instance).join(","),
|
|
482
|
-
FOUNDATION_PROACTIVE_CHANNELS: props.instance.proactive.channels
|
|
483
|
+
FOUNDATION_PROACTIVE_CHANNELS: encodeProactiveChannels(props.instance.proactive.channels),
|
|
483
484
|
FOUNDATION_PROACTIVE_MAX_PER_HOUR: String(props.instance.proactive.maxPerHour),
|
|
484
485
|
...(jobsWebhookSecret === undefined
|
|
485
486
|
? {}
|
package/src/stacks/ci-stack.ts
CHANGED
|
@@ -3,6 +3,7 @@ import * as iam from "aws-cdk-lib/aws-iam";
|
|
|
3
3
|
import type * as kms from "aws-cdk-lib/aws-kms";
|
|
4
4
|
import type * as s3 from "aws-cdk-lib/aws-s3";
|
|
5
5
|
import type { Construct } from "constructs";
|
|
6
|
+
import { releaseAccess } from "../artifacts.ts";
|
|
6
7
|
import { deployStatements } from "../deploy-permissions.ts";
|
|
7
8
|
import { type Instance, namesFor } from "../names.ts";
|
|
8
9
|
|
|
@@ -84,7 +85,7 @@ export class FoundationCi extends cdk.Stack {
|
|
|
84
85
|
statements: deployStatements(
|
|
85
86
|
this,
|
|
86
87
|
names,
|
|
87
|
-
{ bucket: props.bucket, dataKey: props.dataKey },
|
|
88
|
+
{ bucket: props.bucket, dataKey: props.dataKey, release: releaseAccess(this) },
|
|
88
89
|
props.instance,
|
|
89
90
|
),
|
|
90
91
|
}),
|
|
@@ -40,6 +40,7 @@ import type * as s3 from "aws-cdk-lib/aws-s3";
|
|
|
40
40
|
import * as sns from "aws-cdk-lib/aws-sns";
|
|
41
41
|
import * as subscriptions from "aws-cdk-lib/aws-sns-subscriptions";
|
|
42
42
|
import type { Construct } from "constructs";
|
|
43
|
+
import { releaseAccess } from "../artifacts.ts";
|
|
43
44
|
import { deployStatements } from "../deploy-permissions.ts";
|
|
44
45
|
import { type Instance, namesFor } from "../names.ts";
|
|
45
46
|
|
|
@@ -101,7 +102,7 @@ export class FoundationPipeline extends cdk.Stack {
|
|
|
101
102
|
statements: deployStatements(
|
|
102
103
|
this,
|
|
103
104
|
names,
|
|
104
|
-
{ bucket: props.bucket, dataKey: props.dataKey },
|
|
105
|
+
{ bucket: props.bucket, dataKey: props.dataKey, release: releaseAccess(this) },
|
|
105
106
|
props.instance,
|
|
106
107
|
),
|
|
107
108
|
}),
|