mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-10-02 01:33: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.
119 lines
4.5 KiB
TypeScript
119 lines
4.5 KiB
TypeScript
import { Annotations, CfnResource, IAspect, Stack, Token } from "aws-cdk-lib";
|
|
import { IConstruct } from "constructs";
|
|
|
|
/**
|
|
* One "/"-delimited segment must be lower kebab-case: `a-b-c`, digits allowed.
|
|
*/
|
|
const KEBAB_SEGMENT = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
|
|
/**
|
|
* CloudFormation property keys that carry an *explicit physical name*. The
|
|
* codegen'd L1 stores these either camelCased (`roleName`) or CFN-cased
|
|
* (`RoleName`) depending on the construct, so the aspect matches keys
|
|
* case-insensitively.
|
|
*
|
|
* We deliberately validate physical NAMES + the stack name only — not CDK
|
|
* logical construct ids (those are conventionally PascalCase, e.g.
|
|
* `InfraDeployRole`, and validating them would be wrong).
|
|
*/
|
|
const NAME_PROPERTY_KEYS = [
|
|
"RoleName",
|
|
"BucketName",
|
|
"FunctionName",
|
|
"TableName",
|
|
"LogGroupName",
|
|
"QueueName",
|
|
"TopicName",
|
|
"SecretName",
|
|
"StreamName",
|
|
"RepositoryName",
|
|
"DBInstanceIdentifier",
|
|
"DBClusterIdentifier",
|
|
"StateMachineName",
|
|
"RuleName",
|
|
"UserPoolName",
|
|
];
|
|
// NOTE: `PolicyName` is intentionally NOT checked — CDK auto-generates inline
|
|
// `DefaultPolicy` names (e.g. "InstanceRoleDefaultPolicyF15F...") from the
|
|
// logical id; those are not explicit, user-controlled physical names and are
|
|
// outside the naming convention's scope.
|
|
|
|
const NAME_KEYS_LC = new Set(NAME_PROPERTY_KEYS.map((k) => k.toLowerCase()));
|
|
|
|
/**
|
|
* Resource types whose physical NAME is a REQUIRED deviation from kebab-case:
|
|
* the open-swe config store names secrets `open-swe-<env>/<ENV_VAR_NAME>` and SSM
|
|
* params `/open-swe-<env>/<ENV_VAR_NAME>`, where the last segment is the LITERAL
|
|
* UPPER_SNAKE environment-variable name. The boot hook
|
|
* (deploy/seahaven/fetch-config.sh) strips the prefix and exports that segment
|
|
* verbatim, so a lossless store→env round-trip needs the exact env-var name —
|
|
* it cannot be kebab-cased. These two resource types are therefore exempt; every
|
|
* OTHER explicitly-named resource is still validated. (The `open-swe-<env>`
|
|
* prefix is code-generated from `prefix(env)` and is always kebab-case.)
|
|
*/
|
|
const KEBAB_EXEMPT_RESOURCE_TYPES = new Set([
|
|
"AWS::SecretsManager::Secret",
|
|
"AWS::SSM::Parameter",
|
|
]);
|
|
|
|
/**
|
|
* `true` when every non-empty "/"-delimited segment is kebab-case.
|
|
*
|
|
* Path-style names are tolerated so the same check works for Secrets Manager
|
|
* (`open-swe-dev/foo`), SSM params (`/open-swe-dev/foo`) and log groups
|
|
* (`/open-swe/dev/agent`): each segment is validated independently, and a
|
|
* leading slash (empty first segment) is ignored.
|
|
*/
|
|
export function isKebabCase(value: string): boolean {
|
|
return value
|
|
.split("/")
|
|
.filter((seg) => seg.length > 0)
|
|
.every((seg) => KEBAB_SEGMENT.test(seg));
|
|
}
|
|
|
|
/**
|
|
* Aspect that FAILS synth (`Annotations.addError`) when an explicitly-named
|
|
* resource — or a stack name — is not kebab-case. Enforces the org naming
|
|
* convention (naming-conventions.md) deterministically at synth time so a
|
|
* non-conforming name can never reach a deploy. Wired in bin/app.ts via
|
|
* `Aspects.of(app).add(new KebabNamingAspect())`.
|
|
*/
|
|
export class KebabNamingAspect implements IAspect {
|
|
public visit(node: IConstruct): void {
|
|
if (node instanceof Stack) {
|
|
const name = node.stackName;
|
|
if (!Token.isUnresolved(name) && !isKebabCase(name)) {
|
|
Annotations.of(node).addError(
|
|
`Stack name "${name}" is not kebab-case (open-swe naming convention).`,
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (node instanceof CfnResource) {
|
|
// The config store's Secret/Parameter names carry the literal UPPER_SNAKE
|
|
// env-var name per the fetch-config naming contract — a required deviation.
|
|
if (KEBAB_EXEMPT_RESOURCE_TYPES.has(node.cfnResourceType)) {
|
|
return;
|
|
}
|
|
// `_cfnProperties` is the props as set on the L1; resolve to collapse any
|
|
// intrinsic tokens (refs/getatt) so only literal strings are checked.
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const raw = (node as any)._cfnProperties ?? {};
|
|
const resolved = Stack.of(node).resolve(raw) ?? {};
|
|
for (const [key, value] of Object.entries(resolved)) {
|
|
if (
|
|
NAME_KEYS_LC.has(key.toLowerCase()) &&
|
|
typeof value === "string" &&
|
|
!Token.isUnresolved(value) &&
|
|
!isKebabCase(value)
|
|
) {
|
|
Annotations.of(node).addError(
|
|
`Resource "${node.node.path}" property ${key}="${value}" is not kebab-case ` +
|
|
`(open-swe naming convention).`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|