mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 13:53:15 +00:00
Some checks are pending
Infra CD / Infra CI (pre-deploy) (push) Waiting to run
Infra CD / Deploy open-swe-dev (push) Blocked by required conditions
Infra CD / Deploy open-swe-prod (push) Blocked by required conditions
CI / Lint (push) Waiting to run
CI / Format check (push) Waiting to run
CI / Unit tests (push) Waiting to run
CI / Playwright E2E (push) Waiting to run
RETAIN + a fixed secret name means a failed FIRST create leaves empty secret shells behind when the stack rolls back. The shells keep the global `open-swe-<env>/<VAR>` names, so every later create fails with `AlreadyExists`, and a plain delete-secret keeps the name reserved for the recovery window rather than freeing it. Record the trap and the force-delete recovery (only for empty shells) in the config-store construct and the infra README so the next teardown/rebuild, secret logical-id change, or new-env stand-up does not rediscover it the hard way. Prod's first deploy hit this on 2026-06-29: 28 orphaned shells from an earlier failed create reserved the names and had to be force-deleted before the stack would create.
264 lines
12 KiB
TypeScript
264 lines
12 KiB
TypeScript
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`;
|
||
// 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<string, string> = {
|
||
// 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.
|
||
//
|
||
// GOTCHA — RETAIN + fixed name orphans these shells on a FAILED FIRST
|
||
// CREATE. If the stack's initial create fails and rolls back, RETAIN keeps
|
||
// the shells instead of deleting them; the stack is then gone but the
|
||
// secrets survive, still holding the global `open-swe-<env>/<VAR>` names.
|
||
// Every later create then fails with `AlreadyExists` (and a normal
|
||
// delete-secret keeps the name reserved for the 7–30 day recovery window,
|
||
// so it does NOT clear the deadlock). Recovery: before re-creating the
|
||
// stack, force-delete the orphans so the names free immediately, e.g.
|
||
// aws secretsmanager list-secrets --filters Key=name,Values=open-swe-<env>/ \
|
||
// --query 'SecretList[].Name' --output text | tr '\t' '\n' | while read n; do
|
||
// aws secretsmanager delete-secret --secret-id "$n" \
|
||
// --force-delete-without-recovery; done
|
||
// Only force-delete shells that are EMPTY (no value version) — a populated
|
||
// secret holds real operator material. This bites on teardown/rebuild, a
|
||
// secret logical-id change/refactor, or standing up a new env — NOT on
|
||
// routine updates of an already-created stack. (Hit on prod 2026-06-29.)
|
||
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,
|
||
}),
|
||
);
|
||
}
|
||
}
|
||
}
|