mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 10:23:14 +00:00
feat: Secrets Manager + SSM config store for open-swe (T11) (#10)
Create the per-env config surface the EC2 box reads at boot via
fetch-config.sh / seed_store.sh:
- ConfigStore construct (infra/lib/constructs/config-store.ts):
- 28 value-LESS Secrets Manager shells open-swe-<env>/<VAR>
(RemovalPolicy.RETAIN, no SecretString/generateSecretString — real
values are set out-of-band by put-config.sh, never in IaC/state).
- 8 IaC-managed SSM params /open-swe-<env>/<VAR> with real,
stable/derivable values (SANDBOX_TYPE, DEFAULT_REPO_OWNER/NAME,
ALLOWED_GITHUB_ORGS, DASHBOARD_*_URL/ORIGINS, LLM_MODEL_ID).
- OUT_OF_BAND_SSM documents the ~30 params CDK intentionally does NOT
own (operationally-variable / env-specific-unknown).
- Wire ConfigStore into OpenSwe<Env>Stack.
- KebabNamingAspect: exempt Secrets Manager + SSM names, which carry the
literal UPPER_SNAKE env-var segment (open-swe-dev/DASHBOARD_JWT_SECRET).
- deploy/seahaven/put-config.sh: out-of-band populator (placeholders only,
OPENSWE_PUT_<VAR> env indirection; no real values committed).
Synth-only; not deployed. Instance-role read grants on open-swe-<env>/*
already exist from T6 — no IAM/trust changes here.
This commit is contained in:
parent
434d0ad80c
commit
aef6b26912
6 changed files with 511 additions and 1 deletions
155
deploy/seahaven/put-config.sh
Executable file
155
deploy/seahaven/put-config.sh
Executable file
|
|
@ -0,0 +1,155 @@
|
|||
#!/usr/bin/env bash
|
||||
# put-config.sh — out-of-band populator for the open-swe config store.
|
||||
#
|
||||
# T11 (CDK) creates the RESOURCE SHELLS:
|
||||
# - 28 Secrets Manager secrets open-swe-<env>/<VAR> (value-LESS shells)
|
||||
# - the IaC-managed SSM params /open-swe-<env>/<VAR> (real values, owned in CDK)
|
||||
# This script sets the values that CANNOT live in IaC — every secret value, plus
|
||||
# the out-of-band SSM params (operationally-variable / env-specific-unknown). Run
|
||||
# it AFTER `cdk deploy open-swe-<env>` and BEFORE the EC2/T12 box first boots, so
|
||||
# fetch-config.sh finds every REQUIRED var populated and never writes a partial .env.
|
||||
#
|
||||
# The secret list below mirrors config-store.ts SECRET_VARS (28 names — the
|
||||
# inventory's "29" double-counted JUDGE_ANTHROPIC_BASE_URL, which is config
|
||||
# (SSM), not a secret). Keep the two lists in lockstep.
|
||||
#
|
||||
# SAFETY:
|
||||
# - NO real secret values live in this file — every secret is a <FILL> placeholder.
|
||||
# Replace <FILL...> inline at run time, pipe from a vault, or export the
|
||||
# matching OPENSWE_PUT_<VAR> env var; NEVER commit real values.
|
||||
# - It does NOT touch the IaC-managed SSM params (SANDBOX_TYPE, DEFAULT_REPO_OWNER,
|
||||
# ALLOWED_GITHUB_ORGS, DEFAULT_REPO_NAME, DASHBOARD_*_URL/ORIGINS, LLM_MODEL_ID)
|
||||
# — CDK owns those; setting them here would cause drift.
|
||||
# - Secrets go to Secrets Manager; the box's instance role grants read on
|
||||
# open-swe-<env>/* and /open-swe-<env>/* (no kms:Decrypt — AWS-managed keys).
|
||||
#
|
||||
# Idempotent: put-secret-value adds a new AWSCURRENT version; put-parameter
|
||||
# --overwrite updates in place.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ENV="${1:-}"
|
||||
case "$ENV" in
|
||||
dev | prod) ;;
|
||||
*)
|
||||
echo "usage: put-config.sh <dev|prod>" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
REGION="${AWS_REGION:-${AWS_DEFAULT_REGION:-us-east-1}}"
|
||||
SECRET_PREFIX="open-swe-${ENV}/"
|
||||
SSM_PREFIX="/open-swe-${ENV}/"
|
||||
|
||||
command -v aws >/dev/null 2>&1 || { echo "put-config: 'aws' not found on PATH" >&2; exit 3; }
|
||||
|
||||
# --- helpers -----------------------------------------------------------------
|
||||
# put_secret VAR : set the value of an EXISTING secret shell open-swe-<env>/VAR.
|
||||
# Resolution order for the value: $OPENSWE_PUT_<VAR> env var, else the literal
|
||||
# <FILL> placeholder (which aborts so an unset secret is never silently shipped).
|
||||
put_secret() {
|
||||
local var="$1"
|
||||
local override_name="OPENSWE_PUT_${var}"
|
||||
local value="${!override_name:-<FILL>}"
|
||||
if [ "$value" = "<FILL>" ]; then
|
||||
echo "put-config[$ENV]: SKIP secret ${var} (no value; set ${override_name} or edit inline)" >&2
|
||||
return 0
|
||||
fi
|
||||
aws secretsmanager put-secret-value \
|
||||
--secret-id "${SECRET_PREFIX}${var}" \
|
||||
--secret-string "$value" \
|
||||
--region "$REGION" \
|
||||
--no-cli-pager >/dev/null
|
||||
echo "put-config[$ENV]: set secret ${var}"
|
||||
}
|
||||
|
||||
# put_param VAR [TYPE] : create/update an out-of-band SSM param /open-swe-<env>/VAR.
|
||||
# TYPE defaults to String; pass SecureString for anything sensitive-but-not-a-secret.
|
||||
put_param() {
|
||||
local var="$1" type="${2:-String}"
|
||||
local override_name="OPENSWE_PUT_${var}"
|
||||
local value="${!override_name:-<FILL>}"
|
||||
if [ "$value" = "<FILL>" ]; then
|
||||
echo "put-config[$ENV]: SKIP param ${var} (no value; set ${override_name} or edit inline)" >&2
|
||||
return 0
|
||||
fi
|
||||
aws ssm put-parameter \
|
||||
--name "${SSM_PREFIX}${var}" \
|
||||
--value "$value" \
|
||||
--type "$type" \
|
||||
--overwrite \
|
||||
--region "$REGION" \
|
||||
--no-cli-pager >/dev/null
|
||||
echo "put-config[$ENV]: set param ${var} (${type})"
|
||||
}
|
||||
|
||||
# --- 1) Secrets (open-swe-<env>/<VAR>) — 28 shells from config-store.ts -------
|
||||
# REQUIRED at boot (fetch-config fail-fast): DASHBOARD_JWT_SECRET,
|
||||
# TOKEN_ENCRYPTION_KEY, GITHUB_APP_PRIVATE_KEY/CLIENT_SECRET, the active provider
|
||||
# key(s) (ANTHROPIC_API_KEY + OPENAI_API_KEY by default), LANGSMITH_API_KEY_PROD,
|
||||
# and (prod) GITHUB_WEBHOOK_SECRET + SLACK_SIGNING_SECRET.
|
||||
put_secret ANTHROPIC_API_KEY # REQUIRED — primary builder provider key
|
||||
put_secret OPENAI_API_KEY # REQUIRED — default reviewer provider key
|
||||
put_secret DASHBOARD_JWT_SECRET # REQUIRED — dashboard session JWT signing
|
||||
put_secret TOKEN_ENCRYPTION_KEY # REQUIRED — Fernet key(s) for GH-token crypto
|
||||
put_secret GITHUB_APP_PRIVATE_KEY # REQUIRED — GitHub App PEM (multiline; quote it)
|
||||
put_secret GITHUB_APP_CLIENT_SECRET # REQUIRED — dashboard OAuth login
|
||||
put_secret LANGSMITH_API_KEY_PROD # REQUIRED (langsmith sandbox) — prod key
|
||||
put_secret GITHUB_WEBHOOK_SECRET # prod-REQUIRED — GitHub webhook signature
|
||||
put_secret SLACK_SIGNING_SECRET # prod-REQUIRED — Slack webhook signature
|
||||
put_secret LINEAR_WEBHOOK_SECRET # required only when Linear is wired
|
||||
# Optional / conditional secrets — set the ones this deployment actually uses.
|
||||
put_secret CORRIDOR_API_TOKEN # optional — Corridor MCP
|
||||
put_secret CORRIDOR_MCP_TOKEN # optional — Corridor MCP (alt name)
|
||||
put_secret CORRIDOR_TOKEN # optional — Corridor MCP (alt name)
|
||||
put_secret DAYTONA_API_KEY # only if SANDBOX_TYPE=daytona
|
||||
put_secret EXA_API_KEY # optional — Exa web search
|
||||
put_secret FIREWORKS_API_KEY # only if a fireworks: model is used
|
||||
put_secret GITHUB_PAT # optional — PAT fallback
|
||||
put_secret GOOGLE_API_KEY # only if a google_genai: model is used
|
||||
put_secret GROQ_API_KEY # only if a groq: model is used
|
||||
put_secret JUDGE_ANTHROPIC_API_KEY # optional — eval judge (falls back to ANTHROPIC)
|
||||
put_secret LANGSMITH_API_KEY # optional — LangSmith (dev)
|
||||
put_secret LANGCHAIN_API_KEY # optional — LangSmith alt name
|
||||
put_secret LINEAR_API_KEY # optional — Linear API
|
||||
put_secret RUNLOOP_API_KEY # only if SANDBOX_TYPE=runloop
|
||||
put_secret SLACK_BOT_TOKEN # optional — Slack bot token
|
||||
put_secret SLACK_CLIENT_SECRET # optional — Slack OAuth
|
||||
put_secret USER_ID_API_KEY_MAP # optional — JSON map user id -> API key (per-user auth)
|
||||
put_secret X_SERVICE_AUTH_JWT_SECRET # optional — service-auth JWT
|
||||
|
||||
# --- 2) Out-of-band SSM params (/open-swe-<env>/<VAR>) ------------------------
|
||||
# These are NOT created by CDK (operationally-variable / env-specific-unknown).
|
||||
# put_param creates them on first run.
|
||||
put_param DEFAULT_SANDBOX_SNAPSHOT_ID # REQUIRED (langsmith) — changes every rebuild
|
||||
put_param GITHUB_APP_ID # REQUIRED — GitHub App numeric id
|
||||
put_param GITHUB_APP_INSTALLATION_ID # REQUIRED — GitHub App installation id
|
||||
put_param GITHUB_APP_CLIENT_ID # REQUIRED — dashboard OAuth client id
|
||||
put_param GITHUB_OAUTH_PROVIDER_ID # GitHub OAuth provider id
|
||||
put_param LANGSMITH_TENANT_ID_PROD # LangSmith prod tenant id
|
||||
put_param LANGSMITH_URL_PROD # LangSmith prod URL
|
||||
put_param LANGSMITH_ENDPOINT # LangSmith API endpoint
|
||||
put_param LANGSMITH_ENDPOINT_PROD # LangSmith prod API endpoint
|
||||
put_param LANGSMITH_HOST_API_URL # LangSmith host API URL
|
||||
put_param LANGGRAPH_URL # LangGraph server URL
|
||||
put_param LANGGRAPH_URL_PROD # LangGraph server URL (prod)
|
||||
put_param LANGCHAIN_REVISION_ID # LangChain revision id
|
||||
put_param SLACK_CLIENT_ID # Slack OAuth client id
|
||||
put_param SLACK_TEAM_ID # Slack workspace/team id
|
||||
put_param SLACK_BOT_USER_ID # Slack bot user id
|
||||
put_param SLACK_BOT_USERNAME # Slack bot username
|
||||
put_param SLACK_REPO_OWNER # Slack default repo owner
|
||||
put_param SLACK_REPO_NAME # Slack default repo name
|
||||
put_param CONFIGURED_ADMINS # dashboard admin GitHub logins (comma list)
|
||||
put_param OBSERVABILITY_AUTHORIZED_EMAILS # observability allowlist (comma list)
|
||||
put_param PUBLIC_REPO_ORG_GATE # public-repo trigger gate (org name; empty=off)
|
||||
put_param ALLOWED_GITHUB_REPOS # extra repo allowlist (comma list)
|
||||
put_param LLM_FALLBACK_MODEL_ID # optional — model fallback id
|
||||
put_param DATADOG_MCP_TOOLSETS # optional — Datadog MCP toolsets
|
||||
put_param NOTION_MCP_CLIENT_NAME # optional — Notion MCP client name
|
||||
put_param API_STANDARDS_SKILL_HANDLE # optional — API standards skill handle
|
||||
put_param REPO_SNAPSHOT_BASE_IMAGE # optional — repo snapshot base image
|
||||
put_param REPO_SNAPSHOT_BUILD_TIMEOUT_SECONDS # optional — snapshot build timeout
|
||||
put_param REPO_SNAPSHOT_STALE_BUILD_SECONDS # optional — snapshot stale threshold
|
||||
|
||||
echo "put-config[$ENV]: done. Verify with fetch-config.sh ${ENV} before first boot."
|
||||
|
|
@ -19,6 +19,7 @@ infra/
|
|||
│ └── 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)
|
||||
│ └── ami-cache.ts # cached ARM64 AL2023 helper + EBS/AMI discipline docs
|
||||
├── test/
|
||||
│ └── kebab-naming-aspect.test.ts # jest: Aspect passes conforming names, flags bad ones
|
||||
|
|
@ -73,6 +74,71 @@ Manager `a/b`, SSM `/a/b`, log groups `/aws/.../x`) are validated per `/`-segmen
|
|||
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:
|
||||
|
||||
1. **Secret shells (Secrets Manager) — 27 secrets.** Created value-LESS (an L1
|
||||
`CfnSecret` with NEITHER `secretString` NOR `generateSecretString`, which
|
||||
CloudFormation creates as an empty secret). The real value is set **out-of-band**
|
||||
(`put-config.sh`) — CDK never owns it, so a later `cdk deploy` can never clobber
|
||||
it. `UpdateReplacePolicy/DeletionPolicy: Retain` so a teardown can't destroy
|
||||
operator-set secret material. AWS-managed key (no CMK — matches the instance
|
||||
role, which omits `kms: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`).
|
||||
|
||||
2. **IaC-managed SSM config — 8 params, real values owned in code:**
|
||||
|
||||
| Param | dev | prod |
|
||||
|---|---|---|
|
||||
| `SANDBOX_TYPE` | `langsmith` | `langsmith` |
|
||||
| `DEFAULT_REPO_OWNER` | `Sea-Haven-Industries` | `Sea-Haven-Industries` |
|
||||
| `ALLOWED_GITHUB_ORGS` | `Sea-Haven-Industries` | `Sea-Haven-Industries` |
|
||||
| `DEFAULT_REPO_NAME` | `open-swe-pilot` *(confirm)* | `open-swe-pilot` *(confirm)* |
|
||||
| `DASHBOARD_BASE_URL` | `https://openswe-dev.seahaven.com` *(confirm host)* | `https://openswe.seahaven.com` *(confirm host)* |
|
||||
| `DASHBOARD_API_BASE_URL` | same as base | same as base |
|
||||
| `DASHBOARD_ALLOWED_ORIGINS` | same as base | same as base |
|
||||
| `LLM_MODEL_ID` | `anthropic:claude-opus-4-8` *(confirm)* | `anthropic:claude-opus-4-8` *(confirm)* |
|
||||
|
||||
3. **Out-of-band SSM config — NOT created by CDK.** Operationally-variable or
|
||||
env-specific-unknown values listed in `OUT_OF_BAND_SSM` and populated by
|
||||
`put-config.sh`. The keystone is `DEFAULT_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:
|
||||
|
||||
```bash
|
||||
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).
|
||||
|
||||
## AMI cache discipline (EBS-fix plumbing — stub for T12)
|
||||
|
||||
`cachedArm64AmazonLinux2023()` (in `lib/constructs/ami-cache.ts`) returns an
|
||||
|
|
|
|||
|
|
@ -40,6 +40,22 @@ const NAME_PROPERTY_KEYS = [
|
|||
|
||||
const NAME_KEYS_LC = new Set(NAME_PROPERTY_KEYS.map((k) => k.toLowerCase()));
|
||||
|
||||
/**
|
||||
* Resource types whose physical NAME is a REQUIRED deviation from kebab-case:
|
||||
* the open-swe config store names secrets `open-swe-<env>/<ENV_VAR_NAME>` and SSM
|
||||
* params `/open-swe-<env>/<ENV_VAR_NAME>`, where the last segment is the LITERAL
|
||||
* UPPER_SNAKE environment-variable name. The boot hook
|
||||
* (deploy/seahaven/fetch-config.sh) strips the prefix and exports that segment
|
||||
* verbatim, so a lossless store→env round-trip needs the exact env-var name —
|
||||
* it cannot be kebab-cased. These two resource types are therefore exempt; every
|
||||
* OTHER explicitly-named resource is still validated. (The `open-swe-<env>`
|
||||
* prefix is code-generated from `prefix(env)` and is always kebab-case.)
|
||||
*/
|
||||
const KEBAB_EXEMPT_RESOURCE_TYPES = new Set([
|
||||
"AWS::SecretsManager::Secret",
|
||||
"AWS::SSM::Parameter",
|
||||
]);
|
||||
|
||||
/**
|
||||
* `true` when every non-empty "/"-delimited segment is kebab-case.
|
||||
*
|
||||
|
|
@ -75,6 +91,11 @@ export class KebabNamingAspect implements IAspect {
|
|||
}
|
||||
|
||||
if (node instanceof CfnResource) {
|
||||
// The config store's Secret/Parameter names carry the literal UPPER_SNAKE
|
||||
// env-var name per the fetch-config naming contract — a required deviation.
|
||||
if (KEBAB_EXEMPT_RESOURCE_TYPES.has(node.cfnResourceType)) {
|
||||
return;
|
||||
}
|
||||
// `_cfnProperties` is the props as set on the L1; resolve to collapse any
|
||||
// intrinsic tokens (refs/getatt) so only literal strings are checked.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
|
|
|||
235
infra/lib/constructs/config-store.ts
Normal file
235
infra/lib/constructs/config-store.ts
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
import * as cdk from "aws-cdk-lib";
|
||||
import * as secretsmanager from "aws-cdk-lib/aws-secretsmanager";
|
||||
import * as ssm from "aws-cdk-lib/aws-ssm";
|
||||
import { Construct } from "constructs";
|
||||
import { EnvName } from "../config";
|
||||
|
||||
/**
|
||||
* Config / secret "shells" for the boot hook (`deploy/seahaven/fetch-config.sh`).
|
||||
*
|
||||
* The naming contract (source of truth: the T9 env/secret/config inventory + the
|
||||
* fetch-config header) is LITERAL env-var names as the last path segment:
|
||||
*
|
||||
* Secrets open-swe-<env>/<ENV_VAR_NAME> (AWS Secrets Manager)
|
||||
* Config /open-swe-<env>/<ENV_VAR_NAME> (AWS SSM Parameter Store, FLAT)
|
||||
*
|
||||
* fetch-config reads secrets with `batch-get-secret-value --filters
|
||||
* Key=name,Values=open-swe-<env>/` and config with `get-parameters-by-path
|
||||
* --path /open-swe-<env>/` (NON-recursive), then strips the prefix so the last
|
||||
* segment IS the exported variable name. So these resources MUST carry the
|
||||
* UPPER_SNAKE env-var name verbatim — which is why both resource types are
|
||||
* exempted from the kebab-naming Aspect (see aspects/kebab-naming-aspect.ts).
|
||||
*
|
||||
* Three buckets:
|
||||
*
|
||||
* 1. SECRETS_SHELLS — the 29 secrets. Created as value-LESS shells (an L1
|
||||
* `CfnSecret` with NEITHER `secretString` NOR `generateSecretString`, which
|
||||
* CloudFormation creates as an empty secret with no version). The real value
|
||||
* is set out-of-band via `deploy/seahaven/put-config.sh` (put-secret-value)
|
||||
* BEFORE the box boots. Because CDK never owns the value, a later
|
||||
* `cdk deploy` can never clobber the operator-set value. AWS-managed key
|
||||
* (alias/aws/secretsmanager) — no CMK, matching the instance role which
|
||||
* deliberately omits kms:Decrypt.
|
||||
*
|
||||
* 2. IAC_MANAGED_SSM — stable / derivable config. Real values are owned here in
|
||||
* IaC (one StringParameter each) so they are reproducible and reviewed.
|
||||
*
|
||||
* 3. Out-of-band SSM (NOT created here) — operationally-variable or
|
||||
* env-specific-unknown config (e.g. DEFAULT_SANDBOX_SNAPSHOT_ID, which
|
||||
* changes on every snapshot rebuild and would be clobbered by a deploy if it
|
||||
* were CDK-managed; GitHub App ids; Slack ids; LangSmith tenant/urls). These
|
||||
* are listed in OUT_OF_BAND_SSM purely for documentation and are set by
|
||||
* `put-config.sh`, never by CDK.
|
||||
*/
|
||||
|
||||
/** The 29 Secrets Manager secret VAR names (T9 inventory SECRETS table). */
|
||||
export const SECRET_VARS: readonly string[] = [
|
||||
"ANTHROPIC_API_KEY",
|
||||
"CORRIDOR_API_TOKEN",
|
||||
"CORRIDOR_MCP_TOKEN",
|
||||
"CORRIDOR_TOKEN",
|
||||
"DASHBOARD_JWT_SECRET",
|
||||
"DAYTONA_API_KEY",
|
||||
"EXA_API_KEY",
|
||||
"FIREWORKS_API_KEY",
|
||||
"GITHUB_APP_CLIENT_SECRET",
|
||||
"GITHUB_APP_PRIVATE_KEY",
|
||||
"GITHUB_PAT",
|
||||
"GITHUB_WEBHOOK_SECRET",
|
||||
"GOOGLE_API_KEY",
|
||||
"GROQ_API_KEY",
|
||||
"JUDGE_ANTHROPIC_API_KEY",
|
||||
"LANGSMITH_API_KEY",
|
||||
"LANGSMITH_API_KEY_PROD",
|
||||
"LANGCHAIN_API_KEY",
|
||||
"LINEAR_API_KEY",
|
||||
"LINEAR_WEBHOOK_SECRET",
|
||||
"OPENAI_API_KEY",
|
||||
"RUNLOOP_API_KEY",
|
||||
"SLACK_BOT_TOKEN",
|
||||
"SLACK_CLIENT_SECRET",
|
||||
"SLACK_SIGNING_SECRET",
|
||||
"TOKEN_ENCRYPTION_KEY",
|
||||
"USER_ID_API_KEY_MAP",
|
||||
"X_SERVICE_AUTH_JWT_SECRET",
|
||||
] as const;
|
||||
// 28 secret shells. The T9 inventory header said "29" vs 27 enumerated; reconciled
|
||||
// (Adam confirm 2026-06-26): USER_ID_API_KEY_MAP (maps user ids -> API keys; flagged
|
||||
// sensitive by the T5 security review) is a SECRET and is included here.
|
||||
// JUDGE_ANTHROPIC_BASE_URL is a URL (non-sensitive config, eval-only) -> SSM/default,
|
||||
// NOT a secret. So the inventory's "29" was effectively a miscount.
|
||||
|
||||
/** Short, value-free descriptions for the secret shells (no secret material). */
|
||||
const SECRET_DESCRIPTIONS: Record<string, string> = {
|
||||
ANTHROPIC_API_KEY: "Claude LLM API key (primary builder provider).",
|
||||
CORRIDOR_API_TOKEN: "Corridor MCP token (optional).",
|
||||
CORRIDOR_MCP_TOKEN: "Corridor MCP token alt name (optional).",
|
||||
CORRIDOR_TOKEN: "Corridor MCP token alt name (optional).",
|
||||
DASHBOARD_JWT_SECRET: "JWT signing secret for dashboard session cookies (REQUIRED).",
|
||||
DAYTONA_API_KEY: "Daytona sandbox key (only if SANDBOX_TYPE=daytona).",
|
||||
EXA_API_KEY: "Exa web-search key (optional).",
|
||||
FIREWORKS_API_KEY: "Fireworks LLM key (only if a fireworks: model is used).",
|
||||
GITHUB_APP_CLIENT_SECRET: "GitHub App OAuth client secret (dashboard login).",
|
||||
GITHUB_APP_PRIVATE_KEY: "GitHub App private key PEM (installation-token minting).",
|
||||
GITHUB_PAT: "GitHub PAT fallback (optional).",
|
||||
GITHUB_WEBHOOK_SECRET: "GitHub webhook signature secret (prod-required).",
|
||||
GOOGLE_API_KEY: "Google GenAI key (only if a google_genai: model is used).",
|
||||
GROQ_API_KEY: "Groq LLM key (only if a groq: model is used).",
|
||||
JUDGE_ANTHROPIC_API_KEY: "Eval judge key (optional; falls back to ANTHROPIC_API_KEY).",
|
||||
LANGSMITH_API_KEY: "LangSmith key (dev).",
|
||||
LANGSMITH_API_KEY_PROD: "LangSmith key (prod / deployed sandbox).",
|
||||
LANGCHAIN_API_KEY: "LangSmith key alt name (fallback).",
|
||||
LINEAR_API_KEY: "Linear API key (optional).",
|
||||
LINEAR_WEBHOOK_SECRET: "Linear webhook signature secret (required when Linear is wired).",
|
||||
OPENAI_API_KEY: "OpenAI key (primary reviewer provider).",
|
||||
RUNLOOP_API_KEY: "Runloop sandbox key (only if SANDBOX_TYPE=runloop).",
|
||||
SLACK_BOT_TOKEN: "Slack bot token (optional).",
|
||||
SLACK_CLIENT_SECRET: "Slack OAuth client secret.",
|
||||
SLACK_SIGNING_SECRET: "Slack webhook signing secret (prod-required).",
|
||||
TOKEN_ENCRYPTION_KEY: "Fernet key(s) for per-user GitHub-token encryption (REQUIRED).",
|
||||
USER_ID_API_KEY_MAP: "JSON map of user id -> API key for per-user auth (optional, sensitive).",
|
||||
X_SERVICE_AUTH_JWT_SECRET: "Service-auth JWT secret (optional).",
|
||||
};
|
||||
|
||||
/**
|
||||
* IaC-managed SSM config: stable / derivable values owned in code, per env.
|
||||
* Values are functions of envName so dev/prod render correct hosts.
|
||||
*
|
||||
* Anything operationally-variable or env-specific-unknown is deliberately NOT
|
||||
* here — see OUT_OF_BAND_SSM.
|
||||
*/
|
||||
export function iacManagedSsm(env: EnvName): Record<string, string> {
|
||||
// Public host = seahaven.com (the migration's new AWS public face; confirmed by
|
||||
// recon: seahaven.com Route53 zone + *.seahaven.com ACM cert are live on the ALB
|
||||
// — distinct from the on-prem seahavenind.com). dev = openswe-dev, prod = openswe.
|
||||
const host = `https://openswe${env === "dev" ? "-dev" : ""}.seahaven.com`;
|
||||
return {
|
||||
// Sandbox provider — plan keeps stock langsmith (T9). Stable.
|
||||
SANDBOX_TYPE: "langsmith",
|
||||
// Sea Haven org pin. fetch-config ALSO hard-pins this at boot, but owning the
|
||||
// real value here keeps SSM self-consistent rather than blank.
|
||||
DEFAULT_REPO_OWNER: "Sea-Haven-Industries",
|
||||
// Repo allowlist (comma list) — the Sea Haven org. Stable/derivable.
|
||||
ALLOWED_GITHUB_ORGS: "Sea-Haven-Industries",
|
||||
// Pilot repo (project memory: Sea-Haven-Industries/open-swe-pilot).
|
||||
DEFAULT_REPO_NAME: "open-swe-pilot",
|
||||
// Dashboard URLs — derived from the public host.
|
||||
DASHBOARD_BASE_URL: host,
|
||||
DASHBOARD_API_BASE_URL: host,
|
||||
DASHBOARD_ALLOWED_ORIGINS: host,
|
||||
// Primary builder model (project memory team_settings: anthropic:claude-opus-4-8).
|
||||
LLM_MODEL_ID: "anthropic:claude-opus-4-8",
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Out-of-band SSM config: NOT created by CDK. Listed for documentation and for
|
||||
* `put-config.sh` to populate before the box boots. Each MUST stay out of IaC
|
||||
* because its value is operationally-variable or env-specific and unknown at
|
||||
* synth time — making it CDK-managed would either clobber the operator value on
|
||||
* the next deploy (e.g. DEFAULT_SANDBOX_SNAPSHOT_ID) or hardcode a secret-ish id.
|
||||
*/
|
||||
export const OUT_OF_BAND_SSM: readonly string[] = [
|
||||
// Sandbox snapshot id — changes on EVERY snapshot rebuild. MUST NOT be
|
||||
// CDK-managed or a deploy clobbers it. Required for SANDBOX_TYPE=langsmith.
|
||||
"DEFAULT_SANDBOX_SNAPSHOT_ID",
|
||||
// GitHub App identifiers — set when the per-env GitHub App is created.
|
||||
"GITHUB_APP_ID",
|
||||
"GITHUB_APP_CLIENT_ID",
|
||||
"GITHUB_APP_INSTALLATION_ID",
|
||||
"GITHUB_OAUTH_PROVIDER_ID",
|
||||
// LangSmith deployment coordinates (prod tenant/urls/endpoints).
|
||||
"LANGSMITH_TENANT_ID_PROD",
|
||||
"LANGSMITH_URL_PROD",
|
||||
"LANGSMITH_ENDPOINT",
|
||||
"LANGSMITH_ENDPOINT_PROD",
|
||||
"LANGSMITH_HOST_API_URL",
|
||||
"LANGGRAPH_URL",
|
||||
"LANGGRAPH_URL_PROD",
|
||||
"LANGCHAIN_REVISION_ID",
|
||||
// Slack workspace ids — set after the Slack app is installed.
|
||||
"SLACK_CLIENT_ID",
|
||||
"SLACK_TEAM_ID",
|
||||
"SLACK_BOT_USER_ID",
|
||||
"SLACK_BOT_USERNAME",
|
||||
"SLACK_REPO_OWNER",
|
||||
"SLACK_REPO_NAME",
|
||||
// Access / observability allowlists — operator-curated.
|
||||
"CONFIGURED_ADMINS",
|
||||
"OBSERVABILITY_AUTHORIZED_EMAILS",
|
||||
"PUBLIC_REPO_ORG_GATE",
|
||||
"ALLOWED_GITHUB_REPOS",
|
||||
// Optional integrations + tuning knobs (left to code defaults unless set).
|
||||
"LLM_FALLBACK_MODEL_ID",
|
||||
"DATADOG_MCP_TOOLSETS",
|
||||
"NOTION_MCP_CLIENT_NAME",
|
||||
"API_STANDARDS_SKILL_HANDLE",
|
||||
"REPO_SNAPSHOT_BASE_IMAGE",
|
||||
"REPO_SNAPSHOT_BUILD_TIMEOUT_SECONDS",
|
||||
"REPO_SNAPSHOT_STALE_BUILD_SECONDS",
|
||||
] as const;
|
||||
|
||||
export interface ConfigStoreProps {
|
||||
readonly envName: EnvName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-env Secrets Manager + SSM Parameter Store shells the boot hook reads.
|
||||
* Instantiated from OpenSweStack. Synth-able now (T11); values populated
|
||||
* out-of-band BEFORE the EC2/T12 deploy. See infra/README.md "Config store".
|
||||
*/
|
||||
export class ConfigStore extends Construct {
|
||||
public readonly secrets: secretsmanager.CfnSecret[] = [];
|
||||
public readonly params: ssm.StringParameter[] = [];
|
||||
|
||||
constructor(scope: Construct, id: string, props: ConfigStoreProps) {
|
||||
super(scope, id);
|
||||
const env = props.envName;
|
||||
|
||||
// --- 1) Secret shells (value-LESS; populated out-of-band) ----------------
|
||||
for (const varName of SECRET_VARS) {
|
||||
const secret = new secretsmanager.CfnSecret(this, `Secret-${varName}`, {
|
||||
name: `open-swe-${env}/${varName}`,
|
||||
description: SECRET_DESCRIPTIONS[varName] ?? `open-swe ${varName}`,
|
||||
// Deliberately NO secretString / generateSecretString: CloudFormation
|
||||
// creates an empty secret, so the out-of-band value is never clobbered.
|
||||
});
|
||||
// RETAIN: a stack teardown must not destroy operator-set secret material.
|
||||
secret.applyRemovalPolicy(cdk.RemovalPolicy.RETAIN);
|
||||
this.secrets.push(secret);
|
||||
}
|
||||
|
||||
// --- 2) IaC-managed SSM config (real, derivable values) ------------------
|
||||
const managed = iacManagedSsm(env);
|
||||
for (const [varName, value] of Object.entries(managed)) {
|
||||
this.params.push(
|
||||
new ssm.StringParameter(this, `Param-${varName}`, {
|
||||
parameterName: `/open-swe-${env}/${varName}`,
|
||||
stringValue: value,
|
||||
description: `IaC-managed open-swe ${varName} (${env}).`,
|
||||
tier: ssm.ParameterTier.STANDARD,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import * as cdk from "aws-cdk-lib";
|
||||
import { Construct } from "constructs";
|
||||
import { EnvName, prefix } from "./config";
|
||||
import { ConfigStore } from "./constructs/config-store";
|
||||
import { InstanceRole } from "./constructs/instance-role";
|
||||
import {
|
||||
AL2023_ARM64_SSM_CONTEXT_KEY,
|
||||
|
|
@ -23,6 +24,7 @@ export interface OpenSweStackProps extends cdk.StackProps {
|
|||
*/
|
||||
export class OpenSweStack extends cdk.Stack {
|
||||
public readonly instanceRole: InstanceRole;
|
||||
public readonly configStore: ConfigStore;
|
||||
|
||||
constructor(scope: Construct, id: string, props: OpenSweStackProps) {
|
||||
super(scope, id, props);
|
||||
|
|
@ -37,6 +39,12 @@ export class OpenSweStack extends cdk.Stack {
|
|||
// Per-env least-privilege EC2 instance role (open-swe-<env>-instance-role).
|
||||
this.instanceRole = new InstanceRole(this, "Instance", envName);
|
||||
|
||||
// Secrets Manager + SSM Parameter Store shells the boot hook reads
|
||||
// (deploy/seahaven/fetch-config.sh). Secret shells are value-less and
|
||||
// populated out-of-band; IaC-managed SSM params carry real derivable values.
|
||||
// The instance role already grants read on open-swe-<env>/* + /open-swe-<env>/*.
|
||||
this.configStore = new ConfigStore(this, "Config", { envName });
|
||||
|
||||
// AMI cache discipline (see lib/constructs/ami-cache.ts). T3 is synth-only:
|
||||
// resolve + surface the pinned AMI id ONLY when it is already cached in
|
||||
// cdk.context.json, so synth never makes a live SSM call. T12 consumes
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import * as cdk from "aws-cdk-lib";
|
||||
import { Annotations, Match } from "aws-cdk-lib/assertions";
|
||||
import { Annotations, Match, Template } from "aws-cdk-lib/assertions";
|
||||
import * as iam from "aws-cdk-lib/aws-iam";
|
||||
import { KebabNamingAspect, isKebabCase } from "../lib/aspects/kebab-naming-aspect";
|
||||
import { OpenSweIamStack } from "../lib/open-swe-iam-stack";
|
||||
|
|
@ -53,6 +53,31 @@ describe("KebabNamingAspect", () => {
|
|||
}
|
||||
});
|
||||
|
||||
it("exempts Secrets Manager + SSM names that carry the literal env-var segment", () => {
|
||||
// The config store names a secret open-swe-dev/ANTHROPIC_API_KEY and a param
|
||||
// /open-swe-dev/SANDBOX_TYPE — the UPPER_SNAKE last segment is a REQUIRED
|
||||
// deviation from kebab (fetch-config naming contract). Must NOT be flagged.
|
||||
const app = new cdk.App();
|
||||
cdk.Aspects.of(app).add(new KebabNamingAspect());
|
||||
|
||||
const dev = new OpenSweStack(app, "OpenSweDevStack", {
|
||||
stackName: "open-swe-dev",
|
||||
env: ENV,
|
||||
envName: "dev",
|
||||
});
|
||||
|
||||
const tpl = Template.fromStack(dev);
|
||||
// Sanity: the shells actually render with the literal env-var names.
|
||||
tpl.hasResourceProperties("AWS::SecretsManager::Secret", {
|
||||
Name: "open-swe-dev/ANTHROPIC_API_KEY",
|
||||
});
|
||||
tpl.hasResourceProperties("AWS::SSM::Parameter", {
|
||||
Name: "/open-swe-dev/SANDBOX_TYPE",
|
||||
Value: "langsmith",
|
||||
});
|
||||
Annotations.fromStack(dev).hasNoError("*", Match.anyValue());
|
||||
});
|
||||
|
||||
it("flags a deliberately non-kebab-case resource name", () => {
|
||||
const app = new cdk.App();
|
||||
const stack = new cdk.Stack(app, "ConformingStackId", { stackName: "open-swe-test", env: ENV });
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue