* feat(infra): build + pin the baked open-swe-base-arm64 AMI (T12 AMI / item 3)
Packer-build the custom base image and repoint AppService off the AL2023
placeholder onto it.
deploy/ami/open-swe-base.pkr.hcl — fix two bugs that blocked the first real
`packer build` (the config had only ever been `packer validate`'d at T8):
- the file provisioner failed uploading the templates dir ('scp: …: Is a
directory') — a trailing-slash contents-upload needs the dest dir to exist;
added a 'mkdir -p /tmp/open-swe-templates' shell provisioner + dropped the
dest trailing slash.
- the shell provisioner's custom execute_command omitted {{ .Vars }}, so the
environment_vars never reached provision.sh (which runs under set -u and
aborted on CLOUDWATCH_AGENT_DEB_URL). Added {{ .Vars }}.
infra:
- ami-cache.ts: BAKED_OPEN_SWE_AMI_ID = ami-0545363bb147229ff (built 2026-06-26
from open-swe-base-arm64-20260626-201929) + bakedOpenSweArm64() pinning it by
exact id via MachineImage.genericLinux (offline, deterministic). Dropped the
now-dead AL2023 cachedInContext helper + context key; kept the EBS/replacement
discipline docs.
- app-service.ts: machineImage → bakedOpenSweArm64().
- open-swe-stack.ts: output BakedAmiId (was the AL2023 PinnedAmiId guard).
- cdk.context.json → {} (AMI is a static id pin; no context lookups remain).
- README: Baked AMI + EBS-replacement-discipline section.
tsc + cdk synth(dev+prod) + jest(16) clean; template ImageId = the baked AMI.
NOTE: held — do NOT merge until the open-swe-dev secret values are populated
(put-config.sh). The infra CD is live, so merging this to dev auto-deploys
OpenSweDevStack; without secrets the box boots but fetch-config fail-fasts →
unhealthy ALB target on the shared prod ALB. Merge once secrets are set (T14).
* fix(ami): ASCII-only AMI description + re-pin to ami-00080084502093021
Third packer bug: ami_description had an em-dash (non-ASCII); AWS rejects
non-ASCII in the AMI Description attribute, so packer registered then
DEREGISTERED the first AMI (ami-0545…) on the ModifyImageAttribute error.
Replaced with an ASCII '-'. Rebuilt clean → ami-00080084502093021 (available).
Re-pinned BAKED_OPEN_SWE_AMI_ID.
* fix(deploy): GitHub App + Slack required for prod only, not dev
Per the migration decision: do NOT create/duplicate a separate dev GitHub App or
Slack app — only prod owns the single shared app. So fetch-config.sh no longer
hard-requires the GitHub App quintet (ID/PRIVATE_KEY/INSTALLATION_ID/CLIENT_ID/
CLIENT_SECRET) + Slack/webhook secrets for dev; they move into the prod-only
block alongside the existing GITHUB_WEBHOOK_SECRET/SLACK_SIGNING_SECRET.
Dev now boots with just DASHBOARD_JWT_SECRET + TOKEN_ENCRYPTION_KEY + the active
provider key(s) + the langsmith sandbox keys. Dev is a deployment-validation env
(boot/health/boundary) with no GitHub/Slack/webhook integration; prod parity is
unchanged (prod still requires everything).
* feat: stand up dev properly — S3 assets bucket + artifact CD + on-box uv sync (T7+T19)
Make the dev/prod box deployable end-to-end: a real artifact pipeline and a
re-runnable on-box deploy, so OpenSweDevStack can come up genuinely healthy.
Infra (T7):
- assets-bucket.ts: open-swe-<env>-assets S3 bucket — BLOCK_ALL public access,
SSE-S3, enforceSSL (deny non-TLS), versioned, lifecycle (expire noncurrent +
abort MPU), RETAIN. Wired into OpenSweStack + CfnOutput.
- app-service.ts: open-swe-<env>-deploy SSM document that runs the baked
/opt/open-swe/bin/deploy.sh (tag-scoped roll-the-box). machineImage is the
baked open-swe-base-arm64 AMI (folds in the held #16).
IAM (app deploy role — cross-review gated):
- github-deploy-roles.ts: app role gains s3:PutObject/DeleteObject scoped to
open-swe-<env>-assets/releases/* (CI uploads releases). Drops the generic
AWS-RunShellScript grant now that the dedicated open-swe-<env>-deploy document
is the only SendCommand path — closes the T4 BLOCK#3 arbitrary-shell timebox.
Boot/deploy (T19):
- deploy/ami/deploy.sh: single, re-runnable app-deploy procedure — pull
app.tar.gz/spa.tar.gz from S3, `uv sync --frozen --no-dev` (native ARM64 venv
at the real path, py3.12 pre-baked), restart open-swe.service + reload nginx.
- user-data.sh: nginx starts BEFORE the app deploy (static /healthz -> the ALB
target is healthy even before the first release); deploy.sh is base64-rendered
by CDK into user-data (a normal reviewable repo file, not a heredoc) and the
first-boot deploy is NON-FATAL (no release yet -> wait for the first SSM deploy).
CI (T7+T19):
- build-artifacts.yml (+ .github/scripts): build the SPA with bun (vite ->
ui/.output/public -> spa.tar.gz), package the Python source via git archive
(app.tar.gz, no ui/ no .venv), upload to releases/<sha>/ + releases/latest/ via
the githubdeploy-open-swe-app-<env> OIDC role, then fire open-swe-<env>-deploy.
push dev -> dev (auto); push main -> prod (env "prod" approval gate).
Local: ruff/shellcheck clean, tsc clean, jest 16/16, cdk synth offline OK,
deploy.sh base64 round-trips exact.
* harden(sec-review): tar extraction, deploy gating, least-privilege, secret guard
Address the /sh-security-review fan-out + proof-or-kill verifier pass. Only one
confirmed-high surfaced and it is PRE-EXISTING and out-of-diff (OSWE-IAC-AUDIT-01,
the account-wide CDK cfn-exec residual already documented in config.ts; recorded in
.security-review/suppressions.json with justification + flagged for the per-env
bootstrap-qualifier follow-up). The rest were verifier-downgraded to unverified;
these are the cheap defense-in-depth fixes worth taking regardless:
- deploy.sh: extract tarballs with --no-same-owner --no-same-permissions (root
never honors an archive's uid/mode → no setuid/foreign-owned file can land); and
treat "no release in S3 yet" as a benign exit 0, distinct from a real deploy
failure (set -e stays loud once a release exists).
- publish-and-deploy.sh: gate on the AGGREGATE SSM Command.Status (+ TargetCount),
not CommandInvocations[0], so a partial failure across the brief 2-instance
replacement window can't be reported as success.
- instance-role.ts: scope the box's s3:GetObject to releases/* (mirrors the app
role's write scope) instead of the whole bucket.
- package-artifacts.sh: fail-closed secret-shaped-file guard on app.tar.gz
(defense in depth over .gitignore; scoped to data extensions so *_credentials.py
source is not a false positive — verified against the real tree).
Deferred as documented follow-ups (verifier: unverified, supply-chain-gated to the
CI OIDC writer; bucket is BLOCK_ALL + enforceSSL + versioned): SHA-pinned immutable
releases/<sha>/ pulls + signed checksum (vs mutable latest/), single-tarball release
to remove the torn-read window, and app-aware ALB health (vs static nginx /healthz).
shellcheck/tsc/jest(16) clean; both stacks synth offline.
* fix(infra): ASCII-only EC2 SecurityGroup descriptions + synth-time guard
The instance-SG GroupDescription + ingress/egress rule descriptions carried an
em-dash / arrow (—, →). `tsc` and `cdk synth` accept them, but the EC2 API rejects
non-ASCII in GroupDescription ("Character sets beyond ASCII are not supported"),
so OpenSweDevStack's first deploy failed at the SG and rolled back. (Pre-existing
from #14; same class as the AMI-description ASCII bug.)
- app-service.ts: replace —/→ with ASCII (- / ->) in the SG GroupDescription, the
ingress/egress rule descriptions, and the Route53 comment.
- test/ascii-aws-fields.test.ts: synth-time guard asserting EC2 SecurityGroup
GroupDescription + rule descriptions are pure ASCII, so this fails the build
instead of a deploy next time.
jest 18/18; tsc clean.
* fix(infra): SG rule descriptions use ASCII-charset-safe text (no `>`)
The first ASCII fix replaced the arrow with `->`, but EC2 SecurityGroup *rule*
descriptions allow a stricter set than ASCII — `a-zA-Z0-9. _-:/()#,@[]+=&;{}!$*`,
which EXCLUDES `<`/`>`. So OpenSweDevStack's second deploy still failed at the
ingress rule. Use "to" instead of "->", and tighten the guard test from "ASCII
only" to the exact EC2 allowed charset so it catches `>` (and `<`) too.
jest 18/18; tsc clean.
* fix(infra): minify embedded deploy.sh so user-data fits EC2's 25.6 KB limit
The base64 deploy.sh embedded in user-data pushed the encoded boot script to
27184 bytes, over EC2's 25600-byte cap, so OpenSweDevStack's instance failed with
"Encoded User data is limited to 25600 bytes". Strip full-line comments + blank
lines from deploy.sh before base64-embedding it (repo file keeps comments; only
the on-box copy is minified; the script is opaque base64 so user-data heredocs are
unaffected) -> rendered user-data drops to 16424 bytes (9 KB margin). Add a
synth-time guard test asserting EC2 user-data stays under 25600 bytes encoded.
jest 19/19; minified deploy.sh passes bash -n + shellcheck.
|
||
|---|---|---|
| .. | ||
| bin | ||
| lib | ||
| test | ||
| .gitignore | ||
| cdk.context.json | ||
| cdk.json | ||
| jest.config.js | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
open-swe infra (CDK TypeScript)
AWS infrastructure for the Open SWE → AWS migration. Synth-only at this stage —
nothing here is deployed yet. All IAM is applied only after the Phase-1 security
gate (T4 GPT-4.1 IAM cross-review + T5 /sh-security-review) clears (T6).
Layout
infra/
├── bin/
│ └── app.ts # CDK app entry — instantiates the 3 stacks, applies the naming Aspect
├── lib/
│ ├── config.ts # account/region/org constants, env type, OIDC trust subjects
│ ├── open-swe-iam-stack.ts # account-level: shared OIDC deploy roles
│ ├── open-swe-stack.ts # per-env stack (instance role + config store + AppService)
│ ├── aspects/
│ │ └── kebab-naming-aspect.ts # fails synth on any non-kebab-case explicit name
│ └── constructs/
│ ├── github-deploy-roles.ts # githubdeploy-open-swe-infra + githubdeploy-open-swe-app
│ ├── instance-role.ts # open-swe-<env>-instance-role (least-privilege)
│ ├── config-store.ts # Secrets Manager + SSM Parameter Store shells (T11)
│ ├── app-service.ts # EC2 box + imported-ALB ingress + Route53 + logs (T12)
│ └── ami-cache.ts # baked open-swe AMI pin (by id) + EBS/replacement docs
├── test/
│ └── kebab-naming-aspect.test.ts # jest: Aspect passes conforming names, flags bad ones
├── cdk.json
├── cdk.context.json # COMMITTED — {} (AMI is a static id pin; no lookups)
├── package.json # aws-cdk-lib pinned EXACT (2.260.0)
├── tsconfig.json
├── jest.config.js
└── .gitignore
Stacks
| Stack name (kebab) | Construct | Contents |
|---|---|---|
open-swe-iam |
OpenSweIamStack |
Account-level shared GitHub OIDC deploy roles (singletons). |
open-swe-dev |
OpenSweStack (envName: dev) |
open-swe-dev-instance-role, config store (T11), and the EC2 box + ALB ingress (T12, AppService). |
open-swe-prod |
OpenSweStack (envName: prod) |
open-swe-prod-instance-role, config store, and the EC2 box + ALB ingress. |
Account 328440206208, region us-east-1. Stack names are set explicitly so CDK
never defaults to PascalCase; resource names follow open-swe-<env>-*.
The two env stacks (
open-swe-dev/open-swe-prod) are the required pair. The shared OIDC deploy roles are account-wide singletons (oneRoleNameeach), so they live in their own dedicatedopen-swe-iamstack rather than being duplicated across the env stacks — and that stack deploys first (see ordering).
IAM roles defined (unapplied)
githubdeploy-open-swe-infra— GitHub OIDC role for CDK/CFN infra deploys. Trust scoped torepo:Sea-Haven-Industries/open-sweon themain/devbranches only. Permission is the org-standard CDK pattern:sts:AssumeRoleon the CDK bootstrap roles (cdk-hnb659fds-*) — the real CFN/IAM/resource scope lives in the bootstrapcfn-exec-role, not in this role.githubdeploy-open-swe-app— GitHub OIDC role for app deploys. Tag-scopedssm:SendCommand(instances taggedproject=open-swe+env in {dev,prod}) + read-only access to theopen-swe-<env>-assetsS3 artifact buckets.open-swe-<env>-instance-role— EC2 instance role, least-privilege: readopen-swe-<env>-assets(S3), read/open-swe-<env>/*(SSM), readopen-swe-<env>/*(Secrets Manager), put/open-swe/<env>/*CloudWatch Logs, plusAmazonSSMManagedInstanceCorefor SSM agent registration. No admin.
The GitHub OIDC provider already exists account-wide (created for seahaven-site); it is referenced by ARN, never re-created.
Kebab-case naming Aspect
KebabNamingAspect (applied app-wide in bin/app.ts) fails synth via
Annotations.addError when a stack name or an explicit physical resource name
(RoleName, BucketName, …) is not kebab-case. Path-style names (Secrets
Manager a/b, SSM /a/b, log groups /aws/.../x) are validated per /-segment.
CDK logical construct ids are intentionally NOT validated (they are conventionally
PascalCase). Covered by test/kebab-naming-aspect.test.ts.
Config store (Secrets Manager + SSM shells — T11)
ConfigStore (lib/constructs/config-store.ts, one per env from OpenSweStack)
renders the resource shells the boot hook deploy/seahaven/fetch-config.sh reads.
The naming contract (T9 inventory + the fetch-config header) is LITERAL env-var
names as the last path segment — open-swe-<env>/<VAR> for secrets,
/open-swe-<env>/<VAR> (FLAT) for config — because fetch-config strips the prefix
and exports that segment verbatim.
Three buckets:
-
Secret shells (Secrets Manager) — 27 secrets. Created value-LESS (an L1
CfnSecretwith NEITHERsecretStringNORgenerateSecretString, which CloudFormation creates as an empty secret). The real value is set out-of-band (put-config.sh) — CDK never owns it, so a latercdk deploycan never clobber it.UpdateReplacePolicy/DeletionPolicy: Retainso a teardown can't destroy operator-set secret material. AWS-managed key (no CMK — matches the instance role, which omitskms:Decrypt).The T9 header says "29 secrets" but its table enumerates 27 distinct VAR names (the CORRIDOR row holds 3). We create 27 — we don't invent two to hit 29. Confirm the 27-vs-29 count (code-only candidates not in the table:
USER_ID_API_KEY_MAP,JUDGE_ANTHROPIC_BASE_URL). -
IaC-managed SSM config — 8 params, real values owned in code:
Param dev prod SANDBOX_TYPElangsmithlangsmithDEFAULT_REPO_OWNERSea-Haven-IndustriesSea-Haven-IndustriesALLOWED_GITHUB_ORGSSea-Haven-IndustriesSea-Haven-IndustriesDEFAULT_REPO_NAMEopen-swe-pilot(confirm)open-swe-pilot(confirm)DASHBOARD_BASE_URLhttps://openswe-dev.seahaven.com(confirm host)https://openswe.seahaven.com(confirm host)DASHBOARD_API_BASE_URLsame as base same as base DASHBOARD_ALLOWED_ORIGINSsame as base same as base LLM_MODEL_IDanthropic:claude-opus-4-8(confirm)anthropic:claude-opus-4-8(confirm) -
Out-of-band SSM config — NOT created by CDK. Operationally-variable or env-specific-unknown values listed in
OUT_OF_BAND_SSMand populated byput-config.sh. The keystone isDEFAULT_SANDBOX_SNAPSHOT_ID(changes on every snapshot rebuild → must NOT be CDK-managed or a deploy clobbers it); also the GitHub App ids, Slack ids, and LangSmith tenant/urls.
Kebab-Aspect deviation
KebabNamingAspect exempts AWS::SecretsManager::Secret and AWS::SSM::Parameter
from the kebab check (see the KEBAB_EXEMPT_RESOURCE_TYPES set) — the UPPER_SNAKE
env-var segment is a required, documented deviation for a lossless store→env
round-trip. Every other explicitly-named resource is still validated. Covered by a
dedicated case in test/kebab-naming-aspect.test.ts.
Deploy ordering (values BEFORE the box boots)
The shells are synth-able now (T11). Population is out-of-band and happens after
cdk deploy open-swe-<env> but before the EC2/T12 box first boots:
cdk deploy open-swe-<env> # creates the 27 secret shells + 8 IaC params
deploy/seahaven/put-config.sh <dev|prod> # sets the 27 secret values + out-of-band SSM
deploy/seahaven/fetch-config.sh <dev|prod> # (on the box) fail-fast verify before first start
put-config.sh ships <FILL> placeholders only (no real secret values committed);
provide each value inline, via OPENSWE_PUT_<VAR> env vars, or from a vault. It does
NOT touch the IaC-managed params (CDK owns those — editing them here would drift).
Compute + ingress (AppService — T12)
AppService (lib/constructs/app-service.ts, one per env from OpenSweStack)
builds the box and its path to the internet. A single internet-facing ALB
(app/seahaven-com) and a single VPC are shared with the on-prem
seahaven-site stack, so open-swe imports the VPC, the ALB security group
(sg-0b0301deed193258a), the :443 listener, and the public seahaven.com
zone — and never owns/mutates them. It adds:
- One ARM64 EC2 box (
open-swe-<env>-box,t4g.mediumdev /t4g.largeprod) in private1 (us-east-1a) — same AZ as the single NAT for in-AZ egress.requireImdsv2, gp3 encrypted root,deleteOnTermination(no RETAIN volume — see below).userDataCausesReplacement: true; user-data is rendered fromdeploy/ami/user-data.sh. - A standalone instance SG reachable only from the shared ALB SG on
:80(nginx). Egress open (NAT). The ALB SG is opened to the box via a standaloneCfnSecurityGroupEgressso the imported (on-prem-owned) SG is never mutated. - A target group → instance
:80(nginx is the sole ingress; the LangGraph control plane stays on loopback:2024). Health checkGET /healthz. - Two listener rules on the imported
:443listener, both → the same TG:- Webhooks (priority 2 dev / 3 prod):
host ∈ {openswe-<env>, hooks-<env>}.seahaven.comAND path/webhooks/*. - Site (priority 10 dev / 11 prod):
host = openswe-<env>.seahaven.com(dashboard SPA +/dashboard/api/).
- Webhooks (priority 2 dev / 3 prod):
- Route53 alias records
openswe[-dev]+hooks[-dev]→ the shared ALB. - Four CloudWatch log groups (
/open-swe/<env>/{app,user-data,nginx-access,nginx-error}) at 30-day retention (IaC-owned; mirrors the CW-agent config).
Listener-rule ordering (load-bearing)
The shared listener already has a host-agnostic /webhooks/* PATH rule at
priority 5 (on-prem). ALB rules are first-match by ascending priority, so the
open-swe webhook rule must sit below 5 or every …/webhooks/* request (any
host) is forwarded to the on-prem target first. Hence priority 2/3. The rule ANDs
a host condition, so it does not steal the on-prem hosts' webhooks. The
dashboard "site" rule carries no path that collides with rule 5, so it sits at
10/11.
Cross-stack coordination (T13 review). The seahaven-site (on-prem) and
open-swe stacks both add resources to the same imported listener and ALB SG.
This is safe: each stack owns only the resources it declares (its own logical
ids), so an on-prem deploy can't delete open-swe's rules/egress and vice-versa,
and the standalone CfnSecurityGroupEgress never mutates the shared SG's own
definition (the pattern on-prem itself uses). The one shared namespace that needs
care is listener-rule priority (globally unique per listener; a collision is
a fail-safe deploy error, not silent drift). Ownership — keep disjoint:
seahaven-site = 4-7 + default; open-swe = 2, 3, 10, 11. open-swe's
webhook rules are host-scoped to its own *.seahaven.com hosts, so they never
match an on-prem seahavenind.com host.
Security review (T5/T12 /sh-security-review)
The T12 surface was run through the detector-fan-out + proof-or-kill verifier.
One confirmed medium (OSWE-T12-01: nginx's 1 MB default client_max_body_size
would 413 large GitHub webhooks before in-app signature verification) is fixed in
open-swe.nginx.conf (25m on /webhooks/, 10m on /dashboard/api/). The
hooks hostname is scoped to /webhooks/* only (OSWE-T12-02 hygiene). An
X-Forwarded-For spoof candidate was killed — no code trusts the leftmost XFF.
No confirmed critical/high; no block.
Baked AMI + EBS-replacement discipline
bakedOpenSweArm64() (in lib/constructs/ami-cache.ts) pins the custom
open-swe-base-arm64 image by EXACT id (BAKED_OPEN_SWE_AMI_ID) via
MachineImage.genericLinux({ "us-east-1": "<ami-id>" }) — no SSM lookup, so synth
and deploy are fully offline/deterministic. The image is built by
deploy/ami/open-swe-base.pkr.hcl (ARM64 Ubuntu 24.04 + uv/py3.12 + nginx + CW
agent + boot templates, no secrets); the box's user-data.sh assumes that
baked layout (/opt/open-swe, openswe user, nginx, CW agent).
Pinning by exact id (vs a most_recent name filter) is what prevents a routine
deploy from silently swapping the AMI → EC2 instance replacement (the
file-share data-loss root cause — memory feedback_inline_ebs_volumes).
userDataCausesReplacement: trueis deliberate — user-data is provisioning-only and carries no durable state.- No durable state on the box → no RETAIN volume. The in-memory langgraph
store is rebuilt on every boot from S3 + Secrets Manager / SSM, so there is
intentionally no standalone
ec2.Volume+removalPolicy.RETAIN. The goal is replacement-tolerance, not avoidance. - Snapshot-before-replace still applies operationally: before any replacing
deploy snapshot the root volume and wait
state=completed, and re-verify "no local-only durable state" first.
Refresh the AMI deliberately:
cd deploy/ami && packer build open-swe-base.pkr.hcl # prints the new ami-… id
# update BAKED_OPEN_SWE_AMI_ID in infra/lib/constructs/ami-cache.ts
cd infra && npx cdk diff OpenSweDevStack # WILL show "requires replacement"
cdk.context.jsonis{}— nothing is resolved via context anymore (the AMI is a static id pin), so synth makes no live AWS call.
Commands
npm install
npx cdk synth open-swe-iam
npx cdk synth open-swe-dev
npx cdk synth open-swe-prod
npm test # jest — naming Aspect
CI/CD (T18 — .github/workflows/ci-infra.yml + cd-infra.yml)
Path-filtered, OIDC-only (no static keys). The Python agent keeps its own
ci.yml ("Agent CI"); these two add the /infra half.
| Workflow | Trigger | Does |
|---|---|---|
ci-infra.yml |
PR touching infra/** |
tsc + jest + cdk synth (reusable ci-typescript-cdk.yaml). |
cd-infra.yml |
push to dev/main touching infra/**, or dispatch |
CI (pre-deploy) → per-env cdk deploy. |
cd-infra.yml flow:
- push to
dev→ CI green → autocdk deploy OpenSweDevStack(assumesgithubdeploy-open-swe-infra-dev; the job declares noenvironment:, so the OIDC subject is…:ref:refs/heads/dev— matching that role's trust). - push to
main→ CI green →cdk deploy OpenSweProdStackbehind theprodGitHub Environment (required reviewer = Adam). Theenvironment: proddeclaration both fires the manual-approval gate and makes the OIDC subject…:environment:prod— matchinggithubdeploy-open-swe-infra-prod's trust.
Why not the reusable cd-cdk.yaml: it runs cdk deploy --all, which from a
single-env push would deploy the other env + the shared IAM stack — breaking the
per-env boundary. So CD targets one stack explicitly per env. The shared
open-swe-iam stack is not deployed by CD (privileged, human-gated — T6).
Gating note: infra CI is enforced at the deploy boundary (cd-infra's
deploy-* jobs needs: ci), not as a branch-protection required check —
path-filtering a required check would deadlock app-only PRs (a skipped required
check never satisfies). Making Infra CI a required check later needs a
skip-aware shim or dropping its path filter.
Prerequisites (set post-T6, when the roles exist):
- repo variables
AWS_DEPLOY_ROLE_INFRA_DEV/AWS_DEPLOY_ROLE_INFRA_PROD= thegithubdeploy-open-swe-infra-<env>role ARNs (open-swe-iamoutputs). - a GitHub Environment named
prodwith Adam as a required reviewer.
App-side CD (CI → S3 artifact → SSM deploy via
githubdeploy-open-swe-app-<env>) is T19, not here.
Deploy ordering (when the gate clears — NOT yet)
open-swe-iamfirst — apply the IAM stack (T6, human-gated), then set the repoAWS_DEPLOY_ROLE_INFRA_{DEV,PROD}variables from its role-ARN outputs and configure theprodEnvironment reviewer (BLOCK#3).- Security gate — T4 GPT-4.1 IAM cross-review + T5
/sh-security-reviewon the synth; resolve every confirmed critical/high. - IAM applied (T6) — only after the gate.
- Env stacks: first
open-swe-dev(T14, manual validate), then CD auto-deploys dev on push;open-swe-prod(T21) behind theprodEnvironment approval.
Version policy
aws-cdk-lib is pinned EXACT (2.260.0) — no ^/~. Dependabot keeps it
current; CI (npm ci + cdk synth) + dependency review gate each bump. See
aws-infrastructure.md "CDK Version Policy" and memory
feedback_cdk_lib_bundled_deps.