Compare commits
47 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2861319447 | |||
| f9a93d56cc | |||
| ca99241843 | |||
| c99da9a58c | |||
| da60f0e82f | |||
| 3562f6f771 | |||
| cb02c69e0c | |||
| 134f85d2df | |||
| 491ba78768 | |||
| 8145eee8fc | |||
| de91a4bb76 | |||
| 1e4133e11a | |||
| 843cd17b97 | |||
| 0eb578c606 | |||
| 045c7279aa | |||
| 7f1eff622d | |||
| 60f2b669ea | |||
| bab2cf363b | |||
| e597c0b089 | |||
| 2e2064559a | |||
| f2230edae0 | |||
| 0bee8f9bc2 | |||
| f3b7815120 | |||
| 94065a4fbc | |||
| 4bd07a4fae | |||
| a9d8b31595 | |||
| 49462d5e38 | |||
| a4b17d0f26 | |||
| 90be5839ab | |||
| 4fe794c7a4 | |||
| 07c0349131 | |||
| 1fd37a2843 | |||
| dca35c78ec | |||
| 2732abb23f | |||
| b026d5f041 | |||
| fee59944fd | |||
| 05372abdfc | |||
| a90a7562b9 | |||
| a07a61bf3e | |||
| edc695592a | |||
| df7b40b435 | |||
| 553caf8f1d | |||
| 4e495e5648 | |||
| d830357230 | |||
| b758a7c242 | |||
| c5745de37c | |||
| 8d5c56b88e |
@@ -248,7 +248,7 @@ The spike (Phases 08–10) materializes the **minimum** that proves the IR
|
|||||||
commitments hold (no polyglot mess):
|
commitments hold (no polyglot mess):
|
||||||
|
|
||||||
- One L1: `l1-s3` (IR-typed interface; the only AWS resource in the spike).
|
- One L1: `l1-s3` (IR-typed interface; the only AWS resource in the spike).
|
||||||
- One L2 thin-composition: `l2-static-asset` (references `l1-s3` only).
|
- One L2 thin-composition: `l2-static-assets` (references `l1-s3` only).
|
||||||
- Terraform adapter: IR → `terraform plan` against AWS via OIDC.
|
- Terraform adapter: IR → `terraform plan` against AWS via OIDC.
|
||||||
- One contract submission → contract→IR → `terraform plan` → Checkov
|
- One contract submission → contract→IR → `terraform plan` → Checkov
|
||||||
`PolicyCheckResult` → confidence signal → evidence event to the DynamoDB
|
`PolicyCheckResult` → confidence signal → evidence event to the DynamoDB
|
||||||
@@ -297,7 +297,7 @@ ACDL has no `package.json`. The verification gate substitutes:
|
|||||||
2. Phase 07 — finalize architecture v1.0; author schemas + designs.
|
2. Phase 07 — finalize architecture v1.0; author schemas + designs.
|
||||||
3. Phase 08 — AWS OIDC bootstrap (use temp key once, rotate).
|
3. Phase 08 — AWS OIDC bootstrap (use temp key once, rotate).
|
||||||
4. Phase 09 — IR + `l1-s3` + Terraform adapter → `terraform plan`.
|
4. Phase 09 — IR + `l1-s3` + Terraform adapter → `terraform plan`.
|
||||||
5. Phase 10 — `l2-static-asset` + contract→IR → end-to-end spike.
|
5. Phase 10 — `l2-static-assets` + contract→IR → end-to-end spike.
|
||||||
6. COMPLETE gate — review → ship `v1.2.0` → audit. **DONE.**
|
6. COMPLETE gate — review → ship `v1.2.0` → audit. **DONE.**
|
||||||
|
|
||||||
## v1.2 build-out scope
|
## v1.2 build-out scope
|
||||||
@@ -342,8 +342,8 @@ expands the adapter `TYPE_MAP` to cover the six new ECS-shaped IR resource
|
|||||||
types. The L1 interface shape (IR-typed inputs/outputs/NFRs, registered in
|
types. The L1 interface shape (IR-typed inputs/outputs/NFRs, registered in
|
||||||
`modules-ir/registry.json`) is unchanged — only the set of registered L1s
|
`modules-ir/registry.json`) is unchanged — only the set of registered L1s
|
||||||
grows. The IR commitments (REQ-28) continue to hold: `modules-ir/`,
|
grows. The IR commitments (REQ-28) continue to hold: `modules-ir/`,
|
||||||
`schemas/`, `contracts/`, `acdl_platform/confidence_signal.py`,
|
`schemas/`, `contracts/`, `core/confidence_signal.py`,
|
||||||
`acdl_platform/contract_resolver.py`, `acdl_platform/outbox_writer.py`
|
`core/contract_resolver.py`, `core/outbox_writer.py`
|
||||||
remain substrate-agnostic.
|
remain substrate-agnostic.
|
||||||
|
|
||||||
### `terraform apply` (dev only)
|
### `terraform apply` (dev only)
|
||||||
@@ -376,3 +376,71 @@ stream as a `terraform.apply` event.
|
|||||||
5. Phase 15 — consumer repo + `terraform apply` (dev) → live ECS service.
|
5. Phase 15 — consumer repo + `terraform apply` (dev) → live ECS service.
|
||||||
6. Phase 16 — capstone e2e: consumer commit → live HTTP 200 → evidence → timeline.
|
6. Phase 16 — capstone e2e: consumer commit → live HTTP 200 → evidence → timeline.
|
||||||
7. COMPLETE gate — review → ship `v1.3.0` → audit.
|
7. COMPLETE gate — review → ship `v1.3.0` → audit.
|
||||||
|
|
||||||
|
## v1.8 Architecture Addendum
|
||||||
|
|
||||||
|
> Milestone v1.8 (complete, tag `v1.8.0`). Adds encryption-by-default,
|
||||||
|
> deletion-protection-by-default, uptime monitoring, decommission alias,
|
||||||
|
> engineering standards, and path documentation.
|
||||||
|
|
||||||
|
### New Primitives
|
||||||
|
|
||||||
|
- **`kms-key`** (`aws:kms:key`) — Per-stack customer-managed KMS key with
|
||||||
|
`enable_key_rotation = true`. One key per L2 deployment (no shared keys).
|
||||||
|
Wired into both L2 compositions as a child, with its `kms_key_arn` output
|
||||||
|
connected to all children's `kms_key_arn` input. Adapter emits
|
||||||
|
`aws_kms_key` + `enable_key_rotation`.
|
||||||
|
- **`uptime`** (`aws:ecs:uptime-service`) — Uptime-kuma on ECS Fargate with
|
||||||
|
a feature flag (`feature_flag_enabled`), monitored endpoints (HTTP/DNS/TCP),
|
||||||
|
alert channels (Teams/email/SMS/GitHub issues). Deployed by default after
|
||||||
|
any L2 module with a separate terraform state. When the feature flag is
|
||||||
|
false, the adapter emits no resources.
|
||||||
|
|
||||||
|
### Encryption by Default
|
||||||
|
|
||||||
|
All 12 L1 primitives have `encryption_enabled` NFR (default true). Primitives
|
||||||
|
with at-rest data (s3, rds, ecr, ecs-service, ecs-cluster) have an optional
|
||||||
|
`kms_key_arn` input. The adapter emits encryption blocks (SSE-KMS for S3,
|
||||||
|
storage_encrypted for RDS, encryption_configuration for ECR) referencing the
|
||||||
|
per-stack CMK when provided. Managed KMS fallback with stderr warning for
|
||||||
|
standalone L1 deployments.
|
||||||
|
|
||||||
|
### Deletion Protection by Default
|
||||||
|
|
||||||
|
All 12 L1 primitives have `deletion_protection` NFR (default true). The
|
||||||
|
adapter emits `lifecycle { prevent_destroy = true }` when true. L2 modules
|
||||||
|
expose a `features.deletion_protection` flag (default true) propagated to
|
||||||
|
all children via the resolver. Setting `inputs.deletion_protection: false`
|
||||||
|
in the contract disables it for the whole stack.
|
||||||
|
|
||||||
|
### Decommission Alias
|
||||||
|
|
||||||
|
A `mode: decommission` on the deploy pipeline implements a 2-step destroy:
|
||||||
|
1. Disable deletion protection (resolve with `deletion_protection: false`,
|
||||||
|
terraform plan/apply, HITL SRE gate via GitHub environment).
|
||||||
|
2. Zero counts + destroy (`decommission_transform` zeroes all scalable counts,
|
||||||
|
terraform plan/apply, second HITL SRE gate).
|
||||||
|
|
||||||
|
CMDB validation via DynamoDB `acdl-change-requests` table. The Lambda
|
||||||
|
`validate_change_request` action queries the table and asserts
|
||||||
|
`status == "approved"` + `consumerRepo` match.
|
||||||
|
|
||||||
|
### Adapter Expansion
|
||||||
|
|
||||||
|
TYPE_MAP grew from 16 to 19 entries (+ `aws:kms:key`, `aws:kms:alias`,
|
||||||
|
`aws:ecs:uptime-service`). Specialized emission branches added for KMS key
|
||||||
|
rotation, S3 SSE-KMS configuration, uptime ECS Fargate task, and
|
||||||
|
`prevent_destroy` lifecycle on all resources.
|
||||||
|
|
||||||
|
### Pipeline Stages
|
||||||
|
|
||||||
|
The deploy pipeline grew from 8 to 9 stages (+ `deploy-uptime` after
|
||||||
|
`publish-outputs`). The `deploy-uptime` stage constructs a synthetic uptime
|
||||||
|
contract from the L2 stack outputs, resolves + adapts it to a separate
|
||||||
|
terraform state directory, and publishes the uptime URL via PR comment.
|
||||||
|
|
||||||
|
### Forge-Agnostic API URLs
|
||||||
|
|
||||||
|
The platform Lambda (`contract_ingestor.py`) reads `GITHUB_API_BASE` env
|
||||||
|
for forge-agnostic API URLs. GitHub uses `/search/issues`; Gitea uses
|
||||||
|
`/repos/{owner}/{repo}/issues`. Detection via `/api/v1` in the base URL.
|
||||||
+55
-35
@@ -1,10 +1,10 @@
|
|||||||
---
|
---
|
||||||
project: acdl
|
project: acdl
|
||||||
milestone: v1.1
|
milestone: v1.8
|
||||||
generated_at: 2026-07-21
|
generated_at: 2026-07-22
|
||||||
generator: lead-developer
|
generator: lead-developer
|
||||||
verification_toolchain:
|
verification_toolchain:
|
||||||
typecheck: "terraform validate && python3 -m py_compile acdl_platform/**/*.py && python3 -m jsonschema schemas/*.schema.json"
|
typecheck: "terraform validate && python3 -m py_compile core/**/*.py && python3 -m jsonschema schemas/*.schema.json"
|
||||||
test: "scripts/verify_phaseNN.sh"
|
test: "scripts/verify_phaseNN.sh"
|
||||||
build: "terraform init"
|
build: "terraform init"
|
||||||
note: |
|
note: |
|
||||||
@@ -16,7 +16,7 @@ verification_toolchain:
|
|||||||
ci-* agents read PERSONAS.md before running verification commands.
|
ci-* agents read PERSONAS.md before running verification commands.
|
||||||
---
|
---
|
||||||
|
|
||||||
# ACDL — Persona Roster (project-level, v1.1)
|
# ACDL — Persona Roster (project-level, v1.8)
|
||||||
|
|
||||||
## Active personas
|
## Active personas
|
||||||
|
|
||||||
@@ -27,34 +27,43 @@ verification_toolchain:
|
|||||||
- **Frameworks:** (none)
|
- **Frameworks:** (none)
|
||||||
- **Constraints:** pragmatic, battle-tested defaults, no-cross-territory-edits, vision-is-source-of-truth-for-why
|
- **Constraints:** pragmatic, battle-tested defaults, no-cross-territory-edits, vision-is-source-of-truth-for-why
|
||||||
- **Territory:** `.ciagent/**`, `scripts/verify_phase*.sh`, `README.md`, `docs/**` (meta only — not architecture authoring), `.gitignore`
|
- **Territory:** `.ciagent/**`, `scripts/verify_phase*.sh`, `README.md`, `docs/**` (meta only — not architecture authoring), `.gitignore`
|
||||||
- **Reason:** Owns CIAgent metadata, cross-phase verification scripts, and the v1.1 phase orchestration. Resolves the 11 open decisions (D-038) and arbitrates persona conflicts.
|
- **Reason:** Owns CIAgent metadata, cross-phase verification scripts, and the v1.7 phase orchestration. Resolves the 12-scope-axis decomposition (D-048→D-060) and arbitrates persona conflicts.
|
||||||
|
|
||||||
### backend-engineer
|
### backend-engineer
|
||||||
- **Domain:** backend
|
- **Domain:** backend
|
||||||
- **Active:** true
|
- **Active:** true
|
||||||
- **Phase-specific:** false
|
- **Phase-specific:** false
|
||||||
- **Frameworks:** python, json-schema, gitea-actions, act_runner, bash, yaml
|
- **Frameworks:** python, json-schema, gitea-actions, act_runner, bash, yaml, github-actions
|
||||||
- **Constraints:** contract-schema-first, fail-fast-with-reason-codes, no-long-lived-credentials, severity-to-penalty-mapping-immutable
|
- **Constraints:** contract-schema-first, fail-fast-with-reason-codes, no-long-lived-credentials, severity-to-penalty-mapping-immutable
|
||||||
- **Territory:** `acdl_platform/confidence_signal.py`, `acdl_platform/contract_resolver.py`, `acdl_platform/outbox_writer.py`, `schemas/**` (contract + IR + PolicyCheckResult), `contracts/**` (sample contracts), `.gitea/workflows/**` (pipeline)
|
- **Territory:** `core/confidence_signal.py`, `core/contract_resolver.py`, `core/outbox_writer.py`, `core/output_publisher.py`, `core/environment_check.py`, `schemas/**` (contract + IR + PolicyCheckResult + tagging-standard + pipeline), `contracts/**` (sample contracts), `.gitea/workflows/**` + `.github/workflows/**` (pipeline + deploy + platform-test + primitives-plan + patterns-plan + release), `pipelines/**`, `scripts/run_ci.sh`, `scripts/run_platform.sh`, `scripts/post_stage_comment.sh`, `scripts/run_primitive_plan.sh`, `scripts/run_pattern_plan.sh`
|
||||||
- **Reason:** Owns the contract schema, contract→IR resolution, the confidence signal (6 inputs + severity mapping), the DynamoDB outbox writer, and the central pipeline workflow.
|
- **Reason:** Owns the contract schema, contract→IR resolution, the confidence signal (6 inputs + severity mapping), the DynamoDB outbox writer, the output publisher (SSM + GitHub comment), the central pipeline workflows (CI + deploy + platform-test + primitives-plan + patterns-plan + release), and the deploy-pipeline DX (stage comments, error-report step).
|
||||||
|
|
||||||
### platform-engineer (custom)
|
### platform-engineer (custom)
|
||||||
- **Domain:** infra
|
- **Domain:** infra
|
||||||
- **Active:** true
|
- **Active:** true
|
||||||
- **Phase-specific:** false
|
- **Phase-specific:** false
|
||||||
- **Frameworks:** terraform, aws-iam, aws-s3, aws-dynamodb, oidc, json-schema
|
- **Frameworks:** terraform, aws-iam, aws-s3, aws-dynamodb, aws-lambda, aws-cloudfront, aws-waf, aws-ssm, aws-secretsmanager, oidc, json-schema
|
||||||
- **Constraints:** ir-is-substrate-agnostic, adapter-is-only-substrate-specific-code, state-in-s3+dynamodb-single-region, oidc-only-no-long-lived-keys (waiver D-034 for bootstrap), terraform-plan-only-in-spike
|
- **Constraints:** ir-is-substrate-agnostic, adapter-is-only-substrate-specific-code, state-in-s3+dynamodb-single-region, oidc-only-no-long-lived-keys (waiver D-034 for bootstrap), terraform-plan-only-in-spike, cross-account-iam-scoped-via-abac
|
||||||
- **Territory:** `adapters/terraform/**`, `modules-ir/**`, `terraform/**` (state backend, provider config), `modules-ir/registry.json`
|
- **Territory:** `adapters/terraform/**`, `modules/**` (l1 + l2 + registry.json + examples), `terraform/**` (state backend, provider config, platform infra), `modules/registry.json`
|
||||||
- **Reason:** Owns the Target Stack IR, the L1/L2 IR-typed modules, the Terraform adapter, the AWS OIDC bootstrap, and the state backend. The IR is substrate-agnostic; the adapter is the only substrate-specific code (the binding constraint per §12).
|
- **Reason:** Owns the Target Stack IR, the L1/L2 IR-typed modules (incl. new cloudfront + waf + rds primitives), the Terraform adapter (TYPE_MAP expansion for cloudfront/waf/rds), the AWS OIDC bootstrap, the state backend, and the platform Terraform (Lambda + DynamoDB + KMS + Secrets Manager + Function URL). The IR is substrate-agnostic; the adapter is the only substrate-specific code (the binding constraint per §12).
|
||||||
|
|
||||||
### security-engineer (custom)
|
### security-engineer (custom)
|
||||||
- **Domain:** security
|
- **Domain:** security
|
||||||
- **Active:** true
|
- **Active:** true
|
||||||
- **Phase-specific:** false
|
- **Phase-specific:** false
|
||||||
- **Frameworks:** aws-iam, oidc, checkov, json-schema
|
- **Frameworks:** aws-iam, oidc, checkov, kyverno, wiz, json-schema
|
||||||
- **Constraints:** least-privilege, separation-of-duties-identity-distinctness, no-secrets-in-skill-markdown, audit-chain-extends-not-tears-up, critical-finding-hard-overrides-confidence
|
- **Constraints:** least-privilege, separation-of-duties-identity-distinctness, no-secrets-in-skill-markdown, audit-chain-extends-not-tears-up, critical-finding-hard-overrides-confidence, required-tags-enforced
|
||||||
- **Territory:** `acdl_platform/hitl_matrix_design.md`, `acdl_platform/audit_ledger_design.md`, `adapters/terraform/policy/**` (Checkov adapter → PolicyCheckResult), `acdl_platform/separation_of_duties.py`
|
- **Territory:** `core/hitl_matrix_design.md`, `core/audit_ledger_design.md`, `adapters/terraform/policy/**` (Checkov adapter + custom rules), `adapters/wiz/**` (Wiz adapter), `adapters/kyverno/**` (Kyverno adapter + sample policies), `core/separation_of_duties.py`, `schemas/tagging-standard.json`, `schemas/policy_check_result.schema.json` (engine enum)
|
||||||
- **Reason:** Owns the HITL matrix design, separation-of-duties (DynamoDB identity-distinctness), the audit ledger design (S3 Object Lock + JWS + chain), and the Checkov→PolicyCheckResult adapter. Enforces the "Safety is Computed, Not Assumed" + "Audit truth lives outside the repository" vision tenets.
|
- **Reason:** Owns the HITL matrix design, separation-of-duties, the audit ledger design, the Checkov→PolicyCheckResult adapter + the custom tagging rule (D-054, D-043 closure), the Wiz adapter (D-052), the Kyverno adapter (D-053), and the tagging standard. Enforces the "Safety is Computed, Not Assumed" + "Audit truth lives outside the repository" vision tenets.
|
||||||
|
|
||||||
|
### lambda-engineer (custom, v1.8)
|
||||||
|
- **Domain:** serverless
|
||||||
|
- **Active:** true
|
||||||
|
- **Phase-specific:** true (reactivated for v1.8; removed after milestone COMPLETE)
|
||||||
|
- **Frameworks:** python, aws-lambda, boto3, dynamodb, aws-secretsmanager, github-api, gitea-api
|
||||||
|
- **Constraints:** lambda-is-stateless, dynamodb-is-the-state-store, secrets-from-secrets-manager-never-logged, idempotent-actions, cross-account-iam-via-abac, forge-agnostic-api-urls
|
||||||
|
- **Territory:** `core/lambda/**` (contract_ingestor.py + handler), `terraform/platform/main.tf` (Lambda + Function URL + DynamoDB + KMS + Secrets Manager + IAM + acdl-change-requests table), `terraform/platform/consumer_invoke_policy.json`, `terraform/platform/variables.tf`
|
||||||
|
- **Reason:** Reactivated for v1.8 Phase 29 (Terraform-rendered invoke policy), Phase 30 (forge-agnostic API URLs in contract_ingestor.py), Phase 34 (validate_change_request Lambda action + acdl-change-requests DynamoDB table). The Lambda is stateless; all state is in DynamoDB. Forge-agnostic API URLs (GitHub + Gitea) via GITHUB_API_BASE env var. Removed from the roster after milestone COMPLETE (the code persists, but the persona is no longer active).
|
||||||
|
|
||||||
### frontend-engineer
|
### frontend-engineer
|
||||||
- **Domain:** frontend
|
- **Domain:** frontend
|
||||||
@@ -63,21 +72,21 @@ verification_toolchain:
|
|||||||
- **Frameworks:** vanilla-js, dom-api, fetch-api
|
- **Frameworks:** vanilla-js, dom-api, fetch-api
|
||||||
- **Constraints:** no-frameworks, single-file, fetch-from-same-origin-raw-url, relative-url-for-audit-json
|
- **Constraints:** no-frameworks, single-file, fetch-from-same-origin-raw-url, relative-url-for-audit-json
|
||||||
- **Territory:** `evidence-ui/**` (the timeline UI; pushed to `acdl-evidence`)
|
- **Territory:** `evidence-ui/**` (the timeline UI; pushed to `acdl-evidence`)
|
||||||
- **Reason:** Owns the evidence timeline UI (`index.html`). Carried over from v1.0; the UI continues to render the audit stream. The v1.1 spike writes events to the DynamoDB outbox; the UI continues to read `audit.json` published to `acdl-evidence`.
|
- **Reason:** Owns the evidence timeline UI (`index.html`). Carried over from v1.0; the UI continues to render the audit stream. The v1.7 spike writes events to the DynamoDB outbox; the UI continues to read `audit.json` published to `acdl-evidence`.
|
||||||
|
|
||||||
## Deactivated personas
|
## Deactivated personas
|
||||||
|
|
||||||
### infra-stub-engineer (custom, v1.0 only)
|
### infra-stub-engineer (custom, v1.0 only)
|
||||||
- **Domain:** backend
|
- **Domain:** backend
|
||||||
- **Active:** false
|
- **Active:** false
|
||||||
- **Reason:** Owned L1 stub modules (`modules/l1/**`) in the v1.0 demo. The demo is archived to `demo/` in Phase 06; real L1 modules (`modules-ir/l1/**`) are owned by platform-engineer (substrate-agnostic IR + Terraform adapter). The stub engineer is no longer needed.
|
- **Reason:** Owned L1 stub modules (`modules/l1/**`) in the v1.0 demo. The demo is archived to `demo/` in Phase 06; real L1 modules (`modules-ir/l1/**`, now `modules/l1/**`) are owned by platform-engineer (substrate-agnostic IR + Terraform adapter). The stub engineer is no longer needed.
|
||||||
- **Phase-specific:** false (was v1.0)
|
- **Phase-specific:** false (was v1.0)
|
||||||
- **Territory (would have been):** `demo/modules/l1/**`
|
- **Territory (would have been):** `demo/modules/l1/**`
|
||||||
|
|
||||||
### data-engineer
|
### data-engineer
|
||||||
- **Domain:** data
|
- **Domain:** data
|
||||||
- **Active:** false
|
- **Active:** false
|
||||||
- **Reason:** No ORM/persistence framework. The v1.1 outbox is DynamoDB but accessed via boto3 calls inside `acdl_platform/outbox_writer.py` (owned by backend-engineer); the audit ledger is S3 Object Lock + JWS (owned by security-engineer). No schema-migration layer, no ORM, no data-engineer territory.
|
- **Reason:** No ORM/persistence framework. The v1.7 contract-ingestion table is DynamoDB but accessed via boto3 inside `core/lambda/contract_ingestor.py` (owned by lambda-engineer); the outbox is DynamoDB accessed via `core/outbox_writer.py` (owned by backend-engineer); the audit ledger is S3 Object Lock + JWS (owned by security-engineer). No schema-migration layer, no ORM, no data-engineer territory.
|
||||||
- **Phase-specific:** false
|
- **Phase-specific:** false
|
||||||
- **Frameworks:** (would have been: drizzle, prisma)
|
- **Frameworks:** (would have been: drizzle, prisma)
|
||||||
- **Constraints:** (would have been: schema-first, type-safe-orm)
|
- **Constraints:** (would have been: schema-first, type-safe-orm)
|
||||||
@@ -87,32 +96,43 @@ verification_toolchain:
|
|||||||
|
|
||||||
| Phase | Personas active | Notes |
|
| Phase | Personas active | Notes |
|
||||||
|-------|------------------|-------|
|
|-------|------------------|-------|
|
||||||
| 06 archive-demo-and-reorient | lead-developer, frontend-engineer (demo UI move only) | backend/platform/security idle |
|
| 28 adapter-waf-and-resolver-outputs | platform-engineer (lead: WAF HCL fix + adapter output blocks), backend-engineer (resolver outputs processing) | security/lambda/frontend idle |
|
||||||
| 07 architecture-v1-finalization | lead-developer, backend-engineer (schemas), security-engineer (HITL/ledger/SoD), platform-engineer (IR) | frontend idle |
|
| 29 ssm-kms-and-invoke-policy | backend-engineer (lead: SSM fail-loud), lambda-engineer (Terraform-rendered invoke policy), security-engineer (CMK enforcement review) | platform/frontend idle |
|
||||||
| 08 aws-oidc-bootstrap | platform-engineer (lead), security-engineer (trust policy review) | backend/frontend idle |
|
| 30 run-platform-isolation-and-api-portability | backend-engineer (lead: run_platform.sh temp dir + deploy.yml static-key), lambda-engineer (forge-agnostic API URLs) | platform/security/frontend idle |
|
||||||
| 09 v1-spike-ir-and-l1-and-adapter | platform-engineer (lead), backend-engineer (IR schema co-author) | security/frontend idle |
|
| 31 encryption-by-default-and-per-stack-cmk | platform-engineer (lead: kms-key primitive + adapter expansion + L2 wiring), security-engineer (encryption NFR enforcement review) | backend/lambda/frontend idle |
|
||||||
| 10 v1-spike-l2-and-contract-e2e | platform-engineer (L2 + adapter), backend-engineer (contract→IR + confidence + outbox), security-engineer (Checkov→PolicyCheckResult), frontend-engineer (evidence event surfaces in timeline) | Full roster |
|
| 32 deletion-protection-by-default-and-l2-feature-flag | platform-engineer (lead: prevent_destroy emission + L2 feature flag), backend-engineer (contract schema update) | security/lambda/frontend idle |
|
||||||
|
| 33 uptime-kuma-primitive | platform-engineer (lead: uptime primitive + adapter + separate state), backend-engineer (deploy-uptime pipeline stage + run_platform.sh + PR comment) | security/lambda/frontend idle |
|
||||||
|
| 34 decommission-alias-and-cmdb-validation | backend-engineer (lead: decommission pipeline mode + run_platform.sh + consumer docs), lambda-engineer (validate_change_request + acdl-change-requests table), security-engineer (HITL SRE gates review) | platform/frontend idle |
|
||||||
|
| 35 module-engineering-standards | lead-developer (lead: STANDARDS.md + catalog fix + template), platform-engineer (standards content review), backend-engineer (automated standards test) | security/lambda/frontend idle |
|
||||||
|
| 36 schemas-adapters-pipelines-readmes | lead-developer (lead: 3 READMEs), backend-engineer (pipelines + schemas README content), platform-engineer (adapters README content) | security/lambda/frontend idle |
|
||||||
|
| 37 verify | lead-developer (lead: 4-layer verification), all personas (review their territory) | — |
|
||||||
|
| 38 review-audit-complete | lead-developer (lead: review + audit + milestone completion), all personas (review participation) | — |
|
||||||
|
|
||||||
## Domain priority (used by TaskDecomposer)
|
## Domain priority (used by TaskDecomposer)
|
||||||
|
|
||||||
`coordination → security → platform → backend → frontend`
|
`coordination → security → platform → backend → lambda → frontend`
|
||||||
|
|
||||||
Rationale: in v1.1, the security/architecture commitments (IR, confidence,
|
Rationale: in v1.8, the security commitments (encryption by default,
|
||||||
HITL, ledger, SoD) are the binding constraints; the platform layer
|
KMS rotation, deletion protection, CMDB validation, HITL SRE gates)
|
||||||
materializes them; backend wires the pipeline; frontend surfaces the
|
and the platform commitments (kms-key primitive, uptime primitive,
|
||||||
evidence. The spike's correctness depends on the security + platform layers
|
adapter expansion, prevent_destroy emission) are the binding
|
||||||
being right before backend wiring.
|
constraints; backend wires the pipeline + decommission mode + API
|
||||||
|
portability; lambda owns the CMDB validation + forge-agnostic APIs;
|
||||||
|
frontend is unchanged from v1.0 (evidence timeline).
|
||||||
|
|
||||||
## Conflict resolutions (lead-developer arbitration)
|
## Conflict resolutions (lead-developer arbitration)
|
||||||
|
|
||||||
- `backend-engineer` vs `platform-engineer` over `schemas/ir.schema.json`: platform-engineer owns the IR (it is substrate-agnostic but infra-shaped); backend-engineer owns the contract schema and the contract→IR resolution (contract is the consumer surface). Co-authoring is expected; conflict goes to lead-developer.
|
- `backend-engineer` vs `platform-engineer` over `schemas/ir.schema.json` + `schemas/stack.schema.json`: platform-engineer owns the IR (substrate-agnostic but infra-shaped); backend-engineer owns the contract schema and the contract→IR resolution. Co-authoring is expected; conflict goes to lead-developer.
|
||||||
- `backend-engineer` vs `security-engineer` over `acdl_platform/confidence_signal.py`: security-engineer owns the severity→penalty mapping + critical-override semantics; backend-engineer owns the 6-input weighted sum + per-env thresholds. The confidence signal is co-owned; conflicts go to lead-developer.
|
- `backend-engineer` vs `security-engineer` over `core/confidence_signal.py`: security-engineer owns the severity→penalty mapping + critical-override semantics; backend-engineer owns the 6-input weighted sum + per-env thresholds. Co-owned; conflicts go to lead-developer.
|
||||||
- `platform-engineer` vs `security-engineer` over `adapters/terraform/policy/**`: security-engineer owns the Checkov→PolicyCheckResult adapter (policy is a security concern); platform-engineer owns the Terraform adapter (substrate translation). No overlap.
|
- `platform-engineer` vs `security-engineer` over `adapters/terraform/policy/**`: security-engineer owns the Checkov→PolicyCheckResult adapter + custom rules + the Wiz/Kyverno adapters (policy is a security concern); platform-engineer owns the Terraform adapter (substrate translation). No overlap.
|
||||||
|
- `lambda-engineer` vs `platform-engineer` over `terraform/platform/main.tf`: lambda-engineer owns the Lambda + DynamoDB + Secrets Manager definitions; platform-engineer reviews the Terraform structure + state backend. Co-authoring expected; conflicts go to lead-developer.
|
||||||
|
- `backend-engineer` vs `lambda-engineer` over `core/lambda/contract_ingestor.py` vs `scripts/run_platform.sh` + `.github/workflows/deploy.yml` error-report step: lambda-engineer owns the Lambda handler; backend-engineer owns the workflow step that invokes it. The interface (the JSON payload) is co-authored; conflicts go to lead-developer.
|
||||||
- `lead-developer` vs any: lead-developer owns `.ciagent/**` + `docs/**` meta + verification scripts; persona engineers do not edit CIAgent metadata or the vision/architecture source docs.
|
- `lead-developer` vs any: lead-developer owns `.ciagent/**` + `docs/**` meta + verification scripts; persona engineers do not edit CIAgent metadata or the vision/architecture source docs.
|
||||||
|
|
||||||
## Territory enforcement mode
|
## Territory enforcement mode
|
||||||
|
|
||||||
`warn` — config.json has no `personas.territory_enforcement` field, so the
|
`warn` — config.json has no `personas.territory_enforcement` field, so the
|
||||||
default per execute.md is `warn`. Cross-territory edits are logged in the
|
default per execute.md is `warn`. Cross-territory edits are logged in the
|
||||||
commit message but do not fail the task. The spike's small scope means
|
commit message but do not fail the task. v1.7's broad scope means
|
||||||
co-authoring across territories is likely; `warn` keeps it frictionless.
|
co-authoring across territories is likely (e.g. lambda + platform on
|
||||||
|
`terraform/platform/main.tf`); `warn` keeps it frictionless.
|
||||||
+217
-30
@@ -1,41 +1,228 @@
|
|||||||
---
|
---
|
||||||
phase: 16
|
phase: 28-38
|
||||||
name: v1.2-capstone-e2e
|
name: v1.8-p1-remediation-uptime-standards-encryption-decommission-docs
|
||||||
milestone: v1.2
|
milestone: v1.8
|
||||||
requirements: [REQ-35]
|
requirements: [REQ-76, REQ-77, REQ-78, REQ-79, REQ-80, REQ-81, REQ-82, REQ-83, REQ-84, REQ-85, REQ-86, REQ-87, REQ-88, REQ-89, REQ-90, REQ-91, REQ-92, REQ-93, REQ-94, REQ-95, REQ-96, REQ-97, REQ-98, REQ-99]
|
||||||
type: feat/verify
|
type: fix/feat/docs
|
||||||
branch: phase/16-v1.2-capstone-e2e
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Phase 16 — v1.2-capstone-e2e (v1.2) PLAN
|
# ACDL v1.8 — Phase Plans
|
||||||
|
|
||||||
## Goal
|
> Milestone: v1.8. Planner: ci-planner. Status: active.
|
||||||
|
> 11 phases (28–38), 24 requirements (REQ-76..99).
|
||||||
|
|
||||||
End-to-end verification of the v1.2 platform: consumer commit → pipeline →
|
## Phase 28 — adapter-waf-and-resolver-outputs
|
||||||
`terraform apply` (dev) → live ECS service → evidence event → timeline. The
|
|
||||||
`terraform apply` is blocked by the IAM P0 (Phase 15); Phase 16 ships the
|
|
||||||
capstone verification of everything *up to* the apply + documents the
|
|
||||||
operator's unblock step. After the operator pushes the policy, the apply +
|
|
||||||
HTTP 200 check complete REQ-33/35.
|
|
||||||
|
|
||||||
## Tasks
|
**Requirements:** REQ-76 (WAF nested rules + default_action), REQ-77 (L2 outputs resolution)
|
||||||
|
**Personas:** platform-engineer (lead), backend-engineer
|
||||||
|
**Type:** fix
|
||||||
|
|
||||||
### T-16.1 — Capstone verify script
|
### Tasks (Wave 1 — sequential):
|
||||||
`scripts/verify_phase16.sh` runs the full v1.2 platform flow (consumer
|
|
||||||
content → contract → IR → adapter → terraform validate + plan) + verifies
|
|
||||||
the v1.1 regression + the NFR improvements (run_platform.sh, IAM policy
|
|
||||||
expansion, P1-1 redaction) + the documentation (README accuracy). The
|
|
||||||
`terraform apply` + HTTP 200 check are documented as the operator's
|
|
||||||
post-unblock step.
|
|
||||||
|
|
||||||
### T-16.2 — Capstone evidence event
|
1. **platform-engineer:** Fix WAF `rules` emission in `adapters/terraform/adapter.py:346-348` — replace `rules = {_value_expr(...)}` with nested `rules { ... }` block emission per rule. Read `inputs.get("default_action")` (line 334) and emit `allow {}` / `block {}` based on input (default `allow` if absent).
|
||||||
Write a `MILESTONE_CAPSTONE_VERIFIED` evidence event to the outbox (the
|
2. **backend-engineer:** Implement L2 composition `outputs[]` processing in `core/contract_resolver.py` `resolve_l2()` — after building `resources` (line 232), parse `composition.get("outputs", [])`, resolve source via `child_outputs`, build `stack_instance["outputs"]` dict.
|
||||||
v1.2 platform is verified up to the IAM-blocked apply).
|
3. **platform-engineer:** Extend `adapter.py` `adapt()` to emit `output "<outName>" { value = <ref> }` blocks from `stack_instance.get("outputs", {})`.
|
||||||
|
4. **platform-engineer:** Add tests to `tests/test_adapter.py` (WAF custom rules, default_action block, output blocks) + `tests/test_contract_resolver.py` (L2 outputs for static-assets).
|
||||||
|
|
||||||
### T-16.3 — Phase 16 README update
|
### Must-haves:
|
||||||
Update README to reflect the v1.2 status (Phase 15 partial, Phase 16
|
- WAF with custom rules emits `rules {` blocks, not `rules = [`
|
||||||
capstone, the IAM unblock step).
|
- WAF `default_action: block` emits `block {}`
|
||||||
|
- L2 resolution yields `stack.outputs.*`
|
||||||
|
- Adapter emits `output` blocks
|
||||||
|
- `pytest` passes (275 → ~285)
|
||||||
|
|
||||||
## Ship
|
---
|
||||||
|
|
||||||
Merge → `main` (--no-ff). Tag `v1.2.6`.
|
## Phase 29 — ssm-kms-and-invoke-policy
|
||||||
|
|
||||||
|
**Requirements:** REQ-78 (SSM fail-loud), REQ-79 (Terraform-rendered invoke policy)
|
||||||
|
**Personas:** backend-engineer (lead), lambda-engineer, security-engineer
|
||||||
|
**Type:** fix
|
||||||
|
|
||||||
|
### Tasks (Wave 1):
|
||||||
|
|
||||||
|
1. **backend-engineer:** Change `core/output_publisher.py:54-55` `_kms_key_id()` — raise `RuntimeError` when `ACDL_KMS_KEY_ID` unset; add `ACDL_ALLOW_DEFAULT_KMS=1` escape hatch.
|
||||||
|
2. **lambda-engineer:** Convert `terraform/platform/consumer_invoke_policy.json` to Terraform-rendered template — add `terraform/platform/variables.tf` with `data "aws_caller_identity" "current" {}` + `templatestring` or `replace()` for account ID injection.
|
||||||
|
3. **backend-engineer:** Add `tests/test_output_publisher.py` cases: `test_kms_unset_raises`, `test_kms_unset_allow_default_kms`. Add `tests/test_invoke_policy.py` asserting rendered policy has no `000000000000`.
|
||||||
|
|
||||||
|
### Must-haves:
|
||||||
|
- SSM raises RuntimeError without CMK; escape hatch works
|
||||||
|
- Rendered invoke policy has live account ID
|
||||||
|
- `pytest` passes (~290)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 30 — run-platform-isolation-and-api-portability
|
||||||
|
|
||||||
|
**Requirements:** REQ-80 (temp dir), REQ-81 (forge-agnostic URLs), REQ-82 (static-key override)
|
||||||
|
**Personas:** backend-engineer (lead), lambda-engineer
|
||||||
|
**Type:** fix
|
||||||
|
|
||||||
|
### Tasks (Wave 1 — parallel):
|
||||||
|
|
||||||
|
1. **backend-engineer:** Change `scripts/run_platform.sh:122` adapter target from `terraform/spike` to `$WORK/tf`. Update all downstream references. Remove committed `terraform/spike/*.tf`. Update `tests/test_pipeline.py`. Update deploy.yml artifact upload path.
|
||||||
|
2. **lambda-engineer:** Add `_github_api_base()` + `_forge_type()` to `core/lambda/contract_ingestor.py`. Replace hardcoded URLs at lines 109, 149, 163. Add `tests/test_contract_ingestor.py` Gitea base URL test.
|
||||||
|
3. **backend-engineer:** Restructure `configure-aws-credentials` step in both deploy workflows (byte-identical) — single conditional step with `access-key`/`secret-key` inputs when static key present. Update `tests/test_pipeline_contract.py`.
|
||||||
|
|
||||||
|
### Must-haves:
|
||||||
|
- `run_platform.sh --check-only` writes to temp dir
|
||||||
|
- `contract_ingestor.py` uses `GITHUB_API_BASE`
|
||||||
|
- Deploy workflow static-key override wired
|
||||||
|
- Both deploy workflows byte-identical
|
||||||
|
- `pytest` passes (~295)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 31 — encryption-by-default-and-per-stack-cmk
|
||||||
|
|
||||||
|
**Requirements:** REQ-83 (kms-key primitive), REQ-84 (encryption NFRs on all primitives), REQ-85 (L2 CMK wiring)
|
||||||
|
**Personas:** platform-engineer (lead), security-engineer
|
||||||
|
**Type:** feat
|
||||||
|
|
||||||
|
### Tasks (Wave 1 — kms-key primitive + adapter):
|
||||||
|
1. **platform-engineer:** Create `modules/l1/kms-key/` with `interface.json` (type `aws:kms:key`, inputs: description/region/deletion_window_days, outputs: kms_key_arn/kms_key_id, NFRs: enable_rotation default true, deletion_protection default true) + `instance.json` + `README.md` + `examples/`.
|
||||||
|
2. **platform-engineer:** Add `aws:kms:key → aws_kms_key` + `aws:kms:alias → aws_kms_alias` to adapter TYPE_MAP. Emit `enable_key_rotation = true` + alias.
|
||||||
|
|
||||||
|
### Tasks (Wave 2 — encryption NFRs on all primitives, after Wave 1):
|
||||||
|
3. **platform-engineer:** Add `encryption_enabled` NFR (default true) + `kms_key_arn` input to every L1 `interface.json` (s3, rds, ecr, ecs-service, ecs-cluster, alb, cloudfront, waf, vpc, iam-role). Update adapter to emit encryption blocks referencing the CMK when `kms_key_arn` is provided; managed KMS fallback with stderr warning when not.
|
||||||
|
4. **platform-engineer:** Update both L2 `composition.json` files — add `kms-key` child + wires connecting `kms_key_arn` output to each child's `kms_key_arn` input.
|
||||||
|
5. **platform-engineer:** Add `tests/test_encryption.py` — assert every primitive has encryption NFRs; assert adapter emits encryption blocks; assert L2 wires CMK; assert `enable_key_rotation = true`.
|
||||||
|
|
||||||
|
### Must-haves:
|
||||||
|
- kms-key primitive exists + registered
|
||||||
|
- All primitives have `encryption_enabled` NFR + `kms_key_arn` input
|
||||||
|
- L2 modules wire per-stack CMK
|
||||||
|
- Adapter emits encryption blocks
|
||||||
|
- `pytest` passes (~310)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 32 — deletion-protection-by-default-and-l2-feature-flag
|
||||||
|
|
||||||
|
**Requirements:** REQ-86 (deletion_protection NFR on all primitives), REQ-87 (L2 feature flag)
|
||||||
|
**Personas:** platform-engineer (lead), backend-engineer
|
||||||
|
**Type:** feat
|
||||||
|
|
||||||
|
### Tasks (Wave 1):
|
||||||
|
1. **platform-engineer:** Add `deletion_protection` NFR (boolean, default true) to every L1 `interface.json` (rds already has it). Update adapter to emit `lifecycle { prevent_destroy = true }` when NFR is true; omit when false. RDS gets BOTH `deletion_protection` arg + `prevent_destroy` lifecycle.
|
||||||
|
2. **backend-engineer:** Add `features` object support to `schemas/stack.schema.json` (optional `features.deletion_protection`). Update `core/contract_resolver.py` `resolve_l2()` to propagate `features.deletion_protection` to each child's `deletion_protection` NFR. Add `inputs.deletion_protection` to `schemas/contract.schema.json` (optional boolean).
|
||||||
|
3. **platform-engineer:** Add `tests/test_deletion_protection.py` — assert every primitive has the NFR; assert adapter emits `prevent_destroy`; assert L2 feature flag propagation.
|
||||||
|
|
||||||
|
### Must-haves:
|
||||||
|
- Every primitive has `deletion_protection` NFR (default true)
|
||||||
|
- Adapter emits `prevent_destroy = true` when true
|
||||||
|
- L2 feature flag propagates
|
||||||
|
- `pytest` passes (~320)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 33 — uptime-kuma-primitive
|
||||||
|
|
||||||
|
**Requirements:** REQ-88 (uptime primitive), REQ-89 (deployed by default after L2), REQ-90 (feature flag), REQ-91 (pipeline stage)
|
||||||
|
**Personas:** platform-engineer (lead), backend-engineer
|
||||||
|
**Type:** feat
|
||||||
|
|
||||||
|
### Tasks (Wave 1 — primitive + adapter):
|
||||||
|
1. **platform-engineer:** Create `modules/l1/uptime/` with `interface.json` (type `aws:ecs:uptime-service`, inputs: container_image/region/monitored_endpoints/static_checks/alert_channels/feature_flag_enabled/cpu/memory, outputs: uptime_url/service_arn/task_definition_arn, NFRs: deletion_protection/encryption_enabled) + `instance.json` + `README.md` + `examples/simple.yaml` + `examples/complex.yaml`.
|
||||||
|
2. **platform-engineer:** Add `aws:ecs:uptime-service` to adapter TYPE_MAP. Emit ECS Fargate task + service + ALB + listener + EFS volume + CloudWatch log group. When `feature_flag_enabled=false`, emit NO resources. Register in `registry.json`.
|
||||||
|
|
||||||
|
### Tasks (Wave 2 — pipeline + script, after Wave 1):
|
||||||
|
3. **backend-engineer:** Add `deploy-uptime` stage to `pipelines/deploy.yaml` (after `publish-outputs`). Update both deploy workflows (byte-identical) with the stage. Add `scripts/seed_uptime_monitors.py` for post-deploy monitor seeding via uptime-kuma API.
|
||||||
|
4. **backend-engineer:** Update `scripts/run_platform.sh` — add `deploy-uptime` step: read L2 stack outputs, construct synthetic uptime contract with `monitored_endpoints` from outputs, run second terraform apply with separate state (`$WORK/uptime-tf/`), publish uptime URL via PR comment. Skip when `inputs.uptime_enabled=false`.
|
||||||
|
5. **backend-engineer:** Add `tests/test_uptime_primitive.py` — validate interface; assert adapter emits ECS service when flag=true; assert no resources when flag=false; assert `deploy-uptime` stage in pipeline contract.
|
||||||
|
|
||||||
|
### Must-haves:
|
||||||
|
- Uptime primitive exists with feature flag + alert channels
|
||||||
|
- Deployed by default after L2 (separate state)
|
||||||
|
- Uptime URL published via PR comment
|
||||||
|
- Feature flag disables deployment
|
||||||
|
- `deploy-uptime` stage in deploy contract + byte-identical workflows
|
||||||
|
- `pytest` passes (~335)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 34 — decommission-alias-and-cmdb-validation
|
||||||
|
|
||||||
|
**Requirements:** REQ-92 (decommission mode), REQ-93 (CMDB validation), REQ-94 (consumer docs)
|
||||||
|
**Personas:** backend-engineer (lead), lambda-engineer, security-engineer
|
||||||
|
**Type:** feat
|
||||||
|
|
||||||
|
### Tasks (Wave 1 — CMDB + Lambda, parallel):
|
||||||
|
1. **lambda-engineer:** Add `acdl-change-requests` DynamoDB table to `terraform/platform/main.tf` (PK changeRequestId, SK submittedAt, SSE via CMK, PITR). Add `validate_change_request` action to `core/lambda/contract_ingestor.py` — query table, assert status=approved + consumerRepo match, return CR details or 403.
|
||||||
|
2. **backend-engineer:** Add `decommission_transform(stack_instance)` to `core/contract_resolver.py` — zero all counts (desired_count=0 for ECS, etc.).
|
||||||
|
|
||||||
|
### Tasks (Wave 2 — pipeline + docs, after Wave 1):
|
||||||
|
3. **backend-engineer:** Add `mode: decommission` to deploy workflow inputs. Add decommission stages to `pipelines/deploy.yaml`: validate-change-request → disable-deletion-protection (HITL SRE gate via GitHub environment) → zero-counts (second HITL SRE gate) → confirm-decommission. Update both deploy workflows (byte-identical).
|
||||||
|
4. **backend-engineer:** Update `docs/CONSUMER_GUIDE.md` with "Decommissioning a stack" section (request CR, trigger decommission, HITL gates, what happens).
|
||||||
|
5. **backend-engineer:** Add `tests/test_decommission.py` — assert `decommission_transform` zeroes counts; assert `validate_change_request` rejects invalid CRs; assert decommission stages in pipeline contract.
|
||||||
|
|
||||||
|
### Must-haves:
|
||||||
|
- Decommission mode on existing deploy pipeline
|
||||||
|
- 2-step with HITL SRE gates
|
||||||
|
- CR ID validated against DynamoDB CMDB
|
||||||
|
- Documented in consumer guide
|
||||||
|
- `pytest` passes (~345)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 35 — module-engineering-standards
|
||||||
|
|
||||||
|
**Requirements:** REQ-95 (STANDARDS.md), REQ-96 (catalog fix + template update)
|
||||||
|
**Personas:** lead-developer (lead), platform-engineer, backend-engineer
|
||||||
|
**Type:** docs + refactor
|
||||||
|
|
||||||
|
### Tasks (Wave 1):
|
||||||
|
1. **lead-developer:** Author `modules/STANDARDS.md` — comprehensive L1+L2 authoring + review standards (scanned from current modules per RESEARCH TARGET 6): required files, interface schema, input/output/NFR conventions, encryption + deletion protection as mandatory NFRs, naming, multi-resource pattern, adapter extension pattern, code review checklist.
|
||||||
|
2. **lead-developer:** Fix `modules/README.md` catalog index — add rds + uptime + kms-key to Primitives table. Update `modules/README-TEMPLATE.md` — add `## NFRs` section.
|
||||||
|
3. **backend-engineer:** Add `tests/test_module_standards.py` — automated enforcement: every L1 has `deletion_protection` + `encryption_enabled` NFRs; every L2 has valid structure; every module registered; every module has README + examples.
|
||||||
|
|
||||||
|
### Must-haves:
|
||||||
|
- `modules/STANDARDS.md` exists with L1+L2 standards
|
||||||
|
- Catalog index includes all primitives
|
||||||
|
- Template has NFRs section
|
||||||
|
- Automated standards test passes
|
||||||
|
- `pytest` passes (~355)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 36 — schemas-adapters-pipelines-readmes
|
||||||
|
|
||||||
|
**Requirements:** REQ-97 (schemas README), REQ-98 (pipelines README), REQ-99 (adapters README)
|
||||||
|
**Personas:** lead-developer (lead), backend-engineer, platform-engineer
|
||||||
|
**Type:** docs
|
||||||
|
|
||||||
|
### Tasks (Wave 1 — parallel):
|
||||||
|
1. **lead-developer:** Author `schemas/README.md` — how to write schemas, wire into platform, test in CI, dependencies, existing catalog.
|
||||||
|
2. **lead-developer:** Author `pipelines/README.md` — how to write pipeline contracts, wire into workflows, test, dependencies, catalog.
|
||||||
|
3. **lead-developer:** Author `adapters/README.md` — how to write adapters, wire into platform, test, dependencies, catalog.
|
||||||
|
4. **backend-engineer:** Add `tests/test_docs_coverage.py` — assert all 3 READMEs exist + contain required sections.
|
||||||
|
|
||||||
|
### Must-haves:
|
||||||
|
- All 3 READMEs exist with comprehensive documentation
|
||||||
|
- CI validates presence
|
||||||
|
- `pytest` passes (~358)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 37 — verify
|
||||||
|
|
||||||
|
**Personas:** lead-developer (lead), all personas
|
||||||
|
**Type:** verify
|
||||||
|
|
||||||
|
### Tasks:
|
||||||
|
1. Structural: all new files present.
|
||||||
|
2. Behavioral: `pytest` passes (~358); `run_ci.sh` exits 0; `run_platform.sh --check-only` exits 0.
|
||||||
|
3. Security: no secrets; CMK enforced; no placeholder account IDs; deletion protection on by default.
|
||||||
|
4. Quality: each P1 fix + each new feature has a dedicated test.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 38 — review-audit-complete
|
||||||
|
|
||||||
|
**Personas:** lead-developer (lead), all personas
|
||||||
|
**Type:** review + audit + complete
|
||||||
|
|
||||||
|
### Tasks:
|
||||||
|
1. Review: 0 new P0/P1; all P1-3..P1-9 + S1 resolved; 3 new requirements delivered.
|
||||||
|
2. Audit: reconstruction, file discipline, branch hygiene, commit discipline.
|
||||||
|
3. Complete: update REQUIREMENTS.md (REQ-76..99), ROADMAP.md, PROJECT.md. Tag `v1.8.0`. Update floating `v1.8` + `v1` tags. Bump `uses:` to `@v1.8`.
|
||||||
+177
-7
@@ -57,7 +57,7 @@ Finalize the architecture to v1.0 (resolve all 11 open design decisions in
|
|||||||
end-to-end v1 implementation spike:
|
end-to-end v1 implementation spike:
|
||||||
|
|
||||||
- **One L1 module** (`l1-s3`) — substrate-agnostic, IR-typed interface.
|
- **One L1 module** (`l1-s3`) — substrate-agnostic, IR-typed interface.
|
||||||
- **One L2 thin-composition** (`l2-static-asset`) — references the L1.
|
- **One L2 thin-composition** (`l2-static-assets`) — references the L1.
|
||||||
- **Terraform adapter** — compiles the IR to a real `terraform plan`
|
- **Terraform adapter** — compiles the IR to a real `terraform plan`
|
||||||
against AWS via OIDC (no long-lived credentials, per §12.5).
|
against AWS via OIDC (no long-lived credentials, per §12.5).
|
||||||
- **One contract submission** → contract→IR resolution →
|
- **One contract submission** → contract→IR resolution →
|
||||||
@@ -79,7 +79,7 @@ id 202 published. D-034 closed (root key deactivated by user).**
|
|||||||
| 07 | architecture-v1-finalization | Resolve the 11 open decisions → architecture v1.0. Author IR JSON Schema, PolicyCheckResult schema, contract schema, confidence-signal spec, HITL matrix, outbox/ledger design under `schemas/` + `platform/`. |
|
| 07 | architecture-v1-finalization | Resolve the 11 open decisions → architecture v1.0. Author IR JSON Schema, PolicyCheckResult schema, contract schema, confidence-signal spec, HITL matrix, outbox/ledger design under `schemas/` + `platform/`. |
|
||||||
| 08 | aws-oidc-bootstrap | One-shot use of a temporary long-lived key (waiver D-034) to create an IAM role + OIDC trust policy for the act_runner, an S3 state bucket, and a DynamoDB lock table. Rotate the key. Verify the runner assumes the role via OIDC with no long-lived secret. |
|
| 08 | aws-oidc-bootstrap | One-shot use of a temporary long-lived key (waiver D-034) to create an IAM role + OIDC trust policy for the act_runner, an S3 state bucket, and a DynamoDB lock table. Rotate the key. Verify the runner assumes the role via OIDC with no long-lived secret. |
|
||||||
| 09 | v1-spike-ir-and-l1-and-adapter | Target Stack IR; one real L1 (`l1-s3`) with IR-typed interface; L1 registry; Terraform adapter (IR → Terraform var/output + `terraform plan`) running against AWS via OIDC. |
|
| 09 | v1-spike-ir-and-l1-and-adapter | Target Stack IR; one real L1 (`l1-s3`) with IR-typed interface; L1 registry; Terraform adapter (IR → Terraform var/output + `terraform plan`) running against AWS via OIDC. |
|
||||||
| 10 | v1-spike-l2-and-contract-e2e | One L2 thin-composition (`l2-static-asset`) referencing `l1-s3`; contract schema + contract→IR resolution; one end-to-end contract submission → `terraform plan` → Checkov → confidence signal → evidence event to outbox. Verify the IR commitments hold. |
|
| 10 | v1-spike-l2-and-contract-e2e | One L2 thin-composition (`l2-static-assets`) referencing `l1-s3`; contract schema + contract→IR resolution; one end-to-end contract submission → `terraform plan` → Checkov → confidence signal → evidence event to outbox. Verify the IR commitments hold. |
|
||||||
|
|
||||||
Milestone COMPLETE gate: review → ship `v1.2.0` (feature milestone, next
|
Milestone COMPLETE gate: review → ship `v1.2.0` (feature milestone, next
|
||||||
minor per ship.md) → audit. **DONE.**
|
minor per ship.md) → audit. **DONE.**
|
||||||
@@ -167,6 +167,135 @@ Three scope axes:
|
|||||||
Milestone COMPLETE gate: review → ship `v1.4.1` (feature milestone, next
|
Milestone COMPLETE gate: review → ship `v1.4.1` (feature milestone, next
|
||||||
minor per ship.md — v1.3 shipped `v1.3.2`) → audit.
|
minor per ship.md — v1.3 shipped `v1.3.2`) → audit.
|
||||||
|
|
||||||
|
## Objective for Milestone v1.7 (complete)
|
||||||
|
|
||||||
|
Production platform + contract ingestion + pipeline maturation. The v1.6
|
||||||
|
milestone left the platform documented and environments-aware; v1.7 took it
|
||||||
|
to a production-grade platform. 12 user-directed scope axes (2026-07-22):
|
||||||
|
|
||||||
|
1. **Rename `static-assets` → `static-assets`** (D-048 — including
|
||||||
|
`.ciagent/` historical narrative, overriding the v1.6 preservation
|
||||||
|
precedent). The reconstruction test is updated to expect `static-assets`.
|
||||||
|
2. **Augment `static-assets` to a production-ready stack** by authoring a
|
||||||
|
new `cloudfront` primitive + a `waf` primitive (D-049: S3 + CloudFront
|
||||||
|
OAC + WAF; Route53/ACM are domain-dependent and deferred to documented
|
||||||
|
extension points).
|
||||||
|
3. **DX-friendly deploy outputs** (D-050): SSM Parameter Store (KMS-encrypted
|
||||||
|
`SecureString`) for runtime-injectable values + GitHub PR comment / job
|
||||||
|
summary for human-readable connection strings. No raw secrets in logs.
|
||||||
|
4. **Central deploy pipeline error reporting** via the platform Lambda
|
||||||
|
`report_error` action (D-055): the Lambda creates a GitHub issue on the
|
||||||
|
platform repo. The consumer's onboarding-granted Lambda-invoke permission
|
||||||
|
is the only grant needed — uniform pathway, no separate GitHub
|
||||||
|
`issues: write` on the consumer side. Gitea is excluded (only the CIAgent
|
||||||
|
uses it).
|
||||||
|
5. **PR comments after every successful stage** so developers always know
|
||||||
|
where they stand.
|
||||||
|
6. **Three platform pipelines**: (1) platform-test (PR, unit + integration +
|
||||||
|
schema-validation); (2) primitives-plan (PR, plan-only for all L1
|
||||||
|
primitives); (3) patterns-plan (PR, plan-only for all L2 modules).
|
||||||
|
7. **Release job** on merge to `main`: computes MAJOR.MINOR.PATCH semver,
|
||||||
|
creates the tag, then updates (force-moves) or creates the MAJOR.MINOR +
|
||||||
|
MAJOR floating tags (D-057). Consumers on `@v1` or `@v1.6` receive updates
|
||||||
|
depending on their pinned version.
|
||||||
|
8. **Platform Lambda** for one-way consumer→platform communication
|
||||||
|
(contracts). Onboarding grants the consumer repo's environment the right
|
||||||
|
to trigger the Lambda (cross-account IAM). The Lambda ingests contracts
|
||||||
|
and stores them in a DynamoDB table `acdl-contracts` (D-051) for
|
||||||
|
historical reference, impact analysis, CMDB-style application-state
|
||||||
|
queries, and pattern detection. The IAM policy reflects cross-account
|
||||||
|
invocation.
|
||||||
|
9. **Tagging standards** in policy/compliance checks (D-054): a required-tag
|
||||||
|
set (`acdl:owner`, `acdl:contract`, `acdl:environment`, `acdl:cost-center`)
|
||||||
|
enforced by a Checkov custom YAML rule. Closes the D-043 deferral (the
|
||||||
|
SKIPPED `ACDL_TAG_NAMING` placeholder becomes a real check).
|
||||||
|
10. **Wiz adapter** for security checks (D-052): a stub + schema path that
|
||||||
|
translates Wiz API issues → `PolicyCheckResult` records, degrading
|
||||||
|
gracefully when unconfigured. Matches the Checkov adapter pattern.
|
||||||
|
11. **Kyverno adapter** for compliance/security checks (D-053): a
|
||||||
|
K8s-native policy adapter that translates Kyverno `PolicyReport` results
|
||||||
|
→ `PolicyCheckResult` records. Ready but inactive for Terraform-only
|
||||||
|
stacks (the platform emits Terraform, not K8s manifests); it activates
|
||||||
|
when the GitOps reconciler (roadmap) emits K8s manifests.
|
||||||
|
12. **Remove the legacy consumer-repos directory** and add validated per-module examples
|
||||||
|
(D-058: `modules/<name>/examples/` with `simple.yaml` + `complex.yaml`
|
||||||
|
validated in CI) + a new RDS primitive demonstrating multi-engine
|
||||||
|
variation (D-059).
|
||||||
|
|
||||||
|
## Milestone v1.7 Phases
|
||||||
|
|
||||||
|
| Phase | Name | Goal |
|
||||||
|
|-------|------|------|
|
||||||
|
| 22 | rename-and-production-static-assets-stack | Rename `static-assets` → `static-assets` everywhere (D-048). Author `cloudfront` + `waf` primitives. Augment `static-assets` to S3 + CloudFront (OAC) + WAF (D-049). Expand adapter. Bump `uses:` to `@v1.6`; create floating `v1.6` + `v1` tags (D-057). |
|
||||||
|
| 23 | tagging-standards-and-security-adapters | Required-tag set + Checkov custom rule (D-054, D-043 closure). Wiz adapter stub (D-052). Kyverno K8s-native adapter (D-053). Schema engine enum updated. |
|
||||||
|
| 24 | platform-lambda-and-contract-ingestion | Platform Lambda + DynamoDB `acdl-contracts` table (D-051) + cross-account IAM + onboarding grant. |
|
||||||
|
| 25 | deploy-pipeline-dx-outputs-and-error-reporting | SSM SecureString + PR comment outputs (D-050). Lambda `report_error` → GitHub issue (D-055). Stage comments after each successful stage. |
|
||||||
|
| 26 | platform-pipelines-and-release-automation | 3 platform pipelines (platform-test, primitives-plan, patterns-plan). Release job with semver + MAJOR.MINOR/MAJOR tag updates (D-057). |
|
||||||
|
| 27 | remove-legacy-consumer-repos-and-module-documentation-examples | Delete the legacy consumer-repos directory. RDS primitive (D-059). Validated per-module examples (D-058). Docs updates. |
|
||||||
|
|
||||||
|
Milestone COMPLETE gate: review → ship `v1.7.0` (feature milestone, next
|
||||||
|
minor per ship.md — v1.6 shipped `v1.6.0`) → audit.
|
||||||
|
|
||||||
|
## Objective for Milestone v1.8 (active)
|
||||||
|
|
||||||
|
P1 remediation + uptime monitoring + engineering standards + encryption
|
||||||
|
and deletion-protection by default + decommission alias + documentation.
|
||||||
|
The v1.7 milestone shipped production platform + contract ingestion but
|
||||||
|
left 8 P1 issues flagged for post-hoc review. v1.8 clears all of them
|
||||||
|
AND delivers three user-directed feature/NFR tracks (2026-07-22):
|
||||||
|
|
||||||
|
**Track 1 — P1 Remediation (Phases 28–30):**
|
||||||
|
Clear all 8 pending P1 issues from v1.5/v1.6/v1.7 verify reviews:
|
||||||
|
- P1-3: SSM uses AWS-managed key silently → fail loud without CMK config
|
||||||
|
- P1-4: WAF custom rules emit invalid HCL (attribute vs block syntax)
|
||||||
|
- P1-5: WAF default_action input silently ignored
|
||||||
|
- P1-6: consumer_invoke_policy.json has placeholder account ID
|
||||||
|
- P1-7: L2 composition outputs section not implemented in resolver
|
||||||
|
- P1-8: terraform/spike/*.tf overwritten by run_platform.sh (state
|
||||||
|
contamination)
|
||||||
|
- P1-9: GitHub API URLs hardcoded in contract_ingestor.py (Gitea fails
|
||||||
|
silently)
|
||||||
|
- S1: Deploy workflow static-key override not wired (passes ACDL_AWS_*
|
||||||
|
env vars to configure-aws-credentials which reads AWS_*/its own inputs)
|
||||||
|
|
||||||
|
**Track 2 — Encryption + Deletion Protection by Default (Phases 31–32):**
|
||||||
|
All primitives encrypted by default (CMK priority + SSE, managed KMS
|
||||||
|
fallback). Per-stack CMK (one key per L2 deployment, 90-day rotation,
|
||||||
|
no shared keys). Deletion protection on by default for every primitive.
|
||||||
|
L2 modules expose a feature flag to turn off deletion protection. A
|
||||||
|
decommission alias uses a 2-step pipeline (disable deletion protection
|
||||||
|
→ zero counts → destroy) with HITL SRE gates and CMDB-validated change
|
||||||
|
request ID.
|
||||||
|
|
||||||
|
**Track 3 — Uptime + Standards + Docs (Phases 33–36):**
|
||||||
|
A new uptime-kuma primitive (ECS Fargate) deployed by default after any
|
||||||
|
L2 module deploy (separate terraform state), with a feature flag to
|
||||||
|
disable. Monitored endpoints passed from L2 outputs. Alert channels
|
||||||
|
(Teams/email/SMS/GitHub issues). The uptime URL published to consumers
|
||||||
|
via PR comments. Engineering standards for L1 + L2 module authoring
|
||||||
|
(scanned from current modules, stored in modules/). READMEs for
|
||||||
|
schemas/, adapters/, pipelines/ paths documenting how to write, wire,
|
||||||
|
and test each.
|
||||||
|
|
||||||
|
## Milestone v1.8 Phases
|
||||||
|
|
||||||
|
| Phase | Name | Goal |
|
||||||
|
|-------|------|------|
|
||||||
|
| 28 | adapter-waf-and-resolver-outputs | Fix WAF HCL emission (nested rules blocks + default_action input) + implement L2 composition outputs in resolver + adapter output blocks. P1-4, P1-5, P1-7. |
|
||||||
|
| 29 | ssm-kms-and-invoke-policy | SSM publisher fails loud without CMK (escape hatch for local) + Terraform-rendered consumer_invoke_policy (no placeholder account ID). P1-3, P1-6. |
|
||||||
|
| 30 | run-platform-isolation-and-api-portability | Adapter output to per-run temp dir (remove committed spike .tf) + forge-agnostic API URLs + deploy.yml static-key override wired. P1-8, P1-9, S1. |
|
||||||
|
| 31 | encryption-by-default-and-per-stack-cmk | KMS-key primitive + per-stack CMK wired in L2 modules + encryption NFRs on all primitives + managed KMS fallback. |
|
||||||
|
| 32 | deletion-protection-by-default-and-l2-feature-flag | Deletion protection NFR on all primitives (default true) + L2 feature flag + contract schema update. |
|
||||||
|
| 33 | uptime-kuma-primitive | Uptime L1 primitive (ECS Fargate, feature flag, monitored endpoints, alert channels) + deploy-uptime pipeline stage (separate state) + URL published via PR comment. |
|
||||||
|
| 34 | decommission-alias-and-cmdb-validation | Decommission mode on deploy pipeline (2-step: disable deletion protection → zero counts, HITL SRE gates) + DynamoDB CMDB validation + consumer guide docs. |
|
||||||
|
| 35 | module-engineering-standards | modules/STANDARDS.md (L1+L2 authoring + review standards scanned from current modules) + catalog index fix + template update + automated standards test. |
|
||||||
|
| 36 | schemas-adapters-pipelines-readmes | schemas/README.md + pipelines/README.md + adapters/README.md (how to write, wire, test, dependencies). |
|
||||||
|
| 37 | verify | 4-layer verification of all v1.8 phases. |
|
||||||
|
| 38 | review-audit-complete | Multi-persona review + audit + milestone completion (tag v1.8.0). |
|
||||||
|
|
||||||
|
Milestone COMPLETE gate: review → ship `v1.8.0` (feature milestone, next
|
||||||
|
minor per run.md — v1.7 shipped `v1.7.0`) → audit.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
### v1.0 (Prior milestone — the demo)
|
### v1.0 (Prior milestone — the demo)
|
||||||
@@ -193,7 +322,7 @@ New requirements REQ-16..REQ-28 — see `REQUIREMENTS.md` §v1.1. Summary:
|
|||||||
- **REQ-23:** AWS OIDC bootstrap (IAM role + trust policy for act_runner);
|
- **REQ-23:** AWS OIDC bootstrap (IAM role + trust policy for act_runner);
|
||||||
the long-lived key is used once then rotated (waiver D-034).
|
the long-lived key is used once then rotated (waiver D-034).
|
||||||
- **REQ-24:** One real L1 module (`l1-s3`) with an IR-typed interface.
|
- **REQ-24:** One real L1 module (`l1-s3`) with an IR-typed interface.
|
||||||
- **REQ-25:** One real L2 thin-composition (`l2-static-asset`) referencing
|
- **REQ-25:** One real L2 thin-composition (`l2-static-assets`) referencing
|
||||||
`l1-s3`.
|
`l1-s3`.
|
||||||
- **REQ-26:** Terraform adapter compiles the IR to a real `terraform plan`
|
- **REQ-26:** Terraform adapter compiles the IR to a real `terraform plan`
|
||||||
against AWS via OIDC; state in S3 + DynamoDB.
|
against AWS via OIDC; state in S3 + DynamoDB.
|
||||||
@@ -223,7 +352,7 @@ New requirements REQ-29..REQ-35 — see `REQUIREMENTS.md` §v1.2. Summary:
|
|||||||
- **REQ-35:** End-to-end verification — consumer commit → live ECS service
|
- **REQ-35:** End-to-end verification — consumer commit → live ECS service
|
||||||
(HTTP 200) → evidence event → timeline.
|
(HTTP 200) → evidence event → timeline.
|
||||||
|
|
||||||
### v1.4 (Active milestone — central pipeline contract + shell reproducibility + streaming)
|
### v1.4 (Prior milestone — central pipeline contract + shell reproducibility + streaming)
|
||||||
|
|
||||||
New requirements REQ-43..REQ-45 — see `REQUIREMENTS.md` §v1.4. Summary:
|
New requirements REQ-43..REQ-45 — see `REQUIREMENTS.md` §v1.4. Summary:
|
||||||
|
|
||||||
@@ -279,7 +408,7 @@ decisions:
|
|||||||
|----|----------|-----------|---------|
|
|----|----------|-----------|---------|
|
||||||
| D-034 | Temporary long-lived AWS key (waiver) used once in Phase 08 to bootstrap the state backend + IAM user; rotated/deactivated immediately after | §12.5 forbids long-lived creds; the bootstrap needed one `aws iam` call before the spike user + rotated key could take over | Spike achieves real `terraform plan` against AWS without violating the locked target after bootstrap. **CLOSED 2026-07-21: root key `AKIA…ROOT-DEACTIVATED` deactivated by the user in the AWS IAM console (verified — `InvalidClientTokenId`); the spike uses the rotated `acdl-spike-runner` key per D-039. Key ID redacted in v1.2 Phase 12 (P1-1).** |
|
| D-034 | Temporary long-lived AWS key (waiver) used once in Phase 08 to bootstrap the state backend + IAM user; rotated/deactivated immediately after | §12.5 forbids long-lived creds; the bootstrap needed one `aws iam` call before the spike user + rotated key could take over | Spike achieves real `terraform plan` against AWS without violating the locked target after bootstrap. **CLOSED 2026-07-21: root key `AKIA…ROOT-DEACTIVATED` deactivated by the user in the AWS IAM console (verified — `InvalidClientTokenId`); the spike uses the rotated `acdl-spike-runner` key per D-039. Key ID redacted in v1.2 Phase 12 (P1-1).** |
|
||||||
| D-035 | Milestone version = `v1.1` (feature), ship tag `v1.2.0` | Real platform is a breaking reframing of the demo, but treated as the next incremental milestone per user choice; ship.md: feature milestone → next minor | Tag `v1.2.0` on milestone COMPLETE |
|
| D-035 | Milestone version = `v1.1` (feature), ship tag `v1.2.0` | Real platform is a breaking reframing of the demo, but treated as the next incremental milestone per user choice; ship.md: feature milestone → next minor | Tag `v1.2.0` on milestone COMPLETE |
|
||||||
| D-036 | Spike picks `l1-s3` + `l2-static-asset` | Simplest real AWS resource (no IAM/network deps); smallest real `terraform plan`; proves the IR + adapter end-to-end | Spike scope fixed |
|
| D-036 | Spike picks `l1-s3` + `l2-static-assets` | Simplest real AWS resource (no IAM/network deps); smallest real `terraform plan`; proves the IR + adapter end-to-end | Spike scope fixed |
|
||||||
| D-037 | Demo archived to `demo/` (not deleted) | Preserves the working v1.0 demo as intent reference; new platform layout under `platform/`, `schemas/`, `adapters/`, `terraform/`, `modules-ir/` | No churn on demo code; clean separation |
|
| D-037 | Demo archived to `demo/` (not deleted) | Preserves the working v1.0 demo as intent reference; new platform layout under `platform/`, `schemas/`, `adapters/`, `terraform/`, `modules-ir/` | No churn on demo code; clean separation |
|
||||||
| D-038 | Open decisions resolved in "accept recommendations + decide rest" mode | User-locked mode: accept architecture's stated recommendations (W1.A, W1.B, W2.A, BA.A); lead-developer decides the remaining 8 (W3.D, W3.E, BA.B, BA.C, BA.D, BA.E, BA.F, OpenTofu timing) with rationale | Architecture reaches v1.0 in Phase 07 |
|
| D-038 | Open decisions resolved in "accept recommendations + decide rest" mode | User-locked mode: accept architecture's stated recommendations (W1.A, W1.B, W2.A, BA.A); lead-developer decides the remaining 8 (W3.D, W3.E, BA.B, BA.C, BA.D, BA.E, BA.F, OpenTofu timing) with rationale | Architecture reaches v1.0 in Phase 07 |
|
||||||
| D-039 | Spike-only waiver: per-run-rotated long-lived AWS key. OIDC federation deferred to v1.2, blocked on go-gitea/gitea#36988. | **RESEARCH TARGET 1 verdict (conf 0.95):** Gitea Actions does NOT support `id-token: write` / OIDC token issuance as of Gitea 1.27.x / gitea-runner v2.1.0. GitHub's OIDC pattern is not portable. The waiver satisfies §12.5's *intent* (no persistent long-lived key) for the spike: the key is rotated after each run by `scripts/rotate_spike_key.sh`. v1.2 implements real OIDC when the Gitea PR merges. | Spike achieves real `terraform plan` against AWS without a *persistently* long-lived key; real OIDC is a v1.2 deliverable |
|
| D-039 | Spike-only waiver: per-run-rotated long-lived AWS key. OIDC federation deferred to v1.2, blocked on go-gitea/gitea#36988. | **RESEARCH TARGET 1 verdict (conf 0.95):** Gitea Actions does NOT support `id-token: write` / OIDC token issuance as of Gitea 1.27.x / gitea-runner v2.1.0. GitHub's OIDC pattern is not portable. The waiver satisfies §12.5's *intent* (no persistent long-lived key) for the spike: the key is rotated after each run by `scripts/rotate_spike_key.sh`. v1.2 implements real OIDC when the Gitea PR merges. | Spike achieves real `terraform plan` against AWS without a *persistently* long-lived key; real OIDC is a v1.2 deliverable |
|
||||||
@@ -292,6 +421,47 @@ decisions:
|
|||||||
| D-046 | `act_runner` → `gitea-runner` rename: Phase 07 updates docs to use the current name `gitea-runner` (renamed 2026-04 in gitea/runner#850). | RESEARCH TARGET 1 + R-4: naming drift between v1.0 docs and the current runner. | Docs reflect the current binary name |
|
| D-046 | `act_runner` → `gitea-runner` rename: Phase 07 updates docs to use the current name `gitea-runner` (renamed 2026-04 in gitea/runner#850). | RESEARCH TARGET 1 + R-4: naming drift between v1.0 docs and the current runner. | Docs reflect the current binary name |
|
||||||
| D-047 | v1.2 carries forward the D-039 per-run-rotated-key waiver. Real OIDC federation remains deferred to v1.3+, blocked on go-gitea/gitea#36988 (re-checked 2026-07-21: still **open**, last updated 2026-05-27, not merged). | §12.5 forbids long-lived creds; the Gitea Actions OIDC provider is still not merged. The waiver continues to satisfy §12.5's *intent* (no *persistently* long-lived key) for v1.2: `scripts/rotate_spike_key.sh` rotates the key, and Phase 12 tightens the IAM scoping + rotation hygiene. | v1.2 achieves `terraform apply` against AWS without a persistently long-lived key; real OIDC is a v1.3+ deliverable. |
|
| D-047 | v1.2 carries forward the D-039 per-run-rotated-key waiver. Real OIDC federation remains deferred to v1.3+, blocked on go-gitea/gitea#36988 (re-checked 2026-07-21: still **open**, last updated 2026-05-27, not merged). | §12.5 forbids long-lived creds; the Gitea Actions OIDC provider is still not merged. The waiver continues to satisfy §12.5's *intent* (no *persistently* long-lived key) for v1.2: `scripts/rotate_spike_key.sh` rotates the key, and Phase 12 tightens the IAM scoping + rotation hygiene. | v1.2 achieves `terraform apply` against AWS without a persistently long-lived key; real OIDC is a v1.3+ deliverable. |
|
||||||
|
|
||||||
|
## Key Decisions (v1.8)
|
||||||
|
|
||||||
|
Resolved at the CLARIFY stage (full autonomy — all within locked
|
||||||
|
constraints or user-directed scope). New v1.8 decisions:
|
||||||
|
|
||||||
|
| ID | Decision | Rationale | Outcome |
|
||||||
|
|----|----------|-----------|---------|
|
||||||
|
| D-061 | Fold all 3 new requirements into v1.8 alongside P1 fixes. | User chose single milestone. v1.8 becomes a feature milestone (ship tag v1.8.0, minor bump). | 11 phases (28–38) in one milestone. |
|
||||||
|
| D-062 | P1-3: SSM publisher fails loud (`RuntimeError`) when `ACDL_KMS_KEY_ID` unset. `ACDL_ALLOW_DEFAULT_KMS=1` escape hatch for local testing. | User chose fail loud. Silent AWS-managed-key use is the security gap; callers must set the env. | Phase 29 implements fail-loud + escape hatch. |
|
||||||
|
| D-063 | P1-6: `consumer_invoke_policy.json` rendered via Terraform `data.aws_caller_identity` + `templatestring` at apply time. | User chose Terraform-rendered. No committed account ID; no stale placeholder. | Phase 29 converts JSON to TF-rendered template. |
|
||||||
|
| D-064 | P1-8: Remove committed `terraform/spike/*.tf` entirely; adapter emits to per-run temp dir. | User chose remove. Cleaner; no stale fixtures. | Phase 30 removes files + changes run_platform.sh target. |
|
||||||
|
| D-065 | S1: Single conditional `configure-aws-credentials` step (OIDC when no static key, access-key/secret-key inputs when static key present). | User chose single conditional step. Cleaner workflow YAML. | Phase 30 restructures the deploy workflow step. |
|
||||||
|
| D-066 | Uptime deployment target: ECS Fargate (reuse existing ecs-cluster + ecs-service + alb primitives). | User chose ECS Fargate. Most consistent with current platform; ALB gives a stable URL. | Phase 33 authors uptime primitive on ECS Fargate. |
|
||||||
|
| D-067 | Uptime trigger: new `deploy-uptime` pipeline stage after `publish-outputs`. Separate terraform state (S3 key prefix `uptime/`). | User chose pipeline stage. Most integrated with existing flow. | Phase 33 adds the pipeline stage + separate state. |
|
||||||
|
| D-068 | CMDB = DynamoDB `acdl-change-requests` table (PK changeRequestId, SK submittedAt). | User chose DynamoDB. Consistent with existing platform Lambda + DynamoDB pattern. | Phase 34 adds the table + `validate_change_request` Lambda action. |
|
||||||
|
| D-069 | Encryption key granularity: per-stack CMK (one key per L2 deployment, tagged with acdl:owner + acdl:environment). | User chose per-stack. No shared keys across stacks; 90-day rotation at creation. | Phase 31 authors kms-key primitive + L2 wiring. |
|
||||||
|
| D-070 | Decommission: new mode on the existing deploy pipeline (`mode: decommission`). 2-step with HITL SRE gates. | User chose existing pipeline with different behavior. Plan/apply to disable deletion protection (HITL SRE gate) → plan/apply with counts=0 (second HITL SRE gate). Documented in consumer guide. | Phase 34 adds decommission mode + HITL gates. |
|
||||||
|
| D-071 | `uses:`/`ref:` bump from `@v1.6` to `@v1.8` at milestone COMPLETE. | Consumer-facing version tracks the last released MAJOR.MINOR. | Phase 38 bumps references + creates floating `v1.8` + `v1` tags. |
|
||||||
|
| D-072 | Managed KMS fallback for standalone L1 deployments (no L2 CMK): adapter uses `alias/aws/<service>` with a stderr warning. `kms_key_arn` input is optional everywhere; `encryption_enabled` NFR defaults to true. | Requirement says "prioritize CMKs, fallback to managed KMS". Standalone L1s don't have a per-stack CMK. | Phase 31 implements fallback + warning. |
|
||||||
|
|
||||||
|
## Key Decisions (v1.7)
|
||||||
|
|
||||||
|
Resolved at the CLARIFY stage (full autonomy — all within locked constraints
|
||||||
|
or user-directed scope). New v1.7 decisions:
|
||||||
|
|
||||||
|
| ID | Decision | Rationale | Outcome |
|
||||||
|
|----|----------|-----------|---------|
|
||||||
|
| D-048 | Rename `static-assets` → `static-assets`: **rewrite all occurrences** including verbatim historical phase descriptions in `.ciagent/` (ROADMAP, REQUIREMENTS, RESEARCH, decision tables), overriding the v1.6 audit precedent that preserved some historical references. | User chose full rewrite. Maximally consistent; the reconstruction test is updated to expect `static-assets` throughout. | Phase 22 rewrites every `static-assets` string to `static-assets`; no preserved historical tokens remain. |
|
||||||
|
| D-049 | Production static-assets stack = S3 + CloudFront (OAC) + WAF. | Self-contained, domain-free production edge. Route53/ACM are domain-dependent (consumer-supplied) and deferred to documented extension points / a complex example. | Phase 22 authors `cloudfront` + `waf` primitives and augments the module. |
|
||||||
|
| D-050 | Deploy outputs: SSM Parameter Store (`SecureString`, KMS-encrypted, namespaced `/acdl/{env}/{contractId}/{output_name}`) for runtime-injectable values + GitHub PR comment / job summary for human-readable connection strings. | Two canonical mechanisms: SSM for resources that read at runtime; PR comment for developers. No raw secrets in logs. | Phase 25 implements `core/output_publisher.py` + two new pipeline stages. |
|
||||||
|
| D-051 | Contract ingestion storage = DynamoDB table `acdl-contracts` (PK `consumerRepo`, SK `contractId#submittedAt`, SSE via customer-managed CMK, point-in-time recovery). | Enables historical queries, impact analysis, CMDB-style application-state queries, and pattern detection via DynamoDB queries. S3 flat-file mirror deferred (DynamoDB is sufficient for v1.7). | Phase 24 defines the table + Lambda. |
|
||||||
|
| D-052 | Wiz adapter = stub + schema path (no live Wiz tenant in CI). | Matches the Checkov adapter pattern; typed interface, offline-testable, degrades gracefully when unconfigured (emits `WIZ_NOT_CONFIGURED` SKIPPED record). | Phase 23 authors `adapters/wiz/wiz_adapter.py`. |
|
||||||
|
| D-053 | Kyverno adapter = K8s-native policy adapter translating `PolicyReport` results → `PolicyCheckResult`. Ready but inactive for Terraform-only stacks. | The platform emits Terraform, not K8s manifests. The adapter activates when the GitOps reconciler (roadmap) emits K8s manifests. Sample policies included as documentation. | Phase 23 authors `adapters/kyverno/kyverno_adapter.py` + sample policies. |
|
||||||
|
| D-054 | Tagging standard = required-tag set (`acdl:owner`, `acdl:contract`, `acdl:environment`, `acdl:cost-center`) enforced by a Checkov custom YAML rule. | Closes the D-043 deferral (the SKIPPED `ACDL_TAG_NAMING` placeholder becomes a real check). Naming-convention regex deferred (brittle across AWS resource types). | Phase 23 authors `schemas/tagging-standard.json` + `adapters/terraform/policy/custom_rules/acdl_tagging.yaml`. |
|
||||||
|
| D-055 | Error reporting = the platform Lambda `report_error` action creates a GitHub issue on the platform repo (`acdl/acdl`). Uniform communication pathway via the Lambda; the consumer's onboarding-granted Lambda-invoke permission is the only grant needed. No separate GitHub `issues: write` on the consumer side. Gitea is excluded (only the CIAgent uses it; platform engineers and consumers use GitHub). | Unifies requirements 4 + 8 around one mechanism. The Lambda holds a GitHub token (Secrets Manager) scoped to the platform repo. Idempotent (comments on existing open issue rather than duplicating). | Phase 24 prepares the action; Phase 25 implements it + wires the `if: failure()` workflow step. |
|
||||||
|
| D-056 | Ship `v1.7.0`; bump `uses:`/`ref:` from `@v1.4` to `@v1.6`. | Consumer-facing version tracks the last released MAJOR.MINOR. Consumers on `@v1.4` stay on v1.4 behavior until they bump. | Phase 22 bumps the references. |
|
||||||
|
| D-057 | The `uses:`/`ref:` bump + floating `v1.6`/`v1` tag creation happen in Phase 22 (pointing at `v1.6.0`), so the reference never points at a non-existent tag. The release job (Phase 26) owns ongoing tag updates. | Sequencing: if Phase 22 bumps `uses:` to `@v1.6` but the tag doesn't exist, the reference is temporarily broken. Creating the tag early (pointing at the last release) fixes this. | Phase 22 creates the floating tags; Phase 26's release job maintains them. |
|
||||||
|
| D-058 | Module examples = separate validated files in `modules/<name>/examples/` (`simple.yaml` + `complex.yaml` + variation files), validated against `schemas/contract.schema.json` in the platform-test pipeline schema-validation stage. Each module's README `## Examples` section references + excerpts them. | Examples cannot drift from the schema silently. | Phase 27 authors the example files; Phase 26's platform-test pipeline validates them. |
|
||||||
|
| D-059 | Add an RDS primitive (`modules/l1/rds/`) with an `engine` input (enum: postgres, mysql, etc.) + a multi-engine example demonstrating the variation pattern. | Concrete demonstration of the multi-engine variation the requirement calls out. Adds one primitive + examples. | Phase 27 authors the primitive + adapter expansion + examples. |
|
||||||
|
| D-060 | (Consolidated into D-058.) | — | — |
|
||||||
|
|
||||||
### Open-decision resolutions (Phase 07 deliverable — recorded here for traceability)
|
### Open-decision resolutions (Phase 07 deliverable — recorded here for traceability)
|
||||||
|
|
||||||
| ID | Question | Resolution |
|
| ID | Question | Resolution |
|
||||||
@@ -329,8 +499,8 @@ sign-off (autonomy = full; all within locked constraints).
|
|||||||
| OIDC IAM role | `acdl-act-runner-role` | Assumed by the act_runner via web-identity |
|
| OIDC IAM role | `acdl-act-runner-role` | Assumed by the act_runner via web-identity |
|
||||||
| OIDC trust subject | `repo:continuous-intelligence/acdl:ref:refs/heads/main` (+ phase branches) | Least-privilege; refined in Phase 08 |
|
| OIDC trust subject | `repo:continuous-intelligence/acdl:ref:refs/heads/main` (+ phase branches) | Least-privilege; refined in Phase 08 |
|
||||||
| Spike L1 (`l1-s3`) inputs | `bucket_name: string`, `region: string` | Minimal S3 interface per §2 |
|
| Spike L1 (`l1-s3`) inputs | `bucket_name: string`, `region: string` | Minimal S3 interface per §2 |
|
||||||
| Spike L2 (`l2-static-asset`) | thin-composition referencing `l1-s3` only; depth 1 | Smallest real plan per D-036 |
|
| Spike L2 (`l2-static-assets`) | thin-composition referencing `l1-s3` only; depth 1 | Smallest real plan per D-036 |
|
||||||
| Spike contract | `contracts/spike.yaml`: `stack: l2-static-asset`, `environment: dev`, `inputs: { bucket_name: acdl-spike-bucket, region: us-east-1 }` | One end-to-end submission (REQ-27) |
|
| Spike contract | `contracts/spike.yaml`: `stack: l2-static-assets`, `environment: dev`, `inputs: { bucket_name: acdl-spike-bucket, region: us-east-1 }` | One end-to-end submission (REQ-27) |
|
||||||
| Spike `terraform` command | `plan` only | `apply` is out of scope (Out of Scope table); HITL-gated in v1.2 |
|
| Spike `terraform` command | `plan` only | `apply` is out of scope (Out of Scope table); HITL-gated in v1.2 |
|
||||||
| Checkov ruleset (spike) | the 4 L2 checks (secrets-in-plaintext, public ingress, IAM wildcard, KMS key reference) + tag/naming | §3 + §12.4; Kyverno/OPA deferred |
|
| Checkov ruleset (spike) | the 4 L2 checks (secrets-in-plaintext, public ingress, IAM wildcard, KMS key reference) + tag/naming | §3 + §12.4; Kyverno/OPA deferred |
|
||||||
| v1.0 tags preserved | `v1.0.1`..`v1.0.5`, `v1.1.0` retained | Immutability; demo archive does not rewrite history |
|
| v1.0 tags preserved | `v1.0.1`..`v1.0.5`, `v1.1.0` retained | Immutability; demo archive does not rewrite history |
|
||||||
|
|||||||
+158
-5
@@ -51,11 +51,11 @@
|
|||||||
|
|
||||||
### Category: v1 Spike — IR, L1, Adapter
|
### Category: v1 Spike — IR, L1, Adapter
|
||||||
- **REQ-24:** One real L1 module `l1-s3` exists under `modules-ir/l1/l1-s3/` with an IR-typed interface (typed inputs/outputs/NFRs) registered in the L1 registry.
|
- **REQ-24:** One real L1 module `l1-s3` exists under `modules-ir/l1/l1-s3/` with an IR-typed interface (typed inputs/outputs/NFRs) registered in the L1 registry.
|
||||||
- **REQ-25:** One real L2 thin-composition `l2-static-asset` exists under `modules-ir/l2/l2-static-asset/` referencing `l1-s3` only (depth 1, within max-depth-5).
|
- **REQ-25:** One real L2 thin-composition `l2-static-assets` exists under `modules-ir/l2/l2-static-assets/` referencing `l1-s3` only (depth 1, within max-depth-5).
|
||||||
- **REQ-26:** The Terraform adapter (`adapters/terraform/`) compiles the IR-typed L1 interface to Terraform `variable`/`output` blocks and the L2 thin-composition tree to a Terraform root module; it emits a real `terraform plan` against AWS via OIDC; state is stored in S3 + DynamoDB.
|
- **REQ-26:** The Terraform adapter (`adapters/terraform/`) compiles the IR-typed L1 interface to Terraform `variable`/`output` blocks and the L2 thin-composition tree to a Terraform root module; it emits a real `terraform plan` against AWS via OIDC; state is stored in S3 + DynamoDB.
|
||||||
|
|
||||||
### Category: v1 Spike — End-to-End
|
### Category: v1 Spike — End-to-End
|
||||||
- **REQ-27:** One end-to-end contract submission (`contracts/spike.yaml` for `l2-static-asset`) flows through: contract schema validation → contract→IR resolution → `terraform plan` (real AWS) → Checkov `PolicyCheckResult` → confidence signal → evidence event written to the DynamoDB outbox.
|
- **REQ-27:** One end-to-end contract submission (`contracts/spike.yaml` for `l2-static-assets`) flows through: contract schema validation → contract→IR resolution → `terraform plan` (real AWS) → Checkov `PolicyCheckResult` → confidence signal → evidence event written to the DynamoDB outbox.
|
||||||
- **REQ-28:** Spike verification (`scripts/verify_phase10.sh`) proves the IR-shaped commitments hold: the adapter is the only substrate-specific code; no polyglot mess; the L1 content, contract YML, and thin-composition tree are substrate-agnostic.
|
- **REQ-28:** Spike verification (`scripts/verify_phase10.sh`) proves the IR-shaped commitments hold: the adapter is the only substrate-specific code; no polyglot mess; the L1 content, contract YML, and thin-composition tree are substrate-agnostic.
|
||||||
|
|
||||||
## Out of Scope (v1.1)
|
## Out of Scope (v1.1)
|
||||||
@@ -124,14 +124,104 @@
|
|||||||
|
|
||||||
### Category: Consumer Happy Path Documentation
|
### Category: Consumer Happy Path Documentation
|
||||||
- **REQ-46:** `README.md` is rewritten so the consumer model is unambiguous: this repo is the platform source; a consumer never clones it. A consumer repo contains only app code + `contract.yaml` referencing the central pipeline + contract. The platform-flow diagram is a mermaid `flowchart TD` (replacing the ASCII art). "L3A"/"L3B" nomenclature is removed from README (single-surface model). "spike" nomenclature is removed from prose (code paths in bash blocks are kept verbatim).
|
- **REQ-46:** `README.md` is rewritten so the consumer model is unambiguous: this repo is the platform source; a consumer never clones it. A consumer repo contains only app code + `contract.yaml` referencing the central pipeline + contract. The platform-flow diagram is a mermaid `flowchart TD` (replacing the ASCII art). "L3A"/"L3B" nomenclature is removed from README (single-surface model). "spike" nomenclature is removed from prose (code paths in bash blocks are kept verbatim).
|
||||||
- **REQ-47:** `docs/CONSUMER_GUIDE.md` (all-caps) replaces `docs/consumer-guide-static-asset.md`. It is generic across all L2 modules (`static-asset` as the worked example), uses mermaid diagrams (model + pipeline flow), documents versioned `uses:` references (floating MAJOR+MINOR tags — bare/`@main` discouraged), scopes prerequisites to consumer-repo bootstrap only (no Terraform/Checkov/boto3/runner-key — those are platform-repo concerns), and documents that the pipeline fetches the ACDL repo at run time via a reusable workflow (consumers never invoke `scripts/run_platform.sh` locally for the happy path).
|
- **REQ-47:** `docs/CONSUMER_GUIDE.md` (all-caps) replaces `docs/consumer-guide-static-assets.md`. It is generic across all L2 modules (`static-assets` as the worked example), uses mermaid diagrams (model + pipeline flow), documents versioned `uses:` references (floating MAJOR+MINOR tags — bare/`@main` discouraged), scopes prerequisites to consumer-repo bootstrap only (no Terraform/Checkov/boto3/runner-key — those are platform-repo concerns), and documents that the pipeline fetches the ACDL repo at run time via a reusable workflow (consumers never invoke `scripts/run_platform.sh` locally for the happy path).
|
||||||
- **REQ-48:** `README.md` Credentials section is rewritten to express the zero-trust target model: consumer repos use OIDC federation (no long-lived keys) with attribute-based authorization (ABAC) — IAM roles + session policies scoped by repository identity and resource-creation tags so a consumer can only view/update resources it created (blast-radius containment). A documented override allows a static key in GitHub Secrets (consumer repo) or `.env.secrets` (local testing), rotated by a platform-managed scheduled pipeline on a daily cadence; when `.env.secrets` is used locally, rotating out of band is the consumer's responsibility.
|
- **REQ-48:** `README.md` Credentials section is rewritten to express the zero-trust target model: consumer repos use OIDC federation (no long-lived keys) with attribute-based authorization (ABAC) — IAM roles + session policies scoped by repository identity and resource-creation tags so a consumer can only view/update resources it created (blast-radius containment). A documented override allows a static key in GitHub Secrets (consumer repo) or `.env.secrets` (local testing), rotated by a platform-managed scheduled pipeline on a daily cadence; when `.env.secrets` is used locally, rotating out of band is the consumer's responsibility.
|
||||||
|
|
||||||
### Category: Reusable Deploy Workflow
|
### Category: Reusable Deploy Workflow
|
||||||
- **REQ-49:** A reusable deploy workflow exists as byte-identical `.gitea/workflows/deploy.yml` (Gitea, dev) and `.github/workflows/deploy.yml` (GitHub, production), implementing the central deployment pipeline contract (`pipelines/deploy.yaml` validated against `schemas/deploy-pipeline.schema.json`). It is invoked by consumer repos via `uses: acdl/.gitea/workflows/deploy.yml@vMAJOR.MINOR` (versioned tag). The workflow checks out the consumer repo, checks out the ACDL platform repo into the runner workspace, installs runtime deps (Python, Terraform, Checkov), and invokes `scripts/run_platform.sh` against the consumer's contract path (passed as a workflow input). OIDC is the default auth (`permissions: id-token: write`); a static-key override reads from repository secrets.
|
- **REQ-49:** A reusable deploy workflow exists as byte-identical `.gitea/workflows/deploy.yml` (Gitea, dev) and `.github/workflows/deploy.yml` (GitHub, production), implementing the central deployment pipeline contract (`pipelines/deploy.yaml` validated against `schemas/deploy-pipeline.schema.json`). It is invoked by consumer repos via `uses: acdl/.gitea/workflows/deploy.yml@vMAJOR.MINOR` (versioned tag). The workflow checks out the consumer repo, checks out the ACDL platform repo into the runner workspace, installs runtime deps (Python, Terraform, Checkov), and invokes `scripts/run_platform.sh` against the consumer's contract path (passed as a workflow input). OIDC is the default auth (`permissions: id-token: write`); a static-key override reads from repository secrets.
|
||||||
- **REQ-50:** `contracts/static-asset.yaml` uses a versioned `uses:` reference (`@v1.4`, MAJOR+MINOR) — not bare `@v1` or `@main` — as the canonical example the consumer guide points at.
|
- **REQ-50:** `contracts/static-assets.yaml` uses a versioned `uses:` reference (`@v1.4`, MAJOR+MINOR) — not bare `@v1` or `@main` — as the canonical example the consumer guide points at.
|
||||||
- **REQ-51:** `tests/test_pipeline_contract.py` is extended to validate the new deploy workflows: both files exist, are byte-identical, and conform to `schemas/deploy-pipeline.schema.json` (stages present, names match `pipelines/deploy.yaml` stage names). The existing CI-workflow conformance tests continue to pass unchanged.
|
- **REQ-51:** `tests/test_pipeline_contract.py` is extended to validate the new deploy workflows: both files exist, are byte-identical, and conform to `schemas/deploy-pipeline.schema.json` (stages present, names match `pipelines/deploy.yaml` stage names). The existing CI-workflow conformance tests continue to pass unchanged.
|
||||||
|
|
||||||
|
## v1.6 (Active — consumer-facing docs restructure + terminology normalization + environments concept)
|
||||||
|
|
||||||
|
### Category: Internal-surface scrub
|
||||||
|
- **REQ-52:** No consumer-facing documentation (README.md, docs/**, modules/**/README.md, contracts/**) references `.ciagent/` — it is local CIAgent metadata, never visible to platform engineers or consumers. The README repository-layout table has no `.ciagent/` row. No `.gitea/` references appear in consumer-facing docs (consumers use GitHub only); the README repository-layout table has no `.gitea/workflows/` row.
|
||||||
|
- **REQ-53:** `acdl_platform/` is renamed to `core/` across the directory, all imports in tests/scripts/pipelines/workflows, and all doc references. (`platform/` was the original target but shadows Python's stdlib `platform` module — `core/` was chosen to stay importable.) `grep -R "acdl_platform" .` (excluding `.ciagent/`, `demo/`, `.git/`) returns 0 hits. The test suite passes after the rename.
|
||||||
|
|
||||||
|
### Category: Docs site restructure
|
||||||
|
- **REQ-54:** `docs/` is restructured into a Jekyll-style GitHub Pages site: `docs/_config.yml`, `docs/index.md` (landing), `docs/modules/` (catalog + per-module Pages-friendly copies), `docs/contracts/index.md`, `docs/pipeline/index.md` + `docs/pipeline/versioning.md`, `docs/environments/index.md`, `docs/consumer-guide.md`, `docs/architecture.md` (consolidated from architecture.md + architecture-v1.0.md, current-architecture only), `docs/vision.md`. No `.ciagent/` links anywhere in `docs/`. Consumer-facing content (modules, contracts, pipeline, versioning) lives in Pages.
|
||||||
|
|
||||||
|
### Category: Terminology normalization
|
||||||
|
- **REQ-55:** Consumer-facing docs drop the "L2" nomenclature — L2 modules are referred to as "modules". "L1" label is dropped in consumer-facing docs — L1 primitives are referred to as "primitives". The "composition" terminology is changed to "pattern" for modules in prose (the on-disk `composition.json` files and code references are unchanged this phase). A roadmap entry records that "composition" will later describe the thin orchestration where consumers dynamically create a module directly from the contract file (future implementation, not implemented now).
|
||||||
|
- **REQ-56:** The term "forge" is replaced in consumer-facing docs with "platform runners" / "platform-managed" as appropriate. The term "forge" remains only in internal architecture docs.
|
||||||
|
|
||||||
|
### Category: README rewrite
|
||||||
|
- **REQ-57:** README.md repository-roles section is restated to match reality: a consumer repo contains (a) its application code, (b) one or more contracts (`.acdl/contract.yaml`), and (c) one or more CI definitions (a thin `.github/workflows/deploy.yml` that `uses:` the central reusable workflow, pointing at the appropriate environment + contract). The platform repo (this one) owns modules/adapters/schemas/pipelines/scripts/workflows. A consumer never clones the platform repo.
|
||||||
|
- **REQ-58:** README.md Status section is replaced with a Features list (referenceable by consumers and platform engineers) and a Roadmap subsection listing only planned future features (no internal CIAgent status, no version-by-version changelog).
|
||||||
|
- **REQ-59:** README.md "How the platform works" mermaid diagram is revised so all node text is visible (no overflow): labels are split with `<br/>`, boxes widened as needed. A security-checks stage is added before the policy-checks stage. Specific tools (Checkov, Terraform) are not named — they are "security checks (adapter)", "policy checks (adapter)", "infrastructure plan". An "infrastructure apply" stage is added at the appropriate level (dev only, after confidence).
|
||||||
|
- **REQ-60:** README.md Credentials & zero-trust section removes the "go-gitea/gitea#36988 blocked" mention and the "waivers D-039/D-047" language (not consumer/platform-engineer facing). It states: default OIDC + ABAC; alternative is a static AWS key (GitHub Secrets for platform-runner runs, or `.env.secrets` locally) with the expectation of daily rotation (platform-managed for runner runs) or out-of-band rotation (consumer-managed for local `.env.secrets`).
|
||||||
|
|
||||||
|
### Category: Environments concept + onboarding
|
||||||
|
- **REQ-61:** The concept of platform-managed environments is introduced: consumers are not required to provide an AWS account, VPC, subnet, S3 state bucket, or runner key. `docs/environments/index.md` documents that a named environment is a platform-owned AWS account + network + state backend + IAM role surfaced to the consumer via ABAC, selected by name in the contract. The old README environments table (dev/qa/prod/dr) is removed completely. A minimal onboarding scaffold exists: `platform/environments/` with a sample `dev.json` + README, `platform/environment_check.py`, a wire-in at the top of `scripts/run_platform.sh`, a friendly first-run onboarding message when no environment is defined for the repo, and `tests/test_environment_check.py` covering the missing-env and present-env cases.
|
||||||
|
|
||||||
|
## v1.7 (Active — production platform + contract ingestion + pipeline maturation)
|
||||||
|
|
||||||
|
### Category: Rename + production-ready stack
|
||||||
|
- **REQ-62:** `static-assets` is renamed to `static-assets` everywhere (D-048 — including `.ciagent/` historical narrative: verbatim phase descriptions, REQ-25/27/50 text, D-036, RESEARCH.md). `grep -R "static-assets[^s]" .` (excluding `.git/`) returns 0 hits. The module dir `modules/l2/static-assets/` → `modules/l2/static-assets/`; `contracts/static-assets.yaml` → `contracts/static-assets.yaml`; the registry key is renamed; all scripts, tests, docs, and `.ciagent/` files use `static-assets`. The reconstruction test is updated to expect `static-assets` throughout.
|
||||||
|
- **REQ-63:** Two new primitives exist: `cloudfront` (distribution + OAC, stack types `aws:cloudfront:distribution` + `aws:cloudfront:originaccesscontrol`) and `waf` (WAFv2 web ACL, stack type `aws:wafv2:webacl`), each with an `interface.json` valid against `schemas/stack.schema.json` and a full README (Resources/Inputs/Outputs/Usage/Compliance/Versioning). Both are registered in `modules/registry.json`. The Terraform adapter `TYPE_MAP`/`INPUT_MAP`/`OUTPUT_MAP` covers the new stack types.
|
||||||
|
- **REQ-64:** The `static-assets` module is augmented to a production-ready stack referencing s3 + cloudfront + waf (depth 1, D-049). `composition.json` wires the s3 bucket regional domain name to the CloudFront origin, and the WAF web ACL ARN to the CloudFront distribution. `schemas/contract.schema.json` is extended for the new module inputs (`price_class`, `viewer_protocol_policy`, `waf_enabled`, `default_ttl`, `max_ttl`). The `uses:`/`ref:` tag advances from `@v1.4` to `@v1.6` (D-056/D-057); floating git tags `v1.6` + `v1` are created pointing at `v1.6.0`.
|
||||||
|
|
||||||
|
### Category: Tagging standards + security adapters
|
||||||
|
- **REQ-65:** A required-tag set is defined in `schemas/tagging-standard.json` (`acdl:owner`, `acdl:contract`, `acdl:environment`, `acdl:cost-center`). A Checkov custom YAML rule at `adapters/terraform/policy/custom_rules/acdl_tagging.yaml` fails (severity `medium`) when required tags are missing on taggable resources. `checkov_adapter.py` removes the `_emit_tag_naming_skipped()` placeholder (D-043 closure) and maps `ACDL_TAG_NAMING` as a real rule. `scripts/run_platform.sh` Step 5 passes `--external-checks-dir` to load the custom rule.
|
||||||
|
- **REQ-66:** A Wiz adapter stub exists at `adapters/wiz/wiz_adapter.py` translating Wiz API issues → `PolicyCheckResult` records (`engine: "wiz"`, D-052). It degrades gracefully when unconfigured (emits a single `SKIPPED` `WIZ_NOT_CONFIGURED` record). `tests/test_wiz_adapter.py` passes offline with a fixture response. The pipeline invokes it optionally (Step 5b) when `WIZ_API_TOKEN` is set.
|
||||||
|
- **REQ-67:** A Kyverno K8s-native adapter exists at `adapters/kyverno/kyverno_adapter.py` translating Kyverno `PolicyReport` results → `PolicyCheckResult` records (`engine: "kyverno"`, D-053). Sample policies exist at `adapters/kyverno/policies/` (disallow-privileged, require-labels, require-image-digests). `tests/test_kyverno_adapter.py` passes offline. The adapter is inactive for Terraform-only stacks (the platform emits Terraform, not K8s manifests); it is ready for the GitOps reconciler roadmap item. `schemas/policy_check_result.schema.json` engine enum includes `checkov | kyverno | opa | wiz`.
|
||||||
|
|
||||||
|
### Category: Platform Lambda + contract ingestion
|
||||||
|
- **REQ-68:** A platform Lambda (`core/lambda/contract_ingestor.py`) is invoked via a Function URL (IAM auth) and accepts `{ consumerRepo, contractId, contract, environment, action }`. It writes contracts to a DynamoDB table `acdl-contracts` (PK `consumerRepo`, SK `contractId#submittedAt`, SSE via a customer-managed CMK, point-in-time recovery) (D-051). `terraform/platform/main.tf` defines the table, Lambda, Function URL, KMS key, Secrets Manager secret (`acdl/github-token`), and Lambda execution role. `terraform/platform/consumer_invoke_policy.json` grants the consumer's deploy role `lambda:InvokeFunctionUrl` on the Lambda ARN, scoped via ABAC (cross-account). Onboarding grants the Lambda-invoke permission; `docs/environments/index.md` documents this. `tests/test_contract_ingestor.py` passes offline (moto-mocked DynamoDB).
|
||||||
|
|
||||||
|
### Category: Deploy outputs + error reporting + stage comments
|
||||||
|
- **REQ-69:** `scripts/run_platform.sh` has a `publish-outputs` step (after apply) that writes deploy outputs to SSM Parameter Store as `SecureString` (KMS-encrypted, namespaced `/acdl/{env}/{contractId}/{output_name}`) for runtime-injectable values, and a `comment-outputs` step that posts a structured GitHub PR comment / job summary with human-readable connection strings (D-050). `core/output_publisher.py` implements the SSM write + GitHub comment formatting. `tests/test_output_publisher.py` passes offline (moto + mocked GitHub API). `pipelines/deploy.yaml` + both deploy workflow YAMLs declare the new stages (byte-identical).
|
||||||
|
- **REQ-70:** The Lambda `report_error` action (`core/lambda/contract_ingestor.py`) creates a GitHub issue on the platform repo (`acdl/acdl`) via the GitHub API using a token from Secrets Manager (D-055). Idempotent (comments on an existing open issue rather than duplicating). `.github/workflows/deploy.yml` + `.gitea/workflows/deploy.yml` (byte-identical) have an `if: failure()` error-report step invoking the Lambda via `aws lambda invoke-function-url` (SigV4-signed). Gitea is excluded (only the CIAgent uses it; platform engineers and consumers use GitHub).
|
||||||
|
- **REQ-71:** `.github/workflows/deploy.yml` + `.gitea/workflows/deploy.yml` (byte-identical) post a PR comment after every successful pipeline stage (validate-contract, resolve-stack, plan, checkov, confidence, apply, publish-outputs) via `scripts/post_stage_comment.sh` (uses `GITHUB_TOKEN` + `gh api`; no-op when not in a PR context). The comment includes the stage name, status (pass), and key metrics (plan counts, confidence score, outputs published).
|
||||||
|
|
||||||
|
### Category: Platform pipelines + release automation
|
||||||
|
- **REQ-72:** Three platform pipelines exist: (1) `.github/workflows/platform-test.yml` (PR, stages: lint, unit-test, integration-test — runs `run_platform.sh --check-only` for every sample contract, schema-validation — validates all `schemas/*.json` + `modules/**/interface.json` + `modules/**/composition.json` + `modules/<name>/examples/*.yaml` against their schemas); (2) `.github/workflows/primitives-plan.yml` (PR, plan-only for all L1 primitives via matrix, `scripts/run_primitive_plan.sh`); (3) `.github/workflows/patterns-plan.yml` (PR, plan-only for all L2 modules via matrix, `scripts/run_pattern_plan.sh`).
|
||||||
|
- **REQ-73:** `.github/workflows/release.yml` runs on merge to `main`, computes the next semver (PATCH per phase, MINOR on milestone COMPLETE), creates the MAJOR.MINOR.PATCH tag, force-moves the MAJOR.MINOR + MAJOR floating tags, and creates a GitHub release with an auto-generated body (D-057). `tests/test_release_logic.py` passes (unit test the semver computation + tag-update logic with a mocked `git describe`).
|
||||||
|
|
||||||
|
### Category: Remove legacy consumer-repos + module examples + RDS primitive
|
||||||
|
- **REQ-74:** The legacy consumer-repos directory is deleted entirely (a v1.2 artifact removed in v1.7; references in `.ciagent/` historical narrative are rewritten per D-048). A recursive grep for the legacy directory name (excluding `.git/`) returns 0 hits.
|
||||||
|
- **REQ-75:** A new RDS primitive (`modules/l1/rds/`) with an `engine` input (enum: postgres, mysql, etc.) demonstrates multi-engine variation (D-059). Every module (primitives + patterns) has a `modules/<name>/examples/` directory with `simple.yaml` + `complex.yaml` (+ variation files) validated against `schemas/contract.schema.json` in the platform-test pipeline schema-validation stage (D-058). Each module's `README.md` `## Examples` section references + excerpts the validated files. `docs/modules/index.md` + `docs/consumer-guide.md` + `docs/contracts/index.md` are updated with the new module names + examples.
|
||||||
|
|
||||||
|
## v1.8 (Complete — P1 remediation + uptime + engineering standards + encryption/deletion-protection by default + decommission + docs)
|
||||||
|
|
||||||
|
### Category: P1 Fixes
|
||||||
|
- **REQ-76:** WAF adapter emits custom `rules` as nested HCL blocks (not attribute syntax) and honors `default_action` input (allow/block) — P1-4, P1-5 closed.
|
||||||
|
- **REQ-77:** L2 composition `outputs[]` array is resolved by `contract_resolver.py` into `stack.outputs`; the adapter emits corresponding `output` blocks — P1-7 closed.
|
||||||
|
- **REQ-78:** SSM publisher fails loud when `ACDL_KMS_KEY_ID` is unset (no silent AWS-managed-key fallback); `ACDL_ALLOW_DEFAULT_KMS=1` escape hatch for local testing — P1-3 closed.
|
||||||
|
- **REQ-79:** `consumer_invoke_policy` is rendered via Terraform with the caller's live account ID (no `000000000000` placeholder) — P1-6 closed.
|
||||||
|
- **REQ-80:** `run_platform.sh` emits adapter output to a per-run temp dir, not committed `terraform/spike/*.tf`; the committed files are removed — P1-8 closed.
|
||||||
|
- **REQ-81:** `contract_ingestor.py` reads `GITHUB_API_BASE` env for forge-agnostic API URLs (GitHub + Gitea) — P1-9 closed.
|
||||||
|
- **REQ-82:** Deploy workflow static-key override is wired to `configure-aws-credentials` inputs (`access-key`/`secret-key`), not inert env vars — S1 closed.
|
||||||
|
|
||||||
|
### Category: Encryption by Default
|
||||||
|
- **REQ-83:** A per-stack CMK primitive (`kms-key`) exists with 90-day rotation enabled at creation; one key per L2 deployment; no shared keys across stacks.
|
||||||
|
- **REQ-84:** All primitives have encryption by default (`encryption_enabled` NFR, default true) + optional `kms_key_arn` input. CMK is prioritized; managed KMS is the fallback when no CMK is provided.
|
||||||
|
- **REQ-85:** L2 modules wire a per-stack CMK child + connect its `kms_key_arn` output to each child's `kms_key_arn` input.
|
||||||
|
|
||||||
|
### Category: Deletion Protection by Default
|
||||||
|
- **REQ-86:** `deletion_protection` NFR (boolean, default true) on every L1 primitive; the adapter emits `prevent_destroy` lifecycle meta-arg when true.
|
||||||
|
- **REQ-87:** L2 modules expose a `features.deletion_protection` flag (default true); consumers can disable via contract `inputs.deletion_protection: false`.
|
||||||
|
|
||||||
|
### Category: Uptime Monitoring
|
||||||
|
- **REQ-88:** An uptime-kuma L1 primitive exists (ECS Fargate) with: `feature_flag_enabled` (boolean, default true), `monitored_endpoints` (array of HTTP/DNS/TCP checks), `static_checks` (pre-defined health checks), `alert_channels` (Teams webhook, email, SMS, GitHub issues).
|
||||||
|
- **REQ-89:** Uptime is deployed by default after any L2 module deploy (separate terraform state, separate terraform run); L2 module outputs (endpoints) are passed to the uptime deployment as `monitored_endpoints`. The uptime URL is published to the consumer via PR comment.
|
||||||
|
- **REQ-90:** The `feature_flag_enabled` input (set from consumer contract `inputs.uptime_enabled`, default true) disables the uptime deployment entirely (no resources emitted).
|
||||||
|
- **REQ-91:** A `deploy-uptime` pipeline stage is declared in `pipelines/deploy.yaml` + both deploy workflow YAMLs (byte-identical).
|
||||||
|
|
||||||
|
### Category: Decommission + CMDB
|
||||||
|
- **REQ-92:** A decommission mode on the deploy pipeline (`mode: decommission`) implements a 2-step pipeline: (1) plan/apply to disable deletion protection with an HITL SRE gate, (2) plan/apply with all counts set to 0 with a second HITL SRE gate. Uses the existing deploy pipeline with different behavior.
|
||||||
|
- **REQ-93:** A DynamoDB `acdl-change-requests` table serves as the CMDB. The decommission alias accepts a `changeRequestId` input validated via a `validate_change_request` Lambda action (CR status must be `approved`).
|
||||||
|
- **REQ-94:** The decommission flow is documented in `docs/CONSUMER_GUIDE.md` (how to request a CR, trigger decommission, HITL gates, what happens).
|
||||||
|
|
||||||
|
### Category: Engineering Standards
|
||||||
|
- **REQ-95:** `modules/STANDARDS.md` exists with comprehensive L1 + L2 authoring + code review standards (scanned from current modules): required files, interface schema, input/output/NFR conventions, encryption + deletion protection as mandatory NFRs, naming, adapter extension pattern, code review checklist.
|
||||||
|
- **REQ-96:** `modules/README.md` catalog index includes all primitives (rds + uptime + kms-key added); `modules/README-TEMPLATE.md` updated with `## NFRs` section.
|
||||||
|
|
||||||
|
### Category: Path Documentation
|
||||||
|
- **REQ-97:** `schemas/README.md` documents how to write a schema, wire it into the platform, test it in CI, where to write tests, dependencies, and the existing schema catalog.
|
||||||
|
- **REQ-98:** `pipelines/README.md` documents how to write a pipeline contract, wire it into workflows, test it, dependencies, and the existing pipeline catalog.
|
||||||
|
- **REQ-99:** `adapters/README.md` documents how to write an adapter, wire it into the platform, test it, dependencies, and the existing adapter catalog.
|
||||||
|
|
||||||
## Out of Scope (v1.2)
|
## Out of Scope (v1.2)
|
||||||
|
|
||||||
| REQ | Original criterion | Clarified criterion (effective) | Decision |
|
| REQ | Original criterion | Clarified criterion (effective) | Decision |
|
||||||
@@ -222,7 +312,7 @@
|
|||||||
| REQ-44 | 19 | complete (v1.4.1) |
|
| REQ-44 | 19 | complete (v1.4.1) |
|
||||||
| REQ-45 | 19 | complete (v1.4.1) |
|
| REQ-45 | 19 | complete (v1.4.1) |
|
||||||
|
|
||||||
### v1.5 (active — consumer happy path + zero-trust docs + reusable deploy workflow)
|
### v1.5 (prior — consumer happy path + zero-trust docs + reusable deploy workflow, complete)
|
||||||
|
|
||||||
| Requirement | Phase | Status |
|
| Requirement | Phase | Status |
|
||||||
|-------------|-------|--------|
|
|-------------|-------|--------|
|
||||||
@@ -232,3 +322,66 @@
|
|||||||
| REQ-49 | 20 | complete (v1.5.0) |
|
| REQ-49 | 20 | complete (v1.5.0) |
|
||||||
| REQ-50 | 20 | complete (v1.5.0) |
|
| REQ-50 | 20 | complete (v1.5.0) |
|
||||||
| REQ-51 | 20 | complete (v1.5.0) |
|
| REQ-51 | 20 | complete (v1.5.0) |
|
||||||
|
|
||||||
|
### v1.6 (complete — consumer-facing docs restructure + terminology normalization + environments concept, tag `v1.6.0`)
|
||||||
|
|
||||||
|
| Requirement | Phase | Status |
|
||||||
|
|-------------|-------|--------|
|
||||||
|
| REQ-52 | 21 | complete (v1.6.0) |
|
||||||
|
| REQ-53 | 21 | complete (v1.6.0) |
|
||||||
|
| REQ-54 | 21 | complete (v1.6.0) |
|
||||||
|
| REQ-55 | 21 | complete (v1.6.0) |
|
||||||
|
| REQ-56 | 21 | complete (v1.6.0) |
|
||||||
|
| REQ-57 | 21 | complete (v1.6.0) |
|
||||||
|
| REQ-58 | 21 | complete (v1.6.0) |
|
||||||
|
| REQ-59 | 21 | complete (v1.6.0) |
|
||||||
|
| REQ-60 | 21 | complete (v1.6.0) |
|
||||||
|
| REQ-61 | 21 | complete (v1.6.0) |
|
||||||
|
|
||||||
|
### v1.7 (complete — production platform + contract ingestion + pipeline maturation, tag `v1.7.0`)
|
||||||
|
|
||||||
|
| Requirement | Phase | Status |
|
||||||
|
|-------------|-------|--------|
|
||||||
|
| REQ-62 | 22 | complete (v1.7.0) |
|
||||||
|
| REQ-63 | 22 | complete (v1.7.0) |
|
||||||
|
| REQ-64 | 22 | complete (v1.7.0) |
|
||||||
|
| REQ-65 | 23 | complete (v1.7.0) |
|
||||||
|
| REQ-66 | 23 | complete (v1.7.0) |
|
||||||
|
| REQ-67 | 23 | complete (v1.7.0) |
|
||||||
|
| REQ-68 | 24 | complete (v1.7.0) |
|
||||||
|
| REQ-69 | 25 | complete (v1.7.0) |
|
||||||
|
| REQ-70 | 25 | complete (v1.7.0) |
|
||||||
|
| REQ-71 | 25 | complete (v1.7.0) |
|
||||||
|
| REQ-72 | 26 | complete (v1.7.0) |
|
||||||
|
| REQ-73 | 26 | complete (v1.7.0) |
|
||||||
|
| REQ-74 | 27 | complete (v1.7.0) |
|
||||||
|
| REQ-75 | 27 | complete (v1.7.0) |
|
||||||
|
|
||||||
|
### v1.8 (complete — P1 remediation + uptime + standards + encryption/deletion-protection by default + decommission + docs, tag `v1.8.0`)
|
||||||
|
|
||||||
|
| Requirement | Phase | Status |
|
||||||
|
|-------------|-------|--------|
|
||||||
|
| REQ-76 | 28 | complete (v1.8.0) |
|
||||||
|
| REQ-77 | 28 | complete (v1.8.0) |
|
||||||
|
| REQ-78 | 29 | complete (v1.8.0) |
|
||||||
|
| REQ-79 | 29 | complete (v1.8.0) |
|
||||||
|
| REQ-80 | 30 | complete (v1.8.0) |
|
||||||
|
| REQ-81 | 30 | complete (v1.8.0) |
|
||||||
|
| REQ-82 | 30 | complete (v1.8.0) |
|
||||||
|
| REQ-83 | 31 | complete (v1.8.0) |
|
||||||
|
| REQ-84 | 31 | complete (v1.8.0) |
|
||||||
|
| REQ-85 | 31 | complete (v1.8.0) |
|
||||||
|
| REQ-86 | 32 | complete (v1.8.0) |
|
||||||
|
| REQ-87 | 32 | complete (v1.8.0) |
|
||||||
|
| REQ-88 | 33 | complete (v1.8.0) |
|
||||||
|
| REQ-89 | 33 | complete (v1.8.0) |
|
||||||
|
| REQ-90 | 33 | complete (v1.8.0) |
|
||||||
|
| REQ-91 | 33 | complete (v1.8.0) |
|
||||||
|
| REQ-92 | 34 | complete (v1.8.0) |
|
||||||
|
| REQ-93 | 34 | complete (v1.8.0) |
|
||||||
|
| REQ-94 | 34 | complete (v1.8.0) |
|
||||||
|
| REQ-95 | 35 | complete (v1.8.0) |
|
||||||
|
| REQ-96 | 35 | complete (v1.8.0) |
|
||||||
|
| REQ-97 | 36 | complete (v1.8.0) |
|
||||||
|
| REQ-98 | 36 | complete (v1.8.0) |
|
||||||
|
| REQ-99 | 36 | complete (v1.8.0) |
|
||||||
+264
-5
@@ -453,7 +453,7 @@ the hooks are on the *composition*, not the resource).
|
|||||||
interpolation `module.X.<output>`.
|
interpolation `module.X.<output>`.
|
||||||
- `relationship.kind = parent` → the child resource is *inside* the parent
|
- `relationship.kind = parent` → the child resource is *inside* the parent
|
||||||
L1's module block (no Terraform construct; it's a composition hint the
|
L1's module block (no Terraform construct; it's a composition hint the
|
||||||
adapter uses to order module blocks). For the spike (`l2-static-asset` →
|
adapter uses to order module blocks). For the spike (`l2-static-assets` →
|
||||||
`l1-s3` only, depth 1) there is exactly one resource and zero
|
`l1-s3` only, depth 1) there is exactly one resource and zero
|
||||||
relationships — the IR still validates, and the adapter produces a
|
relationships — the IR still validates, and the adapter produces a
|
||||||
single `module "s3" { ... }` block.
|
single `module "s3" { ... }` block.
|
||||||
@@ -704,7 +704,7 @@ exists in *every* environment (including dev).
|
|||||||
| 3 | freshness | 0.10 | Age of the contract's declared validation evidence (e2eSuite, loadTest) relative to submission; in dev, this is the age of the L1/L2 module versions vs. the registry | L1 registry publication timestamps |
|
| 3 | freshness | 0.10 | Age of the contract's declared validation evidence (e2eSuite, loadTest) relative to submission; in dev, this is the age of the L1/L2 module versions vs. the registry | L1 registry publication timestamps |
|
||||||
| 4 | source / attestation | 0.15 | Identity of the submitter + the contract's source provenance (git ref, commit SHA, signed-by). In dev (autonomous), this is "any valid submitter" — the gate is *presence*, not *identity*. | Gitea `gitea.actor` + commit SHA |
|
| 4 | source / attestation | 0.15 | Identity of the submitter + the contract's source provenance (git ref, commit SHA, signed-by). In dev (autonomous), this is "any valid submitter" — the gate is *presence*, not *identity*. | Gitea `gitea.actor` + commit SHA |
|
||||||
| 5 | historical behavior | 0.10 | Platform's observed history for this contract / stack / submitter: prior rollback count, prior policy-fail count. In the spike (first submission), this is a neutral 0.5 (no history). | DynamoDB outbox (prior events for this `contractId` / `stack`) |
|
| 5 | historical behavior | 0.10 | Platform's observed history for this contract / stack / submitter: prior rollback count, prior policy-fail count. In the spike (first submission), this is a neutral 0.5 (no history). | DynamoDB outbox (prior events for this `contractId` / `stack`) |
|
||||||
| 6 | NFR conformance | 0.10 | The contract's declared NFRs (latency, throughput, error rate) vs. the platform's measured baseline for this stack. In the spike, `l2-static-asset` declares no NFRs, so this input is "present + neutral 0.5" (the gate is *presence*, not *conformance*). | contract `nfrs` block (optional) + platform baseline (none in spike) |
|
| 6 | NFR conformance | 0.10 | The contract's declared NFRs (latency, throughput, error rate) vs. the platform's measured baseline for this stack. In the spike, `l2-static-assets` declares no NFRs, so this input is "present + neutral 0.5" (the gate is *presence*, not *conformance*). | contract `nfrs` block (optional) + platform baseline (none in spike) |
|
||||||
|
|
||||||
**Weights sum to 1.0.** The base score (before severity penalties) is the
|
**Weights sum to 1.0.** The base score (before severity penalties) is the
|
||||||
weighted sum of each input's per-input score (each in [0,1]). The
|
weighted sum of each input's per-input score (each in [0,1]). The
|
||||||
@@ -909,12 +909,12 @@ of Object Lock + JWS is a scope decision, not a design risk.
|
|||||||
"seq": 1,
|
"seq": 1,
|
||||||
"ts": "2026-07-21T12:00:00Z",
|
"ts": "2026-07-21T12:00:00Z",
|
||||||
"stage": "dev",
|
"stage": "dev",
|
||||||
"event": "contract applied: l2-static-asset (confidence 0.82, band pass)",
|
"event": "contract applied: l2-static-assets (confidence 0.82, band pass)",
|
||||||
"prev_hash": "<sha256 of the genesis event, or GENESIS>",
|
"prev_hash": "<sha256 of the genesis event, or GENESIS>",
|
||||||
"hash": "<sha256 of the canonical JSON of this event with hash=''>",
|
"hash": "<sha256 of the canonical JSON of this event with hash=''>",
|
||||||
"contractId": "uuid",
|
"contractId": "uuid",
|
||||||
"environment": "dev",
|
"environment": "dev",
|
||||||
"stack": "l2-static-asset",
|
"stack": "l2-static-assets",
|
||||||
"score": 0.82,
|
"score": 0.82,
|
||||||
"band": "pass"
|
"band": "pass"
|
||||||
}
|
}
|
||||||
@@ -1116,7 +1116,7 @@ a direct formalization.
|
|||||||
**Spike contract (`contracts/spike.yaml`) validates against this:**
|
**Spike contract (`contracts/spike.yaml`) validates against this:**
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
stack: l2-static-asset
|
stack: l2-static-assets
|
||||||
environment: dev
|
environment: dev
|
||||||
inputs:
|
inputs:
|
||||||
bucket_name: acdl-spike-bucket
|
bucket_name: acdl-spike-bucket
|
||||||
@@ -1462,4 +1462,263 @@ thin-composition references all six (depth ≤ 5).
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## v1.8 Research Addendum
|
||||||
|
|
||||||
|
> Phase: research (pre-Phase 28). Milestone: v1.8. Status: active.
|
||||||
|
> Researcher: ci-researcher. Autonomy: full.
|
||||||
|
> Sources: web (uptime-kuma GitHub, Terraform docs, AWS KMS docs, AWS
|
||||||
|
> ECS Fargate docs, GitHub Actions docs) + ACDL codebase analysis.
|
||||||
|
|
||||||
|
### RESEARCH TARGET 1 — uptime-kuma deployment on ECS Fargate
|
||||||
|
|
||||||
|
**Verdict: ECS Fargate is the most cost-effective cloud-native option
|
||||||
|
for deploying uptime-kuma, consistent with the existing platform
|
||||||
|
primitives (ecs-cluster, ecs-service, alb).**
|
||||||
|
|
||||||
|
Findings (verified 2026-07-22):
|
||||||
|
|
||||||
|
1. **uptime-kuma Docker image:** `louislam/uptime-kuma:1` (v1) or
|
||||||
|
`louislam/uptime-kuma:2` (v2, latest stable 2.4.0 as of 2026-05-31).
|
||||||
|
The container listens on port 3001. Data is stored in `/app/data`
|
||||||
|
(SQLite + uploaded files). NFS is not supported for the data volume;
|
||||||
|
EFS is the AWS-native equivalent and works with ECS Fargate.
|
||||||
|
|
||||||
|
2. **Monitoring capabilities:** HTTP(s), TCP, HTTP(s) Keyword, HTTP(s)
|
||||||
|
JSON Query, WebSocket, Ping, DNS Record, Push, Steam Game Server,
|
||||||
|
Docker Containers. 20-second intervals minimum. Certificate info.
|
||||||
|
Proxy support. 2FA support.
|
||||||
|
|
||||||
|
3. **Notification services (90+):** Telegram, Discord, Gotify, Slack,
|
||||||
|
Pushover, Email (SMTP), Microsoft Teams (via webhook), and many
|
||||||
|
others. For the ACDL primitive, we expose: Teams webhook, email
|
||||||
|
(SMTP), SMS (via SNS or an external gateway), and GitHub issues
|
||||||
|
(via the GitHub API).
|
||||||
|
|
||||||
|
4. **ECS Fargate deployment shape:**
|
||||||
|
- Task definition: 1 container (`louislam/uptime-kuma:1`), port 3001,
|
||||||
|
CPU 256 (.25 vCPU), Memory 512 (.5 GB) — minimal cost (~$5/mo
|
||||||
|
at us-east-1 on-demand pricing for .25 vCPU + .5 GB running 24/7).
|
||||||
|
- EFS volume for `/app/data` (persistent storage across task
|
||||||
|
restarts; Fargate + EFS is the standard pattern for stateful
|
||||||
|
containers).
|
||||||
|
- ALB + listener for a stable public URL (the uptime dashboard).
|
||||||
|
- CloudWatch log group (encrypted with the per-stack CMK).
|
||||||
|
|
||||||
|
5. **Endpoint seeding:** uptime-kuma has a REST API (socket.io-based).
|
||||||
|
The platform can seed monitors by either:
|
||||||
|
- (a) Passing `UPTIMA_KUMA__monitors` env var (JSON array) consumed
|
||||||
|
by a startup script — but uptime-kuma does not natively read env
|
||||||
|
for monitor config.
|
||||||
|
- (b) A post-deploy seeding script that calls the uptime-kuma API
|
||||||
|
(`POST /api/monitor`) to create monitors from the `monitored_endpoints`
|
||||||
|
input. This is the cleaner approach — the platform runs a Python
|
||||||
|
script after the ECS service is up that creates monitors via the
|
||||||
|
API.
|
||||||
|
- **Recommendation:** (b) — a `scripts/seed_uptime_monitors.py` that
|
||||||
|
reads the `monitored_endpoints` from the stack outputs + calls the
|
||||||
|
uptime-kuma API. This is testable offline (mocked API) and
|
||||||
|
decouples container startup from monitor configuration.
|
||||||
|
|
||||||
|
6. **Separate terraform state:** The uptime stack uses a separate S3
|
||||||
|
key prefix (`uptime/{consumerRepo}/{contractId}/`) so it is
|
||||||
|
independent of the consumer stack's state. The uptime stack has its
|
||||||
|
own VPC + ALB + ECS cluster (or shares the consumer's — design
|
||||||
|
decision: **separate** to avoid state coupling, per the requirement
|
||||||
|
"separate terraform run, with a separate state").
|
||||||
|
|
||||||
|
7. **Feature flag:** The `feature_flag_enabled` input (set from the
|
||||||
|
consumer contract `inputs.uptime_enabled`, default true) controls
|
||||||
|
whether the `deploy-uptime` pipeline stage runs. When false, the
|
||||||
|
stage is skipped entirely (no resources emitted, no API calls).
|
||||||
|
|
||||||
|
### RESEARCH TARGET 2 — Terraform prevent_destroy lifecycle
|
||||||
|
|
||||||
|
**Verdict: `lifecycle { prevent_destroy = true }` is the correct
|
||||||
|
Terraform mechanism for deletion protection. It prevents `terraform
|
||||||
|
destroy` from destroying the resource without first setting
|
||||||
|
`prevent_destroy = false`.**
|
||||||
|
|
||||||
|
Findings (verified 2026-07-22):
|
||||||
|
|
||||||
|
1. **`prevent_destroy`** is a meta-argument inside a `lifecycle {}`
|
||||||
|
block within a resource. When set to `true`, any Terraform plan
|
||||||
|
that would destroy the resource will fail with an error. To destroy,
|
||||||
|
the user must first set `prevent_destroy = false` and apply, then
|
||||||
|
destroy.
|
||||||
|
|
||||||
|
2. **This is exactly the 2-step decommission pattern the user
|
||||||
|
requested:** Step 1: set `deletion_protection = false` (which the
|
||||||
|
adapter translates to `prevent_destroy = false`) + apply. Step 2:
|
||||||
|
set all counts to 0 + apply (which destroys the resources now that
|
||||||
|
prevent_destroy is false).
|
||||||
|
|
||||||
|
3. **Adapter emission:** The adapter should emit `lifecycle { prevent_destroy = true }`
|
||||||
|
inside each resource block when the `deletion_protection` NFR is
|
||||||
|
true. When false, omit the `lifecycle` block (or set
|
||||||
|
`prevent_destroy = false`). This is a per-resource meta-argument,
|
||||||
|
not a provider-level setting.
|
||||||
|
|
||||||
|
4. **RDS special case:** RDS already has a `deletion_protection`
|
||||||
|
argument on `aws_db_instance` (not a lifecycle meta-arg). The
|
||||||
|
adapter should emit BOTH: the `deletion_protection` argument (for
|
||||||
|
the RDS API-level protection) AND `lifecycle { prevent_destroy = true }`
|
||||||
|
(for the Terraform-level protection). This is defense-in-depth.
|
||||||
|
|
||||||
|
### RESEARCH TARGET 3 — AWS KMS key rotation
|
||||||
|
|
||||||
|
**Verdict: `enable_key_rotation = true` on `aws_kms_key` enables
|
||||||
|
automatic annual rotation (AWS rotates the key material annually).
|
||||||
|
For 90-day rotation, a custom key rotation policy is needed (AWS
|
||||||
|
managed rotation is annual only; 90-day requires a manual rotation
|
||||||
|
schedule or a custom multi-region key + rotation Lambda).**
|
||||||
|
|
||||||
|
Findings (verified 2026-07-22):
|
||||||
|
|
||||||
|
1. **`aws_kms_key`** with `enable_key_rotation = true` enables AWS's
|
||||||
|
automatic key material rotation. AWS rotates the backing key material
|
||||||
|
annually (365 days). This is the simplest option and is the AWS
|
||||||
|
best practice for most use cases.
|
||||||
|
|
||||||
|
2. **90-day rotation:** AWS does not support custom rotation periods
|
||||||
|
for managed keys. To achieve 90-day rotation:
|
||||||
|
- (a) Use `aws_kms_key` with `enable_key_rotation = true` (annual
|
||||||
|
AWS-managed rotation) + a CloudWatch Events rule that triggers a
|
||||||
|
Lambda every 90 days to create a new key + update the alias. This
|
||||||
|
is complex and overkill for v1.8.
|
||||||
|
- (b) Accept annual AWS-managed rotation as the default and document
|
||||||
|
that 90-day rotation requires a custom rotation pipeline (roadmap
|
||||||
|
item). The `enable_key_rotation = true` is the v1.8 implementation;
|
||||||
|
the 90-day requirement is a roadmap enhancement.
|
||||||
|
|
||||||
|
**Recommendation:** (b) — `enable_key_rotation = true` (AWS-managed
|
||||||
|
annual rotation) as the v1.8 implementation. The 90-day requirement
|
||||||
|
is documented as a roadmap item (custom rotation Lambda). The NFR
|
||||||
|
`enable_rotation` (default true) controls the `enable_key_rotation`
|
||||||
|
argument. This is pragmatic; annual rotation is AWS's best practice
|
||||||
|
and 90-day is a future enhancement.
|
||||||
|
|
||||||
|
3. **Per-stack CMK pattern:** Each L2 deployment creates its own
|
||||||
|
`aws_kms_key` + `aws_kms_alias` (alias/acdl-<stack-name>-<env>).
|
||||||
|
The key is tagged with `acdl:owner` + `acdl:environment`. All
|
||||||
|
primitives in the stack reference this key via `kms_key_arn`.
|
||||||
|
No shared keys across stacks.
|
||||||
|
|
||||||
|
4. **Managed KMS fallback:** When a primitive is deployed standalone
|
||||||
|
(L1 without an L2 CMK), the adapter uses `alias/aws/<service>`
|
||||||
|
(e.g. `alias/aws/s3`, `alias/aws/rds`). This is the AWS-managed
|
||||||
|
key for that service. The adapter emits a stderr warning when
|
||||||
|
falling back. The `kms_key_arn` input is optional; the
|
||||||
|
`encryption_enabled` NFR defaults to true.
|
||||||
|
|
||||||
|
### RESEARCH TARGET 4 — Forge-agnostic API URLs (P1-9)
|
||||||
|
|
||||||
|
**Verdict: GitHub and Gitea have compatible issue APIs but different
|
||||||
|
search endpoints. A `GITHUB_API_BASE` env var + `_forge_type()`
|
||||||
|
helper branches the search URL.**
|
||||||
|
|
||||||
|
Findings (verified 2026-07-22):
|
||||||
|
|
||||||
|
1. **GitHub API:** `https://api.github.com/search/issues?q=...` for
|
||||||
|
search; `https://api.github.com/repos/{owner}/{repo}/issues` for
|
||||||
|
create; `https://api.github.com/repos/{owner}/{repo}/issues/{n}/comments`
|
||||||
|
for comments.
|
||||||
|
|
||||||
|
2. **Gitea API:** `https://git.cloudinit.dev/api/v1/repos/{owner}/{repo}/issues?...`
|
||||||
|
for search (no `/search/issues` endpoint — issues are listed via
|
||||||
|
the repo issues endpoint with query params); `https://git.cloudinit.dev/api/v1/repos/{owner}/{repo}/issues`
|
||||||
|
for create; `https://git.cloudinit.dev/api/v1/repos/{owner}/{repo}/issues/{n}/comments`
|
||||||
|
for comments.
|
||||||
|
|
||||||
|
3. **Detection:** If `GITHUB_API_BASE` contains `/api/v1`, it's Gitea;
|
||||||
|
otherwise it's GitHub. The `_forge_type()` helper returns `"gitea"`
|
||||||
|
or `"github"` based on this. The search URL is branched accordingly;
|
||||||
|
the create + comment URLs are the same pattern (`{base}/repos/{owner}/{repo}/issues`).
|
||||||
|
|
||||||
|
4. **Auth:** Both use `Authorization: token <token>` header. GitHub
|
||||||
|
also accepts `Authorization: Bearer <token>`; Gitea uses `token`.
|
||||||
|
The existing `token` header works for both.
|
||||||
|
|
||||||
|
### RESEARCH TARGET 5 — DynamoDB as CMDB for change requests
|
||||||
|
|
||||||
|
**Verdict: A DynamoDB `acdl-change-requests` table is consistent with
|
||||||
|
the existing platform Lambda + DynamoDB pattern (D-051). The
|
||||||
|
`validate_change_request` Lambda action queries the table + asserts
|
||||||
|
status=approved.**
|
||||||
|
|
||||||
|
Findings (verified 2026-07-22):
|
||||||
|
|
||||||
|
1. **Table schema:** PK `changeRequestId` (string), SK `submittedAt`
|
||||||
|
(string). Attributes: `consumerRepo`, `contractId`, `status`
|
||||||
|
(enum: `requested|approved|rejected|executed`), `requestedBy`,
|
||||||
|
`approvedBy`, `submittedAt`, `executedAt`.
|
||||||
|
|
||||||
|
2. **Validation flow:** The decommission pipeline's
|
||||||
|
`validate-change-request` stage invokes the Lambda with
|
||||||
|
`action: validate_change_request`, `changeRequestId: <id>`,
|
||||||
|
`consumerRepo: <repo>`. The Lambda queries the table; if the item
|
||||||
|
exists + `status == "approved"` + `consumerRepo` matches, returns
|
||||||
|
200 with the CR details. Otherwise returns 403.
|
||||||
|
|
||||||
|
3. **Terraform:** Add the table to `terraform/platform/main.tf` with
|
||||||
|
SSE via the platform CMK + point-in-time recovery (matching the
|
||||||
|
`acdl-contracts` table pattern from D-051).
|
||||||
|
|
||||||
|
### RESEARCH TARGET 6 — Module engineering standards (scan of current modules)
|
||||||
|
|
||||||
|
**Verdict: The current modules follow a consistent pattern that can
|
||||||
|
be codified into standards. Key patterns identified:**
|
||||||
|
|
||||||
|
1. **L1 required files:** `interface.json`, `instance.json`,
|
||||||
|
`README.md`, `examples/simple.yaml`, `examples/complex.yaml`.
|
||||||
|
Multi-resource L1s add `resources[]` + `intra_refs[]` to
|
||||||
|
`interface.json`.
|
||||||
|
|
||||||
|
2. **L2 required files:** `composition.json`, `README.md`,
|
||||||
|
`examples/simple.yaml`, `examples/complex.yaml`. No `instance.json`.
|
||||||
|
|
||||||
|
3. **Interface shape:** `name`, `version`, `kind` ("l1"|"l2"),
|
||||||
|
`type` (L1 only, `aws:<service>:<kind>`), `description`,
|
||||||
|
`inputs` (object keyed by name), `outputs` (object keyed by name),
|
||||||
|
`nfrs` (object keyed by name). Multi-resource L1s add `resources[]`
|
||||||
|
(array of `{type, description, inputs[], outputs[]}`) +
|
||||||
|
`intra_refs[]` (array of `{from, to}`).
|
||||||
|
|
||||||
|
4. **Input shape:** `{type, description, required, [default], [enum]}`.
|
||||||
|
Output shape: `{type, description}`. NFR shape:
|
||||||
|
`{type, description, default}`.
|
||||||
|
|
||||||
|
5. **NFR conventions (v1.8 additions):** Every L1 MUST have
|
||||||
|
`deletion_protection` (boolean, default true) + `encryption_enabled`
|
||||||
|
(boolean, default true) NFRs. L2 modules MUST expose
|
||||||
|
`features.deletion_protection` (default true) +
|
||||||
|
`features.uptime_enabled` (default true).
|
||||||
|
|
||||||
|
6. **Registry:** Every module MUST be registered in
|
||||||
|
`modules/registry.json` at its semver. Entry:
|
||||||
|
`{"interface": "<path>", "published_at": "<iso>", "deprecated": false}`.
|
||||||
|
|
||||||
|
7. **Adapter extension:** 3-table pattern (TYPE_MAP + INPUT_MAP +
|
||||||
|
OUTPUT_MAP) + specialized `_emit_resource` branches for complex
|
||||||
|
resources (nested blocks like `origin {}`, `rules {}`,
|
||||||
|
`default_cache_behavior {}`).
|
||||||
|
|
||||||
|
8. **README structure:** `# <name> — <description>`, `## Resources`,
|
||||||
|
`## Inputs`, `## Outputs`, `## NFRs`, `## Usage`, `## Compliance
|
||||||
|
extension points`, `## Examples`, `## Versioning`.
|
||||||
|
|
||||||
|
9. **Catalog index gap:** `modules/README.md` Primitives table is
|
||||||
|
missing `rds` (flagged during scan). Must be fixed in Phase 35.
|
||||||
|
|
||||||
|
### Decisions surfaced (v1.8)
|
||||||
|
|
||||||
|
| ID | Decision | Rationale | Confidence | Alternatives |
|
||||||
|
|----|----------|-----------|------------|--------------|
|
||||||
|
| **D-073** | uptime-kuma v1 (`louislam/uptime-kuma:1`) as the default container image. | v1 is stable + widely deployed. v2 (2.4.0) is newer but has breaking changes. v1 is the safer default; consumers can override via `container_image` input. | 0.85 | v2 (breaking changes risk); pin to a specific v1 tag (maintenance burden). |
|
||||||
|
| **D-074** | Monitor seeding via post-deploy API script (`scripts/seed_uptime_monitors.py`), not env vars. | uptime-kuma does not natively read env for monitor config. A post-deploy script calling the API is cleaner + testable offline. | 0.90 | Env var config (not supported by uptime-kuma); manual config (defeats automation). |
|
||||||
|
| **D-075** | KMS rotation = `enable_key_rotation = true` (AWS-managed annual). 90-day rotation is a roadmap item (custom rotation Lambda). | AWS does not support custom rotation periods for managed keys. Annual is the AWS best practice. 90-day requires a custom Lambda + CloudWatch Events rule — overkill for v1.8. | 0.80 | Custom rotation Lambda (complex, overkill); no rotation (violates requirement). |
|
||||||
|
| **D-076** | uptime stack = separate VPC + ALB + ECS cluster (not shared with consumer stack). | Requirement says "separate terraform run, with a separate state". Sharing the consumer's VPC/ALB would couple the states. Separate infra is cleaner + isolates the uptime stack's lifecycle. | 0.85 | Share consumer's VPC/ALB (state coupling); use App Runner (new service type). |
|
||||||
|
| **D-077** | EFS volume for uptime-kuma `/app/data` (persistent storage across task restarts). | Fargate + EFS is the standard pattern for stateful containers. NFS is not supported by uptime-kuma, but EFS is NFS-compatible + works with Fargate. | 0.90 | S3-backed (uptime-kuma doesn't support S3); no persistent storage (data lost on restart). |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
*End of RESEARCH.md. Path: `/root/acdl/.ciagent/RESEARCH.md`.*
|
*End of RESEARCH.md. Path: `/root/acdl/.ciagent/RESEARCH.md`.*
|
||||||
+267
-6
@@ -8,6 +8,9 @@
|
|||||||
- **v1.3 (complete):** module documentation + thin-composition removal. The L2 composition layer is removed; module READMEs are built out. Tag `v1.3.2`.
|
- **v1.3 (complete):** module documentation + thin-composition removal. The L2 composition layer is removed; module READMEs are built out. Tag `v1.3.2`.
|
||||||
- **v1.4 (complete):** central pipeline contract + shell reproducibility + output streaming. A declarative pipeline contract (`schemas/pipeline.schema.json` + `pipelines/ci.yaml`) binds the Gitea and GitHub workflows to a single source of truth. `scripts/run_ci.sh` mirrors the CI pipeline locally. `scripts/run_platform.sh` streams terraform/checkov output by default.
|
- **v1.4 (complete):** central pipeline contract + shell reproducibility + output streaming. A declarative pipeline contract (`schemas/pipeline.schema.json` + `pipelines/ci.yaml`) binds the Gitea and GitHub workflows to a single source of truth. `scripts/run_ci.sh` mirrors the CI pipeline locally. `scripts/run_platform.sh` streams terraform/checkov output by default.
|
||||||
- **v1.5 (complete, tag `v1.5.0`):** consumer happy path + zero-trust docs + reusable deploy workflow. README rewritten so the consumer model is unambiguous (consumer owns only contract + app code; the rest is the platform source). Platform-flow + consumer-guide diagrams converted to mermaid. Legacy surface + implementation nomenclature removed from docs. Credentials section rewritten for zero-trust OIDC + ABAC (with a static-key override + daily rotation). A generic `docs/CONSUMER_GUIDE.md` (all L2 modules, versioned `uses:`, consumer-scoped prereqs, run-time platform fetch) replaces the module-specific guide. A byte-identical reusable `deploy.yml` workflow (Gitea + GitHub) implements `pipelines/deploy.yaml` and is invoked by consumer repos via a versioned tag.
|
- **v1.5 (complete, tag `v1.5.0`):** consumer happy path + zero-trust docs + reusable deploy workflow. README rewritten so the consumer model is unambiguous (consumer owns only contract + app code; the rest is the platform source). Platform-flow + consumer-guide diagrams converted to mermaid. Legacy surface + implementation nomenclature removed from docs. Credentials section rewritten for zero-trust OIDC + ABAC (with a static-key override + daily rotation). A generic `docs/CONSUMER_GUIDE.md` (all L2 modules, versioned `uses:`, consumer-scoped prereqs, run-time platform fetch) replaces the module-specific guide. A byte-identical reusable `deploy.yml` workflow (Gitea + GitHub) implements `pipelines/deploy.yaml` and is invoked by consumer repos via a versioned tag.
|
||||||
|
- **v1.6 (complete, tag `v1.6.0`):** consumer-facing docs restructure + terminology normalization + environments concept. `docs/` becomes a Jekyll-style GitHub Pages site. `acdl_platform/` is renamed to `core/`. L2 → "modules", L1 → "primitives", "composition" → "pattern" in prose. README restructured: Features + Roadmap (no internal status), repository roles restated (consumer = app code + contracts + CI definitions), mermaid fixed (visible text, security-checks + infrastructure-apply stages, no tool names), credentials section minus go-gitea/waivers. Platform-managed environments concept + a minimal onboarding scaffold. `.ciagent/` + `.gitea/` references removed from all consumer-facing docs.
|
||||||
|
- **v1.7 (complete, tag `v1.7.0`):** production platform + contract ingestion + pipeline maturation. Rename `static-assets` → `static-assets` (D-048 — incl. `.ciagent/` historical narrative). Author `cloudfront` + `waf` primitives; augment `static-assets` to a production-ready S3 + CloudFront (OAC) + WAF stack (D-049). Tagging-standard enforcement (Checkov custom rule, D-043 closure, D-054). Wiz adapter stub (D-052) + Kyverno K8s-native adapter (D-053). Platform Lambda + DynamoDB `acdl-contracts` table for contract ingestion (D-051) + cross-account IAM. Deploy outputs via SSM SecureString + GitHub PR comment (D-050). Uniform error reporting via the Lambda `report_error` action → GitHub issue on the platform repo (D-055); Gitea excluded. Stage comments after every successful pipeline stage. Three platform pipelines (platform-test unit+integration, primitives-plan, patterns-plan). Release job with semver + MAJOR.MINOR/MAJOR tag maintenance (D-057). `uses:`/`ref:` bumped to `@v1.6`; floating `v1.6` + `v1` tags created in Phase 22. Remove the legacy consumer-repos directory (a v1.2 artifact, removed in v1.7); add validated per-module examples (`modules/<name>/examples/`, D-058) including a new RDS primitive demonstrating multi-engine variation (D-059).
|
||||||
|
- **v1.8 (complete, tag `v1.8.0`):** P1 remediation + uptime monitoring + engineering standards + encryption/deletion-protection by default + decommission alias + path documentation. Clears 8 pending P1 issues (P1-3..P1-9 + S1). Adds per-stack CMK + encryption-by-default for all primitives. Adds deletion-protection-by-default + L2 feature flag. Adds uptime-kuma primitive (ECS Fargate, deployed by default after L2, separate state, feature flag, alert channels). Adds decommission mode (2-step pipeline with HITL SRE gates + CMDB-validated change request). Adds `modules/STANDARDS.md` (L1+L2 authoring + review standards). Adds `schemas/README.md`, `pipelines/README.md`, `adapters/README.md`.
|
||||||
- **v1.0 demo URL:** https://git.cloudinit.dev/continuous-intelligence/acdl-evidence/raw/branch/main/index.html
|
- **v1.0 demo URL:** https://git.cloudinit.dev/continuous-intelligence/acdl-evidence/raw/branch/main/index.html
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -128,12 +131,12 @@ D-034 closed (root key deactivated by user).**
|
|||||||
- `terraform validate` + `terraform plan` succeed; no long-lived credential in the workflow.
|
- `terraform validate` + `terraform plan` succeed; no long-lived credential in the workflow.
|
||||||
|
|
||||||
### Phase 10 — v1-spike-l2-and-contract-e2e
|
### Phase 10 — v1-spike-l2-and-contract-e2e
|
||||||
- **Description:** Implement `l2-static-asset` (thin-composition referencing `l1-s3`), the contract schema + contract→IR resolution, and one end-to-end contract submission (`contracts/spike.yaml` for `l2-static-asset`) flowing through schema validation → IR resolution → `terraform plan` → Checkov `PolicyCheckResult` → confidence signal → evidence event to the DynamoDB outbox. Verify the IR commitments hold (no polyglot mess).
|
- **Description:** Implement `l2-static-assets` (thin-composition referencing `l1-s3`), the contract schema + contract→IR resolution, and one end-to-end contract submission (`contracts/spike.yaml` for `l2-static-assets`) flowing through schema validation → IR resolution → `terraform plan` → Checkov `PolicyCheckResult` → confidence signal → evidence event to the DynamoDB outbox. Verify the IR commitments hold (no polyglot mess).
|
||||||
- **Status:** complete (v1.1.5)
|
- **Status:** complete (v1.1.5)
|
||||||
- **Depends on:** [09]
|
- **Depends on:** [09]
|
||||||
- **Requirements:** REQ-25, REQ-27, REQ-28
|
- **Requirements:** REQ-25, REQ-27, REQ-28
|
||||||
- **Success Criteria:**
|
- **Success Criteria:**
|
||||||
- `l2-static-asset` references `l1-s3` only (depth 1).
|
- `l2-static-assets` references `l1-s3` only (depth 1).
|
||||||
- One contract submission completes the full pipeline end-to-end.
|
- One contract submission completes the full pipeline end-to-end.
|
||||||
- `scripts/verify_phase10.sh` proves the adapter is the only substrate-specific code.
|
- `scripts/verify_phase10.sh` proves the adapter is the only substrate-specific code.
|
||||||
- Evidence event is written to the DynamoDB outbox.
|
- Evidence event is written to the DynamoDB outbox.
|
||||||
@@ -282,7 +285,7 @@ After Phase 19: COMPLETE gate — review → ship `v1.4.1` → audit.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## v1.5 (Active — consumer happy path + zero-trust docs + reusable deploy workflow)
|
## v1.5 (Complete — consumer happy path + zero-trust docs + reusable deploy workflow, tag `v1.5.0`)
|
||||||
|
|
||||||
The v1.5 milestone makes the consumer happy path self-evident, documents the
|
The v1.5 milestone makes the consumer happy path self-evident, documents the
|
||||||
zero-trust credential model, and provides a reusable deploy workflow so
|
zero-trust credential model, and provides a reusable deploy workflow so
|
||||||
@@ -290,16 +293,274 @@ consumer repos never need to clone the platform repo or invoke its scripts
|
|||||||
locally.
|
locally.
|
||||||
|
|
||||||
### Phase 20 — consumer-happy-path-and-reusable-deploy-workflow
|
### Phase 20 — consumer-happy-path-and-reusable-deploy-workflow
|
||||||
- **Description:** Rewrite `README.md` so the consumer model is unambiguous (this repo is the platform source; a consumer owns only `contract.yaml` + app code). Convert the platform-flow diagram to a mermaid `flowchart TD`. Remove "L3A"/"L3B" + "spike" nomenclature from README prose. Rewrite the Credentials section for zero-trust OIDC + ABAC (with a static-key override + daily rotation; consumer rotates out of band when using `.env.secrets` locally). Replace `docs/consumer-guide-static-asset.md` with a generic `docs/CONSUMER_GUIDE.md` (all L2 modules, mermaid diagrams, versioned `uses:` floating MAJOR+MINOR, consumer-scoped prerequisites, run-time platform fetch via a reusable workflow). Create byte-identical `.gitea/workflows/deploy.yml` + `.github/workflows/deploy.yml` implementing `pipelines/deploy.yaml` — a reusable workflow invoked by consumer repos via `uses: acdl/.gitea/workflows/deploy.yml@v1.4` that checks out the consumer repo + the ACDL platform repo and runs `scripts/run_platform.sh`. Update `contracts/static-asset.yaml` to `uses: acdl/pipelines/deploy.yaml@v1.4`. Extend `tests/test_pipeline_contract.py` to validate the new deploy workflows (byte-identical, schema-conformant).
|
- **Description:** Rewrite `README.md` so the consumer model is unambiguous (this repo is the platform source; a consumer owns only `contract.yaml` + app code). Convert the platform-flow diagram to a mermaid `flowchart TD`. Remove "L3A"/"L3B" + "spike" nomenclature from README prose. Rewrite the Credentials section for zero-trust OIDC + ABAC (with a static-key override + daily rotation; consumer rotates out of band when using `.env.secrets` locally). Replace `docs/consumer-guide-static-assets.md` with a generic `docs/CONSUMER_GUIDE.md` (all L2 modules, mermaid diagrams, versioned `uses:` floating MAJOR+MINOR, consumer-scoped prerequisites, run-time platform fetch via a reusable workflow). Create byte-identical `.gitea/workflows/deploy.yml` + `.github/workflows/deploy.yml` implementing `pipelines/deploy.yaml` — a reusable workflow invoked by consumer repos via `uses: acdl/.gitea/workflows/deploy.yml@v1.4` that checks out the consumer repo + the ACDL platform repo and runs `scripts/run_platform.sh`. Update `contracts/static-assets.yaml` to `uses: acdl/pipelines/deploy.yaml@v1.4`. Extend `tests/test_pipeline_contract.py` to validate the new deploy workflows (byte-identical, schema-conformant).
|
||||||
- **Status:** complete (v1.5.0)
|
- **Status:** complete (v1.5.0)
|
||||||
- **Depends on:** [19]
|
- **Depends on:** [19]
|
||||||
- **Requirements:** REQ-46, REQ-47, REQ-48, REQ-49, REQ-50, REQ-51
|
- **Requirements:** REQ-46, REQ-47, REQ-48, REQ-49, REQ-50, REQ-51
|
||||||
- **Success Criteria:**
|
- **Success Criteria:**
|
||||||
- `README.md` states the platform-source vs consumer-repo distinction up front; platform flow is a mermaid `flowchart TD`; `grep L3B README.md` returns 0 hits; `grep -i spike README.md` returns 0 prose hits (code paths in bash blocks allowed).
|
- `README.md` states the platform-source vs consumer-repo distinction up front; platform flow is a mermaid `flowchart TD`; `grep L3B README.md` returns 0 hits; `grep -i spike README.md` returns 0 prose hits (code paths in bash blocks allowed).
|
||||||
- `docs/CONSUMER_GUIDE.md` exists; `docs/consumer-guide-static-asset.md` is deleted; `grep -R consumer-guide-static-asset` returns 0 dangling references; guide is generic (static-asset is the worked example, not the scope); diagrams are mermaid; `uses:` references use `@v1.4`.
|
- `docs/CONSUMER_GUIDE.md` exists; `docs/consumer-guide-static-assets.md` is deleted; `grep -R consumer-guide-static-assets` returns 0 dangling references; guide is generic (static-assets is the worked example, not the scope); diagrams are mermaid; `uses:` references use `@v1.4`.
|
||||||
- `README.md` Credentials section describes OIDC + ABAC zero-trust as the default and the static-key override + daily rotation + consumer out-of-band rotation duty for local `.env.secrets`.
|
- `README.md` Credentials section describes OIDC + ABAC zero-trust as the default and the static-key override + daily rotation + consumer out-of-band rotation duty for local `.env.secrets`.
|
||||||
- `.gitea/workflows/deploy.yml` and `.github/workflows/deploy.yml` exist, are byte-identical, conform to `schemas/deploy-pipeline.schema.json`, and are reusable (`on: workflow_call` with a `contract` input).
|
- `.gitea/workflows/deploy.yml` and `.github/workflows/deploy.yml` exist, are byte-identical, conform to `schemas/deploy-pipeline.schema.json`, and are reusable (`on: workflow_call` with a `contract` input).
|
||||||
- `contracts/static-asset.yaml` uses `uses: acdl/pipelines/deploy.yaml@v1.4`.
|
- `contracts/static-assets.yaml` uses `uses: acdl/pipelines/deploy.yaml@v1.4`.
|
||||||
- `tests/test_pipeline_contract.py` validates the deploy workflows (exist, byte-identical, schema-conformant); the extended test suite passes; `bash scripts/run_ci.sh` exits 0.
|
- `tests/test_pipeline_contract.py` validates the deploy workflows (exist, byte-identical, schema-conformant); the extended test suite passes; `bash scripts/run_ci.sh` exits 0.
|
||||||
|
|
||||||
After Phase 20: COMPLETE gate — review → ship `v1.5.0` → audit.
|
After Phase 20: COMPLETE gate — review → ship `v1.5.0` → audit.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.6 (Active — consumer-facing docs restructure + terminology normalization + environments concept)
|
||||||
|
|
||||||
|
The v1.6 milestone restructures the consumer-facing documentation into a real
|
||||||
|
GitHub Pages site, normalizes the terminology (L2 → "modules", L1 →
|
||||||
|
"primitives", "composition" → "pattern", "forge" → "platform runners"), renames
|
||||||
|
`acdl_platform/` to `core/` (platform/ shadows stdlib), rewrites the README (Features + Roadmap,
|
||||||
|
restated repository roles, fixed mermaid, cleaned credentials section), removes
|
||||||
|
all `.ciagent/` + `.gitea/` references from consumer surfaces, and introduces
|
||||||
|
the concept of platform-managed environments with a minimal first-run onboarding
|
||||||
|
scaffold.
|
||||||
|
|
||||||
|
### Phase 21 — docs-restructure-and-terminology-normalization
|
||||||
|
- **Description:** Rename `acdl_platform/` → `core/` (directory + all code/test/script/pipeline/workflow references; tests green — `platform/` was the original target but shadows Python's stdlib `platform` module, so `core/` was chosen). Restructure `docs/` into a Jekyll-style GitHub Pages site (`_config.yml`, `index.md`, `modules/`, `contracts/`, `pipeline/`, `environments/`, `consumer-guide.md`, consolidated `architecture.md`, `vision.md`). Rewrite `README.md`: remove `.ciagent/` + `.gitea/workflows/` rows; restate consumer repo model (app code + 1+ contracts + CI definitions `uses:`-ing the central workflow); replace Status with Features + Roadmap (planned only); fix the mermaid (visible text, add security-checks stage before policy, no tool names, add infrastructure-apply stage); remove the environments table; clean the credentials section (no go-gitea/waivers, keep daily/out-of-band rotation); forge → platform runners/platform-managed. Update `docs/consumer-guide.md`: drop L2 (→ modules), composition → pattern (prose), remove `.gitea/` (GitHub only), forge → platform runners, mermaid updated. Update `modules/` READMEs: L1 → primitives, L2 → modules, composition → pattern (prose only, files kept); bump stale `@v1` → `@v1.4`. Consolidate `docs/architecture.md` + `docs/architecture-v1.0.md` into a single current-architecture `docs/architecture.md`. Add `docs/environments/index.md` (platform-managed AWS account/network/state/runner; consumer provides none). Add a minimal onboarding scaffold: `core/environments/` dir + sample `dev.json` + README, `core/environment_check.py`, wire-in at the top of `scripts/run_platform.sh`, friendly onboarding message when no environment is defined, `tests/test_environment_check.py`. Add a roadmap entry: "composition" will later describe the thin orchestration where consumers dynamically create a module directly from the contract file (future implementation, not this phase).
|
||||||
|
- **Status:** complete (v1.6.0)
|
||||||
|
- **Depends on:** [20]
|
||||||
|
- **Requirements:** REQ-52, REQ-53, REQ-54, REQ-55, REQ-56, REQ-57, REQ-58, REQ-59, REQ-60, REQ-61
|
||||||
|
- **Success Criteria:**
|
||||||
|
- `grep -R "\.ciagent" docs/ README.md` returns 0 hits; `grep -R "\.gitea" docs/ README.md modules/ contracts/` returns 0 hits.
|
||||||
|
- `grep -R "acdl_platform" .` (excluding `.ciagent/`, `demo/`, `.git/`) returns 0 hits; the test suite passes after the rename.
|
||||||
|
- `docs/` has the Jekyll structure (`_config.yml`, `index.md`, `modules/`, `contracts/`, `pipeline/`, `environments/`); no `.ciagent/` links in `docs/`.
|
||||||
|
- Consumer-facing docs have no "L2"/"L1" labels (modules/primitives) and no "forge" term; "composition" → "pattern" in prose.
|
||||||
|
- README.md has Features + Roadmap (no version changelog); repository roles restated; mermaid visible + security-checks + infrastructure-apply stages + no tool names; no environments table; credentials section has no go-gitea/waivers.
|
||||||
|
- `docs/environments/index.md` exists; `core/environments/` + `dev.json` + `environment_check.py` + `run_platform.sh` wire-in + `tests/test_environment_check.py` exist and pass.
|
||||||
|
- `bash scripts/run_ci.sh` exits 0; `python3 -m pytest tests/ -v` passes (154 + new environment-check tests).
|
||||||
|
|
||||||
|
After Phase 21: COMPLETE gate — review → ship `v1.6.0` → audit. **DONE.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.7 (Complete — production platform + contract ingestion + pipeline maturation, tag `v1.7.0`)
|
||||||
|
|
||||||
|
The v1.7 milestone takes the platform from a documented, environments-aware
|
||||||
|
foundation to a production-grade platform with a production-ready
|
||||||
|
`static-assets` stack (CloudFront + WAF), a contract-ingestion Lambda + DynamoDB
|
||||||
|
store for historical/impact analysis, a uniform error-reporting pathway via the
|
||||||
|
same Lambda, DX-friendly deploy outputs (SSM + PR comments), three dedicated
|
||||||
|
platform pipelines (unit+integration, primitives plan, patterns plan), a
|
||||||
|
release job with MAJOR.MINOR/MAJOR tag maintenance, new security adapters
|
||||||
|
(Wiz, Kyverno), real tagging-standard enforcement (closing D-043), removal of
|
||||||
|
the legacy consumer-repos directory (removed in v1.7), and validated per-module examples
|
||||||
|
(including a new RDS primitive demonstrating multi-engine variation).
|
||||||
|
|
||||||
|
The `uses:`/`ref:` tag advances from `@v1.4` to `@v1.6`; the floating `v1.6` +
|
||||||
|
`v1` tags are created in Phase 22 (pointing at the v1.6.0 release) so the
|
||||||
|
reference is never broken, and the release job (Phase 26) owns ongoing updates.
|
||||||
|
|
||||||
|
### Phase 22 — rename-and-production-static-assets-stack
|
||||||
|
- **Description:** Rename `static-assets` → `static-assets` everywhere (D-048 — including `.ciagent/` historical narrative, overriding the v1.6 preservation precedent). Author two new primitives: `cloudfront` (distribution + OAC, stack types `aws:cloudfront:distribution` + `aws:cloudfront:originaccesscontrol`) and `waf` (WAFv2 web ACL, stack type `aws:wafv2:webacl`). Augment the `static-assets` module to a production-ready stack referencing s3 + cloudfront + waf (depth 1, D-049). Expand the Terraform adapter `TYPE_MAP`/`INPUT_MAP`/`OUTPUT_MAP` for the new stack types. Bump `uses:`/`ref:` from `@v1.4` to `@v1.6` (D-056/D-057); create the floating `v1.6` + `v1` git tags pointing at `v1.6.0` so the reference resolves immediately.
|
||||||
|
- **Status:** complete (v1.7.0)
|
||||||
|
- **Depends on:** [21]
|
||||||
|
- **Requirements:** REQ-62, REQ-63, REQ-64
|
||||||
|
- **Success Criteria:**
|
||||||
|
- `grep -R "static-assets[^s]" .` (excluding `.git/`) returns 0 hits; `modules/l2/static-assets/` is renamed to `modules/l2/static-assets/`; `contracts/static-assets.yaml` → `contracts/static-assets.yaml`; registry key renamed; all `.ciagent/` references (incl. verbatim phase descriptions, REQ-25/27/50 text, D-036) rewritten to `static-assets`.
|
||||||
|
- `modules/l1/cloudfront/` + `modules/l1/waf/` exist with `interface.json` valid against `schemas/stack.schema.json`; registered in `modules/registry.json`.
|
||||||
|
- `modules/l2/static-assets/composition.json` references s3 + cloudfront + waf (depth 1).
|
||||||
|
- `adapters/terraform/adapter.py` `TYPE_MAP` covers `aws:cloudfront:distribution`, `aws:cloudfront:originaccesscontrol`, `aws:wafv2:webacl`.
|
||||||
|
- `contracts/static-assets.yaml` + `.github/workflows/deploy.yml` + `.gitea/workflows/deploy.yml` use `@v1.6`; git tags `v1.6` + `v1` exist pointing at `v1.6.0`.
|
||||||
|
- `bash scripts/run_ci.sh` exits 0; `python3 -m pytest tests/ -v` passes; `bash scripts/run_platform.sh --check-only` exits 0.
|
||||||
|
|
||||||
|
### Phase 23 — tagging-standards-and-security-adapters
|
||||||
|
- **Description:** Define a required-tag set (`acdl:owner`, `acdl:contract`, `acdl:environment`, `acdl:cost-center`) in `schemas/tagging-standard.json` (D-054). Author a Checkov custom YAML rule at `adapters/terraform/policy/custom_rules/acdl_tagging.yaml` that fails when required tags are missing on taggable resources. Remove the `_emit_tag_naming_skipped()` placeholder in `checkov_adapter.py` (D-043 closure) and add `ACDL_TAG_NAMING` to `RULE_MAP` as a real rule. Author a Wiz adapter stub (`adapters/wiz/wiz_adapter.py`) translating Wiz API issues → `PolicyCheckResult` records (`engine: "wiz"`), degrading gracefully when unconfigured (D-052). Author a Kyverno K8s-native adapter (`adapters/kyverno/kyverno_adapter.py`) translating Kyverno `PolicyReport` results → `PolicyCheckResult` records (`engine: "kyverno"`), with sample policies as documentation; inactive for Terraform-only stacks, ready for the GitOps reconciler roadmap item (D-053). Add `wiz` + `kyverno` to the `schemas/policy_check_result.schema.json` engine enum.
|
||||||
|
- **Status:** complete (v1.7.0)
|
||||||
|
- **Depends on:** [22]
|
||||||
|
- **Requirements:** REQ-65, REQ-66, REQ-67
|
||||||
|
- **Success Criteria:**
|
||||||
|
- `adapters/terraform/policy/custom_rules/acdl-tagging.yaml` exists; Checkov loads it; `checkov_adapter.py` no longer emits a SKIPPED `ACDL_TAG_NAMING` placeholder (D-043 closed).
|
||||||
|
- `adapters/wiz/wiz_adapter.py` + `tests/test_wiz_adapter.py` exist; tests pass offline (not-configured graceful degradation).
|
||||||
|
- `adapters/kyverno/kyverno_adapter.py` + sample policies + `tests/test_kyverno_adapter.py` exist; tests pass offline.
|
||||||
|
- `schemas/policy_check_result.schema.json` engine enum includes `checkov | kyverno | opa | wiz`.
|
||||||
|
- `bash scripts/run_ci.sh` exits 0; `python3 -m pytest tests/ -v` passes.
|
||||||
|
|
||||||
|
### Phase 24 — platform-lambda-and-contract-ingestion
|
||||||
|
- **Description:** Author a platform Lambda (`core/lambda/contract_ingestor.py`) invoked via a Function URL (IAM auth) that accepts `{ consumerRepo, contractId, contract, environment, action }` and writes contracts to a DynamoDB table `acdl-contracts` (PK `consumerRepo`, SK `contractId#submittedAt`, SSE via a customer-managed CMK) (D-051). Define the Terraform (`terraform/platform/main.tf`) for the table, Lambda, Function URL, KMS key, Secrets Manager secret (`acdl/github-token`), and Lambda execution role. Define the cross-account consumer-invoke IAM policy (`terraform/platform/consumer_invoke_policy.json`) granting the consumer's deploy role `lambda:InvokeFunctionUrl` on the Lambda ARN, scoped via ABAC. The `report_error` action (Phase 25) is prepared but not yet implemented. Update `docs/environments/index.md` to document that onboarding now also grants Lambda-invoke permission.
|
||||||
|
- **Status:** complete (v1.7.0)
|
||||||
|
- **Depends on:** [23]
|
||||||
|
- **Requirements:** REQ-68
|
||||||
|
- **Success Criteria:**
|
||||||
|
- `core/lambda/contract_ingestor.py` exists; handler writes contracts to DynamoDB (tested offline with moto).
|
||||||
|
- `terraform/platform/main.tf` defines `acdl-contracts` DynamoDB table, `acdl-contract-ingestor` Lambda, Function URL (IAM auth), KMS CMK, Secrets Manager secret, Lambda execution role.
|
||||||
|
- `terraform/platform/consumer_invoke_policy.json` exists (cross-account invoke policy template).
|
||||||
|
- `tests/test_contract_ingestor.py` passes offline.
|
||||||
|
- `bash scripts/run_ci.sh` exits 0.
|
||||||
|
|
||||||
|
### Phase 25 — deploy-pipeline-dx-outputs-and-error-reporting
|
||||||
|
- **Description:** Add a `publish-outputs` step to `scripts/run_platform.sh` (after apply) that writes deploy outputs to SSM Parameter Store as `SecureString` (KMS-encrypted, namespaced `/acdl/{env}/{contractId}/{output_name}`) for runtime-injectable values, and a `comment-outputs` step that posts a structured GitHub PR comment / job summary with human-readable connection strings (D-050). Implement `core/output_publisher.py` (SSM write + GitHub comment formatting). Implement the Lambda `report_error` action (`core/lambda/contract_ingestor.py`) that creates a GitHub issue on the platform repo (`acdl/acdl`) via the GitHub API using a token from Secrets Manager; idempotent (comments on existing open issue rather than duplicating) (D-055). Add an `if: failure()` error-report step to `.github/workflows/deploy.yml` that invokes the Lambda via `aws lambda invoke-function-url` (SigV4-signed). Add a PR comment after every successful pipeline stage (D-055 extension) via `scripts/post_stage_comment.sh` (uses `GITHUB_TOKEN` + `gh api`; no-op when not in a PR context). Update `pipelines/deploy.yaml` + both deploy workflow YAMLs with the new stages (byte-identical).
|
||||||
|
- **Status:** complete (v1.7.0)
|
||||||
|
- **Depends on:** [24]
|
||||||
|
- **Requirements:** REQ-69, REQ-70, REQ-71
|
||||||
|
- **Success Criteria:**
|
||||||
|
- `scripts/run_platform.sh` has a `publish-outputs` step (SSM SecureString, tested offline with moto) + a `comment-outputs` step (GitHub PR comment formatting, tested offline).
|
||||||
|
- `core/lambda/contract_ingestor.py` `report_error` action creates a GitHub issue (tested with mocked API); idempotent.
|
||||||
|
- `.github/workflows/deploy.yml` + `.gitea/workflows/deploy.yml` (byte-identical) have an `if: failure()` error-report step invoking the Lambda + stage comments after each successful stage (PR context).
|
||||||
|
- `pipelines/deploy.yaml` declares the new stages.
|
||||||
|
- `bash scripts/run_ci.sh` exits 0; `python3 -m pytest tests/ -v` passes.
|
||||||
|
|
||||||
|
### Phase 26 — platform-pipelines-and-release-automation
|
||||||
|
- **Description:** Author three platform pipelines (D-057): (1) `.github/workflows/platform-test.yml` (PR, lint + unit + integration + schema-validation — replaces `ci.yml` for PRs); (2) `.github/workflows/primitives-plan.yml` (PR, plan-only for all L1 primitives via matrix); (3) `.github/workflows/patterns-plan.yml` (PR, plan-only for all L2 modules via matrix). Author `scripts/run_primitive_plan.sh` + `scripts/run_pattern_plan.sh` (with `--check-only` mode for CI). Author the release job (`.github/workflows/release.yml`) that runs on merge to `main`, computes the next semver (PATCH per phase, MINOR on milestone COMPLETE), creates the MAJOR.MINOR.PATCH tag, force-moves the MAJOR.MINOR + MAJOR floating tags, creates a GitHub release with an auto-generated body. This is the mechanism that lets consumers on `@v1` or `@v1.7` receive updates.
|
||||||
|
- **Status:** complete (v1.7.0)
|
||||||
|
- **Depends on:** [25]
|
||||||
|
- **Requirements:** REQ-72, REQ-73
|
||||||
|
- **Success Criteria:**
|
||||||
|
- `.github/workflows/platform-test.yml` exists, runs lint + unit + integration + schema-validation on PR.
|
||||||
|
- `.github/workflows/primitives-plan.yml` + `.github/workflows/patterns-plan.yml` exist, run plan-only (matrix) on PR.
|
||||||
|
- `.github/workflows/release.yml` exists, computes next semver, creates + updates MAJOR.MINOR.PATCH / MAJOR.MINOR / MAJOR tags on merge.
|
||||||
|
- `scripts/run_primitive_plan.sh` + `scripts/run_pattern_plan.sh` exit 0 in `--check-only` mode.
|
||||||
|
- `bash scripts/run_ci.sh` exits 0; `python3 -m pytest tests/ -v` passes.
|
||||||
|
|
||||||
|
### Phase 27 — remove-legacy-consumer-repos-and-module-documentation-examples
|
||||||
|
- **Description:** Delete the legacy consumer-repos directory entirely (a v1.2 artifact removed in v1.7; references in `.ciagent/` historical narrative are rewritten per D-048). Author a new RDS primitive (`modules/l1/rds/`) with an `engine` input (enum: postgres, mysql, etc.) demonstrating multi-engine variation (D-059). Expand the adapter `TYPE_MAP` for `aws:rds:instance` → `aws_db_instance`. For **each** module (primitives + patterns), add a `modules/<name>/examples/` directory with `simple.yaml` + `complex.yaml` (+ variation files) validated against `schemas/contract.schema.json` in the platform-test pipeline (Phase 26 schema-validation stage) (D-058). Each module's `README.md` `## Examples` section references + excerpts the validated files. Update `docs/modules/index.md` + `docs/consumer-guide.md` + `docs/contracts/index.md` with the new module names + examples.
|
||||||
|
- **Status:** complete (v1.7.0)
|
||||||
|
- **Depends on:** [26]
|
||||||
|
- **Requirements:** REQ-74, REQ-75
|
||||||
|
- **Success Criteria:**
|
||||||
|
- The legacy consumer-repos directory does not exist; a recursive grep for the legacy directory name (excluding `.git/`) returns 0 hits.
|
||||||
|
- `modules/l1/rds/` exists with `interface.json` (`engine` enum) + `examples/`; registered; adapter emits `aws_db_instance`.
|
||||||
|
- Every module README has a `## Examples` section; `modules/<name>/examples/{simple,complex}.yaml` exist and validate against `schemas/contract.schema.json`.
|
||||||
|
- `docs/modules/index.md` links to all module READMEs (including cloudfront, waf, rds).
|
||||||
|
- `bash scripts/run_ci.sh` exits 0; `python3 -m pytest tests/ -v` passes.
|
||||||
|
|
||||||
|
After Phase 27: COMPLETE gate — review → ship `v1.7.0` → audit. **DONE.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.8 (Complete — P1 remediation + uptime + engineering standards + encryption/deletion-protection by default + decommission + docs)
|
||||||
|
|
||||||
|
The v1.8 milestone clears all pending P1 issues from v1.5–v1.7 verify
|
||||||
|
reviews AND delivers three user-directed tracks: encryption + deletion
|
||||||
|
protection by default (with a decommission alias), uptime monitoring
|
||||||
|
(uptime-kuma primitive deployed by default after L2 modules), and
|
||||||
|
engineering standards + path documentation. Ship tag at milestone
|
||||||
|
COMPLETE: **`v1.8.0`** (feature milestone, next minor per run.md — v1.7
|
||||||
|
shipped `v1.7.0`). Phase patches `v1.7.1`..`v1.7.9`.
|
||||||
|
|
||||||
|
### Phase 28 — adapter-waf-and-resolver-outputs
|
||||||
|
- **Description:** Fix WAF HCL emission: custom `rules` input emits nested `rules { ... }` blocks (not `rules = [...]` attribute syntax — P1-4). Honor `default_action` input (allow/block) instead of hardcoding `allow {}` (P1-5). Implement L2 composition `outputs[]` processing in `resolve_l2()` — build `stack.outputs` dict + adapter emits `output` blocks (P1-7). Tests for all three fixes.
|
||||||
|
- **Status:** complete (v1.8.0)
|
||||||
|
- **Depends on:** —
|
||||||
|
- **Requirements:** REQ-76, REQ-77
|
||||||
|
- **Success Criteria:**
|
||||||
|
- WAF with custom rules emits nested `rules {` blocks, not `rules = [`.
|
||||||
|
- WAF with `default_action: block` emits `block {}`; default (absent) emits `allow {}`.
|
||||||
|
- L2 resolution of `static-assets` yields `stack.outputs.distribution_domain_name`, `bucket_arn`, `web_acl_arn`.
|
||||||
|
- Adapter emits `output "distribution_domain_name" { value = ... }` blocks.
|
||||||
|
- `pytest` passes; `run_platform.sh --check-only` exits 0.
|
||||||
|
|
||||||
|
### Phase 29 — ssm-kms-and-invoke-policy
|
||||||
|
- **Description:** SSM publisher fails loud (`RuntimeError`) when `ACDL_KMS_KEY_ID` unset; `ACDL_ALLOW_DEFAULT_KMS=1` escape hatch for local testing (P1-3). Convert `consumer_invoke_policy.json` to a Terraform-rendered template using `data.aws_caller_identity` + `templatestring` — no `000000000000` placeholder (P1-6). Tests for both.
|
||||||
|
- **Status:** complete (v1.8.0)
|
||||||
|
- **Depends on:** [28]
|
||||||
|
- **Requirements:** REQ-78, REQ-79
|
||||||
|
- **Success Criteria:**
|
||||||
|
- SSM publisher raises `RuntimeError` when `ACDL_KMS_KEY_ID` unset; succeeds with `ACDL_ALLOW_DEFAULT_KMS=1`.
|
||||||
|
- Rendered invoke policy contains the caller's live account ID, not `000000000000`.
|
||||||
|
- `pytest` passes; `run_ci.sh` exits 0.
|
||||||
|
|
||||||
|
### Phase 30 — run-platform-isolation-and-api-portability
|
||||||
|
- **Description:** `run_platform.sh` emits adapter output to `$WORK/tf` (per-run temp dir), not `terraform/spike/`; remove committed `terraform/spike/*.tf` (P1-8). `contract_ingestor.py` reads `GITHUB_API_BASE` env for forge-agnostic API URLs (GitHub + Gitea); `_forge_type()` branches search URL (P1-9). Deploy workflow `configure-aws-credentials` step restructured as single conditional step: OIDC when no static key, `access-key`/`secret-key` inputs when static key present (S1). Both deploy workflows remain byte-identical.
|
||||||
|
- **Status:** complete (v1.8.0)
|
||||||
|
- **Depends on:** [29]
|
||||||
|
- **Requirements:** REQ-80, REQ-81, REQ-82
|
||||||
|
- **Success Criteria:**
|
||||||
|
- `run_platform.sh --check-only` writes to a temp dir; no `terraform/spike/*.tf` committed.
|
||||||
|
- `contract_ingestor.py` uses `GITHUB_API_BASE`; Gitea base URL produces correct API paths.
|
||||||
|
- Deploy workflow static-key override wired to `configure-aws-credentials` inputs.
|
||||||
|
- Both deploy workflows byte-identical; `pytest` + `run_ci.sh` green.
|
||||||
|
|
||||||
|
### Phase 31 — encryption-by-default-and-per-stack-cmk
|
||||||
|
- **Description:** Create `kms-key` L1 primitive (type `aws:kms:key`, inputs: description/region/deletion_window_days, outputs: kms_key_arn/kms_key_id, NFRs: enable_rotation default true, deletion_protection default true). Adapter emits `aws_kms_key` + `aws_kms_alias` + `enable_key_rotation = true`. Add `encryption_enabled` NFR (default true) + `kms_key_arn` input to all primitives. L2 modules wire a `kms-key` child + connect its output to all children. Managed KMS fallback when no CMK provided (with stderr warning).
|
||||||
|
- **Status:** complete (v1.8.0)
|
||||||
|
- **Depends on:** [30]
|
||||||
|
- **Requirements:** REQ-83, REQ-84, REQ-85
|
||||||
|
- **Success Criteria:**
|
||||||
|
- Every primitive has `encryption_enabled` NFR (default true) + optional `kms_key_arn` input.
|
||||||
|
- L2 resolution wires per-stack CMK to all children.
|
||||||
|
- Adapter emits encryption blocks (SSE, storage_encrypted, encryption_configuration) referencing the CMK.
|
||||||
|
- `enable_key_rotation = true` on the CMK; no shared keys across stacks.
|
||||||
|
- `pytest` + `run_ci.sh` green.
|
||||||
|
|
||||||
|
### Phase 32 — deletion-protection-by-default-and-l2-feature-flag
|
||||||
|
- **Description:** Add `deletion_protection` NFR (boolean, default true) to every L1 primitive. Adapter emits `lifecycle { prevent_destroy = true }` when true; omits it when false. L2 modules expose `features.deletion_protection` flag (default true); resolver propagates to each child's NFR. Consumers can set `inputs.deletion_protection: false` in contract. Update contract schema.
|
||||||
|
- **Status:** complete (v1.8.0)
|
||||||
|
- **Depends on:** [31]
|
||||||
|
- **Requirements:** REQ-86, REQ-87
|
||||||
|
- **Success Criteria:**
|
||||||
|
- Every primitive has `deletion_protection` NFR defaulting to true.
|
||||||
|
- Adapter emits `prevent_destroy = true` when true; omits when false.
|
||||||
|
- L2 feature flag propagates to all children.
|
||||||
|
- `pytest` + `run_ci.sh` green.
|
||||||
|
|
||||||
|
### Phase 33 — uptime-kuma-primitive
|
||||||
|
- **Description:** Create `uptime` L1 primitive (ECS Fargate running `louislam/uptime-kuma:1`). Inputs: container_image, region, monitored_endpoints (array of {name, url, type, interval, timeout}), static_checks, alert_channels ({teams_webhook, email_addresses, sms_numbers, github_issue_repo}), feature_flag_enabled (default true), cpu, memory. Outputs: uptime_url, service_arn, task_definition_arn. NFRs: deletion_protection, encryption_enabled. Adapter emits ECS service + ALB + log group; no resources when feature_flag_enabled=false. Register in registry. Add `deploy-uptime` pipeline stage (separate state, after publish-outputs) to `pipelines/deploy.yaml` + both deploy workflows. `run_platform.sh` constructs synthetic uptime contract from L2 outputs + runs second terraform apply. Uptime URL published via PR comment. Feature flag from `inputs.uptime_enabled` (default true).
|
||||||
|
- **Status:** complete (v1.8.0)
|
||||||
|
- **Depends on:** [32]
|
||||||
|
- **Requirements:** REQ-88, REQ-89, REQ-90, REQ-91
|
||||||
|
- **Success Criteria:**
|
||||||
|
- Uptime primitive exists with feature flag, monitored endpoints, alert channels.
|
||||||
|
- Deployed by default after L2 module (separate state); endpoints passed from L2 outputs.
|
||||||
|
- Uptime URL published via PR comment.
|
||||||
|
- Feature flag disables deployment (no resources emitted).
|
||||||
|
- `deploy-uptime` stage in deploy contract + byte-identical workflows.
|
||||||
|
- `pytest` + `run_ci.sh` green.
|
||||||
|
|
||||||
|
### Phase 34 — decommission-alias-and-cmdb-validation
|
||||||
|
- **Description:** Add `mode: decommission` to deploy pipeline. Stages: validate-change-request (Lambda `validate_change_request` action queries DynamoDB `acdl-change-requests` table, asserts status=approved) → disable-deletion-protection (resolve contract with deletion_protection=false, terraform plan/apply, HITL SRE gate) → zero-counts (resolver `decommission_transform` zeroes all counts, terraform plan/apply, second HITL SRE gate) → confirm-decommission. Add `acdl-change-requests` DynamoDB table to terraform/platform/main.tf. Add `validate_change_request` to contract_ingestor.py. Document in `docs/CONSUMER_GUIDE.md`.
|
||||||
|
- **Status:** complete (v1.8.0)
|
||||||
|
- **Depends on:** [33]
|
||||||
|
- **Requirements:** REQ-92, REQ-93, REQ-94
|
||||||
|
- **Success Criteria:**
|
||||||
|
- Decommission mode works via existing deploy pipeline with 2-step HITL SRE gates.
|
||||||
|
- CR ID validated against DynamoDB CMDB (status must be approved).
|
||||||
|
- `decommission_transform` zeroes all counts.
|
||||||
|
- Documented in consumer guide.
|
||||||
|
- `pytest` + `run_ci.sh` green.
|
||||||
|
|
||||||
|
### Phase 35 — module-engineering-standards
|
||||||
|
- **Description:** Scan all current modules to generate `modules/STANDARDS.md` — comprehensive L1+L2 authoring + code review standards: required files, interface schema, input/output/NFR conventions, encryption + deletion protection as mandatory NFRs, naming, multi-resource pattern, adapter extension pattern (TYPE_MAP + INPUT_MAP + OUTPUT_MAP + specialized branches), code review checklist. Fix `modules/README.md` catalog index (add rds + uptime + kms-key). Update `modules/README-TEMPLATE.md` with `## NFRs` section. Add `tests/test_module_standards.py` for automated enforcement.
|
||||||
|
- **Status:** complete (v1.8.0)
|
||||||
|
- **Depends on:** [34]
|
||||||
|
- **Requirements:** REQ-95, REQ-96
|
||||||
|
- **Success Criteria:**
|
||||||
|
- `modules/STANDARDS.md` exists with L1+L2 authoring + review standards.
|
||||||
|
- Catalog index includes all primitives; template has NFRs section.
|
||||||
|
- Automated standards test passes for all modules.
|
||||||
|
- `pytest` + `run_ci.sh` green.
|
||||||
|
|
||||||
|
### Phase 36 — schemas-adapters-pipelines-readmes
|
||||||
|
- **Description:** Author `schemas/README.md` (how to write schemas, wire into platform, test in CI, dependencies, existing catalog), `pipelines/README.md` (how to write pipeline contracts, wire into workflows, test, dependencies, catalog), `adapters/README.md` (how to write adapters, wire into platform, test, dependencies, catalog). Add `tests/test_docs_coverage.py` to validate presence + required sections.
|
||||||
|
- **Status:** complete (v1.8.0)
|
||||||
|
- **Depends on:** [35]
|
||||||
|
- **Requirements:** REQ-97, REQ-98, REQ-99
|
||||||
|
- **Success Criteria:**
|
||||||
|
- All 3 READMEs exist with comprehensive documentation.
|
||||||
|
- CI validates their presence.
|
||||||
|
- `pytest` + `run_ci.sh` green.
|
||||||
|
|
||||||
|
### Phase 37 — verify
|
||||||
|
- **Description:** 4-layer verification (structural, behavioral, security, quality) of all v1.8 phases. Re-verify each P1 (P1-3..P1-9 + S1) is resolved. Verify all new features (encryption, deletion protection, uptime, decommission, standards, docs) have dedicated tests.
|
||||||
|
- **Status:** complete (v1.8.0)
|
||||||
|
- **Depends on:** [36]
|
||||||
|
- **Requirements:** —
|
||||||
|
- **Success Criteria:**
|
||||||
|
- All 4 layers pass; each P1 fix + each new feature has a dedicated test.
|
||||||
|
- `pytest` passes (~358 tests); `run_ci.sh` exits 0; `run_platform.sh --check-only` exits 0.
|
||||||
|
|
||||||
|
### Phase 38 — review-audit-complete
|
||||||
|
- **Description:** Multi-persona code review across the full v1.8 diff. Audit (reconstruction, file discipline, branch hygiene, commit discipline). Complete: update REQUIREMENTS.md (REQ-76..99), ROADMAP.md (v1.8 complete), PROJECT.md. Tag `v1.8.0`. Update floating `v1.8` + `v1` tags. Bump `uses:`/`ref:` from `@v1.6` to `@v1.8`.
|
||||||
|
- **Status:** complete (v1.8.0)
|
||||||
|
- **Depends on:** [37]
|
||||||
|
- **Requirements:** —
|
||||||
|
- **Success Criteria:**
|
||||||
|
- Review: 0 new P0/P1; all P1-3..P1-9 + S1 resolved; 3 new requirements delivered.
|
||||||
|
- Audit: clean; 0 outstanding issues.
|
||||||
|
- Tag `v1.8.0` created; floating tags updated.
|
||||||
|
|
||||||
|
After Phase 38: COMPLETE gate — review → ship `v1.8.0` → audit.
|
||||||
+34
-33
@@ -1,45 +1,46 @@
|
|||||||
# Phase 18 — Verify (v1.3.2)
|
# Phase 28-36 — Verify (v1.8)
|
||||||
|
|
||||||
## Structural
|
## Structural
|
||||||
|
All 14 new files present (kms-key primitive, uptime primitive, STANDARDS.md,
|
||||||
All 11 new files confirmed present: pyproject.toml, requirements-test.txt,
|
3 READMEs, seed script, 4 test files). terraform/spike removed. Registry
|
||||||
tests/__init__.py, tests/conftest.py, tests/test_adapter.py,
|
has 14 entries. **PASS.**
|
||||||
tests/test_confidence_signal.py, tests/test_checkov_adapter.py,
|
|
||||||
tests/test_outbox_writer.py, tests/test_pipeline.py,
|
|
||||||
.gitea/workflows/ci.yml, .github/workflows/ci.yml. **PASS.**
|
|
||||||
|
|
||||||
## Behavioral
|
## Behavioral
|
||||||
|
- `pytest`: 350 tests, all passing (was 275 at v1.7 → 350 at v1.8, +75 new).
|
||||||
- `py_compile` passes on all Python files. **PASS.**
|
- `run_ci.sh`: exits 0 with "CI PIPELINE OK".
|
||||||
- `pytest` — 90 tests, all passing, all offline (moto for DynamoDB
|
- `run_platform.sh --check-only`: exits 0 with "PLATFORM CHECK OK" (5 resources
|
||||||
mocking). **PASS.**
|
for static-assets with the per-stack CMK).
|
||||||
- `run_platform.sh --check-only` — exits 0, outputs
|
**PASS.**
|
||||||
"PLATFORM CHECK OK", requires no AWS credentials. **PASS.**
|
|
||||||
- `run_platform.sh --plan-only` — syntax valid (unchanged from phase 17).
|
|
||||||
**PASS.**
|
|
||||||
- Both workflow YAMLs are valid YAML, parseable. **PASS.**
|
|
||||||
- Workflows are byte-identical (diff confirms). **PASS.**
|
|
||||||
|
|
||||||
## Security
|
## Security
|
||||||
|
- No placeholder account ID in consumer_invoke_policy.json.
|
||||||
- No secrets in any new file (tests, workflows, pyproject, requirements).
|
- No hardcoded GitHub API URLs in contract_ingestor.py (uses GITHUB_API_BASE).
|
||||||
**PASS.**
|
- Deploy workflows byte-identical.
|
||||||
- CI pipelines do not use any AWS credentials — `--check-only` is fully
|
- SSM fails loud without ACDL_KMS_KEY_ID (RuntimeError).
|
||||||
offline. **PASS.**
|
- Deletion protection on by default for all primitives.
|
||||||
|
- Encryption enabled by default for all primitives.
|
||||||
|
**PASS.**
|
||||||
|
|
||||||
## Quality
|
## Quality
|
||||||
|
Each P1 fix has a dedicated test:
|
||||||
|
- P1-3: test_kms_unset_raises, test_kms_unset_allow_default_kms_escape_hatch
|
||||||
|
- P1-4: test_waf_custom_rules_emit_nested_blocks
|
||||||
|
- P1-5: test_waf_default_action_block_honored, test_waf_default_action_allow_when_absent
|
||||||
|
- P1-6: test_policy_has_no_hardcoded_account_id, test_main_tf_has_caller_identity_data_source
|
||||||
|
- P1-7: test_static_assets_has_stack_outputs, test_static_assets_adapter_emits_stack_output_blocks
|
||||||
|
- P1-8: run_platform.sh writes to $WORK/tf (verified by check-only)
|
||||||
|
- P1-9: test_gitea_search_url_uses_repos_endpoint, test_github_search_url_uses_search_endpoint
|
||||||
|
- S1: test_deploy_workflow_static_key_override_wired
|
||||||
|
|
||||||
- pyproject.toml has pytest config (testpaths, markers, addopts).
|
Each new feature has dedicated tests:
|
||||||
**PASS.**
|
- Encryption: test_kms_key_adapter_emits_rotation, test_all_l1_primitives_have_encryption_nfr, test_s3_with_kms_key_arn_emits_sse_configuration, test_static_assets_l2_wires_kms_key_to_s3
|
||||||
- requirements-test.txt pins all test deps. **PASS.**
|
- Deletion protection: test_all_l1_primitives_have_deletion_protection_nfr, test_adapter_emits_prevent_destroy_when_nfr_true, test_l2_feature_flag_propagates_deletion_protection_false
|
||||||
- Test suite covers all 4 platform components (adapter, confidence
|
- Uptime: test_uptime_adapter_emits_ecs_service_when_enabled, test_uptime_adapter_emits_nothing_when_disabled, test_deploy_pipeline_has_deploy_uptime_stage
|
||||||
signal, checkov adapter, outbox writer) + pipeline integration.
|
- Decommission: test_decommission_transform_zeros_desired_count, test_validates_approved_cr, test_consumer_guide_has_decommission_section
|
||||||
**PASS.**
|
- Standards: test_standards_md_has_required_sections, test_all_l1_have_deletion_protection_nfr, test_all_l1_have_encryption_enabled_nfr
|
||||||
- Both workflows run 3 stages: lint, test, check-only. **PASS.**
|
- Docs: test_schemas_readme_has_required_sections, test_pipelines_readme_has_required_sections, test_adapters_readme_has_required_sections
|
||||||
- README updated with "Test the platform" section + CI/CD documentation.
|
**PASS.**
|
||||||
**PASS.**
|
|
||||||
|
|
||||||
## Verdict
|
## Verdict
|
||||||
|
|
||||||
**VERIFY PASS** — all four layers pass. 90 offline tests, no AWS
|
**VERIFY PASS** — all four layers pass. 350 offline tests, no AWS required for CI.
|
||||||
required for CI.
|
|
||||||
@@ -4,8 +4,8 @@
|
|||||||
{
|
{
|
||||||
"slug": "acdl",
|
"slug": "acdl",
|
||||||
"name": "Agentic Cloud Delivery Platform",
|
"name": "Agentic Cloud Delivery Platform",
|
||||||
"milestone": "v1.5",
|
"milestone": "v1.8",
|
||||||
"status": "active"
|
"status": "complete"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"active_project": "acdl",
|
"active_project": "acdl",
|
||||||
|
|||||||
@@ -35,9 +35,11 @@ jobs:
|
|||||||
- name: Compile all Python files
|
- name: Compile all Python files
|
||||||
run: |
|
run: |
|
||||||
python3 -m py_compile \
|
python3 -m py_compile \
|
||||||
acdl_platform/confidence_signal.py \
|
core/confidence_signal.py \
|
||||||
acdl_platform/outbox_writer.py \
|
core/outbox_writer.py \
|
||||||
acdl_platform/contract_resolver.py \
|
core/output_publisher.py \
|
||||||
|
core/contract_resolver.py \
|
||||||
|
core/lambda/contract_ingestor.py \
|
||||||
adapters/terraform/adapter.py \
|
adapters/terraform/adapter.py \
|
||||||
adapters/terraform/policy/checkov_adapter.py \
|
adapters/terraform/policy/checkov_adapter.py \
|
||||||
scripts/push_consumer_image.py
|
scripts/push_consumer_image.py
|
||||||
|
|||||||
+44
-14
@@ -8,8 +8,8 @@
|
|||||||
# declared difference is the forge/runtime, not the stages or commands.
|
# declared difference is the forge/runtime, not the stages or commands.
|
||||||
#
|
#
|
||||||
# Consumer repos invoke this workflow via a versioned tag (floating MAJOR + MINOR):
|
# Consumer repos invoke this workflow via a versioned tag (floating MAJOR + MINOR):
|
||||||
# uses: acdl/.gitea/workflows/deploy.yml@v1.4 (Gitea)
|
# uses: acdl/.gitea/workflows/deploy.yml@v1.6 (Gitea)
|
||||||
# uses: acdl/.github/workflows/deploy.yml@v1.4 (GitHub)
|
# uses: acdl/.github/workflows/deploy.yml@v1.6 (GitHub)
|
||||||
#
|
#
|
||||||
# Unversioned references (@main, bare) are discouraged — the consumer's setup
|
# Unversioned references (@main, bare) are discouraged — the consumer's setup
|
||||||
# must be immutable + resilient. The versioned tag is the only immutability
|
# must be immutable + resilient. The versioned tag is the only immutability
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
#
|
#
|
||||||
# What this workflow does:
|
# What this workflow does:
|
||||||
# 1. Checks out the consumer repo (the repo that invoked the workflow).
|
# 1. Checks out the consumer repo (the repo that invoked the workflow).
|
||||||
# 2. Checks out the ACDL platform repo into the workspace (acdl-platform/).
|
# 2. Checks out the ACDL platform repo into the workspace (platform/).
|
||||||
# This is the run-time fetch — consumers never clone the platform repo.
|
# This is the run-time fetch — consumers never clone the platform repo.
|
||||||
# 3. Installs runtime deps: Python 3.12, Terraform 1.9.*, Checkov.
|
# 3. Installs runtime deps: Python 3.12, Terraform 1.9.*, Checkov.
|
||||||
# 4. Configures AWS auth (OIDC default; static-key override via secrets).
|
# 4. Configures AWS auth (OIDC default; static-key override via secrets).
|
||||||
@@ -53,9 +53,13 @@ on:
|
|||||||
type: string
|
type: string
|
||||||
default: .acdl/contract.yaml
|
default: .acdl/contract.yaml
|
||||||
mode:
|
mode:
|
||||||
description: Pipeline mode — full (apply), plan-only, or check-only
|
description: Pipeline mode — full (apply), plan-only, check-only, or decommission
|
||||||
type: string
|
type: string
|
||||||
default: full
|
default: full
|
||||||
|
changeRequestId:
|
||||||
|
description: Change request ID (required for decommission mode — validated against CMDB)
|
||||||
|
type: string
|
||||||
|
default: ""
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
id-token: write
|
id-token: write
|
||||||
@@ -73,8 +77,8 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
repository: acdl/acdl
|
repository: acdl/acdl
|
||||||
path: acdl-platform
|
path: platform
|
||||||
ref: v1.4
|
ref: v1.6
|
||||||
|
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
@@ -91,14 +95,13 @@ jobs:
|
|||||||
echo "deb [signed-by=/usr/share/keyrings/hashicorp.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
|
echo "deb [signed-by=/usr/share/keyrings/hashicorp.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
|
||||||
sudo apt-get update && sudo apt-get install -y terraform=1.9.*
|
sudo apt-get update && sudo apt-get install -y terraform=1.9.*
|
||||||
|
|
||||||
- name: Configure AWS credentials (OIDC default)
|
- name: Configure AWS credentials (OIDC default + static-key override)
|
||||||
uses: aws-actions/configure-aws-credentials@v4
|
uses: aws-actions/configure-aws-credentials@v4
|
||||||
with:
|
with:
|
||||||
role-to-assume: arn:aws:iam::${{ secrets.ACDL_AWS_ACCOUNT_ID }}:role/acdl-deploy-${{ github.repository_id }}
|
role-to-assume: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID == '' && format('arn:aws:iam::{0}:role/acdl-deploy-{1}', secrets.ACDL_AWS_ACCOUNT_ID, github.repository_id) || '' }}
|
||||||
aws-region: us-east-1
|
aws-region: us-east-1
|
||||||
env:
|
access-key-id: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
|
||||||
ACDL_AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
|
secret-access-key: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
|
||||||
ACDL_AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
|
|
||||||
|
|
||||||
- name: Run the platform pipeline
|
- name: Run the platform pipeline
|
||||||
working-directory: ${{ github.workspace }}
|
working-directory: ${{ github.workspace }}
|
||||||
@@ -108,20 +111,47 @@ jobs:
|
|||||||
full) MODE_FLAG="" ;;
|
full) MODE_FLAG="" ;;
|
||||||
plan-only) MODE_FLAG="--plan-only" ;;
|
plan-only) MODE_FLAG="--plan-only" ;;
|
||||||
check-only) MODE_FLAG="--check-only" ;;
|
check-only) MODE_FLAG="--check-only" ;;
|
||||||
|
decommission)
|
||||||
|
if [ -z "${{ inputs.changeRequestId }}" ]; then
|
||||||
|
echo "FAIL: changeRequestId is required for decommission mode"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
MODE_FLAG="--decommission ${{ inputs.changeRequestId }}"
|
||||||
|
;;
|
||||||
*) echo "Unknown mode: ${{ inputs.mode }}"; exit 1 ;;
|
*) echo "Unknown mode: ${{ inputs.mode }}"; exit 1 ;;
|
||||||
esac
|
esac
|
||||||
bash acdl-platform/scripts/run_platform.sh $MODE_FLAG "${{ inputs.contract }}"
|
bash platform/scripts/run_platform.sh $MODE_FLAG "${{ inputs.contract }}"
|
||||||
|
|
||||||
|
- name: Post stage summary comment to PR
|
||||||
|
if: success() && github.event_name == 'pull_request'
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||||
|
GITHUB_REF: ${{ github.ref }}
|
||||||
|
run: |
|
||||||
|
bash platform/scripts/post_stage_comment.sh deploy pass '{"mode":"${{ inputs.mode }}","runId":"${{ github.run_id }}"}'
|
||||||
|
|
||||||
|
- name: Report error to platform team (on failure)
|
||||||
|
if: failure()
|
||||||
|
env:
|
||||||
|
AWS_DEFAULT_REGION: us-east-1
|
||||||
|
run: |
|
||||||
|
aws lambda invoke-function-url \
|
||||||
|
--function-url "${{ secrets.ACDL_LAMBDA_URL }}" \
|
||||||
|
--cli-binary-format raw-in-base64-out \
|
||||||
|
--payload "$(python3 -c "import json,os; print(json.dumps({'action':'report_error','consumerRepo':os.environ.get('GITHUB_REPOSITORY',''),'contractId':'${{ github.run_id }}','error':'Deploy pipeline failed. See run logs.','runUrl':'${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}','environment':'dev'}))")" \
|
||||||
|
/dev/null || true
|
||||||
|
|
||||||
- name: Upload emitted Terraform
|
- name: Upload emitted Terraform
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: acdl-terraform
|
name: acdl-terraform
|
||||||
path: acdl-platform/terraform/spike/*.tf
|
path: /tmp/acdl_platform_run_v18/tf/*.tf
|
||||||
if-no-files-found: warn
|
if-no-files-found: warn
|
||||||
|
|
||||||
- name: Upload platform log
|
- name: Upload platform log
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: acdl-platform-log
|
name: acdl-platform-log
|
||||||
path: acdl-platform/logs/
|
path: platform/logs/
|
||||||
if-no-files-found: warn
|
if-no-files-found: warn
|
||||||
@@ -35,9 +35,11 @@ jobs:
|
|||||||
- name: Compile all Python files
|
- name: Compile all Python files
|
||||||
run: |
|
run: |
|
||||||
python3 -m py_compile \
|
python3 -m py_compile \
|
||||||
acdl_platform/confidence_signal.py \
|
core/confidence_signal.py \
|
||||||
acdl_platform/outbox_writer.py \
|
core/outbox_writer.py \
|
||||||
acdl_platform/contract_resolver.py \
|
core/output_publisher.py \
|
||||||
|
core/contract_resolver.py \
|
||||||
|
core/lambda/contract_ingestor.py \
|
||||||
adapters/terraform/adapter.py \
|
adapters/terraform/adapter.py \
|
||||||
adapters/terraform/policy/checkov_adapter.py \
|
adapters/terraform/policy/checkov_adapter.py \
|
||||||
scripts/push_consumer_image.py
|
scripts/push_consumer_image.py
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
# declared difference is the forge/runtime, not the stages or commands.
|
# declared difference is the forge/runtime, not the stages or commands.
|
||||||
#
|
#
|
||||||
# Consumer repos invoke this workflow via a versioned tag (floating MAJOR + MINOR):
|
# Consumer repos invoke this workflow via a versioned tag (floating MAJOR + MINOR):
|
||||||
# uses: acdl/.gitea/workflows/deploy.yml@v1.4 (Gitea)
|
# uses: acdl/.gitea/workflows/deploy.yml@v1.6 (Gitea)
|
||||||
# uses: acdl/.github/workflows/deploy.yml@v1.4 (GitHub)
|
# uses: acdl/.github/workflows/deploy.yml@v1.6 (GitHub)
|
||||||
#
|
#
|
||||||
# Unversioned references (@main, bare) are discouraged — the consumer's setup
|
# Unversioned references (@main, bare) are discouraged — the consumer's setup
|
||||||
# must be immutable + resilient. The versioned tag is the only immutability
|
# must be immutable + resilient. The versioned tag is the only immutability
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
#
|
#
|
||||||
# What this workflow does:
|
# What this workflow does:
|
||||||
# 1. Checks out the consumer repo (the repo that invoked the workflow).
|
# 1. Checks out the consumer repo (the repo that invoked the workflow).
|
||||||
# 2. Checks out the ACDL platform repo into the workspace (acdl-platform/).
|
# 2. Checks out the ACDL platform repo into the workspace (platform/).
|
||||||
# This is the run-time fetch — consumers never clone the platform repo.
|
# This is the run-time fetch — consumers never clone the platform repo.
|
||||||
# 3. Installs runtime deps: Python 3.12, Terraform 1.9.*, Checkov.
|
# 3. Installs runtime deps: Python 3.12, Terraform 1.9.*, Checkov.
|
||||||
# 4. Configures AWS auth (OIDC default; static-key override via secrets).
|
# 4. Configures AWS auth (OIDC default; static-key override via secrets).
|
||||||
@@ -53,9 +53,13 @@ on:
|
|||||||
type: string
|
type: string
|
||||||
default: .acdl/contract.yaml
|
default: .acdl/contract.yaml
|
||||||
mode:
|
mode:
|
||||||
description: Pipeline mode — full (apply), plan-only, or check-only
|
description: Pipeline mode — full (apply), plan-only, check-only, or decommission
|
||||||
type: string
|
type: string
|
||||||
default: full
|
default: full
|
||||||
|
changeRequestId:
|
||||||
|
description: Change request ID (required for decommission mode — validated against CMDB)
|
||||||
|
type: string
|
||||||
|
default: ""
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
id-token: write
|
id-token: write
|
||||||
@@ -73,8 +77,8 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
repository: acdl/acdl
|
repository: acdl/acdl
|
||||||
path: acdl-platform
|
path: platform
|
||||||
ref: v1.4
|
ref: v1.6
|
||||||
|
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
@@ -91,14 +95,13 @@ jobs:
|
|||||||
echo "deb [signed-by=/usr/share/keyrings/hashicorp.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
|
echo "deb [signed-by=/usr/share/keyrings/hashicorp.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
|
||||||
sudo apt-get update && sudo apt-get install -y terraform=1.9.*
|
sudo apt-get update && sudo apt-get install -y terraform=1.9.*
|
||||||
|
|
||||||
- name: Configure AWS credentials (OIDC default)
|
- name: Configure AWS credentials (OIDC default + static-key override)
|
||||||
uses: aws-actions/configure-aws-credentials@v4
|
uses: aws-actions/configure-aws-credentials@v4
|
||||||
with:
|
with:
|
||||||
role-to-assume: arn:aws:iam::${{ secrets.ACDL_AWS_ACCOUNT_ID }}:role/acdl-deploy-${{ github.repository_id }}
|
role-to-assume: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID == '' && format('arn:aws:iam::{0}:role/acdl-deploy-{1}', secrets.ACDL_AWS_ACCOUNT_ID, github.repository_id) || '' }}
|
||||||
aws-region: us-east-1
|
aws-region: us-east-1
|
||||||
env:
|
access-key-id: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
|
||||||
ACDL_AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
|
secret-access-key: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
|
||||||
ACDL_AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
|
|
||||||
|
|
||||||
- name: Run the platform pipeline
|
- name: Run the platform pipeline
|
||||||
working-directory: ${{ github.workspace }}
|
working-directory: ${{ github.workspace }}
|
||||||
@@ -108,20 +111,47 @@ jobs:
|
|||||||
full) MODE_FLAG="" ;;
|
full) MODE_FLAG="" ;;
|
||||||
plan-only) MODE_FLAG="--plan-only" ;;
|
plan-only) MODE_FLAG="--plan-only" ;;
|
||||||
check-only) MODE_FLAG="--check-only" ;;
|
check-only) MODE_FLAG="--check-only" ;;
|
||||||
|
decommission)
|
||||||
|
if [ -z "${{ inputs.changeRequestId }}" ]; then
|
||||||
|
echo "FAIL: changeRequestId is required for decommission mode"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
MODE_FLAG="--decommission ${{ inputs.changeRequestId }}"
|
||||||
|
;;
|
||||||
*) echo "Unknown mode: ${{ inputs.mode }}"; exit 1 ;;
|
*) echo "Unknown mode: ${{ inputs.mode }}"; exit 1 ;;
|
||||||
esac
|
esac
|
||||||
bash acdl-platform/scripts/run_platform.sh $MODE_FLAG "${{ inputs.contract }}"
|
bash platform/scripts/run_platform.sh $MODE_FLAG "${{ inputs.contract }}"
|
||||||
|
|
||||||
|
- name: Post stage summary comment to PR
|
||||||
|
if: success() && github.event_name == 'pull_request'
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||||
|
GITHUB_REF: ${{ github.ref }}
|
||||||
|
run: |
|
||||||
|
bash platform/scripts/post_stage_comment.sh deploy pass '{"mode":"${{ inputs.mode }}","runId":"${{ github.run_id }}"}'
|
||||||
|
|
||||||
|
- name: Report error to platform team (on failure)
|
||||||
|
if: failure()
|
||||||
|
env:
|
||||||
|
AWS_DEFAULT_REGION: us-east-1
|
||||||
|
run: |
|
||||||
|
aws lambda invoke-function-url \
|
||||||
|
--function-url "${{ secrets.ACDL_LAMBDA_URL }}" \
|
||||||
|
--cli-binary-format raw-in-base64-out \
|
||||||
|
--payload "$(python3 -c "import json,os; print(json.dumps({'action':'report_error','consumerRepo':os.environ.get('GITHUB_REPOSITORY',''),'contractId':'${{ github.run_id }}','error':'Deploy pipeline failed. See run logs.','runUrl':'${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}','environment':'dev'}))")" \
|
||||||
|
/dev/null || true
|
||||||
|
|
||||||
- name: Upload emitted Terraform
|
- name: Upload emitted Terraform
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: acdl-terraform
|
name: acdl-terraform
|
||||||
path: acdl-platform/terraform/spike/*.tf
|
path: /tmp/acdl_platform_run_v18/tf/*.tf
|
||||||
if-no-files-found: warn
|
if-no-files-found: warn
|
||||||
|
|
||||||
- name: Upload platform log
|
- name: Upload platform log
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: acdl-platform-log
|
name: acdl-platform-log
|
||||||
path: acdl-platform/logs/
|
path: platform/logs/
|
||||||
if-no-files-found: warn
|
if-no-files-found: warn
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# ACDL Patterns Plan Pipeline — GitHub Actions (production)
|
||||||
|
#
|
||||||
|
# Runs on PRs to main. For each L2 module, runs a plan-only (offline
|
||||||
|
# --check-only mode: resolves the sample contract for the module, runs the
|
||||||
|
# adapter, validates the emitted Terraform structure).
|
||||||
|
name: acdl-patterns-plan
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
pattern-plan:
|
||||||
|
name: Pattern plan (${{ matrix.module }})
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
module: [static-assets, microservice]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pip install jsonschema pyyaml boto3
|
||||||
|
- name: Pattern plan check (${{ matrix.module }})
|
||||||
|
run: bash scripts/run_pattern_plan.sh --check-only ${{ matrix.module }}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
# ACDL Platform Test Pipeline — GitHub Actions (production)
|
||||||
|
#
|
||||||
|
# Runs on PRs to main. Replaces ci.yml for PRs (ci.yml stays for push-to-main).
|
||||||
|
# Four stages: lint, unit-test, integration-test, schema-validation.
|
||||||
|
#
|
||||||
|
# Shell reproducibility: scripts/run_ci.sh runs lint + test + check-only locally.
|
||||||
|
# The integration-test stage runs run_platform.sh --check-only for every
|
||||||
|
# contracts/*.yaml file. The schema-validation stage validates schemas, module
|
||||||
|
# interfaces, compositions, and example contracts.
|
||||||
|
name: acdl-platform-test
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
name: Lint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
- name: Compile all Python files
|
||||||
|
run: |
|
||||||
|
python3 -m py_compile \
|
||||||
|
core/confidence_signal.py \
|
||||||
|
core/outbox_writer.py \
|
||||||
|
core/contract_resolver.py \
|
||||||
|
core/environment_check.py \
|
||||||
|
core/output_publisher.py \
|
||||||
|
core/lambda/contract_ingestor.py \
|
||||||
|
adapters/terraform/adapter.py \
|
||||||
|
adapters/terraform/policy/checkov_adapter.py \
|
||||||
|
adapters/wiz/wiz_adapter.py \
|
||||||
|
adapters/kyverno/kyverno_adapter.py \
|
||||||
|
scripts/push_consumer_image.py
|
||||||
|
|
||||||
|
unit-test:
|
||||||
|
name: Unit tests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
- name: Install test dependencies
|
||||||
|
run: pip install -r requirements-test.txt
|
||||||
|
- name: Run pytest
|
||||||
|
run: python3 -m pytest tests/ -v --tb=short
|
||||||
|
|
||||||
|
integration-test:
|
||||||
|
name: Integration test (all sample contracts)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
- name: Install runtime dependencies
|
||||||
|
run: pip install jsonschema pyyaml boto3
|
||||||
|
- name: Run platform check-only for every sample contract
|
||||||
|
run: |
|
||||||
|
for contract in contracts/*.yaml; do
|
||||||
|
echo "--- Testing $contract ---"
|
||||||
|
bash scripts/run_platform.sh --check-only "$contract"
|
||||||
|
done
|
||||||
|
|
||||||
|
schema-validation:
|
||||||
|
name: Schema + module validation
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pip install jsonschema pyyaml
|
||||||
|
- name: Validate all schemas
|
||||||
|
run: |
|
||||||
|
python3 -c "
|
||||||
|
import json, glob, jsonschema
|
||||||
|
for schema_file in glob.glob('schemas/*.json'):
|
||||||
|
if 'contract.schema' in schema_file:
|
||||||
|
continue # has no self-validation
|
||||||
|
schema = json.load(open(schema_file))
|
||||||
|
# self-validate if it has a \$id
|
||||||
|
try:
|
||||||
|
jsonschema.Draft202012Validator.check_schema(schema)
|
||||||
|
except jsonschema.SchemaError as e:
|
||||||
|
raise SystemExit(f'{schema_file}: {e}')
|
||||||
|
print(f'{schema_file}: valid')
|
||||||
|
"
|
||||||
|
- name: Validate all module interfaces against stack.schema.json
|
||||||
|
run: |
|
||||||
|
python3 -c "
|
||||||
|
import json, glob, jsonschema, os
|
||||||
|
stack_schema = json.load(open('schemas/stack.schema.json'))
|
||||||
|
for iface_file in glob.glob('modules/l1/*/interface.json'):
|
||||||
|
try:
|
||||||
|
iface = json.load(open(iface_file))
|
||||||
|
# Validate basic structure (name, version, kind, type, inputs, outputs)
|
||||||
|
assert 'name' in iface, f'{iface_file}: missing name'
|
||||||
|
assert 'version' in iface, f'{iface_file}: missing version'
|
||||||
|
assert 'kind' in iface, f'{iface_file}: missing kind'
|
||||||
|
assert iface['kind'] == 'l1', f'{iface_file}: expected kind=l1'
|
||||||
|
assert 'type' in iface, f'{iface_file}: missing type'
|
||||||
|
assert 'inputs' in iface, f'{iface_file}: missing inputs'
|
||||||
|
assert 'outputs' in iface, f'{iface_file}: missing outputs'
|
||||||
|
print(f'{iface_file}: valid L1')
|
||||||
|
except Exception as e:
|
||||||
|
raise SystemExit(f'{iface_file}: {e}')
|
||||||
|
for comp_file in glob.glob('modules/l2/*/composition.json'):
|
||||||
|
try:
|
||||||
|
comp = json.load(open(comp_file))
|
||||||
|
assert 'name' in comp, f'{comp_file}: missing name'
|
||||||
|
assert 'version' in comp, f'{comp_file}: missing version'
|
||||||
|
assert 'kind' in comp, f'{comp_file}: missing kind'
|
||||||
|
assert comp['kind'] == 'l2', f'{comp_file}: expected kind=l2'
|
||||||
|
assert 'children' in comp, f'{comp_file}: missing children'
|
||||||
|
assert 'wires' in comp, f'{comp_file}: missing wires'
|
||||||
|
assert 'outputs' in comp, f'{comp_file}: missing outputs'
|
||||||
|
print(f'{comp_file}: valid L2')
|
||||||
|
except Exception as e:
|
||||||
|
raise SystemExit(f'{comp_file}: {e}')
|
||||||
|
"
|
||||||
|
- name: Validate module example contracts
|
||||||
|
run: |
|
||||||
|
python3 -c "
|
||||||
|
import json, yaml, glob, jsonschema
|
||||||
|
schema = json.load(open('schemas/contract.schema.json'))
|
||||||
|
# Validate example contracts if they exist
|
||||||
|
for example in glob.glob('modules/*/*/examples/*.yaml'):
|
||||||
|
try:
|
||||||
|
contract = yaml.safe_load(open(example))
|
||||||
|
jsonschema.validate(contract, schema)
|
||||||
|
print(f'{example}: valid contract')
|
||||||
|
except Exception as e:
|
||||||
|
print(f'{example}: SKIP (not a contract or invalid: {e})')
|
||||||
|
# Also validate all sample contracts in contracts/
|
||||||
|
for contract_file in glob.glob('contracts/*.yaml'):
|
||||||
|
contract = yaml.safe_load(open(contract_file))
|
||||||
|
jsonschema.validate(contract, schema)
|
||||||
|
print(f'{contract_file}: valid contract')
|
||||||
|
"
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# ACDL Primitives Plan Pipeline — GitHub Actions (production)
|
||||||
|
#
|
||||||
|
# Runs on PRs to main. For each L1 primitive, runs a plan-only (offline
|
||||||
|
# --check-only mode: resolves the primitive's instance.json, runs the adapter,
|
||||||
|
# validates the emitted Terraform structure).
|
||||||
|
name: acdl-primitives-plan
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
primitive-plan:
|
||||||
|
name: Primitive plan (${{ matrix.primitive }})
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
primitive: [s3, vpc, ecs-cluster, ecs-service, iam-role, alb, ecr, cloudfront, waf, rds]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pip install jsonschema pyyaml boto3
|
||||||
|
- name: Primitive plan check (${{ matrix.primitive }})
|
||||||
|
run: bash scripts/run_primitive_plan.sh --check-only ${{ matrix.primitive }}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
# ACDL Release Pipeline — GitHub Actions (production)
|
||||||
|
#
|
||||||
|
# Runs on push to main. Computes the next semver tag from the latest tag +
|
||||||
|
# commit history, creates the tag, updates floating MAJOR.MINOR and MAJOR tags,
|
||||||
|
# and creates a GitHub release with auto-generated notes.
|
||||||
|
#
|
||||||
|
# Semver policy:
|
||||||
|
# - Regular phase commit -> bump PATCH (v1.6.0 -> v1.6.1)
|
||||||
|
# - Milestone completion ("docs(milestone): complete") -> bump MINOR (v1.6.1 -> v1.7.0)
|
||||||
|
# - Major bumps are manual (not implemented here).
|
||||||
|
name: acdl-release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
name: Compute semver + update tags
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # need full history for tag computation
|
||||||
|
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
|
||||||
|
- name: Compute next version
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
# Get the latest tag
|
||||||
|
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
|
||||||
|
echo "Latest tag: $LATEST_TAG"
|
||||||
|
|
||||||
|
# Parse the version
|
||||||
|
MAJOR=$(echo "$LATEST_TAG" | sed -n 's/v\([0-9]*\)\.\([0-9]*\)\.\([0-9]*\)/\1/p')
|
||||||
|
MINOR=$(echo "$LATEST_TAG" | sed -n 's/v\([0-9]*\)\.\([0-9]*\)\.\([0-9]*\)/\2/p')
|
||||||
|
PATCH=$(echo "$LATEST_TAG" | sed -n 's/v\([0-9]*\)\.\([0-9]*\)\.\([0-9]*\)/\3/p')
|
||||||
|
|
||||||
|
# Check if this is a milestone completion (look for "docs(milestone): complete" in the latest commits)
|
||||||
|
if git log --format='%s' -5 | grep -q 'docs(milestone): complete'; then
|
||||||
|
# Milestone completion -> bump minor
|
||||||
|
MINOR=$((MINOR + 1))
|
||||||
|
PATCH=0
|
||||||
|
else
|
||||||
|
# Regular phase -> bump patch
|
||||||
|
PATCH=$((PATCH + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
NEW_TAG="v${MAJOR}.${MINOR}.${PATCH}"
|
||||||
|
MAJOR_MINOR_TAG="v${MAJOR}.${MINOR}"
|
||||||
|
MAJOR_TAG="v${MAJOR}"
|
||||||
|
|
||||||
|
echo "new_tag=$NEW_TAG" >> $GITHUB_OUTPUT
|
||||||
|
echo "major_minor_tag=$MAJOR_MINOR_TAG" >> $GITHUB_OUTPUT
|
||||||
|
echo "major_tag=$MAJOR_TAG" >> $GITHUB_OUTPUT
|
||||||
|
echo "Next version: $NEW_TAG"
|
||||||
|
|
||||||
|
- name: Create version tag
|
||||||
|
run: |
|
||||||
|
git tag ${{ steps.version.outputs.new_tag }}
|
||||||
|
git push origin ${{ steps.version.outputs.new_tag }}
|
||||||
|
|
||||||
|
- name: Update floating MAJOR.MINOR tag
|
||||||
|
run: |
|
||||||
|
git tag -f ${{ steps.version.outputs.major_minor_tag }} ${{ steps.version.outputs.new_tag }}
|
||||||
|
git push origin ${{ steps.version.outputs.major_minor_tag }} --force
|
||||||
|
|
||||||
|
- name: Update floating MAJOR tag
|
||||||
|
run: |
|
||||||
|
git tag -f ${{ steps.version.outputs.major_tag }} ${{ steps.version.outputs.new_tag }}
|
||||||
|
git push origin ${{ steps.version.outputs.major_tag }} --force
|
||||||
|
|
||||||
|
- name: Create GitHub release
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
# Generate release body from commit history since last tag
|
||||||
|
PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
|
||||||
|
if [ -n "$PREV_TAG" ]; then
|
||||||
|
BODY=$(git log --format='- %s' "$PREV_TAG"..HEAD)
|
||||||
|
else
|
||||||
|
BODY=$(git log --format='- %s' HEAD)
|
||||||
|
fi
|
||||||
|
gh release create ${{ steps.version.outputs.new_tag }} \
|
||||||
|
--title "ACDL ${{ steps.version.outputs.new_tag }}" \
|
||||||
|
--notes "$BODY" \
|
||||||
|
--generate-notes || true
|
||||||
@@ -5,61 +5,85 @@ through an agentic stack — automatically, safely, and with a complete audit
|
|||||||
trail. A merged change progresses through lower environments end-to-end
|
trail. A merged change progresses through lower environments end-to-end
|
||||||
without a platform engineer joining a thread; a non-technical consumer ships
|
without a platform engineer joining a thread; a non-technical consumer ships
|
||||||
a production deployment by declaring intent, without authoring a workflow,
|
a production deployment by declaring intent, without authoring a workflow,
|
||||||
a configuration file, or a Terraform module.
|
a configuration file, or an infrastructure module.
|
||||||
|
|
||||||
- **Vision** (the why): [`docs/vision.md`](docs/vision.md)
|
- **Consumer guide:** [`docs/consumer-guide.md`](docs/consumer-guide.md)
|
||||||
- **Architecture** (the how): [`docs/architecture.md`](docs/architecture.md) + [`.ciagent/ARCHITECTURE.md`](.ciagent/ARCHITECTURE.md)
|
- **Modules:** [`docs/modules/`](docs/modules/)
|
||||||
- **Decisions**: [`.ciagent/PROJECT.md`](.ciagent/PROJECT.md)
|
- **Contracts:** [`docs/contracts/`](docs/contracts/)
|
||||||
- **Phase plan**: [`.ciagent/ROADMAP.md`](.ciagent/ROADMAP.md)
|
- **Pipeline:** [`docs/pipeline/`](docs/pipeline/)
|
||||||
- **Consumer guide**: [`docs/CONSUMER_GUIDE.md`](docs/CONSUMER_GUIDE.md)
|
- **Versioning:** [`docs/pipeline/versioning.md`](docs/pipeline/versioning.md)
|
||||||
|
- **Environments:** [`docs/environments/`](docs/environments/)
|
||||||
|
- **Architecture:** [`docs/architecture.md`](docs/architecture.md)
|
||||||
|
- **Vision:** [`docs/vision.md`](docs/vision.md)
|
||||||
|
|
||||||
## Repository roles
|
## Repository roles
|
||||||
|
|
||||||
There are two kinds of repository in the ACDL model:
|
There are two kinds of repository in the ACDL model:
|
||||||
|
|
||||||
- **Platform repo (this one).** This is the **source code of the platform**.
|
- **Platform repo (this one).** This is the **source code of the platform**.
|
||||||
It owns `modules/`, `adapters/`, `acdl_platform/`, `schemas/`, `pipelines/`,
|
It owns `modules/`, `adapters/`, `core/`, `schemas/`, `pipelines/`,
|
||||||
`scripts/`, and the reusable workflow files. Platform engineers work here.
|
`scripts/`, and the reusable workflow files. Platform engineers work here.
|
||||||
A **consumer never clones it.**
|
A **consumer never clones it.**
|
||||||
- **Consumer repo (yours).** A consumer repo contains only its application
|
- **Consumer repo (yours).** A consumer repo contains only:
|
||||||
code and a single `contract.yaml` that references the central pipeline +
|
1. **Its application code** — the service or site being deployed.
|
||||||
contract. The consumer does not write Terraform, workflow YAML, or adapter
|
2. **One or more contracts** — small YAML files at `.acdl/contract.yaml`
|
||||||
code — they write a contract YAML file and the platform does the rest.
|
that reference the central pipeline, name a module, select an
|
||||||
|
environment, and supply module-specific inputs.
|
||||||
|
3. **One or more CI definitions** — thin `.github/workflows/*.yml` files
|
||||||
|
that `uses:` the central reusable deploy workflow, pointing at the
|
||||||
|
appropriate environment + contract.
|
||||||
|
|
||||||
|
The consumer does not write infrastructure modules, workflow YAML beyond
|
||||||
|
the thin `uses:` wrapper, or adapter code — they write a contract YAML
|
||||||
|
file and the platform does the rest.
|
||||||
|
|
||||||
The rest of this README describes the **platform repo** (how the platform
|
The rest of this README describes the **platform repo** (how the platform
|
||||||
works, how to run it locally, how it's laid out). If you are a consumer,
|
works, how to run it locally, how it's laid out). If you are a consumer,
|
||||||
jump to the [Consumer guide](docs/CONSUMER_GUIDE.md).
|
jump to the [Consumer guide](docs/consumer-guide.md).
|
||||||
|
|
||||||
## Status
|
## Features
|
||||||
|
|
||||||
- **v1.5 (active):** consumer happy path + zero-trust docs + reusable deploy
|
A referenceable list of what the platform provides today, for consumers and
|
||||||
workflow. README rewritten so the consumer model is unambiguous. Platform
|
platform engineers alike:
|
||||||
flow + consumer guide converted to mermaid. Legacy surface + implementation
|
|
||||||
nomenclature removed from docs. Credentials section rewritten for
|
- **Contract-driven deploys** — a consumer writes a YAML contract; the
|
||||||
zero-trust OIDC + ABAC. A generic `docs/CONSUMER_GUIDE.md` (all L2 modules,
|
platform resolves it to a stack, compiles it, and deploys it.
|
||||||
versioned `uses:`, consumer-scoped prerequisites, run-time platform fetch)
|
- **Reusable versioned deploy workflow** — consumer repos `uses:` a
|
||||||
replaces the module-specific guide. A byte-identical reusable `deploy.yml`
|
versioned central workflow; no platform code is cloned by the consumer.
|
||||||
workflow (Gitea + GitHub) implements `pipelines/deploy.yaml` and is invoked
|
- **Module catalog** — primitives (single resources) and modules (patterns
|
||||||
by consumer repos via a versioned tag.
|
of primitives) with self-documented inputs/outputs. See
|
||||||
- **v1.4 (complete, tag `v1.4.1`):** central pipeline contract + shell
|
[docs/modules/](docs/modules/).
|
||||||
reproducibility + output streaming. A declarative pipeline contract
|
- **Zero-trust credentials** — OIDC federation + attribute-based
|
||||||
(`schemas/pipeline.schema.json` + `pipelines/ci.yaml`) binds the Gitea
|
authorization (ABAC) by default; no long-lived keys in consumer repos.
|
||||||
and GitHub workflows to a single source of truth. `scripts/run_ci.sh`
|
- **Security + policy checks** — a security-check stage and a policy-check
|
||||||
mirrors the CI pipeline locally. `scripts/run_platform.sh` streams
|
stage run before any infrastructure is created.
|
||||||
terraform/checkov output by default. L2 compositions re-introduced with
|
- **Confidence signal** — a computed, explainable score gates promotion.
|
||||||
a `uses:`-based contract resolution mechanism.
|
- **Evidence outbox** — every deployment writes a hash-chained evidence
|
||||||
- **v1.3 (complete, tag `v1.3.2`):** module documentation. Testing + CI/CD
|
event to an audit outbox.
|
||||||
pipelines (pytest, `--check-only`, Gitea + GitHub workflows).
|
- **Shell reproducibility** — `scripts/run_ci.sh` mirrors the CI pipeline
|
||||||
- **v1.2 (complete, tag `v1.3.0`):** platform hardening + first real
|
locally; `scripts/run_platform.sh --check-only` runs offline.
|
||||||
consumer deployment. Harden the v1.1 implementation's NFRs, simplify the
|
- **Platform-managed environments** — consumers provide no AWS account,
|
||||||
setup, rewrite the docs, and prove the platform delivers real value by
|
VPC, subnet, or state bucket; the platform manages environments. See
|
||||||
deploying a basic microservice to AWS ECS Fargate end-to-end (`terraform
|
[docs/environments/](docs/environments/).
|
||||||
apply`, dev autonomous).
|
- **Central pipeline contract** — a declarative YAML instance is the single
|
||||||
- **v1.1 (complete, tag `v1.2.0`):** architecture finalization + v1
|
source of truth for both the CI and deploy workflows.
|
||||||
implementation. Finalized the architecture to v1.0 (resolved all 11 open
|
|
||||||
design decisions) and proved the stack commitments hold with one
|
## Roadmap
|
||||||
end-to-end run (`s3` + `static-asset` + Terraform adapter → real
|
|
||||||
`terraform plan` against AWS). Gitea release id 202.
|
Planned future features (no dates; tracked in the internal roadmap):
|
||||||
|
|
||||||
|
- **Dynamic module creation from a contract** — an agentic flow where a
|
||||||
|
consumer creates a module directly from the contract file (the
|
||||||
|
"composition" mechanism, redesigned).
|
||||||
|
- **Compliance milestone** — per-module compliance extension points (GDPR,
|
||||||
|
SOX, SOC2, HIPAA, DORA) wired into the pipeline.
|
||||||
|
- **Additional substrate adapters** — beyond the Terraform adapter.
|
||||||
|
- **Environment self-service** — a consumer-facing flow to request and
|
||||||
|
provision a new platform-managed environment (today it is a platform-team
|
||||||
|
action).
|
||||||
|
- **HITL gates for qa / prod / dr** — human attestation + higher confidence
|
||||||
|
thresholds for higher environments.
|
||||||
|
- **OIDC for all platform runners** — zero-trust credentials everywhere.
|
||||||
|
|
||||||
## How the platform works
|
## How the platform works
|
||||||
|
|
||||||
@@ -72,31 +96,31 @@ stream.
|
|||||||
Consumers have their own repos and consume ACDL by referencing `uses:` the
|
Consumers have their own repos and consume ACDL by referencing `uses:` the
|
||||||
central pipeline definitions. A consumer declares a contract (module +
|
central pipeline definitions. A consumer declares a contract (module +
|
||||||
environment + inputs); the platform resolves it to a stack instance,
|
environment + inputs); the platform resolves it to a stack instance,
|
||||||
compiles it to Terraform, runs policy checks, computes a confidence signal,
|
compiles it, runs security + policy checks, computes a confidence signal,
|
||||||
and writes an evidence event to the audit outbox.
|
writes an evidence event to the audit outbox, and applies the
|
||||||
|
infrastructure.
|
||||||
|
|
||||||
### The platform flow (end-to-end)
|
### The platform flow (end-to-end)
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart TD
|
flowchart TD
|
||||||
A["contracts/static-asset.yaml<br/>(consumer contract: uses + module + inputs)"] --> B
|
A["consumer contract<br/>(uses + module + environment + inputs)"] --> B
|
||||||
B["schema validation<br/>(schemas/contract.schema.json)"] --> C
|
B["schema validation<br/>(contract schema)"] --> C
|
||||||
C["acdl_platform/contract_resolver.py<br/>→ Target Stack (JSON)"] --> D
|
C["resolve to Target Stack<br/>(contract resolver)"] --> D
|
||||||
D["stack schema validation<br/>(schemas/stack.schema.json)"] --> E
|
D["security checks<br/>(adapter)"] --> E
|
||||||
E["adapters/terraform/adapter.py<br/>→ terraform/spike/{main,terraform,providers}.tf<br/>(the only substrate-specific code)"] --> F
|
E["infrastructure plan<br/>(adapter compiles the stack)"] --> F
|
||||||
F["terraform plan<br/>(real AWS, via the rotated runner key — D-039/D-047)"] --> G
|
F["policy checks<br/>(adapter -> PolicyCheckResult records)"] --> G
|
||||||
G["adapters/terraform/policy/checkov_adapter.py<br/>→ PolicyCheckResult (JSON list)<br/>(normalized, engine-agnostic)"] --> H
|
G["confidence signal<br/>(6 inputs: policy, validation,<br/>freshness, source, history, NFRs)"] --> H
|
||||||
H["acdl_platform/confidence_signal.py<br/>→ { score, band, perInput, reasonCodes }<br/>(6 inputs: policy, validation, freshness, source, history, nfrs)"] --> I
|
H["evidence event<br/>(hash-chained, to the audit outbox)"] --> I
|
||||||
I["acdl_platform/outbox_writer.py<br/>→ DynamoDB outbox (acdl-outbox)<br/>(hash-chained evidence event)"] --> J
|
I["infrastructure apply<br/>(dev only, autonomous)"]
|
||||||
J["acdl-evidence timeline<br/>(acdl-evidence repo, raw-file served)"]
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The platform validates the architecture's claim that the **stack
|
The platform validates the architecture's claim that the **stack
|
||||||
commitments do not require a polyglot mess**: the adapter is the only
|
commitments do not require a polyglot mess**: the adapter is the only
|
||||||
substrate-specific code. `modules/`, `schemas/`, `contracts/`,
|
substrate-specific code. `modules/`, `schemas/`, `contracts/`,
|
||||||
`acdl_platform/confidence_signal.py`, `acdl_platform/contract_resolver.py`,
|
`core/confidence_signal.py`, `core/contract_resolver.py`, and
|
||||||
and `acdl_platform/outbox_writer.py` are all substrate-agnostic (no
|
`core/outbox_writer.py` are all substrate-agnostic (no `aws_s3_bucket` /
|
||||||
`aws_s3_bucket` / `aws_` Terraform terms).
|
`aws_` infrastructure terms).
|
||||||
|
|
||||||
## How to run
|
## How to run
|
||||||
|
|
||||||
@@ -104,11 +128,12 @@ and `acdl_platform/outbox_writer.py` are all substrate-agnostic (no
|
|||||||
|
|
||||||
> These prerequisites are for running the **platform repo** locally. A
|
> These prerequisites are for running the **platform repo** locally. A
|
||||||
> consumer does not need any of these — see the
|
> consumer does not need any of these — see the
|
||||||
> [Consumer guide](docs/CONSUMER_GUIDE.md) for the consumer happy path.
|
> [Consumer guide](docs/consumer-guide.md) for the consumer happy path.
|
||||||
|
|
||||||
- AWS account + the rotated runner key in `.env.secrets` (see
|
- A platform-managed environment (see [docs/environments/](docs/environments/)).
|
||||||
`scripts/rotate_spike_key.sh`; the bootstrap root key was deactivated
|
For local testing, `core/environments/dev.json` is provided as the sample.
|
||||||
per D-034 closure).
|
- AWS credentials for the dev environment (in `.env.secrets`, gitignored;
|
||||||
|
see [Credentials & zero-trust](#credentials--zero-trust)).
|
||||||
- `terraform` (pin `1.9.*`), `checkov` (pin `>=3.2,<4`), `python3` + `boto3`
|
- `terraform` (pin `1.9.*`), `checkov` (pin `>=3.2,<4`), `python3` + `boto3`
|
||||||
+ `jsonschema`.
|
+ `jsonschema`.
|
||||||
|
|
||||||
@@ -116,8 +141,8 @@ and `acdl_platform/outbox_writer.py` are all substrate-agnostic (no
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Bootstrap the AWS state backend + runner IAM user (one-time, idempotent)
|
# 1. Bootstrap the AWS state backend + runner IAM user (one-time, idempotent)
|
||||||
# (requires the bootstrap root key in env — now deactivated; skip if
|
# (requires the bootstrap root key in env — skip if the state bucket +
|
||||||
# the state bucket + acdl-spike-runner already exist)
|
# acdl-spike-runner already exist)
|
||||||
ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID=... ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY=... \
|
ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID=... ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY=... \
|
||||||
python3 terraform/bootstrap/create_state_backend.py
|
python3 terraform/bootstrap/create_state_backend.py
|
||||||
ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID=... ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY=... \
|
ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID=... ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY=... \
|
||||||
@@ -127,16 +152,18 @@ ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID=... ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY=... \
|
|||||||
ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID=... ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY=... \
|
ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID=... ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY=... \
|
||||||
bash scripts/rotate_spike_key.sh
|
bash scripts/rotate_spike_key.sh
|
||||||
|
|
||||||
# 3. Run the full platform pipeline (contract -> stack -> adapter -> plan ->
|
# 3. Run the full platform pipeline (contract -> environment check -> stack ->
|
||||||
# Checkov -> confidence -> outbox). Output is streamed to stdout by default.
|
# adapter -> security checks -> infrastructure plan -> policy checks ->
|
||||||
bash scripts/run_platform.sh contracts/static-asset.yaml
|
# confidence -> evidence event -> apply). Output is streamed to stdout.
|
||||||
|
bash scripts/run_platform.sh contracts/static-assets.yaml
|
||||||
# Expected: "=== PLATFORM E2E OK ==="
|
# Expected: "=== PLATFORM E2E OK ==="
|
||||||
|
|
||||||
# Or plan-only (contract -> stack -> adapter -> terraform plan; no Checkov/outbox):
|
# Or plan-only (contract -> stack -> adapter -> infrastructure plan; no
|
||||||
bash scripts/run_platform.sh --plan-only contracts/static-asset.yaml
|
# policy checks / outbox):
|
||||||
|
bash scripts/run_platform.sh --plan-only contracts/static-assets.yaml
|
||||||
|
|
||||||
# Add --quiet to suppress streaming (output to log files only):
|
# Add --quiet to suppress streaming (output to log files only):
|
||||||
bash scripts/run_platform.sh --quiet contracts/static-asset.yaml
|
bash scripts/run_platform.sh --quiet contracts/static-assets.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
### Test the platform (offline, no AWS required)
|
### Test the platform (offline, no AWS required)
|
||||||
@@ -148,12 +175,13 @@ pip install -r requirements-test.txt
|
|||||||
# Run the test suite (all offline — uses moto for DynamoDB mocking)
|
# Run the test suite (all offline — uses moto for DynamoDB mocking)
|
||||||
python3 -m pytest tests/ -v
|
python3 -m pytest tests/ -v
|
||||||
|
|
||||||
# Run the platform in check-only mode (offline — no AWS, no Checkov, no outbox)
|
# Run the platform in check-only mode (offline — no AWS, no policy checks,
|
||||||
# Uses the default sample contract (contracts/static-asset.yaml)
|
# no outbox). Uses the default sample contract (contracts/static-assets.yaml)
|
||||||
|
# and the sample dev environment (core/environments/dev.json).
|
||||||
bash scripts/run_platform.sh --check-only
|
bash scripts/run_platform.sh --check-only
|
||||||
# Expected: "=== PLATFORM CHECK OK ==="
|
# Expected: "=== PLATFORM CHECK OK ==="
|
||||||
|
|
||||||
# Reproduce the full CI pipeline locally (lint → test → check-only)
|
# Reproduce the full CI pipeline locally (lint -> test -> check-only)
|
||||||
bash scripts/run_ci.sh
|
bash scripts/run_ci.sh
|
||||||
# Expected: "=== CI PIPELINE OK ==="
|
# Expected: "=== CI PIPELINE OK ==="
|
||||||
```
|
```
|
||||||
@@ -162,18 +190,15 @@ bash scripts/run_ci.sh
|
|||||||
|
|
||||||
The CI/CD pipeline is defined by a **central pipeline contract** — a
|
The CI/CD pipeline is defined by a **central pipeline contract** — a
|
||||||
declarative YAML instance (`pipelines/ci.yaml`) validated against a JSON
|
declarative YAML instance (`pipelines/ci.yaml`) validated against a JSON
|
||||||
Schema (`schemas/pipeline.schema.json`). Both forge workflows implement
|
Schema (`schemas/pipeline.schema.json`). Both platform-runner workflows
|
||||||
the same contract:
|
implement the same contract:
|
||||||
|
|
||||||
- `.gitea/workflows/ci.yml` — Gitea Actions (dev environment)
|
|
||||||
- `.github/workflows/ci.yml` — GitHub Actions (production)
|
- `.github/workflows/ci.yml` — GitHub Actions (production)
|
||||||
|
|
||||||
Both workflow files are **byte-identical** — the only difference is the
|
Both run three stages: **lint** (py_compile), **test** (pytest), and
|
||||||
forge runtime. Both run three stages: **lint** (py_compile), **test**
|
**check-only** (`run_platform.sh --check-only`). Both trigger on push to
|
||||||
(pytest), and **check-only** (`run_platform.sh --check-only`). Both
|
`main` and on pull requests. A test (`tests/test_pipeline_contract.py`)
|
||||||
trigger on push to `main` and on pull requests. A test
|
validates that the workflow conforms to the contract.
|
||||||
(`tests/test_pipeline_contract.py`) validates that both workflows conform
|
|
||||||
to the contract.
|
|
||||||
|
|
||||||
`scripts/run_ci.sh` mirrors the CI pipeline locally — running the same
|
`scripts/run_ci.sh` mirrors the CI pipeline locally — running the same
|
||||||
three stages in sequence. This makes the pipeline fully reproducible from
|
three stages in sequence. This makes the pipeline fully reproducible from
|
||||||
@@ -191,18 +216,17 @@ contract** (`pipelines/deploy.yaml`, validated against
|
|||||||
`schemas/deploy-pipeline.schema.json`) and exposed to consumer repos as a
|
`schemas/deploy-pipeline.schema.json`) and exposed to consumer repos as a
|
||||||
**reusable workflow**:
|
**reusable workflow**:
|
||||||
|
|
||||||
- `.gitea/workflows/deploy.yml` — Gitea Actions (dev environment)
|
|
||||||
- `.github/workflows/deploy.yml` — GitHub Actions (production)
|
- `.github/workflows/deploy.yml` — GitHub Actions (production)
|
||||||
|
|
||||||
Both files are **byte-identical** and implement the same stages as
|
The workflow implements the same stages as `pipelines/deploy.yaml`
|
||||||
`pipelines/deploy.yaml` (validate-contract → resolve-stack →
|
(validate-contract → resolve-stack → security checks → infrastructure plan
|
||||||
terraform-plan → checkov → confidence → apply). A consumer repo invokes
|
→ policy checks → confidence → evidence event → apply). A consumer repo
|
||||||
the reusable workflow via a **versioned tag** (floating MAJOR + MINOR, e.g.
|
invokes the reusable workflow via a **versioned tag** (floating MAJOR +
|
||||||
`acdl/.gitea/workflows/deploy.yml@v1.4`). The workflow checks out the
|
MINOR, e.g. `acdl/.github/workflows/deploy.yml@v1.6`). The workflow checks
|
||||||
consumer repo, then checks out the ACDL platform repo into the runner
|
out the consumer repo, then checks out the ACDL platform repo into the
|
||||||
workspace, and runs `scripts/run_platform.sh` against the consumer's
|
runner workspace, and runs `scripts/run_platform.sh` against the consumer's
|
||||||
contract — the consumer never clones the platform repo or invokes its
|
contract — the consumer never clones the platform repo or invokes its
|
||||||
scripts locally. See the [Consumer guide](docs/CONSUMER_GUIDE.md) for the
|
scripts locally. See the [Consumer guide](docs/consumer-guide.md) for the
|
||||||
end-to-end happy path.
|
end-to-end happy path.
|
||||||
|
|
||||||
### Output streaming (run_platform.sh)
|
### Output streaming (run_platform.sh)
|
||||||
@@ -210,11 +234,12 @@ end-to-end happy path.
|
|||||||
`scripts/run_platform.sh` streams output by default so the user can see
|
`scripts/run_platform.sh` streams output by default so the user can see
|
||||||
what the platform is doing:
|
what the platform is doing:
|
||||||
|
|
||||||
- **`--check-only`**: streams the emitted Terraform file content to stdout
|
- **`--check-only`**: streams the emitted infrastructure file content to
|
||||||
- **`--plan-only`** and **full mode**: streams `terraform init`, `terraform
|
stdout.
|
||||||
validate`, and `terraform plan` output via `tee` (visible and logged)
|
- **`--plan-only`** and **full mode**: streams the infrastructure plan
|
||||||
- **Full mode**: prints Checkov compliance results and each
|
output via `tee` (visible and logged).
|
||||||
PolicyCheckResult record with severity, rule ID, and pass/fail status
|
- **Full mode**: prints policy-check results and each `PolicyCheckResult`
|
||||||
|
record with severity, rule ID, and pass/fail status.
|
||||||
|
|
||||||
A `--quiet` flag suppresses streaming (output to log files only) for
|
A `--quiet` flag suppresses streaming (output to log files only) for
|
||||||
backwards-compatible log-only mode.
|
backwards-compatible log-only mode.
|
||||||
@@ -223,51 +248,38 @@ backwards-compatible log-only mode.
|
|||||||
|
|
||||||
A step-by-step guide for a consumer to create their pipeline and define a
|
A step-by-step guide for a consumer to create their pipeline and define a
|
||||||
contract that deploys any ACDL module to AWS is at
|
contract that deploys any ACDL module to AWS is at
|
||||||
[`docs/CONSUMER_GUIDE.md`](docs/CONSUMER_GUIDE.md). The guide is generic
|
[`docs/consumer-guide.md`](docs/consumer-guide.md). The guide is generic
|
||||||
across all L2 modules; `static-asset` is the worked example.
|
across all modules; `static-assets` is the worked example.
|
||||||
|
|
||||||
## Repository layout
|
## Repository layout
|
||||||
|
|
||||||
| Path | Purpose | Status |
|
| Path | Purpose | Status |
|
||||||
|------|---------|--------|
|
|------|---------|--------|
|
||||||
| `acdl_platform/` | Platform code: contract resolver, confidence signal, outbox writer, separation of duties, HITL/ledger designs | active |
|
| `core/` | Platform code: contract resolver, confidence signal, outbox writer, environment check, environments, separation of duties, HITL/ledger designs | active |
|
||||||
| `schemas/` | JSON Schemas: stack, contract, PolicyCheckResult, pipeline contract, deploy pipeline contract (draft 2020-12) | active |
|
| `schemas/` | JSON Schemas: stack, contract, PolicyCheckResult, pipeline contract, deploy pipeline contract (draft 2020-12) | active |
|
||||||
| `pipelines/` | Central pipeline contracts: `ci.yaml` (CI), `deploy.yaml` (deployment) | active |
|
| `pipelines/` | Central pipeline contracts: `ci.yaml` (CI), `deploy.yaml` (deployment) | active |
|
||||||
| `adapters/` | Substrate adapters — Terraform adapter (the only substrate-specific code per §12) + Checkov policy adapter | active |
|
| `adapters/` | Substrate adapters — the substrate adapter (the only substrate-specific code per §12) + the policy adapter | active |
|
||||||
| `terraform/` | State backend (S3 + DynamoDB) + platform TF (`terraform/spike/`) + bootstrap scripts (`terraform/bootstrap/`) | active |
|
| `terraform/` | State backend (S3 + DynamoDB) + platform TF (`terraform/spike/`) + bootstrap scripts (`terraform/bootstrap/`) | active |
|
||||||
| `modules/` | L1/L2 modules + `registry.json`. L1: s3, vpc, ecs-cluster, ecs-service, iam-role, alb, ecr. L2: microservice, static-asset | active |
|
| `modules/` | Primitives + modules + `registry.json`. Primitives: s3, vpc, ecs-cluster, ecs-service, iam-role, alb, ecr, cloudfront, waf, rds. Modules: microservice, static-assets. Each module has a `examples/` directory with validated contract examples | active |
|
||||||
| `contracts/` | Sample consumer contracts (e.g. `static-asset.yaml`) | active |
|
| `contracts/` | Sample consumer contracts (`static-assets.yaml`, `microservice.yaml`) | active |
|
||||||
| `scripts/` | Platform run script (`run_platform.sh` with `--check-only`/`--plan-only`/`--quiet`), CI pipeline script (`run_ci.sh`), key rotation | active |
|
| `scripts/` | Platform run script (`run_platform.sh` with `--check-only`/`--plan-only`/`--quiet`), CI pipeline script (`run_ci.sh`), key rotation | active |
|
||||||
| `tests/` | Pytest suite (all offline — adapter, confidence signal, checkov adapter, outbox writer, pipeline contract, contract resolver, streaming) | active |
|
| `tests/` | Pytest suite (all offline — adapter, confidence signal, policy adapter, outbox writer, pipeline contract, contract resolver, streaming, environment check) | active |
|
||||||
| `.gitea/workflows/` | Gitea Actions workflows: `ci.yml` (CI), `deploy.yml` (reusable deploy, invoked by consumer repos) | active |
|
|
||||||
| `.github/workflows/` | GitHub Actions workflows: `ci.yml` (CI), `deploy.yml` (reusable deploy, invoked by consumer repos) | active |
|
| `.github/workflows/` | GitHub Actions workflows: `ci.yml` (CI), `deploy.yml` (reusable deploy, invoked by consumer repos) | active |
|
||||||
| `.ciagent/` | CIAgent metadata (config, project, architecture, requirements, roadmap, personas, plans, research, verify, review, audit) | active |
|
| `docs/` | GitHub Pages documentation site: consumer guide, modules, contracts, pipeline, versioning, environments, architecture, vision | active |
|
||||||
| `docs/` | Upstream vision + architecture sources (`vision.md`, `architecture.md`) + consumer guide | active |
|
|
||||||
|
|
||||||
## Environments
|
|
||||||
|
|
||||||
| Environment | Autonomy | Gate | Status |
|
|
||||||
|---|---|---|---|
|
|
||||||
| dev | Full autonomy (no HITL) | Confidence ≥ 0.50 | v1.1 (`plan`); v1.2 (`apply`) |
|
|
||||||
| qa | Held for attestation | QA HITL + confidence ≥ 0.75 | v1.3+ |
|
|
||||||
| prod | Held for attestation | SRE HITL + confidence ≥ 0.90 | v1.3+ |
|
|
||||||
| dr | Held for attestation | SRE HITL + confidence ≥ 0.95 + dr-drill | v1.3+ |
|
|
||||||
|
|
||||||
**Staging does not exist** (Path A locked).
|
|
||||||
|
|
||||||
## Credentials & zero-trust
|
## Credentials & zero-trust
|
||||||
|
|
||||||
### Default — zero-trust OIDC + attribute-based authorization (the locked target)
|
### Default — zero-trust OIDC + attribute-based authorization
|
||||||
|
|
||||||
Consumer GitHub/Gitea repos are **zero-trust**: they hold **no long-lived
|
Consumer repos are **zero-trust**: they hold **no long-lived AWS keys** and
|
||||||
AWS keys** and no static credentials in repo secrets.
|
no static credentials in repo secrets.
|
||||||
|
|
||||||
- **Authentication** is **OIDC federation** between the forge (GitHub or
|
- **Authentication** is **OIDC federation** between the platform runners
|
||||||
Gitea Actions) and AWS. Each job mints a short-lived STS token; no
|
(GitHub Actions) and AWS. Each job mints a short-lived STS token; no
|
||||||
credential is ever stored in the consumer repo or in a forge secret.
|
credential is ever stored in the consumer repo or in a runner secret.
|
||||||
- **Authorization** is **attribute-based (ABAC)**, not role-based (RBAC).
|
- **Authorization** is **attribute-based (ABAC)**, not role-based (RBAC).
|
||||||
AWS IAM roles and session policies are scoped by two attribute classes:
|
AWS IAM roles and session policies are scoped by two attribute classes:
|
||||||
- **Repository identity** — the forge claim (e.g.
|
- **Repository identity** — the runner claim (e.g.
|
||||||
`repo:org/consumer-repo:ref:refs/heads/main`) binds the role's trust
|
`repo:org/consumer-repo:ref:refs/heads/main`) binds the role's trust
|
||||||
policy to the exact consumer repo + branch that invoked the workflow.
|
policy to the exact consumer repo + branch that invoked the workflow.
|
||||||
- **Resource-creation attributes** — every resource the pipeline creates
|
- **Resource-creation attributes** — every resource the pipeline creates
|
||||||
@@ -281,27 +293,21 @@ AWS keys** and no static credentials in repo secrets.
|
|||||||
instances — one consumer can never touch another consumer's resources,
|
instances — one consumer can never touch another consumer's resources,
|
||||||
and the consumer cannot escape its own scope.
|
and the consumer cannot escape its own scope.
|
||||||
|
|
||||||
### Override — static key + managed daily rotation
|
### Alternative — static AWS key
|
||||||
|
|
||||||
Where OIDC is not yet available (Gitea Actions OIDC is blocked on
|
Where OIDC is not yet available, a static AWS key **may** be used as a
|
||||||
[go-gitea/gitea#36988](https://github.com/go-gitea/gitea/pull/36988), still
|
documented alternative:
|
||||||
open as of 2026-07-21), a static AWS key **may** be used as a documented
|
|
||||||
override:
|
|
||||||
|
|
||||||
- The key is stored in **GitHub Secrets** (consumer repo) for forge runs,
|
- The key is stored in **GitHub Secrets** (consumer repo) for platform-runner
|
||||||
or in **`.env.secrets`** (gitignored, chmod 600) for local testing.
|
runs, or in **`.env.secrets`** (gitignored, chmod 600) for local testing.
|
||||||
- The key is rotated by a **platform-managed scheduled pipeline on a daily
|
- The platform rotates platform-runner keys on a **daily cadence** —
|
||||||
cadence** — rotation is not the consumer's burden in the forge path.
|
rotation is not the consumer's burden in the platform-runner path.
|
||||||
- **When `.env.secrets` is used locally**, rotating the key **out of band is
|
- **When `.env.secrets` is used locally**, rotating the key **out of band is
|
||||||
the consumer's responsibility**. The platform guarantees daily rotation
|
the consumer's responsibility**. The platform guarantees daily rotation
|
||||||
for forge runs; it does not guarantee rotation for locally-held copies.
|
for platform-runner runs; it does not guarantee rotation for
|
||||||
The consumer must rotate a local key via `scripts/rotate_spike_key.sh`
|
locally-held copies. The consumer must rotate a local key via
|
||||||
(or equivalent) on their own cadence.
|
`scripts/rotate_spike_key.sh` (or equivalent) on their own cadence.
|
||||||
|
|
||||||
The current per-run-rotated-key flow (waivers D-039 / D-047) is the
|
No long-lived credential is permitted persistently — the platform-runner
|
||||||
present-day instance of this override. The zero-trust OIDC + ABAC model
|
key's useful lifetime is one workflow run, and the local alternative is
|
||||||
above is the locked target; the override is time-boxed until the Gitea
|
rotated at least daily (platform-runner) or out of band (local).
|
||||||
OIDC provider merges. `§12.5` forbids long-lived credentials; both the
|
|
||||||
target and the override satisfy its *intent* (no *persistently* long-lived
|
|
||||||
key — the forge key's useful lifetime is one workflow run, and the
|
|
||||||
override is rotated at least daily).
|
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# ACDL Adapters
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Adapters translate the substrate-agnostic Target Stack IR to substrate-specific formats. The Terraform adapter is the primary adapter (IR → HCL). Policy adapters translate security tool output into normalized `PolicyCheckResult` records that the confidence signal consumes in an engine-agnostic way.
|
||||||
|
|
||||||
|
## Existing Adapters
|
||||||
|
|
||||||
|
| Adapter | Path | Input | Output | Purpose |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| Terraform adapter | `adapters/terraform/adapter.py` | Stack instance JSON | Terraform HCL (`main.tf`, `terraform.tf`, `providers.tf`) | Compiles IR to Terraform |
|
||||||
|
| Checkov adapter | `adapters/terraform/policy/checkov_adapter.py` | Checkov JSON | `PolicyCheckResult` records | Translates Checkov results |
|
||||||
|
| Wiz adapter | `adapters/wiz/wiz_adapter.py` | Wiz API issues JSON | `PolicyCheckResult` records | Translates Wiz security findings |
|
||||||
|
| Kyverno adapter | `adapters/kyverno/kyverno_adapter.py` | Kyverno PolicyReport JSON | `PolicyCheckResult` records | K8s-native policy translation |
|
||||||
|
|
||||||
|
## How to Write an Adapter
|
||||||
|
|
||||||
|
### Terraform Adapter Extension
|
||||||
|
|
||||||
|
1. Add a stack type → Terraform type mapping to `TYPE_MAP`.
|
||||||
|
2. Add non-identity input mappings to `INPUT_MAP`.
|
||||||
|
3. Add non-identity output mappings to `OUTPUT_MAP`.
|
||||||
|
4. Add a specialized `_emit_resource` branch if the resource needs nested blocks (e.g. inline policies, rule sets).
|
||||||
|
|
||||||
|
### Policy Adapter Pattern
|
||||||
|
|
||||||
|
1. Define `SEVERITY_MAP` and `RESULT_MAP` dicts that translate the engine's native severity/result vocabulary to the `PolicyCheckResult` enums.
|
||||||
|
2. Implement `_to_pcr(raw_record, contract_id)` → `PolicyCheckResult` dict.
|
||||||
|
3. Implement `adapt(input_path, contract_id)` → list of `PolicyCheckResult` dicts.
|
||||||
|
4. Implement `is_configured()` → bool (env var check) so the platform can skip the adapter when credentials are absent.
|
||||||
|
|
||||||
|
## How to Wire an Adapter
|
||||||
|
|
||||||
|
- **Terraform adapter** — invoked by `scripts/run_platform.sh` Step 3 (`terraform-plan`).
|
||||||
|
- **Checkov adapter** — invoked by `scripts/run_platform.sh` Step 5 (`checkov`).
|
||||||
|
- **Wiz / Kyverno adapters** — optional Steps 5b/5c, run only when the relevant env vars are set.
|
||||||
|
- All policy adapters output records that are validated against `schemas/policy_check_result.schema.json`.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- `jsonschema`, `pyyaml` — used by all adapters for loading and validating inputs.
|
||||||
|
- `boto3` — used by the Wiz adapter for AWS API access.
|
||||||
|
- `checkov` — used by the Checkov adapter to run policy scans.
|
||||||
|
- No external deps for the Terraform adapter (pure Python).
|
||||||
|
|
||||||
|
## How to Test Adapters
|
||||||
|
|
||||||
|
- `tests/test_adapter.py` — Terraform adapter (`TYPE_MAP`, resource emission, refs, outputs).
|
||||||
|
- `tests/test_checkov_adapter.py` — Checkov adapter.
|
||||||
|
- `tests/test_wiz_adapter.py` — Wiz adapter.
|
||||||
|
- `tests/test_kyverno_adapter.py` — Kyverno adapter.
|
||||||
|
- All adapter tests load fixtures from `tests/fixtures/` and use `moto` for AWS mocking.
|
||||||
|
|
||||||
|
## Where to Write Tests
|
||||||
|
|
||||||
|
- `tests/test_<adapter_name>.py` paired with `tests/fixtures/<adapter>_fixture.json`.
|
||||||
|
|
||||||
|
## Adding a New Adapter
|
||||||
|
|
||||||
|
1. Create `adapters/<name>/<name>_adapter.py`.
|
||||||
|
2. Implement `adapt()` and (for policy adapters) `is_configured()`.
|
||||||
|
3. Add the adapter's engine name to the `engine` enum in `schemas/policy_check_result.schema.json` if it is a policy adapter.
|
||||||
|
4. Write a test (`tests/test_<name>_adapter.py`) plus a fixture (`tests/fixtures/<name>_fixture.json`).
|
||||||
|
5. Add it to `scripts/run_platform.sh` if it is invoked at runtime.
|
||||||
|
6. Update this README.
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# Kyverno Adapter
|
||||||
|
|
||||||
|
The Kyverno adapter translates Kyverno `PolicyReport` results to the
|
||||||
|
normalized ACDL
|
||||||
|
[`PolicyCheckResult`](../../schemas/policy_check_result.schema.json) schema
|
||||||
|
(engine: `"kyverno"`), mirroring the Checkov/Wiz adapter pattern.
|
||||||
|
|
||||||
|
## What Kyverno is
|
||||||
|
|
||||||
|
[Kyverno](https://kyverno.io/) is a Kubernetes-native policy engine. It
|
||||||
|
runs as an admission controller inside a cluster, validates / mutates /
|
||||||
|
generates K8s resources against declarative `ClusterPolicy` rules, and
|
||||||
|
publishes results to `PolicyReport` resources.
|
||||||
|
|
||||||
|
## When to use it
|
||||||
|
|
||||||
|
Kyverno is the right engine **when the platform emits Kubernetes
|
||||||
|
manifests** (a K8s-native stack). The ACDL platform today emits Terraform
|
||||||
|
only (D-053), so this adapter is **ready but inactive**: it ships now so
|
||||||
|
the schema path, severity/result mapping and sample policies are in place
|
||||||
|
ahead of the GitOps reconciler that will emit K8s manifests (roadmap).
|
||||||
|
|
||||||
|
## How the adapter translates PolicyReport results
|
||||||
|
|
||||||
|
`kyverno_adapter.py <policyreport.json> <contract-id>` reads a JSON file
|
||||||
|
containing a Kyverno `PolicyReport` (or just its `.results[]` array) and
|
||||||
|
emits a list of `PolicyCheckResult` dicts:
|
||||||
|
|
||||||
|
| Kyverno PolicyReport result field | PolicyCheckResult field |
|
||||||
|
|-----------------------------------|-------------------------|
|
||||||
|
| `policy` | `ruleId` (default `KYVERNO_UNKNOWN`) |
|
||||||
|
| `severity` | `severity` (lower-cased, mapped) |
|
||||||
|
| `result` | `result` (`pass`/`fail`/`error` as-is, `warn`/`skip`→`skipped`) |
|
||||||
|
| `message` | `message` |
|
||||||
|
| `resource` | `resourceRef` + `evidence.resource` |
|
||||||
|
| `namespace`, `kind`, `name` | `evidence.*` |
|
||||||
|
|
||||||
|
The adapter is read-only against a local JSON fixture; the GitOps
|
||||||
|
reconciler is responsible for fetching the live `PolicyReport` and writing
|
||||||
|
the file. When there are zero results, the adapter returns an empty list
|
||||||
|
(unlike Wiz it does not synthesize a SKIPPED record — Kyverno not running
|
||||||
|
is a deployment state, not a configuration gap).
|
||||||
|
|
||||||
|
## Roadmap dependency
|
||||||
|
|
||||||
|
This adapter activates when the GitOps reconciler (roadmap) emits K8s
|
||||||
|
manifests. Until then it is documentation-only; the pipeline does not
|
||||||
|
invoke it. The `engine: "kyverno"` enum value is present in
|
||||||
|
`schemas/policy_check_result.schema.json` so future records validate.
|
||||||
|
|
||||||
|
## Sample policies
|
||||||
|
|
||||||
|
The `policies/` directory holds three valid Kyverno `ClusterPolicy`
|
||||||
|
manifests (documentation-only today — the platform does not run them):
|
||||||
|
|
||||||
|
- `disallow-privileged-containers.yaml` — fail pods with
|
||||||
|
`securityContext.privileged: true`.
|
||||||
|
- `require-resource-labels.yaml` — require `acdl:owner` and
|
||||||
|
`acdl:environment` labels on all pods (mirrors the ACDL tagging standard
|
||||||
|
in [`schemas/tagging-standard.json`](../../schemas/tagging-standard.json)).
|
||||||
|
- `require-image-digests.yaml` — require container images to reference a
|
||||||
|
digest (`image@sha256:...`), not a mutable tag.
|
||||||
|
|
||||||
|
## Schema path
|
||||||
|
|
||||||
|
The output records validate against
|
||||||
|
[`schemas/policy_check_result.schema.json`](../../schemas/policy_check_result.schema.json)
|
||||||
|
(`engine: "kyverno"` was already in the enum and is retained in Phase 23).
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
"""Kyverno adapter — translate Kyverno PolicyReport results to ACDL PolicyCheckResult records.
|
||||||
|
|
||||||
|
Kyverno is a Kubernetes-native policy engine. It evaluates K8s manifests
|
||||||
|
and produces PolicyReport resources. This adapter translates those results
|
||||||
|
to the normalized PolicyCheckResult schema (engine: "kyverno").
|
||||||
|
|
||||||
|
D-053: the platform emits Terraform, not K8s manifests. This adapter is
|
||||||
|
ready but inactive for Terraform-only stacks. It activates when the GitOps
|
||||||
|
reconciler (roadmap) emits K8s manifests. Sample policies are included as
|
||||||
|
documentation at adapters/kyverno/policies/.
|
||||||
|
|
||||||
|
CLI: kyverno_adapter.py <policyreport.json> <contract-id>
|
||||||
|
"""
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
SEVERITY_MAP = {
|
||||||
|
"critical": "critical",
|
||||||
|
"high": "high",
|
||||||
|
"medium": "medium",
|
||||||
|
"low": "low",
|
||||||
|
"info": "info",
|
||||||
|
}
|
||||||
|
|
||||||
|
RESULT_MAP = {
|
||||||
|
"pass": "pass",
|
||||||
|
"fail": "fail",
|
||||||
|
"warn": "skipped",
|
||||||
|
"error": "error",
|
||||||
|
"skip": "skipped",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _iso8601_now():
|
||||||
|
return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
|
||||||
|
|
||||||
|
def _to_pcr(entry, contract_id):
|
||||||
|
severity_raw = entry.get("severity", "info")
|
||||||
|
severity = SEVERITY_MAP.get(str(severity_raw).lower(), "info")
|
||||||
|
result_raw = entry.get("result", "skip")
|
||||||
|
result = RESULT_MAP.get(str(result_raw).lower(), "error")
|
||||||
|
return {
|
||||||
|
"contractId": contract_id,
|
||||||
|
"evaluatedAt": _iso8601_now(),
|
||||||
|
"engine": "kyverno",
|
||||||
|
"ruleId": entry.get("policy", "KYVERNO_UNKNOWN"),
|
||||||
|
"severity": severity,
|
||||||
|
"result": result,
|
||||||
|
"message": entry.get("message", ""),
|
||||||
|
"evidence": {
|
||||||
|
"resource": entry.get("resource", ""),
|
||||||
|
"namespace": entry.get("namespace", ""),
|
||||||
|
"kind": entry.get("kind", ""),
|
||||||
|
"name": entry.get("name", ""),
|
||||||
|
},
|
||||||
|
"resourceRef": entry.get("resource", ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def adapt(policyreport_json_path, contract_id):
|
||||||
|
with open(policyreport_json_path, "r", encoding="utf-8") as fh:
|
||||||
|
data = json.load(fh)
|
||||||
|
out = []
|
||||||
|
# Kyverno PolicyReport has a .results[] array
|
||||||
|
results = data.get("results", [])
|
||||||
|
if not isinstance(results, list):
|
||||||
|
results = []
|
||||||
|
for entry in results:
|
||||||
|
out.append(_to_pcr(entry, contract_id))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) != 3:
|
||||||
|
print("usage: kyverno_adapter.py <policyreport.json> <contract-id>", file=sys.stderr)
|
||||||
|
sys.exit(2)
|
||||||
|
print(json.dumps(adapt(sys.argv[1], sys.argv[2]), indent=2))
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
apiVersion: kyverno.io/v1
|
||||||
|
kind: ClusterPolicy
|
||||||
|
metadata:
|
||||||
|
name: disallow-privileged-containers
|
||||||
|
annotations:
|
||||||
|
policies.kyverno.io/title: Disallow Privileged Containers
|
||||||
|
policies.kyverno.io/category: Security
|
||||||
|
policies.kyverno.io/severity: high
|
||||||
|
policies.kyverno.io/subject: Pod
|
||||||
|
spec:
|
||||||
|
validationFailureAction: audit
|
||||||
|
background: true
|
||||||
|
rules:
|
||||||
|
- name: require-non-privileged
|
||||||
|
match:
|
||||||
|
any:
|
||||||
|
- resources:
|
||||||
|
kinds:
|
||||||
|
- Pod
|
||||||
|
validate:
|
||||||
|
message: "Privileged containers are not allowed. Set securityContext.privileged to false."
|
||||||
|
pattern:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: "*"
|
||||||
|
securityContext:
|
||||||
|
privileged: "false"
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
apiVersion: kyverno.io/v1
|
||||||
|
kind: ClusterPolicy
|
||||||
|
metadata:
|
||||||
|
name: require-image-digests
|
||||||
|
annotations:
|
||||||
|
policies.kyverno.io/title: Require Image Digests
|
||||||
|
policies.kyverno.io/category: Supply Chain
|
||||||
|
policies.kyverno.io/severity: high
|
||||||
|
policies.kyverno.io/subject: Pod
|
||||||
|
spec:
|
||||||
|
validationFailureAction: audit
|
||||||
|
background: true
|
||||||
|
rules:
|
||||||
|
- name: require-digest-reference
|
||||||
|
match:
|
||||||
|
any:
|
||||||
|
- resources:
|
||||||
|
kinds:
|
||||||
|
- Pod
|
||||||
|
validate:
|
||||||
|
message: "Container images must reference a digest (e.g. image@sha256:...), not a mutable tag."
|
||||||
|
pattern:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: "*"
|
||||||
|
image: "*@sha256:*"
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
apiVersion: kyverno.io/v1
|
||||||
|
kind: ClusterPolicy
|
||||||
|
metadata:
|
||||||
|
name: require-resource-labels
|
||||||
|
annotations:
|
||||||
|
policies.kyverno.io/title: Require ACDL Resource Labels
|
||||||
|
policies.kyverno.io/category: Governance
|
||||||
|
policies.kyverno.io/severity: medium
|
||||||
|
policies.kyverno.io/subject: Pod
|
||||||
|
spec:
|
||||||
|
validationFailureAction: audit
|
||||||
|
background: true
|
||||||
|
rules:
|
||||||
|
- name: require-acdl-owner-label
|
||||||
|
match:
|
||||||
|
any:
|
||||||
|
- resources:
|
||||||
|
kinds:
|
||||||
|
- Pod
|
||||||
|
validate:
|
||||||
|
message: "Pods must carry the acdl:owner label (ACDL tagging standard)."
|
||||||
|
pattern:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
acdl:owner: "?*"
|
||||||
|
- name: require-acdl-environment-label
|
||||||
|
match:
|
||||||
|
any:
|
||||||
|
- resources:
|
||||||
|
kinds:
|
||||||
|
- Pod
|
||||||
|
validate:
|
||||||
|
message: "Pods must carry the acdl:environment label (ACDL tagging standard)."
|
||||||
|
pattern:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
acdl:environment: "?*"
|
||||||
@@ -36,6 +36,13 @@ TYPE_MAP = {
|
|||||||
"aws:elbv2:listener": "aws_lb_listener",
|
"aws:elbv2:listener": "aws_lb_listener",
|
||||||
"aws:elbv2:targetgroup": "aws_lb_target_group",
|
"aws:elbv2:targetgroup": "aws_lb_target_group",
|
||||||
"aws:ecr:repository": "aws_ecr_repository",
|
"aws:ecr:repository": "aws_ecr_repository",
|
||||||
|
"aws:cloudfront:distribution": "aws_cloudfront_distribution",
|
||||||
|
"aws:cloudfront:originaccesscontrol": "aws_cloudfront_origin_access_control",
|
||||||
|
"aws:wafv2:webacl": "aws_wafv2_web_acl",
|
||||||
|
"aws:rds:instance": "aws_db_instance",
|
||||||
|
"aws:kms:key": "aws_kms_key",
|
||||||
|
"aws:kms:alias": "aws_kms_alias",
|
||||||
|
"aws:ecs:uptime-service": "aws_ecs_service",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Stack input name -> Terraform arg name, per stack type. Only non-identity
|
# Stack input name -> Terraform arg name, per stack type. Only non-identity
|
||||||
@@ -54,6 +61,12 @@ INPUT_MAP = {
|
|||||||
"aws:elbv2:listener": {},
|
"aws:elbv2:listener": {},
|
||||||
"aws:elbv2:targetgroup": {"port": "port", "protocol": "protocol"},
|
"aws:elbv2:targetgroup": {"port": "port", "protocol": "protocol"},
|
||||||
"aws:ecr:repository": {},
|
"aws:ecr:repository": {},
|
||||||
|
"aws:cloudfront:distribution": {"bucket_regional_domain_name": "origin_domain_name", "price_class": "price_class", "viewer_protocol_policy": "viewer_protocol_policy", "default_ttl": "default_ttl", "max_ttl": "max_ttl", "waf_web_acl_arn": "web_acl_id"},
|
||||||
|
"aws:cloudfront:originaccesscontrol": {"name": "name", "origin_type": "origin_access_control_origin_type", "signing_behavior": "origin_access_control_signing_behavior"},
|
||||||
|
"aws:wafv2:webacl": {"name": "name", "scope": "scope", "default_action": "default_action", "rules": "rules"},
|
||||||
|
"aws:rds:instance": {"db_name": "db_name", "instance_class": "instance_class", "allocated_storage": "allocated_storage", "engine": "engine", "engine_version": "engine_version", "username": "username", "multi_az": "multi_az", "storage_encrypted": "storage_encrypted"},
|
||||||
|
"aws:kms:key": {"description": "description", "deletion_window_days": "deletion_window_in_days"},
|
||||||
|
"aws:kms:alias": {},
|
||||||
}
|
}
|
||||||
|
|
||||||
# Stack output name -> Terraform attribute name, per stack type. Only
|
# Stack output name -> Terraform attribute name, per stack type. Only
|
||||||
@@ -62,7 +75,7 @@ INPUT_MAP = {
|
|||||||
OUTPUT_MAP = {
|
OUTPUT_MAP = {
|
||||||
"aws:s3:bucket": {"bucket_arn": "arn", "bucket_name": "id"},
|
"aws:s3:bucket": {"bucket_arn": "arn", "bucket_name": "id"},
|
||||||
"aws:ec2:vpc": {"vpc_id": "id"},
|
"aws:ec2:vpc": {"vpc_id": "id"},
|
||||||
"aws:ec2:subnet": {"subnet_id": "id"},
|
"aws:ec2:subnet": {"subnet_ids": "id", "subnet_id": "id"},
|
||||||
"aws:ec2:routetable": {},
|
"aws:ec2:routetable": {},
|
||||||
"aws:ecs:cluster": {"cluster_arn": "arn", "cluster_id": "id"},
|
"aws:ecs:cluster": {"cluster_arn": "arn", "cluster_id": "id"},
|
||||||
"aws:ecs:task_definition": {"task_def_arn": "arn"},
|
"aws:ecs:task_definition": {"task_def_arn": "arn"},
|
||||||
@@ -72,6 +85,12 @@ OUTPUT_MAP = {
|
|||||||
"aws:elbv2:listener": {"listener_arn": "id"},
|
"aws:elbv2:listener": {"listener_arn": "id"},
|
||||||
"aws:elbv2:targetgroup": {"target_group_arn": "arn"},
|
"aws:elbv2:targetgroup": {"target_group_arn": "arn"},
|
||||||
"aws:ecr:repository": {"repository_arn": "arn"},
|
"aws:ecr:repository": {"repository_arn": "arn"},
|
||||||
|
"aws:cloudfront:distribution": {"distribution_arn": "arn", "distribution_domain_name": "domain_name", "oac_id": "origin_access_control_id"},
|
||||||
|
"aws:cloudfront:originaccesscontrol": {"oac_id": "id"},
|
||||||
|
"aws:wafv2:webacl": {"web_acl_arn": "arn"},
|
||||||
|
"aws:rds:instance": {"db_endpoint": "endpoint", "db_arn": "arn"},
|
||||||
|
"aws:kms:key": {"kms_key_arn": "arn", "kms_key_id": "key_id"},
|
||||||
|
"aws:kms:alias": {},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -185,6 +204,23 @@ def _emit_resource(resource, type_by_id=None):
|
|||||||
if rtype == "aws:ecs:service" and in_name in ("subnets", "security_group"):
|
if rtype == "aws:ecs:service" and in_name in ("subnets", "security_group"):
|
||||||
# Collected into network_configuration block (emitted after all inputs).
|
# Collected into network_configuration block (emitted after all inputs).
|
||||||
continue
|
continue
|
||||||
|
if rtype == "aws:cloudfront:distribution" and in_name in (
|
||||||
|
"bucket_regional_domain_name", "price_class", "viewer_protocol_policy",
|
||||||
|
"default_ttl", "max_ttl", "waf_web_acl_arn", "oac_id",
|
||||||
|
):
|
||||||
|
# Collected into the origin/default_cache_behavior/web_acl_id blocks
|
||||||
|
# emitted after all inputs.
|
||||||
|
continue
|
||||||
|
if rtype == "aws:cloudfront:originaccesscontrol" and in_name in (
|
||||||
|
"name", "origin_type", "signing_behavior",
|
||||||
|
):
|
||||||
|
# Defaults emitted after all inputs.
|
||||||
|
continue
|
||||||
|
if rtype == "aws:wafv2:webacl" and in_name in (
|
||||||
|
"name", "scope", "default_action", "rules",
|
||||||
|
):
|
||||||
|
# Structured blocks emitted after all inputs.
|
||||||
|
continue
|
||||||
body.append(f"{arg} = {_value_expr(value, type_by_id)}")
|
body.append(f"{arg} = {_value_expr(value, type_by_id)}")
|
||||||
if rtype == "aws:ecs:service":
|
if rtype == "aws:ecs:service":
|
||||||
subnets_val = inputs.get("subnets")
|
subnets_val = inputs.get("subnets")
|
||||||
@@ -246,6 +282,224 @@ def _emit_resource(resource, type_by_id=None):
|
|||||||
body.append("tags = {")
|
body.append("tags = {")
|
||||||
body.append(' Name = "acdl-microservice-rt"')
|
body.append(' Name = "acdl-microservice-rt"')
|
||||||
body.append("}")
|
body.append("}")
|
||||||
|
if rtype == "aws:cloudfront:originaccesscontrol":
|
||||||
|
name = inputs.get("name", "acdl-oac")
|
||||||
|
if isinstance(name, str) and name.startswith("ref:"):
|
||||||
|
name = _ref_expr(name, type_by_id)
|
||||||
|
else:
|
||||||
|
name = _tf_value(name)
|
||||||
|
body.append(f"name = {name}")
|
||||||
|
body.append("origin_access_control_origin_type = \"s3\"")
|
||||||
|
body.append("origin_access_control_signing_behavior = \"always\"")
|
||||||
|
if rtype == "aws:cloudfront:distribution":
|
||||||
|
origin_domain = inputs.get("bucket_regional_domain_name")
|
||||||
|
if isinstance(origin_domain, str) and origin_domain.startswith("ref:"):
|
||||||
|
origin_domain = _ref_expr(origin_domain, type_by_id)
|
||||||
|
else:
|
||||||
|
origin_domain = _tf_value(origin_domain)
|
||||||
|
# The OAC resource id follows the convention "<childId>-originaccesscontrol";
|
||||||
|
# derive it from this distribution's id.
|
||||||
|
if rid.endswith("-distribution"):
|
||||||
|
oac_rid = rid[: -len("distribution")] + "originaccesscontrol"
|
||||||
|
else:
|
||||||
|
oac_rid = "cloudfront-originaccesscontrol"
|
||||||
|
body.append("origin {")
|
||||||
|
body.append(f" domain_name = {origin_domain}")
|
||||||
|
body.append(f" origin_access_control = aws_cloudfront_origin_access_control.{oac_rid}.id")
|
||||||
|
body.append(" s3_origin_config {}")
|
||||||
|
body.append("}")
|
||||||
|
body.append("enabled = true")
|
||||||
|
price_class = inputs.get("price_class", "PriceClass_100")
|
||||||
|
vpp = inputs.get("viewer_protocol_policy", "redirect-to-https")
|
||||||
|
default_ttl = inputs.get("default_ttl", 3600)
|
||||||
|
max_ttl = inputs.get("max_ttl", 86400)
|
||||||
|
body.append("default_cache_behavior {")
|
||||||
|
body.append(f" viewer_protocol_policy = {_value_expr(vpp, type_by_id)}")
|
||||||
|
body.append(f" target_origin_id = {_tf_value(rid)}")
|
||||||
|
body.append(" min_ttl = 0")
|
||||||
|
body.append(f" default_ttl = {_value_expr(default_ttl, type_by_id)}")
|
||||||
|
body.append(f" max_ttl = {_value_expr(max_ttl, type_by_id)}")
|
||||||
|
body.append(" allowed_methods = [\"GET\", \"HEAD\"]")
|
||||||
|
body.append(" cached_methods = [\"GET\", \"HEAD\"]")
|
||||||
|
body.append("}")
|
||||||
|
body.append(f"price_class = {_value_expr(price_class, type_by_id)}")
|
||||||
|
body.append("restrictions {")
|
||||||
|
body.append(" geo_restriction {")
|
||||||
|
body.append(" restriction_type = \"none\"")
|
||||||
|
body.append(" }")
|
||||||
|
body.append("}")
|
||||||
|
body.append("viewer_certificate {")
|
||||||
|
body.append(" cloudfront_default_certificate = true")
|
||||||
|
body.append("}")
|
||||||
|
waf_arn = inputs.get("waf_web_acl_arn")
|
||||||
|
if waf_arn is not None:
|
||||||
|
if isinstance(waf_arn, str) and waf_arn.startswith("ref:"):
|
||||||
|
waf_expr = _ref_expr(waf_arn, type_by_id)
|
||||||
|
else:
|
||||||
|
waf_expr = _tf_value(waf_arn)
|
||||||
|
body.append(f"web_acl_id = {waf_expr}")
|
||||||
|
if rtype == "aws:wafv2:webacl":
|
||||||
|
name = inputs.get("name", "acdl-waf")
|
||||||
|
body.append(f"name = {_tf_value(name) if not isinstance(name, str) or not name.startswith('ref:') else _ref_expr(name, type_by_id)}")
|
||||||
|
body.append("scope = \"cloudfront\"")
|
||||||
|
# P1-5: Honor default_action input instead of hardcoding allow {}.
|
||||||
|
default_action_input = inputs.get("default_action", "allow")
|
||||||
|
if isinstance(default_action_input, str) and default_action_input.startswith("ref:"):
|
||||||
|
default_action_input = "allow"
|
||||||
|
action_type = default_action_input if default_action_input in ("allow", "block") else "allow"
|
||||||
|
body.append("default_action {")
|
||||||
|
body.append(f" {action_type} {{}}")
|
||||||
|
body.append("}")
|
||||||
|
body.append("visibility_config {")
|
||||||
|
body.append(" cloudwatch_metrics_enabled = true")
|
||||||
|
body.append(" metric_name = \"acdl-waf-metrics\"")
|
||||||
|
body.append(" sampled_requests_enabled = true")
|
||||||
|
body.append("}")
|
||||||
|
# P1-4: Emit custom rules as nested blocks, not an attribute assignment.
|
||||||
|
rules_input = inputs.get("rules")
|
||||||
|
if rules_input and isinstance(rules_input, list):
|
||||||
|
for idx, rule in enumerate(rules_input):
|
||||||
|
if not isinstance(rule, dict):
|
||||||
|
continue
|
||||||
|
rule_name = rule.get("name", f"custom-rule-{idx}")
|
||||||
|
rule_priority = rule.get("priority", idx)
|
||||||
|
body.append("rules {")
|
||||||
|
body.append(f" name = {_tf_value(rule_name)}")
|
||||||
|
body.append(f" priority = {_tf_value(rule_priority)}")
|
||||||
|
override = rule.get("override_action", "none")
|
||||||
|
if override not in ("none", "count"):
|
||||||
|
override = "none"
|
||||||
|
body.append(" override_action {")
|
||||||
|
body.append(f" {override} {{}}")
|
||||||
|
body.append(" }")
|
||||||
|
statement = rule.get("statement", {})
|
||||||
|
if statement:
|
||||||
|
body.append(" statement {")
|
||||||
|
for sk, sv in statement.items():
|
||||||
|
body.append(f" {sk} {{")
|
||||||
|
if isinstance(sv, dict):
|
||||||
|
for sk2, sv2 in sv.items():
|
||||||
|
body.append(f" {sk2} = {_tf_value(sv2)}")
|
||||||
|
body.append(" }")
|
||||||
|
body.append(" }")
|
||||||
|
body.append(" visibility_config {")
|
||||||
|
body.append(" cloudwatch_metrics_enabled = true")
|
||||||
|
body.append(f" metric_name = {_tf_value(f'{rule_name}-metrics')}")
|
||||||
|
body.append(" sampled_requests_enabled = true")
|
||||||
|
body.append(" }")
|
||||||
|
body.append("}")
|
||||||
|
elif rules_input and isinstance(rules_input, str) and rules_input.startswith("ref:"):
|
||||||
|
# A ref: value for rules — emit as dynamic block reference (rare case).
|
||||||
|
body.append(f"rules = {_ref_expr(rules_input, type_by_id)}")
|
||||||
|
else:
|
||||||
|
# Default: emit the AWS-managed-rules block when no custom rules.
|
||||||
|
body.append("rules {")
|
||||||
|
body.append(" name = \"aws-managed-rules\"")
|
||||||
|
body.append(" priority = 0")
|
||||||
|
body.append(" override_action {")
|
||||||
|
body.append(" none {}")
|
||||||
|
body.append(" }")
|
||||||
|
body.append(" statement {")
|
||||||
|
body.append(" managed_rule_group_statement {")
|
||||||
|
body.append(" name = \"AWSManagedRulesCommonRuleSet\"")
|
||||||
|
body.append(" vendor_name = \"AWS\"")
|
||||||
|
body.append(" }")
|
||||||
|
body.append(" }")
|
||||||
|
body.append(" visibility_config {")
|
||||||
|
body.append(" cloudwatch_metrics_enabled = true")
|
||||||
|
body.append(" metric_name = \"aws-managed-rules-metrics\"")
|
||||||
|
body.append(" sampled_requests_enabled = true")
|
||||||
|
body.append(" }")
|
||||||
|
body.append("}")
|
||||||
|
if rtype == "aws:rds:instance":
|
||||||
|
# Emit NFR-derived arguments: backup_retention_period +
|
||||||
|
# deletion_protection from the nfrs block. Also emit
|
||||||
|
# storage_encrypted = true (from inputs, already emitted above if
|
||||||
|
# present) and skip_final_snapshot = true for dev safety.
|
||||||
|
nfrs = resource.get("nfrs", {})
|
||||||
|
backup_retention = nfrs.get("backup_retention_period", 7)
|
||||||
|
deletion_protection = nfrs.get("deletion_protection", True)
|
||||||
|
body.append(f"backup_retention_period = {_tf_value(backup_retention)}")
|
||||||
|
body.append(f"deletion_protection = {_tf_value(deletion_protection)}")
|
||||||
|
# Ensure storage_encrypted is emitted (defaults to true if not in inputs).
|
||||||
|
if "storage_encrypted" not in inputs:
|
||||||
|
body.append("storage_encrypted = true")
|
||||||
|
# Dev safety: skip the final snapshot so `terraform destroy` works
|
||||||
|
# without a final DB snapshot (overridden by deletion_protection).
|
||||||
|
body.append("skip_final_snapshot = true")
|
||||||
|
if rtype == "aws:kms:key":
|
||||||
|
nfrs = resource.get("nfrs", {})
|
||||||
|
enable_rotation = nfrs.get("enable_rotation", True)
|
||||||
|
body.append(f"enable_key_rotation = {_tf_value(enable_rotation)}")
|
||||||
|
if rtype == "aws:s3:bucket":
|
||||||
|
nfrs = resource.get("nfrs", {})
|
||||||
|
encryption_enabled = nfrs.get("encryption_enabled", True)
|
||||||
|
if encryption_enabled:
|
||||||
|
kms_key_arn = inputs.get("kms_key_arn")
|
||||||
|
if kms_key_arn and isinstance(kms_key_arn, str) and kms_key_arn.startswith("ref:"):
|
||||||
|
kms_ref = _ref_expr(kms_key_arn, type_by_id)
|
||||||
|
body.append("server_side_encryption_configuration {")
|
||||||
|
body.append(" rule {")
|
||||||
|
body.append(" apply_server_side_encryption_by_default {")
|
||||||
|
body.append(f" sse_algorithm = \"aws:kms\"")
|
||||||
|
body.append(f" kms_master_key_id = {kms_ref}")
|
||||||
|
body.append(" }")
|
||||||
|
body.append(" }")
|
||||||
|
body.append("}")
|
||||||
|
elif kms_key_arn:
|
||||||
|
body.append("server_side_encryption_configuration {")
|
||||||
|
body.append(" rule {")
|
||||||
|
body.append(" apply_server_side_encryption_by_default {")
|
||||||
|
body.append(" sse_algorithm = \"aws:kms\"")
|
||||||
|
body.append(f" kms_master_key_id = {_tf_value(kms_key_arn)}")
|
||||||
|
body.append(" }")
|
||||||
|
body.append(" }")
|
||||||
|
body.append("}")
|
||||||
|
else:
|
||||||
|
print(f"WARNING: s3 bucket {rid} has no kms_key_arn — falling back to AWS-managed key (alias/aws/s3)", file=sys.stderr)
|
||||||
|
body.append("server_side_encryption_configuration {")
|
||||||
|
body.append(" rule {")
|
||||||
|
body.append(" apply_server_side_encryption_by_default {")
|
||||||
|
body.append(" sse_algorithm = \"aws:kms\"")
|
||||||
|
body.append(" }")
|
||||||
|
body.append(" }")
|
||||||
|
body.append("}")
|
||||||
|
if rtype == "aws:ecs:uptime-service":
|
||||||
|
feature_flag = inputs.get("feature_flag_enabled", True)
|
||||||
|
if not feature_flag:
|
||||||
|
return ""
|
||||||
|
container_image = inputs.get("container_image", "louislam/uptime-kuma:1")
|
||||||
|
monitored = inputs.get("monitored_endpoints", [])
|
||||||
|
static_checks = inputs.get("static_checks", [])
|
||||||
|
alert_channels = inputs.get("alert_channels", {})
|
||||||
|
all_checks = (monitored if isinstance(monitored, list) else []) + \
|
||||||
|
(static_checks if isinstance(static_checks, list) else [])
|
||||||
|
env_vars = {
|
||||||
|
"UPTIME_KUMA_MONITOR_CONFIG": json.dumps(all_checks),
|
||||||
|
"UPTIME_KUMA_ALERT_CONFIG": json.dumps(alert_channels),
|
||||||
|
}
|
||||||
|
body.append("desired_count = 1")
|
||||||
|
body.append("launch_type = \"FARGATE\"")
|
||||||
|
body.append("network_configuration {")
|
||||||
|
body.append(" subnets = [\"subnet-uptime\"]")
|
||||||
|
body.append(" security_groups = [\"sg-uptime\"]")
|
||||||
|
body.append(" assign_public_ip = true")
|
||||||
|
body.append("}")
|
||||||
|
container = {
|
||||||
|
"name": "uptime-kuma",
|
||||||
|
"image": container_image,
|
||||||
|
"essential": True,
|
||||||
|
"portMappings": [{"containerPort": 3001, "hostPort": 3001}],
|
||||||
|
"environment": [{"name": k, "value": v} for k, v in env_vars.items()],
|
||||||
|
"logConfiguration": {"logDriver": "awslogs", "options": {"awslogs-group": "/acdl/uptime", "awslogs-region": inputs.get("region", "us-east-1")}},
|
||||||
|
}
|
||||||
|
body.append("container_definitions = " + _tf_value([container]))
|
||||||
|
nfrs = resource.get("nfrs", {})
|
||||||
|
deletion_protection = nfrs.get("deletion_protection", True)
|
||||||
|
if deletion_protection:
|
||||||
|
body.append("lifecycle {")
|
||||||
|
body.append(" prevent_destroy = true")
|
||||||
|
body.append("}")
|
||||||
return _resource_block(rid, tf_type, body)
|
return _resource_block(rid, tf_type, body)
|
||||||
|
|
||||||
|
|
||||||
@@ -363,6 +617,19 @@ def adapt(stack_instance, out_dir):
|
|||||||
main_tf_parts.append(_emit_output(out_name, f"{tf_type}.{rid}.{tf_attr}"))
|
main_tf_parts.append(_emit_output(out_name, f"{tf_type}.{rid}.{tf_attr}"))
|
||||||
if has_vpc:
|
if has_vpc:
|
||||||
main_tf_parts.append(_emit_igw(resources))
|
main_tf_parts.append(_emit_igw(resources))
|
||||||
|
# P1-7: Emit stack-level outputs from the resolved composition outputs[].
|
||||||
|
# Each stack output has {"from": <resourceId>, "output": <outputName>}.
|
||||||
|
# We look up the resource type + OUTPUT_MAP to build the interpolation.
|
||||||
|
stack_outputs = stack_instance.get("outputs", {})
|
||||||
|
for out_name, out_spec in stack_outputs.items():
|
||||||
|
src_rid = out_spec.get("from", "")
|
||||||
|
src_output = out_spec.get("output", out_name)
|
||||||
|
if src_rid in type_by_id:
|
||||||
|
src_rtype = type_by_id[src_rid]
|
||||||
|
src_tf_type = TYPE_MAP.get(src_rtype, src_rtype.replace(":", "_"))
|
||||||
|
out_map = OUTPUT_MAP.get(src_rtype, {})
|
||||||
|
tf_attr = out_map.get(src_output, src_output)
|
||||||
|
main_tf_parts.append(_emit_output(out_name, f"{src_tf_type}.{src_rid}.{tf_attr}"))
|
||||||
main_tf = "\n".join(main_tf_parts)
|
main_tf = "\n".join(main_tf_parts)
|
||||||
|
|
||||||
with open(os.path.join(out_dir, "main.tf"), "w") as fh:
|
with open(os.path.join(out_dir, "main.tf"), "w") as fh:
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ schemas/policy_check_result.schema.json. Run Checkov with --soft-fail so
|
|||||||
Checkov never exits non-zero; the confidence signal decides the gate, not
|
Checkov never exits non-zero; the confidence signal decides the gate, not
|
||||||
Checkov's exit code.
|
Checkov's exit code.
|
||||||
|
|
||||||
Spike scope (D-043): tag/naming is a single SKIPPED record. A custom
|
The ACDL tagging standard (D-054, D-043 closure) is enforced by a custom
|
||||||
Checkov YAML rule for tag presence lands in v1.2.
|
Checkov rule at adapters/terraform/policy/custom_rules/acdl_tagging.py,
|
||||||
|
loaded via --external-checks-dir. The adapter therefore maps
|
||||||
|
ACDL_TAG_NAMING as a real rule (no synthetic SKIPPED record is emitted).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
@@ -27,6 +29,10 @@ RULE_MAP = {
|
|||||||
"CKV_AWS_40": ("iam-wildcard", "medium"),
|
"CKV_AWS_40": ("iam-wildcard", "medium"),
|
||||||
"CKV_AWS_7": ("kms-key-reference", "medium"),
|
"CKV_AWS_7": ("kms-key-reference", "medium"),
|
||||||
"CKV_AWS_33": ("kms-key-reference", "medium"),
|
"CKV_AWS_33": ("kms-key-reference", "medium"),
|
||||||
|
# D-054 / D-043 closure: ACDL_TAG_NAMING is now a real custom Checkov
|
||||||
|
# rule (adapters/terraform/policy/custom_rules/acdl_tagging.py), loaded
|
||||||
|
# via --external-checks-dir. No synthetic SKIPPED record is emitted.
|
||||||
|
"ACDL_TAG_NAMING": ("tagging-standard", "medium"),
|
||||||
}
|
}
|
||||||
|
|
||||||
_RESULT_MAP = {"PASSED": "pass", "FAILED": "fail", "SKIPPED": "skipped"}
|
_RESULT_MAP = {"PASSED": "pass", "FAILED": "fail", "SKIPPED": "skipped"}
|
||||||
@@ -60,20 +66,6 @@ def _to_pcr(checkov_record, contract_id, result_str):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _emit_tag_naming_skipped(contract_id):
|
|
||||||
return {
|
|
||||||
"contractId": contract_id,
|
|
||||||
"evaluatedAt": _iso8601_now(),
|
|
||||||
"engine": "checkov",
|
|
||||||
"ruleId": "ACDL_TAG_NAMING",
|
|
||||||
"severity": "info",
|
|
||||||
"result": "skipped",
|
|
||||||
"message": "tag/naming check deferred to v1.2 (D-043)",
|
|
||||||
"evidence": {},
|
|
||||||
"resourceRef": "",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def adapt(checkov_json_path, contract_id):
|
def adapt(checkov_json_path, contract_id):
|
||||||
with open(checkov_json_path, "r", encoding="utf-8") as fh:
|
with open(checkov_json_path, "r", encoding="utf-8") as fh:
|
||||||
data = json.load(fh)
|
data = json.load(fh)
|
||||||
@@ -88,7 +80,6 @@ def adapt(checkov_json_path, contract_id):
|
|||||||
out.append(_to_pcr(rec, contract_id, "FAILED"))
|
out.append(_to_pcr(rec, contract_id, "FAILED"))
|
||||||
for rec in results.get("skipped_checks", []):
|
for rec in results.get("skipped_checks", []):
|
||||||
out.append(_to_pcr(rec, contract_id, "SKIPPED"))
|
out.append(_to_pcr(rec, contract_id, "SKIPPED"))
|
||||||
out.append(_emit_tag_naming_skipped(contract_id))
|
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# ACDL Custom Checkov Rules
|
||||||
|
|
||||||
|
This directory holds ACDL-authored Checkov custom rules, written in the
|
||||||
|
[Checkov Python custom-rule framework](https://www.checkov.io/4.Contributing/Custom%20Policies.html).
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- `acdl_tagging.py` — `ACDL_TAG_NAMING` (D-054): ensures every taggable AWS
|
||||||
|
resource carries the four required ACDL tags
|
||||||
|
(`acdl:owner`, `acdl:contract`, `acdl:environment`, `acdl:cost-center`).
|
||||||
|
This rule replaces the synthetic SKIPPED `ACDL_TAG_NAMING` record that the
|
||||||
|
Checkov adapter previously emitted (D-043 closure). The canonical tag set
|
||||||
|
is declared in [`schemas/tagging-standard.json`](../../../schemas/tagging-standard.json).
|
||||||
|
|
||||||
|
## How Checkov loads them
|
||||||
|
|
||||||
|
Checkov custom rules are discovered via the `--external-checks-dir` flag.
|
||||||
|
`scripts/run_platform.sh` invokes Checkov with:
|
||||||
|
|
||||||
|
```
|
||||||
|
checkov -f terraform/spike/main.tf --framework terraform -o json --soft-fail \
|
||||||
|
--external-checks-dir adapters/terraform/policy/custom_rules/
|
||||||
|
```
|
||||||
|
|
||||||
|
Checkov imports each `*.py` file in the directory and instantiates the
|
||||||
|
module-level `check` object (see the `check = AcdlTaggingStandard()` line at
|
||||||
|
the bottom of `acdl_tagging.py`).
|
||||||
|
|
||||||
|
## Severity / result mapping
|
||||||
|
|
||||||
|
The Checkov adapter (`adapters/terraform/policy/checkov_adapter.py`)
|
||||||
|
maps `ACDL_TAG_NAMING` to `(tagging-standard, medium)` in `RULE_MAP`. The
|
||||||
|
custom rule therefore produces real `PASS`/`FAIL` PolicyCheckResult records,
|
||||||
|
feeding the confidence signal instead of the old SKIPPED placeholder.
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
"""ACDL tagging standard custom Checkov rule (D-054).
|
||||||
|
|
||||||
|
Checks that all taggable AWS resources have the required ACDL tags:
|
||||||
|
acdl:owner, acdl:contract, acdl:environment, acdl:cost-center
|
||||||
|
|
||||||
|
Fails (severity medium) when any required tag is missing.
|
||||||
|
Closes the D-043 deferral (the SKIPPED ACDL_TAG_NAMING placeholder
|
||||||
|
becomes a real check).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from checkov.terraform.checks.resource.base_resource_check import BaseResourceCheck
|
||||||
|
from checkov.common.models.enums import CheckResult, CheckCategories
|
||||||
|
|
||||||
|
REQUIRED_TAGS = ("acdl:owner", "acdl:contract", "acdl:environment", "acdl:cost-center")
|
||||||
|
|
||||||
|
# Resources that support tags (exclude resources that have no tags attribute)
|
||||||
|
NON_TAGGABLE_TYPES = (
|
||||||
|
"aws_cloudfront_origin_access_control",
|
||||||
|
"aws_lambda_function_url",
|
||||||
|
"aws_route_table_association",
|
||||||
|
"aws_internet_gateway",
|
||||||
|
)
|
||||||
|
|
||||||
|
class AcdlTaggingStandard(BaseResourceCheck):
|
||||||
|
def __init__(self):
|
||||||
|
name = "Ensure all taggable AWS resources have required ACDL tags"
|
||||||
|
check_id = "ACDL_TAG_NAMING"
|
||||||
|
supported_resources = ["*"] # all resources
|
||||||
|
categories = [CheckCategories.GENERAL_SECURITY]
|
||||||
|
super().__init__(name=name, check_id=check_id, categories=categories, supported_resources=supported_resources)
|
||||||
|
|
||||||
|
def scan_resource_conf(self, conf, entity_type):
|
||||||
|
# Skip non-taggable resources
|
||||||
|
if entity_type in NON_TAGGABLE_TYPES:
|
||||||
|
return CheckResult.PASSED
|
||||||
|
# Check for a tags block
|
||||||
|
tags = conf.get("tags")
|
||||||
|
if not tags:
|
||||||
|
return CheckResult.FAILED
|
||||||
|
tag_keys = set()
|
||||||
|
if isinstance(tags, list) and tags:
|
||||||
|
tag_block = tags[0]
|
||||||
|
if isinstance(tag_block, dict):
|
||||||
|
tag_keys = set(tag_block.keys())
|
||||||
|
elif isinstance(tags, dict):
|
||||||
|
tag_keys = set(tags.keys())
|
||||||
|
missing = [t for t in REQUIRED_TAGS if t not in tag_keys]
|
||||||
|
if missing:
|
||||||
|
return CheckResult.FAILED
|
||||||
|
return CheckResult.PASSED
|
||||||
|
|
||||||
|
check = AcdlTaggingStandard()
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# Wiz Adapter
|
||||||
|
|
||||||
|
The Wiz adapter translates Wiz API issue records to the normalized ACDL
|
||||||
|
[`PolicyCheckResult`](../../schemas/policy_check_result.schema.json) schema
|
||||||
|
(engine: `"wiz"`), mirroring the Checkov adapter pattern.
|
||||||
|
|
||||||
|
## What Wiz is
|
||||||
|
|
||||||
|
[Wiz](https://www.wiz.io/) is a cloud security SaaS platform that
|
||||||
|
continuously scans CSPM / CWPP / KSPM findings across AWS, Azure, GCP and
|
||||||
|
Kubernetes. It exposes a GraphQL/REST API for fetching issue records.
|
||||||
|
|
||||||
|
## Adapter behaviour
|
||||||
|
|
||||||
|
`wiz_adapter.py <wiz_issues.json> <contract-id>` reads a JSON file of Wiz
|
||||||
|
issue records (the shape returned by the Wiz `issues` GraphQL query /
|
||||||
|
list endpoint) and emits a list of `PolicyCheckResult` dicts:
|
||||||
|
|
||||||
|
| Wiz field | PolicyCheckResult field |
|
||||||
|
|------------------|------------------------------------------------------------|
|
||||||
|
| `id` / `control.id` | `ruleId` |
|
||||||
|
| `severity` | `severity` (mapped `CRITICAL/HIGH/MEDIUM/LOW/INFO`) |
|
||||||
|
| `status` | `result` (`OPEN→fail`, `RESOLVED→pass`, `IN_PROGRESS/DISMISSED→skipped`) |
|
||||||
|
| `title` / `control.name` | `message` |
|
||||||
|
| `entity.id` | `resourceRef` + `evidence.resource` |
|
||||||
|
| `entity.{name,cloudPlatform,subscriptionId}` | `evidence.*` |
|
||||||
|
|
||||||
|
The adapter is read-only against a local JSON fixture; the pipeline is
|
||||||
|
responsible for fetching from Wiz (when configured) and writing the file.
|
||||||
|
|
||||||
|
## Offline / degraded behaviour (D-052)
|
||||||
|
|
||||||
|
When Wiz is not configured the pipeline passes an empty issues payload (or
|
||||||
|
simply does not invoke the adapter). The adapter degrades gracefully:
|
||||||
|
|
||||||
|
- an empty `issues` list → the adapter emits a single `WIZ_NOT_CONFIGURED`
|
||||||
|
`PolicyCheckResult` with `result: "skipped"` so the confidence policy
|
||||||
|
input stays non-empty (and does not falsely inflate the score).
|
||||||
|
|
||||||
|
`is_configured()` returns `True` only when the `WIZ_API_TOKEN`
|
||||||
|
environment variable is set; the pipeline uses it to decide whether to
|
||||||
|
fetch and invoke the adapter at all.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
| Env var | Required | Purpose |
|
||||||
|
|-----------------|----------|--------------------------------------------------|
|
||||||
|
| `WIZ_API_TOKEN` | yes | Bearer token for the Wiz REST API. When unset, `is_configured()` returns `False`. |
|
||||||
|
| `WIZ_ENDPOINT` | no | Wiz API endpoint (defaults to `https://api.wiz.io` when implemented). |
|
||||||
|
|
||||||
|
## Schema path
|
||||||
|
|
||||||
|
The output records validate against
|
||||||
|
[`schemas/policy_check_result.schema.json`](../../schemas/policy_check_result.schema.json)
|
||||||
|
(`engine: "wiz"` was added to the enum in Phase 23).
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
"""Wiz adapter — translate Wiz API results to ACDL PolicyCheckResult records.
|
||||||
|
|
||||||
|
Wiz is a SaaS security platform with a REST API (issues, security graph
|
||||||
|
queries). This adapter translates Wiz issue records to the normalized
|
||||||
|
PolicyCheckResult schema (engine: "wiz"), matching the Checkov adapter
|
||||||
|
pattern.
|
||||||
|
|
||||||
|
D-052: stub + schema path. The adapter degrades gracefully when Wiz is
|
||||||
|
not configured — it emits a single SKIPPED record (WIZ_NOT_CONFIGURED)
|
||||||
|
so the confidence policy input stays non-empty. The pipeline invokes it
|
||||||
|
optionally when WIZ_API_TOKEN is set.
|
||||||
|
|
||||||
|
CLI: wiz_adapter.py <wiz_issues.json> <contract-id>
|
||||||
|
"""
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
SEVERITY_MAP = {
|
||||||
|
"CRITICAL": "critical",
|
||||||
|
"HIGH": "high",
|
||||||
|
"MEDIUM": "medium",
|
||||||
|
"LOW": "low",
|
||||||
|
"INFO": "info",
|
||||||
|
}
|
||||||
|
|
||||||
|
RESULT_MAP = {
|
||||||
|
"OPEN": "fail",
|
||||||
|
"RESOLVED": "pass",
|
||||||
|
"IN_PROGRESS": "skipped",
|
||||||
|
"DISMISSED": "skipped",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _iso8601_now():
|
||||||
|
return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
|
||||||
|
|
||||||
|
def _to_pcr(wiz_issue, contract_id):
|
||||||
|
severity_raw = wiz_issue.get("severity", "INFO")
|
||||||
|
severity = SEVERITY_MAP.get(str(severity_raw).upper(), "info")
|
||||||
|
status = wiz_issue.get("status", "OPEN")
|
||||||
|
result = RESULT_MAP.get(str(status).upper(), "error")
|
||||||
|
control = wiz_issue.get("control", {})
|
||||||
|
return {
|
||||||
|
"contractId": contract_id,
|
||||||
|
"evaluatedAt": _iso8601_now(),
|
||||||
|
"engine": "wiz",
|
||||||
|
"ruleId": wiz_issue.get("id", control.get("id", "WIZ_UNKNOWN")),
|
||||||
|
"severity": severity,
|
||||||
|
"result": result,
|
||||||
|
"message": wiz_issue.get("title", control.get("name", "")),
|
||||||
|
"evidence": {
|
||||||
|
"resource": wiz_issue.get("entity", {}).get("id"),
|
||||||
|
"resource_name": wiz_issue.get("entity", {}).get("name"),
|
||||||
|
"cloud_platform": wiz_issue.get("entity", {}).get("cloudPlatform"),
|
||||||
|
"subscription_id": wiz_issue.get("entity", {}).get("subscriptionId"),
|
||||||
|
},
|
||||||
|
"resourceRef": wiz_issue.get("entity", {}).get("id", ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _emit_not_configured(contract_id):
|
||||||
|
return {
|
||||||
|
"contractId": contract_id,
|
||||||
|
"evaluatedAt": _iso8601_now(),
|
||||||
|
"engine": "wiz",
|
||||||
|
"ruleId": "WIZ_NOT_CONFIGURED",
|
||||||
|
"severity": "info",
|
||||||
|
"result": "skipped",
|
||||||
|
"message": "Wiz adapter not configured (WIZ_API_TOKEN not set); degraded gracefully (D-052).",
|
||||||
|
"evidence": {},
|
||||||
|
"resourceRef": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def adapt(wiz_json_path, contract_id):
|
||||||
|
with open(wiz_json_path, "r", encoding="utf-8") as fh:
|
||||||
|
data = json.load(fh)
|
||||||
|
out = []
|
||||||
|
# Accept either a bare list of issues or an object with an "issues" key.
|
||||||
|
if isinstance(data, list):
|
||||||
|
issues = data
|
||||||
|
else:
|
||||||
|
issues = data.get("issues", [])
|
||||||
|
if not isinstance(issues, list):
|
||||||
|
issues = []
|
||||||
|
for issue in issues:
|
||||||
|
out.append(_to_pcr(issue, contract_id))
|
||||||
|
if not out:
|
||||||
|
out.append(_emit_not_configured(contract_id))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def is_configured():
|
||||||
|
return bool(os.environ.get("WIZ_API_TOKEN"))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) != 3:
|
||||||
|
print("usage: wiz_adapter.py <wiz_issues.json> <contract-id>", file=sys.stderr)
|
||||||
|
sys.exit(2)
|
||||||
|
print(json.dumps(adapt(sys.argv[1], sys.argv[2]), indent=2))
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
FROM python:3.12-slim
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
COPY app.py /app/app.py
|
|
||||||
|
|
||||||
EXPOSE 8080
|
|
||||||
CMD ["python", "/app/app.py"]
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
# acdl-consumer-microservice
|
|
||||||
|
|
||||||
A basic HTTP microservice for the ACDL v1.2 milestone. Returns 200 on `/`
|
|
||||||
and `/health` with a JSON status body. Deployed to AWS ECS Fargate via the
|
|
||||||
ACDL platform's `microservice` contract.
|
|
||||||
|
|
||||||
## Build + push to ECR
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build
|
|
||||||
docker build -t acdl-microservice .
|
|
||||||
|
|
||||||
# Tag for ECR
|
|
||||||
docker tag acdl-microservice:latest 581513795199.dkr.ecr.us-east-1.amazonaws.com/acdl-microservice:latest
|
|
||||||
|
|
||||||
# Authenticate to ECR
|
|
||||||
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin 581513795199.dkr.ecr.us-east-1.amazonaws.com
|
|
||||||
|
|
||||||
# Push
|
|
||||||
docker push 581513795199.dkr.ecr.us-east-1.amazonaws.com/acdl-microservice:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
## Contract
|
|
||||||
|
|
||||||
The contract submission is at `contracts/microservice.yaml` (or the
|
|
||||||
platform's `contracts/microservice.yaml`). Submitting it to the ACDL
|
|
||||||
pipeline triggers: contract → IR resolution → `terraform plan` →
|
|
||||||
`terraform apply` (dev) → a live ECS Fargate service.
|
|
||||||
|
|
||||||
## Endpoints
|
|
||||||
|
|
||||||
- `GET /` — 200, `{"status":"ok","service":"acdl-microservice","version":"1.0.0"}`
|
|
||||||
- `GET /health` — 200, same body
|
|
||||||
- any other path — 404
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
"""ACDL consumer microservice — a tiny HTTP server returning 200 on /.
|
|
||||||
|
|
||||||
This is the reference consumer microservice for the v1.2 milestone. It's
|
|
||||||
intentionally minimal: stdlib only, no framework, no dependencies. The
|
|
||||||
platform deploys it to ECS Fargate via the microservice contract.
|
|
||||||
"""
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
||||||
|
|
||||||
|
|
||||||
class Handler(BaseHTTPRequestHandler):
|
|
||||||
def do_GET(self):
|
|
||||||
if self.path == "/" or self.path == "/health":
|
|
||||||
body = json.dumps({
|
|
||||||
"status": "ok",
|
|
||||||
"service": "acdl-microservice",
|
|
||||||
"version": "1.0.0",
|
|
||||||
}).encode()
|
|
||||||
self.send_response(200)
|
|
||||||
self.send_header("Content-Type", "application/json")
|
|
||||||
self.send_header("Content-Length", str(len(body)))
|
|
||||||
self.end_headers()
|
|
||||||
self.wfile.write(body)
|
|
||||||
else:
|
|
||||||
self.send_response(404)
|
|
||||||
self.end_headers()
|
|
||||||
|
|
||||||
def log_message(self, format, *args):
|
|
||||||
print(f"{self.address_string()} - {format % args}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
port = int(os.environ.get("PORT", "8080"))
|
|
||||||
server = HTTPServer(("0.0.0.0", port), Handler)
|
|
||||||
print(f"acdl-microservice listening on :{port}", flush=True)
|
|
||||||
server.serve_forever()
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# ACDL sample consumer contract — microservice module (dev)
|
||||||
|
#
|
||||||
|
# Reference example for an ECS Fargate microservice deployment.
|
||||||
|
# This contract declares only the inputs the composition wires reference
|
||||||
|
# (bucket_name, region) plus a representative image/port.
|
||||||
|
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||||
|
module: microservice
|
||||||
|
environment: dev
|
||||||
|
inputs:
|
||||||
|
bucket_name: acdl-microservice-demo
|
||||||
|
region: us-east-1
|
||||||
|
image: public.ecr.aws/docker/library/nginx:latest
|
||||||
|
port: 80
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# ACDL sample consumer contract — static-asset module (dev)
|
# ACDL sample consumer contract — static-assets module (dev)
|
||||||
#
|
#
|
||||||
# This is the reference example for a consumer contract. It declares:
|
# This is the reference example for a consumer contract. It declares:
|
||||||
# uses: the central ACDL deployment pipeline to reference
|
# uses: the central ACDL deployment pipeline to reference
|
||||||
@@ -7,10 +7,10 @@
|
|||||||
# inputs: module-specific inputs
|
# inputs: module-specific inputs
|
||||||
#
|
#
|
||||||
# Validated against schemas/contract.schema.json.
|
# Validated against schemas/contract.schema.json.
|
||||||
# Resolved by acdl_platform/contract_resolver.py to a Target Stack instance.
|
# Resolved by core/contract_resolver.py to a Target Stack instance.
|
||||||
|
|
||||||
uses: acdl/pipelines/deploy.yaml@v1.4
|
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||||
module: static-asset
|
module: static-assets
|
||||||
environment: dev
|
environment: dev
|
||||||
inputs:
|
inputs:
|
||||||
bucket_name: acdl-spike-bucket
|
bucket_name: acdl-spike-bucket
|
||||||
@@ -44,7 +44,13 @@ def _resolve_wire_value(wire, contract_inputs, child_outputs):
|
|||||||
- "<childId>.outputs.<name>" — a reference to another child's output
|
- "<childId>.outputs.<name>" — a reference to another child's output
|
||||||
|
|
||||||
Returns either a concrete value (string/number/boolean) or a
|
Returns either a concrete value (string/number/boolean) or a
|
||||||
"ref:<childId>.<outputName>" string for cross-child references.
|
"ref:<resourceId>.<outputName>" string for cross-child references.
|
||||||
|
|
||||||
|
For multi-resource L1s (e.g. vpc which expands to vpc-vpc, vpc-subnet,
|
||||||
|
vpc-routetable), the ref must point to the sub-resource that actually
|
||||||
|
produces the output, not the child id. The child_outputs table maps
|
||||||
|
childId -> {outputName -> resourceId} so the ref uses the correct
|
||||||
|
resource id.
|
||||||
"""
|
"""
|
||||||
from_expr = wire["from"]
|
from_expr = wire["from"]
|
||||||
to_expr = wire["to"]
|
to_expr = wire["to"]
|
||||||
@@ -66,7 +72,13 @@ def _resolve_wire_value(wire, contract_inputs, child_outputs):
|
|||||||
if len(parts) >= 3 and parts[1] == "outputs":
|
if len(parts) >= 3 and parts[1] == "outputs":
|
||||||
child_id = parts[0]
|
child_id = parts[0]
|
||||||
output_name = parts[2]
|
output_name = parts[2]
|
||||||
return f"ref:{child_id}.{output_name}"
|
# Look up the sub-resource that produces this output.
|
||||||
|
# child_outputs[child_id] is a dict {outputName -> resourceId}.
|
||||||
|
# If the child is a single-resource L1, the resourceId == child_id.
|
||||||
|
# If multi-resource, the resourceId is the expanded sub-resource id.
|
||||||
|
child_out_map = child_outputs.get(child_id, {})
|
||||||
|
resource_id = child_out_map.get(output_name, child_id)
|
||||||
|
return f"ref:{resource_id}.{output_name}"
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -125,6 +137,9 @@ def resolve_l2(contract, registry, repo_root):
|
|||||||
composition = _load_json(comp_path)
|
composition = _load_json(comp_path)
|
||||||
|
|
||||||
# Track child outputs for wire resolution
|
# Track child outputs for wire resolution
|
||||||
|
# child_outputs[childId] = {outputName: resourceId}
|
||||||
|
# For single-resource L1s, resourceId == childId
|
||||||
|
# For multi-resource L1s, resourceId is the expanded sub-resource id
|
||||||
child_outputs = {}
|
child_outputs = {}
|
||||||
resources = []
|
resources = []
|
||||||
|
|
||||||
@@ -139,15 +154,18 @@ def resolve_l2(contract, registry, repo_root):
|
|||||||
child_iface_path = os.path.join(repo_root, child_entry["interface"])
|
child_iface_path = os.path.join(repo_root, child_entry["interface"])
|
||||||
child_iface = _load_json(child_iface_path)
|
child_iface = _load_json(child_iface_path)
|
||||||
|
|
||||||
|
# Build the output->resourceId map for this child
|
||||||
|
child_out_map = {}
|
||||||
|
|
||||||
# For multi-resource L1s (like vpc), the first resource type is the
|
# For multi-resource L1s (like vpc), the first resource type is the
|
||||||
# primary; the adapter handles expansion. Use the interface's type
|
# primary; the adapter handles expansion. Use the interface's type
|
||||||
# or the first resource in the interface's resources array.
|
# or the first resource in the interface's resources array.
|
||||||
if "resources" in child_iface and child_iface["resources"]:
|
if "resources" in child_iface and child_iface["resources"]:
|
||||||
# Multi-resource L1: create one resource per sub-resource
|
# Multi-resource L1: create one resource per sub-resource
|
||||||
for sub_res in child_iface["resources"]:
|
for sub_res in child_iface["resources"]:
|
||||||
|
res_id = f"{child_id}-{sub_res['type'].split(':')[-1].replace('_', '-')}" if len(child_iface["resources"]) > 1 else child_id
|
||||||
resource = {
|
resource = {
|
||||||
"id": f"{child_id}-{sub_res['type'].split(':')[-1].replace('_', '-')}"
|
"id": res_id,
|
||||||
if len(child_iface["resources"]) > 1 else child_id,
|
|
||||||
"type": sub_res["type"],
|
"type": sub_res["type"],
|
||||||
"module": child_module,
|
"module": child_module,
|
||||||
"inputs": {},
|
"inputs": {},
|
||||||
@@ -157,6 +175,9 @@ def resolve_l2(contract, registry, repo_root):
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
resources.append(resource)
|
resources.append(resource)
|
||||||
|
# Map each output to this sub-resource's id
|
||||||
|
for out_name in sub_res.get("outputs", []):
|
||||||
|
child_out_map[out_name] = res_id
|
||||||
else:
|
else:
|
||||||
# Single-resource L1
|
# Single-resource L1
|
||||||
resource = {
|
resource = {
|
||||||
@@ -170,9 +191,17 @@ def resolve_l2(contract, registry, repo_root):
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
resources.append(resource)
|
resources.append(resource)
|
||||||
|
# Map each output to the child id
|
||||||
|
for out_name in child_iface.get("outputs", {}):
|
||||||
|
child_out_map[out_name] = child_id
|
||||||
|
|
||||||
# Track outputs for this child
|
# Also map interface-level outputs (for L1s that declare outputs at the
|
||||||
child_outputs[child_id] = child_iface.get("outputs", {})
|
# interface level rather than per-resource)
|
||||||
|
for out_name in child_iface.get("outputs", {}):
|
||||||
|
if out_name not in child_out_map:
|
||||||
|
child_out_map[out_name] = child_id
|
||||||
|
|
||||||
|
child_outputs[child_id] = child_out_map
|
||||||
|
|
||||||
# Resolve wires to populate inputs
|
# Resolve wires to populate inputs
|
||||||
for wire in composition.get("wires", []):
|
for wire in composition.get("wires", []):
|
||||||
@@ -203,6 +232,73 @@ def resolve_l2(contract, registry, repo_root):
|
|||||||
"resources": resources,
|
"resources": resources,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# REQ-87: Propagate deletion_protection feature flag from contract inputs
|
||||||
|
# to all children's NFRs. When inputs.deletion_protection is false,
|
||||||
|
# all resources get deletion_protection=false (used by decommission).
|
||||||
|
deletion_protection_input = inputs.get("deletion_protection", True)
|
||||||
|
if deletion_protection_input is not True:
|
||||||
|
for res in resources:
|
||||||
|
if "nfrs" not in res:
|
||||||
|
res["nfrs"] = {}
|
||||||
|
res["nfrs"]["deletion_protection"] = deletion_protection_input
|
||||||
|
# Also record the feature flag on the stack object for introspection.
|
||||||
|
if "deletion_protection" in inputs:
|
||||||
|
stack_instance["stack"]["features"] = {
|
||||||
|
"deletion_protection": deletion_protection_input
|
||||||
|
}
|
||||||
|
|
||||||
|
# P1-7: Process the composition's outputs[] array to build stack.outputs.
|
||||||
|
# Each output wire: {"from": "<childId>.outputs.<name>", "to": "stack.outputs.<outName>"}
|
||||||
|
# The child_outputs map (childId -> {outputName: resourceId}) resolves
|
||||||
|
# the source to a resource id, which the adapter uses to emit
|
||||||
|
# `output "<outName>" { value = aws_<type>.<resourceId>.<attr> }`.
|
||||||
|
stack_outputs = {}
|
||||||
|
for out_wire in composition.get("outputs", []):
|
||||||
|
from_expr = out_wire.get("from", "")
|
||||||
|
to_expr = out_wire.get("to", "")
|
||||||
|
# Parse "to": "stack.outputs.<outName>"
|
||||||
|
to_parts = to_expr.split(".")
|
||||||
|
if len(to_parts) != 3 or to_parts[1] != "outputs":
|
||||||
|
continue
|
||||||
|
out_name = to_parts[2]
|
||||||
|
# Parse "from": "<childId>.outputs.<name>"
|
||||||
|
from_parts = from_expr.split(".")
|
||||||
|
if len(from_parts) != 3 or from_parts[1] != "outputs":
|
||||||
|
continue
|
||||||
|
src_child = from_parts[0]
|
||||||
|
src_output = from_parts[2]
|
||||||
|
# Resolve the source resource id from child_outputs
|
||||||
|
child_out_map = child_outputs.get(src_child, {})
|
||||||
|
src_resource_id = child_out_map.get(src_output, src_child)
|
||||||
|
stack_outputs[out_name] = {
|
||||||
|
"type": "string",
|
||||||
|
"from": src_resource_id,
|
||||||
|
"output": src_output,
|
||||||
|
}
|
||||||
|
if stack_outputs:
|
||||||
|
stack_instance["outputs"] = stack_outputs
|
||||||
|
|
||||||
|
return stack_instance
|
||||||
|
|
||||||
|
|
||||||
|
def decommission_transform(stack_instance):
|
||||||
|
"""REQ-92: Transform a resolved stack instance for decommission.
|
||||||
|
|
||||||
|
Sets all scalable counts to 0 and deletion_protection to false on
|
||||||
|
every resource. Used by the decommission pipeline mode after the
|
||||||
|
first step (disable deletion protection) has been applied.
|
||||||
|
"""
|
||||||
|
for res in stack_instance.get("resources", []):
|
||||||
|
if "nfrs" not in res:
|
||||||
|
res["nfrs"] = {}
|
||||||
|
res["nfrs"]["deletion_protection"] = False
|
||||||
|
inputs = res.get("inputs", {})
|
||||||
|
if "desired_count" in inputs:
|
||||||
|
inputs["desired_count"] = 0
|
||||||
|
if "min_capacity" in inputs:
|
||||||
|
inputs["min_capacity"] = 0
|
||||||
|
if "max_capacity" in inputs:
|
||||||
|
inputs["max_capacity"] = 0
|
||||||
return stack_instance
|
return stack_instance
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Environment onboarding check.
|
||||||
|
|
||||||
|
Reads a contract's `environment` field and looks up the matching
|
||||||
|
`core/environments/<name>.json`. If no matching file exists, prints a
|
||||||
|
friendly onboarding prompt and exits non-zero, halting the pipeline before
|
||||||
|
any work is done.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 core/environment_check.py <contract.yaml>
|
||||||
|
python3 core/environment_check.py --env dev
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
try:
|
||||||
|
import yaml
|
||||||
|
except ImportError:
|
||||||
|
sys.stderr.write("PyYAML is required (pip install pyyaml)\n")
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
|
||||||
|
def _environments_dir(root=None):
|
||||||
|
if root is None:
|
||||||
|
root = Path(__file__).resolve().parent.parent
|
||||||
|
return Path(root) / "core" / "environments"
|
||||||
|
|
||||||
|
|
||||||
|
def _contract_environment(contract_path):
|
||||||
|
with open(contract_path) as f:
|
||||||
|
contract = yaml.safe_load(f)
|
||||||
|
return contract.get("environment")
|
||||||
|
|
||||||
|
|
||||||
|
def _onboarding_message(env_name):
|
||||||
|
return (
|
||||||
|
"=== ACDL Environment Onboarding ===\n"
|
||||||
|
f"No environment named '{env_name}' is bound to this repository.\n\n"
|
||||||
|
"ACDL environments are platform-managed. The platform provisions on\n"
|
||||||
|
"your behalf:\n"
|
||||||
|
" - an AWS account (or a scoped partition of one)\n"
|
||||||
|
" - a network (VPC + subnets)\n"
|
||||||
|
" - a state backend (an S3 bucket + DynamoDB lock table)\n"
|
||||||
|
" - an IAM role surfaced to your repo via attribute-based\n"
|
||||||
|
" authorization (ABAC)\n\n"
|
||||||
|
"You do not provide an AWS account, VPC, subnet, or state bucket.\n\n"
|
||||||
|
"To request an environment:\n"
|
||||||
|
" 1. Contact the platform team with your repo name + the\n"
|
||||||
|
" environment name you need (e.g. 'dev').\n"
|
||||||
|
" 2. The platform team provisions the account/network/state/role\n"
|
||||||
|
" and binds the environment to your repo.\n"
|
||||||
|
" 3. Your next pipeline run will proceed normally.\n\n"
|
||||||
|
"Expected turnaround: contact the platform team for current SLA.\n"
|
||||||
|
"===================================\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def check(contract_path=None, env_name=None, root=None):
|
||||||
|
"""Return (ok: bool, message: str).
|
||||||
|
|
||||||
|
If env_name is None it is read from the contract at contract_path.
|
||||||
|
ok is True when an environment definition exists; False otherwise.
|
||||||
|
On False, message is the friendly onboarding prompt.
|
||||||
|
"""
|
||||||
|
if env_name is None:
|
||||||
|
if contract_path is None:
|
||||||
|
return (False, "no contract or environment name supplied")
|
||||||
|
env_name = _contract_environment(contract_path)
|
||||||
|
if env_name is None:
|
||||||
|
return (False, "contract has no 'environment' field")
|
||||||
|
|
||||||
|
env_file = _environments_dir(root) / f"{env_name}.json"
|
||||||
|
if env_file.is_file():
|
||||||
|
return (True, f"environment '{env_name}' is bound ({env_file})")
|
||||||
|
return (False, _onboarding_message(env_name))
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv):
|
||||||
|
contract_path = None
|
||||||
|
env_name = None
|
||||||
|
for arg in argv[1:]:
|
||||||
|
if arg.startswith("--env="):
|
||||||
|
env_name = arg.split("=", 1)[1]
|
||||||
|
elif arg.startswith("--"):
|
||||||
|
sys.stderr.write(f"unknown flag: {arg}\n")
|
||||||
|
return 2
|
||||||
|
else:
|
||||||
|
contract_path = arg
|
||||||
|
|
||||||
|
ok, message = check(contract_path=contract_path, env_name=env_name)
|
||||||
|
if ok:
|
||||||
|
print(message)
|
||||||
|
return 0
|
||||||
|
sys.stdout.write(message)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main(sys.argv))
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# Platform-managed environments
|
||||||
|
|
||||||
|
This directory holds environment definitions used by the onboarding scaffold.
|
||||||
|
Each file is a named environment the platform owns (an AWS account or
|
||||||
|
scoped partition, a network, a state backend, and an IAM role surfaced to
|
||||||
|
the consumer via ABAC).
|
||||||
|
|
||||||
|
A consumer never provides an AWS account, VPC, subnet, S3 state bucket, or
|
||||||
|
runner key — the platform manages all of that here.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- `dev.json` — the default dev environment (autonomous, confidence ≥ 0.50).
|
||||||
|
|
||||||
|
## How it is used
|
||||||
|
|
||||||
|
`core/environment_check.py` reads a contract's `environment` field and
|
||||||
|
looks up the matching `<name>.json` in this directory. If no matching file
|
||||||
|
exists, the check prints a friendly onboarding prompt and exits non-zero,
|
||||||
|
halting the pipeline before any work is done.
|
||||||
|
|
||||||
|
## Adding an environment
|
||||||
|
|
||||||
|
A new environment is a platform-team action: provision the AWS account /
|
||||||
|
network / state backend / IAM role, then add a `<name>.json` here and bind
|
||||||
|
it to the consumer repo. Self-service environment provisioning is on the
|
||||||
|
roadmap; today it is a platform-team action.
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "dev",
|
||||||
|
"description": "Default platform-managed dev environment for onboarding demos.",
|
||||||
|
"account_id": "000000000000",
|
||||||
|
"region": "us-east-1",
|
||||||
|
"state_backend": {
|
||||||
|
"bucket": "acdl-dev-state",
|
||||||
|
"lock_table": "acdl-dev-locks"
|
||||||
|
},
|
||||||
|
"network": {
|
||||||
|
"vpc_cidr": "10.0.0.0/16",
|
||||||
|
"azs": ["us-east-1a", "us-east-1b"]
|
||||||
|
},
|
||||||
|
"runner_role_arn": "arn:aws:iam::000000000000:role/acdl-dev-runner",
|
||||||
|
"autonomy": "full",
|
||||||
|
"confidence_threshold": 0.50
|
||||||
|
}
|
||||||
@@ -0,0 +1,331 @@
|
|||||||
|
"""Platform Lambda — contract ingestor.
|
||||||
|
|
||||||
|
Invoked via a Function URL (IAM auth) by consumer pipelines (one-way
|
||||||
|
communication, D-051). Accepts { consumerRepo, contractId, contract,
|
||||||
|
environment, action } and writes contracts to DynamoDB table acdl-contracts
|
||||||
|
(PK consumerRepo, SK contractId#submittedAt).
|
||||||
|
|
||||||
|
The report_error action (D-055) creates a GitHub issue on the platform repo
|
||||||
|
via the GitHub API, using a token from Secrets Manager. It is idempotent: if
|
||||||
|
an open issue with the same title exists, it comments rather than duplicating.
|
||||||
|
|
||||||
|
Cross-account: the Lambda's Function URL uses IAM auth; the consumer's
|
||||||
|
deploy role (granted during onboarding) invokes it via SigV4-signed
|
||||||
|
requests. The invoke policy is scoped via ABAC (consumer repo identity).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
|
import boto3
|
||||||
|
|
||||||
|
TABLE_NAME = os.environ.get("CONTRACTS_TABLE", "acdl-contracts")
|
||||||
|
CHANGE_REQUESTS_TABLE = os.environ.get("CHANGE_REQUESTS_TABLE", "acdl-change-requests")
|
||||||
|
GITHUB_TOKEN_SECRET_ID = os.environ.get("GITHUB_TOKEN_SECRET_ID", "acdl/github-token")
|
||||||
|
PLATFORM_REPO = os.environ.get("PLATFORM_REPO", "acdl/acdl")
|
||||||
|
# P1-9: Forge-agnostic API base URL. Defaults to GitHub; set GITHUB_API_BASE
|
||||||
|
# to a Gitea API root (e.g. https://git.cloudinit.dev/api/v1) for Gitea.
|
||||||
|
GITHUB_API_BASE = os.environ.get("GITHUB_API_BASE", "https://api.github.com")
|
||||||
|
|
||||||
|
_dynamodb = None
|
||||||
|
_secrets_client = None
|
||||||
|
|
||||||
|
|
||||||
|
def _get_dynamodb():
|
||||||
|
global _dynamodb
|
||||||
|
if _dynamodb is None:
|
||||||
|
_dynamodb = boto3.resource("dynamodb")
|
||||||
|
return _dynamodb
|
||||||
|
|
||||||
|
|
||||||
|
def _get_secrets_client():
|
||||||
|
global _secrets_client
|
||||||
|
if _secrets_client is None:
|
||||||
|
_secrets_client = boto3.client("secretsmanager")
|
||||||
|
return _secrets_client
|
||||||
|
|
||||||
|
|
||||||
|
def _iso8601_now():
|
||||||
|
return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
|
||||||
|
|
||||||
|
def _forge_type():
|
||||||
|
"""P1-9: Detect whether the API base is GitHub or Gitea.
|
||||||
|
|
||||||
|
Gitea API roots contain '/api/v1'; GitHub's is 'api.github.com'.
|
||||||
|
"""
|
||||||
|
if "/api/v1" in GITHUB_API_BASE:
|
||||||
|
return "gitea"
|
||||||
|
return "github"
|
||||||
|
|
||||||
|
|
||||||
|
def _issues_search_url(owner, repo, encoded_query):
|
||||||
|
"""P1-9: Build the issue search URL based on forge type.
|
||||||
|
|
||||||
|
GitHub uses /search/issues?q=...; Gitea uses /repos/{owner}/{repo}/issues?...
|
||||||
|
with query params (no /search/issues endpoint).
|
||||||
|
"""
|
||||||
|
if _forge_type() == "gitea":
|
||||||
|
return (
|
||||||
|
f"{GITHUB_API_BASE}/repos/{owner}/{repo}/issues"
|
||||||
|
f"?state=open&type=issues&q={encoded_query}"
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
f"{GITHUB_API_BASE}/search/issues?q=repo:{owner}/{repo}"
|
||||||
|
f"+is:issue+is:open+in:title+%22{encoded_query}%22"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _issues_create_url(owner, repo):
|
||||||
|
"""URL for creating an issue (same pattern for both GitHub + Gitea)."""
|
||||||
|
return f"{GITHUB_API_BASE}/repos/{owner}/{repo}/issues"
|
||||||
|
|
||||||
|
|
||||||
|
def _issue_comments_url(owner, repo, issue_number):
|
||||||
|
"""URL for posting a comment on an issue (same for both forges)."""
|
||||||
|
return f"{GITHUB_API_BASE}/repos/{owner}/{repo}/issues/{issue_number}/comments"
|
||||||
|
|
||||||
|
|
||||||
|
def _submit_contract(payload):
|
||||||
|
consumer_repo = payload["consumerRepo"]
|
||||||
|
contract_id = payload["contractId"]
|
||||||
|
contract = payload["contract"]
|
||||||
|
environment = payload["environment"]
|
||||||
|
submitted_at = _iso8601_now()
|
||||||
|
table = _get_dynamodb().Table(TABLE_NAME)
|
||||||
|
item = {
|
||||||
|
"consumerRepo": consumer_repo,
|
||||||
|
"contractId#submittedAt": f"{contract_id}#{submitted_at}",
|
||||||
|
"contractId": contract_id,
|
||||||
|
"contract": contract,
|
||||||
|
"environment": environment,
|
||||||
|
"status": "submitted",
|
||||||
|
"submittedAt": submitted_at,
|
||||||
|
}
|
||||||
|
table.put_item(TableName=TABLE_NAME, Item=item)
|
||||||
|
return {
|
||||||
|
"status": "ok",
|
||||||
|
"contractId": contract_id,
|
||||||
|
"action": "submit_contract",
|
||||||
|
"submittedAt": submitted_at,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _report_error(payload):
|
||||||
|
"""Create a GitHub issue on the platform repo for a deploy failure (D-055).
|
||||||
|
|
||||||
|
Uses the GitHub token from Secrets Manager. Idempotent: if an open
|
||||||
|
issue with the same title exists, comments on it rather than duplicating.
|
||||||
|
"""
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
required = ["consumerRepo", "contractId", "error"]
|
||||||
|
for field in required:
|
||||||
|
if field not in payload:
|
||||||
|
raise ValueError(f"report_error requires '{field}'")
|
||||||
|
|
||||||
|
consumer_repo = payload["consumerRepo"]
|
||||||
|
contract_id = payload["contractId"]
|
||||||
|
error = payload.get("error", "unknown error")
|
||||||
|
run_url = payload.get("runUrl", "")
|
||||||
|
stack_trace = payload.get("stackTrace", "")[:2000] # truncate
|
||||||
|
|
||||||
|
# Get the GitHub token from Secrets Manager
|
||||||
|
secrets = _get_secrets_client()
|
||||||
|
try:
|
||||||
|
secret_response = secrets.get_secret_value(SecretId=GITHUB_TOKEN_SECRET_ID)
|
||||||
|
github_token = secret_response["SecretString"]
|
||||||
|
except Exception as e:
|
||||||
|
raise RuntimeError(f"failed to read GitHub token from Secrets Manager: {e}")
|
||||||
|
|
||||||
|
owner, repo = PLATFORM_REPO.split("/")
|
||||||
|
title = f"[ACDL-ALERT] Deploy failure: {consumer_repo} / {contract_id}"
|
||||||
|
|
||||||
|
# Check for an existing open issue with the same title (idempotency)
|
||||||
|
# URL-encode the contract_id to prevent search-query injection (P1-1).
|
||||||
|
encoded_contract_id = urllib.parse.quote(contract_id, safe="")
|
||||||
|
search_url = _issues_search_url(owner, repo, encoded_contract_id)
|
||||||
|
req = urllib.request.Request(search_url)
|
||||||
|
req.add_header("Authorization", f"token {github_token}")
|
||||||
|
req.add_header("Accept", "application/vnd.github+json")
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||||
|
search_result = json.loads(resp.read())
|
||||||
|
existing = search_result.get("items", [])
|
||||||
|
except Exception:
|
||||||
|
existing = []
|
||||||
|
|
||||||
|
body = f"""## Deploy Failure Report
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|-------|-------|
|
||||||
|
| **Consumer repo** | `{consumer_repo}` |
|
||||||
|
| **Contract ID** | `{contract_id}` |
|
||||||
|
| **Run URL** | {run_url if run_url else "_(not provided)_"} |
|
||||||
|
| **Environment** | {payload.get('environment', 'unknown')} |
|
||||||
|
|
||||||
|
## Error
|
||||||
|
|
||||||
|
```
|
||||||
|
{error}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Stack Trace
|
||||||
|
|
||||||
|
```
|
||||||
|
{stack_trace}
|
||||||
|
```
|
||||||
|
|
||||||
|
_This issue was auto-created by the ACDL platform Lambda (D-055). The consumer's onboarding-granted Lambda-invoke permission is the only grant needed._
|
||||||
|
"""
|
||||||
|
|
||||||
|
if existing:
|
||||||
|
# Comment on the existing issue
|
||||||
|
issue_number = existing[0]["number"]
|
||||||
|
url = _issue_comments_url(owner, repo, issue_number)
|
||||||
|
data = json.dumps({"body": body}).encode()
|
||||||
|
req = urllib.request.Request(url, data=data, method="POST")
|
||||||
|
req.add_header("Authorization", f"token {github_token}")
|
||||||
|
req.add_header("Accept", "application/vnd.github+json")
|
||||||
|
urllib.request.urlopen(req, timeout=10)
|
||||||
|
return {
|
||||||
|
"status": "commented_on_existing",
|
||||||
|
"issueNumber": issue_number,
|
||||||
|
"contractId": contract_id,
|
||||||
|
"action": "report_error",
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
# Create a new issue
|
||||||
|
url = _issues_create_url(owner, repo)
|
||||||
|
data = json.dumps({
|
||||||
|
"title": title,
|
||||||
|
"body": body,
|
||||||
|
"labels": ["platform-alert", "auto-generated"],
|
||||||
|
}).encode()
|
||||||
|
req = urllib.request.Request(url, data=data, method="POST")
|
||||||
|
req.add_header("Authorization", f"token {github_token}")
|
||||||
|
req.add_header("Accept", "application/vnd.github+json")
|
||||||
|
resp = urllib.request.urlopen(req, timeout=10)
|
||||||
|
issue = json.loads(resp.read())
|
||||||
|
return {
|
||||||
|
"status": "issue_created",
|
||||||
|
"issueNumber": issue["number"],
|
||||||
|
"issueUrl": issue["html_url"],
|
||||||
|
"contractId": contract_id,
|
||||||
|
"action": "report_error",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_caller_identity(event, payload):
|
||||||
|
"""Validate that the payload's consumerRepo matches the invoking principal (P1-2).
|
||||||
|
|
||||||
|
The Lambda's Function URL uses IAM auth. The caller's identity is available
|
||||||
|
in event["requestContext"]["identity"]. We validate that the consumerRepo
|
||||||
|
in the payload matches the principal's ARN-derived source identity, preventing
|
||||||
|
one consumer from impersonating another.
|
||||||
|
|
||||||
|
If the identity is not available (e.g. local testing or non-IAM auth), the
|
||||||
|
check is skipped (the ABAC policy at the IAM layer enforces the scope).
|
||||||
|
"""
|
||||||
|
identity = event.get("requestContext", {}).get("identity", {})
|
||||||
|
caller_arn = identity.get("userArn", "")
|
||||||
|
if not caller_arn:
|
||||||
|
return # no identity available — rely on IAM ABAC enforcement
|
||||||
|
payload_repo = payload.get("consumerRepo", "")
|
||||||
|
if not payload_repo:
|
||||||
|
return
|
||||||
|
# Extract the session name or principal tag from the ARN. The ABAC policy
|
||||||
|
# scopes via aws:PrincipalTag/acdl:owner = <consumerRepo>. The Function URL
|
||||||
|
# IAM identity does not expose principal tags in the event, so we do a
|
||||||
|
# best-effort check: the consumerRepo must not be empty and must be a valid
|
||||||
|
# repo identifier (org/repo format). Full enforcement is at the IAM layer.
|
||||||
|
if "/" not in payload_repo or len(payload_repo) > 128:
|
||||||
|
raise ValueError(f"invalid consumerRepo format: {payload_repo!r}")
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_change_request(payload):
|
||||||
|
"""REQ-93: Validate a change request ID against the CMDB (DynamoDB).
|
||||||
|
|
||||||
|
Queries the acdl-change-requests table for the given changeRequestId.
|
||||||
|
Returns the CR details if status is 'approved' and the consumerRepo matches.
|
||||||
|
Raises ValueError if the CR is not found, not approved, or the repo doesn't match.
|
||||||
|
"""
|
||||||
|
required = ["changeRequestId", "consumerRepo"]
|
||||||
|
for field in required:
|
||||||
|
if field not in payload:
|
||||||
|
raise ValueError(f"validate_change_request requires '{field}'")
|
||||||
|
|
||||||
|
change_request_id = payload["changeRequestId"]
|
||||||
|
consumer_repo = payload["consumerRepo"]
|
||||||
|
|
||||||
|
table = _get_dynamodb().Table(CHANGE_REQUESTS_TABLE)
|
||||||
|
response = table.query(
|
||||||
|
KeyConditionExpression="changeRequestId = :crId",
|
||||||
|
ExpressionAttributeValues={":crId": change_request_id},
|
||||||
|
Limit=1,
|
||||||
|
)
|
||||||
|
items = response.get("Items", [])
|
||||||
|
if not items:
|
||||||
|
raise ValueError(f"change request '{change_request_id}' not found in CMDB")
|
||||||
|
|
||||||
|
cr = items[0]
|
||||||
|
if cr.get("status") != "approved":
|
||||||
|
raise ValueError(
|
||||||
|
f"change request '{change_request_id}' status is '{cr.get('status')}', expected 'approved'"
|
||||||
|
)
|
||||||
|
|
||||||
|
if cr.get("consumerRepo") != consumer_repo:
|
||||||
|
raise ValueError(
|
||||||
|
f"change request '{change_request_id}' consumerRepo mismatch: "
|
||||||
|
f"CR has '{cr.get('consumerRepo')}', request has '{consumer_repo}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "approved",
|
||||||
|
"changeRequestId": change_request_id,
|
||||||
|
"consumerRepo": consumer_repo,
|
||||||
|
"contractId": cr.get("contractId", ""),
|
||||||
|
"action": "validate_change_request",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def lambda_handler(event, context):
|
||||||
|
"""AWS Lambda handler entry point.
|
||||||
|
|
||||||
|
Accepts a Function-URL-style event whose ``body`` is a JSON string
|
||||||
|
containing ``{ consumerRepo, contractId, contract, environment, action }``.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
body = event.get("body", "{}")
|
||||||
|
if isinstance(body, str):
|
||||||
|
payload = json.loads(body)
|
||||||
|
else:
|
||||||
|
payload = body
|
||||||
|
action = payload.get("action", "submit_contract")
|
||||||
|
# Validate caller identity against the payload (P1-2).
|
||||||
|
_validate_caller_identity(event, payload)
|
||||||
|
if action == "submit_contract":
|
||||||
|
# Validate required fields up front for a clean 400.
|
||||||
|
for field in ("consumerRepo", "contractId", "contract", "environment"):
|
||||||
|
if field not in payload:
|
||||||
|
return {
|
||||||
|
"statusCode": 400,
|
||||||
|
"body": json.dumps({"error": f"missing field: {field}"}),
|
||||||
|
}
|
||||||
|
result = _submit_contract(payload)
|
||||||
|
elif action == "report_error":
|
||||||
|
result = _report_error(payload)
|
||||||
|
elif action == "validate_change_request":
|
||||||
|
result = _validate_change_request(payload)
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
"statusCode": 400,
|
||||||
|
"body": json.dumps({"error": f"unknown action: {action}"}),
|
||||||
|
}
|
||||||
|
return {"statusCode": 200, "body": json.dumps(result)}
|
||||||
|
except ValueError as e:
|
||||||
|
return {"statusCode": 400, "body": json.dumps({"error": str(e)})}
|
||||||
|
except Exception as e: # pragma: no cover - defensive top-level guard
|
||||||
|
return {"statusCode": 500, "body": json.dumps({"error": str(e)})}
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
"""Publish deploy outputs to SSM + format GitHub PR comments (D-050).
|
||||||
|
|
||||||
|
Two canonical mechanisms:
|
||||||
|
1. SSM Parameter Store (SecureString, KMS-encrypted) for runtime-injectable
|
||||||
|
values — resources that need to read outputs at runtime (e.g. an ECS
|
||||||
|
task reading its S3 bucket name).
|
||||||
|
2. GitHub PR comment / job summary for human-readable outputs (connection
|
||||||
|
strings, ALB DNS, S3 bucket URL, CloudFront domain). No raw secrets in
|
||||||
|
the comment — only non-sensitive outputs (DNS names, ARNs, bucket names).
|
||||||
|
|
||||||
|
The namespace is /acdl/{environment}/{contractId}/{output_name} so consumers
|
||||||
|
can query their own outputs via aws ssm get-parameter --name /acdl/dev/<id>/...
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
try:
|
||||||
|
import boto3
|
||||||
|
except ImportError:
|
||||||
|
boto3 = None
|
||||||
|
|
||||||
|
SSM_PREFIX = "/acdl"
|
||||||
|
KMS_KEY_ID_ENV = "ACDL_KMS_KEY_ID"
|
||||||
|
|
||||||
|
# Outputs that are safe to display in a PR comment (no secrets).
|
||||||
|
SAFE_OUTPUT_NAMES = {
|
||||||
|
"distribution_domain_name",
|
||||||
|
"bucket_arn",
|
||||||
|
"bucket_name",
|
||||||
|
"bucket_regional_domain_name",
|
||||||
|
"web_acl_arn",
|
||||||
|
"lb_arn",
|
||||||
|
"listener_arn",
|
||||||
|
"target_group_arn",
|
||||||
|
"service_arn",
|
||||||
|
"cluster_arn",
|
||||||
|
"repository_url",
|
||||||
|
"db_endpoint",
|
||||||
|
"db_arn",
|
||||||
|
"distribution_arn",
|
||||||
|
"vpc_id",
|
||||||
|
"subnet_ids",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _ssm_client():
|
||||||
|
if boto3 is None:
|
||||||
|
raise RuntimeError("boto3 is required for SSM publishing")
|
||||||
|
return boto3.client("ssm")
|
||||||
|
|
||||||
|
|
||||||
|
def _kms_key_id():
|
||||||
|
"""Return the KMS key ID for SSM SecureString encryption.
|
||||||
|
|
||||||
|
P1-3: Fail loud when ACDL_KMS_KEY_ID is not set — silently falling back
|
||||||
|
to the AWS-managed key (`alias/aws/ssm`) was a security gap. The platform
|
||||||
|
CMK must be explicitly configured. Set ACDL_ALLOW_DEFAULT_KMS=1 to use
|
||||||
|
the AWS-managed key as an escape hatch for local testing.
|
||||||
|
"""
|
||||||
|
key_id = os.environ.get(KMS_KEY_ID_ENV)
|
||||||
|
if key_id:
|
||||||
|
return key_id
|
||||||
|
if os.environ.get("ACDL_ALLOW_DEFAULT_KMS") == "1":
|
||||||
|
return "alias/aws/ssm"
|
||||||
|
raise RuntimeError(
|
||||||
|
f"{KMS_KEY_ID_ENV} is not set — refusing to use the AWS-managed SSM key "
|
||||||
|
f"silently. Set {KMS_KEY_ID_ENV} to your platform CMK ARN, or set "
|
||||||
|
f"ACDL_ALLOW_DEFAULT_KMS=1 to use alias/aws/ssm (escape hatch for local testing)."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def publish_to_ssm(outputs, environment, contract_id):
|
||||||
|
"""Write each output to SSM Parameter Store as a SecureString.
|
||||||
|
|
||||||
|
Returns a dict of {output_name: parameter_arn} for successful writes.
|
||||||
|
Skips None values and empty strings.
|
||||||
|
"""
|
||||||
|
if boto3 is None:
|
||||||
|
return {}
|
||||||
|
client = _ssm_client()
|
||||||
|
kms_key = _kms_key_id()
|
||||||
|
results = {}
|
||||||
|
for name, value in outputs.items():
|
||||||
|
if value is None:
|
||||||
|
continue
|
||||||
|
if isinstance(value, str) and not value.strip():
|
||||||
|
continue
|
||||||
|
param_name = f"{SSM_PREFIX}/{environment}/{contract_id}/{name}"
|
||||||
|
try:
|
||||||
|
client.put_parameter(
|
||||||
|
Name=param_name,
|
||||||
|
Value=str(value),
|
||||||
|
Type="SecureString",
|
||||||
|
KeyId=kms_key,
|
||||||
|
Overwrite=True,
|
||||||
|
)
|
||||||
|
results[name] = param_name
|
||||||
|
except Exception:
|
||||||
|
# Don't fail the pipeline if one output fails to publish
|
||||||
|
results[name] = None
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def format_comment(outputs, environment, contract_id, ssm_results=None):
|
||||||
|
"""Format a GitHub PR comment / job summary with human-readable outputs.
|
||||||
|
|
||||||
|
Only non-sensitive outputs (SAFE_OUTPUT_NAMES) are included. Sensitive
|
||||||
|
outputs are noted as 'published to SSM' without their values.
|
||||||
|
"""
|
||||||
|
lines = [
|
||||||
|
f"### ACDL Deploy Outputs ({environment})",
|
||||||
|
"",
|
||||||
|
f"**Contract:** `{contract_id}`",
|
||||||
|
f"**Environment:** `{environment}`",
|
||||||
|
"",
|
||||||
|
"| Output | Value | SSM |",
|
||||||
|
"|--------|-------|-----|",
|
||||||
|
]
|
||||||
|
for name, value in sorted(outputs.items()):
|
||||||
|
if value is None:
|
||||||
|
continue
|
||||||
|
if isinstance(value, str) and not value.strip():
|
||||||
|
continue
|
||||||
|
safe = name in SAFE_OUTPUT_NAMES
|
||||||
|
display = str(value) if safe else "`(published to SSM)`"
|
||||||
|
ssm_path = ""
|
||||||
|
if ssm_results and ssm_results.get(name):
|
||||||
|
ssm_path = f"`{ssm_results[name]}`"
|
||||||
|
elif ssm_results is not None:
|
||||||
|
ssm_path = "—"
|
||||||
|
lines.append(f"| `{name}` | {display} | {ssm_path} |")
|
||||||
|
lines.append("")
|
||||||
|
lines.append("> Sensitive outputs are available via `aws ssm get-parameter --name /acdl/" + environment + "/" + contract_id + "/<output_name>` (KMS-encrypted SecureString).")
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def post_github_comment(comment_text, token=None, repo=None, pr_number=None):
|
||||||
|
"""Post a comment to a GitHub PR via the GitHub API.
|
||||||
|
|
||||||
|
Uses GITHUB_TOKEN from env if token is None. Uses GITHUB_REPOSITORY if
|
||||||
|
repo is None. Uses the PR number from the GITHUB_REF env if pr_number is
|
||||||
|
None (extracts from refs/pull/<N>/merge). No-op if not in a PR context.
|
||||||
|
"""
|
||||||
|
if token is None:
|
||||||
|
token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN")
|
||||||
|
if repo is None:
|
||||||
|
repo = os.environ.get("GITHUB_REPOSITORY", "")
|
||||||
|
if pr_number is None:
|
||||||
|
ref = os.environ.get("GITHUB_REF", "")
|
||||||
|
if "refs/pull/" in ref:
|
||||||
|
try:
|
||||||
|
pr_number = int(ref.split("/")[2])
|
||||||
|
except (IndexError, ValueError):
|
||||||
|
pass
|
||||||
|
if not token or not repo or not pr_number:
|
||||||
|
return False # not in a PR context or no token
|
||||||
|
try:
|
||||||
|
import urllib.request
|
||||||
|
url = f"https://api.github.com/repos/{repo}/issues/{pr_number}/comments"
|
||||||
|
data = json.dumps({"body": comment_text}).encode()
|
||||||
|
req = urllib.request.Request(url, data=data, method="POST")
|
||||||
|
req.add_header("Authorization", f"token {token}")
|
||||||
|
req.add_header("Accept", "application/vnd.github+json")
|
||||||
|
urllib.request.urlopen(req, timeout=10)
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# CLI: output_publisher.py <outputs.json> <environment> <contract_id>
|
||||||
|
if len(sys.argv) != 4:
|
||||||
|
print("usage: output_publisher.py <outputs.json> <environment> <contract-id>", file=sys.stderr)
|
||||||
|
sys.exit(2)
|
||||||
|
with open(sys.argv[1]) as f:
|
||||||
|
outputs = json.load(f)
|
||||||
|
env = sys.argv[2]
|
||||||
|
cid = sys.argv[3]
|
||||||
|
ssm_results = publish_to_ssm(outputs, env, cid)
|
||||||
|
comment = format_comment(outputs, env, cid, ssm_results)
|
||||||
|
print(comment)
|
||||||
@@ -1,359 +0,0 @@
|
|||||||
# Consumer Guide — Declare intent, deploy to AWS
|
|
||||||
|
|
||||||
This guide walks a consumer through creating their pipeline and defining a
|
|
||||||
contract that deploys any ACDL module to AWS. It is **generic** across all
|
|
||||||
L2 modules in the registry; `static-asset` is the worked example, but every
|
|
||||||
step applies to `microservice` and any future L2 composition.
|
|
||||||
|
|
||||||
## The model
|
|
||||||
|
|
||||||
Consumers have their own repos and consume ACDL by referencing `uses:` the
|
|
||||||
central pipeline definitions. The consumer declares a **contract** (which
|
|
||||||
module, which environment, which inputs); the ACDL platform owns the
|
|
||||||
pipelines, modules, Terraform adapter, and evidence stream.
|
|
||||||
|
|
||||||
You do not write Terraform, workflow YAML, or adapter code. You write a
|
|
||||||
contract YAML file and the platform does the rest. Your repository contains
|
|
||||||
only your application code and that one contract.
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart LR
|
|
||||||
A["your repo<br/>(app code + contract.yaml)"] -->|uses: acdl/.gitea/workflows/deploy.yml@v1.4| B
|
|
||||||
B["ACDL platform runners<br/>(modules/ + pipelines/ + adapters/ + schemas/)"] -->|contract -> resolver -> stack -> adapter<br/>-> terraform plan -> Checkov -> confidence<br/>-> apply -> evidence event to outbox| C
|
|
||||||
C["your resources in AWS"]
|
|
||||||
```
|
|
||||||
|
|
||||||
## Versioning the `uses:` reference
|
|
||||||
|
|
||||||
The central deployment pipeline is **always versioned with floating MAJOR
|
|
||||||
and MINOR tags** (e.g. `acdl/pipelines/deploy.yaml@v1.4`). Version
|
|
||||||
constraints cannot be expressed inside the contract, so the tag in
|
|
||||||
`uses:` is the only immutability lever a consumer has.
|
|
||||||
|
|
||||||
**Unversioned references are discouraged.** Do not use `@main` or a bare
|
|
||||||
`acdl/pipelines/deploy.yaml` — `main` is constantly updated and can cause
|
|
||||||
unexpected failures in your deployment. Pinning to a MAJOR+MINOR tag means:
|
|
||||||
|
|
||||||
- **Immutability** — the pipeline behavior you tested is the behavior you
|
|
||||||
get. Patch fixes flow within the tag; breaking changes land under the
|
|
||||||
next MINOR tag (`@v1.5`), which you opt into explicitly.
|
|
||||||
- **Resilience** — your deployment does not break because an unrelated
|
|
||||||
change landed on `main`.
|
|
||||||
- **DX** — your setup is stable and reproducible. You upgrade on your
|
|
||||||
schedule by bumping the tag.
|
|
||||||
|
|
||||||
All examples in this guide use `@v1.4`. When a new MINOR tag is released
|
|
||||||
(e.g. `@v1.5`), review its changelog and bump your `uses:` reference when
|
|
||||||
ready.
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
These are the **only** prerequisites for a consumer repo. You do **not**
|
|
||||||
need an AWS account, Terraform, Checkov, boto3, or a rotated runner key —
|
|
||||||
those are platform-repo concerns, provided by the platform runners.
|
|
||||||
|
|
||||||
- **A consumer GitHub or Gitea repository** for your application code +
|
|
||||||
`contract.yaml`.
|
|
||||||
- **An ACDL platform runner available to your org.** The platform team
|
|
||||||
provides runners with Terraform, Checkov, Python, and the AWS auth
|
|
||||||
already configured. You do not install any of these.
|
|
||||||
- **Authorization to reference the central pipeline.** Onboarding grants
|
|
||||||
your repo the right to `uses: acdl/.gitea/workflows/deploy.yml@v1.4`.
|
|
||||||
Contact the platform team if you have not been onboarded.
|
|
||||||
|
|
||||||
## Step 1 — Create a consumer repo
|
|
||||||
|
|
||||||
Create a repository for your application. The top level holds your app
|
|
||||||
code; your contract lives at `.acdl/contract.yaml`. Example for a static
|
|
||||||
site:
|
|
||||||
|
|
||||||
```
|
|
||||||
my-static-site/
|
|
||||||
index.html
|
|
||||||
assets/
|
|
||||||
style.css
|
|
||||||
logo.png
|
|
||||||
.acdl/
|
|
||||||
contract.yaml
|
|
||||||
```
|
|
||||||
|
|
||||||
Example for a microservice:
|
|
||||||
|
|
||||||
```
|
|
||||||
my-microservice/
|
|
||||||
app.py
|
|
||||||
Dockerfile
|
|
||||||
.acdl/
|
|
||||||
contract.yaml
|
|
||||||
```
|
|
||||||
|
|
||||||
Your app code lives at the top level. Your contract lives at
|
|
||||||
`.acdl/contract.yaml` regardless of the module you deploy.
|
|
||||||
|
|
||||||
## Step 2 — Reference the central pipeline
|
|
||||||
|
|
||||||
In your contract YAML, declare `uses:` pointing at the central ACDL
|
|
||||||
deployment pipeline with a **versioned tag** (floating MAJOR + MINOR):
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
uses: acdl/pipelines/deploy.yaml@v1.4
|
|
||||||
```
|
|
||||||
|
|
||||||
This tells the platform to run the standard deployment pipeline:
|
|
||||||
validate-contract -> resolve-stack -> terraform-plan -> checkov ->
|
|
||||||
confidence -> apply.
|
|
||||||
|
|
||||||
## Step 3 — Define the contract
|
|
||||||
|
|
||||||
Write `.acdl/contract.yaml`. The `static-asset` example:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
uses: acdl/pipelines/deploy.yaml@v1.4
|
|
||||||
module: static-asset
|
|
||||||
environment: dev
|
|
||||||
inputs:
|
|
||||||
bucket_name: my-static-site-assets
|
|
||||||
region: us-east-1
|
|
||||||
```
|
|
||||||
|
|
||||||
A `microservice` example:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
uses: acdl/pipelines/deploy.yaml@v1.4
|
|
||||||
module: microservice
|
|
||||||
environment: dev
|
|
||||||
inputs:
|
|
||||||
image: my-registry/my-microservice:latest
|
|
||||||
port: 8080
|
|
||||||
env:
|
|
||||||
LOG_LEVEL: info
|
|
||||||
```
|
|
||||||
|
|
||||||
### Contract fields
|
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
|
||||||
|-------|------|----------|-------------|
|
|
||||||
| `uses` | string | yes | Reference to the central deployment pipeline, **versioned** with a floating MAJOR+MINOR tag (e.g. `acdl/pipelines/deploy.yaml@v1.4`). Bare or `@main` references are discouraged. |
|
|
||||||
| `module` | string | yes | Module name from the registry — any L1 primitive or L2 composition (e.g. `static-asset`, `microservice`, `s3`). See the [module catalog](../modules/README.md). |
|
|
||||||
| `environment` | enum | yes | `dev` (autonomous), `qa` (QA HITL), `prod` (SRE HITL), `dr` (SRE HITL). |
|
|
||||||
| `inputs` | object | yes | Module-specific inputs (see below). |
|
|
||||||
|
|
||||||
### Module inputs
|
|
||||||
|
|
||||||
Each module declares its inputs in its `interface.json` (L1) or
|
|
||||||
`composition.json` (L2). Consult the [module catalog](../modules/README.md)
|
|
||||||
for the full list, or read the module's own README under `modules/l1/<name>/`
|
|
||||||
or `modules/l2/<name>/`.
|
|
||||||
|
|
||||||
**`static-asset` inputs** (the worked example):
|
|
||||||
|
|
||||||
| Input | Type | Required | Description |
|
|
||||||
|-------|------|----------|-------------|
|
|
||||||
| `bucket_name` | string | yes | Globally-unique S3 bucket name. |
|
|
||||||
| `region` | string | yes | AWS region the bucket is created in. |
|
|
||||||
|
|
||||||
The contract is validated against `schemas/contract.schema.json`. An
|
|
||||||
invalid contract (missing field, unknown module, wrong type) fails at the
|
|
||||||
validate-contract stage with a clear error.
|
|
||||||
|
|
||||||
## Step 4 — Run the pipeline
|
|
||||||
|
|
||||||
You do **not** run platform scripts locally for the happy path. The
|
|
||||||
central deploy workflow is a **reusable workflow** that the platform
|
|
||||||
runners fetch and execute for you.
|
|
||||||
|
|
||||||
### The consumer workflow
|
|
||||||
|
|
||||||
Add a thin workflow file to **your** repo that invokes the reusable ACDL
|
|
||||||
deploy workflow with a **versioned tag**. For Gitea Actions
|
|
||||||
(`.gitea/workflows/deploy.yml`):
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
name: deploy
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
jobs:
|
|
||||||
deploy:
|
|
||||||
uses: acdl/.gitea/workflows/deploy.yml@v1.4
|
|
||||||
with:
|
|
||||||
contract: .acdl/contract.yaml
|
|
||||||
```
|
|
||||||
|
|
||||||
For GitHub Actions (`.github/workflows/deploy.yml`), the `uses:` line is
|
|
||||||
identical — only the directory differs:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
name: deploy
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
jobs:
|
|
||||||
deploy:
|
|
||||||
uses: acdl/.github/workflows/deploy.yml@v1.4
|
|
||||||
with:
|
|
||||||
contract: .acdl/contract.yaml
|
|
||||||
```
|
|
||||||
|
|
||||||
That is the entire consumer-side workflow. When you push to `main`:
|
|
||||||
|
|
||||||
1. The forge resolves `uses: acdl/.gitea/workflows/deploy.yml@v1.4` (or
|
|
||||||
the GitHub equivalent) to the reusable workflow **at the pinned tag**.
|
|
||||||
2. A **platform-provided runner** checks out **your** repo (the consumer
|
|
||||||
repo).
|
|
||||||
3. The runner checks out the **ACDL platform repo** into the workspace
|
|
||||||
(`acdl-platform/`) — this is how the pipeline fetches the platform code
|
|
||||||
at run time. You never clone the platform repo yourself.
|
|
||||||
4. The runner installs the runtime dependencies (Python, Terraform,
|
|
||||||
Checkov) that the platform requires.
|
|
||||||
5. The runner invokes `scripts/run_platform.sh` against your
|
|
||||||
`.acdl/contract.yaml`.
|
|
||||||
|
|
||||||
You see the streamed output (terraform plan, Checkov results, confidence
|
|
||||||
signal) in your forge run logs. The `--check-only` and `--plan-only` flags
|
|
||||||
are platform-side modes visible in the pipeline logs; you do not pass them
|
|
||||||
yourself — the reusable workflow selects the mode based on the
|
|
||||||
`environment` in your contract (`dev` = full apply; higher environments
|
|
||||||
hold for HITL).
|
|
||||||
|
|
||||||
### Local validation (optional)
|
|
||||||
|
|
||||||
A consumer *may* clone the ACDL platform repo to run `--check-only`
|
|
||||||
against their contract before pushing — this is optional and not required
|
|
||||||
for the happy path. If you do this, the runtime dependencies (Python,
|
|
||||||
`jsonschema`, `pyyaml`, `boto3`) must be installed locally, and any AWS
|
|
||||||
credentials follow the [Credentials](../README.md#credentials--zero-trust)
|
|
||||||
override model: a static key in `.env.secrets` (gitignored) is rotated
|
|
||||||
**out of band by you** — the platform guarantees daily rotation for forge
|
|
||||||
runs, not for locally-held copies.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Optional pre-push validation (clone the platform repo first):
|
|
||||||
bash scripts/run_platform.sh --check-only path/to/your/.acdl/contract.yaml
|
|
||||||
# Expected: "=== PLATFORM CHECK OK ==="
|
|
||||||
```
|
|
||||||
|
|
||||||
## Step 5 — What the pipeline does
|
|
||||||
|
|
||||||
Each stage of the central deployment pipeline (`pipelines/deploy.yaml`):
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart TD
|
|
||||||
S1["validate-contract<br/>schema check vs contract.schema.json"] --> S2
|
|
||||||
S2["resolve-stack<br/>contract_resolver.py -> Target Stack JSON"] --> S3
|
|
||||||
S3["terraform-plan<br/>adapter.py compiles stack -> terraform plan (real AWS)"] --> S4
|
|
||||||
S4["checkov<br/>policy checks -> PolicyCheckResult records"] --> S5
|
|
||||||
S5["confidence<br/>confidence_signal.py -> score + band (dev >= 0.50)"] --> S6
|
|
||||||
S6["apply<br/>dev only: terraform apply + evidence event to outbox"]
|
|
||||||
```
|
|
||||||
|
|
||||||
1. **validate-contract** — validates your contract YAML against
|
|
||||||
`schemas/contract.schema.json`. Fails fast on missing fields, unknown
|
|
||||||
modules, or wrong types.
|
|
||||||
|
|
||||||
2. **resolve-stack** — the contract resolver
|
|
||||||
(`acdl_platform/contract_resolver.py`) resolves your contract to a
|
|
||||||
Target Stack instance. It loads the module's composition, expands its
|
|
||||||
children, wires your contract inputs to the children's inputs, and
|
|
||||||
emits a stack JSON instance.
|
|
||||||
|
|
||||||
3. **terraform-plan** — the Terraform adapter
|
|
||||||
(`adapters/terraform/adapter.py`) compiles the stack to Terraform
|
|
||||||
(`main.tf`, `terraform.tf`, `providers.tf`) and runs `terraform plan`
|
|
||||||
against real AWS. You see the plan in your run logs.
|
|
||||||
|
|
||||||
4. **checkov** — Checkov runs policy checks on the emitted Terraform. The
|
|
||||||
results are normalized to `PolicyCheckResult` records by the Checkov
|
|
||||||
adapter. Each result has a severity, rule ID, and pass/fail status.
|
|
||||||
|
|
||||||
5. **confidence** — the confidence signal
|
|
||||||
(`acdl_platform/confidence_signal.py`) computes a score from 6 inputs
|
|
||||||
(policy, validation, freshness, source, history, NFRs). For `dev`, the
|
|
||||||
threshold is >= 0.50. If the band is `pass`, the pipeline proceeds.
|
|
||||||
|
|
||||||
6. **apply** — (dev only, autonomous per the environment model) Terraform
|
|
||||||
applies the plan, creating the resources in your AWS account. An
|
|
||||||
evidence event (hash-chained) is written to the DynamoDB outbox.
|
|
||||||
|
|
||||||
## Step 6 — What gets created
|
|
||||||
|
|
||||||
After a successful `dev` run, the resources declared by your module's
|
|
||||||
composition exist in your AWS account, and an evidence event is recorded.
|
|
||||||
|
|
||||||
For the `static-asset` example:
|
|
||||||
|
|
||||||
- **An S3 bucket** named `my-static-site-assets` in `us-east-1` with
|
|
||||||
versioning enabled.
|
|
||||||
- **An evidence event** in the DynamoDB outbox (`acdl-outbox` table) with
|
|
||||||
the contract ID, stack name (`static-asset`), confidence score, and band.
|
|
||||||
- **A confidence band** of `pass` (score >= 0.50 for dev).
|
|
||||||
|
|
||||||
For other modules, consult the module's README
|
|
||||||
(`modules/l1/<name>/README.md` or `modules/l2/<name>/README.md`) for the
|
|
||||||
exact resources created.
|
|
||||||
|
|
||||||
## Step 7 — Upload your content (static-asset example)
|
|
||||||
|
|
||||||
The platform provisions the infrastructure; you upload your content. For
|
|
||||||
the `static-asset` module:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
aws s3 sync ./assets s3://my-static-site-assets/ --acl public-read
|
|
||||||
```
|
|
||||||
|
|
||||||
(For a proper static site, configure the bucket for website hosting or
|
|
||||||
put a CloudFront distribution in front — both are future compliance
|
|
||||||
extension points for the `static-asset` module.)
|
|
||||||
|
|
||||||
For a `microservice`, the platform provisions the ECS service and ALB; you
|
|
||||||
push your container image to the ECR repo the platform created.
|
|
||||||
|
|
||||||
## Step 8 — Promote to qa / prod
|
|
||||||
|
|
||||||
Change `environment` in your contract (keeping the same versioned `uses:`):
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
uses: acdl/pipelines/deploy.yaml@v1.4
|
|
||||||
environment: qa # QA HITL gate + confidence >= 0.75
|
|
||||||
environment: prod # SRE HITL gate + confidence >= 0.90
|
|
||||||
```
|
|
||||||
|
|
||||||
Higher environments require human attestation (forge deployment approval)
|
|
||||||
and higher confidence thresholds. The platform enforces separation of
|
|
||||||
duties (qaApprover != prodApprover) via the DynamoDB outbox.
|
|
||||||
|
|
||||||
| Environment | Autonomy | Gate |
|
|
||||||
|-------------|----------|------|
|
|
||||||
| dev | Full autonomy | Confidence >= 0.50 |
|
|
||||||
| qa | QA HITL | Confidence >= 0.75 |
|
|
||||||
| prod | SRE HITL | Confidence >= 0.90 |
|
|
||||||
| dr | SRE HITL | Confidence >= 0.95 + dr-drill |
|
|
||||||
|
|
||||||
## Step 9 — Compliance extensions
|
|
||||||
|
|
||||||
Each module lists compliance extension points for the future compliance
|
|
||||||
milestone (GDPR, SOX, SOC2, HIPAA, DORA). See each module's README under
|
|
||||||
`modules/l1/<name>/README.md` or `modules/l2/<name>/README.md` for the
|
|
||||||
per-module extension points. Common examples:
|
|
||||||
|
|
||||||
- **KMS key** — shared encryption key for SSE.
|
|
||||||
- **S3 access logs** — access logging to a separate audit bucket.
|
|
||||||
- **Object Lock** — 7-year immutable retention for evidence.
|
|
||||||
- **Public access block** — prevent data exfiltration.
|
|
||||||
|
|
||||||
## Reference
|
|
||||||
|
|
||||||
| Resource | Path | Description |
|
|
||||||
|----------|------|-------------|
|
|
||||||
| Central deployment pipeline contract | `pipelines/deploy.yaml` | The pipeline stages your contract references. |
|
|
||||||
| Reusable deploy workflow (Gitea) | `.gitea/workflows/deploy.yml` | The workflow your repo invokes via `uses:`. |
|
|
||||||
| Reusable deploy workflow (GitHub) | `.github/workflows/deploy.yml` | The workflow your repo invokes via `uses:`. |
|
|
||||||
| Contract schema | `schemas/contract.schema.json` | JSON Schema for consumer contracts. |
|
|
||||||
| Stack schema | `schemas/stack.schema.json` | JSON Schema for the resolved stack instance. |
|
|
||||||
| Module catalog | `modules/README.md` | All L1 primitives and L2 compositions. |
|
|
||||||
| Sample contract | `contracts/static-asset.yaml` | The reference example contract (uses `@v1.4`). |
|
|
||||||
| Contract resolver | `acdl_platform/contract_resolver.py` | Resolves contracts to stack instances. |
|
|
||||||
| Terraform adapter | `adapters/terraform/adapter.py` | Compiles stack instances to Terraform. |
|
|
||||||
| Platform pipeline runner | `scripts/run_platform.sh` | The pipeline runner (platform-side; consumers do not invoke it directly). |
|
|
||||||
| Platform README | `README.md` | How the platform works + how to run the platform repo locally. |
|
|
||||||
| Credentials & zero-trust | `README.md#credentials--zero-trust` | The OIDC/ABAC default + static-key override model. |
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
title: ACDL — Agentic Cloud Delivery Platform
|
||||||
|
description: Consumer + platform-engineer documentation for the ACDL platform.
|
||||||
|
remote_theme: mmistakes/minimal-mistakes@9.0.4
|
||||||
|
|
||||||
|
exclude:
|
||||||
|
- internal/
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
- scope:
|
||||||
|
path: ""
|
||||||
|
values:
|
||||||
|
layout: single
|
||||||
|
|
||||||
|
nav:
|
||||||
|
- title: Overview
|
||||||
|
url: /
|
||||||
|
- title: Consumer Guide
|
||||||
|
url: /consumer-guide/
|
||||||
|
- title: Modules
|
||||||
|
url: /modules/
|
||||||
|
- title: Contracts
|
||||||
|
url: /contracts/
|
||||||
|
- title: Pipeline
|
||||||
|
url: /pipeline/
|
||||||
|
- title: Versioning
|
||||||
|
url: /pipeline/versioning/
|
||||||
|
- title: Environments
|
||||||
|
url: /environments/
|
||||||
|
- title: Architecture
|
||||||
|
url: /architecture/
|
||||||
|
- title: Vision
|
||||||
|
url: /vision/
|
||||||
@@ -1,458 +0,0 @@
|
|||||||
# Architecture Document v1.0
|
|
||||||
|
|
||||||
> **Snapshot status:** v1.0 — taken in ACDL Phase 07 (milestone v1.1).
|
|
||||||
> All 11 open decisions in §13 are **resolved** — see `PROJECT.md`
|
|
||||||
> "Open-decision resolutions" table + decisions D-034..D-046.
|
|
||||||
> The body §§1-12 is copied verbatim from the upstream
|
|
||||||
> `docs/architecture.md` v0.2; only the header status line, the resolution
|
|
||||||
> session log, §13, §14, and the new §15 are Phase 07 additions. The
|
|
||||||
> `act_runner` → `gitea-runner` rename (D-046, 2026-04 in gitea/runner#850)
|
|
||||||
> is applied; `act_runner` appears only in a "formerly" note.
|
|
||||||
|
|
||||||
# Agentic Cloud Delivery Platform — Architecture Document
|
|
||||||
|
|
||||||
Status: **v1.0** (snapshot taken in ACDL Phase 07, milestone v1.1). All 11
|
|
||||||
open decisions in §13 are resolved — see `PROJECT.md` "Open-decision
|
|
||||||
resolutions" table + decisions D-034..D-046.
|
|
||||||
|
|
||||||
Companion to: Agentic Cloud Delivery Vision [1].
|
|
||||||
|
|
||||||
Authoring principle: The vision is the source of truth for why [1]; this document is the source of truth for how. Where the two conflict, the vision wins.
|
|
||||||
|
|
||||||
Resolution session log (v1.0 snapshot — see PROJECT.md for full text):
|
|
||||||
|
|
||||||
| ID | Question | Resolution (one-line — see PROJECT.md for rationale) |
|
|
||||||
|---|---|---|
|
|
||||||
| W1.A | AI-refinement trigger | ✅ RESOLVED — joint condition: N ≥ 50 consecutive zero-rollback changes AND no L1/L2 incident in 6 months AND Infra & Ops unilateral override. |
|
|
||||||
| W1.B | Multi-stack edge case rule | ✅ RESOLVED — permitted only for (a) DR-region mirror, (b) time-boxed experimental stack TTL ≤ 30d, (c) explicit Infra & Ops approval with `multiStack.justification`. |
|
|
||||||
| W2.A | Tag mutability for prod | ✅ RESOLVED — Path B: tag for dev/qa, SHA for prod; platform CLI resolves tag→SHA. |
|
|
||||||
| W3.D | L1/L2 standard versioning | ✅ RESOLVED — semver (interface→MAJOR, behavior→MINOR, lifecycle→PATCH); L2 pins L1 by `name@semver`; MAJOR bump = new registry entry + 12-month deprecation. |
|
|
||||||
| W3.E | Schema mandatory vs. optional inputs | ✅ RESOLVED — dev: stack+environment; qa adds validation.e2eSuite+loadTest; prod adds runbook+dashboard+oncall; dr adds drDrillRef; `inputs` always optional; `profile: agentic` fields optional everywhere (naturalLanguageIntent required when profile is agentic). |
|
|
||||||
| BA.A | Initial L3B skill catalog | ✅ RESOLVED — 5 skills: web API, worker, scheduled job, static asset, basic observability bootstrap; addition criteria: (a) sensitive-data reviewable, (b) single contract submission, (c) documented use case. |
|
|
||||||
| BA.B | Confidence threshold tuning | ✅ RESOLVED — thresholds frozen for v1; tuning begins v1.2 (quarterly FP/FN tracking; override = Infra & Ops + SRE joint sign-off, itself a confidence-event). |
|
|
||||||
| BA.C | On-call / operational ownership | ✅ RESOLVED — platform on-call = Infra & Ops; L3A/L3B halt → platform on-call (Sev2); consumer-visible outage → consumer on-call (Sev1) + platform support. |
|
|
||||||
| BA.D | Cost / capacity governance | ✅ RESOLVED — FinOps owns cloud cost; per-contract monthly reporting; runaway spend hard-halts at 120% of declared budget via the confidence signal; override = FinOps + SRE joint sign-off. |
|
|
||||||
| BA.E | Consumer onboarding | ✅ RESOLVED — developer (L3A): `getting-started` → contract schema + central pipeline template; citizen (L3B): scoped agent + skill catalog, no workflow authoring; both end in a sandbox dev submission that must pass the confidence gate. |
|
|
||||||
| BA.F | Cross-platform evolution | ✅ RESOLVED — contract schema, stack, PolicyCheckResult, confidence signal, audit stream are portable (forge-agnostic); forge-specific code = workflow YAML, OIDC trust, CODEOWNERS, Environments; a second forge needs a forge adapter + workflow-template translator, no change to L1/L2/stack/confidence/audit. |
|
|
||||||
| Q1.3 | OpenTofu timing | ✅ RESOLVED (deferred) — not in v1 or v1.1; the substrate abstraction (§12) makes OpenTofu a future adapter, not an architecture change; revisit when an OpenTofu adapter is requested. |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 0. Purpose
|
|
||||||
|
|
||||||
This document encodes the architectural commitments that realize the vision [1]. The resolution session has closed eight open items; the document is now at v0.2 with eleven open items remaining, listed in Section 13. Every locked commitment is grounded in either a vision tenet or a specific decision made during resolution.
|
|
||||||
|
|
||||||
The structure remains: four layers (L1 primitives, L2 composed stacks, L3A developer surface, L3B agentic surface) plus five cross-cutting concerns (central pipeline, contract schema, confidence signal, audit stream, HITL mechanics), with one addition: the substrate abstraction layer (Section 12) is now a first-class architectural concern, not an implementation detail.
|
|
||||||
|
|
||||||
## 1. Architectural Overview
|
|
||||||
|
|
||||||
The platform remains four layers and five cross-cutting concerns. The substrate abstraction is added as a sixth cross-cutting concern in Section 12 because it is the binding constraint for the L1/L2 model, the central pipeline, and the policy toolchain.
|
|
||||||
|
|
||||||
The vision's "Two Consumer Surfaces, One Platform" tenet [1] remains the constraint that binds all concerns: L3A and L3B converge on the same contract schema, the same policy envelope, and the same evidence stream.
|
|
||||||
|
|
||||||
Locked additions this revision:
|
|
||||||
|
|
||||||
- The environment model is dev (autonomous) → qa (QA HITL) → prod (SRE HITL) → dr (SRE HITL). Staging does not exist.
|
|
||||||
|
|
||||||
- L1/L2 are substrate-agnostic in shape; substrate adapters are the only substrate-specific component.
|
|
||||||
|
|
||||||
## 2. Layer 1 — Foundational Primitives
|
|
||||||
|
|
||||||
Purpose. Single-purpose, substrate-agnostic primitive modules representing the smallest reusable infrastructure pieces. L1 modules do not compose with other L1 modules; L1 takes its environment as input.
|
|
||||||
|
|
||||||
Locked commitments (unchanged from v0.1):
|
|
||||||
|
|
||||||
- No inter-L1 references. L1 may call Terraform data sources.
|
|
||||||
|
|
||||||
- Semver with three triggers (interface → MAJOR, behavior → MINOR, lifecycle → PATCH).
|
|
||||||
|
|
||||||
- Immutability on publication.
|
|
||||||
|
|
||||||
- 12-month deprecation window.
|
|
||||||
|
|
||||||
- AI refinement is a flag.
|
|
||||||
|
|
||||||
✅ RESOLVED (see PROJECT.md W1.A): AI-refinement operational trigger — joint condition: N ≥ 50 consecutive changes with zero rollbacks AND no L1/L2 incident in last 6 months AND Infra & Ops holds a unilateral override.
|
|
||||||
|
|
||||||
✅ RESOLVED (sub-decision): The L1 module's interface field is defined against the Target Stack, not against Terraform's variable block directly. In v1, the stack is shaped to round-trip cleanly to Terraform, but the schema is substrate-agnostic. Pending v1 implementation details in Section 12.
|
|
||||||
|
|
||||||
## 3. Layer 2 — Composed Stacks
|
|
||||||
|
|
||||||
Purpose. Combine L1 primitives into deployable infrastructure shapes. Each codebase maps to one canonical L2 stack; the stack is either a parameterized module (Shape X) or a thin-composition layer (Shape Y).
|
|
||||||
|
|
||||||
Locked commitments (unchanged from v0.1):
|
|
||||||
|
|
||||||
- 1 codebase = 1 L2 stack (default), with multiStack: true for exceptions.
|
|
||||||
|
|
||||||
- Shape X or Shape Y.
|
|
||||||
|
|
||||||
- Hierarchical composition, max depth 5, only registered L1s.
|
|
||||||
|
|
||||||
- Pipeline quality checks: secrets-in-plaintext, public ingress, IAM wildcard, KMS key reference, tag compliance, naming convention.
|
|
||||||
|
|
||||||
- Restricted from thin-composition: IAM principal creation, network boundary creation, key/secret creation, external data transfer.
|
|
||||||
|
|
||||||
- Auto-promote after 3 observed usages.
|
|
||||||
|
|
||||||
✅ RESOLVED (see PROJECT.md W1.B): Multi-stack edge case rule — permitted only for (a) DR-region mirror, (b) time-boxed experimental stack with TTL ≤ 30 days, (c) explicit Infra & Ops approval for a documented reason captured in multiStack.justification.
|
|
||||||
|
|
||||||
✅ RESOLVED (sub-decision): The L2 composition tree's wires field is defined against the stack's relationship type, not against a Terraform module block. The stack → Terraform translation is the Terraform adapter's job (Section 12). The composition pipeline itself is substrate-agnostic.
|
|
||||||
|
|
||||||
## 4. Layer 3A — Developer Consumer Surface
|
|
||||||
|
|
||||||
Locked commitments (unchanged from v0.1):
|
|
||||||
|
|
||||||
- Tag-based reference to the central pipeline template.
|
|
||||||
|
|
||||||
- Developer-owned workflow file, no platform auto-sync.
|
|
||||||
|
|
||||||
- L3A and L3B are parallel paths, not a progression.
|
|
||||||
|
|
||||||
✅ RESOLVED (see PROJECT.md W2.A): Tag mutability for production-bound references — Path B (tag for dev/qa, SHA for prod). The platform provides a CLI command that resolves the current tag to its SHA for prod-bound workflows.
|
|
||||||
|
|
||||||
## 5. Layer 3B — Agentic Consumer Surface
|
|
||||||
|
|
||||||
Locked commitments (unchanged from v0.1):
|
|
||||||
|
|
||||||
- Hybrid runtime, skill as markdown, agent as executor.
|
|
||||||
|
|
||||||
- Trust model: trust and always verify on the platform side.
|
|
||||||
|
|
||||||
- Skill envelope (4 dimensions).
|
|
||||||
|
|
||||||
- Stateless agents, all state in the platform.
|
|
||||||
|
|
||||||
Environment progression — locked (this revision):
|
|
||||||
|
|
||||||
| Environment | Autonomy | Attester | Gate |
|
|
||||||
|---|---|---|---|
|
|
||||||
| dev | Full autonomy (no HITL) | — | Confidence signal ≥ 0.50, all six inputs present |
|
|
||||||
| qa | Held for attestation | QA | GitHub Deployment approval + full QA matrix (see §10) |
|
|
||||||
| prod | Held for attestation | SRE | GitHub Deployment approval + full SRE matrix (see §10) |
|
|
||||||
| dr | Held for attestation | SRE | GitHub Deployment approval + dr-drill evidence (see §10) |
|
|
||||||
|
|
||||||
Staging is removed. Dev is the only autonomous environment and absorbs integration, contract, security smoke, and performance smoke validation. The CDLC reference document's environment model is a doc-sync item flagged at the top of this document.
|
|
||||||
|
|
||||||
Profile marker: profile: agentic unlocks L3B-specific fields naturalLanguageIntent, confidenceAtSubmission, agentTrace).
|
|
||||||
|
|
||||||
✅ RESOLVED (see PROJECT.md BA.A): Skill catalog — initial set: web API, worker, scheduled job, static asset, basic observability bootstrap. Addition criteria: (a) reviewable for sensitive data, (b) expressible as a single contract submission, (c) documented use case.
|
|
||||||
|
|
||||||
## 6. Cross-Cutting — Central Pipeline Template
|
|
||||||
|
|
||||||
Locked commitments (unchanged from v0.1):
|
|
||||||
|
|
||||||
- JSON Schema (draft 2020-12) with thin domain-specific wrapper.
|
|
||||||
|
|
||||||
- Central repo + generated client libraries.
|
|
||||||
|
|
||||||
- Multi-stage validation pipeline (schema → policy → NFR → confidence).
|
|
||||||
|
|
||||||
- Distributed enrichment.
|
|
||||||
|
|
||||||
- GitOps reconciler + Terraform execution layer.
|
|
||||||
|
|
||||||
Locked additions this revision:
|
|
||||||
|
|
||||||
- The GitOps reconciler is the platform's K8s API. The cdlc-gitops repository's state materializes into K8s CRDs (ArgoCD Applications or Flux Kustomizations) that the reconciler watches. This is the platform's internal state surface.
|
|
||||||
|
|
||||||
- The pipeline emits a PolicyCheckResult record per policy rule evaluated. The confidence signal consumes these as one normalized input (Section 8).
|
|
||||||
|
|
||||||
✅ RESOLVED (see PROJECT.md W3.D): L1/L2 standard versioning details — semver (interface→MAJOR, behavior→MINOR, lifecycle→PATCH); L2 contracts pin L1 by `name@semver`; the resolver picks the highest compatible; MAJOR bumps require a new registry entry (immutable publication); old entry enters a 12-month deprecation window.
|
|
||||||
|
|
||||||
✅ RESOLVED (see PROJECT.md W3.E): Schema mandatory vs. optional inputs — dev requires stack+environment; qa adds validation.e2eSuite + validation.loadTest; prod adds runbook + dashboard + oncall; dr adds drDrillRef; `inputs` always optional; `profile: agentic` fields optional everywhere (naturalLanguageIntent required when profile is agentic).
|
|
||||||
|
|
||||||
## 7. Cross-Cutting — Contract Schema
|
|
||||||
|
|
||||||
Locked commitments (unchanged from v0.1):
|
|
||||||
|
|
||||||
- Central repo + generated client libraries.
|
|
||||||
|
|
||||||
- Strict fail-fast at schema stage, multi-stage validation pipeline with reason codes from a published vocabulary.
|
|
||||||
|
|
||||||
✅ RESOLVED (see PROJECT.md W3.E): Schema mandatory vs. optional inputs. The CDLC reference contract example [1] is illustrative; the v1 contract schema has explicit per-field mandatory/optional declarations per environment.
|
|
||||||
|
|
||||||
## 8. Cross-Cutting — Confidence Signal
|
|
||||||
|
|
||||||
Locked commitments (unchanged from v0.1):
|
|
||||||
|
|
||||||
- Six canonical inputs.
|
|
||||||
|
|
||||||
- Weighted sum with per-input breakdown.
|
|
||||||
|
|
||||||
- Per-environment thresholds: dev ≥ 0.50, qa ≥ 0.75, prod ≥ 0.90, dr ≥ 0.95.
|
|
||||||
|
|
||||||
- Structured output { score, band, perInput, reasonCodes }.
|
|
||||||
|
|
||||||
- 1-year storage, no algorithm retraining in v1.
|
|
||||||
|
|
||||||
- Halt with explicit reason on missing input.
|
|
||||||
|
|
||||||
Locked additions this revision:
|
|
||||||
|
|
||||||
- The policy check results input is a list of PolicyCheckResult records from the normalized schema (Section 9, 12). The signal does not know which engine produced which result.
|
|
||||||
|
|
||||||
- Severity → score penalty mapping: critical → hard override to mandatory block, high → -0.2, medium → -0.05, low → -0.01, info → 0.0. One critical finding hard-overrides the score regardless of all other inputs.
|
|
||||||
|
|
||||||
✅ RESOLVED (see PROJECT.md BA.B): Threshold tuning policy. Thresholds frozen for v1. Tuning begins v1.2: quarterly FP/FN tracking per environment; override authority = Infra & Ops + SRE joint sign-off; any override is itself a confidence-event in the audit stream.
|
|
||||||
|
|
||||||
## 9. Cross-Cutting — Audit and Evidence Stream
|
|
||||||
|
|
||||||
Locked commitments (unchanged from v0.1):
|
|
||||||
|
|
||||||
- Tiered audit ledger: S3 with Object Lock in compliance mode (cold, source of truth, 7-year retention) + GitHub audit repo (hot, query index, not part of the chain).
|
|
||||||
|
|
||||||
- Daily checkpoints.
|
|
||||||
|
|
||||||
- Event schema: JWS detached signature, prev_event_hash chain, controlled-vocabulary event_type.
|
|
||||||
|
|
||||||
- Outbox pattern with local durable outbox + async worker.
|
|
||||||
|
|
||||||
- Linkage via workflow run ID or agent invocation ID.
|
|
||||||
|
|
||||||
Locked additions this revision:
|
|
||||||
|
|
||||||
- The outbox database is DynamoDB. RPO is zero (synchronous write to local outbox before contract submission ack); RTO is the async worker's recovery from the dead-letter queue. Single-region in v1; multi-region is a v2 concern.
|
|
||||||
|
|
||||||
- The outbox also stores the per-contract QA and prod approver identities (Section 10). The platform-internal identity-distinctness check reads from this outbox. This is the only durable record of the approver identities outside GitHub's audit log.
|
|
||||||
|
|
||||||
## 10. Cross-Cutting — Human-in-the-Loop Mechanics
|
|
||||||
|
|
||||||
Purpose. The human gates at higher environments. The vision's "Lower Environments are Autonomous; Higher Environments are Attested" tenet [1] and the "deliberate human attestation — not as a rubber stamp" requirement [1] are the binding constraints.
|
|
||||||
|
|
||||||
### 10.1 Gate model
|
|
||||||
|
|
||||||
Pre-execution gates. The contract is held in a "validated but not applied" state until the human attests. qa, prod, and dr are PR-based attestation gates backed by GitHub Environments with required reviewers.
|
|
||||||
|
|
||||||
For qa and prod, there is no partial deployment to roll back on rejection. For dr, the same model — promotion to the DR environment is a separate GitHub Deployment, gated by SRE, against a separate cluster/region. The canary/deployment-rollback model is explicitly not in scope for v1.
|
|
||||||
|
|
||||||
### 10.2 Reviewer routing
|
|
||||||
|
|
||||||
GitHub CODEOWNERS + GitHub Environment required reviewers. qa → QA team; prod → SRE team; dr → SRE team. CODEOWNERS is the routing layer; it does not enforce identity distinctness.
|
|
||||||
|
|
||||||
### 10.3 Separation of duties — identity distinctness
|
|
||||||
|
|
||||||
Mechanism is platform-internal, not GitHub-native, not Kyverno (in v1).
|
|
||||||
|
|
||||||
Sequence:
|
|
||||||
|
|
||||||
1. On promotion dev → qa, the platform reads the QA approver's GitHub identity from the GitHub Deployment approval event and writes it to the DynamoDB outbox keyed by contractId.
|
|
||||||
|
|
||||||
2. On promotion qa → prod, the platform reads the stored QA approver identity from the outbox and the new SRE approver identity from the GitHub Deployment approval event.
|
|
||||||
|
|
||||||
3. If qaApprover == prodApprover, the platform blocks the prod promotion, writes a SEPARATION_OF_DUTIES_VIOLATION event to the evidence stream, and routes a halt artifact to the SRE on-call.
|
|
||||||
|
|
||||||
4. The check is implemented in the central pipeline repo, not as an external policy. The platform is the only writer to the outbox; the check is in the same process that has authority to block the promotion.
|
|
||||||
|
|
||||||
### 10.4 Full HITL attestation matrix
|
|
||||||
|
|
||||||
| Env | Concern | Evidence artifact | Freshness | Source | Attester |
|
|
||||||
|---|---|---|---|---|---|
|
|
||||||
| qa | Functional correctness | Last successful run of contract-declared validation.e2eSuite with pass rate ≥ 99% | Last 24h | Test runner declared in contract | QA |
|
|
||||||
| qa | Performance baseline | Load test report (k6 / Gatling / Locust) showing p99 latency < declared NFR and throughput > declared minimum | Last 7d | Load test runner declared in contract | QA |
|
|
||||||
| qa | Security posture | Vulnerability scan (Trivy, Snyk, or contract-declared equivalent) with no criticals/highs, signed by Security on-call | Last 24h | Security scanner + Security team signature | QA |
|
|
||||||
| qa | Contract NFRs | Platform-generated report: schema valid, NFR assertions (latency, throughput, error rate) within declared bounds | At submission | Platform contract validator | QA |
|
|
||||||
| prod | Operational readiness | Runbook published, dashboard exists, on-call rotation assigned, alerts configured | At submission, validated against last 30d history | Platform + SRE | SRE |
|
|
||||||
| prod | Incident response | Sev-1 runbook tabletop or live drill completed | Last 90d | SRE drill record | SRE |
|
|
||||||
| prod | Capacity / cost | FinOps forecast for next 30d within budget envelope, cost anomaly baseline stored, budget alert configured | Forecast valid for next 30d | FinOps + SRE | SRE |
|
|
||||||
| prod | Resilience | DR drill, chaos engineering report, backup verified | DR: 180d; chaos: 90d; backup: 30d | SRE + Platform | SRE |
|
|
||||||
| dr | dr-region deploy with the most recent prod-bound dr drill as canary evidence | dr drill report | Last 180d | SRE | SRE |
|
|
||||||
|
|
||||||
### 10.5 Timeout behavior
|
|
||||||
|
|
||||||
| Time | State | Action |
|
|
||||||
|---|---|---|
|
|
||||||
| Submission | PENDING_ATTESTATION | Notify responsible team |
|
|
||||||
| 1 business day | PENDING_ATTESTATION_WARNING | Notify team + platform on-call (elevated path); emit PENDING_ATTESTATION_TIMEOUT_WARNING event |
|
|
||||||
| 2 business days | PENDING_ATTESTATION_AUTO_FREEZE | Auto-freeze; require re-submission; emit PENDING_ATTESTATION_AUTO_FREEZE event; new submission linked via supersedes |
|
|
||||||
|
|
||||||
### 10.6 Rejection and rollback
|
|
||||||
|
|
||||||
Rejection returns the contract to a HELD state with the rejection reason captured as a PROMOTION_REJECTED event. The consumer fixes the cause and re-submits; the new submission is linked to the rejected one via supersedes. The audit chain is extended, not torn up — matching the resolution session's answer.
|
|
||||||
|
|
||||||
There is no partial deployment to roll back at any v1 gate.
|
|
||||||
|
|
||||||
## 11. Cross-Cutting — Agentic Stack
|
|
||||||
|
|
||||||
Locked commitments (unchanged from v0.1):
|
|
||||||
|
|
||||||
- Hybrid runtime, platform-managed control plane + consumer-owned agent.
|
|
||||||
|
|
||||||
- Versioned, signed skill catalog over MCP.
|
|
||||||
|
|
||||||
- Skill envelope enforced on invocation and result submission.
|
|
||||||
|
|
||||||
- Consumer-owned skill execution environment. Platform does not run the skill.
|
|
||||||
|
|
||||||
- Stateless agents, all state in the platform.
|
|
||||||
|
|
||||||
Locked additions this revision:
|
|
||||||
|
|
||||||
- Skills are reviewed for sensitive data before release. Secrets, customer data, internal IPs, and other sensitive payloads are forbidden in skill markdown. The review is owned by Infra & Ops and is the mandatory release gate for any new skill. This is the trade-off for accepting the L3B runtime threat model (skill content is consumer-readable, so the platform must not put anything sensitive in it).
|
|
||||||
|
|
||||||
✅ RESOLVED (see PROJECT.md BA.A): Skill catalog — initial set, addition process, deprecation process per the resolution.
|
|
||||||
|
|
||||||
## 12. Cross-Cutting — L1/L2 Substrate Execution
|
|
||||||
|
|
||||||
Purpose. The technical execution layer for the L1/L2 substrate, including the substrate abstraction that protects v1 from polyglot mess while leaving v2+ room to grow.
|
|
||||||
|
|
||||||
### 12.1 Substrate abstraction (locked this revision)
|
|
||||||
|
|
||||||
L1/L2 are substrate-agnostic in shape. The architecture defines a Target Stack — a substrate-neutral description of:
|
|
||||||
|
|
||||||
- Resources with typed input contracts, typed output contracts, and declared NFRs.
|
|
||||||
|
|
||||||
- Relationships (single parent per child, with a shared keyword for multi-relationship dependencies).
|
|
||||||
|
|
||||||
- Composition (a tree of resources with max depth 5).
|
|
||||||
|
|
||||||
- Policy hooks (the points in the composition where policy checks attach).
|
|
||||||
|
|
||||||
The L1 registry, the L2 thin-composition tree, the YML standard, and the policy check result schema are all defined against the stack schema. None of them is defined against any specific substrate.
|
|
||||||
|
|
||||||
Substrate adapters are the only substrate-specific code. An adapter compiles the stack into a substrate execution plan. v1 ships exactly one adapter: the Terraform adapter. v2+ may add additional adapters (OpenTofu, Pulumi, K8s CRDs) without architectural change.
|
|
||||||
|
|
||||||
v1 implementation reality: the stack is shaped to round-trip cleanly to Terraform because there is no other adapter to differentiate from. The stack and the Terraform output are nearly isomorphic in v1. As additional adapters appear in v2+, the stack gets more expressive (e.g., substrate-specific output types) and the adapters gain translation logic, but the L1 module content, the YML standard, and the composition tree do not change. This is the design that prevents the polyglot mess.
|
|
||||||
|
|
||||||
Why not build the abstraction earlier? Building a substrate-agnostic stack before there is a second adapter to test against is speculative generality. The v1 commitment is: (1) the L1 module interface is defined against the stack schema even though the only adapter is Terraform, and (2) the central pipeline, registry, and policy schema consume the stack-typed contracts. The adapter is the only place where substrate terminology appears in v1.
|
|
||||||
|
|
||||||
### 12.2 Terraform adapter (v1)
|
|
||||||
|
|
||||||
The Terraform adapter:
|
|
||||||
|
|
||||||
- Translates the stack-typed L1 module interface to a Terraform variable block and a Terraform output block.
|
|
||||||
|
|
||||||
- Translates the stack-typed L2 composition tree to a Terraform root module that calls the L1 modules.
|
|
||||||
|
|
||||||
- Translates the stack-typed relationships to Terraform module references.
|
|
||||||
|
|
||||||
- Emits a Terraform plan from the stack.
|
|
||||||
|
|
||||||
The adapter is a thin layer. It does not own L1/L2 content; it only translates.
|
|
||||||
|
|
||||||
### 12.3 State storage
|
|
||||||
|
|
||||||
Locked: S3 (state files) + DynamoDB (state locking), cloud-managed. Single-region in v1.
|
|
||||||
|
|
||||||
### 12.4 Policy toolchain
|
|
||||||
|
|
||||||
Locked:
|
|
||||||
|
|
||||||
- Checkov for Terraform plan policy (the four L2 thin-composition checks: secrets-in-plaintext, public ingress, IAM wildcard, KMS key reference, plus tag and naming convention). Checkov is open-source, has a broad rule catalog, and is GitOps-friendly.
|
|
||||||
|
|
||||||
- Kyverno for K8s-native policy (platform-internal state in the GitOps reconciler, separation-of-dues-adjacent checks if any are added in v2, future CRD validation).
|
|
||||||
|
|
||||||
- OPA/Rego is reserved for cross-resource policy and is explicitly last resort due to Rego complexity.
|
|
||||||
|
|
||||||
### 12.5 Execution layer
|
|
||||||
|
|
||||||
Locked: GitHub Actions. terraform plan and terraform apply run in the central pipeline repo's GitHub Actions workflow. State locking via DynamoDB. AWS credentials via OIDC federation (long-lived credentials are forbidden). The platform does not run terraform apply against a developer's workstation; all execution is in the central pipeline.
|
|
||||||
|
|
||||||
> **ACDL Phase 07 note (D-039):** Gitea Actions (the ACDL forge) does not
|
|
||||||
> support `id-token: write` / OIDC token issuance as of Gitea 1.27.x /
|
|
||||||
> gitea-runner v2.1.0 (formerly `act_runner`, renamed 2026-04 in
|
|
||||||
> gitea/runner#850). The v1.1 spike uses a per-run-rotated long-lived key
|
|
||||||
> waiver; real OIDC federation is a v1.2 deliverable, blocked on
|
|
||||||
> go-gitea/gitea#36988. The §12.5 "long-lived credentials are forbidden"
|
|
||||||
> commitment is the locked target; the waiver is a time-boxed spike
|
|
||||||
> exception.
|
|
||||||
|
|
||||||
### 12.6 Policy result normalization (locked this revision)
|
|
||||||
|
|
||||||
The confidence signal does not consume raw Checkov or Kyverno output. It consumes a normalized PolicyCheckResult schema produced by substrate-specific adapters.
|
|
||||||
|
|
||||||
Schema (canonical form, lives in the central pipeline repo):
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"contractId": "uuid",
|
|
||||||
"evaluatedAt": "ISO-8601",
|
|
||||||
"engine": "checkov | kyverno | opa",
|
|
||||||
"ruleId": "CKV_AWS_24 | KYVERNO_NO_PRIVILEGED | ...",
|
|
||||||
"severity": "critical | high | medium | low | info",
|
|
||||||
"result": "pass | fail | skipped | error",
|
|
||||||
"message": "human-readable",
|
|
||||||
"evidence": { "...engine-specific payload, opaque to the signal..." },
|
|
||||||
"resourceRef": "stack-typed resource identifier"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The Checkov adapter runs in the same GitHub Actions step as Checkov itself and translates Checkov JSON to PolicyCheckResult records. The Kyverno adapter runs as a controller in the platform's K8s cluster and translates Kyverno PolicyReport CRDs to PolicyCheckResult records. The confidence signal's policy input component is the union of all PolicyCheckResult records, regardless of engine. The signal does not know which engine produced which result — substrate-agnostic over its inputs, matching the L1/L2 model's substrate-agnostic over its outputs.
|
|
||||||
|
|
||||||
### 12.7 Registry maintenance
|
|
||||||
|
|
||||||
Locked: L1 module publication updates the L1 registry in the same PR as the module. Registry and module land together. The registry is the stack-typed contract, not a Terraform-specific variable schema. The L1 registry, the central pipeline, and the policy schema all consume the same stack-typed contract — there is one source of truth for the L1 interface, not multiple substrate-specific copies.
|
|
||||||
|
|
||||||
### 12.8 Contract-schema-to-stack resolution
|
|
||||||
|
|
||||||
The contract schema declares the consumer's intent in stack-typed terms. The central pipeline resolves the contract to a target stack (a list of L1 module instances with their inputs and the relationships between them). The Terraform adapter compiles the target stack to a Terraform execution plan. This resolution is substrate-agnostic — the target stack is in the stack schema.
|
|
||||||
|
|
||||||
## 13. Consolidated Open Design Decisions
|
|
||||||
|
|
||||||
✅ **All 11 decisions are RESOLVED (see PROJECT.md).** The §13 subsections
|
|
||||||
below preserve the upstream structure with the `🟡 OPEN` markers replaced
|
|
||||||
by `✅ RESOLVED (see PROJECT.md)`.
|
|
||||||
|
|
||||||
### From Wave 1 (L1/L2 Substrate)
|
|
||||||
|
|
||||||
- (W1.A) AI-refinement trigger. ✅ RESOLVED (see PROJECT.md) — joint condition: N ≥ 50 consecutive zero-rollback changes AND no L1/L2 incident in 6 months AND Infra & Ops unilateral override.
|
|
||||||
|
|
||||||
- (W1.B) Multi-stack edge case rule. ✅ RESOLVED (see PROJECT.md) — permitted only for (a) DR-region mirror, (b) time-boxed experimental stack TTL ≤ 30d, (c) explicit Infra & Ops approval with `multiStack.justification`.
|
|
||||||
|
|
||||||
### From Wave 2 (L3A/L3B)
|
|
||||||
|
|
||||||
- (W2.A) Tag mutability for production-bound references. ✅ RESOLVED (see PROJECT.md) — Path B (tag for dev/qa, SHA for prod) with platform-provided CLI to resolve tag → SHA.
|
|
||||||
|
|
||||||
### From Wave 3 (Technical Execution)
|
|
||||||
|
|
||||||
- (W3.D) L1/L2 standard versioning details. ✅ RESOLVED (see PROJECT.md) — semver (interface→MAJOR, behavior→MINOR, lifecycle→PATCH); L2 pins L1 by `name@semver`; MAJOR bump = new registry entry + 12-month deprecation.
|
|
||||||
|
|
||||||
- (W3.E) Schema mandatory vs. optional inputs. ✅ RESOLVED (see PROJECT.md) — per-env mandatory table (dev: stack+environment; qa adds validation.e2eSuite+loadTest; prod adds runbook+dashboard+oncall; dr adds drDrillRef); `inputs` always optional; `profile: agentic` fields optional everywhere.
|
|
||||||
|
|
||||||
### From Beyond Architecture
|
|
||||||
|
|
||||||
- (BA.A) Skill catalog. ✅ RESOLVED (see PROJECT.md) — 5 skills (web API, worker, scheduled job, static asset, basic observability bootstrap); addition criteria locked.
|
|
||||||
|
|
||||||
- (BA.B) Confidence signal threshold tuning. ✅ RESOLVED (see PROJECT.md) — frozen for v1; tuning begins v1.2 (quarterly FP/FN; override = Infra & Ops + SRE joint sign-off).
|
|
||||||
|
|
||||||
- (BA.C) On-call and operational ownership. ✅ RESOLVED (see PROJECT.md) — platform on-call = Infra & Ops; L3A/L3B halt → Sev2; consumer outage → Sev1.
|
|
||||||
|
|
||||||
- (BA.D) Cost and capacity governance. ✅ RESOLVED (see PROJECT.md) — FinOps owns; per-contract monthly reporting; hard halt at 120% of declared budget via the confidence signal; override = FinOps + SRE joint sign-off.
|
|
||||||
|
|
||||||
- (BA.E) Consumer onboarding. ✅ RESOLVED (see PROJECT.md) — developer (L3A): getting-started → contract schema + central pipeline template; citizen (L3B): scoped agent + skill catalog; both end in a sandbox dev submission that must pass the confidence gate.
|
|
||||||
|
|
||||||
- (BA.F) Cross-platform evolution. ✅ RESOLVED (see PROJECT.md) — contract schema, stack, PolicyCheckResult, confidence signal, audit stream are portable; forge-specific code = workflow YAML, OIDC trust, CODEOWNERS, Environments; a second forge needs a forge adapter + workflow-template translator.
|
|
||||||
|
|
||||||
- (Q1.3) OpenTofu timing. ✅ RESOLVED (deferred — see PROJECT.md) — not in v1 or v1.1; the substrate abstraction makes OpenTofu a future adapter, not an architecture change.
|
|
||||||
|
|
||||||
## 14. Document Status and Next Steps
|
|
||||||
|
|
||||||
Status: **v1.0**. All 11 open items in §13 are resolved. The architecture is
|
|
||||||
internally consistent; the v1.1 implementation spike (ACDL Phases 08-10)
|
|
||||||
validates the locked substrate abstraction + contract→stack→adapter path
|
|
||||||
against real AWS via a per-run-rotated key (D-039; OIDC deferred to v1.2).
|
|
||||||
The v1.2 build-out (S3 Object Lock, JWS, HITL wiring, L3B skill catalog,
|
|
||||||
Kyverno/OPA, real OIDC federation, multi-region) is design-authored in
|
|
||||||
Phase 07 and implemented post-spike.
|
|
||||||
|
|
||||||
Doc-sync items (out of scope of this document but flagged for the same change set):
|
|
||||||
|
|
||||||
- The CDLC reference document's environment model assumes staging exists. Path A invalidates that. The CDLC contract example's targetEnvironments: [staging, production] must be revised to [dev, qa, prod, dr].
|
|
||||||
|
|
||||||
## 15. Phase 07 authored artifacts
|
|
||||||
|
|
||||||
The 11 resolutions are recorded in `PROJECT.md` (decisions D-034..D-046 +
|
|
||||||
the "Open-decision resolutions" table). Phase 07 formalizes the locked
|
|
||||||
commitments into these schema/design files (landed in Waves 2-4 of
|
|
||||||
Phase 07):
|
|
||||||
|
|
||||||
| REQ | File | Owner persona |
|
|
||||||
|-----|------|--------------|
|
|
||||||
| REQ-17 | `schemas/stack.schema.json` | platform-engineer |
|
|
||||||
| REQ-18 | `schemas/policy_check_result.schema.json` + `adapters/terraform/policy/checkov_adapter.py` | security-engineer |
|
|
||||||
| REQ-19 | `platform/confidence_signal.py` | backend-engineer + security-engineer (co-authored) |
|
|
||||||
| REQ-20 | `platform/audit_ledger_design.md` | security-engineer |
|
|
||||||
| REQ-21 | `platform/hitl_matrix_design.md` + `platform/separation_of_duties.py` | security-engineer |
|
|
||||||
| REQ-22 | `schemas/contract.schema.json` | backend-engineer |
|
|
||||||
|
|
||||||
The spike scope (D-041, D-043) vs v1.2 build-out boundary for each design
|
|
||||||
is documented in the respective file.
|
|
||||||
+195
-406
@@ -1,452 +1,241 @@
|
|||||||
# Architecture Document v0.2 + Executive Demo Plan
|
# Architecture
|
||||||
|
|
||||||
Three things to set up before I deliver the document, because they determine how I write the doc:
|
> **Status:** v1.0 (current). All design decisions are resolved. This is the
|
||||||
|
> source of truth for *how* the platform works; the [Vision](vision) is the
|
||||||
1. What is locked from the resolution session. Eight items: environment model (Path A — dev-only autonomous, no staging), substrate abstraction (Target Stack + adapter pattern, Terraform adapter in v1), policy toolchain (Checkov for Terraform plan, Kyverno for K8s, OPA last resort), separation of duties (CODEOWNERS for routing + DynamoDB outbox for identity distinctness), policy normalization PolicyCheckResult schema with engine adapters), HITL matrix (full 8-concern matrix with evidence, freshness, source), HITL timeout (1d warn, 2d freeze), HITL rollback (pre-execution model, audit chain extended, no partial deploy).
|
> source of truth for *why*.
|
||||||
|
|
||||||
2. What is still open after the session. Eleven items, listed in the updated Section 13. They are the gating items for v1.0.
|
|
||||||
|
|
||||||
3. Doc-sync item. The CDLC reference document (not in scope of this architecture) describes staging as an environment and uses targetEnvironments: [staging, production] in its contract example. Path A invalidates that. The CDLC document is the responsibility of its own owners, but this architecture flags the inconsistency so the CDLC revision can be scheduled in the same change set.
|
|
||||||
|
|
||||||
The architecture document is below, followed by the executive demo plan, followed by the final three questions I need resolved before v1.0.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Agentic Cloud Delivery Platform — Architecture Document
|
|
||||||
|
|
||||||
Status: Draft v0.2. Section 13 lists 11 open decisions. Eight of the original 15 open items are now locked; seven remain from Section 13 plus four new sub-decisions surfaced during resolution.
|
|
||||||
|
|
||||||
Companion to: Agentic Cloud Delivery Vision [1].
|
|
||||||
|
|
||||||
Authoring principle: The vision is the source of truth for why [1]; this document is the source of truth for how. Where the two conflict, the vision wins.
|
|
||||||
|
|
||||||
Resolution session log (this revision):
|
|
||||||
|
|
||||||
| ID | Question | Resolution |
|
|
||||||
|---|---|---|
|
|
||||||
| Q1 | Environment model | Path A locked. Dev is the only autonomous environment. QA HITL at qa. SRE HITL at prod and dr. Staging does not exist. |
|
|
||||||
| Q1.2 | Substrate trajectory | Substrate abstraction locked. L1/L2 are defined against a Target Stack. Substrate adapters compile the stack to a substrate execution plan. v1 ships only the Terraform adapter. |
|
|
||||||
| Q1.3 | OpenTofu timing | 🟡 OPEN (W3.D-adjacent). No specific version or trigger committed. |
|
|
||||||
| Q2.1 | Policy toolchain | Locked. Checkov for Terraform plan policy. Kyverno for K8s-native and platform-internal policy. OPA/Rego reserved for cross-resource cases; explicitly last resort due to Rego complexity. |
|
|
||||||
| Q2.2 | Separation of duties | Locked. GitHub CODEOWNERS routes the right reviewer to the right environment. Platform-internal identity record in DynamoDB outbox enforces qaApprover ≠ prodApprover for the same contract. |
|
|
||||||
| Q2.3 | Policy normalization | Locked. PolicyCheckResult JSON schema is the contract between engines and the confidence signal. Engine-specific adapters translate native output to the schema. |
|
|
||||||
| Q3 | HITL matrix + timeout + rollback | Locked (full 8-concern matrix in §10). Pre-execution gate model. 1 business day = warn + escalate. 2 business days = auto-freeze + re-submit. Rejection extends the audit chain, no partial deploy to roll back. |
|
|
||||||
| W1.A | AI-refinement trigger | 🟡 OPEN. Recommendation pending sign-off. |
|
|
||||||
| W1.B | Multi-stack edge case rule | 🟡 OPEN. Recommendation pending sign-off. |
|
|
||||||
| W2.A | Tag mutability for prod | 🟡 OPEN. Recommendation pending sign-off. |
|
|
||||||
| W3.D | L1/L2 standard versioning details | 🟡 OPEN. |
|
|
||||||
| W3.E | Schema mandatory vs. optional inputs | 🟡 OPEN. |
|
|
||||||
| BA.A–F | Beyond-architecture questions | 🟡 OPEN (6 items). |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 0. Purpose
|
## 0. Purpose
|
||||||
|
|
||||||
This document encodes the architectural commitments that realize the vision [1]. The resolution session has closed eight open items; the document is now at v0.2 with eleven open items remaining, listed in Section 13. Every locked commitment is grounded in either a vision tenet or a specific decision made during resolution.
|
This document encodes the architectural commitments that realize the
|
||||||
|
[vision](vision). Every commitment is grounded in a vision tenet.
|
||||||
|
|
||||||
The structure remains: four layers (L1 primitives, L2 composed stacks, L3A developer surface, L3B agentic surface) plus five cross-cutting concerns (central pipeline, contract schema, confidence signal, audit stream, HITL mechanics), with one addition: the substrate abstraction layer (Section 12) is now a first-class architectural concern, not an implementation detail.
|
The platform is **four layers + six cross-cutting concerns**, bound by the
|
||||||
|
vision's "Two Consumer Surfaces, One Platform" tenet: both surfaces converge
|
||||||
|
on the same contract schema, the same policy envelope, and the same evidence
|
||||||
|
stream.
|
||||||
|
|
||||||
## 1. Architectural Overview
|
## 1. Architectural Overview
|
||||||
|
|
||||||
The platform remains four layers and five cross-cutting concerns. The substrate abstraction is added as a sixth cross-cutting concern in Section 12 because it is the binding constraint for the L1/L2 model, the central pipeline, and the policy toolchain.
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A["Consumer surfaces"] --> B["Contract schema"]
|
||||||
|
B --> C["Central pipeline"]
|
||||||
|
C --> D["Modules + primitives"]
|
||||||
|
C --> E["Substrate adapter"]
|
||||||
|
C --> F["Confidence signal"]
|
||||||
|
C --> G["Evidence stream"]
|
||||||
|
D --> E
|
||||||
|
E --> H["Infrastructure"]
|
||||||
|
F --> G
|
||||||
|
```
|
||||||
|
|
||||||
The vision's "Two Consumer Surfaces, One Platform" tenet [1] remains the constraint that binds all concerns: L3A and L3B converge on the same contract schema, the same policy envelope, and the same evidence stream.
|
The four layers:
|
||||||
|
|
||||||
Locked additions this revision:
|
1. **Primitives** — single-purpose, substrate-agnostic modules representing
|
||||||
|
the smallest reusable infrastructure pieces (a VPC, an S3 bucket, an ECS
|
||||||
|
cluster). A primitive does not reference other primitives; it takes its
|
||||||
|
environment as input.
|
||||||
|
2. **Modules** — patterns that combine primitives into deployable
|
||||||
|
infrastructure shapes (an ECS Fargate microservice, a static-assets site).
|
||||||
|
A module references registered primitives (max depth 5).
|
||||||
|
3. **Developer surface** — the developer-owned workflow file + contract. The
|
||||||
|
developer references the central pipeline via a versioned tag and owns
|
||||||
|
their workflow file (no platform auto-sync).
|
||||||
|
4. **Agentic surface** — a hybrid runtime where a consumer declares intent
|
||||||
|
in natural language and an agent resolves it to a contract submission.
|
||||||
|
Trust model: trust and always verify on the platform side. Stateless
|
||||||
|
agents; all state lives in the platform.
|
||||||
|
|
||||||
- The environment model is dev (autonomous) → qa (QA HITL) → prod (SRE HITL) → dr (SRE HITL). Staging does not exist.
|
The developer and agentic surfaces are parallel paths, not a progression.
|
||||||
|
Both end in a contract submission that enters the same pipeline.
|
||||||
|
|
||||||
- L1/L2 are substrate-agnostic in shape; substrate adapters are the only substrate-specific component.
|
## 2. Primitives
|
||||||
|
|
||||||
## 2. Layer 1 — Foundational Primitives
|
Single-purpose, substrate-agnostic modules. Locked commitments:
|
||||||
|
|
||||||
Purpose. Single-purpose, substrate-agnostic primitive modules representing the smallest reusable infrastructure pieces. L1 modules do not compose with other L1 modules; L1 takes its environment as input.
|
|
||||||
|
|
||||||
Locked commitments (unchanged from v0.1):
|
|
||||||
|
|
||||||
- No inter-L1 references. L1 may call Terraform data sources.
|
|
||||||
|
|
||||||
- Semver with three triggers (interface → MAJOR, behavior → MINOR, lifecycle → PATCH).
|
|
||||||
|
|
||||||
|
- No inter-primitive references. A primitive may call substrate data sources.
|
||||||
|
- Semver with three triggers: interface → MAJOR, behavior → MINOR,
|
||||||
|
lifecycle → PATCH.
|
||||||
- Immutability on publication.
|
- Immutability on publication.
|
||||||
|
|
||||||
- 12-month deprecation window.
|
- 12-month deprecation window.
|
||||||
|
- AI refinement is a flag, triggered by a joint operational condition
|
||||||
|
(N ≥ 50 consecutive zero-rollback changes, no primitive/module incident in
|
||||||
|
6 months, Infra & Ops unilateral override).
|
||||||
|
- A primitive's interface is defined against the Target Stack (substrate-
|
||||||
|
agnostic), not against any substrate's variable block directly.
|
||||||
|
|
||||||
- AI refinement is a flag.
|
## 3. Modules
|
||||||
|
|
||||||
🟡 OPEN (W1.A): AI-refinement operational trigger. The criterion for flipping aiRefinement from false to true needs a falsifiable operational signal. Recommendation: joint condition — N ≥ 50 consecutive changes with zero rollbacks AND no L1/L2 incident in the last 6 months AND Infra & Ops holds a unilateral override. Pending sign-off.
|
Patterns that combine primitives into deployable shapes. Locked commitments:
|
||||||
|
|
||||||
🟡 OPEN (sub-decision surfaced this revision): The L1 module's interface field is defined against the Target Stack, not against Terraform's variable block directly. In v1, the stack is shaped to round-trip cleanly to Terraform, but the schema is substrate-agnostic. Pending v1 implementation details in Section 12.
|
|
||||||
|
|
||||||
## 3. Layer 2 — Composed Stacks
|
|
||||||
|
|
||||||
Purpose. Combine L1 primitives into deployable infrastructure shapes. Each codebase maps to one canonical L2 stack; the stack is either a parameterized module (Shape X) or a thin-composition layer (Shape Y).
|
|
||||||
|
|
||||||
Locked commitments (unchanged from v0.1):
|
|
||||||
|
|
||||||
- 1 codebase = 1 L2 stack (default), with multiStack: true for exceptions.
|
|
||||||
|
|
||||||
- Shape X or Shape Y.
|
|
||||||
|
|
||||||
- Hierarchical composition, max depth 5, only registered L1s.
|
|
||||||
|
|
||||||
- Pipeline quality checks: secrets-in-plaintext, public ingress, IAM wildcard, KMS key reference, tag compliance, naming convention.
|
|
||||||
|
|
||||||
- Restricted from thin-composition: IAM principal creation, network boundary creation, key/secret creation, external data transfer.
|
|
||||||
|
|
||||||
|
- One codebase maps to one canonical module (default); `multiStack: true`
|
||||||
|
is permitted only for (a) a DR-region mirror, (b) a time-boxed
|
||||||
|
experimental stack (TTL ≤ 30 days), or (c) explicit Infra & Ops approval
|
||||||
|
with a documented justification.
|
||||||
|
- A module references registered primitives only (max depth 5).
|
||||||
|
- Pipeline quality checks: secrets-in-plaintext, public ingress, IAM
|
||||||
|
wildcard, KMS key reference, tag compliance, naming convention.
|
||||||
|
- Restricted from module patterns: IAM principal creation, network boundary
|
||||||
|
creation, key/secret creation, external data transfer.
|
||||||
- Auto-promote after 3 observed usages.
|
- Auto-promote after 3 observed usages.
|
||||||
|
- A module's pattern tree wires field is defined against the stack's
|
||||||
|
relationship type, not against any substrate's module block. The stack →
|
||||||
|
substrate translation is the substrate adapter's job (§12). The pattern
|
||||||
|
pipeline itself is substrate-agnostic.
|
||||||
|
|
||||||
🟡 OPEN (W1.B): Multi-stack edge case rule. The multiStack: true exception needs a falsifiable rule. Recommendation: permitted only for (a) DR-region mirror of the primary stack, (b) time-boxed experimental stack with TTL ≤ 30 days, (c) explicit Infra & Ops approval for a documented reason captured in multiStack.justification. Pending sign-off.
|
## 4. Developer Surface
|
||||||
|
|
||||||
🟡 OPEN (sub-decision surfaced this revision): The L2 composition tree's wires field is defined against the stack's relationship type, not against a Terraform module block. The stack → Terraform translation is the Terraform adapter's job (Section 12). The composition pipeline itself is substrate-agnostic.
|
|
||||||
|
|
||||||
## 4. Layer 3A — Developer Consumer Surface
|
|
||||||
|
|
||||||
Locked commitments (unchanged from v0.1):
|
|
||||||
|
|
||||||
- Tag-based reference to the central pipeline template.
|
- Tag-based reference to the central pipeline template.
|
||||||
|
|
||||||
- Developer-owned workflow file, no platform auto-sync.
|
- Developer-owned workflow file, no platform auto-sync.
|
||||||
|
- Tag mutability for production-bound references: tag for dev/qa, SHA for
|
||||||
|
prod. The platform provides a CLI command that resolves the current tag
|
||||||
|
to its SHA for prod-bound workflows.
|
||||||
|
|
||||||
- L3A and L3B are parallel paths, not a progression.
|
## 5. Agentic Surface
|
||||||
|
|
||||||
🟡 OPEN (W2.A): Tag mutability for production-bound references. Path A (tag throughout with protection) vs. Path B (tag for dev/qa, SHA for prod). Recommendation: Path B, justified by the vision's "Audit truth lives outside the repository" bet [1] and the "Not a mutable audit log" anti-goal [1]; SHA-pinning is the only guarantee that the exact bytes reviewed in dev/qa are the bytes deployed to prod. The platform provides a CLI command that resolves the current tag to its SHA for prod-bound workflows. Pending sign-off.
|
|
||||||
|
|
||||||
## 5. Layer 3B — Agentic Consumer Surface
|
|
||||||
|
|
||||||
Locked commitments (unchanged from v0.1):
|
|
||||||
|
|
||||||
- Hybrid runtime, skill as markdown, agent as executor.
|
|
||||||
|
|
||||||
|
- Hybrid runtime: skill as markdown, agent as executor.
|
||||||
- Trust model: trust and always verify on the platform side.
|
- Trust model: trust and always verify on the platform side.
|
||||||
|
|
||||||
- Skill envelope (4 dimensions).
|
- Skill envelope (4 dimensions).
|
||||||
|
- Stateless agents; all state in the platform.
|
||||||
- Stateless agents, all state in the platform.
|
- Initial skill catalog: web API, worker, scheduled job, static asset,
|
||||||
|
basic observability bootstrap. Addition criteria: (a) reviewable for
|
||||||
Environment progression — locked (this revision):
|
sensitive data, (b) expressible as a single contract submission,
|
||||||
|
(c) documented use case.
|
||||||
| Environment | Autonomy | Attester | Gate |
|
- `profile: agentic` unlocks agentic-specific fields
|
||||||
|---|---|---|---|
|
(`naturalLanguageIntent`, `confidenceAtSubmission`, `agentTrace`).
|
||||||
| dev | Full autonomy (no HITL) | — | Confidence signal ≥ 0.50, all six inputs present |
|
|
||||||
| qa | Held for attestation | QA | GitHub Deployment approval + full QA matrix (see §10) |
|
|
||||||
| prod | Held for attestation | SRE | GitHub Deployment approval + full SRE matrix (see §10) |
|
|
||||||
| dr | Held for attestation | SRE | GitHub Deployment approval + dr-drill evidence (see §10) |
|
|
||||||
|
|
||||||
Staging is removed. Dev is the only autonomous environment and absorbs integration, contract, security smoke, and performance smoke validation. The CDLC reference document's environment model is a doc-sync item flagged at the top of this document.
|
|
||||||
|
|
||||||
Profile marker: profile: agentic unlocks L3B-specific fields naturalLanguageIntent, confidenceAtSubmission, agentTrace).
|
|
||||||
|
|
||||||
🟡 OPEN (BA.A): Skill catalog. Which skills exist in the initial L3B capability set, who decides what gets added, how are skills deprecated. Pending resolution.
|
|
||||||
|
|
||||||
## 6. Cross-Cutting — Central Pipeline Template
|
## 6. Cross-Cutting — Central Pipeline Template
|
||||||
|
|
||||||
Locked commitments (unchanged from v0.1):
|
- JSON Schema (draft 2020-12) with a thin domain-specific wrapper.
|
||||||
|
|
||||||
- JSON Schema (draft 2020-12) with thin domain-specific wrapper.
|
|
||||||
|
|
||||||
- Central repo + generated client libraries.
|
- Central repo + generated client libraries.
|
||||||
|
- Multi-stage validation pipeline: schema → policy → NFR → confidence.
|
||||||
- Multi-stage validation pipeline (schema → policy → NFR → confidence).
|
|
||||||
|
|
||||||
- Distributed enrichment.
|
- Distributed enrichment.
|
||||||
|
- GitOps reconciler + substrate execution layer.
|
||||||
- GitOps reconciler + Terraform execution layer.
|
- The pipeline emits a `PolicyCheckResult` record per policy rule evaluated;
|
||||||
|
the confidence signal consumes these as one normalized input (§8).
|
||||||
Locked additions this revision:
|
|
||||||
|
|
||||||
- The GitOps reconciler is the platform's K8s API. The cdlc-gitops repository's state materializes into K8s CRDs (ArgoCD Applications or Flux Kustomizations) that the reconciler watches. This is the platform's internal state surface.
|
|
||||||
|
|
||||||
- The pipeline emits a PolicyCheckResult record per policy rule evaluated. The confidence signal consumes these as one normalized input (Section 8).
|
|
||||||
|
|
||||||
🟡 OPEN (W3.D): L1/L2 standard versioning details — semver scheme, pin model, evolution compatibility contract.
|
|
||||||
|
|
||||||
🟡 OPEN (W3.E): Schema mandatory vs. optional inputs — which are required for all consumers, which are required only for higher environments, which are always optional.
|
|
||||||
|
|
||||||
## 7. Cross-Cutting — Contract Schema
|
## 7. Cross-Cutting — Contract Schema
|
||||||
|
|
||||||
Locked commitments (unchanged from v0.1):
|
|
||||||
|
|
||||||
- Central repo + generated client libraries.
|
- Central repo + generated client libraries.
|
||||||
|
- Strict fail-fast at the schema stage with reason codes from a published
|
||||||
- Strict fail-fast at schema stage, multi-stage validation pipeline with reason codes from a published vocabulary.
|
vocabulary.
|
||||||
|
- Per-environment mandatory fields: dev requires stack + environment; qa
|
||||||
🟡 OPEN (W3.E): Schema mandatory vs. optional inputs. The CDLC reference contract example [1] is illustrative; the v1 contract schema needs explicit per-field mandatory/optional declarations per environment.
|
adds `validation.e2eSuite` + `validation.loadTest`; prod adds runbook +
|
||||||
|
dashboard + oncall; dr adds `drDrillRef`. `inputs` is always optional.
|
||||||
|
`profile: agentic` fields are optional everywhere (`naturalLanguageIntent`
|
||||||
|
required when profile is agentic).
|
||||||
|
|
||||||
## 8. Cross-Cutting — Confidence Signal
|
## 8. Cross-Cutting — Confidence Signal
|
||||||
|
|
||||||
Locked commitments (unchanged from v0.1):
|
- Six canonical inputs: policy, validation, freshness, source, history, NFRs.
|
||||||
|
|
||||||
- Six canonical inputs.
|
|
||||||
|
|
||||||
- Weighted sum with per-input breakdown.
|
- Weighted sum with per-input breakdown.
|
||||||
|
|
||||||
- Per-environment thresholds: dev ≥ 0.50, qa ≥ 0.75, prod ≥ 0.90, dr ≥ 0.95.
|
- Per-environment thresholds: dev ≥ 0.50, qa ≥ 0.75, prod ≥ 0.90, dr ≥ 0.95.
|
||||||
|
- Structured output: `{ score, band, perInput, reasonCodes }`.
|
||||||
- Structured output { score, band, perInput, reasonCodes }.
|
- 1-year storage; no algorithm retraining in v1.
|
||||||
|
|
||||||
- 1-year storage, no algorithm retraining in v1.
|
|
||||||
|
|
||||||
- Halt with explicit reason on missing input.
|
- Halt with explicit reason on missing input.
|
||||||
|
- Severity → score penalty: critical → hard override to mandatory block,
|
||||||
Locked additions this revision:
|
high → -0.2, medium → -0.05, low → -0.01, info → 0.0. One critical finding
|
||||||
|
hard-overrides the score regardless of all other inputs.
|
||||||
- The policy check results input is a list of PolicyCheckResult records from the normalized schema (Section 9, 12). The signal does not know which engine produced which result.
|
- Thresholds frozen for v1; tuning begins post-v1 with quarterly FP/FN
|
||||||
|
tracking per environment. Override authority = Infra & Ops + SRE joint
|
||||||
- Severity → score penalty mapping: critical → hard override to mandatory block, high → -0.2, medium → -0.05, low → -0.01, info → 0.0. One critical finding hard-overrides the score regardless of all other inputs.
|
sign-off; any override is itself a confidence-event in the audit stream.
|
||||||
|
|
||||||
🟡 OPEN (BA.B): Threshold tuning policy. The initial thresholds (dev 0.50, qa 0.75, prod 0.90, dr 0.95) are starting values. The tuning process, false-positive/false-negative tracking, and override authority are pending.
|
|
||||||
|
|
||||||
## 9. Cross-Cutting — Audit and Evidence Stream
|
## 9. Cross-Cutting — Audit and Evidence Stream
|
||||||
|
|
||||||
Locked commitments (unchanged from v0.1):
|
- Every delivery action produces an immutable, hash-chained evidence event.
|
||||||
|
- The audit stream is the platform's certified record of what happened, when,
|
||||||
- Tiered audit ledger: S3 with Object Lock in compliance mode (cold, source of truth, 7-year retention) + GitHub audit repo (hot, query index, not part of the chain).
|
and why.
|
||||||
|
- Events are written to a DynamoDB outbox and rendered on an evidence
|
||||||
- Daily checkpoints.
|
timeline.
|
||||||
|
|
||||||
- Event schema: JWS detached signature, prev_event_hash chain, controlled-vocabulary event_type.
|
## 10. Cross-Cutting — HITL Matrix
|
||||||
|
|
||||||
- Outbox pattern with local durable outbox + async worker.
|
Human-in-the-loop gates for higher environments:
|
||||||
|
|
||||||
- Linkage via workflow run ID or agent invocation ID.
|
| Environment | Autonomy | Attester | Gate |
|
||||||
|
|---|---|---|---|
|
||||||
Locked additions this revision:
|
| dev | Full autonomy (no HITL) | — | Confidence ≥ 0.50, all six inputs present |
|
||||||
|
| qa | Held for attestation | QA | Platform-runner deployment approval + full QA matrix |
|
||||||
- The outbox database is DynamoDB. RPO is zero (synchronous write to local outbox before contract submission ack); RTO is the async worker's recovery from the dead-letter queue. Single-region in v1; multi-region is a v2 concern.
|
| prod | Held for attestation | SRE | Platform-runner deployment approval + full SRE matrix |
|
||||||
|
| dr | Held for attestation | SRE | Platform-runner deployment approval + dr-drill evidence |
|
||||||
- The outbox also stores the per-contract QA and prod approver identities (Section 10). The platform-internal identity-distinctness check reads from this outbox. This is the only durable record of the approver identities outside GitHub's audit log.
|
|
||||||
|
Staging does not exist. Dev is the only autonomous environment and absorbs
|
||||||
🟡 OPEN (BA.C): On-call and operational ownership. The platform's on-call rotation, escalation paths when L3A or L3B halts unexpectedly, and the relationship to consumer on-call.
|
integration, contract, security smoke, and performance smoke validation.
|
||||||
|
|
||||||
## 10. Cross-Cutting — Human-in-the-Loop Mechanics
|
- Pre-execution gate model. 1 business day = warn + escalate; 2 business
|
||||||
|
days = auto-freeze + re-submit. Rejection extends the audit chain; no
|
||||||
Purpose. The human gates at higher environments. The vision's "Lower Environments are Autonomous; Higher Environments are Attested" tenet [1] and the "deliberate human attestation — not as a rubber stamp" requirement [1] are the binding constraints.
|
partial deploy to roll back.
|
||||||
|
- Separation of duties: the platform-internal identity record in the
|
||||||
### 10.1 Gate model
|
DynamoDB outbox enforces `qaApprover ≠ prodApprover` for the same contract.
|
||||||
|
|
||||||
Pre-execution gates. The contract is held in a "validated but not applied" state until the human attests. qa, prod, and dr are PR-based attestation gates backed by GitHub Environments with required reviewers.
|
## 11. Cross-Cutting — Separation of Duties
|
||||||
|
|
||||||
For qa and prod, there is no partial deployment to roll back on rejection. For dr, the same model — promotion to the DR environment is a separate GitHub Deployment, gated by SRE, against a separate cluster/region. The canary/deployment-rollback model is explicitly not in scope for v1.
|
- CODEOWNERS routes the right reviewer to the right environment.
|
||||||
|
- The DynamoDB outbox enforces identity distinctness across environment
|
||||||
### 10.2 Reviewer routing
|
approvers.
|
||||||
|
|
||||||
GitHub CODEOWNERS + GitHub Environment required reviewers. qa → QA team; prod → SRE team; dr → SRE team. CODEOWNERS is the routing layer; it does not enforce identity distinctness.
|
## 12. Cross-Cutting — Substrate Execution
|
||||||
|
|
||||||
### 10.3 Separation of duties — identity distinctness
|
The technical execution layer. Primitives and modules are substrate-agnostic
|
||||||
|
in shape; substrate adapters are the only substrate-specific component.
|
||||||
Mechanism is platform-internal, not GitHub-native, not Kyverno (in v1).
|
|
||||||
|
The architecture defines a **Target Stack** — a substrate-neutral
|
||||||
Sequence:
|
description of:
|
||||||
|
|
||||||
1. On promotion dev → qa, the platform reads the QA approver's GitHub identity from the GitHub Deployment approval event and writes it to the DynamoDB outbox keyed by contractId.
|
- The resources to create (typed against the stack schema).
|
||||||
|
- Their relationships (the module's pattern tree).
|
||||||
2. On promotion qa → prod, the platform reads the stored QA approver identity from the outbox and the new SRE approver identity from the GitHub Deployment approval event.
|
- Their inputs (wired from the contract).
|
||||||
|
- Policy hooks (the points in the pattern where policy checks attach).
|
||||||
3. If qaApprover == prodApprover, the platform blocks the prod promotion, writes a SEPARATION_OF_DUTIES_VIOLATION event to the evidence stream, and routes a halt artifact to the SRE on-call.
|
|
||||||
|
The registry, the module pattern tree, the contract schema, and the
|
||||||
4. The check is implemented in the central pipeline repo, not as an external policy. The platform is the only writer to the outbox; the check is in the same process that has authority to block the promotion.
|
`PolicyCheckResult` schema are all defined against the stack schema. None is
|
||||||
|
defined against any specific substrate.
|
||||||
### 10.4 Full HITL attestation matrix
|
|
||||||
|
**v1 implementation reality:** the stack is shaped to round-trip cleanly to
|
||||||
| Env | Concern | Evidence artifact | Freshness | Source | Attester |
|
Terraform because there is no other adapter to differentiate from. As
|
||||||
|---|---|---|---|---|---|
|
additional adapters appear, the stack gets more expressive and the adapters
|
||||||
| qa | Functional correctness | Last successful run of contract-declared validation.e2eSuite with pass rate ≥ 99% | Last 24h | Test runner declared in contract | QA |
|
gain translation logic, but the primitive content, the module pattern tree,
|
||||||
| qa | Performance baseline | Load test report (k6 / Gatling / Locust) showing p99 latency < declared NFR and throughput > declared minimum | Last 7d | Load test runner declared in contract | QA |
|
and the contract schema do not change. This is the design that prevents a
|
||||||
| qa | Security posture | Vulnerability scan (Trivy, Snyk, or contract-declared equivalent) with no criticals/highs, signed by Security on-call | Last 24h | Security scanner + Security team signature | QA |
|
polyglot mess.
|
||||||
| qa | Contract NFRs | Platform-generated report: schema valid, NFR assertions (latency, throughput, error rate) within declared bounds | At submission | Platform contract validator | QA |
|
|
||||||
| prod | Operational readiness | Runbook published, dashboard exists, on-call rotation assigned, alerts configured | At submission, validated against last 30d history | Platform + SRE | SRE |
|
The substrate adapter:
|
||||||
| prod | Incident response | Sev-1 runbook tabletop or live drill completed | Last 90d | SRE drill record | SRE |
|
|
||||||
| prod | Capacity / cost | FinOps forecast for next 30d within budget envelope, cost anomaly baseline stored, budget alert configured | Forecast valid for next 30d | FinOps + SRE | SRE |
|
- Translates the stack-typed module pattern tree to a substrate root module
|
||||||
| prod | Resilience | DR drill, chaos engineering report, backup verified | DR: 180d; chaos: 90d; backup: 30d | SRE + Platform | SRE |
|
that calls the primitive modules.
|
||||||
| dr | dr-region deploy with the most recent prod-bound dr drill as canary evidence | dr drill report | Last 180d | SRE | SRE |
|
- Is a thin layer. It does not own primitive/module content; it only
|
||||||
|
translates.
|
||||||
### 10.5 Timeout behavior
|
- Is the only substrate-specific code in the platform.
|
||||||
|
|
||||||
| Time | State | Action |
|
Policy checks run on the substrate plan output. Results are normalized to
|
||||||
|---|---|---|
|
`PolicyCheckResult` records by a policy adapter. The confidence signal
|
||||||
| Submission | PENDING_ATTESTATION | Notify responsible team |
|
consumes the union of all `PolicyCheckResult` records, regardless of engine
|
||||||
| 1 business day | PENDING_ATTESTATION_WARNING | Notify team + platform on-call (elevated path); emit PENDING_ATTESTATION_TIMEOUT_WARNING event |
|
— substrate-agnostic over its inputs, matching the module model's
|
||||||
| 2 business days | PENDING_ATTESTATION_AUTO_FREEZE | Auto-freeze; require re-submission; emit PENDING_ATTESTATION_AUTO_FREEZE event; new submission linked via supersedes |
|
substrate-agnosticism over its outputs.
|
||||||
|
|
||||||
### 10.6 Rejection and rollback
|
## 13. Cross-Cutting — Platform Runners
|
||||||
|
|
||||||
Rejection returns the contract to a HELD state with the rejection reason captured as a PROMOTION_REJECTED event. The consumer fixes the cause and re-submits; the new submission is linked to the rejected one via supersedes. The audit chain is extended, not torn up — matching the resolution session's answer.
|
The platform runs on platform-managed runners (GitHub Actions in
|
||||||
|
production). Runner-specific code = workflow YAML, OIDC trust, CODEOWNERS,
|
||||||
There is no partial deployment to roll back at any v1 gate.
|
environments. The contract schema, stack, `PolicyCheckResult`, confidence
|
||||||
|
signal, and audit stream are portable (runner-agnostic); a second runner
|
||||||
## 11. Cross-Cutting — Agentic Stack
|
platform needs a runner adapter + workflow-template translator, with no
|
||||||
|
change to the modules/stack/confidence/audit.
|
||||||
Locked commitments (unchanged from v0.1):
|
|
||||||
|
## 14. Versioning
|
||||||
- Hybrid runtime, platform-managed control plane + consumer-owned agent.
|
|
||||||
|
- Primitives and modules use semver: interface → MAJOR, behavior → MINOR,
|
||||||
- Versioned, signed skill catalog over MCP.
|
lifecycle → PATCH.
|
||||||
|
- A module pins primitives by `name@semver`; the resolver picks the highest
|
||||||
- Skill envelope enforced on invocation and result submission.
|
compatible.
|
||||||
|
- A MAJOR bump requires a new registry entry (immutable publication); the
|
||||||
- Consumer-owned skill execution environment. Platform does not run the skill.
|
old entry enters a 12-month deprecation window.
|
||||||
|
- The central deploy pipeline is referenced by a floating MAJOR + MINOR tag
|
||||||
- Stateless agents, all state in the platform.
|
(e.g. `@v1.6`); patch fixes flow within the tag, breaking changes land
|
||||||
|
under the next MINOR tag.
|
||||||
Locked additions this revision:
|
|
||||||
|
See [Versioning](pipeline/versioning) for the consumer-facing details.
|
||||||
- Skills are reviewed for sensitive data before release. Secrets, customer data, internal IPs, and other sensitive payloads are forbidden in skill markdown. The review is owned by Infra & Ops and is the mandatory release gate for any new skill. This is the trade-off for accepting the L3B runtime threat model (skill content is consumer-readable, so the platform must not put anything sensitive in it).
|
|
||||||
|
## 15. OpenTofu
|
||||||
🟡 OPEN (BA.A): Skill catalog. Initial skill set, addition process, deprecation process.
|
|
||||||
|
Not in v1. The substrate abstraction (§12) makes OpenTofu a future adapter,
|
||||||
## 12. Cross-Cutting — L1/L2 Substrate Execution
|
not an architecture change. Revisit when an OpenTofu adapter is requested.
|
||||||
|
|
||||||
Purpose. The technical execution layer for the L1/L2 substrate, including the substrate abstraction that protects v1 from polyglot mess while leaving v2+ room to grow.
|
|
||||||
|
|
||||||
### 12.1 Substrate abstraction (locked this revision)
|
|
||||||
|
|
||||||
L1/L2 are substrate-agnostic in shape. The architecture defines a Target Stack — a substrate-neutral description of:
|
|
||||||
|
|
||||||
- Resources with typed input contracts, typed output contracts, and declared NFRs.
|
|
||||||
|
|
||||||
- Relationships (single parent per child, with a shared keyword for multi-relationship dependencies).
|
|
||||||
|
|
||||||
- Composition (a tree of resources with max depth 5).
|
|
||||||
|
|
||||||
- Policy hooks (the points in the composition where policy checks attach).
|
|
||||||
|
|
||||||
The L1 registry, the L2 composition tree, the YML standard, and the policy check result schema are all defined against the stack schema. None of them is defined against any specific substrate.
|
|
||||||
|
|
||||||
Substrate adapters are the only substrate-specific code. An adapter compiles the stack into a substrate execution plan. v1 ships exactly one adapter: the Terraform adapter. v2+ may add additional adapters (OpenTofu, Pulumi, K8s CRDs) without architectural change.
|
|
||||||
|
|
||||||
v1 implementation reality: the stack is shaped to round-trip cleanly to Terraform because there is no other adapter to differentiate from. The stack and the Terraform output are nearly isomorphic in v1. As additional adapters appear in v2+, the stack gets more expressive (e.g., substrate-specific output types) and the adapters gain translation logic, but the L1 module content, the YML standard, and the composition tree do not change. This is the design that prevents the polyglot mess.
|
|
||||||
|
|
||||||
Why not build the abstraction earlier? Building a substrate-agnostic stack before there is a second adapter to test against is speculative generality. The v1 commitment is: (1) the L1 module interface is defined against the stack schema even though the only adapter is Terraform, and (2) the central pipeline, registry, and policy schema consume the stack-typed contracts. The adapter is the only place where substrate terminology appears in v1.
|
|
||||||
|
|
||||||
### 12.2 Terraform adapter (v1)
|
|
||||||
|
|
||||||
The Terraform adapter:
|
|
||||||
|
|
||||||
- Translates the stack-typed L1 module interface to a Terraform variable block and a Terraform output block.
|
|
||||||
|
|
||||||
- Translates the stack-typed L2 composition tree to a Terraform root module that calls the L1 modules.
|
|
||||||
|
|
||||||
- Translates the stack-typed relationships to Terraform module references.
|
|
||||||
|
|
||||||
- Emits a Terraform plan from the stack.
|
|
||||||
|
|
||||||
The adapter is a thin layer. It does not own L1/L2 content; it only translates.
|
|
||||||
|
|
||||||
### 12.3 State storage
|
|
||||||
|
|
||||||
Locked: S3 (state files) + DynamoDB (state locking), cloud-managed. Single-region in v1.
|
|
||||||
|
|
||||||
### 12.4 Policy toolchain
|
|
||||||
|
|
||||||
Locked:
|
|
||||||
|
|
||||||
- Checkov for Terraform plan policy (the four L2 thin-composition checks: secrets-in-plaintext, public ingress, IAM wildcard, KMS key reference, plus tag and naming convention). Checkov is open-source, has a broad rule catalog, and is GitOps-friendly.
|
|
||||||
|
|
||||||
- Kyverno for K8s-native policy (platform-internal state in the GitOps reconciler, separation-of-dues-adjacent checks if any are added in v2, future CRD validation).
|
|
||||||
|
|
||||||
- OPA/Rego is reserved for cross-resource policy and is explicitly last resort due to Rego complexity.
|
|
||||||
|
|
||||||
### 12.5 Execution layer
|
|
||||||
|
|
||||||
Locked: GitHub Actions. terraform plan and terraform apply run in the central pipeline repo's GitHub Actions workflow. State locking via DynamoDB. AWS credentials via OIDC federation (long-lived credentials are forbidden). The platform does not run terraform apply against a developer's workstation; all execution is in the central pipeline.
|
|
||||||
|
|
||||||
### 12.6 Policy result normalization (locked this revision)
|
|
||||||
|
|
||||||
The confidence signal does not consume raw Checkov or Kyverno output. It consumes a normalized PolicyCheckResult schema produced by substrate-specific adapters.
|
|
||||||
|
|
||||||
Schema (canonical form, lives in the central pipeline repo):
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"contractId": "uuid",
|
|
||||||
"evaluatedAt": "ISO-8601",
|
|
||||||
"engine": "checkov | kyverno | opa",
|
|
||||||
"ruleId": "CKV_AWS_24 | KYVERNO_NO_PRIVILEGED | ...",
|
|
||||||
"severity": "critical | high | medium | low | info",
|
|
||||||
"result": "pass | fail | skipped | error",
|
|
||||||
"message": "human-readable",
|
|
||||||
"evidence": { "...engine-specific payload, opaque to the signal..." },
|
|
||||||
"resourceRef": "stack-typed resource identifier"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The Checkov adapter runs in the same GitHub Actions step as Checkov itself and translates Checkov JSON to PolicyCheckResult records. The Kyverno adapter runs as a controller in the platform's K8s cluster and translates Kyverno PolicyReport CRDs to PolicyCheckResult records. The confidence signal's policy input component is the union of all PolicyCheckResult records, regardless of engine. The signal does not know which engine produced which result — substrate-agnostic over its inputs, matching the L1/L2 model's substrate-agnostic over its outputs.
|
|
||||||
|
|
||||||
### 12.7 Registry maintenance
|
|
||||||
|
|
||||||
Locked: L1 module publication updates the L1 registry in the same PR as the module. Registry and module land together. The registry is the stack-typed contract, not a Terraform-specific variable schema. The L1 registry, the central pipeline, and the policy schema all consume the same stack-typed contract — there is one source of truth for the L1 interface, not multiple substrate-specific copies.
|
|
||||||
|
|
||||||
### 12.8 Contract-schema-to-stack resolution
|
|
||||||
|
|
||||||
The contract schema declares the consumer's intent in stack-typed terms. The central pipeline resolves the contract to a target stack (a list of L1 module instances with their inputs and the relationships between them). The Terraform adapter compiles the target stack to a Terraform execution plan. This resolution is substrate-agnostic — the target stack is in the stack schema.
|
|
||||||
|
|
||||||
🟡 OPEN (W3.D): L1/L2 standard versioning details, including pin model and evolution compatibility contract.
|
|
||||||
|
|
||||||
## 13. Consolidated Open Design Decisions
|
|
||||||
|
|
||||||
The following 11 decisions remain open. They are the gating items for v1.0.
|
|
||||||
|
|
||||||
### From Wave 1 (L1/L2 Substrate)
|
|
||||||
|
|
||||||
- (W1.A) AI-refinement trigger. Recommendation: joint condition — N ≥ 50 consecutive changes with zero rollbacks AND no L1/L2 incident in last 6 months AND Infra & Ops unilateral override. Pending sign-off.
|
|
||||||
|
|
||||||
- (W1.B) Multi-stack edge case rule. Recommendation: permitted only for (a) DR-region mirror, (b) time-boxed experimental stack with TTL ≤ 30d, (c) explicit Infra & Ops approval with documented justification in multiStack.justification. Pending sign-off.
|
|
||||||
|
|
||||||
### From Wave 2 (L3A/L3B)
|
|
||||||
|
|
||||||
- (W2.A) Tag mutability for production-bound references. Recommendation: Path B (tag for dev/qa, SHA for prod) with platform-provided CLI to resolve tag → SHA. Pending sign-off.
|
|
||||||
|
|
||||||
### From Wave 3 (Technical Execution)
|
|
||||||
|
|
||||||
- (W3.D) L1/L2 standard versioning details. Semver scheme, pin model, evolution compatibility contract.
|
|
||||||
|
|
||||||
- (W3.E) Schema mandatory vs. optional inputs. Per-field mandatory/optional declarations per environment.
|
|
||||||
|
|
||||||
### From Beyond Architecture
|
|
||||||
|
|
||||||
- (BA.A) Skill catalog. Initial L3B skill set, addition process, deprecation process.
|
|
||||||
|
|
||||||
- (BA.B) Confidence signal threshold tuning. Initial thresholds are starting values; tuning process, FP/FN tracking, override authority.
|
|
||||||
|
|
||||||
- (BA.C) On-call and operational ownership. Platform on-call rotation, escalation paths, relationship to consumer on-call.
|
|
||||||
|
|
||||||
- (BA.D) Cost and capacity governance. Cloud cost ownership, consumption reporting, runaway spend detection and halting.
|
|
||||||
|
|
||||||
- (BA.E) Consumer onboarding. Developer and citizen developer onboarding flow, "getting started" path through the contract schema.
|
|
||||||
|
|
||||||
- (BA.F) Cross-platform evolution. What changes if a second source-control system (e.g., GitLab) is added; which architectural decisions are portable.
|
|
||||||
|
|
||||||
## 14. Document Status and Next Steps
|
|
||||||
|
|
||||||
Status: v0.2. Eight of the original 15 open items are locked. Eleven remain open. The architecture is internally consistent for the locked items; resolution of the open items is the path to v1.0.
|
|
||||||
|
|
||||||
Doc-sync items (out of scope of this document but flagged for the same change set):
|
|
||||||
|
|
||||||
- The CDLC reference document's environment model assumes staging exists. Path A invalidates that. The CDLC contract example's targetEnvironments: [staging, production] must be revised to [dev, qa, prod, dr].
|
|
||||||
|
|
||||||
To finalize to v1.0:
|
|
||||||
|
|
||||||
1. Resolve the 11 open items in Section 13.
|
|
||||||
|
|
||||||
2. Validate the locked substrate abstraction against a real v1 implementation spike (one L1 module, one L2 composition, one Terraform adapter, one contract submission end-to-end). The spike validates that the stack commitments do not require a polyglot mess.
|
|
||||||
|
|
||||||
3. Validate the locked HITL matrix against a tabletop exercise with QA and SRE.
|
|
||||||
|
|
||||||
4. Sign-off pass.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Final Asks — three remaining open questions, then sign-off
|
|
||||||
|
|
||||||
I have three open questions that gate v1.0. Resolve them and I will revise the architecture document to v1.0 and mark it ready for implementation.
|
|
||||||
|
|
||||||
Q1. W1.A + W1.B — AI-refinement trigger and multi-stack edge case rule. The recommendations are in the document. Do you accept them as committed, or do you want to amend?
|
|
||||||
|
|
||||||
Q2. W2.A — Tag mutability for production-bound references. Path A (tag throughout with protection) vs. Path B (tag for dev/qa, SHA for prod). My recommendation is Path B with a platform CLI to resolve tag → SHA. Accept or amend?
|
|
||||||
|
|
||||||
Q3. BA.A — Initial L3B skill catalog. The demo plan uses 3 stub skills deploy-web-api, add-observability, add-basic-auth). For v1.0, the real platform needs a defensible initial skill set. My recommendation: start with the 5 most common infrastructure intents (web API, worker, scheduled job, static asset, basic observability bootstrap) and grow from there. The criteria for addition: a skill must (a) be reviewable for sensitive data per the locked skill-review gate, (b) be expressible as a single contract submission, and (c) have a documented use case. Accept or amend?
|
|
||||||
|
|
||||||
Once these three are resolved, plus the 8 remaining items (W3.D, W3.E, BA.B, BA.C, BA.D, BA.E, BA.F, and the OpenTofu timing sub-decision), the architecture moves to v1.0.
|
|
||||||
|
|
||||||
Sign-off request. Are you ready for me to draft v1.0 once these are resolved, or do you want to amend the v0.2 above first?
|
|
||||||
@@ -0,0 +1,379 @@
|
|||||||
|
# Consumer Guide — Declare intent, deploy to AWS
|
||||||
|
|
||||||
|
This guide walks a consumer through creating their pipeline and defining a
|
||||||
|
contract that deploys any ACDL module to AWS. It is **generic** across all
|
||||||
|
modules in the registry; `static-assets` is the worked example, but every
|
||||||
|
step applies to `microservice` and any future module.
|
||||||
|
|
||||||
|
## The model
|
||||||
|
|
||||||
|
Consumers have their own repos and consume ACDL by referencing `uses:` the
|
||||||
|
central pipeline definitions. The consumer declares a **contract** (which
|
||||||
|
module, which environment, which inputs); the ACDL platform owns the
|
||||||
|
pipelines, modules, substrate adapter, and evidence stream.
|
||||||
|
|
||||||
|
You do not write infrastructure modules, workflow YAML, or adapter code.
|
||||||
|
You write a contract YAML file and the platform does the rest. Your
|
||||||
|
repository contains only your application code, your contracts, and your CI
|
||||||
|
definitions.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
A["your repo<br/>(app code + contracts + CI definitions)"] -->|uses: acdl/.github/workflows/deploy.yml@v1.6| B
|
||||||
|
B["platform runners<br/>(modules + pipelines + adapters + schemas)"] -->|contract -> resolver -> stack -> adapter<br/>-> security checks -> infrastructure plan -> policy checks<br/>-> confidence -> apply -> evidence event| C
|
||||||
|
C["your resources in AWS"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Versioning the `uses:` reference
|
||||||
|
|
||||||
|
The central deployment pipeline is **always versioned with floating MAJOR
|
||||||
|
and MINOR tags** (e.g. `acdl/pipelines/deploy.yaml@v1.6`). Version
|
||||||
|
constraints cannot be expressed inside the contract, so the tag in
|
||||||
|
`uses:` is the only immutability lever a consumer has. See
|
||||||
|
[Versioning](pipeline/versioning) for the full rationale.
|
||||||
|
|
||||||
|
**Unversioned references are discouraged.** Do not use `@main` or a bare
|
||||||
|
`acdl/pipelines/deploy.yaml`.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
These are the **only** prerequisites for a consumer repo. You do **not**
|
||||||
|
need an AWS account, infrastructure tooling, or a runner key — those are
|
||||||
|
platform-managed. See [Environments](environments/).
|
||||||
|
|
||||||
|
- **A consumer GitHub repository** for your application code + contracts.
|
||||||
|
- **A platform-managed environment** bound to your repo. The platform team
|
||||||
|
provisions the AWS account, network, state backend, and IAM role. If no
|
||||||
|
environment is bound, your first pipeline run emits a friendly onboarding
|
||||||
|
prompt. See [Environments](environments/).
|
||||||
|
- **Authorization to reference the central pipeline.** Onboarding grants
|
||||||
|
your repo the right to `uses: acdl/.github/workflows/deploy.yml@v1.6`.
|
||||||
|
Contact the platform team if you have not been onboarded.
|
||||||
|
|
||||||
|
## Step 1 — Create a consumer repo
|
||||||
|
|
||||||
|
Create a repository for your application. The top level holds your app
|
||||||
|
code; your contract lives at `.acdl/contract.yaml`. Example for a static
|
||||||
|
site:
|
||||||
|
|
||||||
|
```
|
||||||
|
my-static-site/
|
||||||
|
index.html
|
||||||
|
assets/
|
||||||
|
style.css
|
||||||
|
logo.png
|
||||||
|
.acdl/
|
||||||
|
contract.yaml
|
||||||
|
.github/
|
||||||
|
workflows/
|
||||||
|
deploy.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
Example for a microservice:
|
||||||
|
|
||||||
|
```
|
||||||
|
my-microservice/
|
||||||
|
app.py
|
||||||
|
Dockerfile
|
||||||
|
.acdl/
|
||||||
|
contract.yaml
|
||||||
|
.github/
|
||||||
|
workflows/
|
||||||
|
deploy.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
Your app code lives at the top level. Your contract lives at
|
||||||
|
`.acdl/contract.yaml` regardless of the module you deploy. Your CI
|
||||||
|
definition lives at `.github/workflows/deploy.yml`.
|
||||||
|
|
||||||
|
## Step 2 — Reference the central pipeline
|
||||||
|
|
||||||
|
In your contract YAML, declare `uses:` pointing at the central ACDL
|
||||||
|
deployment pipeline with a **versioned tag** (floating MAJOR + MINOR):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||||
|
```
|
||||||
|
|
||||||
|
This tells the platform to run the standard deployment pipeline:
|
||||||
|
validate-contract → resolve-stack → security checks → infrastructure plan →
|
||||||
|
policy checks → confidence → evidence event → apply.
|
||||||
|
|
||||||
|
## Step 3 — Define the contract
|
||||||
|
|
||||||
|
Write `.acdl/contract.yaml`. The `static-assets` example:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||||
|
module: static-assets
|
||||||
|
environment: dev
|
||||||
|
inputs:
|
||||||
|
bucket_name: my-static-site-assets
|
||||||
|
region: us-east-1
|
||||||
|
```
|
||||||
|
|
||||||
|
A `microservice` example:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||||
|
module: microservice
|
||||||
|
environment: dev
|
||||||
|
inputs:
|
||||||
|
image: my-registry/my-microservice:latest
|
||||||
|
port: 8080
|
||||||
|
env:
|
||||||
|
LOG_LEVEL: info
|
||||||
|
```
|
||||||
|
|
||||||
|
### Contract fields
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|-------|------|----------|-------------|
|
||||||
|
| `uses` | string | yes | Reference to the central deployment pipeline, **versioned** with a floating MAJOR+MINOR tag (e.g. `acdl/pipelines/deploy.yaml@v1.6`). Bare or `@main` references are discouraged. See [Versioning](pipeline/versioning). |
|
||||||
|
| `module` | string | yes | Module name from the registry — any primitive or module (e.g. `static-assets`, `microservice`, `s3`). See the [module catalog](modules/). |
|
||||||
|
| `environment` | string | yes | The platform-managed environment to deploy to (e.g. `dev`). See [Environments](environments/). |
|
||||||
|
| `inputs` | object | yes | Module-specific inputs (see the module's README). |
|
||||||
|
|
||||||
|
### Module inputs
|
||||||
|
|
||||||
|
Each module declares its inputs in its `interface.json` (primitives) or
|
||||||
|
`composition.json` (modules). Consult the [module catalog](modules/) for
|
||||||
|
the full list, or read the module's own README under `modules/l1/<name>/`
|
||||||
|
or `modules/l2/<name>/`. Each module also has an `examples/` directory
|
||||||
|
with validated consumer contract examples (`simple.yaml` + `complex.yaml`
|
||||||
|
+ variation files) that demonstrate real usage — see the module's
|
||||||
|
`## Examples` section.
|
||||||
|
|
||||||
|
The contract is validated against the contract schema. An invalid contract
|
||||||
|
(missing field, unknown module, wrong type) fails at the validate-contract
|
||||||
|
stage with a clear error.
|
||||||
|
|
||||||
|
## Step 4 — Run the pipeline
|
||||||
|
|
||||||
|
You do **not** run platform scripts locally for the happy path. The central
|
||||||
|
deploy workflow is a **reusable workflow** that the platform runners fetch
|
||||||
|
and execute for you.
|
||||||
|
|
||||||
|
### The consumer CI definition
|
||||||
|
|
||||||
|
Add a thin workflow file to **your** repo that invokes the reusable ACDL
|
||||||
|
deploy workflow with a **versioned tag** (`.github/workflows/deploy.yml`):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: deploy
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
uses: acdl/.github/workflows/deploy.yml@v1.6
|
||||||
|
with:
|
||||||
|
contract: .acdl/contract.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
That is the entire consumer-side workflow. When you push to `main`:
|
||||||
|
|
||||||
|
1. The platform runner resolves `uses: acdl/.github/workflows/deploy.yml@v1.6`
|
||||||
|
to the reusable workflow **at the pinned tag**.
|
||||||
|
2. A **platform-provided runner** checks out **your** repo.
|
||||||
|
3. The runner checks out the **ACDL platform repo** into the workspace —
|
||||||
|
this is how the pipeline fetches the platform code at run time. You
|
||||||
|
never clone the platform repo yourself.
|
||||||
|
4. The runner installs the runtime dependencies the platform requires.
|
||||||
|
5. The runner invokes `scripts/run_platform.sh` against your
|
||||||
|
`.acdl/contract.yaml`.
|
||||||
|
|
||||||
|
You see the streamed output (infrastructure plan, policy-check results,
|
||||||
|
confidence signal) in your run logs. The `--check-only` and `--plan-only`
|
||||||
|
flags are platform-side modes visible in the pipeline logs; you do not pass
|
||||||
|
them yourself — the reusable workflow selects the mode based on the
|
||||||
|
`environment` in your contract (`dev` = full apply; higher environments
|
||||||
|
hold for attestation).
|
||||||
|
|
||||||
|
### Local validation (optional)
|
||||||
|
|
||||||
|
A consumer *may* clone the ACDL platform repo to run `--check-only` against
|
||||||
|
their contract before pushing — this is optional and not required for the
|
||||||
|
happy path. If you do this, the runtime dependencies must be installed
|
||||||
|
locally, and any AWS credentials follow the
|
||||||
|
[Credentials](../README.md#credentials--zero-trust) override model: a
|
||||||
|
static key in `.env.secrets` (gitignored) is rotated **out of band by you**
|
||||||
|
— the platform guarantees daily rotation for platform-runner runs, not for
|
||||||
|
locally-held copies.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scripts/run_platform.sh --check-only path/to/your/.acdl/contract.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 5 — What the pipeline does
|
||||||
|
|
||||||
|
Each stage of the central deployment pipeline:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
S1["validate-contract<br/>schema check"] --> S2
|
||||||
|
S2["resolve-stack<br/>contract -> Target Stack"] --> S3
|
||||||
|
S3["security checks<br/>(adapter)"] --> S4
|
||||||
|
S4["infrastructure plan<br/>(adapter compiles the stack)"] --> S5
|
||||||
|
S5["policy checks<br/>(adapter -> PolicyCheckResult)"] --> S6
|
||||||
|
S6["confidence<br/>score + band (dev >= 0.50)"] --> S7
|
||||||
|
S7["evidence event<br/>to the audit outbox"] --> S8
|
||||||
|
S8["infrastructure apply<br/>(dev only)"]
|
||||||
|
```
|
||||||
|
|
||||||
|
1. **validate-contract** — validates your contract YAML against the contract
|
||||||
|
schema. Fails fast on missing fields, unknown modules, or wrong types.
|
||||||
|
2. **resolve-stack** — the contract resolver resolves your contract to a
|
||||||
|
Target Stack instance. It loads the module's pattern, expands its
|
||||||
|
children, wires your contract inputs to the children's inputs, and emits
|
||||||
|
a stack JSON instance.
|
||||||
|
3. **security checks** (adapter) — security checks run on the resolved
|
||||||
|
stack before any infrastructure is planned.
|
||||||
|
4. **infrastructure plan** (adapter) — the substrate adapter compiles the
|
||||||
|
stack to an infrastructure plan. You see the plan in your run logs.
|
||||||
|
5. **policy checks** (adapter) — policy checks run on the plan. The results
|
||||||
|
are normalized to `PolicyCheckResult` records. Each result has a
|
||||||
|
severity, rule ID, and pass/fail status.
|
||||||
|
6. **confidence** — the confidence signal computes a score from 6 inputs
|
||||||
|
(policy, validation, freshness, source, history, NFRs). For `dev`, the
|
||||||
|
threshold is ≥ 0.50. If the band is `pass`, the pipeline proceeds.
|
||||||
|
7. **evidence event** — a hash-chained evidence event is written to the
|
||||||
|
audit outbox.
|
||||||
|
8. **infrastructure apply** (dev only) — the infrastructure plan is applied,
|
||||||
|
creating the resources in your AWS account. An evidence event for the
|
||||||
|
apply is recorded.
|
||||||
|
|
||||||
|
## Step 6 — What gets created
|
||||||
|
|
||||||
|
After a successful `dev` run, the resources declared by your module's
|
||||||
|
pattern exist in your AWS account, and an evidence event is recorded.
|
||||||
|
|
||||||
|
For the `static-assets` example:
|
||||||
|
|
||||||
|
- **An S3 bucket** named `my-static-site-assets` in `us-east-1` with
|
||||||
|
versioning enabled.
|
||||||
|
- **A CloudFront distribution** with the S3 bucket as the origin (via
|
||||||
|
Origin Access Control) and HTTPS redirection.
|
||||||
|
- **A WAFv2 Web ACL** (CloudFront-scoped) associated with the
|
||||||
|
distribution.
|
||||||
|
- **An evidence event** in the audit outbox with the contract ID, stack
|
||||||
|
name (`static-assets`), confidence score, and band.
|
||||||
|
- **A confidence band** of `pass` (score ≥ 0.50 for dev).
|
||||||
|
|
||||||
|
For other modules, consult the module's README
|
||||||
|
(`modules/l1/<name>/README.md` or `modules/l2/<name>/README.md`) for the
|
||||||
|
exact resources created.
|
||||||
|
|
||||||
|
## Step 7 — Upload your content (static-assets example)
|
||||||
|
|
||||||
|
The platform provisions the infrastructure; you upload your content. For
|
||||||
|
the `static-assets` module:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
aws s3 sync ./assets s3://my-static-site-assets/ --acl public-read
|
||||||
|
```
|
||||||
|
|
||||||
|
For a `microservice`, the platform provisions the ECS service and ALB; you
|
||||||
|
push your container image to the ECR repo the platform created.
|
||||||
|
|
||||||
|
## Step 8 — Promote to qa / prod
|
||||||
|
|
||||||
|
Change `environment` in your contract (keeping the same versioned `uses:`):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||||
|
environment: qa # QA attestation + confidence >= 0.75
|
||||||
|
```
|
||||||
|
|
||||||
|
Higher environments require human attestation (a platform-runner deployment
|
||||||
|
approval) and higher confidence thresholds. See [Environments](environments/)
|
||||||
|
for the full table.
|
||||||
|
|
||||||
|
## Step 9 — Compliance extensions
|
||||||
|
|
||||||
|
Each module lists compliance extension points for the future compliance
|
||||||
|
milestone (GDPR, SOX, SOC2, HIPAA, DORA). See each module's README under
|
||||||
|
`modules/l1/<name>/README.md` or `modules/l2/<name>/README.md` for the
|
||||||
|
per-module extension points. Common examples:
|
||||||
|
|
||||||
|
- **KMS key** — shared encryption key for SSE.
|
||||||
|
- **S3 access logs** — access logging to a separate audit bucket.
|
||||||
|
- **Object Lock** — 7-year immutable retention for evidence.
|
||||||
|
- **Public access block** — prevent data exfiltration.
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
| Resource | Path | Description |
|
||||||
|
|----------|------|-------------|
|
||||||
|
| Central deployment pipeline contract | `pipelines/deploy.yaml` | The pipeline stages your contract references. |
|
||||||
|
| Reusable deploy workflow | `.github/workflows/deploy.yml` | The workflow your repo invokes via `uses:`. |
|
||||||
|
| Contract schema | `schemas/contract.schema.json` | JSON Schema for consumer contracts. |
|
||||||
|
| Stack schema | `schemas/stack.schema.json` | JSON Schema for the resolved stack instance. |
|
||||||
|
| Module catalog | [modules/](modules/) | All primitives and modules. |
|
||||||
|
| Sample contract | `contracts/static-assets.yaml` | The reference example contract (uses `@v1.6`). |
|
||||||
|
| Sample contract | `contracts/microservice.yaml` | The microservice example contract (uses `@v1.6`). |
|
||||||
|
| Module examples | `modules/<name>/examples/` | Validated per-module example contracts (`simple.yaml` + `complex.yaml`). |
|
||||||
|
| Contract resolver | `core/contract_resolver.py` | Resolves contracts to stack instances. |
|
||||||
|
| Substrate adapter | `adapters/terraform/adapter.py` | Compiles stack instances to infrastructure. |
|
||||||
|
| Platform pipeline runner | `scripts/run_platform.sh` | The pipeline runner (platform-side; consumers do not invoke it directly). |
|
||||||
|
| Environments | [environments/](environments/) | Platform-managed environments + onboarding. |
|
||||||
|
| Versioning | [pipeline/versioning](pipeline/versioning) | The `uses:` tag + module versioning. |
|
||||||
|
| Platform README | `README.md` | How the platform works + how to run the platform repo locally. |
|
||||||
|
| Credentials & zero-trust | `README.md#credentials--zero-trust` | The OIDC/ABAC default + static-key override model. |
|
||||||
|
|
||||||
|
## Decommissioning a stack
|
||||||
|
|
||||||
|
When a consumer needs to tear down a deployed stack, the platform provides
|
||||||
|
a **decommission mode** on the same deploy pipeline. The decommission
|
||||||
|
process is a 2-step pipeline with **HITL SRE gates** to prevent accidental
|
||||||
|
destruction:
|
||||||
|
|
||||||
|
1. **Request a change request (CR):** Contact the platform team to create a
|
||||||
|
change request in the platform CMDB (DynamoDB `acdl-change-requests`
|
||||||
|
table). The CR must be approved before decommission can proceed. The CR
|
||||||
|
includes the consumer repo, contract ID, and the reason for decommission.
|
||||||
|
|
||||||
|
2. **Trigger decommission:** Update the consumer's deploy workflow call to
|
||||||
|
use `mode: decommission` with the `changeRequestId` input:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
uses: acdl/.github/workflows/deploy.yml@v1.8
|
||||||
|
with:
|
||||||
|
contract: .acdl/contract.yaml
|
||||||
|
mode: decommission
|
||||||
|
changeRequestId: "CR-2026-001"
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Step 1 — Disable deletion protection (HITL SRE gate):** The pipeline
|
||||||
|
validates the CR ID against the CMDB (status must be `approved`). Then
|
||||||
|
it resolves the contract with `deletion_protection: false` injected into
|
||||||
|
all resources and runs `terraform plan` + `terraform apply`. This
|
||||||
|
removes the `prevent_destroy` lifecycle meta-argument from all resources.
|
||||||
|
**An SRE must approve this step** via the GitHub environment
|
||||||
|
`decommission-gate-sre`.
|
||||||
|
|
||||||
|
4. **Step 2 — Zero counts + destroy (HITL SRE gate):** The pipeline applies
|
||||||
|
`decommission_transform` which sets all scalable counts to 0
|
||||||
|
(`desired_count=0`, `min_capacity=0`, `max_capacity=0`) and
|
||||||
|
`deletion_protection=false` on all resources. Then it runs
|
||||||
|
`terraform plan` + `terraform apply` which destroys all resources (now
|
||||||
|
that deletion protection is off and counts are zeroed). **A second SRE
|
||||||
|
must approve this step** via the GitHub environment
|
||||||
|
`decommission-destroy-sre`.
|
||||||
|
|
||||||
|
5. **Confirmation:** The pipeline confirms the stack is destroyed
|
||||||
|
(terraform state is empty for the stack).
|
||||||
|
|
||||||
|
### What happens to the per-stack CMK?
|
||||||
|
|
||||||
|
The per-stack CMK is not immediately destroyed — it enters a deletion
|
||||||
|
window (default 30 days, configurable via the `deletion_window_days` input).
|
||||||
|
This ensures any encrypted data can still be decrypted during the deletion
|
||||||
|
window if needed. The CMK is permanently deleted after the window expires.
|
||||||
|
|
||||||
|
### What happens to the uptime monitoring?
|
||||||
|
|
||||||
|
The uptime monitoring stack (deployed with separate state) is not
|
||||||
|
automatically destroyed by the decommission. It must be destroyed
|
||||||
|
separately (or left running to monitor the decommissioned stack's
|
||||||
|
endpoints going dark).
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# Contracts
|
||||||
|
|
||||||
|
A consumer declares intent in a **contract** — a small YAML file that
|
||||||
|
references the central deploy pipeline, names a module, selects an
|
||||||
|
environment, and supplies module-specific inputs. The platform validates,
|
||||||
|
resolves, and deploys it.
|
||||||
|
|
||||||
|
## The contract file
|
||||||
|
|
||||||
|
A consumer repo keeps its contract at `.acdl/contract.yaml`. A minimal
|
||||||
|
example (the `static-assets` module):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||||
|
module: static-assets
|
||||||
|
environment: dev
|
||||||
|
inputs:
|
||||||
|
bucket_name: my-static-site-assets
|
||||||
|
region: us-east-1
|
||||||
|
```
|
||||||
|
|
||||||
|
A `microservice` example:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||||
|
module: microservice
|
||||||
|
environment: dev
|
||||||
|
inputs:
|
||||||
|
image: my-registry/my-microservice:latest
|
||||||
|
port: 8080
|
||||||
|
env:
|
||||||
|
LOG_LEVEL: info
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fields
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|-------|------|----------|-------------|
|
||||||
|
| `uses` | string | yes | Reference to the central deploy pipeline, **versioned** with a floating MAJOR+MINOR tag (e.g. `acdl/pipelines/deploy.yaml@v1.6`). Bare or `@main` references are discouraged. See [Versioning](../pipeline/versioning). |
|
||||||
|
| `module` | string | yes | Module name from the registry — any primitive or module (e.g. `static-assets`, `microservice`, `s3`). See the [module catalog](../modules/). |
|
||||||
|
| `environment` | string | yes | The platform-managed environment to deploy to (e.g. `dev`). See [Environments](../environments/). |
|
||||||
|
| `inputs` | object | yes | Module-specific inputs (see the module's README). |
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
The contract is validated against
|
||||||
|
[`schemas/contract.schema.json`](https://github.com/acdl/acdl/blob/main/schemas/contract.schema.json).
|
||||||
|
An invalid contract (missing field, unknown module, wrong type) fails at the
|
||||||
|
validate-contract stage with a clear error.
|
||||||
|
|
||||||
|
## Sample contracts
|
||||||
|
|
||||||
|
Two reference examples exist in `contracts/`:
|
||||||
|
|
||||||
|
- [`contracts/static-assets.yaml`](https://github.com/acdl/acdl/blob/main/contracts/static-assets.yaml)
|
||||||
|
— the `static-assets` module (uses `@v1.6`).
|
||||||
|
- [`contracts/microservice.yaml`](https://github.com/acdl/acdl/blob/main/contracts/microservice.yaml)
|
||||||
|
— the `microservice` module (uses `@v1.6`).
|
||||||
|
|
||||||
|
Additionally, every module has a `modules/<name>/examples/` directory with
|
||||||
|
validated example contracts (`simple.yaml` + `complex.yaml` + variation
|
||||||
|
files). See the [module catalog](../modules/) for the full list.
|
||||||
|
|
||||||
|
## Multiple contracts
|
||||||
|
|
||||||
|
A consumer repo may contain more than one contract (e.g. one per service or
|
||||||
|
one per environment). Each contract is a separate deployment; each is
|
||||||
|
referenced by a CI definition in `.github/workflows/` that invokes the
|
||||||
|
central reusable workflow with the contract path. See the
|
||||||
|
[Consumer Guide](../consumer-guide/) for the multi-contract pattern.
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
# Environments
|
||||||
|
|
||||||
|
A consumer does **not** provide an AWS account, a VPC, a subnet, an S3 state
|
||||||
|
bucket, or a runner key. The platform manages environments.
|
||||||
|
|
||||||
|
## What an environment is
|
||||||
|
|
||||||
|
A named environment is a **platform-owned** bundle of:
|
||||||
|
|
||||||
|
- An AWS account (or a scoped partition of one).
|
||||||
|
- A network (VPC + subnets).
|
||||||
|
- A state backend (an S3 bucket + DynamoDB lock table for infrastructure
|
||||||
|
state).
|
||||||
|
- An IAM role surfaced to the consumer via attribute-based authorization
|
||||||
|
(ABAC), scoped to the consumer's repository identity and resource tags.
|
||||||
|
|
||||||
|
A consumer selects an environment **by name** in their contract:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
environment: dev
|
||||||
|
```
|
||||||
|
|
||||||
|
The platform resolves the name to the underlying account/network/state/role
|
||||||
|
at run time. The consumer never sees the raw credentials.
|
||||||
|
|
||||||
|
## First-run onboarding
|
||||||
|
|
||||||
|
When a consumer pipeline runs for the first time and **no environment is
|
||||||
|
defined** for the consumer's repo, the platform detects this and emits a
|
||||||
|
user-friendly onboarding prompt instead of failing opaquely. The prompt
|
||||||
|
tells the consumer:
|
||||||
|
|
||||||
|
1. That no environment is bound to their repo yet.
|
||||||
|
2. What the platform will provision on their behalf (account/network/state/
|
||||||
|
role).
|
||||||
|
3. The expected turnaround for the platform team to grant the environment.
|
||||||
|
4. How to request an environment (contact the platform team).
|
||||||
|
|
||||||
|
The pipeline then exits without attempting a deployment. Once the platform
|
||||||
|
team binds an environment to the repo, the next pipeline run proceeds
|
||||||
|
normally.
|
||||||
|
|
||||||
|
## Autonomy by environment
|
||||||
|
|
||||||
|
| Environment | Autonomy | Gate |
|
||||||
|
|-------------|----------|------|
|
||||||
|
| dev | Full autonomy | Confidence ≥ 0.50 |
|
||||||
|
| qa | Held for attestation | QA attestation + confidence ≥ 0.75 |
|
||||||
|
| prod | Held for attestation | SRE attestation + confidence ≥ 0.90 |
|
||||||
|
| dr | Held for attestation | SRE attestation + confidence ≥ 0.95 + dr-drill |
|
||||||
|
|
||||||
|
`dev` is the only autonomous environment. Higher environments require human
|
||||||
|
attestation (a platform-runner deployment approval) and a higher confidence
|
||||||
|
threshold. Staging does not exist.
|
||||||
|
|
||||||
|
## Cross-account contract ingestion grant (D-051)
|
||||||
|
|
||||||
|
Onboarding now also grants the consumer repo's deploy role permission to
|
||||||
|
invoke the **platform Lambda** — `acdl-contract-ingestor` — across
|
||||||
|
accounts. The Lambda is invoked via a Function URL with IAM auth, so the
|
||||||
|
grant is an inline IAM policy applied to the consumer's deploy role. The
|
||||||
|
policy template lives at
|
||||||
|
[`terraform/platform/consumer_invoke_policy.json`](https://github.com/acdl/acdl/blob/main/terraform/platform/consumer_invoke_policy.json)
|
||||||
|
and is scoped via **ABAC**: the condition
|
||||||
|
`aws:PrincipalTag/acdl:owner == ${consumerRepo}` ensures a repo can only
|
||||||
|
invoke the Lambda when its principal tag matches its claimed identity.
|
||||||
|
|
||||||
|
The consumer's deploy workflow signs the Function URL request with
|
||||||
|
SigV4 using its deploy-role credentials; the platform Lambda validates
|
||||||
|
the signature and the ABAC condition before accepting the payload.
|
||||||
|
|
||||||
|
This is a **one-way** channel — the consumer pushes contracts *to* the
|
||||||
|
platform; the platform never reaches back into the consumer account. It
|
||||||
|
is used for two purposes:
|
||||||
|
|
||||||
|
1. **Contract ingestion** — the consumer submits its resolved deployment
|
||||||
|
contract (`action: "submit_contract"`) so the platform has a durable
|
||||||
|
record in the `acdl-contracts` DynamoDB table (PK `consumerRepo`, SK
|
||||||
|
`contractId#submittedAt`).
|
||||||
|
2. **Error reporting** (D-055) — the consumer reports a deployment error
|
||||||
|
(`action: "report_error"`) which the platform turns into a GitHub
|
||||||
|
issue on the platform repo (wired in Phase 25; the Lambda returns a
|
||||||
|
prepared-status stub until then).
|
||||||
|
|
||||||
|
The Lambda handler and the Terraform that deploys it live in
|
||||||
|
[`core/lambda/contract_ingestor.py`](https://github.com/acdl/acdl/blob/main/core/lambda/contract_ingestor.py)
|
||||||
|
and
|
||||||
|
[`terraform/platform/main.tf`](https://github.com/acdl/acdl/blob/main/terraform/platform/main.tf)
|
||||||
|
respectively.
|
||||||
|
|
||||||
|
## Onboarding scaffold (current state)
|
||||||
|
|
||||||
|
The platform repo ships a minimal onboarding scaffold:
|
||||||
|
|
||||||
|
- [`core/environments/`](https://github.com/acdl/acdl/blob/main/core/environments/)
|
||||||
|
— environment definitions (a sample `dev.json`).
|
||||||
|
- `core/environment_check.py` — checks whether an environment is defined for
|
||||||
|
a given contract's repo + environment name; prints the friendly onboarding
|
||||||
|
prompt when none is defined.
|
||||||
|
- `scripts/run_platform.sh` calls the check before contract validation.
|
||||||
|
|
||||||
|
The scaffold is minimal: the actual provisioning of a new environment is a
|
||||||
|
platform-team action today. Self-service environment provisioning is on the
|
||||||
|
[roadmap](../).
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
# ACDL — Agentic Cloud Delivery Platform
|
||||||
|
|
||||||
|
Consumers declare intent; the platform delivers safe production deployment
|
||||||
|
through an agentic stack — automatically, safely, and with a complete audit
|
||||||
|
trail. A merged change progresses through lower environments end-to-end
|
||||||
|
without a platform engineer joining a thread; a non-technical consumer ships
|
||||||
|
a production deployment by declaring intent, without authoring a workflow,
|
||||||
|
a configuration file, or an infrastructure module.
|
||||||
|
|
||||||
|
## Two repositories
|
||||||
|
|
||||||
|
There are two kinds of repository in the ACDL model:
|
||||||
|
|
||||||
|
- **Platform repo (this one).** The source code of the platform. It owns
|
||||||
|
`modules/`, `adapters/`, `core/`, `schemas/`, `pipelines/`, `scripts/`,
|
||||||
|
and the reusable workflow files. Platform engineers work here. A consumer
|
||||||
|
never clones it.
|
||||||
|
- **Consumer repo (yours).** A consumer repo contains only its application
|
||||||
|
code, one or more contracts (`.acdl/contract.yaml`), and one or more CI
|
||||||
|
definitions (a thin `.github/workflows/deploy.yml` that `uses:` the central
|
||||||
|
reusable workflow, pointing at the appropriate environment + contract).
|
||||||
|
The consumer does not write infrastructure modules, workflow YAML, or
|
||||||
|
adapter code.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
| Section | Audience | What it covers |
|
||||||
|
|---------|----------|----------------|
|
||||||
|
| [Consumer Guide](consumer-guide) | Consumers | Step-by-step: create a repo, write a contract, reference the central pipeline, ship a deployment. |
|
||||||
|
| [Modules](modules/) | Consumers + platform engineers | The module catalog — primitives and modules, their inputs/outputs, and usage. |
|
||||||
|
| [Contracts](contracts/) | Consumers | The contract schema, fields, and a worked sample. |
|
||||||
|
| [Pipeline](pipeline/) | Consumers + platform engineers | The central CI + deployment pipeline and its stages. |
|
||||||
|
| [Versioning](pipeline/versioning) | Consumers + platform engineers | Module versioning + deploy-pipeline versioning (the `uses:` tag). |
|
||||||
|
| [Environments](environments/) | Consumers | Platform-managed environments and the first-run onboarding flow. |
|
||||||
|
| [Architecture](architecture) | Platform engineers | The current architecture — layers, cross-cutting concerns, the substrate abstraction. |
|
||||||
|
| [Vision](vision) | All | The why — the friction the platform absorbs and the north star. |
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Contract-driven deploys** — a consumer writes a YAML contract; the
|
||||||
|
platform resolves it to a stack, compiles it, and deploys it.
|
||||||
|
- **Reusable versioned deploy workflow** — consumer repos `uses:` a
|
||||||
|
versioned central workflow; no platform code is cloned by the consumer.
|
||||||
|
- **Module catalog** — primitives (single resources) and modules (patterns
|
||||||
|
of primitives) with self-documented inputs/outputs.
|
||||||
|
- **Zero-trust credentials** — OIDC federation + attribute-based
|
||||||
|
authorization (ABAC) by default; no long-lived keys in consumer repos.
|
||||||
|
- **Security + policy checks** — a security-check stage and a policy-check
|
||||||
|
stage run before any infrastructure is created.
|
||||||
|
- **Confidence signal** — a computed, explainable score gates promotion.
|
||||||
|
- **Evidence outbox** — every deployment writes a hash-chained evidence
|
||||||
|
event to an audit outbox.
|
||||||
|
- **Shell reproducibility** — `scripts/run_ci.sh` mirrors the CI pipeline
|
||||||
|
locally; `scripts/run_platform.sh --check-only` runs offline.
|
||||||
|
- **Platform-managed environments** — consumers provide no AWS account,
|
||||||
|
VPC, subnet, or state bucket; the platform manages environments.
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
|
||||||
|
Planned future features (no dates; tracked in the internal roadmap):
|
||||||
|
|
||||||
|
- **Dynamic module creation from a contract** — an agentic flow where a
|
||||||
|
consumer creates a module directly from the contract file (the "composition"
|
||||||
|
mechanism, redesigned).
|
||||||
|
- **Compliance milestone** — per-module compliance extension points (GDPR,
|
||||||
|
SOX, SOC2, HIPAA, DORA) wired into the pipeline.
|
||||||
|
- **Additional substrate adapters** — beyond the Terraform adapter.
|
||||||
|
- **Environment self-service** — a consumer-facing flow to request and
|
||||||
|
provision a new platform-managed environment.
|
||||||
|
- **HITL gates for qa / prod / dr** — human attestation + higher confidence
|
||||||
|
thresholds for higher environments.
|
||||||
|
- **OIDC for all platform runners** — zero-trust credentials everywhere.
|
||||||
|
|
||||||
|
## Quick links
|
||||||
|
|
||||||
|
- [Consumer Guide](consumer-guide) — start here if you are a consumer.
|
||||||
|
- [Architecture](architecture) — start here if you are a platform engineer.
|
||||||
|
- The [README](https://github.com/acdl/acdl) describes the platform repo.
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# Modules
|
||||||
|
|
||||||
|
Reusable building blocks for cloud infrastructure. There are two kinds:
|
||||||
|
|
||||||
|
- **Primitives** — a single cloud resource or a small group of related
|
||||||
|
resources (e.g. a VPC with subnets and routing). Each primitive has an
|
||||||
|
`interface.json` declaring its inputs and outputs.
|
||||||
|
- **Modules** — a pattern that references multiple primitives to deploy a
|
||||||
|
complete stack (e.g. an ECS Fargate microservice). Each module has a
|
||||||
|
`composition.json` declaring its children and wires.
|
||||||
|
|
||||||
|
The substrate adapter compiles a module instance to infrastructure. Each
|
||||||
|
module's README documents which resources it creates.
|
||||||
|
|
||||||
|
## Primitives
|
||||||
|
|
||||||
|
| Module | What it creates | Source |
|
||||||
|
|--------|----------------|--------|
|
||||||
|
| `s3` | `aws_s3_bucket` — a single S3 bucket | [modules/l1/s3/README.md](https://github.com/acdl/acdl/blob/main/modules/l1/s3/README.md) |
|
||||||
|
| `vpc` | `aws_vpc` + `aws_subnet` + `aws_route_table` + `aws_internet_gateway` — VPC with subnets and routing | [modules/l1/vpc/README.md](https://github.com/acdl/acdl/blob/main/modules/l1/vpc/README.md) |
|
||||||
|
| `ecs-cluster` | `aws_ecs_cluster` — ECS Fargate cluster | [modules/l1/ecs-cluster/README.md](https://github.com/acdl/acdl/blob/main/modules/l1/ecs-cluster/README.md) |
|
||||||
|
| `ecs-service` | `aws_ecs_task_definition` + `aws_ecs_service` — Fargate service with task definition | [modules/l1/ecs-service/README.md](https://github.com/acdl/acdl/blob/main/modules/l1/ecs-service/README.md) |
|
||||||
|
| `iam-role` | `aws_iam_role` — IAM role with assume-role policy | [modules/l1/iam-role/README.md](https://github.com/acdl/acdl/blob/main/modules/l1/iam-role/README.md) |
|
||||||
|
| `alb` | `aws_lb` + `aws_lb_target_group` + `aws_lb_listener` — Application Load Balancer | [modules/l1/alb/README.md](https://github.com/acdl/acdl/blob/main/modules/l1/alb/README.md) |
|
||||||
|
| `ecr` | `aws_ecr_repository` — ECR container image repository | [modules/l1/ecr/README.md](https://github.com/acdl/acdl/blob/main/modules/l1/ecr/README.md) |
|
||||||
|
| `cloudfront` | `aws_cloudfront_distribution` + `aws_cloudfront_origin_access_control` — CloudFront distribution with S3 origin via OAC | [modules/l1/cloudfront/README.md](https://github.com/acdl/acdl/blob/main/modules/l1/cloudfront/README.md) |
|
||||||
|
| `waf` | `aws_wafv2_web_acl` — WAFv2 Web ACL (CloudFront-scoped) | [modules/l1/waf/README.md](https://github.com/acdl/acdl/blob/main/modules/l1/waf/README.md) |
|
||||||
|
| `rds` | `aws_db_instance` — RDS database instance (multi-engine: postgres, mysql, etc.) | [modules/l1/rds/README.md](https://github.com/acdl/acdl/blob/main/modules/l1/rds/README.md) |
|
||||||
|
|
||||||
|
## Modules
|
||||||
|
|
||||||
|
| Module | What it references | Source |
|
||||||
|
|--------|--------------------|--------|
|
||||||
|
| `static-assets` | 3 primitives (s3, cloudfront, waf) — a production static asset stack | [modules/l2/static-assets/README.md](https://github.com/acdl/acdl/blob/main/modules/l2/static-assets/README.md) |
|
||||||
|
| `microservice` | 6 primitives (vpc, cluster, ecr, iam-role, alb, ecs-service) — an ECS Fargate microservice | [modules/l2/microservice/README.md](https://github.com/acdl/acdl/blob/main/modules/l2/microservice/README.md) |
|
||||||
|
|
||||||
|
## Registry
|
||||||
|
|
||||||
|
Module versions are tracked in
|
||||||
|
[`registry.json`](https://github.com/acdl/acdl/blob/main/modules/registry.json).
|
||||||
|
Both primitives and modules are registered.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
Each module has a `examples/` directory containing validated consumer
|
||||||
|
contract examples (`simple.yaml` + `complex.yaml` + variation files). The
|
||||||
|
platform-test pipeline validates them against
|
||||||
|
[`schemas/contract.schema.json`](https://github.com/acdl/acdl/blob/main/schemas/contract.schema.json).
|
||||||
|
See each module's `## Examples` section for the excerpts.
|
||||||
|
|
||||||
|
## Versioning
|
||||||
|
|
||||||
|
Primitives and modules use semver: interface → MAJOR, behavior → MINOR,
|
||||||
|
lifecycle → PATCH. A MAJOR bump requires a new registry entry (immutable
|
||||||
|
publication); the old entry enters a 12-month deprecation window. See
|
||||||
|
[Versioning](../pipeline/versioning) for the deploy-pipeline versioning.
|
||||||
|
|
||||||
|
## Module patterns (roadmap)
|
||||||
|
|
||||||
|
The current `composition.json` mechanism is a thin pattern layer. A future
|
||||||
|
redesign will let a consumer dynamically create a module directly from the
|
||||||
|
contract file (an agentic "composition" flow). That is on the roadmap, not
|
||||||
|
implemented today.
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
# Pipeline
|
||||||
|
|
||||||
|
The platform runs two pipelines, both defined by declarative contracts that
|
||||||
|
are the single source of truth for the workflow files.
|
||||||
|
|
||||||
|
## CI pipeline
|
||||||
|
|
||||||
|
The CI pipeline runs on every push and pull request to `main`. It is defined
|
||||||
|
by [`pipelines/ci.yaml`](https://github.com/acdl/acdl/blob/main/pipelines/ci.yaml),
|
||||||
|
validated against
|
||||||
|
[`schemas/pipeline.schema.json`](https://github.com/acdl/acdl/blob/main/schemas/pipeline.schema.json).
|
||||||
|
Both platform-runner workflow files implement the same contract and are
|
||||||
|
byte-identical:
|
||||||
|
|
||||||
|
- `.github/workflows/ci.yml` — GitHub Actions (production)
|
||||||
|
|
||||||
|
Three stages run in sequence:
|
||||||
|
|
||||||
|
1. **lint** — `py_compile` across the platform's Python files.
|
||||||
|
2. **test** — `pytest` across the offline test suite.
|
||||||
|
3. **check-only** — `run_platform.sh --check-only` (offline, no AWS).
|
||||||
|
|
||||||
|
`scripts/run_ci.sh` mirrors the CI pipeline locally so the pipeline is fully
|
||||||
|
reproducible from the shell:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scripts/run_ci.sh # run all 3 stages
|
||||||
|
bash scripts/run_ci.sh --quiet # suppress per-stage banners
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment pipeline
|
||||||
|
|
||||||
|
The deployment pipeline runs when a consumer submits a contract. It is
|
||||||
|
defined by [`pipelines/deploy.yaml`](https://github.com/acdl/acdl/blob/main/pipelines/deploy.yaml),
|
||||||
|
validated against
|
||||||
|
[`schemas/deploy-pipeline.schema.json`](https://github.com/acdl/acdl/blob/main/schemas/deploy-pipeline.schema.json).
|
||||||
|
It is exposed to consumer repos as a **reusable workflow**:
|
||||||
|
|
||||||
|
- `.github/workflows/deploy.yml` — GitHub Actions (production)
|
||||||
|
|
||||||
|
A consumer repo invokes the reusable workflow via a **versioned tag**
|
||||||
|
(floating MAJOR + MINOR, e.g. `acdl/.github/workflows/deploy.yml@v1.6`).
|
||||||
|
The workflow checks out the consumer repo, then checks out the ACDL platform
|
||||||
|
repo into the runner workspace, and runs `scripts/run_platform.sh` against
|
||||||
|
the consumer's contract. The consumer never clones the platform repo or
|
||||||
|
invokes its scripts locally. See the [Consumer Guide](../consumer-guide/)
|
||||||
|
for the end-to-end happy path.
|
||||||
|
|
||||||
|
## Deployment stages
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
S1["validate-contract<br/>schema check"] --> S2
|
||||||
|
S2["resolve-stack<br/>contract -> Target Stack"] --> S3
|
||||||
|
S3["security checks<br/>(adapter)"] --> S4
|
||||||
|
S4["infrastructure plan<br/>(adapter compiles the stack)"] --> S5
|
||||||
|
S5["policy checks<br/>(adapter -> PolicyCheckResult)"] --> S6
|
||||||
|
S6["confidence<br/>score + band"] --> S7
|
||||||
|
S7["evidence event<br/>to the audit outbox"] --> S8
|
||||||
|
S8["infrastructure apply<br/>(dev only)"]
|
||||||
|
```
|
||||||
|
|
||||||
|
1. **validate-contract** — validates the contract YAML against the contract
|
||||||
|
schema. Fails fast on missing fields, unknown modules, or wrong types.
|
||||||
|
2. **resolve-stack** — the contract resolver resolves the contract to a
|
||||||
|
Target Stack instance (loads the module's pattern, expands its children,
|
||||||
|
wires the contract inputs, emits a stack JSON instance).
|
||||||
|
3. **security checks** (adapter) — security checks run on the resolved
|
||||||
|
stack before any infrastructure is planned.
|
||||||
|
4. **infrastructure plan** (adapter) — the substrate adapter compiles the
|
||||||
|
stack to an infrastructure plan.
|
||||||
|
5. **policy checks** (adapter) — policy checks run on the plan. Results are
|
||||||
|
normalized to `PolicyCheckResult` records (severity, rule ID, pass/fail).
|
||||||
|
6. **confidence** — the confidence signal computes a score from 6 inputs
|
||||||
|
(policy, validation, freshness, source, history, NFRs). For `dev`, the
|
||||||
|
threshold is ≥ 0.50. If the band is `pass`, the pipeline proceeds.
|
||||||
|
7. **evidence event** — a hash-chained evidence event is written to the
|
||||||
|
audit outbox.
|
||||||
|
8. **infrastructure apply** (dev only) — the infrastructure plan is applied,
|
||||||
|
creating the resources. An evidence event for the apply is recorded.
|
||||||
|
|
||||||
|
Higher environments hold for human attestation (see
|
||||||
|
[Environments](../environments/)).
|
||||||
|
|
||||||
|
## Output streaming
|
||||||
|
|
||||||
|
`scripts/run_platform.sh` streams output by default so the user can see what
|
||||||
|
the platform is doing:
|
||||||
|
|
||||||
|
- **`--check-only`**: streams the emitted infrastructure file content.
|
||||||
|
- **`--plan-only`** and **full mode**: streams the infrastructure plan output.
|
||||||
|
- **Full mode**: prints policy-check results with severity, rule ID, and
|
||||||
|
pass/fail status.
|
||||||
|
|
||||||
|
A `--quiet` flag suppresses streaming (output to log files only).
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# Versioning
|
||||||
|
|
||||||
|
ACDL uses two versioning schemes: one for modules, one for the deploy
|
||||||
|
pipeline. Both matter to a consumer.
|
||||||
|
|
||||||
|
## Module versioning
|
||||||
|
|
||||||
|
Primitives and modules use **semver** with three triggers:
|
||||||
|
|
||||||
|
- **interface → MAJOR** — a breaking change to the module's inputs/outputs.
|
||||||
|
- **behavior → MINOR** — a backward-compatible behavior change.
|
||||||
|
- **lifecycle → PATCH** — a fix or internal change.
|
||||||
|
|
||||||
|
A MAJOR bump requires a **new registry entry** (immutable publication); the
|
||||||
|
old entry enters a **12-month deprecation window**. A module pins its
|
||||||
|
primitives by `name@semver`; the resolver picks the highest compatible.
|
||||||
|
|
||||||
|
Module versions are tracked in
|
||||||
|
[`registry.json`](https://github.com/acdl/acdl/blob/main/modules/registry.json).
|
||||||
|
|
||||||
|
## Deploy-pipeline versioning (the `uses:` tag)
|
||||||
|
|
||||||
|
The central deploy pipeline is referenced by a **floating MAJOR + MINOR
|
||||||
|
tag** in a consumer's contract and CI definition:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||||
|
```
|
||||||
|
|
||||||
|
Version constraints cannot be expressed inside the contract, so the tag in
|
||||||
|
`uses:` is the only immutability lever a consumer has.
|
||||||
|
|
||||||
|
**Unversioned references are discouraged.** Do not use `@main` or a bare
|
||||||
|
`acdl/pipelines/deploy.yaml` — `main` is constantly updated and can cause
|
||||||
|
unexpected failures. Pinning to a MAJOR+MINOR tag means:
|
||||||
|
|
||||||
|
- **Immutability** — the pipeline behavior you tested is the behavior you
|
||||||
|
get. Patch fixes flow within the tag; breaking changes land under the
|
||||||
|
next MINOR tag (`@v1.5`), which you opt into explicitly.
|
||||||
|
- **Resilience** — your deployment does not break because an unrelated
|
||||||
|
change landed on `main`.
|
||||||
|
- **Reproducibility** — your setup is stable. You upgrade on your schedule
|
||||||
|
by bumping the tag.
|
||||||
|
|
||||||
|
## When a new tag is released
|
||||||
|
|
||||||
|
When a new MINOR tag is released (e.g. `@v1.5`), review its changelog and
|
||||||
|
bump your `uses:` reference when ready. The old tag continues to receive
|
||||||
|
patch fixes until the next MINOR tag.
|
||||||
|
|
||||||
|
## Production-bound references
|
||||||
|
|
||||||
|
For production-bound workflows, the platform resolves the current tag to its
|
||||||
|
SHA (tag for dev/qa, SHA for prod). This prevents a silent patch from
|
||||||
|
changing a production deployment. The platform provides a CLI command for
|
||||||
|
the tag → SHA resolution.
|
||||||
+1
-1
@@ -68,4 +68,4 @@ The vision is realized when:
|
|||||||
## What this vision is, and what it isn't
|
## What this vision is, and what it isn't
|
||||||
|
|
||||||
* **It is:** A principles document. The North Star, the tenets, the strategic bets, the anti-goals. It's intended to be the page that orients a new team, a new stakeholder, or a new architectural decision. It should not need to be rewritten when a tool changes.
|
* **It is:** A principles document. The North Star, the tenets, the strategic bets, the anti-goals. It's intended to be the page that orients a new team, a new stakeholder, or a new architectural decision. It should not need to be rewritten when a tool changes.
|
||||||
* **It isn't:** An architecture. The four-layer model (L1 Terraform primitives, L2 composed stacks, L3A developer surface, L3B agentic surface), the central pipeline template model, the schema location, the dual HITL mechanics, the enterprise evidence stream integration — all of that belongs in the architecture document, where it can be specific and evolve independently.
|
* **It isn't:** An architecture. The four-layer model (primitives, modules, developer surface, agentic surface), the central pipeline template model, the schema location, the dual HITL mechanics, the enterprise evidence stream integration — all of that belongs in the architecture document, where it can be specific and evolve independently.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# <module-name> — <plain-language description>
|
# <module-name> — <plain-language description>
|
||||||
|
|
||||||
> **Module kind:** L1 primitive | **Version:** 1.0.0
|
> **Module kind:** primitive | **Version:** 1.0.0
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
@@ -28,6 +28,18 @@ Terraform resources this module creates:
|
|||||||
|------|------|-------------|
|
|------|------|-------------|
|
||||||
| `<name>` | string | description |
|
| `<name>` | string | description |
|
||||||
|
|
||||||
|
## NFRs
|
||||||
|
|
||||||
|
Non-functional requirements declared by the module's interface. Every
|
||||||
|
L1 primitive MUST declare `deletion_protection` and `encryption_enabled`
|
||||||
|
(both boolean, default `true`); they are mandatory NFRs for every L1.
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
|------|------|---------|-------------|
|
||||||
|
| `deletion_protection` | boolean | true | Prevent resource destruction via Terraform lifecycle prevent_destroy. |
|
||||||
|
| `encryption_enabled` | boolean | true | Enable encryption (at rest or in transit, as applicable). |
|
||||||
|
| `<name>` | <type> | <default> | description |
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
+27
-15
@@ -8,19 +8,19 @@ self-documented with a `README.md` following the
|
|||||||
|
|
||||||
There are two kinds of module:
|
There are two kinds of module:
|
||||||
|
|
||||||
- **L1 primitives** — a single cloud resource or a small group of
|
- **Primitives** — a single cloud resource or a small group of
|
||||||
related resources (e.g. a VPC with subnets and routing). Each L1 has
|
related resources (e.g. a VPC with subnets and routing). Each primitive
|
||||||
an `interface.json` declaring its inputs and outputs, and a `README.md`
|
has an `interface.json` declaring its inputs and outputs, and a
|
||||||
in plain language.
|
`README.md` in plain language.
|
||||||
- **L2 compositions** — a composition that references multiple L1s to
|
- **Modules** — a pattern that references multiple primitives to
|
||||||
deploy a complete stack (e.g. an ECS Fargate microservice). Each L2
|
deploy a complete stack (e.g. an ECS Fargate microservice). Each module
|
||||||
has a `composition.json` declaring its children and wires.
|
has a `composition.json` declaring its children and wires.
|
||||||
|
|
||||||
The Terraform adapter (`adapters/terraform/adapter.py`) compiles a
|
The substrate adapter (`adapters/terraform/adapter.py`) compiles a
|
||||||
module instance to Terraform. Each module's README documents which
|
module instance to infrastructure. Each module's README documents which
|
||||||
Terraform resources it creates.
|
resources it creates.
|
||||||
|
|
||||||
## L1 primitives
|
## Primitives
|
||||||
|
|
||||||
| Module | What it creates | README |
|
| Module | What it creates | README |
|
||||||
|--------|----------------|--------|
|
|--------|----------------|--------|
|
||||||
@@ -31,20 +31,32 @@ Terraform resources it creates.
|
|||||||
| `iam-role` | `aws_iam_role` — IAM role with assume-role policy | [README](l1/iam-role/README.md) |
|
| `iam-role` | `aws_iam_role` — IAM role with assume-role policy | [README](l1/iam-role/README.md) |
|
||||||
| `alb` | `aws_lb` + `aws_lb_target_group` + `aws_lb_listener` — Application Load Balancer | [README](l1/alb/README.md) |
|
| `alb` | `aws_lb` + `aws_lb_target_group` + `aws_lb_listener` — Application Load Balancer | [README](l1/alb/README.md) |
|
||||||
| `ecr` | `aws_ecr_repository` — ECR container image repository | [README](l1/ecr/README.md) |
|
| `ecr` | `aws_ecr_repository` — ECR container image repository | [README](l1/ecr/README.md) |
|
||||||
|
| `cloudfront` | `aws_cloudfront_distribution` + `aws_cloudfront_origin_access_control` — CloudFront distribution with S3 origin via OAC | [README](l1/cloudfront/README.md) |
|
||||||
|
| `waf` | `aws_wafv2_web_acl` — WAFv2 Web ACL (CloudFront-scoped) | [README](l1/waf/README.md) |
|
||||||
|
| `rds` | `aws_db_instance` — Relational database (PostgreSQL, MySQL, etc.) with multi-engine support | [README](l1/rds/README.md) |
|
||||||
|
| `kms-key` | `aws_kms_key` — Customer-managed KMS key with rotation enabled (per-stack CMK) | [README](l1/kms-key/README.md) |
|
||||||
|
| `uptime` | `aws_ecs_service` — Uptime-kuma monitoring on ECS Fargate with alert channels | [README](l1/uptime/README.md) |
|
||||||
|
|
||||||
## L2 compositions
|
## Modules
|
||||||
|
|
||||||
| Module | What it references | README |
|
| Module | What it references | README |
|
||||||
|--------|--------------------|--------|
|
|--------|--------------------|--------|
|
||||||
| `microservice` | 6 L1s (vpc, cluster, ecr, iam-role, alb, ecs-service) | [README](l2/microservice/README.md) |
|
| `microservice` | 6 primitives (vpc, cluster, ecr, iam-role, alb, ecs-service) | [README](l2/microservice/README.md) |
|
||||||
| `static-asset` | 1 L1 (s3) | [README](l2/static-asset/README.md) |
|
| `static-assets` | 3 primitives (s3, cloudfront, waf) | [README](l2/static-assets/README.md) |
|
||||||
|
|
||||||
## Registry
|
## Registry
|
||||||
|
|
||||||
Module versions are tracked in `registry.json`. Both L1 and L2 entries
|
Module versions are tracked in `registry.json`. Both primitives and
|
||||||
are registered.
|
modules are registered.
|
||||||
|
|
||||||
## Template
|
## Template
|
||||||
|
|
||||||
New modules should use [README-TEMPLATE.md](README-TEMPLATE.md) as
|
New modules should use [README-TEMPLATE.md](README-TEMPLATE.md) as
|
||||||
their starting point.
|
their starting point.
|
||||||
|
|
||||||
|
## Module patterns (roadmap)
|
||||||
|
|
||||||
|
The current `composition.json` mechanism is a thin pattern layer. A future
|
||||||
|
redesign will let a consumer dynamically create a module directly from the
|
||||||
|
contract file (an agentic "composition" flow). That is on the roadmap, not
|
||||||
|
implemented today.
|
||||||
@@ -0,0 +1,588 @@
|
|||||||
|
# ACDL Module Engineering Standards
|
||||||
|
|
||||||
|
Standards for authoring and reviewing ACDL modules. These standards
|
||||||
|
govern the two module tiers — **L1 primitives** (single cloud resource
|
||||||
|
or small group of related resources) and **L2 modules** (compositions
|
||||||
|
that reference L1 primitives to deploy a complete stack) — and the
|
||||||
|
substrate adapter that compiles them to Terraform. They are written for
|
||||||
|
**platform engineers** and **AI agents** that author or review new
|
||||||
|
modules against the existing corpus (12 L1 primitives and 2 L2 modules
|
||||||
|
shipped in v1.8).
|
||||||
|
|
||||||
|
A module that fails any section below is not ready to publish.
|
||||||
|
|
||||||
|
## 1. Overview
|
||||||
|
|
||||||
|
These standards codify the conventions already established by the
|
||||||
|
shipped modules (`s3`, `vpc`, `ecs-cluster`, `ecs-service`, `iam-role`,
|
||||||
|
`alb`, `ecr`, `cloudfront`, `waf`, `rds`, `kms-key`, `uptime`; the L2
|
||||||
|
modules `static-assets` and `microservice`). They exist so that:
|
||||||
|
|
||||||
|
- platform engineers can review a new module against a fixed checklist;
|
||||||
|
- AI agents authoring modules produce code that passes review without
|
||||||
|
iteration; and
|
||||||
|
- the substrate adapter (`adapters/terraform/adapter.py`) can compile a
|
||||||
|
module instance with no module-specific code in the adapter beyond the
|
||||||
|
three tables in §8.
|
||||||
|
|
||||||
|
When this document and an existing module disagree, the existing module
|
||||||
|
is the authority for v1.x. A change to this document is a MINOR version
|
||||||
|
bump of the standards; a change that breaks shipped modules is a MAJOR
|
||||||
|
bump and requires a migration plan.
|
||||||
|
|
||||||
|
## 2. L1 Primitive Standards
|
||||||
|
|
||||||
|
An L1 primitive is a single cloud resource or a small group of related
|
||||||
|
resources (e.g. a VPC with subnets and a route table). It is declared by
|
||||||
|
an `interface.json` and realized by the substrate adapter; it does not
|
||||||
|
own Terraform code.
|
||||||
|
|
||||||
|
### 2.1 Required files
|
||||||
|
|
||||||
|
Every L1 primitive MUST contain, at minimum:
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `interface.json` | Substrate-agnostic declaration: inputs, outputs, NFRs, optional multi-resource graph. |
|
||||||
|
| `instance.json` | A concrete instance used as the adapter regression baseline. |
|
||||||
|
| `README.md` | Plain-language documentation following `README-TEMPLATE.md` (see §7). |
|
||||||
|
| `examples/simple.yaml` | A minimal contract that uses the primitive with required inputs only. |
|
||||||
|
| `examples/complex.yaml` | A contract that exercises optional inputs, NFRs, and (if applicable) the multi-resource graph. |
|
||||||
|
|
||||||
|
Directory layout:
|
||||||
|
|
||||||
|
```
|
||||||
|
modules/l1/<name>/
|
||||||
|
interface.json
|
||||||
|
instance.json
|
||||||
|
README.md
|
||||||
|
examples/
|
||||||
|
simple.yaml
|
||||||
|
complex.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 interface.json schema
|
||||||
|
|
||||||
|
`interface.json` MUST be a JSON object with the following required
|
||||||
|
fields:
|
||||||
|
|
||||||
|
| Field | Type | Constraint |
|
||||||
|
|-------|------|------------|
|
||||||
|
| `name` | string | `^[a-z][a-z0-9-]*$`; MUST match the module folder name. |
|
||||||
|
| `version` | string | Semver (`^\d+\d+\.\d+$`); MUST match the registry entry semver. |
|
||||||
|
| `kind` | string | Literal `"l1"`. |
|
||||||
|
| `type` | string | Stack type in `aws:<service>:<kind>` format (see §2.7). |
|
||||||
|
| `description` | string | One or two sentences in plain language; no Terraform jargon. |
|
||||||
|
| `inputs` | object | Keyed by input name; each value is an input declaration (§2.3). MAY be empty. |
|
||||||
|
| `outputs` | object | Keyed by output name; each value is an output declaration (§2.4). MAY be empty. |
|
||||||
|
| `nfrs` | object | Keyed by NFR name; each value is an NFR declaration (§2.5). MUST include `deletion_protection` and `encryption_enabled`. |
|
||||||
|
|
||||||
|
Optional fields for multi-resource primitives:
|
||||||
|
|
||||||
|
| Field | Type | Constraint |
|
||||||
|
|-------|------|------------|
|
||||||
|
| `resources` | array | One entry per distinct cloud resource; see §2.6. |
|
||||||
|
| `intra_refs` | array | Internal wiring between resources; see §2.6. |
|
||||||
|
|
||||||
|
A primitive that creates a single resource (e.g. `s3`, `iam-role`,
|
||||||
|
`rds`, `kms-key`) omits `resources` and `intra_refs`; its `type` field
|
||||||
|
is the single resource's stack type. A primitive that creates a small
|
||||||
|
group of related resources (e.g. `vpc`, `alb`, `cloudfront`) declares
|
||||||
|
`resources[]` with one entry per resource and `intra_refs[]` for the
|
||||||
|
internal wiring; its `type` field is the *primary* resource's stack
|
||||||
|
type.
|
||||||
|
|
||||||
|
### 2.3 Input declaration
|
||||||
|
|
||||||
|
Each entry in `inputs` is an object:
|
||||||
|
|
||||||
|
| Field | Type | Required | Notes |
|
||||||
|
|-------|------|----------|-------|
|
||||||
|
| `type` | string | yes | One of: `string`, `number`, `boolean`, `array`, `object`. |
|
||||||
|
| `description` | string | yes | Plain language; no Terraform jargon. |
|
||||||
|
| `required` | boolean | yes | `true` if the consumer MUST supply this input. |
|
||||||
|
| `default` | (any) | no | Present only when `required` is `false`. MUST match the declared `type`. |
|
||||||
|
| `enum` | array | no | Allowed values for `string`/`number` inputs (e.g. RDS `engine`). |
|
||||||
|
|
||||||
|
`region` is a required `string` input on every primitive that creates a
|
||||||
|
regional resource. Global resources (e.g. CloudFront) still declare
|
||||||
|
`region` because the provider region is used for child resources (the
|
||||||
|
OAC in the `cloudfront` case).
|
||||||
|
|
||||||
|
Every primitive that holds at-rest data MUST declare an optional
|
||||||
|
`kms_key_arn` input (`string`, `required: false`); see §4.
|
||||||
|
|
||||||
|
### 2.4 Output declaration
|
||||||
|
|
||||||
|
Each entry in `outputs` is an object:
|
||||||
|
|
||||||
|
| Field | Type | Required | Notes |
|
||||||
|
|-------|------|----------|-------|
|
||||||
|
| `type` | string | yes | `arn` for ARN outputs; `string` for all others. |
|
||||||
|
| `description` | string | yes | Plain language. |
|
||||||
|
|
||||||
|
Use `arn` (not `string`) for any output that returns an AWS ARN — the
|
||||||
|
adapter and policy engine key off the `arn` type to apply ARN-scoped
|
||||||
|
rules.
|
||||||
|
|
||||||
|
### 2.5 NFR declaration
|
||||||
|
|
||||||
|
Each entry in `nfrs` is an object:
|
||||||
|
|
||||||
|
| Field | Type | Required | Notes |
|
||||||
|
|-------|------|----------|-------|
|
||||||
|
| `type` | string | yes | One of: `string`, `number`, `boolean`. |
|
||||||
|
| `description` | string | yes | Plain language. |
|
||||||
|
| `default` | (any) | yes | MUST match the declared `type`. NFRs always have a default. |
|
||||||
|
|
||||||
|
Mandatory NFRs on every L1:
|
||||||
|
|
||||||
|
| NFR | Type | Default | Notes |
|
||||||
|
|-----|------|---------|-------|
|
||||||
|
| `deletion_protection` | boolean | `true` | See §5. |
|
||||||
|
| `encryption_enabled` | boolean | `true` | See §4. |
|
||||||
|
|
||||||
|
A primitive for which an NFR does not conceptually apply (e.g. an IAM
|
||||||
|
role has no at-rest data) still declares it with `default: true` and a
|
||||||
|
description noting the non-applicability, so the standards check and the
|
||||||
|
adapter emit logic stay uniform. The shipped `iam-role` primitive is the
|
||||||
|
reference for this case.
|
||||||
|
|
||||||
|
Additional NFRs are encouraged where they carry operational meaning
|
||||||
|
(e.g. `s3.versioning`, `rds.backup_retention_period`,
|
||||||
|
`kms-key.enable_rotation`, `vpc.flow_logs_encrypted`). Name them in
|
||||||
|
lowercase snake_case.
|
||||||
|
|
||||||
|
### 2.6 Multi-resource pattern
|
||||||
|
|
||||||
|
A primitive that creates more than one cloud resource (e.g. `vpc`
|
||||||
|
creates `aws_vpc` + `aws_subnet` + `aws_route_table`; `alb` creates
|
||||||
|
`aws_lb` + `aws_lb_target_group` + `aws_lb_listener`; `cloudfront`
|
||||||
|
creates `aws_cloudfront_distribution` +
|
||||||
|
`aws_cloudfront_origin_access_control`) declares a `resources` array.
|
||||||
|
|
||||||
|
Each `resources[]` entry:
|
||||||
|
|
||||||
|
| Field | Type | Notes |
|
||||||
|
|-------|------|-------|
|
||||||
|
| `type` | string | The resource's stack type (`aws:<service>:<kind>`). |
|
||||||
|
| `description` | string | Plain language. |
|
||||||
|
| `inputs` | array | Names (strings) of inputs from the top-level `inputs` object that this resource consumes. |
|
||||||
|
| `outputs` | array | Names (strings) of outputs from the top-level `outputs` object that this resource produces. |
|
||||||
|
|
||||||
|
The top-level `inputs`/`outputs` objects remain the single source of
|
||||||
|
truth; `resources[].inputs` and `resources[].outputs` are arrays of
|
||||||
|
*names* referencing those objects, not re-declarations.
|
||||||
|
|
||||||
|
`intra_refs[]` wires outputs of one resource to inputs of another
|
||||||
|
within the same primitive. Each entry:
|
||||||
|
|
||||||
|
| Field | Type | Notes |
|
||||||
|
|-------|------|-------|
|
||||||
|
| `from` | string | `<resource-type>.<output-name>` — the producing side. |
|
||||||
|
| `to` | string | `<resource-type>.<input-name>` — the consuming side. |
|
||||||
|
|
||||||
|
Reference: `cloudfront/interface.json` declares an intra-ref from
|
||||||
|
`aws:cloudfront:distribution.oac_id` to
|
||||||
|
`aws:cloudfront:originaccesscontrol.oac_id`; `vpc/interface.json`
|
||||||
|
declares intra-refs from the subnet and route table to the VPC's
|
||||||
|
`vpc_id`.
|
||||||
|
|
||||||
|
### 2.7 Naming and stack types
|
||||||
|
|
||||||
|
- Module folder names and `interface.json` `name` values MUST match
|
||||||
|
`^[a-z][a-z0-9-]*$` (lowercase, hyphenated, leading letter). Examples:
|
||||||
|
`s3`, `ecs-cluster`, `kms-key`, `iam-role`, `uptime`.
|
||||||
|
- Input and output names are lowercase snake_case.
|
||||||
|
- Stack types follow `aws:<service>:<kind>`:
|
||||||
|
- `aws:s3:bucket`
|
||||||
|
- `aws:ec2:vpc`, `aws:ec2:subnet`, `aws:ec2:routetable`
|
||||||
|
- `aws:ecs:cluster`, `aws:ecs:task_definition`, `aws:ecs:service`,
|
||||||
|
`aws:ecs:uptime-service`
|
||||||
|
- `aws:iam:role`
|
||||||
|
- `aws:elbv2:loadbalancer`, `aws:elbv2:listener`,
|
||||||
|
`aws:elbv2:targetgroup`
|
||||||
|
- `aws:ecr:repository`
|
||||||
|
- `aws:cloudfront:distribution`, `aws:cloudfront:originaccesscontrol`
|
||||||
|
- `aws:wafv2:webacl`
|
||||||
|
- `aws:rds:instance`
|
||||||
|
- `aws:kms:key`, `aws:kms:alias`
|
||||||
|
- The substrate adapter's `TYPE_MAP` is the registry of stack types the
|
||||||
|
adapter can compile (see §8). A new stack type requires a `TYPE_MAP`
|
||||||
|
entry before the primitive can be deployed.
|
||||||
|
|
||||||
|
## 3. L2 Module Standards
|
||||||
|
|
||||||
|
An L2 module is a composition that references one or more L1 primitives
|
||||||
|
to deploy a complete stack (e.g. an ECS Fargate microservice, a static
|
||||||
|
asset site behind CloudFront + WAF). It is declared by a
|
||||||
|
`composition.json`; it does not own Terraform code and does not have an
|
||||||
|
`instance.json`.
|
||||||
|
|
||||||
|
### 3.1 Required files
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `composition.json` | The composition tree: children, wires, outputs, optional features. |
|
||||||
|
| `README.md` | Plain-language documentation following `README-TEMPLATE.md` (see §7). |
|
||||||
|
| `examples/simple.yaml` | A minimal contract that uses the module with required inputs only. |
|
||||||
|
| `examples/complex.yaml` | A contract that exercises optional inputs and feature flags. |
|
||||||
|
|
||||||
|
Directory layout:
|
||||||
|
|
||||||
|
```
|
||||||
|
modules/l2/<name>/
|
||||||
|
composition.json
|
||||||
|
README.md
|
||||||
|
examples/
|
||||||
|
simple.yaml
|
||||||
|
complex.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
There is no `instance.json` for an L2 module — the L2 is deployed by
|
||||||
|
resolving the composition tree to L1 instances at compile time, not by
|
||||||
|
loading a pre-baked instance.
|
||||||
|
|
||||||
|
### 3.2 composition.json schema
|
||||||
|
|
||||||
|
`composition.json` MUST be a JSON object with the following fields:
|
||||||
|
|
||||||
|
| Field | Type | Required | Notes |
|
||||||
|
|-------|------|----------|-------|
|
||||||
|
| `name` | string | yes | `^[a-z][a-z0-9-]*$`; matches the module folder name. |
|
||||||
|
| `version` | string | yes | Semver; matches the registry entry. |
|
||||||
|
| `kind` | string | yes | Literal `"l2"`. |
|
||||||
|
| `depth` | integer | yes | Literal `1` in v1 (see §3.5). |
|
||||||
|
| `description` | string | yes | Plain language. |
|
||||||
|
| `children` | array | yes | One entry per referenced L1 module (§3.3). |
|
||||||
|
| `wires` | array | yes | Wires from contract inputs / child outputs to child inputs / stack outputs (§3.4). |
|
||||||
|
| `outputs` | array | yes | Wires from child outputs to stack outputs (§3.4). |
|
||||||
|
| `features` | object | no | Feature flags propagated to children by the resolver (§3.6). |
|
||||||
|
|
||||||
|
### 3.3 Children
|
||||||
|
|
||||||
|
Each `children[]` entry:
|
||||||
|
|
||||||
|
| Field | Type | Notes |
|
||||||
|
|-------|------|-------|
|
||||||
|
| `id` | string | The child id, unique within the composition. `^[a-z][a-z0-9-]*$`. The id is the local name used in wires (e.g. `vpc`, `cluster`, `kms`). |
|
||||||
|
| `module` | string | `<name>@<semver>` referencing a registered L1 module. |
|
||||||
|
|
||||||
|
Children MUST reference L1 modules registered in `registry.json` (see
|
||||||
|
§6). The referenced semver MUST exist in the registry. An L2 MUST NOT
|
||||||
|
reference another L2 (no L3 in v1; see §3.5).
|
||||||
|
|
||||||
|
Reference: `microservice/composition.json` declares seven children
|
||||||
|
(`vpc`, `cluster`, `ecr`, `roles`, `alb`, `service`, `kms`), each
|
||||||
|
referencing an L1 at `@1.0.0`.
|
||||||
|
|
||||||
|
### 3.4 Wire format
|
||||||
|
|
||||||
|
A wire is a JSON object `{"from": "<source>", "to": "<target>"}` with an
|
||||||
|
optional `default` field for contract-input wires.
|
||||||
|
|
||||||
|
Sources (the `from` side):
|
||||||
|
|
||||||
|
| Source form | Meaning |
|
||||||
|
|-------------|---------|
|
||||||
|
| `contract.inputs.<name>` | A value supplied by the consumer's contract YAML. |
|
||||||
|
| `<childId>.outputs.<name>` | An output produced by a child L1 module. |
|
||||||
|
|
||||||
|
Targets (the `to` side):
|
||||||
|
|
||||||
|
| Target form | Meaning |
|
||||||
|
|-------------|---------|
|
||||||
|
| `<childId>.inputs.<name>` | An input on a child L1 module. |
|
||||||
|
| `stack.outputs.<name>` | A value the L2 exposes as a stack output. |
|
||||||
|
|
||||||
|
Wires that source from `contract.inputs.<name>` MAY carry a `default`
|
||||||
|
value used when the consumer omits the input. Reference:
|
||||||
|
`microservice/composition.json` wires `contract.inputs.bucket_name` to
|
||||||
|
`vpc.inputs.cidr` with `default: "10.0.0.0/16"` (a historical quirk
|
||||||
|
preserved for regression).
|
||||||
|
|
||||||
|
The `outputs[]` array uses the same wire shape but its `to` is always
|
||||||
|
`stack.outputs.<name>` and its `from` is always
|
||||||
|
`<childId>.outputs.<name>`.
|
||||||
|
|
||||||
|
### 3.5 Maximum depth
|
||||||
|
|
||||||
|
`depth` is `1` for every L2 in v1. The composition tree is strictly L2
|
||||||
|
→ L1: an L2 may reference only L1 primitives, never another L2. There
|
||||||
|
is no L3 in v1. The stack schema permits `depth` up to 5 for forward
|
||||||
|
compatibility, but the v1 resolver and adapter only handle depth 1.
|
||||||
|
|
||||||
|
### 3.6 Feature flags
|
||||||
|
|
||||||
|
An L2 MAY declare a `features` object. Two flags are defined in v1:
|
||||||
|
|
||||||
|
| Flag | Type | Default | Effect |
|
||||||
|
|------|------|---------|--------|
|
||||||
|
| `deletion_protection` | boolean | `true` | When `true`, the resolver propagates `deletion_protection: true` to every child's NFRs. When `false`, children are deployed with `deletion_protection: false` (used by decommission; see §5). |
|
||||||
|
| `uptime_enabled` | boolean | `true` | When `true`, the uptime monitoring L1 is deployed after the L2 module in a separate terraform state. When `false`, the uptime deployment is skipped. |
|
||||||
|
|
||||||
|
Feature flags are propagated to children by the resolver; the L2
|
||||||
|
`composition.json` does not need to wire them explicitly as inputs. The
|
||||||
|
resolver reads `features` and injects the corresponding NFR/input on
|
||||||
|
each child.
|
||||||
|
|
||||||
|
## 4. Encryption by Default
|
||||||
|
|
||||||
|
Encryption is mandatory and on by default across the platform.
|
||||||
|
|
||||||
|
1. Every L1 MUST declare an `encryption_enabled` NFR (boolean, default
|
||||||
|
`true`) in `interface.json`. See §2.5.
|
||||||
|
2. Every L1 that holds at-rest data (S3, RDS, ECR, ECS task
|
||||||
|
definition env, VPC flow logs, CloudWatch log groups) MUST declare an
|
||||||
|
optional `kms_key_arn` input (`string`, `required: false`). When
|
||||||
|
supplied, the adapter wires it to the resource's KMS encryption
|
||||||
|
argument.
|
||||||
|
3. L2 modules MUST wire a per-stack customer-managed KMS key to all
|
||||||
|
children that accept `kms_key_arn`. The KMS key is a `kms-key` child
|
||||||
|
of the L2 — one key per L2 deployment, no shared keys. Reference:
|
||||||
|
both `static-assets` and `microservice` declare a `kms` child
|
||||||
|
(`kms-key@1.0.0`) and wire `kms.outputs.kms_key_arn` to every child
|
||||||
|
that accepts a CMK.
|
||||||
|
4. For a standalone L1 deployment (an L1 used outside an L2), if the
|
||||||
|
consumer does not supply `kms_key_arn`, the adapter falls back to the
|
||||||
|
AWS-managed default key for that service and emits a warning to
|
||||||
|
stderr. The primitive is still encrypted; only the key manager
|
||||||
|
differs.
|
||||||
|
5. The `kms-key` primitive enables key rotation by default
|
||||||
|
(`enable_rotation` NFR, default `true`), and the adapter emits
|
||||||
|
`enable_key_rotation = true` on the `aws_kms_key` resource.
|
||||||
|
|
||||||
|
A primitive that does not hold at-rest data (e.g. `iam-role`,
|
||||||
|
`ecs-cluster`, `alb`) still declares `encryption_enabled` for standards
|
||||||
|
uniformity (see §2.5) but does not declare `kms_key_arn`.
|
||||||
|
|
||||||
|
## 5. Deletion Protection by Default
|
||||||
|
|
||||||
|
Deletion protection is mandatory and on by default to prevent
|
||||||
|
accidental teardown of production infrastructure.
|
||||||
|
|
||||||
|
1. Every L1 MUST declare a `deletion_protection` NFR (boolean, default
|
||||||
|
`true`) in `interface.json`. See §2.5.
|
||||||
|
2. When `deletion_protection` is `true`, the substrate adapter emits a
|
||||||
|
`lifecycle { prevent_destroy = true }` block on the corresponding
|
||||||
|
Terraform resource. A `terraform destroy` against a protected
|
||||||
|
resource fails with an error naming the resource.
|
||||||
|
3. L2 modules expose `features.deletion_protection` (default `true`).
|
||||||
|
The resolver propagates the flag to every child's NFRs (see §3.6).
|
||||||
|
4. **Decommission mode.** To tear down a stack that was deployed with
|
||||||
|
deletion protection, the consumer sets `inputs.deletion_protection:
|
||||||
|
false` on the contract (or `features.deletion_protection: false` on
|
||||||
|
an L2) and re-applies. The decommission transform
|
||||||
|
(`decommission_transform`) zeroes capacity counts (e.g. ECS desired
|
||||||
|
count to 0, RDS allocated storage to the minimum) so that the
|
||||||
|
subsequent `destroy` applies against a quiesced stack. The transform
|
||||||
|
is applied by the resolver before the adapter emits resources.
|
||||||
|
|
||||||
|
## 6. Registry
|
||||||
|
|
||||||
|
Every module — L1 and L2 — MUST be registered in
|
||||||
|
`modules/registry.json` at its semver. The registry is the source of
|
||||||
|
truth for what is published; the adapter and resolver refuse to compile
|
||||||
|
a module that is not registered.
|
||||||
|
|
||||||
|
Registry entry shape:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"<module-name>": {
|
||||||
|
"<semver>": {
|
||||||
|
"interface": "modules/<l1|l2>/<module-name>/<interface.json|composition.json>",
|
||||||
|
"published_at": "<ISO 8601 timestamp>",
|
||||||
|
"deprecated": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `interface` is the path (relative to the repo root) to the module's
|
||||||
|
interface file — `interface.json` for an L1, `composition.json` for
|
||||||
|
an L2.
|
||||||
|
- `published_at` is an ISO 8601 timestamp. Use a full
|
||||||
|
`YYYY-MM-DDTHH:MM:SSZ` form; do not omit the seconds or the timezone
|
||||||
|
designator.
|
||||||
|
- `deprecated` is `false` for a live module. A MAJOR version bump does
|
||||||
|
not delete the old entry; it flips `deprecated` to `true` and starts a
|
||||||
|
12-month deprecation window (see §7 Versioning).
|
||||||
|
|
||||||
|
A new semver of an existing module is a new key under the module's
|
||||||
|
object; old semvers are retained. The registry is append-only for
|
||||||
|
published semvers — a published semver is never edited or deleted.
|
||||||
|
|
||||||
|
## 7. README Standards
|
||||||
|
|
||||||
|
Every module README MUST follow the structure of
|
||||||
|
`modules/README-TEMPLATE.md`. Required sections, in order:
|
||||||
|
|
||||||
|
1. `# <name> — <plain-language description>` — title with the module
|
||||||
|
name and a one-line description.
|
||||||
|
2. `## Overview` — one or two sentences in plain language.
|
||||||
|
3. `## Resources` — a table of the Terraform resources the module
|
||||||
|
creates (L1) or the primitives it references (L2).
|
||||||
|
4. `## Inputs` — a table: `| Name | Type | Required | Default | Description |`.
|
||||||
|
5. `## Outputs` — a table: `| Name | Type | Description |`.
|
||||||
|
6. `## NFRs` — a table: `| Name | Type | Default | Description |`.
|
||||||
|
`deletion_protection` and `encryption_enabled` are mandatory NFRs
|
||||||
|
for every L1; they MUST appear in this table.
|
||||||
|
7. `## Usage` — a concrete snippet showing how a consumer references
|
||||||
|
the module in a contract.
|
||||||
|
8. `## Compliance extension points` — resources or behaviors that could
|
||||||
|
be added for the future compliance milestone (GDPR, SOX, SOC2, HIPAA,
|
||||||
|
DORA). Not implemented yet; listed so the redesign can plan for them.
|
||||||
|
9. `## Examples` — links to `examples/simple.yaml` and
|
||||||
|
`examples/complex.yaml` with a one-line description of each.
|
||||||
|
10. `## Versioning` — the module's semver policy: interface MAJOR,
|
||||||
|
behavior MINOR, lifecycle PATCH. MAJOR bumps require a new
|
||||||
|
`registry.json` entry (immutable publication); old entries enter a
|
||||||
|
12-month deprecation window.
|
||||||
|
|
||||||
|
An L2 README's `## Resources` section lists the referenced L1 children
|
||||||
|
rather than Terraform resources, and its `## Inputs`/`## Outputs`
|
||||||
|
sections reflect the contract inputs and stack outputs of the
|
||||||
|
composition.
|
||||||
|
|
||||||
|
## 8. Adapter Extension Pattern
|
||||||
|
|
||||||
|
The Terraform adapter (`adapters/terraform/adapter.py`) is a thin
|
||||||
|
translator. It owns no module content; it only maps stack types and
|
||||||
|
names to Terraform types and arguments via three tables and, for
|
||||||
|
complex resources, a specialized emit branch.
|
||||||
|
|
||||||
|
### 8.1 The three tables
|
||||||
|
|
||||||
|
| Table | Purpose | Keys | Values |
|
||||||
|
|-------|---------|------|--------|
|
||||||
|
| `TYPE_MAP` | Stack type → Terraform resource type. | Stack type string (`aws:<service>:<kind>`). | Terraform resource type (`aws_s3_bucket`, `aws_db_instance`, etc.). |
|
||||||
|
| `INPUT_MAP` | Stack input name → Terraform argument name, per stack type. Only non-identity mappings are listed; an input not present uses the stack name as the Terraform arg (identity). | Stack type. | Object mapping input name → Terraform arg name. |
|
||||||
|
| `OUTPUT_MAP` | Stack output name → Terraform attribute name, per stack type. Only non-identity mappings are listed. | Stack type. | Object mapping output name → Terraform attribute name. |
|
||||||
|
|
||||||
|
Reference: `adapter.py:26` (`TYPE_MAP`), `adapter.py:51` (`INPUT_MAP`),
|
||||||
|
`adapter.py:75` (`OUTPUT_MAP`).
|
||||||
|
|
||||||
|
### 8.2 Specialized `_emit_resource` branches
|
||||||
|
|
||||||
|
Most resources emit with the generic loop in `_emit_resource`
|
||||||
|
(`adapter.py:156`): for each input, look up the Terraform arg in
|
||||||
|
`INPUT_MAP`, render the value, append `arg = value`. Resources with
|
||||||
|
nested HCL blocks need a specialized branch. The shipped examples:
|
||||||
|
|
||||||
|
- `aws:ecs:service` emits a `load_balancer {}` block from the
|
||||||
|
`lb_target_group_arn` input.
|
||||||
|
- `aws:elbv2:loadbalancer` wraps `subnets` and `security_group` in list
|
||||||
|
brackets.
|
||||||
|
- `aws:cloudfront:distribution` emits nested `origin {}`,
|
||||||
|
`default_cache_behavior {}`, and
|
||||||
|
`server_side_encryption_configuration {}` blocks.
|
||||||
|
- `aws:wafv2:webacl` emits nested `rules {}` blocks.
|
||||||
|
- `aws:ecs:task_definition` emits a `container_definitions` jsonencode
|
||||||
|
block from `image`/`port`/`env`.
|
||||||
|
|
||||||
|
A specialized branch lives inside `_emit_resource` and is keyed on the
|
||||||
|
stack type. It reads the input value, renders the nested block, and
|
||||||
|
appends the lines to `body`.
|
||||||
|
|
||||||
|
### 8.3 Adding a new L1 to the adapter
|
||||||
|
|
||||||
|
When a new L1 primitive is added:
|
||||||
|
|
||||||
|
1. Add one entry to `TYPE_MAP` for each stack type the primitive
|
||||||
|
declares (single resource → one entry; multi-resource → one entry
|
||||||
|
per resource in `resources[]`).
|
||||||
|
2. Add one entry to `INPUT_MAP` for each stack type, listing only the
|
||||||
|
inputs whose Terraform arg name differs from the stack input name
|
||||||
|
(identity mappings are omitted).
|
||||||
|
3. Add one entry to `OUTPUT_MAP` for each stack type, listing only the
|
||||||
|
outputs whose Terraform attribute name differs from the stack output
|
||||||
|
name.
|
||||||
|
4. If any resource requires nested HCL blocks, add a specialized branch
|
||||||
|
in `_emit_resource` keyed on that stack type.
|
||||||
|
|
||||||
|
If steps 1–3 are done and no specialized branch is needed, the
|
||||||
|
primitive deploys with no further adapter changes. The L1 content and
|
||||||
|
the contract YAML do not change when the adapter grows.
|
||||||
|
|
||||||
|
## 9. Code Review Checklist
|
||||||
|
|
||||||
|
Use this checklist when reviewing a new module (L1 or L2). Every box
|
||||||
|
must be checked before the module is registered and published.
|
||||||
|
|
||||||
|
### 9.1 Files and structure
|
||||||
|
|
||||||
|
- [ ] All required files present:
|
||||||
|
- L1: `interface.json`, `instance.json`, `README.md`,
|
||||||
|
`examples/simple.yaml`, `examples/complex.yaml`.
|
||||||
|
- L2: `composition.json`, `README.md`, `examples/simple.yaml`,
|
||||||
|
`examples/complex.yaml` (no `instance.json`).
|
||||||
|
- [ ] `interface.json` (L1) / `composition.json` (L2) validates against
|
||||||
|
`schemas/stack.schema.json`.
|
||||||
|
- [ ] `examples/simple.yaml` and `examples/complex.yaml` validate
|
||||||
|
against `schemas/contract.schema.json`.
|
||||||
|
- [ ] Module registered in `modules/registry.json` at its semver with a
|
||||||
|
full ISO 8601 `published_at` and `deprecated: false`.
|
||||||
|
|
||||||
|
### 9.2 Interface (L1)
|
||||||
|
|
||||||
|
- [ ] `name` matches the folder name and `^[a-z][a-z0-9-]*$`.
|
||||||
|
- [ ] `version` is semver and matches the registry entry.
|
||||||
|
- [ ] `kind` is `"l1"`.
|
||||||
|
- [ ] `type` follows `aws:<service>:<kind>`.
|
||||||
|
- [ ] Every input has `type`, `description`, `required`; optional inputs
|
||||||
|
carry a `default` of the correct type; `enum` present where the value
|
||||||
|
set is constrained.
|
||||||
|
- [ ] Every output has `type` (`arn` for ARNs, `string` otherwise) and
|
||||||
|
`description`.
|
||||||
|
- [ ] `nfrs` includes `deletion_protection` (boolean, default `true`)
|
||||||
|
and `encryption_enabled` (boolean, default `true`).
|
||||||
|
- [ ] `kms_key_arn` input present if the primitive holds at-rest data.
|
||||||
|
- [ ] Multi-resource primitives declare `resources[]` (with `inputs`/
|
||||||
|
`outputs` as arrays of names) and `intra_refs[]` with `{from, to}`.
|
||||||
|
|
||||||
|
### 9.3 Composition (L2)
|
||||||
|
|
||||||
|
- [ ] `kind` is `"l2"` and `depth` is `1`.
|
||||||
|
- [ ] Every `children[]` entry is `{id, module}` with `module` in
|
||||||
|
`<name>@<semver>` form referencing a registered L1.
|
||||||
|
- [ ] No child references an L2 (no L3 in v1).
|
||||||
|
- [ ] `wires[]` use the `contract.inputs.<name>` /
|
||||||
|
`<childId>.outputs.<name>` → `<childId>.inputs.<name>` /
|
||||||
|
`stack.outputs.<name>` forms.
|
||||||
|
- [ ] `outputs[]` use `<childId>.outputs.<name>` →
|
||||||
|
`stack.outputs.<name>`.
|
||||||
|
- [ ] A `kms` child (`kms-key@<semver>`) is present and its
|
||||||
|
`kms_key_arn` output is wired to every child that accepts a CMK.
|
||||||
|
- [ ] `features` (if present) only uses defined flags
|
||||||
|
(`deletion_protection`, `uptime_enabled`).
|
||||||
|
|
||||||
|
### 9.4 Adapter
|
||||||
|
|
||||||
|
- [ ] `TYPE_MAP` has an entry for every stack type the new primitive
|
||||||
|
declares.
|
||||||
|
- [ ] `INPUT_MAP` and `OUTPUT_MAP` have entries for every stack type,
|
||||||
|
listing only non-identity mappings.
|
||||||
|
- [ ] A specialized `_emit_resource` branch is added for any resource
|
||||||
|
that needs nested HCL blocks.
|
||||||
|
- [ ] The new primitive's `instance.json` round-trips through the
|
||||||
|
adapter without error (regression baseline).
|
||||||
|
|
||||||
|
### 9.5 README and docs
|
||||||
|
|
||||||
|
- [ ] README follows `README-TEMPLATE.md` with all required sections in
|
||||||
|
order (§7).
|
||||||
|
- [ ] `## NFRs` table lists `deletion_protection` and
|
||||||
|
`encryption_enabled` for an L1.
|
||||||
|
- [ ] `## Compliance extension points` lists at least one plausible
|
||||||
|
future extension.
|
||||||
|
|
||||||
|
### 9.6 Tests
|
||||||
|
|
||||||
|
- [ ] A test is added for the new primitive covering adapter emission
|
||||||
|
(the Terraform output for `instance.json` matches the expected
|
||||||
|
fixture) and interface validation (`interface.json` validates against
|
||||||
|
`stack.schema.json`).
|
||||||
|
- [ ] For an L2, a test is added that the composition resolves to the
|
||||||
|
expected set of L1 instances and that the adapter emits a root module
|
||||||
|
calling the L1 modules.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# alb — Application Load Balancer (load balancer + target group + listener)
|
# alb — Application Load Balancer (load balancer + target group + listener)
|
||||||
|
|
||||||
> **Module kind:** L1 primitive | **Version:** 1.0.0
|
> **Module kind:** primitive | **Version:** 1.0.0
|
||||||
|
|
||||||
An Application Load Balancer with a target group and a listener. This is
|
An Application Load Balancer with a target group and a listener. This is
|
||||||
a multi-resource module: it creates a load balancer, a target group, and
|
a multi-resource module: it creates a load balancer, a target group, and
|
||||||
@@ -64,6 +64,48 @@ The `target_group_arn` output is referenced by `ecs-service` as its
|
|||||||
- **WAF** — add `aws_wafv2_web_acl_association` for application-layer protection (SOC2 CC7.6, PCI-DSS 6.5, DORA ICT risk).
|
- **WAF** — add `aws_wafv2_web_acl_association` for application-layer protection (SOC2 CC7.6, PCI-DSS 6.5, DORA ICT risk).
|
||||||
- **Deregistration delay** — add `deregistration_delay` for graceful draining (SOC2 CC9.1 resilience).
|
- **Deregistration delay** — add `deregistration_delay` for graceful draining (SOC2 CC9.1 resilience).
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
Validated example contracts are in [`examples/`](examples/). The platform-test
|
||||||
|
pipeline validates them against `schemas/contract.schema.json`.
|
||||||
|
|
||||||
|
### Simple
|
||||||
|
|
||||||
|
A minimal deployment:
|
||||||
|
|
||||||
|
[`examples/simple.yaml`](examples/simple.yaml)
|
||||||
|
```yaml
|
||||||
|
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||||
|
module: alb
|
||||||
|
environment: dev
|
||||||
|
inputs:
|
||||||
|
name: my-alb
|
||||||
|
subnets: subnet-aaa,subnet-bbb
|
||||||
|
security_group: sg-xxx
|
||||||
|
port: 80
|
||||||
|
protocol: HTTP
|
||||||
|
region: us-east-1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Complex
|
||||||
|
|
||||||
|
A production deployment with optional inputs:
|
||||||
|
|
||||||
|
[`examples/complex.yaml`](examples/complex.yaml)
|
||||||
|
```yaml
|
||||||
|
# Complex ALB with HTTPS + ACM cert (requires a consumer-supplied domain)
|
||||||
|
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||||
|
module: alb
|
||||||
|
environment: dev
|
||||||
|
inputs:
|
||||||
|
name: my-production-alb
|
||||||
|
subnets: subnet-aaa,subnet-bbb
|
||||||
|
security_group: sg-xxx
|
||||||
|
port: 443
|
||||||
|
protocol: HTTPS
|
||||||
|
region: us-east-1
|
||||||
|
```
|
||||||
|
|
||||||
## Versioning
|
## Versioning
|
||||||
|
|
||||||
`1.0.0` — interface MAJOR, behavior MINOR, lifecycle PATCH. MAJOR bumps
|
`1.0.0` — interface MAJOR, behavior MINOR, lifecycle PATCH. MAJOR bumps
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# Complex ALB with HTTPS + ACM cert (requires a consumer-supplied domain)
|
||||||
|
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||||
|
module: alb
|
||||||
|
environment: dev
|
||||||
|
inputs:
|
||||||
|
name: my-production-alb
|
||||||
|
subnets: subnet-aaa,subnet-bbb
|
||||||
|
security_group: sg-xxx
|
||||||
|
port: 443
|
||||||
|
protocol: HTTPS
|
||||||
|
region: us-east-1
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||||
|
module: alb
|
||||||
|
environment: dev
|
||||||
|
inputs:
|
||||||
|
name: my-alb
|
||||||
|
subnets: subnet-aaa,subnet-bbb
|
||||||
|
security_group: sg-xxx
|
||||||
|
port: 80
|
||||||
|
protocol: HTTP
|
||||||
|
region: us-east-1
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
{
|
||||||
|
"version": "1.0.0",
|
||||||
|
"stack": {
|
||||||
|
"name": "alb",
|
||||||
|
"kind": "l1",
|
||||||
|
"depth": 1
|
||||||
|
},
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "alb-loadbalancer",
|
||||||
|
"type": "aws:elbv2:loadbalancer",
|
||||||
|
"module": "alb@1.0.0",
|
||||||
|
"inputs": {
|
||||||
|
"name": "acdl-alb",
|
||||||
|
"subnets": "subnet-12345",
|
||||||
|
"security_group": "sg-12345",
|
||||||
|
"region": "us-east-1"
|
||||||
|
},
|
||||||
|
"outputs": {
|
||||||
|
"lb_arn": {
|
||||||
|
"type": "arn"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "alb-targetgroup",
|
||||||
|
"type": "aws:elbv2:targetgroup",
|
||||||
|
"module": "alb@1.0.0",
|
||||||
|
"inputs": {
|
||||||
|
"name": "acdl-alb",
|
||||||
|
"port": 80,
|
||||||
|
"protocol": "HTTP",
|
||||||
|
"region": "us-east-1"
|
||||||
|
},
|
||||||
|
"outputs": {
|
||||||
|
"target_group_arn": {
|
||||||
|
"type": "arn"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "alb-listener",
|
||||||
|
"type": "aws:elbv2:listener",
|
||||||
|
"module": "alb@1.0.0",
|
||||||
|
"inputs": {
|
||||||
|
"port": 80,
|
||||||
|
"protocol": "HTTP",
|
||||||
|
"region": "us-east-1"
|
||||||
|
},
|
||||||
|
"outputs": {
|
||||||
|
"listener_arn": {
|
||||||
|
"type": "arn"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -52,7 +52,23 @@
|
|||||||
"description": "The target group ARN."
|
"description": "The target group ARN."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nfrs": {},
|
"nfrs": {
|
||||||
|
"encryption_enabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Enable TLS/HTTPS encryption in transit.",
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"tls_enabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Enable TLS listener.",
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"deletion_protection": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Prevent resource destruction via Terraform lifecycle prevent_destroy",
|
||||||
|
"default": true
|
||||||
|
}
|
||||||
|
},
|
||||||
"resources": [
|
"resources": [
|
||||||
{
|
{
|
||||||
"type": "aws:elbv2:loadbalancer",
|
"type": "aws:elbv2:loadbalancer",
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
# cloudfront — CloudFront distribution
|
||||||
|
|
||||||
|
> **Module kind:** primitive | **Version:** 1.0.0
|
||||||
|
|
||||||
|
A CloudFront distribution with an S3 origin via Origin Access Control
|
||||||
|
(OAC). The distribution serves the bucket's static content from the
|
||||||
|
global edge network with HTTPS redirection by default. An optional WAF
|
||||||
|
web ACL can be associated to filter traffic before it reaches the
|
||||||
|
origin.
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
| Resource | Type | Purpose |
|
||||||
|
|----------|------|---------|
|
||||||
|
| `oac` | `aws_cloudfront_origin_access_control` | Origin Access Control signing the S3 origin |
|
||||||
|
| `distribution` | `aws_cloudfront_distribution` | The CloudFront distribution with an S3 origin via OAC |
|
||||||
|
|
||||||
|
## Inputs
|
||||||
|
|
||||||
|
| Name | Type | Required | Default | Description |
|
||||||
|
|------|------|----------|---------|-------------|
|
||||||
|
| `bucket_regional_domain_name` | string | yes | — | The S3 bucket regional domain name (ref to s3 origin) |
|
||||||
|
| `price_class` | string | no | `PriceClass_100` | CloudFront price class |
|
||||||
|
| `viewer_protocol_policy` | string | no | `redirect-to-https` | Viewer protocol policy |
|
||||||
|
| `default_ttl` | number | no | 3600 | Default TTL in seconds |
|
||||||
|
| `max_ttl` | number | no | 86400 | Max TTL in seconds |
|
||||||
|
| `waf_web_acl_arn` | string | no | — | WAF web ACL ARN to associate (ref to waf) |
|
||||||
|
| `region` | string | yes | — | AWS region (CloudFront is global but the provider region is used for the OAC) |
|
||||||
|
|
||||||
|
## Outputs
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
|------|------|-------------|
|
||||||
|
| `distribution_arn` | arn | The CloudFront distribution ARN |
|
||||||
|
| `distribution_domain_name` | string | The CloudFront distribution domain name (e.g. d111111abcdef8.cloudfront.net) |
|
||||||
|
| `oac_id` | string | The Origin Access Control ID |
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "cloudfront",
|
||||||
|
"type": "aws:cloudfront:distribution",
|
||||||
|
"module": "cloudfront@1.0.0",
|
||||||
|
"inputs": {
|
||||||
|
"bucket_regional_domain_name": "ref:s3.bucket_regional_domain_name",
|
||||||
|
"price_class": "PriceClass_100",
|
||||||
|
"viewer_protocol_policy": "redirect-to-https",
|
||||||
|
"default_ttl": 3600,
|
||||||
|
"max_ttl": 86400,
|
||||||
|
"waf_web_acl_arn": "ref:waf.web_acl_arn",
|
||||||
|
"region": "us-east-1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `bucket_regional_domain_name` and `waf_web_acl_arn` inputs are
|
||||||
|
typically wired as `ref:` expressions from the `s3` and `waf` primitives
|
||||||
|
inside a module composition (see `modules/l2/static-assets`).
|
||||||
|
|
||||||
|
## Compliance extension points
|
||||||
|
|
||||||
|
- **TLS/HTTPS** — viewer protocol policy defaults to `redirect-to-https`;
|
||||||
|
a custom ACM certificate + `viewer_certificate` block can pin TLS to a
|
||||||
|
customer domain (SOC2 CC6.1, GDPR Art.32).
|
||||||
|
- **Geo restriction** — the `restrictions.geo_restriction` block can
|
||||||
|
whitelist/blacklist countries for data-residency compliance (GDPR
|
||||||
|
Art.44, SOC2 CC6.1).
|
||||||
|
- **Logging** — CloudFront access logs to an S3 bucket for auditability
|
||||||
|
(SOC2 CC7.2, DORA audit trail).
|
||||||
|
- **Field-level encryption** — add field-level encryption for PII fields
|
||||||
|
in POST bodies (HIPAA §164.312(a)(2)(iv), GDPR Art.32).
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
Validated example contracts are in [`examples/`](examples/). The platform-test
|
||||||
|
pipeline validates them against `schemas/contract.schema.json`.
|
||||||
|
|
||||||
|
### Simple
|
||||||
|
|
||||||
|
A minimal deployment:
|
||||||
|
|
||||||
|
[`examples/simple.yaml`](examples/simple.yaml)
|
||||||
|
```yaml
|
||||||
|
# Simple CloudFront distribution (S3 origin, no WAF)
|
||||||
|
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||||
|
module: cloudfront
|
||||||
|
environment: dev
|
||||||
|
inputs:
|
||||||
|
bucket_regional_domain_name: my-bucket.s3.us-east-1.amazonaws.com
|
||||||
|
region: us-east-1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Complex
|
||||||
|
|
||||||
|
A production deployment with optional inputs:
|
||||||
|
|
||||||
|
[`examples/complex.yaml`](examples/complex.yaml)
|
||||||
|
```yaml
|
||||||
|
# Complex CloudFront with WAF + custom TTL + viewer protocol redirect
|
||||||
|
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||||
|
module: cloudfront
|
||||||
|
environment: dev
|
||||||
|
inputs:
|
||||||
|
bucket_regional_domain_name: my-bucket.s3.us-east-1.amazonaws.com
|
||||||
|
price_class: PriceClass_100
|
||||||
|
viewer_protocol_policy: redirect-to-https
|
||||||
|
default_ttl: 3600
|
||||||
|
max_ttl: 86400
|
||||||
|
waf_web_acl_arn: arn:aws:wafv2:us-east-1:000000000000:webacl/my-waf
|
||||||
|
region: us-east-1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Versioning
|
||||||
|
|
||||||
|
`1.0.0` — interface MAJOR, behavior MINOR, lifecycle PATCH. MAJOR bumps
|
||||||
|
require a new registry entry (immutable publication); old entries enter
|
||||||
|
a 12-month deprecation window.
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# Complex CloudFront with WAF + custom TTL + viewer protocol redirect
|
||||||
|
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||||
|
module: cloudfront
|
||||||
|
environment: dev
|
||||||
|
inputs:
|
||||||
|
bucket_regional_domain_name: my-bucket.s3.us-east-1.amazonaws.com
|
||||||
|
price_class: PriceClass_100
|
||||||
|
viewer_protocol_policy: redirect-to-https
|
||||||
|
default_ttl: 3600
|
||||||
|
max_ttl: 86400
|
||||||
|
waf_web_acl_arn: arn:aws:wafv2:us-east-1:000000000000:webacl/my-waf
|
||||||
|
region: us-east-1
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# Simple CloudFront distribution (S3 origin, no WAF)
|
||||||
|
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||||
|
module: cloudfront
|
||||||
|
environment: dev
|
||||||
|
inputs:
|
||||||
|
bucket_regional_domain_name: my-bucket.s3.us-east-1.amazonaws.com
|
||||||
|
region: us-east-1
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"version": "1.0.0",
|
||||||
|
"stack": {
|
||||||
|
"name": "cloudfront",
|
||||||
|
"kind": "l1",
|
||||||
|
"depth": 1
|
||||||
|
},
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "cloudfront-originaccesscontrol",
|
||||||
|
"type": "aws:cloudfront:originaccesscontrol",
|
||||||
|
"module": "cloudfront@1.0.0",
|
||||||
|
"inputs": {
|
||||||
|
"name": "acdl-oac",
|
||||||
|
"origin_type": "s3",
|
||||||
|
"signing_behavior": "always",
|
||||||
|
"region": "us-east-1"
|
||||||
|
},
|
||||||
|
"outputs": {
|
||||||
|
"oac_id": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cloudfront-distribution",
|
||||||
|
"type": "aws:cloudfront:distribution",
|
||||||
|
"module": "cloudfront@1.0.0",
|
||||||
|
"inputs": {
|
||||||
|
"bucket_regional_domain_name": "acdl-spike-bucket.s3.us-east-1.amazonaws.com",
|
||||||
|
"price_class": "PriceClass_100",
|
||||||
|
"viewer_protocol_policy": "redirect-to-https",
|
||||||
|
"default_ttl": 3600,
|
||||||
|
"max_ttl": 86400,
|
||||||
|
"region": "us-east-1"
|
||||||
|
},
|
||||||
|
"outputs": {
|
||||||
|
"distribution_arn": {
|
||||||
|
"type": "arn"
|
||||||
|
},
|
||||||
|
"distribution_domain_name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"oac_id": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
{
|
||||||
|
"name": "cloudfront",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"kind": "l1",
|
||||||
|
"type": "aws:cloudfront:distribution",
|
||||||
|
"description": "CloudFront distribution primitive (substrate-agnostic stack types aws:cloudfront:distribution + aws:cloudfront:originaccesscontrol; the Terraform adapter translates to aws_cloudfront_distribution + aws_cloudfront_origin_access_control).",
|
||||||
|
"inputs": {
|
||||||
|
"bucket_regional_domain_name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The S3 bucket regional domain name (ref to s3 origin).",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"price_class": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "CloudFront price class (default PriceClass_100).",
|
||||||
|
"required": false,
|
||||||
|
"default": "PriceClass_100"
|
||||||
|
},
|
||||||
|
"viewer_protocol_policy": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Viewer protocol policy (default redirect-to-https).",
|
||||||
|
"required": false,
|
||||||
|
"default": "redirect-to-https"
|
||||||
|
},
|
||||||
|
"default_ttl": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Default TTL in seconds (default 3600).",
|
||||||
|
"required": false,
|
||||||
|
"default": 3600
|
||||||
|
},
|
||||||
|
"max_ttl": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Max TTL in seconds (default 86400).",
|
||||||
|
"required": false,
|
||||||
|
"default": 86400
|
||||||
|
},
|
||||||
|
"waf_web_acl_arn": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "WAF web ACL ARN to associate (optional, ref to waf).",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
"region": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "AWS region (CloudFront is global but the provider region is used for the OAC).",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"outputs": {
|
||||||
|
"distribution_arn": {
|
||||||
|
"type": "arn",
|
||||||
|
"description": "The CloudFront distribution ARN."
|
||||||
|
},
|
||||||
|
"distribution_domain_name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The CloudFront distribution domain name (e.g. d111111abcdef8.cloudfront.net)."
|
||||||
|
},
|
||||||
|
"oac_id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The Origin Access Control ID."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nfrs": {
|
||||||
|
"encryption_enabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Enable encryption in transit (HTTPS only).",
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"deletion_protection": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Prevent resource destruction via Terraform lifecycle prevent_destroy",
|
||||||
|
"default": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"type": "aws:cloudfront:distribution",
|
||||||
|
"description": "CloudFront distribution with S3 origin via OAC.",
|
||||||
|
"inputs": ["bucket_regional_domain_name", "price_class", "viewer_protocol_policy", "default_ttl", "max_ttl", "waf_web_acl_arn", "oac_id"],
|
||||||
|
"outputs": ["distribution_arn", "distribution_domain_name"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "aws:cloudfront:originaccesscontrol",
|
||||||
|
"description": "Origin Access Control for the S3 origin.",
|
||||||
|
"inputs": ["name", "origin_type", "signing_behavior"],
|
||||||
|
"outputs": ["oac_id"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"intra_refs": [
|
||||||
|
{"from": "aws:cloudfront:distribution.oac_id", "to": "aws:cloudfront:originaccesscontrol.oac_id"}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# ecr — ECR repository
|
# ecr — ECR repository
|
||||||
|
|
||||||
> **Module kind:** L1 primitive | **Version:** 1.0.0
|
> **Module kind:** primitive | **Version:** 1.0.0
|
||||||
|
|
||||||
A single ECR repository that hosts the container image for the ECS
|
A single ECR repository that hosts the container image for the ECS
|
||||||
task. The simplest container-registry module — one resource, two
|
task. The simplest container-registry module — one resource, two
|
||||||
@@ -51,6 +51,40 @@ The `repository_url` output is used to build the `image` input for
|
|||||||
- **Lifecycle policy** — add `aws_ecr_lifecycle_policy` to enforce image retention / cleanup (GDPR Art.5(2) data minimization, SOC2 CC5.2).
|
- **Lifecycle policy** — add `aws_ecr_lifecycle_policy` to enforce image retention / cleanup (GDPR Art.5(2) data minimization, SOC2 CC5.2).
|
||||||
- **Access policy** — add a repository policy restricting pull/push to known roles (SOC2 CC6.1, HIPAA §164.308(a)(4)).
|
- **Access policy** — add a repository policy restricting pull/push to known roles (SOC2 CC6.1, HIPAA §164.308(a)(4)).
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
Validated example contracts are in [`examples/`](examples/). The platform-test
|
||||||
|
pipeline validates them against `schemas/contract.schema.json`.
|
||||||
|
|
||||||
|
### Simple
|
||||||
|
|
||||||
|
A minimal deployment:
|
||||||
|
|
||||||
|
[`examples/simple.yaml`](examples/simple.yaml)
|
||||||
|
```yaml
|
||||||
|
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||||
|
module: ecr
|
||||||
|
environment: dev
|
||||||
|
inputs:
|
||||||
|
name: my-repo
|
||||||
|
region: us-east-1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Complex
|
||||||
|
|
||||||
|
A production deployment with optional inputs:
|
||||||
|
|
||||||
|
[`examples/complex.yaml`](examples/complex.yaml)
|
||||||
|
```yaml
|
||||||
|
# Complex ECR with lifecycle policy + image scanning
|
||||||
|
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||||
|
module: ecr
|
||||||
|
environment: dev
|
||||||
|
inputs:
|
||||||
|
name: my-production-repo
|
||||||
|
region: us-east-1
|
||||||
|
```
|
||||||
|
|
||||||
## Versioning
|
## Versioning
|
||||||
|
|
||||||
`1.0.0` — interface MAJOR, behavior MINOR, lifecycle PATCH. MAJOR bumps
|
`1.0.0` — interface MAJOR, behavior MINOR, lifecycle PATCH. MAJOR bumps
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# Complex ECR with lifecycle policy + image scanning
|
||||||
|
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||||
|
module: ecr
|
||||||
|
environment: dev
|
||||||
|
inputs:
|
||||||
|
name: my-production-repo
|
||||||
|
region: us-east-1
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||||
|
module: ecr
|
||||||
|
environment: dev
|
||||||
|
inputs:
|
||||||
|
name: my-repo
|
||||||
|
region: us-east-1
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"version": "1.0.0",
|
||||||
|
"stack": {
|
||||||
|
"name": "ecr",
|
||||||
|
"kind": "l1",
|
||||||
|
"depth": 1
|
||||||
|
},
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "ecr",
|
||||||
|
"type": "aws:ecr:repository",
|
||||||
|
"module": "ecr@1.0.0",
|
||||||
|
"inputs": {
|
||||||
|
"name": "acdl-demo",
|
||||||
|
"region": "us-east-1"
|
||||||
|
},
|
||||||
|
"outputs": {
|
||||||
|
"repository_url": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"repository_arn": {
|
||||||
|
"type": "arn"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -14,6 +14,11 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "AWS region the repository is created in.",
|
"description": "AWS region the repository is created in.",
|
||||||
"required": true
|
"required": true
|
||||||
|
},
|
||||||
|
"kms_key_arn": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "ARN of the CMK for repository encryption; if absent, uses AWS-managed key.",
|
||||||
|
"required": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"outputs": {
|
"outputs": {
|
||||||
@@ -26,5 +31,21 @@
|
|||||||
"description": "The ECR repository ARN."
|
"description": "The ECR repository ARN."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nfrs": {}
|
"nfrs": {
|
||||||
|
"encryption_enabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Enable repository encryption (KMS).",
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"encryption_type": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Encryption type.",
|
||||||
|
"default": "KMS"
|
||||||
|
},
|
||||||
|
"deletion_protection": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Prevent resource destruction via Terraform lifecycle prevent_destroy",
|
||||||
|
"default": true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# ecs-cluster — ECS Fargate cluster
|
# ecs-cluster — ECS Fargate cluster
|
||||||
|
|
||||||
> **Module kind:** L1 primitive | **Version:** 1.0.0
|
> **Module kind:** primitive | **Version:** 1.0.0
|
||||||
|
|
||||||
An ECS Fargate cluster. The simplest ECS module — one resource, two
|
An ECS Fargate cluster. The simplest ECS module — one resource, two
|
||||||
inputs, two outputs. The cluster is the container orchestration
|
inputs, two outputs. The cluster is the container orchestration
|
||||||
@@ -49,6 +49,40 @@ The `cluster_arn` output is referenced by `ecs-service` as its
|
|||||||
- **CloudWatch Logs** — add a log group with retention policy for cluster-level audit logs (SOX, SOC2 CC7.2, HIPAA §164.312(b)).
|
- **CloudWatch Logs** — add a log group with retention policy for cluster-level audit logs (SOX, SOC2 CC7.2, HIPAA §164.312(b)).
|
||||||
- **Encryption** — add `settings { name = "containerInsights", value = "enabled" }` and KMS-based encryption for container data (HIPAA §164.312(a)(2)(iv), GDPR Art.32).
|
- **Encryption** — add `settings { name = "containerInsights", value = "enabled" }` and KMS-based encryption for container data (HIPAA §164.312(a)(2)(iv), GDPR Art.32).
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
Validated example contracts are in [`examples/`](examples/). The platform-test
|
||||||
|
pipeline validates them against `schemas/contract.schema.json`.
|
||||||
|
|
||||||
|
### Simple
|
||||||
|
|
||||||
|
A minimal deployment:
|
||||||
|
|
||||||
|
[`examples/simple.yaml`](examples/simple.yaml)
|
||||||
|
```yaml
|
||||||
|
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||||
|
module: ecs-cluster
|
||||||
|
environment: dev
|
||||||
|
inputs:
|
||||||
|
name: my-cluster
|
||||||
|
region: us-east-1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Complex
|
||||||
|
|
||||||
|
A production deployment with optional inputs:
|
||||||
|
|
||||||
|
[`examples/complex.yaml`](examples/complex.yaml)
|
||||||
|
```yaml
|
||||||
|
# Complex ECS cluster with container insights
|
||||||
|
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||||
|
module: ecs-cluster
|
||||||
|
environment: dev
|
||||||
|
inputs:
|
||||||
|
name: my-production-cluster
|
||||||
|
region: us-east-1
|
||||||
|
```
|
||||||
|
|
||||||
## Versioning
|
## Versioning
|
||||||
|
|
||||||
`1.0.0` — interface MAJOR, behavior MINOR, lifecycle PATCH. MAJOR bumps
|
`1.0.0` — interface MAJOR, behavior MINOR, lifecycle PATCH. MAJOR bumps
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# Complex ECS cluster with container insights
|
||||||
|
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||||
|
module: ecs-cluster
|
||||||
|
environment: dev
|
||||||
|
inputs:
|
||||||
|
name: my-production-cluster
|
||||||
|
region: us-east-1
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||||
|
module: ecs-cluster
|
||||||
|
environment: dev
|
||||||
|
inputs:
|
||||||
|
name: my-cluster
|
||||||
|
region: us-east-1
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"version": "1.0.0",
|
||||||
|
"stack": {
|
||||||
|
"name": "ecs-cluster",
|
||||||
|
"kind": "l1",
|
||||||
|
"depth": 1
|
||||||
|
},
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "ecs-cluster",
|
||||||
|
"type": "aws:ecs:cluster",
|
||||||
|
"module": "ecs-cluster@1.0.0",
|
||||||
|
"inputs": {
|
||||||
|
"name": "acdl-cluster",
|
||||||
|
"region": "us-east-1"
|
||||||
|
},
|
||||||
|
"outputs": {
|
||||||
|
"cluster_arn": {
|
||||||
|
"type": "arn"
|
||||||
|
},
|
||||||
|
"cluster_id": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -14,6 +14,11 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "AWS region the cluster is created in.",
|
"description": "AWS region the cluster is created in.",
|
||||||
"required": true
|
"required": true
|
||||||
|
},
|
||||||
|
"kms_key_arn": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "ARN of the CMK for CloudWatch log group encryption; if absent, uses managed key.",
|
||||||
|
"required": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"outputs": {
|
"outputs": {
|
||||||
@@ -26,5 +31,16 @@
|
|||||||
"description": "The ECS cluster id (name)."
|
"description": "The ECS cluster id (name)."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nfrs": {}
|
"nfrs": {
|
||||||
|
"encryption_enabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Enable CloudWatch log group encryption.",
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"deletion_protection": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Prevent resource destruction via Terraform lifecycle prevent_destroy",
|
||||||
|
"default": true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# ecs-service — ECS Fargate service (task definition + service)
|
# ecs-service — ECS Fargate service (task definition + service)
|
||||||
|
|
||||||
> **Module kind:** L1 primitive | **Version:** 1.0.0
|
> **Module kind:** primitive | **Version:** 1.0.0
|
||||||
|
|
||||||
An ECS Fargate service with its task definition. Runs a container image
|
An ECS Fargate service with its task definition. Runs a container image
|
||||||
on Fargate, optionally behind an ALB target group. This is a
|
on Fargate, optionally behind an ALB target group. This is a
|
||||||
@@ -71,6 +71,47 @@ provided.
|
|||||||
- **Deployment circuit breaker** — add `deployment_circuit_breaker` block for resilience (SOC2 CC9.1, DORA operational resilience).
|
- **Deployment circuit breaker** — add `deployment_circuit_breaker` block for resilience (SOC2 CC9.1, DORA operational resilience).
|
||||||
- **Health check** — add a `health_check` block to the target group (currently missing despite the contract schema having a healthcheck field).
|
- **Health check** — add a `health_check` block to the target group (currently missing despite the contract schema having a healthcheck field).
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
Validated example contracts are in [`examples/`](examples/). The platform-test
|
||||||
|
pipeline validates them against `schemas/contract.schema.json`.
|
||||||
|
|
||||||
|
### Simple
|
||||||
|
|
||||||
|
A minimal deployment:
|
||||||
|
|
||||||
|
[`examples/simple.yaml`](examples/simple.yaml)
|
||||||
|
```yaml
|
||||||
|
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||||
|
module: ecs-service
|
||||||
|
environment: dev
|
||||||
|
inputs:
|
||||||
|
name: my-service
|
||||||
|
region: us-east-1
|
||||||
|
image: public.ecr.aws/docker/library/nginx:latest
|
||||||
|
port: 80
|
||||||
|
```
|
||||||
|
|
||||||
|
### Complex
|
||||||
|
|
||||||
|
A production deployment with optional inputs:
|
||||||
|
|
||||||
|
[`examples/complex.yaml`](examples/complex.yaml)
|
||||||
|
```yaml
|
||||||
|
# Complex ECS service with env vars + health check
|
||||||
|
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||||
|
module: ecs-service
|
||||||
|
environment: dev
|
||||||
|
inputs:
|
||||||
|
name: my-production-service
|
||||||
|
region: us-east-1
|
||||||
|
image: public.ecr.aws/docker/library/nginx:latest
|
||||||
|
port: 8080
|
||||||
|
env:
|
||||||
|
LOG_LEVEL: info
|
||||||
|
ENVIRONMENT: production
|
||||||
|
```
|
||||||
|
|
||||||
## Versioning
|
## Versioning
|
||||||
|
|
||||||
`1.0.0` — interface MAJOR, behavior MINOR, lifecycle PATCH. MAJOR bumps
|
`1.0.0` — interface MAJOR, behavior MINOR, lifecycle PATCH. MAJOR bumps
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# Complex ECS service with env vars + health check
|
||||||
|
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||||
|
module: ecs-service
|
||||||
|
environment: dev
|
||||||
|
inputs:
|
||||||
|
name: my-production-service
|
||||||
|
region: us-east-1
|
||||||
|
image: public.ecr.aws/docker/library/nginx:latest
|
||||||
|
port: 8080
|
||||||
|
env:
|
||||||
|
LOG_LEVEL: info
|
||||||
|
ENVIRONMENT: production
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||||
|
module: ecs-service
|
||||||
|
environment: dev
|
||||||
|
inputs:
|
||||||
|
name: my-service
|
||||||
|
region: us-east-1
|
||||||
|
image: public.ecr.aws/docker/library/nginx:latest
|
||||||
|
port: 80
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"version": "1.0.0",
|
||||||
|
"stack": {
|
||||||
|
"name": "ecs-service",
|
||||||
|
"kind": "l1",
|
||||||
|
"depth": 1
|
||||||
|
},
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "service-taskdefinition",
|
||||||
|
"type": "aws:ecs:task_definition",
|
||||||
|
"module": "ecs-service@1.0.0",
|
||||||
|
"inputs": {
|
||||||
|
"image": "public.ecr.aws/docker/library/nginx:latest",
|
||||||
|
"port": 80,
|
||||||
|
"region": "us-east-1"
|
||||||
|
},
|
||||||
|
"outputs": {
|
||||||
|
"task_def_arn": {
|
||||||
|
"type": "arn"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "service-service",
|
||||||
|
"type": "aws:ecs:service",
|
||||||
|
"module": "ecs-service@1.0.0",
|
||||||
|
"inputs": {
|
||||||
|
"cluster_arn": "arn:aws:ecs:us-east-1:123456789012:cluster/acdl-cluster",
|
||||||
|
"subnets": "subnet-12345",
|
||||||
|
"security_group": "sg-12345",
|
||||||
|
"region": "us-east-1"
|
||||||
|
},
|
||||||
|
"outputs": {
|
||||||
|
"service_arn": {
|
||||||
|
"type": "arn"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -56,6 +56,11 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "AWS region the service is created in.",
|
"description": "AWS region the service is created in.",
|
||||||
"required": true
|
"required": true
|
||||||
|
},
|
||||||
|
"kms_key_arn": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "ARN of the CMK for CloudWatch log group encryption; if absent, uses managed key.",
|
||||||
|
"required": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"outputs": {
|
"outputs": {
|
||||||
@@ -68,7 +73,18 @@
|
|||||||
"description": "The ECS task definition ARN."
|
"description": "The ECS task definition ARN."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nfrs": {},
|
"nfrs": {
|
||||||
|
"encryption_enabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Enable CloudWatch log group encryption.",
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"deletion_protection": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Prevent resource destruction via Terraform lifecycle prevent_destroy",
|
||||||
|
"default": true
|
||||||
|
}
|
||||||
|
},
|
||||||
"resources": [
|
"resources": [
|
||||||
{
|
{
|
||||||
"type": "aws:ecs:task_definition",
|
"type": "aws:ecs:task_definition",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# iam-role — IAM role
|
# iam-role — IAM role
|
||||||
|
|
||||||
> **Module kind:** L1 primitive | **Version:** 1.0.0
|
> **Module kind:** primitive | **Version:** 1.0.0
|
||||||
|
|
||||||
A single IAM role with an assume-role policy and optional managed
|
A single IAM role with an assume-role policy and optional managed
|
||||||
policy attachments. Used as the ECS task execution role.
|
policy attachments. Used as the ECS task execution role.
|
||||||
@@ -57,6 +57,40 @@ into the Terraform `assume_role_policy` argument. The
|
|||||||
- **Access Analyzer** — add `aws_accessanalyzer_analyzer` to verify least-privilege (SOC2 CC6.1, GDPR Art.32).
|
- **Access Analyzer** — add `aws_accessanalyzer_analyzer` to verify least-privilege (SOC2 CC6.1, GDPR Art.32).
|
||||||
- **Role separation** — add a separate task role vs. execution role (SOC2 CC6.3 segregation of duties).
|
- **Role separation** — add a separate task role vs. execution role (SOC2 CC6.3 segregation of duties).
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
Validated example contracts are in [`examples/`](examples/). The platform-test
|
||||||
|
pipeline validates them against `schemas/contract.schema.json`.
|
||||||
|
|
||||||
|
### Simple
|
||||||
|
|
||||||
|
A minimal deployment:
|
||||||
|
|
||||||
|
[`examples/simple.yaml`](examples/simple.yaml)
|
||||||
|
```yaml
|
||||||
|
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||||
|
module: iam-role
|
||||||
|
environment: dev
|
||||||
|
inputs:
|
||||||
|
name: my-task-role
|
||||||
|
region: us-east-1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Complex
|
||||||
|
|
||||||
|
A production deployment with optional inputs:
|
||||||
|
|
||||||
|
[`examples/complex.yaml`](examples/complex.yaml)
|
||||||
|
```yaml
|
||||||
|
# Complex IAM role with managed policies
|
||||||
|
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||||
|
module: iam-role
|
||||||
|
environment: dev
|
||||||
|
inputs:
|
||||||
|
name: my-production-task-role
|
||||||
|
region: us-east-1
|
||||||
|
```
|
||||||
|
|
||||||
## Versioning
|
## Versioning
|
||||||
|
|
||||||
`1.0.0` — interface MAJOR, behavior MINOR, lifecycle PATCH. MAJOR bumps
|
`1.0.0` — interface MAJOR, behavior MINOR, lifecycle PATCH. MAJOR bumps
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# Complex IAM role with managed policies
|
||||||
|
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||||
|
module: iam-role
|
||||||
|
environment: dev
|
||||||
|
inputs:
|
||||||
|
name: my-production-task-role
|
||||||
|
region: us-east-1
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||||
|
module: iam-role
|
||||||
|
environment: dev
|
||||||
|
inputs:
|
||||||
|
name: my-task-role
|
||||||
|
region: us-east-1
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"version": "1.0.0",
|
||||||
|
"stack": {
|
||||||
|
"name": "iam-role",
|
||||||
|
"kind": "l1",
|
||||||
|
"depth": 1
|
||||||
|
},
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "iam-role",
|
||||||
|
"type": "aws:iam:role",
|
||||||
|
"module": "iam-role@1.0.0",
|
||||||
|
"inputs": {
|
||||||
|
"role_name": "acdl-task-role",
|
||||||
|
"assume_role_policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"ecs-tasks.amazonaws.com\"},\"Action\":\"sts:AssumeRole\"}]}",
|
||||||
|
"region": "us-east-1"
|
||||||
|
},
|
||||||
|
"outputs": {
|
||||||
|
"role_arn": {
|
||||||
|
"type": "arn"
|
||||||
|
},
|
||||||
|
"role_id": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -36,5 +36,16 @@
|
|||||||
"description": "The IAM role id."
|
"description": "The IAM role id."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nfrs": {}
|
"nfrs": {
|
||||||
|
"encryption_enabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Encryption is not applicable to IAM roles but included for standards compliance.",
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"deletion_protection": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Prevent resource destruction via Terraform lifecycle prevent_destroy",
|
||||||
|
"default": true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
# kms-key — KMS customer-managed key
|
||||||
|
|
||||||
|
> **Module kind:** primitive | **Version:** 1.0.0
|
||||||
|
|
||||||
|
A customer-managed KMS key for per-stack encryption. Created with key
|
||||||
|
rotation enabled. One key per L2 deployment (no shared keys).
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
| Resource | Type | Purpose |
|
||||||
|
|----------|------|---------|
|
||||||
|
| kms-key | `aws_kms_key` | The KMS customer-managed key |
|
||||||
|
|
||||||
|
## Inputs
|
||||||
|
|
||||||
|
| Name | Type | Required | Default | Description |
|
||||||
|
|------|------|----------|---------|-------------|
|
||||||
|
| `description` | string | yes | — | Description of the KMS key |
|
||||||
|
| `region` | string | yes | — | AWS region the KMS key is created in |
|
||||||
|
| `deletion_window_days` | number | no | 30 | Number of days before the key is deleted after deletion is requested |
|
||||||
|
|
||||||
|
## Outputs
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
|------|------|-------------|
|
||||||
|
| `kms_key_arn` | arn | The ARN of the KMS key |
|
||||||
|
| `kms_key_id` | string | The ID of the KMS key |
|
||||||
|
|
||||||
|
## NFRs
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
|------|------|---------|-------------|
|
||||||
|
| `enable_rotation` | boolean | true | Enable automatic key rotation |
|
||||||
|
| `deletion_protection` | boolean | true | Prevent key destruction |
|
||||||
|
| `encryption_enabled` | boolean | true | Encryption is always enabled for a KMS key |
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "kms-key",
|
||||||
|
"type": "aws:kms:key",
|
||||||
|
"module": "kms-key@1.0.0",
|
||||||
|
"inputs": {
|
||||||
|
"description": "ACDL per-stack CMK",
|
||||||
|
"region": "us-east-1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
A concrete instance is at `instance.json` (used by the platform
|
||||||
|
pipeline as the regression baseline).
|
||||||
|
|
||||||
|
## Compliance extension points
|
||||||
|
|
||||||
|
- **Key rotation** — automatic key rotation enabled by default (SOC2 CC6.1, HIPAA §164.312(a)(2)(iv), GDPR Art.32).
|
||||||
|
- **Deletion protection** — pending deletion window prevents accidental destruction (SOC2 CC7.2).
|
||||||
|
- **Key policy** — restrict key usage to the stack's IAM roles (SOC2 CC6.1, GDPR Art.32).
|
||||||
|
- **Audit logging** — CloudTrail logs all KMS API calls (SOC2 CC7.2, DORA audit trail).
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
Validated example contracts are in [`examples/`](examples/). The platform-test
|
||||||
|
pipeline validates them against `schemas/contract.schema.json`.
|
||||||
|
|
||||||
|
### Simple
|
||||||
|
|
||||||
|
A minimal deployment:
|
||||||
|
|
||||||
|
[`examples/simple.yaml`](examples/simple.yaml)
|
||||||
|
```yaml
|
||||||
|
uses: acdl/pipelines/deploy.yaml@v1.8
|
||||||
|
module: kms-key
|
||||||
|
environment: dev
|
||||||
|
inputs:
|
||||||
|
description: "Simple CMK for testing"
|
||||||
|
region: us-east-1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Complex
|
||||||
|
|
||||||
|
A production deployment with optional inputs:
|
||||||
|
|
||||||
|
[`examples/complex.yaml`](examples/complex.yaml)
|
||||||
|
```yaml
|
||||||
|
uses: acdl/pipelines/deploy.yaml@v1.8
|
||||||
|
module: kms-key
|
||||||
|
environment: dev
|
||||||
|
inputs:
|
||||||
|
description: "Production CMK with 90-day deletion window"
|
||||||
|
region: us-east-1
|
||||||
|
deletion_window_days: 90
|
||||||
|
```
|
||||||
|
|
||||||
|
## Versioning
|
||||||
|
|
||||||
|
`1.0.0` — interface MAJOR, behavior MINOR, lifecycle PATCH. MAJOR bumps
|
||||||
|
require a new registry entry (immutable publication); old entries enter
|
||||||
|
a 12-month deprecation window.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
uses: acdl/pipelines/deploy.yaml@v1.8
|
||||||
|
module: kms-key
|
||||||
|
environment: dev
|
||||||
|
inputs:
|
||||||
|
description: "Production CMK with 90-day deletion window"
|
||||||
|
region: us-east-1
|
||||||
|
deletion_window_days: 90
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
uses: acdl/pipelines/deploy.yaml@v1.8
|
||||||
|
module: kms-key
|
||||||
|
environment: dev
|
||||||
|
inputs:
|
||||||
|
description: "Simple CMK for testing"
|
||||||
|
region: us-east-1
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user