mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 12:43:16 +00:00
Some checks failed
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
Build & publish app artifacts / Publish + deploy (dev) (push) Has been cancelled
Build & publish app artifacts / Publish + deploy (prod) (push) Has been cancelled
Infra CD / Infra CI (pre-deploy) (push) Has been cancelled
Infra CD / Deploy open-swe-dev (push) Has been cancelled
Infra CD / Deploy open-swe-prod (push) Has been cancelled
* feat: switch model providers to AWS Bedrock (Claude) and Fireworks (non-Claude)
Migrate off direct provider APIs: AWS Bedrock for Anthropic/Claude via the
cross-region inference profile us.anthropic.claude-opus-4-8, Fireworks AI for
all non-Claude models. Drop OpenAI (gpt-5.5) and Google (gemini-3.5-flash)
entirely. DEFAULT_MODEL_ID is now Bedrock Claude; all Fireworks models stay
freely selectable for the agent and reviewer graphs and via team/profile
defaults.
- pyproject: add langchain-aws (ChatBedrockConverse + boto3)
- options.py: Bedrock Claude entry + default; remove openai/google entries
- model.py: bedrock_converse provider_model_kwargs (effort -> thinking budget),
region pin in make_model, bedrock<->fireworks fallback pairing, AWS_REGION/
FIREWORKS_API_KEY local-dev validation
- server.py: provider-aware fallback kwargs build
- sanitize_thinking_blocks: also sanitize ChatBedrockConverse thinking blocks
- model_fallback: treat transient botocore ClientError codes as fallback-worthy
- eval_jobs: repoint hardcoded eval model id to Bedrock Claude
- tests: repoint dropped model ids; drop obsolete google test module
* fix(bedrock): use adaptive thinking + output_config.effort for Opus 4.8
The handoff spec wired Bedrock Converse thinking as
{type: enabled, budget_tokens: N}, but Opus 4.7+ rejects that with a
ValidationException: thinking.type "enabled" is not supported; it requires
thinking.type "adaptive" plus output_config.effort. Verified by live invoke
against us.anthropic.claude-opus-4-8 (account 328440206208, us-east-1):
the enabled+budget shape 400s, adaptive+effort returns normally.
Map profile effort to additional_model_request_fields:
{thinking: {type: adaptive, display: summarized},
output_config: {effort: <low|medium|high|xhigh|max>}}
reusing anthropic_thinking_for/anthropic_effort_for. Update the two
subagent-model tests asserting the old shape.
* fix(deploy): seed Bedrock/Fireworks models, not the dropped anthropic:/openai: ids
Model selection is store-driven, so seed_store.sh's team_settings/default seed is
what runs in prod. It still seeded the removed providers, which would fail at runtime
after the migration:
- agent/builder: anthropic:claude-opus-4-8 -> bedrock_converse:us.anthropic.claude-opus-4-8
- reviewer: openai:gpt-5.5 (dropped) -> bedrock_converse:us.anthropic.claude-opus-4-8
(set SEED_REVIEWER_MODEL to a Fireworks model for a cross-family reviewer)
- fetch-config REQUIRED_PROVIDER_KEYS default ANTHROPIC_API_KEY,OPENAI_API_KEY ->
FIREWORKS_API_KEY (Bedrock auths via host IAM role; dropping the old keys would
otherwise fail-fast at boot)
- docs (DEPLOYMENT/ROTATION/put-config) updated to match.
Surfaced by the cross-family review + verified against deploy/.
* fix(bedrock): security-review NITs — region resolution, error sanitization, reasoning-block strip
From /sh-security-review (all confirmed-low):
- model.py: resolve region from AWS_REGION OR AWS_DEFAULT_REGION (matches
validate_local_dev_llm_config) so the validated region is the one actually used.
- model_fallback.py: sanitize Bedrock AccessDenied/ResourceNotFound errors to the
error code only, so the role ARN + account id in the raw botocore message never
reach logs or the user channel (CWE-209).
- sanitize_thinking_blocks.py: also strip empty Bedrock reasoning_content blocks
(Converse emits reasoning_content, not thinking) so the middleware is not a no-op
on Bedrock; + unit tests. (Empty blocks replay fine today; defensive.)
* deploy(bedrock): grant instance-role Bedrock invoke + repoint LLM_MODEL_ID / eval model ids
Deployment-readiness for the Bedrock migration (PR #62):
- instance-role.ts: least-privilege bedrock:InvokeModel[WithResponseStream] on the
us.anthropic.claude-opus-4-8 inference-profile ARN + the foundation-model ARN in
each routed region (us-east-1/2, us-west-2). The model runs in the server process
on the box, so the EC2 instance role is the principal. Simulator-verified (allowed
for opus-4-8, implicitDeny for other models) and synth-verified. Passed the
mandatory GPT-4.1 IAM cross-review (no blockers, least-privilege confirmed).
- config-store.ts: IaC SSM LLM_MODEL_ID anthropic:claude-opus-4-8 ->
bedrock_converse:us.anthropic.claude-opus-4-8. This SSM value overrides
seed_store.sh's default via pick precedence, so the seed-script fix alone was
insufficient — both sources now point at the supported Bedrock id.
- infra/README.md + evals/reviewer/config.toml: repoint stale anthropic:/google_genai:
ids to the Bedrock id (config.toml's model_id was an active, now-broken value).
AWS_REGION is already wired via user-data.sh (IMDS -> boot.env), so no change needed there.
* chore(secrets): drop OPENAI/GOOGLE/GROQ key shells (revoked, providers removed)
Those three providers were dropped in the Bedrock/Fireworks migration and their keys
revoked; the live Secrets Manager objects (open-swe-{dev,prod}/{OPENAI,GOOGLE,GROQ}_API_KEY)
were deleted (7-day recovery). Remove them from the IaC so a future cdk deploy does not
recreate the shells, and from fetch-config's mirror array so boot stops requesting them:
- config-store.ts SECRET_VARS + descriptions (28 -> 25 shells)
- fetch-config.sh SECRET_VARS array (kept in lockstep)
- put-config.sh: drop the put_secret lines; ANTHROPIC_API_KEY re-labelled optional
(eval judge only — Bedrock builder/reviewer auth via the host IAM role).
REQUIRED_PROVIDER_KEYS is not set in SSM, so it uses the FIREWORKS_API_KEY default.
265 lines
13 KiB
TypeScript
265 lines
13 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",
|
||
"JUDGE_ANTHROPIC_API_KEY",
|
||
"LANGSMITH_API_KEY",
|
||
"LANGSMITH_API_KEY_PROD",
|
||
"LANGCHAIN_API_KEY",
|
||
"LINEAR_API_KEY",
|
||
"LINEAR_WEBHOOK_SECRET",
|
||
"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;
|
||
// 25 secret shells (OPENAI_API_KEY / GOOGLE_API_KEY / GROQ_API_KEY removed in the
|
||
// Bedrock/Fireworks migration — those providers are dropped from SUPPORTED_MODELS and
|
||
// their keys revoked + secret objects deleted). 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).",
|
||
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).",
|
||
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. seed_store.sh's `pick` precedence is
|
||
// OPENSWE_AGENT_MODEL > SEED_AGENT_MODEL > LLM_MODEL_ID > script default, so this
|
||
// SSM value overrides the seed-script default — it MUST be a supported id. Post
|
||
// Bedrock/Fireworks migration the only Bedrock-Claude id is the inference profile;
|
||
// `anthropic:claude-opus-4-8` was removed from SUPPORTED_MODELS.
|
||
LLM_MODEL_ID: "bedrock_converse:us.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,
|
||
}),
|
||
);
|
||
}
|
||
}
|
||
}
|