mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 17:23:15 +00:00
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.
235 lines
10 KiB
TypeScript
235 lines
10 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`;
|
|
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,
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
}
|