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-/ (AWS Secrets Manager) * Config /open-swe-/ (AWS SSM Parameter Store, FLAT) * * fetch-config reads secrets with `batch-get-secret-value --filters * Key=name,Values=open-swe-/` and config with `get-parameters-by-path * --path /open-swe-/` (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 = { 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 { // 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`; // Repo targeting is PER-ENV: dev drives the disposable sandbox repo in the // dedicated seahaven-open-swe-dev org (isolates dev-agent activity from the real // Sea Haven org); prod stays on the Sea Haven org pilot repo. fetch-config's owner // GUARD honors this value (rejecting only blank / the upstream langchain-ai org). const repo = env === "dev" ? { owner: "seahaven-open-swe-dev", name: "openswe-dev-sandbox" } : { owner: "Sea-Haven-Industries", name: "open-swe-pilot" }; const managed: Record = { // Sandbox provider — plan keeps stock langsmith (T9). Stable. SANDBOX_TYPE: "langsmith", DEFAULT_REPO_OWNER: repo.owner, // Repo-owner allowlist (comma list) — the env's org. The app gates triggers on this. ALLOWED_GITHUB_ORGS: repo.owner, DEFAULT_REPO_NAME: repo.name, // 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", }; // Dev e2e smoke: seed the owner's user_mapping so an @openswe comment from the // triggering GitHub login resolves (an unmapped commenter is silently skipped). // Dev-only — prod seeds its mappings via its own operator config. login:email. if (env === "dev") { managed.SEED_USER_MAPPINGS = "amoussa1229:adam@seahavenind.com"; } return managed; } /** * 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, }), ); } } }