@deployfoundation/foundation-deploy 0.1.0

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.
Files changed (54) hide show
  1. package/README.md +174 -0
  2. package/agent-image/Dockerfile +254 -0
  3. package/agent-image/bin/aws +36 -0
  4. package/agent-image/bin/gh +193 -0
  5. package/agent-image/bin/git-credential-sky +89 -0
  6. package/agent-image/security-overlay.yml +176 -0
  7. package/cdk.json +6 -0
  8. package/dist/bin/app.js +112 -0
  9. package/dist/bin/foundation-deploy.js +1906 -0
  10. package/dist/bin/release-account.js +154 -0
  11. package/dist/chunk-4aye5cee.js +2416 -0
  12. package/dist/chunk-9ddxyvq2.js +1455 -0
  13. package/dist/chunk-v7tz8g50.js +428 -0
  14. package/dist/src/index.js +88 -0
  15. package/package.json +38 -0
  16. package/pipeline/buildspec.yml +34 -0
  17. package/src/artifacts.ts +318 -0
  18. package/src/deploy/assets/github-app-manifest.yml +29 -0
  19. package/src/deploy/assets/slack-app-manifest.yml +95 -0
  20. package/src/deploy/aws.ts +265 -0
  21. package/src/deploy/cli.ts +212 -0
  22. package/src/deploy/config-sync.ts +93 -0
  23. package/src/deploy/config.ts +29 -0
  24. package/src/deploy/deploy.ts +566 -0
  25. package/src/deploy/endpoint.ts +242 -0
  26. package/src/deploy/github-app-create.ts +154 -0
  27. package/src/deploy/github-app-manifest.ts +53 -0
  28. package/src/deploy/image.ts +80 -0
  29. package/src/deploy/instance.ts +87 -0
  30. package/src/deploy/license-cache.ts +47 -0
  31. package/src/deploy/license.ts +272 -0
  32. package/src/deploy/paths.ts +65 -0
  33. package/src/deploy/post-deploy.ts +97 -0
  34. package/src/deploy/release.ts +282 -0
  35. package/src/deploy/runtime-secret.ts +241 -0
  36. package/src/deploy/setup.ts +393 -0
  37. package/src/deploy/sh.ts +74 -0
  38. package/src/deploy/slack-manifest.ts +112 -0
  39. package/src/deploy/stage-customization.ts +224 -0
  40. package/src/deploy/tracing.ts +243 -0
  41. package/src/deploy-permissions.ts +165 -0
  42. package/src/index.ts +60 -0
  43. package/src/lambda-bundle-context.ts +64 -0
  44. package/src/names.ts +170 -0
  45. package/src/release/kms.ts +86 -0
  46. package/src/release/manifest.ts +265 -0
  47. package/src/stacks/agent-stack.ts +938 -0
  48. package/src/stacks/api-stack.ts +1005 -0
  49. package/src/stacks/ci-stack.ts +96 -0
  50. package/src/stacks/data-stack.ts +446 -0
  51. package/src/stacks/network-stack.ts +282 -0
  52. package/src/stacks/newsletter-stack.ts +572 -0
  53. package/src/stacks/pipeline-stack.ts +242 -0
  54. package/src/stacks/release-account-stack.ts +229 -0
@@ -0,0 +1,282 @@
1
+ /**
2
+ * Deploying a published release rather than this checkout.
3
+ *
4
+ * A customer's pipeline runs one line — `npx @deployfoundation/foundation-deploy@X.Y.Z
5
+ * deploy --instance .foundation/instance.yaml` — and that pinned version *is*
6
+ * the release. Nothing is built: the manifest is fetched from Foundation's
7
+ * release bucket, its KMS signature and every artifact digest are checked, and
8
+ * only then is the CDK app invoked with `-c releaseManifest=…`.
9
+ *
10
+ * Which mode a run is in is not a flag a pipeline has to remember:
11
+ *
12
+ * - `--release <version>` or `--manifest <path|s3://…>` — explicit.
13
+ * - neither, and this install has no Foundation workspace to build from
14
+ * (the published npm package) — the release matching the tool's own
15
+ * version.
16
+ * - neither, in a Foundation checkout — the local build, unchanged.
17
+ *
18
+ * The verification lives in `src/release/manifest.ts` and is pure; this module
19
+ * is the part that talks to S3 and KMS.
20
+ */
21
+ import { existsSync, mkdirSync, readFileSync } from "node:fs";
22
+ import { dirname, isAbsolute, join, resolve } from "node:path";
23
+ import { RELEASE_BUCKET_ENV, releaseCacheDir } from "../artifacts.ts";
24
+ import { kmsVerifier } from "../release/kms.ts";
25
+ import {
26
+ type KmsVerifier,
27
+ RELEASE_VERSION_RE,
28
+ type ReleaseManifest,
29
+ manifestKey,
30
+ verifyManifest,
31
+ } from "../release/manifest.ts";
32
+ import { type AwsCallContext, aws } from "./aws.ts";
33
+ import { FOUNDATION_ROOT, INFRA_ROOT } from "./paths.ts";
34
+
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
+ /** What the caller asked for, before anything has been fetched. */
61
+ export interface ReleaseRequest {
62
+ /** `vX.Y.Z`, when the caller named a version or one was implied. */
63
+ version?: string;
64
+ /** `--manifest`: a local path or an `s3://bucket/key` URI. */
65
+ manifestRef?: string;
66
+ bucket: string;
67
+ }
68
+
69
+ /** A release that has been fetched and verified (or, in a dry run, planned). */
70
+ export interface ResolvedRelease {
71
+ version: string;
72
+ bucket: string;
73
+ /** The local manifest file the CDK app is pointed at. */
74
+ manifestPath: string;
75
+ /** Present once verified; absent in a dry run, which fetches nothing. */
76
+ manifest?: ReleaseManifest;
77
+ }
78
+
79
+ function flag(args: readonly string[], name: string): string | undefined {
80
+ const index = args.indexOf(name);
81
+ const value = index === -1 ? undefined : args[index + 1];
82
+ if (value?.startsWith("-") === true)
83
+ throw new Error(`${name} needs a value, e.g. ${name} ${name === "--release" ? "v0.1.0" : "…"}`);
84
+ const inline = args.find((arg) => arg.startsWith(`${name}=`));
85
+ return value ?? (inline === undefined ? undefined : inline.slice(name.length + 1));
86
+ }
87
+
88
+ /** This tool's own version, which is the release a published copy deploys. */
89
+ export function toolVersion(infraRoot: string = INFRA_ROOT): string {
90
+ const { version } = JSON.parse(readFileSync(join(infraRoot, "package.json"), "utf8")) as {
91
+ version: string;
92
+ };
93
+ return version;
94
+ }
95
+
96
+ /**
97
+ * Is there a Foundation workspace here to build Lambda bundles from?
98
+ *
99
+ * The published package carries the CDK app and the deploy tool, not the
100
+ * product's source. Its absence is what makes a bare `deploy` a release
101
+ * deploy, so no pipeline has to pass a flag that duplicates the version it
102
+ * already pinned in `npx`.
103
+ */
104
+ export function hasFoundationWorkspace(foundationRoot: string = FOUNDATION_ROOT): boolean {
105
+ return existsSync(join(foundationRoot, "packages", "gateway", "package.json"));
106
+ }
107
+
108
+ export interface ReleaseRequestOptions {
109
+ env?: Record<string, string | undefined>;
110
+ /** Overridable for tests. */
111
+ version?: string;
112
+ workspace?: boolean;
113
+ }
114
+
115
+ /**
116
+ * What release this command should deploy, or `undefined` for a local build.
117
+ */
118
+ export function releaseRequest(
119
+ args: readonly string[],
120
+ options: ReleaseRequestOptions = {},
121
+ ): ReleaseRequest | undefined {
122
+ const env = options.env ?? process.env;
123
+ const bucket =
124
+ flag(args, "--release-bucket") ?? env[RELEASE_BUCKET_ENV] ?? DEFAULT_RELEASE_BUCKET;
125
+ const manifestRef = flag(args, "--manifest");
126
+ const explicit = flag(args, "--release");
127
+ if (explicit !== undefined) {
128
+ if (!RELEASE_VERSION_RE.test(explicit))
129
+ throw new Error(`--release wants a version like v0.1.0, not "${explicit}"`);
130
+ return { version: explicit, bucket, ...(manifestRef === undefined ? {} : { manifestRef }) };
131
+ }
132
+ if (manifestRef !== undefined) return { manifestRef, bucket };
133
+
134
+ const workspace = options.workspace ?? hasFoundationWorkspace();
135
+ if (workspace) return undefined;
136
+ const version = `v${options.version ?? toolVersion()}`;
137
+ if (!RELEASE_VERSION_RE.test(version))
138
+ throw new Error(
139
+ `this copy of foundation-deploy is version ${version.slice(1)}, which is not a release; pass --release <version>`,
140
+ );
141
+ return { version, bucket };
142
+ }
143
+
144
+ /** The key ARN a deployer will accept a manifest from. */
145
+ export function expectedKeyArn(env: Record<string, string | undefined> = process.env): string {
146
+ const configured = env[RELEASE_KMS_KEY_ARN_ENV] ?? RELEASE_KMS_KEY_ARN;
147
+ if (configured === undefined || configured === "")
148
+ throw new Error(
149
+ `no Foundation release signing key is configured; set ${RELEASE_KMS_KEY_ARN_ENV} to the key ARN published with the release`,
150
+ );
151
+ return configured;
152
+ }
153
+
154
+ export interface ResolveReleaseDeps {
155
+ /** Defaults to the SDK-backed verifier for the expected key. */
156
+ kms?: KmsVerifier;
157
+ kmsKeyArn?: string;
158
+ /** Copies `s3://<bucket>/<key>` to a local path. Defaults to `aws s3 cp`. */
159
+ download?: (ctx: AwsCallContext, bucket: string, key: string, dest: string) => Promise<void>;
160
+ /** Where the verified manifest is cached for the CDK app. */
161
+ cacheDir?: string;
162
+ /**
163
+ * Check every artifact's digest, not only the signature. On by default:
164
+ * the signature says the list is Foundation's, the digests say the bytes
165
+ * the deploy will hand CloudFormation are the ones on the list.
166
+ */
167
+ verifyArtifacts?: boolean;
168
+ env?: Record<string, string | undefined>;
169
+ }
170
+
171
+ /** `aws s3 cp` — the same CLI every other read in the deploy tool uses. */
172
+ export async function s3Download(
173
+ ctx: AwsCallContext,
174
+ bucket: string,
175
+ key: string,
176
+ dest: string,
177
+ ): Promise<void> {
178
+ mkdirSync(dirname(dest), { recursive: true });
179
+ await aws(ctx, ["s3", "cp", `s3://${bucket}/${key}`, dest]);
180
+ }
181
+
182
+ /**
183
+ * Fetch and verify the release a request names. Throws unless the manifest is
184
+ * signed by the expected key and every artifact hashes to what it claims.
185
+ *
186
+ * Nothing else in the deploy may run before this returns — that ordering is
187
+ * the whole point of a signed manifest, and `deploy.ts` is written so the
188
+ * image step, the CDK call and the config sync all sit after it.
189
+ */
190
+ export async function verifyRelease(
191
+ ctx: AwsCallContext,
192
+ request: ReleaseRequest,
193
+ deps: ResolveReleaseDeps = {},
194
+ ): Promise<ResolvedRelease> {
195
+ const env = deps.env ?? process.env;
196
+ const kmsKeyArn = deps.kmsKeyArn ?? expectedKeyArn(env);
197
+ const download = deps.download ?? s3Download;
198
+ const local = localManifestRef(request);
199
+ const version = request.version;
200
+ const cacheDir =
201
+ deps.cacheDir ?? releaseCacheDir(version ?? "unversioned", env as NodeJS.ProcessEnv);
202
+ const manifestPath = local ?? join(cacheDir, "manifest.json");
203
+
204
+ if (local === undefined) {
205
+ const source = s3ManifestRef(request);
206
+ await download(ctx, source.bucket, source.key, manifestPath);
207
+ }
208
+
209
+ const manifest = await verifyManifest(JSON.parse(readFileSync(manifestPath, "utf8")), {
210
+ kmsKeyArn,
211
+ kms: deps.kms ?? kmsVerifier(kmsKeyArn, ctx.profile === "" ? {} : { profile: ctx.profile }),
212
+ ...(version === undefined ? {} : { expectVersion: version }),
213
+ ...(deps.verifyArtifacts === false
214
+ ? {}
215
+ : {
216
+ readArtifact: async (key: string) => {
217
+ const dest = join(cacheDir, "artifacts", key.split("/").slice(-1)[0] ?? "artifact");
218
+ await download(ctx, request.bucket, key, dest);
219
+ return readFileSync(dest);
220
+ },
221
+ }),
222
+ });
223
+ return { version: manifest.version, bucket: request.bucket, manifestPath, manifest };
224
+ }
225
+
226
+ /**
227
+ * The release a command will deploy: verified for real, or — in a dry run,
228
+ * which has neither credentials nor any business downloading megabytes —
229
+ * planned.
230
+ */
231
+ export async function resolveRelease(
232
+ ctx: AwsCallContext,
233
+ request: ReleaseRequest,
234
+ deps: ResolveReleaseDeps = {},
235
+ ): Promise<ResolvedRelease> {
236
+ return ctx.dryRun === true ? plannedRelease(request) : verifyRelease(ctx, request, deps);
237
+ }
238
+
239
+ /** What a dry run reports: the release it would verify, fetching nothing. */
240
+ export function plannedRelease(request: ReleaseRequest): ResolvedRelease {
241
+ const version = request.version ?? "<from the manifest>";
242
+ const local = localManifestRef(request);
243
+ return {
244
+ version,
245
+ bucket: request.bucket,
246
+ manifestPath: local ?? join(releaseCacheDir(version), "manifest.json"),
247
+ };
248
+ }
249
+
250
+ /** The manifest as a local path, when the caller gave one. */
251
+ function localManifestRef(request: ReleaseRequest): string | undefined {
252
+ const ref = request.manifestRef;
253
+ if (ref === undefined || ref.startsWith("s3://")) return undefined;
254
+ return isAbsolute(ref) ? ref : resolve(process.cwd(), ref);
255
+ }
256
+
257
+ /** Where the manifest is fetched from: an explicit `s3://…`, else the version's key. */
258
+ export function s3ManifestRef(request: ReleaseRequest): { bucket: string; key: string } {
259
+ const ref = request.manifestRef;
260
+ if (ref?.startsWith("s3://") === true) {
261
+ const [bucket, ...rest] = ref.slice("s3://".length).split("/");
262
+ const key = rest.join("/");
263
+ if (bucket === undefined || bucket === "" || key === "")
264
+ throw new Error(`--manifest ${ref} is not an s3://bucket/key URI`);
265
+ return { bucket, key };
266
+ }
267
+ if (request.version === undefined)
268
+ throw new Error("a release needs a version or a manifest to fetch");
269
+ return { bucket: request.bucket, key: manifestKey(request.version) };
270
+ }
271
+
272
+ /** The `-c` context a release deploy adds to the CDK invocation. */
273
+ export function releaseContext(release: ResolvedRelease): string[] {
274
+ return [
275
+ "-c",
276
+ `release=${release.version}`,
277
+ "-c",
278
+ `releaseManifest=${release.manifestPath}`,
279
+ "-c",
280
+ `releaseBucket=${release.bucket}`,
281
+ ];
282
+ }
@@ -0,0 +1,241 @@
1
+ /**
2
+ * Composition of `<secretPrefix>/agent/runtime` — the one secret the container is told
3
+ * about (RUNTIME_SECRET_ID). server.ts fetches it at boot and setdefaults each
4
+ * key into the environment, so this object IS the runtime's env contract.
5
+ *
6
+ * The bot token lives here rather than being fetched separately so the agent
7
+ * needs exactly one Secrets Manager read at start.
8
+ */
9
+ import {
10
+ type Instance,
11
+ type InstanceNames,
12
+ instanceNames as namesFor,
13
+ slackCommandPrefix,
14
+ } from "@deployfoundation/foundation-core/instance";
15
+
16
+ /** The pinned AgentCore endpoint a deploy promotes and the invoker calls. */
17
+ export const LIVE_ENDPOINT = "live";
18
+
19
+ export interface SlackAppSecret {
20
+ bot_token: string;
21
+ bot_user_id: string;
22
+ team_id?: string;
23
+ api_app_id?: string;
24
+ }
25
+
26
+ export interface RuntimeSecretInputs {
27
+ /** The instance being deployed: every secret id and default below is its own. */
28
+ instance: Instance;
29
+ slackApp: SlackAppSecret;
30
+ tableName: string;
31
+ /** `FoundationData.ItemsTableName` — the pk+sk table holding todos and routines. */
32
+ itemsTableName?: string;
33
+ bucketName: string;
34
+ /**
35
+ * `FoundationData.DocumentsBucketName` — absent before the dedicated bucket has
36
+ * been deployed, in which case the documents capability has no binding.
37
+ */
38
+ documentsBucketName?: string;
39
+ /** `FoundationAgent.WebSearchGatewayUrl` — omitted before the gateway exists. */
40
+ webSearchUrl?: string;
41
+ /** `FoundationApi.InvokeQueueUrl` — where a fired routine re-enqueues its turn. */
42
+ invokeQueueUrl?: string;
43
+ /** `FoundationApi.RoutineSchedulerRoleArn` — passed to EventBridge Scheduler. */
44
+ routineSchedulerRoleArn?: string;
45
+ /** `FoundationAgent.AgentRuntimeArn` — named by every schedule's target input. */
46
+ agentRuntimeArn?: string;
47
+ /**
48
+ * The pinned endpoint humans talk to. The container cannot learn it from
49
+ * inside AgentCore, and it is half the name of its own span log group
50
+ * (`<runtime id>-<endpoint>`), so the deploy tells it.
51
+ */
52
+ agentEndpoint?: string;
53
+ /**
54
+ * `FoundationAgent.ReadOnlyRoleArn` — the instance's OWN account, expressed as a
55
+ * role the agent assumes. Absent on a stack deployed before it existed;
56
+ * `aws-readonly` then offers nothing unless `config.yaml` names another role.
57
+ */
58
+ readOnlyRoleArn?: string;
59
+ /**
60
+ * `FoundationApi.EmailProxyFunctionArn` — the ONLY thing the container is told
61
+ * about email. No mailbox credential is ever in this secret, or reachable
62
+ * from the execution role: the agent can invoke the proxy and nothing more.
63
+ * Absent on a stack deployed before the proxy existed; the registry then
64
+ * offers no email tools.
65
+ */
66
+ emailProxyFunctionArn?: string;
67
+ /**
68
+ * `FoundationApi.BrowserProxyFunctionArn`; absent when managed browser is disabled.
69
+ * The runtime receives no Browser API grant or signing secret.
70
+ */
71
+ browserProxyFunctionArn?: string;
72
+ /**
73
+ * `FoundationApi.CrmProxyFunctionArn` — the sole CRM binding supplied to the
74
+ * shell-capable container. Storage remains reachable only through the proxy.
75
+ */
76
+ crmProxyFunctionArn?: string;
77
+ /** Hash of the CRM policy baked into the deployed proxy; never a credential. */
78
+ crmPolicyFingerprint?: string;
79
+ /**
80
+ * `FoundationApi.OtterProxyFunctionArn` — the ONLY Otter binding given to the
81
+ * shell-capable container. The API credential remains readable solely by
82
+ * the proxy role. Absent deployments offer no Otter tools.
83
+ */
84
+ otterProxyFunctionArn?: string;
85
+ /**
86
+ * `FoundationApi.KnockProxyFunctionArn` — the agent's sole Knock binding. OAuth
87
+ * client and credential secret ids never enter this runtime secret.
88
+ */
89
+ knockProxyFunctionArn?: string;
90
+ /**
91
+ * `FoundationApi.UpworkProxyFunctionArn` — the sole Upwork binding supplied to the
92
+ * shell-capable container. OAuth client and token state stay in the proxy.
93
+ */
94
+ upworkProxyFunctionArn?: string;
95
+ /** Trusted gateway ingress used instead of granting the agent normal-queue writes. */
96
+ routineIngressFunctionArn?: string;
97
+ defaultRepo?: string;
98
+ }
99
+
100
+ export function composeRuntimeSecret(inputs: RuntimeSecretInputs): Record<string, string> {
101
+ const { instance, slackApp, tableName, bucketName } = inputs;
102
+ const names: InstanceNames = namesFor(instance);
103
+ for (const [key, value] of [
104
+ ["bot_token", slackApp.bot_token],
105
+ ["bot_user_id", slackApp.bot_user_id],
106
+ ["table name", tableName],
107
+ ["bucket name", bucketName],
108
+ ] as const) {
109
+ if (typeof value !== "string" || value === "")
110
+ throw new Error(`runtime secret: missing ${key}`);
111
+ }
112
+ return {
113
+ // Which deployment this container is. Logs and the message footer say so;
114
+ // nothing else branches on it.
115
+ FOUNDATION_INSTANCE: instance.name,
116
+ // The prefix this deployment's slash commands carry. A tool that tells
117
+ // someone to run a connect command has to name the command their own
118
+ // workspace installed, not the reference instance's `/sky-*` set.
119
+ FOUNDATION_COMMAND_PREFIX: slackCommandPrefix(instance),
120
+ FOUNDATION_SLACK_BOT_TOKEN: slackApp.bot_token,
121
+ FOUNDATION_SLACK_BOT_USER_ID: slackApp.bot_user_id,
122
+ FOUNDATION_GITHUB_APP_SECRET_ID: names.secretGithubApp,
123
+ FOUNDATION_CODEX_SECRET_ID: names.secretCodex,
124
+ // The image key. Always named: server.ts reads it lazily, so a secret
125
+ // still holding the CDK placeholder costs nothing until an image is asked for.
126
+ FOUNDATION_GOOGLE_AI_STUDIO_SECRET_ID: names.secretGoogleAiStudio,
127
+ // Drive service account JSON. Its `client_email` is the address users
128
+ // share folders/files with; the private key is fetched only on tool use.
129
+ FOUNDATION_GOOGLE_DRIVE_SECRET_ID: names.secretGoogleDrive,
130
+ // Calendar OAuth *client*. Per-person refresh tokens live in FoundationItems,
131
+ // never in a secret and never in the runtime environment.
132
+ FOUNDATION_GOOGLE_CALENDAR_SECRET_ID: names.secretGoogleCalendar,
133
+ // The shared OAuth client. Preferred over the one Calendar historically
134
+ // kept inside its own secret, which stays a fallback.
135
+ FOUNDATION_GOOGLE_OAUTH_SECRET_ID: names.secretGoogleOauth,
136
+ FOUNDATION_TABLE_NAME: tableName,
137
+ // Absent on a stack deployed before the table existed; server.ts then
138
+ // offers neither the todo nor the routine tools.
139
+ ...(inputs.itemsTableName !== undefined && inputs.itemsTableName !== ""
140
+ ? { FOUNDATION_ITEMS_TABLE_NAME: inputs.itemsTableName }
141
+ : {}),
142
+ FOUNDATION_BUCKET_NAME: bucketName,
143
+ // Deliberately absent until FoundationData has exported the dedicated bucket:
144
+ // configuration alone must not expose document tools without storage.
145
+ ...(inputs.documentsBucketName !== undefined && inputs.documentsBucketName !== ""
146
+ ? { FOUNDATION_DOCUMENTS_BUCKET_NAME: inputs.documentsBucketName }
147
+ : {}),
148
+ FOUNDATION_CONFIG_KEY: names.configKey,
149
+ FOUNDATION_DEFAULT_REPO: inputs.defaultRepo ?? names.defaultRepo,
150
+ // The org the instance's GitHub App is installed on. The prompt names it
151
+ // and `gh repo list` is scoped by it; nothing in the image hard-codes one.
152
+ FOUNDATION_GITHUB_ORG: instance.github.org,
153
+ // Workflow-file changes are executable code, so pass the narrow repo
154
+ // allowlist only when this instance explicitly opts in.
155
+ ...(instance.github.workflowWriteRepos !== undefined &&
156
+ instance.github.workflowWriteRepos.length > 0
157
+ ? {
158
+ FOUNDATION_GITHUB_WORKFLOW_WRITE_REPOS: JSON.stringify(
159
+ instance.github.workflowWriteRepos,
160
+ ),
161
+ }
162
+ : {}),
163
+ // This instance's AgentCore runtime name. The commit author falls back to
164
+ // it while the GitHub App has no slug yet.
165
+ FOUNDATION_RUNTIME_NAME: names.runtimeName,
166
+ // The App's slug: the agent authors commits as `<slug>[bot]` so GitHub
167
+ // attributes them to the App. Absent until bootstrap has created the App.
168
+ ...(instance.github.appSlug !== "TBD"
169
+ ? { FOUNDATION_GITHUB_APP_SLUG: instance.github.appSlug }
170
+ : {}),
171
+ // Absent (rather than empty) when there is no gateway yet: server.ts
172
+ // treats a missing url as "no search_web tool".
173
+ ...(inputs.webSearchUrl !== undefined && inputs.webSearchUrl !== ""
174
+ ? { FOUNDATION_WEB_SEARCH_URL: inputs.webSearchUrl }
175
+ : {}),
176
+ // The routine trio: all three or none, and server.ts offers no
177
+ // `routine_*` tools until every one of them is present.
178
+ ...(inputs.invokeQueueUrl !== undefined && inputs.invokeQueueUrl !== ""
179
+ ? { FOUNDATION_INVOKE_QUEUE_URL: inputs.invokeQueueUrl }
180
+ : {}),
181
+ ...(inputs.routineSchedulerRoleArn !== undefined && inputs.routineSchedulerRoleArn !== ""
182
+ ? { FOUNDATION_ROUTINE_SCHEDULER_ROLE_ARN: inputs.routineSchedulerRoleArn }
183
+ : {}),
184
+ ...(inputs.agentRuntimeArn !== undefined && inputs.agentRuntimeArn !== ""
185
+ ? { FOUNDATION_AGENT_RUNTIME_ARN: inputs.agentRuntimeArn }
186
+ : {}),
187
+ // The EventBridge Scheduler group the runtime role is allowed to write
188
+ // (`<prefix>-routines`, api-stack RoutineGroup). Derived, never optional:
189
+ // without it the agent fell back to a hard-coded group name, which only
190
+ // one instance's role may touch, and every other instance logged
191
+ // AccessDenied on CreateSchedule (2026-09-07).
192
+ FOUNDATION_ROUTINE_GROUP: names.routineGroup,
193
+ // Always `live` today: every deploy promotes that one endpoint, and it is
194
+ // what the span log group is named after.
195
+ FOUNDATION_AGENT_ENDPOINT:
196
+ inputs.agentEndpoint !== undefined && inputs.agentEndpoint !== ""
197
+ ? inputs.agentEndpoint
198
+ : LIVE_ENDPOINT,
199
+ ...(inputs.readOnlyRoleArn !== undefined && inputs.readOnlyRoleArn !== ""
200
+ ? { FOUNDATION_AWS_READONLY_ROLE_ARN: inputs.readOnlyRoleArn }
201
+ : {}),
202
+ ...(inputs.emailProxyFunctionArn !== undefined && inputs.emailProxyFunctionArn !== ""
203
+ ? { FOUNDATION_EMAIL_PROXY_FUNCTION_ARN: inputs.emailProxyFunctionArn }
204
+ : {}),
205
+ ...(inputs.browserProxyFunctionArn !== undefined && inputs.browserProxyFunctionArn !== ""
206
+ ? { FOUNDATION_BROWSER_PROXY_FUNCTION_ARN: inputs.browserProxyFunctionArn }
207
+ : {}),
208
+ ...(inputs.crmProxyFunctionArn !== undefined && inputs.crmProxyFunctionArn !== ""
209
+ ? { FOUNDATION_CRM_PROXY_FUNCTION_ARN: inputs.crmProxyFunctionArn }
210
+ : {}),
211
+ ...(inputs.crmPolicyFingerprint !== undefined && inputs.crmPolicyFingerprint !== ""
212
+ ? { FOUNDATION_CRM_POLICY_FINGERPRINT: inputs.crmPolicyFingerprint }
213
+ : {}),
214
+ ...(inputs.otterProxyFunctionArn !== undefined && inputs.otterProxyFunctionArn !== ""
215
+ ? { FOUNDATION_OTTER_PROXY_FUNCTION_ARN: inputs.otterProxyFunctionArn }
216
+ : {}),
217
+ ...(inputs.knockProxyFunctionArn !== undefined && inputs.knockProxyFunctionArn !== ""
218
+ ? { FOUNDATION_KNOCK_PROXY_FUNCTION_ARN: inputs.knockProxyFunctionArn }
219
+ : {}),
220
+ ...(inputs.upworkProxyFunctionArn !== undefined && inputs.upworkProxyFunctionArn !== ""
221
+ ? { FOUNDATION_UPWORK_PROXY_ARN: inputs.upworkProxyFunctionArn }
222
+ : {}),
223
+ ...(inputs.routineIngressFunctionArn !== undefined && inputs.routineIngressFunctionArn !== ""
224
+ ? { FOUNDATION_ROUTINE_INGRESS_FUNCTION_ARN: inputs.routineIngressFunctionArn }
225
+ : {}),
226
+ // The read-only Atlas credential. Always named: server.ts reads it
227
+ // LAZILY, so a secret still holding the CDK placeholder costs nothing
228
+ // until someone asks a MongoDB question.
229
+ FOUNDATION_MONGODB_SECRET_ID: names.secretMongodbReadonly,
230
+ // CRM storage, the Otter credential, the Knock OAuth secrets, and the Upwork
231
+ // OAuth secret are intentionally absent here: the shell-capable agent receives
232
+ // only their fixed proxy ARNs.
233
+ // A routine fire builds its own session key, so the container needs the
234
+ // team id the gateway takes from the event. The app secret carries it
235
+ // when setup wrote one; the instance file is the fallback.
236
+ FOUNDATION_SLACK_TEAM_ID:
237
+ slackApp.team_id !== undefined && slackApp.team_id !== ""
238
+ ? slackApp.team_id
239
+ : instance.slack.teamId,
240
+ };
241
+ }