Merge branch 'dev' into fix/promote-dev-to-main-push

This commit is contained in:
Adam Moussa 2026-06-28 20:45:59 -04:00 • committed by GitHub
commit 3d8128ee18
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 72 additions and 49 deletions

View file

@ -158,34 +158,64 @@ while IFS=$'\t' read -r nb vb; do
done < <(jq -r '.Parameters[] | (.Name|@base64) + "\t" + (.Value|@base64)' <<<"$ssm_json")
log "loaded ${ssm_count} config param(s) from SSM"
# 2) Secrets Manager (sensitive values). batch-get-secret-value filters by name
# prefix; one secret per env var, SecretString = the value. The AWS CLI does NOT
# auto-paginate this operation (unlike list/get-parameters-by-path), and a single
# response caps well under the full set (~10 items/page) — so we MUST follow
# NextToken ourselves or secrets on later pages are silently dropped (they then
# surface as FAIL-FAST "missing required var"). `--no-cli-pager` only disables the
# OUTPUT pager, not API pagination. Each page's records are base64(name<TAB>value).
# 2) Secrets Manager (sensitive values). We request secrets by EXPLICIT id
# (`batch-get-secret-value --secret-id-list ...`) rather than a name-prefix
# `--filters` collection scan (OSWE-IAC-SECRETS-LIST-01). Two wins:
# (a) Least privilege — an explicit id list lets the instance role scope
# BatchGetSecretValue to the per-secret ARN prefix and DROP the account-wide
# `secretsmanager:ListSecrets` grant that a filtered scan unavoidably forces
# (ListSecrets has no resource-level scoping). A filtered batch call also only
# authorizes against `*`; an id-list call authorizes per-secret ARN.
# (b) Deterministic set — the value set is the fixed SECRET_VARS shells created by
# the CDK ConfigStore, so we no longer depend on a list scan returning every
# page. Value-less shells and ids with no current value come back in the
# response `.Errors[]` (ResourceNotFound), never in `.SecretValues[]`, so a
# genuinely-missing REQUIRED secret is still caught by the FAIL-FAST check
# below — an absent optional secret is simply skipped.
# `--secret-id-list` is capped at 20 ids per call, so we chunk it. `--no-cli-pager`
# disables only the OUTPUT pager. Each record is base64(name<TAB>value).
#
# SOURCE OF TRUTH for this list: infra/lib/constructs/config-store.ts `SECRET_VARS`.
# Keep the two in lockstep — a new secret shell created there must be added here or it
# will never be fetched into the .env.
SECRET_VARS=(
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
)
batch_get_secrets_tsv() {
local token="" page
while :; do
if [ -n "$token" ]; then
page="$(aws secretsmanager batch-get-secret-value \
--filters "Key=name,Values=${SECRET_PREFIX}" \
--region "$REGION" --no-cli-pager --output json --next-token "$token")"
else
page="$(aws secretsmanager batch-get-secret-value \
--filters "Key=name,Values=${SECRET_PREFIX}" \
--region "$REGION" --no-cli-pager --output json)"
fi
local -a ids=()
local v
for v in "${SECRET_VARS[@]}"; do ids+=("${SECRET_PREFIX}${v}"); done
local i page
local -a chunk
for ((i = 0; i < ${#ids[@]}; i += 20)); do
chunk=("${ids[@]:i:20}")
# Capture the response into a variable FIRST so a non-zero `aws` exit (throttle,
# AccessDenied, KMS DecryptionFailure) aborts under set -e instead of being
# silently swallowed — then we'd FAIL-FAST below as "missing secret" with a wrong
# root cause. (Value-less / absent shells come back in .Errors[], not .SecretValues[].)
page="$(aws secretsmanager batch-get-secret-value \
--secret-id-list "${chunk[@]}" \
--region "$REGION" --no-cli-pager --output json)"
printf '%s' "$page" \
| jq -r '.SecretValues[] | select(.SecretString != null) | (.Name|@base64) + "\t" + (.SecretString|@base64)'
token="$(printf '%s' "$page" | jq -r '.NextToken // empty')"
[ -n "$token" ] || break
done
}
log "reading secrets under ${SECRET_PREFIX} ..."
secret_count=0
# Capture into a variable (NOT `done < <(...)` process substitution) so a non-zero
# exit from batch_get_secrets_tsv propagates under set -e — process substitution hides
# the producer's exit status from the parent shell, which would let a failed AWS call
# fall through to a misleading "missing required var" FAIL-FAST. Mirrors the SSM read.
secrets_tsv="$(batch_get_secrets_tsv)"
while IFS=$'\t' read -r nb vb; do
[ -n "$nb" ] || continue
name="$(printf '%s' "$nb" | b64d)"
@ -198,7 +228,7 @@ while IFS=$'\t' read -r nb vb; do
[ -n "$key" ] || continue
accept_var "$key" "$value" "Secrets"
secret_count=$((secret_count + 1))
done < <(batch_get_secrets_tsv)
done <<<"$secrets_tsv"
log "loaded ${secret_count} secret(s) from Secrets Manager"
# --- Sea Haven DEFAULT_REPO_OWNER guard --------------------------------------

View file

@ -64,35 +64,28 @@ export class InstanceRole extends Construct {
// random 6-char suffix, hence the trailing `*`. This is the statement that
// actually gates which secret VALUES the box can read: prefix-scoped, so the
// dev box can never read prod secret values (and vice versa). GetSecretValue is
// checked per-secret even when the value is returned via the batch call below.
// checked per-secret even when the value is returned via BatchGetSecretValue.
//
// BatchGetSecretValue is included here (prefix-scoped, NOT `*`) because
// fetch-config.sh now fetches by EXPLICIT `--secret-id-list` rather than a name
// `--filters` scan (OSWE-IAC-SECRETS-LIST-01). An id-list batch call authorizes
// against each referenced secret's ARN, so the prefix ARN satisfies it — and we
// no longer need the account-wide `secretsmanager:ListSecrets` grant that a
// filtered collection scan unavoidably forced (ListSecrets has no resource-level
// scoping). Net effect: the box can read open-swe-<env>/* secret values and
// nothing else — it can no longer enumerate secret names account-wide.
this.role.addToPolicy(
new iam.PolicyStatement({
sid: "ReadSecretValues",
actions: ["secretsmanager:GetSecretValue", "secretsmanager:DescribeSecret"],
actions: [
"secretsmanager:GetSecretValue",
"secretsmanager:BatchGetSecretValue",
"secretsmanager:DescribeSecret",
],
resources: [`arn:aws:secretsmanager:${REGION}:${ACCOUNT}:secret:${p}/*`],
}),
);
// OPERATION-level grants for fetch-config.sh's prefix-FILTERED batch read
// (`batch-get-secret-value --filters Key=name,Values=open-swe-<env>/`). Both of
// these are collection operations that AWS authorizes against `*`, NOT a
// per-secret ARN: BatchGetSecretValue with a filter is a collection call (a
// prefix-scoped ARN does NOT satisfy it — it AccessDenies), and ListSecrets is a
// list action with no resource-level scoping at all. Neither returns or widens
// VALUE access: a secret's value is still only returned when the prefix-scoped
// GetSecretValue above allows it, so cross-env VALUE isolation is preserved. The
// residual is metadata-only (the box can ENUMERATE secret names account-wide).
// Future hardening to drop both `*` grants: switch fetch-config to an explicit
// `--secret-id-list` (no filter), which lets BatchGetSecretValue be prefix-scoped
// and needs no ListSecrets. Tracked as OSWE-IAC-SECRETS-LIST-01.
this.role.addToPolicy(
new iam.PolicyStatement({
sid: "SecretsBatchListOps",
actions: ["secretsmanager:BatchGetSecretValue", "secretsmanager:ListSecrets"],
resources: ["*"],
}),
);
// NOTE (T11): SSM SecureString + Secrets Manager here are assumed to use the
// AWS-managed keys (alias/aws/ssm, alias/aws/secretsmanager) for which the
// service grants Decrypt implicitly — so NO kms:Decrypt is granted. If T11

View file

@ -10,7 +10,7 @@ dependencies = [
"fastapi>=0.136.3",
"uvicorn>=0.48.0",
"httpx>=0.28.1",
"PyJWT>=2.12.1",
"PyJWT>=2.13.0",
"cryptography>=48.0.1",
"langgraph-sdk>=0.4.2",
"langchain>=1.3.9",

10
uv.lock generated
View file

@ -1,5 +1,5 @@
version = 1
revision = 3
revision = 2
requires-python = ">=3.11"
resolution-markers = [
"python_full_version >= '3.14'",
@ -2045,7 +2045,7 @@ requires-dist = [
{ name = "langsmith", specifier = "==0.8.18" },
{ name = "markdownify", specifier = ">=1.2.2" },
{ name = "pygments", marker = "extra == 'dev'", specifier = ">=2.20.0" },
{ name = "pyjwt", specifier = ">=2.12.1" },
{ name = "pyjwt", specifier = ">=2.13.0" },
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=9.0.3" },
{ name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=1.4.0" },
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.15.15" },
@ -2618,11 +2618,11 @@ wheels = [
[[package]]
name = "pyjwt"
version = "2.12.1"
version = "2.13.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c2/27/a3b6e5bf6ff856d2509292e95c8f57f0df7017cf5394921fc4e4ef40308a/pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b", size = 102564, upload-time = "2026-03-13T19:27:37.25Z" }
sdist = { url = "https://files.pythonhosted.org/packages/3b/81/58d0ac84e1ef3a3843791d6954d94c0b33d526c75eeb1efbce9d0a4c4077/pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423", size = 107515, upload-time = "2026-05-21T19:54:36.618Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e5/7a/8dd906bd22e79e47397a61742927f6747fe93242ef86645ee9092e610244/pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c", size = 29726, upload-time = "2026-03-13T19:27:35.677Z" },
{ url = "https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728", size = 31274, upload-time = "2026-05-21T19:54:35.362Z" },
]
[package.optional-dependencies]