Compare commits
172 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 807b17d04b | |||
| 0f250d2bbd | |||
| 7585c828f0 | |||
| fc070ccb15 | |||
| be6dc7cff6 | |||
| 2682719f24 | |||
| 5079d07e64 | |||
| ec30f4ae56 | |||
| 2cd9ae150d | |||
| ae0cb589ab | |||
| b0a2728f59 | |||
| fca618916c | |||
| 7cccf989b1 | |||
| 6e41f09c6e | |||
| c4d966359f | |||
| 5365bb4e0a | |||
| 80d2a6cc6c | |||
| e74a8c2f5d | |||
| 5ebf7a62c8 | |||
| cd637808f5 | |||
| 481cfe760c | |||
| bee9d02f01 | |||
| 8118d6ee27 | |||
| e1be05287b | |||
| 58100c485e | |||
| 2bea048bb6 | |||
| c05ed7a26f | |||
| 136ec6abf3 | |||
| 2f0e69272a | |||
| 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 | |||
| f68f85c9fd | |||
| 75c227429a | |||
| 04bf6bc31a | |||
| 9a1ea04f93 | |||
| 2a84c0047b | |||
| 895a2f3806 | |||
| e050e65158 | |||
| 6e23c168f1 | |||
| c816493e7e | |||
| 1598c54a8b | |||
| 2c6464afd4 | |||
| 431341a0ab | |||
| ae86a29a5e | |||
| 3508671377 | |||
| f874879973 | |||
| 0fc69b4d0c | |||
| 2ec2a87a4e | |||
| 18875cd7c8 | |||
| faea213a4c | |||
| 3bb44d9967 | |||
| 64d35c78e6 | |||
| 3cca5bb43f | |||
| b993c15fae | |||
| 699aa542df | |||
| d5cc01edbd | |||
| a3c7330b75 | |||
| d103a37419 | |||
| 7c6b8c8c84 | |||
| 5a3ab5e86b | |||
| 4ed2542ecf | |||
| 4c8de8e962 | |||
| 599db2e80d | |||
| 0fea29cdbb | |||
| 7ee57aa6c7 | |||
| 87febc7129 | |||
| 81c6e3995e | |||
| 1ad9c35fb6 | |||
| 9504782a77 | |||
| 6f865a6b3d | |||
| ab69d1069f | |||
| 031c320551 | |||
| d6b192307a | |||
| 2ed2ca6bac | |||
| 4b8758404c | |||
| 35a336aba2 | |||
| d3aa960eb8 | |||
| e29319a720 | |||
| 7afaa34b60 | |||
| 622abe015b | |||
| 8437a51c6c | |||
| cc4c27c8ab | |||
| 798f430218 | |||
| e71539d681 | |||
| 55557962bd | |||
| 4c9314710b | |||
| 3936bf460a | |||
| 3070a68e1d | |||
| e054a95fd5 | |||
| 327ba1de75 | |||
| 6d27dad114 | |||
| 067fef14aa | |||
| 96ab42fde1 | |||
| d28630d1f1 | |||
| 1d5c4d2ae7 | |||
| f8ddd8b182 | |||
| a003168b3a | |||
| 727c87339b | |||
| 167a92f621 | |||
| 8723206f5a | |||
| 412e1ef62e | |||
| 68d90c08a7 | |||
| 6ed93f0311 | |||
| f8e99ed906 | |||
| 92d4535f5f | |||
| b40aadd195 | |||
| 0779a92e2f | |||
| ecb2c78d11 | |||
| 4ab15cb7a5 | |||
| e044a2de0d | |||
| b927f9026a | |||
| 930c24be6d | |||
| 087c89edbf | |||
| 288607b3fa | |||
| 30e63d6cb5 | |||
| b84a8a2241 | |||
| 7614c41530 | |||
| 52665b8f0c | |||
| d700148063 | |||
| 80ac975e61 | |||
| 58adf9e231 | |||
| 0672edfc3f | |||
| 1415c85d35 | |||
| 72b359c9a9 | |||
| 711b61d63e | |||
| 3ea36ef3ab | |||
| 6e27df7404 |
+485
-118
@@ -1,146 +1,513 @@
|
||||
# ACDL — Architecture (initial)
|
||||
# ACDL — Architecture (v1.1 target)
|
||||
|
||||
> Initial architecture for the ACDL demo. May be incomplete; refined at phase boundaries.
|
||||
> Target architecture for the real Agentic Cloud Delivery Platform.
|
||||
> Source of truth for **how**: `docs/architecture.md` (v0.2) is the upstream
|
||||
> draft; this file is the ACDL-repo operating copy, refined at phase
|
||||
> boundaries. Where this file and `docs/vision.md` conflict, the vision wins.
|
||||
|
||||
## Status
|
||||
|
||||
Architecture is at **v0.2** upstream (`docs/architecture.md`). Milestone v1.1
|
||||
**finalizes it to v1.0** in Phase 07 by resolving the 11 open decisions
|
||||
(see `PROJECT.md` open-decision resolutions table). This file records the
|
||||
locked commitments and the v1.1 spike scope.
|
||||
|
||||
## Overview
|
||||
|
||||
The demo is a three-repo, stub-driven system that simulates an autonomous cloud delivery platform. No real cloud or AI is used; every "infrastructure" action is a bash/Python stub that emits structured evidence. The platform is driven by either a developer-supplied `contract.yaml` (L3A) or a natural-language GitHub Issue parsed by a keyword script (L3B), then flows through an autonomous Dev stage, manual QA and Prod approval gates, and finally publishes a hash-chained audit trail to a Pages site.
|
||||
The platform is **four layers + six cross-cutting concerns**. The sixth
|
||||
concern — the engine abstraction (§12) — is first-class, not an
|
||||
implementation detail. The vision's "Two Consumer Surfaces, One Platform"
|
||||
tenet binds everything: L3A and L3B converge on the same contract schema,
|
||||
the same policy envelope, and the same evidence stream.
|
||||
|
||||
```
|
||||
┌──────────────── acdl-contracts ─────────────────┐
|
||||
Developer ───▶ │ commit contract.yaml Issue (NL intent) │
|
||||
└────────────┬───────────────────┬────────────────┘
|
||||
│ (push) │ (issue opened)
|
||||
▼ ▼
|
||||
┌─────────────────┐ ┌──────────────────────┐
|
||||
│ reusable │ │ issue workflow → │
|
||||
│ pipeline │ │ l3b_agent_stub.py → │
|
||||
│ (acdl repo) │ │ contract.yaml → push │
|
||||
└────────┬────────┘ └──────────────────────┘
|
||||
│
|
||||
┌────────────────────┼────────────────────┐
|
||||
▼ ▼ ▼
|
||||
Dev (autonomous) QA (approval) Prod (approval)
|
||||
mock_executor.sh environment gate environment gate
|
||||
policy_checker.py
|
||||
confidence_signal.py
|
||||
┌──────────── acdl-contracts ────────────┐
|
||||
Developer ───▶ │ commit contract.yaml │ (L3A)
|
||||
Citizen dev ──▶ │ Issue → agent → contract.yaml │ (L3B)
|
||||
└────────────────┬───────────────────────┘
|
||||
│ (push)
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ central pipeline │
|
||||
│ (acdl repo, Gitea │
|
||||
│ Actions / act_runner) │
|
||||
└────────┬─────────────┘
|
||||
│
|
||||
┌─────────────────────────┼─────────────────────────┐
|
||||
▼ ▼ ▼
|
||||
contract→IR resolution policy (Checkov/Kyverno) confidence signal
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
Terraform adapter ──▶ terraform plan ──▶ PolicyCheckResult ──▶ {score,band}
|
||||
│ │
|
||||
▼ ▼
|
||||
dev (autonomous, ≥0.50) qa (HITL, ≥0.75) prod (HITL, ≥0.90) dr (HITL, ≥0.95)
|
||||
│
|
||||
▼
|
||||
evidence_writer.py ──▶ audit.json (hash-chained) ──▶ acdl-evidence
|
||||
│
|
||||
▼
|
||||
index.html (Pages)
|
||||
timeline UI
|
||||
DynamoDB outbox ──▶ S3 Object Lock (7-yr, source of truth) ──▶ GitHub audit repo (hot index)
|
||||
│
|
||||
▼
|
||||
acdl-evidence (timeline UI)
|
||||
```
|
||||
|
||||
## Components
|
||||
## Layers
|
||||
|
||||
| Name | Description | Boundaries | Depends On |
|
||||
|------|-------------|-----------|------------|
|
||||
| `acdl` repo | Platform meta repo: reusable workflows, L1/L2 stub modules, core scripts | Owns workflows + stubs; does not hold contracts or evidence | — |
|
||||
| L1 modules | Single-purpose infra primitives (EKS Fargate, IAM, Lambda, API Gateway, EventBridge, SQS, S3, CloudWatch) | One folder per L1; `manifest.yaml` + `mock_apply.sh`; do not compose with other L1s | `acdl` repo |
|
||||
| L2 modules | Composed stacks (invoice, commodity-price-feed, energy-analytics-api, regulatory-reporting) | Reference L1s by name; max depth 5; expressed as a composition manifest | L1 modules |
|
||||
| `mock_executor.sh` | Reads an L2 composition, invokes each L1 `mock_apply.sh`, writes `state.json` | Bash; reads L2 manifest + L1 manifests | L1/L2 modules |
|
||||
| `policy_checker.py` | Reads `contract.yaml`; fails on forbidden keys (e.g. `public-ingress: true`) | Python; emits `POLICY_VIOLATION:<REASON>` or pass | contract.yaml |
|
||||
| `confidence_signal.py` | Base 0.90; on policy failure drops to 0.40 and echoes reason | Python; calls policy_checker | policy_checker.py |
|
||||
| `evidence_writer.py` | Appends an event to `audit.json`, links to previous event via SHA-256 chain | Python; canonical-JSON hashing | audit.json |
|
||||
| `l3b_agent_stub.py` | Parses Issue text by keywords, emits `contract.yaml` | Python keyword map; no external APIs | contract.yaml schema |
|
||||
| `acdl-contracts` repo | Developer + agentic entry surface; holds contracts + issue workflow | Triggers main pipeline on push | `acdl` reusable workflow |
|
||||
| `acdl-evidence` repo | Pages host for `audit.json` + `index.html` timeline | Read-only for the pipeline; written at finalize stage | evidence_writer.py output |
|
||||
| Reusable pipeline workflow | Dev → QA → Prod → Finalize stages with environment gates | Gitea Actions; calls core scripts | All core scripts |
|
||||
### Layer 1 — Foundational Primitives
|
||||
Single-purpose, **engine-agnostic** primitive modules. L1 modules do
|
||||
not compose with other L1s; L1 takes its environment as input. The L1
|
||||
interface is defined against the **Target Stack IR**, not against Terraform
|
||||
directly (the IR is shaped to round-trip to Terraform in v1, per §12.1).
|
||||
|
||||
## Data Flow
|
||||
- No inter-L1 references. L1 may call Terraform data sources.
|
||||
- Semver: interface → MAJOR, behavior → MINOR, lifecycle → PATCH (W3.D).
|
||||
- Immutability on publication. 12-month deprecation window.
|
||||
- AI refinement is a flag; the trigger is the W1.A joint condition.
|
||||
|
||||
1. A `contract.yaml` arrives either by direct push (L3A) or by the issue workflow running `l3b_agent_stub.py` (L3B).
|
||||
2. Push to `acdl-contracts` triggers the reusable pipeline in the `acdl` repo.
|
||||
3. **Dev stage:** `policy_checker.py` validates the contract; `mock_executor.sh` applies the L2 composition's L1s; `confidence_signal.py` computes the score; `evidence_writer.py` records each step. If score < 0.50, the stage fails and evidence records the rejection.
|
||||
4. **QA stage:** the workflow pauses on the `qa` environment; a human approves.
|
||||
5. **Prod stage:** same gate on the `prod` environment.
|
||||
6. **Finalize:** the workflow commits the updated `audit.json` to `acdl-evidence`; Pages republishes `index.html`, which fetches and renders the timeline.
|
||||
### Layer 2 — Composed Stacks
|
||||
Combine L1 primitives into deployable shapes. Each codebase maps to one
|
||||
canonical L2 stack (`multiStack: true` only per W1.B). Shape X
|
||||
(parameterized module) or Shape Y (thin-composition layer). Hierarchical
|
||||
composition, max depth 5, only registered L1s. The thin-composition tree's
|
||||
`wires` field is defined against the IR's relationship type, not a Terraform
|
||||
module block.
|
||||
|
||||
## Build Order
|
||||
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.
|
||||
|
||||
1. Repo scaffolding: create `acdl-contracts` and `acdl-evidence` in the org; seed `acdl` directory layout.
|
||||
2. L1 modules (8 stubs).
|
||||
3. L2 modules (4 compositions).
|
||||
4. Core scripts (`mock_executor.sh`, `policy_checker.py`, `confidence_signal.py`, `evidence_writer.py`, `l3b_agent_stub.py`).
|
||||
5. Reusable pipeline workflow (Dev → QA → Prod → Finalize) + environment gates.
|
||||
6. Issue-triggered L3B workflow in `acdl-contracts`.
|
||||
7. Evidence UI (`index.html` + Pages config).
|
||||
8. Demo dry-run + the four scripted acts.
|
||||
### Layer 3A — Developer Consumer Surface
|
||||
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. **W2.A (Path B):** tag for dev/qa, SHA for prod; platform CLI
|
||||
resolves tag→SHA for prod-bound workflows.
|
||||
|
||||
## Gitea API Surface (Phase 01 research)
|
||||
### Layer 3B — Agentic Consumer Surface
|
||||
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. `profile: agentic` marker
|
||||
unlocks `naturalLanguageIntent`, `confidenceAtSubmission`, `agentTrace`.
|
||||
Initial skill catalog (BA.A): web API, worker, scheduled job, static asset,
|
||||
basic observability bootstrap.
|
||||
|
||||
Authoritative findings from the Gitea docs (added in RESEARCH; supersedes any
|
||||
GitHub-Pages / GitHub-Environments assumptions carried over from the spec):
|
||||
Environment progression:
|
||||
|
||||
| Capability | Gitea support | ACDL approach |
|
||||
|------------|---------------|---------------|
|
||||
| Org-scoped repo create | `POST /api/v1/orgs/{org}/repos` (`CreateRepoOption`) | Used to create `acdl-contracts` + `acdl-evidence` |
|
||||
| Native Pages | **None** (no `[pages]` config section) | Serve `acdl-evidence` via raw file URLs: `https://git.cloudinit.dev/continuous-intelligence/acdl-evidence/raw/branch/main/index.html`; `index.html` fetches `audit.json` from the same raw path. Requires `[cors] ENABLED=true` on the server if the UI is loaded cross-origin. |
|
||||
| Environments API | **None**; `jobs.<id>.environment` is ignored by act_runner | Model QA/Prod gates as `workflow_dispatch` approval inputs (D-004 / D-013); optionally create `qa` and `prod` branches as a visible stand-in |
|
||||
| `repository_dispatch` trigger | **Not supported** | Cross-repo trigger via `workflow_dispatch` API: `POST /api/v1/repos/{owner}/{repo}/actions/workflows/{workflow_id}/dispatches` called from a step using `$GITEA_TOKEN` |
|
||||
| Reusable workflows (`workflow_call`) | Supported | `acdl/.gitea/workflows/pipeline.yml` called via `uses: continuous-intelligence/acdl/.gitea/workflows/pipeline.yml@milestone/v1.0-initial` |
|
||||
| `workflow_dispatch` | Supported (trigger + API) | Used for the manual-approval fallback and the issue workflow's cross-repo trigger |
|
||||
| `issues.opened` trigger | Supported | Drives the L3B issue-trigger workflow in `acdl-contracts` |
|
||||
| `act_runner` labels | Single label only (`runs-on: ubuntu-latest`) | All workflows use `runs-on: ubuntu-latest` |
|
||||
| Context | `${{ gitea.* }}` and `${{ github.* }}` both work | Workflows use `gitea.*` for clarity |
|
||||
| Environment | Autonomy | Attester | Gate |
|
||||
|---|---|---|---|
|
||||
| dev | Full autonomy (no HITL) | — | Confidence ≥ 0.50, all six inputs present |
|
||||
| qa | Held for attestation | QA | GitHub Deployment approval + full QA matrix (§10) |
|
||||
| prod | Held for attestation | SRE | GitHub Deployment approval + full SRE matrix (§10) |
|
||||
| dr | Held for attestation | SRE | GitHub Deployment approval + dr-drill evidence |
|
||||
|
||||
### Branch pinning rule
|
||||
**Staging is removed.** Dev is the only autonomous environment.
|
||||
|
||||
The reusable workflow in the `acdl` repo lives on `milestone/v1.0-initial`
|
||||
(that is the repo's default branch). `uses:` references from `acdl-contracts`
|
||||
must pin to `@milestone/v1.0-initial`, not `@main` (the `acdl` repo has no
|
||||
`main` branch). The new repos `acdl-contracts` and `acdl-evidence` use
|
||||
`default_branch: "main"` (D-015) so their default branch exists immediately
|
||||
for pushes.
|
||||
## Cross-cutting concerns
|
||||
|
||||
### Default verification toolchain
|
||||
### Central pipeline template (§6)
|
||||
JSON Schema (draft 2020-12) with a thin domain wrapper. Central repo +
|
||||
generated client libraries. Multi-stage validation: schema → policy → NFR →
|
||||
confidence. Distributed enrichment. GitOps reconciler (K8s API; cdlc-gitops
|
||||
state → CRDs) + Terraform execution layer (§12.5). The pipeline emits one
|
||||
`PolicyCheckResult` per policy rule; the confidence signal consumes them as
|
||||
one normalized input.
|
||||
|
||||
There is no `package.json`; ACDL is bash + python stubs. The verification gate
|
||||
substitutes `bash -n` and `python -m py_compile` for `npm run typecheck`, and
|
||||
per-phase `scripts/verify_phaseNN.sh` for `npm test`. `npm run build` is a
|
||||
no-op (no build step). See PERSONAS.md / VERIFICATION note.
|
||||
### Contract schema (§7)
|
||||
Central repo + generated client libraries. Strict fail-fast at schema
|
||||
stage, multi-stage validation with reason codes from a published
|
||||
vocabulary. **W3.E:** per-env mandatory inputs —
|
||||
- dev: `stack`, `environment`
|
||||
- qa adds: `validation.e2eSuite`, `validation.loadTest`
|
||||
- prod adds: `runbook`, `dashboard`, `oncall`
|
||||
- dr adds: `drDrillRef`
|
||||
- `inputs` always optional; `profile: agentic` fields optional everywhere.
|
||||
|
||||
## L1 module schema (Phase 02 research)
|
||||
### Confidence signal (§8)
|
||||
Six canonical inputs, weighted sum with per-input breakdown. Per-env
|
||||
thresholds: dev ≥ 0.50, qa ≥ 0.75, prod ≥ 0.90, dr ≥ 0.95. Structured output
|
||||
`{ score, band, perInput, reasonCodes }`. 1-year storage, no retraining in
|
||||
v1. Halt with explicit reason on missing input.
|
||||
|
||||
Each L1 module lives at `modules/l1/<name>/` with exactly two files:
|
||||
Policy input = list of `PolicyCheckResult` records (engine-agnostic).
|
||||
Severity → penalty: 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.
|
||||
|
||||
- `manifest.yaml` — declares the L1's identity + a flat `inputs:` map.
|
||||
Schema (D-017):
|
||||
```yaml
|
||||
name: l1-eks-fargate # matches the folder name
|
||||
kind: l1 # literal "l1"; substrate-agnostic
|
||||
description: <one-line>
|
||||
inputs:
|
||||
<key>:
|
||||
description: <one-line>
|
||||
type: string # only "string" allowed (flat, max-depth-1)
|
||||
```
|
||||
- `mock_apply.sh` — uniform stub per D-007 + D-018:
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
echo "[L1: <name>] applying..."
|
||||
sleep 1
|
||||
echo "[L1: <name>] OK"
|
||||
exit 0
|
||||
```
|
||||
`mock_apply.sh` does NOT read input values; the manifest is for traceability
|
||||
and for Phase 03's `mock_executor.sh` to enumerate the L1s in an L2.
|
||||
**BA.B:** thresholds frozen for v1; tuning begins v1.2 (quarterly FP/FN
|
||||
tracking; override = Infra & Ops + SRE joint sign-off, itself a
|
||||
confidence-event).
|
||||
|
||||
### L1 list (fixed per REQ-02 / D-019)
|
||||
### Audit and evidence stream (§9)
|
||||
Tiered ledger: **S3 with Object Lock in compliance mode** (cold, source of
|
||||
truth, 7-year retention) + **GitHub audit repo** (`acdl-evidence`, 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: local durable outbox + async worker.
|
||||
|
||||
| Folder | Description |
|
||||
|--------|-------------|
|
||||
| `l1-eks-fargate` | Serverless container compute substrate |
|
||||
| `l1-iam-role` | Identity and access role primitive |
|
||||
| `l1-lambda` | Event-driven function primitive |
|
||||
| `l1-api-gateway` | HTTP routing primitive |
|
||||
| `l1-eventbridge` | Event bus primitive |
|
||||
| `l1-sqs` | Queue primitive |
|
||||
| `l1-s3` | Object store primitive |
|
||||
| `l1-cloudwatch` | Observability primitive |
|
||||
Outbox database = **DynamoDB**. RPO = 0 (synchronous write to local outbox
|
||||
before contract submission ack); RTO = async worker's dead-letter recovery.
|
||||
Single-region in v1. The outbox also stores per-contract QA and prod
|
||||
approver identities (the only durable record outside GitHub's audit log).
|
||||
|
||||
L1 modules are single-purpose, substrate-agnostic, max-depth-1 (per
|
||||
PROJECT.md Constraints). They do not compose with other L1s.
|
||||
### Human-in-the-Loop mechanics (§10)
|
||||
Pre-execution gates. qa, prod, dr are PR-based attestation gates backed by
|
||||
GitHub Environments with required reviewers. No partial deployment to roll
|
||||
back on rejection (qa, prod); dr is a separate GitHub Deployment against a
|
||||
separate cluster/region.
|
||||
|
||||
Reviewer routing: GitHub CODEOWNERS + Environment required reviewers
|
||||
(qa → QA; prod → SRE; dr → SRE). CODEOWNERS routes, does not enforce
|
||||
identity distinctness.
|
||||
|
||||
**Separation of duties** (platform-internal, not GitHub-native, not Kyverno
|
||||
in v1): on dev→qa promotion the platform writes the QA approver's GitHub
|
||||
identity to the DynamoDB outbox keyed by `contractId`; on qa→prod it reads
|
||||
the stored QA approver and the new SRE approver; if equal, it blocks, emits
|
||||
`SEPARATION_OF_DUTIES_VIOLATION`, and routes a halt artifact to SRE on-call.
|
||||
|
||||
Full 8-concern attestation matrix (functional, performance, security
|
||||
posture, contract NFRs, operational readiness, incident response,
|
||||
capacity/cost, resilience) — see `docs/architecture.md` §10.4.
|
||||
|
||||
Timeout: 1 business day = warn + escalate; 2 business days = auto-freeze +
|
||||
re-submit (linked via `supersedes`). Rejection returns the contract to HELD;
|
||||
the audit chain is extended, not torn up.
|
||||
|
||||
### Agentic stack (§11)
|
||||
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; the
|
||||
platform does not run the skill. Stateless agents, all state in the
|
||||
platform. Skills are reviewed for sensitive data before release (Infra &
|
||||
Ops owns the review; it is the mandatory release gate).
|
||||
|
||||
### Angine execution (§12) — the binding constraint
|
||||
**Target Stack IR** (locked): a engine-neutral description of resources
|
||||
(typed inputs/outputs/NFRs), relationships (single parent per child),
|
||||
composition (tree, max depth 5), and policy hooks. The L1 registry, L2
|
||||
thin-composition tree, contract YML, and PolicyCheckResult schema are all
|
||||
defined against the IR — none against any specific engine.
|
||||
|
||||
**Angine adapters** are the only engine-specific code. An adapter
|
||||
compiles the IR into a engine execution plan. **v1 ships exactly one
|
||||
adapter: the Terraform adapter.** v2+ may add OpenTofu, Pulumi, K8s CRDs
|
||||
without architectural change.
|
||||
|
||||
v1 reality: the IR is shaped to round-trip cleanly to Terraform (nearly
|
||||
isomorphic). As more adapters appear, the IR gets more expressive and the
|
||||
adapters gain translation logic; the L1 content, the YML standard, and the
|
||||
thin-composition tree do not change.
|
||||
|
||||
**Terraform adapter (v1):** translates IR-typed L1 interface → Terraform
|
||||
`variable`/`output` blocks; IR-typed L2 thin-composition tree → Terraform
|
||||
root module; IR-typed relationships → module references; emits a
|
||||
`terraform plan` from the IR. The adapter is a thin layer; it does not own
|
||||
L1/L2 content.
|
||||
|
||||
State storage: S3 (state) + DynamoDB (locking), cloud-managed,
|
||||
single-region in v1.
|
||||
|
||||
Policy toolchain: **Checkov** for Terraform plan policy (the L2 checks +
|
||||
tag/naming); **Kyverno** for K8s-native/platform-internal policy; **OPA**
|
||||
reserved for cross-resource cases, explicitly last resort.
|
||||
|
||||
**Policy result normalization (§12.6):** the confidence signal consumes a
|
||||
normalized `PolicyCheckResult` schema, not raw engine output.
|
||||
|
||||
```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, opaque to the signal..." },
|
||||
"resourceRef": "IR-typed resource identifier"
|
||||
}
|
||||
```
|
||||
|
||||
Execution layer: GitHub/Gitea Actions in the central pipeline repo. State
|
||||
locking via DynamoDB. **AWS credentials via OIDC federation — long-lived
|
||||
credentials are forbidden** (§12.5). The platform does not run
|
||||
`terraform apply` against a developer's workstation; all execution is in
|
||||
the central pipeline.
|
||||
|
||||
Registry maintenance: L1 publication updates the L1 registry in the same
|
||||
PR. The registry is the IR-typed contract, not a Terraform-specific
|
||||
variable schema.
|
||||
|
||||
Contract→IR resolution: the contract declares intent in IR-typed terms;
|
||||
the pipeline resolves it to a target stack (list of L1 instances + inputs +
|
||||
relationships); the Terraform adapter compiles the target stack to a plan.
|
||||
|
||||
## v1.1 spike scope
|
||||
|
||||
The spike (Phases 08–10) materializes the **minimum** that proves the IR
|
||||
commitments hold (no polyglot mess):
|
||||
|
||||
- One L1: `l1-s3` (IR-typed interface; the only AWS resource in the spike).
|
||||
- One L2 thin-composition: `l2-static-assets` (references `l1-s3` only).
|
||||
- Terraform adapter: IR → `terraform plan` against AWS via OIDC.
|
||||
- One contract submission → contract→IR → `terraform plan` → Checkov
|
||||
`PolicyCheckResult` → confidence signal → evidence event to the DynamoDB
|
||||
outbox.
|
||||
- State: S3 + DynamoDB (real AWS, single-region).
|
||||
|
||||
Out of spike scope: full HITL matrix wiring, Kyverno, OPA, MCP skill
|
||||
catalog, GitOps reconciler, multi-region, prod/dr environments, the 5-skill
|
||||
L3B catalog. Those are post-spike (v1.2+) platform build-out.
|
||||
|
||||
## Gitea API surface (carried from v1.0, refined)
|
||||
|
||||
| Capability | Gitea support | ACDL approach (v1.1) |
|
||||
|------------|---------------|----------------------|
|
||||
| Org-scoped repo create | `POST /api/v1/orgs/{org}/repos` | Used for any new repos |
|
||||
| Native Pages | **None** | Serve `acdl-evidence` via raw file URLs (unchanged from v1.0) |
|
||||
| Environments API | **None**; act_runner ignores `environment:` | Model HITL gates via `workflow_dispatch` approval inputs (v1.0 D-013 pattern) — **refined in Phase 07** for the real pre-execution gate model |
|
||||
| `repository_dispatch` | Not supported | Cross-repo trigger via `workflow_dispatch` API (unchanged) |
|
||||
| Reusable workflows | Supported | `acdl/.gitea/workflows/pipeline.yml` via `uses: ...@<ref>` |
|
||||
| `id-token: write` / OIDC | **Not supported** (RESEARCH TARGET 1, conf 0.95). Gitea docs list `id-token` as an unsupported GitHub-only scope; open proposal go-gitea/gitea#33681; draft PR go-gitea/gitea#36988 unmerged. Even Gitea's own CI uses long-lived AWS keys (issue #37980). | **Spike waiver D-039:** per-run-rotated long-lived key (rotated after each run by `scripts/rotate_spike_key.sh`). Real OIDC deferred to v1.2, blocked on PR #36988. |
|
||||
| `actions/configure-aws-credentials` | Unusable without OIDC | Spike uses static AWS creds from a (rotated) Gitea Actions secret via the `aws-actions/configure-aws-credentials@v4` `access-key-id`/`secret-access-key` inputs, or plain `AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY` env vars. v1.2 switches to `role-to-assume` when OIDC lands. |
|
||||
|
||||
### Branch pinning rule (refined for W2.A)
|
||||
|
||||
- Dev/qa contracts reference the reusable workflow by **tag**
|
||||
(`@v1.1-spike`).
|
||||
- Prod-bound workflows reference by **SHA**; the platform CLI
|
||||
(`platform/cli/resolve-tag.ts`, Phase 07) resolves the current tag to its
|
||||
SHA. (Spike scope: the CLI is a stub; the real CLI lands in v1.2.)
|
||||
|
||||
### Verification toolchain
|
||||
|
||||
ACDL has no `package.json`. The verification gate substitutes:
|
||||
- **typecheck:** `terraform validate`, `python3 -m py_compile`, JSON Schema
|
||||
validation (`ajv` or `python -m jsonschema`) against `schemas/`.
|
||||
- **test:** per-phase `scripts/verify_phaseNN.sh` (Phase 06: archive integrity;
|
||||
Phase 07: schema validation + decision-resolution completeness; Phase 08:
|
||||
OIDC assume-role + state backend; Phase 09: IR + L1 + adapter `terraform
|
||||
plan`; Phase 10: end-to-end contract submission).
|
||||
- **build:** `terraform init` (real build for the spike).
|
||||
- See `PERSONAS.md` verification_toolchain.
|
||||
|
||||
## Build order (v1.1)
|
||||
|
||||
1. Phase 06 — archive demo, reorient repo.
|
||||
2. Phase 07 — finalize architecture v1.0; author schemas + designs.
|
||||
3. Phase 08 — AWS OIDC bootstrap (use temp key once, rotate).
|
||||
4. Phase 09 — IR + `l1-s3` + Terraform adapter → `terraform plan`.
|
||||
5. Phase 10 — `l2-static-assets` + contract→IR → end-to-end spike.
|
||||
6. COMPLETE gate — review → ship `v1.2.0` → audit. **DONE.**
|
||||
|
||||
## v1.2 build-out scope
|
||||
|
||||
v1.2 takes the v1.1 spike (dev-only, `plan`-only, single S3 L1) to a real,
|
||||
simpler, better-documented platform that delivers a microservice to AWS ECS
|
||||
Fargate end-to-end. The locked architecture (§1–§12) is unchanged — v1.2
|
||||
extends the *implementation*, not the design.
|
||||
|
||||
### In scope (five axes, user-directed 2026-07-21)
|
||||
|
||||
1. **Re-evaluate the current state.** go-gitea/gitea#36988 (OIDC for Gitea
|
||||
Actions) re-checked 2026-07-21: still **open** (last updated 2026-05-27,
|
||||
not merged). Real OIDC remains deferred to v1.3+; v1.2 extends the D-039
|
||||
per-run-rotated-key waiver as **D-047**. The waiver continues to satisfy
|
||||
§12.5's *intent* (no *persistently* long-lived key): the spike key is
|
||||
rotated after each run by `scripts/rotate_spike_key.sh`, and Phase 12
|
||||
tightens the IAM scoping + rotation hygiene.
|
||||
2. **NFR improvements on the existing spike.** Least-privilege IAM audit of
|
||||
`spike_runner_policy.json`; idempotent `create_state_backend.py` /
|
||||
`create_iam_user.py`; proper exit codes / error handling; P1-1 redaction
|
||||
(two AWS access key IDs in `.ciagent/VERIFY.md` Phase 09 narrative).
|
||||
3. **Streamline / simplify the current setup.** Consolidate
|
||||
`run_spike_plan.sh` + `run_spike_e2e.sh` into one
|
||||
`scripts/run_platform.sh`; remove dead code and stale `platform/` paths.
|
||||
4. **README.md fully up to date on how the platform works.** Reflect v1.1
|
||||
complete; document the actual spike flow, `scripts/run_platform.sh`, the
|
||||
real repo layout, and the v1.2 objective.
|
||||
5. **Bootstrap a consumer repo with a basic microservice deployed to ECS
|
||||
end-to-end.** New Gitea repo `acdl-consumer-microservice` (org
|
||||
`continuous-intelligence`); new IR-typed L1s (`l1-vpc`, `l1-ecs-cluster`,
|
||||
`l1-ecs-service`, `l1-iam-role`, `l1-alb`, `l1-ecr`); new
|
||||
`l2-microservice` thin-composition; one contract submission →
|
||||
`terraform apply` (dev, autonomous per §10, confidence ≥ 0.50) → a live
|
||||
ECS Fargate service serving HTTP 200 → evidence event to the DynamoDB
|
||||
outbox → acdl-evidence timeline.
|
||||
|
||||
### Angine extension (ECS Fargate)
|
||||
|
||||
The Terraform adapter (§12) remains the only engine-specific code. v1.2
|
||||
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
|
||||
`modules-ir/registry.json`) is unchanged — only the set of registered L1s
|
||||
grows. The IR commitments (REQ-28) continue to hold: `modules-ir/`,
|
||||
`schemas/`, `contracts/`, `core/confidence_signal.py`,
|
||||
`core/contract_resolver.py`, `core/outbox_writer.py`
|
||||
remain engine-agnostic.
|
||||
|
||||
### `terraform apply` (dev only)
|
||||
|
||||
v1.2 lifts the engine execution from `plan` to `apply` for the `dev`
|
||||
environment only. Dev is autonomous per §10 (confidence ≥ 0.50, no HITL).
|
||||
`apply` for qa/prod/dr remains HITL-gated and out of scope for v1.2. The
|
||||
apply result (resources created, plan diff) is captured in the evidence
|
||||
stream as a `terraform.apply` event.
|
||||
|
||||
### Out of scope for v1.2 (deferred to v1.3+)
|
||||
|
||||
| Feature | Reason |
|
||||
|---------|--------|
|
||||
| Real OIDC federation | go-gitea/gitea#36988 still open. v1.2 extends D-039 waiver (D-047); real OIDC is v1.3+. |
|
||||
| Full HITL matrix wiring (qa/prod/dr) | v1.2 is dev-only autonomous `apply`; HITL wiring is v1.3. |
|
||||
| Kyverno + OPA policy engines | v1.2 keeps Checkov only; Kyverno/OPA are v1.3. |
|
||||
| MCP skill catalog + real L3B agent | v1.2 keeps the L3B stub; the 5-skill catalog is v1.3. |
|
||||
| Audit ledger build-out (S3 Object Lock + JWS + async worker + DLQ + daily checkpoints) | v1.2 keeps the v1.1 outbox; the regulatory ledger is v1.3. |
|
||||
| Multi-region state / outbox | Single-region in v1 (§9, §12.3); multi-region is v1.3+. |
|
||||
| Prod/dr environments | v1.2 is dev-only; prod/dr are v1.3. |
|
||||
| GitOps reconciler (ArgoCD/Flux) | v1.3+. |
|
||||
|
||||
## Build order (v1.2)
|
||||
|
||||
1. Phase 11 — re-eval #36988 + NFR audit + simplification findings + README rewrite.
|
||||
2. Phase 12 — NFR harden + simplify (idempotent bootstrap, one `run_platform.sh`, IAM audit, redactions).
|
||||
3. Phase 13 — six ECS L1s + adapter `TYPE_MAP` expansion.
|
||||
4. Phase 14 — `l2-microservice` + contract schema extension.
|
||||
5. Phase 15 — consumer repo + `terraform apply` (dev) → live ECS service.
|
||||
6. Phase 16 — capstone e2e: consumer commit → live HTTP 200 → evidence → timeline.
|
||||
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.
|
||||
|
||||
## v1.9 Addendum (2026-07-23)
|
||||
|
||||
### New Components
|
||||
|
||||
- **`core/contract_resolver.py` interpolation** (D-081): the resolver
|
||||
now expands `${env.<field>}` + `${contract.<field>}` tokens
|
||||
post-schema-validation, pre-IR-resolution. The env context is the
|
||||
loaded environment onboarding JSON (`core/environments/<name>.json`,
|
||||
schema `schemas/environment.schema.json`). The resolver's
|
||||
`child_input_map` routes L2 wires to the sub-resource that declares the
|
||||
input (P1-1 — `desired_count` → `aws:ecs:service`, `family` →
|
||||
`aws:ecs:task_definition`).
|
||||
- **`core/environment_check.py` `load()`** (REQ-104): loads + returns the
|
||||
parsed environment JSON; emits a stderr warning for placeholder
|
||||
`account_id` when env != dev.
|
||||
- **`core/hitl_gates.py`** (REQ-108, D-084): the HITL pre-execution
|
||||
attestation gate. Records the approver identity to the DynamoDB outbox
|
||||
(`approver_qa`/`approver_prod`/`approver_dr`), runs the separation-of-
|
||||
duties check on prod, invokes the attestation matrix, returns
|
||||
`(ok, reason)`. Dev skips (autonomous). `run_platform.sh` calls
|
||||
`attest` before apply for qa/prod/dr.
|
||||
- **`core/attestation_matrix.py`** (REQ-109, D-084): the 8-concern
|
||||
attestation matrix from `hitl_matrix_design.md` §10.4. Offline-testable
|
||||
concerns (contract NFRs, schema validity, policy pass) run for real;
|
||||
operator-supplied concerns accept signed evidence artifacts validated
|
||||
for freshness + schema. Signature verification skips when
|
||||
`ACDL_ATTESTATION_SIGNING_KEY_ID` is unset (D-089).
|
||||
- **`core/separation_of_duties.py` `route_halt_artifact`** (REQ-107):
|
||||
real SNS publish (`acdl-sod-halt` topic, ARN from
|
||||
`ACDL_SOD_HALT_TOPIC_ARN`) + outbox fallback
|
||||
(`SEPARATION_OF_DUTIES_VIOLATION` event). The SNS topic is defined in
|
||||
`terraform/platform/main.tf`.
|
||||
- **`adapters/wiz/wiz_adapter.py` `WizClient`** (REQ-110): real GraphQL
|
||||
API client (`<WIZ_API_URL>/graphql`, Bearer auth, pagination via
|
||||
`pageInfo.hasNextPage`). `fetch_and_adapt` translates issues →
|
||||
`PolicyCheckResult`. Graceful degrade when unconfigured.
|
||||
- **`adapters/kyverno/kyverno_adapter.py`** (REQ-111): fleshed-out
|
||||
`PolicyReport` → `PolicyCheckResult` mapping (pass/fail/skip/warn +
|
||||
severity + skip-with-reason + resource construction). Inactive-for-TF
|
||||
guard preserved.
|
||||
|
||||
### Per-Environment Promotion (D-082)
|
||||
|
||||
The deploy workflow (`.github/workflows/deploy.yml` +
|
||||
`.gitea/workflows/deploy.yml`, byte-identical) declares an `environment`
|
||||
`workflow_call` input. When non-empty, `run_platform.sh --environment
|
||||
<name>` overrides the contract's `environment` field before schema
|
||||
validation (D-088). One CI job per environment; promotion = running the
|
||||
matching job, no `environment:` field editing. Per-env contract files
|
||||
(`contracts/<module>.<env>.yaml`) use interpolation for env-specific
|
||||
values.
|
||||
|
||||
### Adapter Parameterization (P1-1, D-085)
|
||||
|
||||
The adapter (`adapters/terraform/adapter.py`) reads ECS/ALB/VPC defaults
|
||||
from L1 `interface.json` inputs (`desired_count`, `launch_type`,
|
||||
`family`, `target_type`, `load_balancer_type`, `name`). The adapter is a
|
||||
thin translator; the `child_input_map` routes wires to the declaring
|
||||
sub-resource.
|
||||
|
||||
### Deferred (D-083)
|
||||
|
||||
S3 Object Lock + JWS detached signatures + async worker + DLQ + daily
|
||||
checkpoints (audit ledger build-out) — deferred to a future milestone.
|
||||
The hash-chain + DynamoDB-outbox path remains the v1.9 production audit
|
||||
record.
|
||||
@@ -0,0 +1,63 @@
|
||||
# ACDL v1.9 — Audit Report
|
||||
|
||||
> Audit date: 2026-07-23. Auditor: ci-debugger. Milestone: v1.9. Result: PASS.
|
||||
|
||||
## Step 1: Reconstruction Test
|
||||
|
||||
- 16 v1.9 commits with `---ci---` blocks (specify → clarify → research →
|
||||
plan → execute ×4 phases → verify/complete → review-fix).
|
||||
- Reconstructed state: milestone v1.9, phase 43, status complete.
|
||||
- Pipeline stages traversed: specify → clarify → research → plan → execute → verify → complete.
|
||||
- Decisions D-080..D-089 all present in git log + `.ciagent/` files.
|
||||
- config.json (v1.9 complete), PROJECT.md (v1.9 complete), REQUIREMENTS.md
|
||||
(v1.9 complete, 12 reqs), ROADMAP.md (v1.9 complete, phases 39–43),
|
||||
REVIEW.md (READY TO SHIP), PERSONAS.md (v1.9), VERIFY.md, AUDIT.md.
|
||||
**PASS.**
|
||||
|
||||
## Step 2: File Discipline
|
||||
|
||||
- `.ciagent/config.json`: valid JSON; mode, projects[] present. **PASS.**
|
||||
- `.ciagent/PROJECT.md`: Vision/Core Value (≡ "What This Is"), Key
|
||||
Decisions (v1.9 D-080..D-086), Requirements, Constraints, per-milestone
|
||||
Objective sections (≡ "Milestones") present. Section names follow the
|
||||
v1.0 established conventions (not the generic audit template). **PASS.**
|
||||
- `.ciagent/ROADMAP.md`: phases 39–43 present; all marked complete.
|
||||
**PASS.**
|
||||
- `.ciagent/REQUIREMENTS.md`: v1.9 traceability table complete (12/12
|
||||
REQ-100..111 marked `complete (v1.9.0)`). **PASS.**
|
||||
- `.ciagent/ARCHITECTURE.md`: **fixed during audit** — v1.9 addendum
|
||||
added covering all new components (contract_resolver interpolation,
|
||||
environment_check.load, hitl_gates, attestation_matrix,
|
||||
separation_of_duties.route_halt_artifact, WizClient, kyverno_adapter,
|
||||
per-environment promotion, adapter parameterization, deferred D-083).
|
||||
All 9 v1.9 code components now referenced. **PASS (after fix).**
|
||||
|
||||
## Step 3: Branch Hygiene
|
||||
|
||||
- Local: `main` only. Remote: `origin/main` only.
|
||||
- No phase or milestone branches remain (all 5 v1.9 phase branches merged
|
||||
+ pruned during the run/ship workflow).
|
||||
- No orphan branches.
|
||||
**PASS.**
|
||||
|
||||
## Step 4: Commit Discipline
|
||||
|
||||
- 16/16 v1.9 commits have `---ci---` blocks with project/phase/milestone/
|
||||
status fields.
|
||||
- No stale implementation decisions (D-081..D-085, D-087..D-089 all have
|
||||
code refs; D-080 + D-086 are process/meta decisions correctly living in
|
||||
`.ciagent/` files).
|
||||
- No unresolved v1.9 escalations (the 3 `audit(...)` commits in history
|
||||
are from prior milestones v1.0/v1.6/v1.7).
|
||||
**PASS.**
|
||||
|
||||
## Issues fixed during audit
|
||||
|
||||
1. **ARCHITECTURE.md missing v1.9 addendum** — the architecture doc had
|
||||
no coverage of the v1.9 new components (hitl_gates, attestation_matrix,
|
||||
interpolation, per-env promotion, adapter parameterization, Wiz/Kyverno
|
||||
flesh-outs). Fixed: added a v1.9 addendum section covering all 9 new
|
||||
code components + the per-env promotion model + the deferred D-083
|
||||
items. Verified all 9 components now referenced.
|
||||
|
||||
## Audit result: PASS
|
||||
+96
-44
@@ -1,21 +1,22 @@
|
||||
---
|
||||
project: acdl
|
||||
milestone: v1.0
|
||||
generated_at: 2026-07-21
|
||||
milestone: v1.9
|
||||
generated_at: 2026-07-23
|
||||
generator: lead-developer
|
||||
verification_toolchain:
|
||||
typecheck: "bash -n scripts/**/*.sh modules/**/*.sh && python3 -m py_compile scripts/**/*.py"
|
||||
typecheck: "terraform validate && python3 -m py_compile core/**/*.py && python3 -m jsonschema schemas/*.schema.json"
|
||||
test: "scripts/verify_phaseNN.sh"
|
||||
build: "no-op (no build step; bash + python stubs)"
|
||||
build: "terraform init"
|
||||
note: |
|
||||
ACDL has no package.json. The execute/verify/ship workflows substitute
|
||||
bash -n and python -m py_compile for npm run typecheck, a per-phase
|
||||
verify script for npm test, and treat npm run build as a no-op. This
|
||||
override is documented here as the single source of truth; the ci-*
|
||||
agents read PERSONAS.md before running verification commands.
|
||||
`terraform validate` + `python -m py_compile` + JSON Schema validation
|
||||
(`python -m jsonschema` or `ajv`) for npm run typecheck, a per-phase
|
||||
verify script for npm test, and `terraform init` for npm run build.
|
||||
This override is documented here as the single source of truth; the
|
||||
ci-* agents read PERSONAS.md before running verification commands.
|
||||
---
|
||||
|
||||
# ACDL — Persona Roster (project-level)
|
||||
# ACDL — Persona Roster (project-level, v1.9)
|
||||
|
||||
## Active personas
|
||||
|
||||
@@ -24,68 +25,119 @@ verification_toolchain:
|
||||
- **Active:** true
|
||||
- **Phase-specific:** false
|
||||
- **Frameworks:** (none)
|
||||
- **Constraints:** pragmatic, battle-tested defaults, no-cross-territory-edits
|
||||
- **Territory:** `.ciagent/**`, `scripts/verify_phase*.sh`, `README.md`, `.gitignore`
|
||||
- **Reason:** Owns CIAgent metadata and cross-phase verification scripts.
|
||||
- **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`
|
||||
- **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
|
||||
- **Domain:** backend
|
||||
- **Active:** true
|
||||
- **Phase-specific:** false
|
||||
- **Frameworks:** gitea-actions, act_runner, bash, python, yaml
|
||||
- **Constraints:** no-cloud, no-ai, stub-only, hash-chain-must-be-deterministic, max-depth-5
|
||||
- **Territory:** `.gitea/workflows/**`, `scripts/**` (except `scripts/verify_phase*.sh`), `modules/l2/**/manifest.yaml`
|
||||
- **Reason:** Owns workflow YAML, core scripts (mock_executor, policy_checker, confidence_signal, evidence_writer, l3b_agent_stub), and L2 composition manifests.
|
||||
- **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
|
||||
- **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, 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).
|
||||
|
||||
### infra-stub-engineer (custom)
|
||||
- **Domain:** backend
|
||||
### platform-engineer (custom)
|
||||
- **Domain:** infra
|
||||
- **Active:** true
|
||||
- **Phase-specific:** false
|
||||
- **Frameworks:** bash, yaml
|
||||
- **Constraints:** mock-only, echo-contract-from-D-007, sleep-1s-exit-0, substrate-agnostic, single-purpose
|
||||
- **Territory:** `modules/l1/**`
|
||||
- **Reason:** Created to own L1 stub modules (Phase 02) and their uniform mock_apply.sh behavior per D-007. Domain is backend (bash stubs) but territory is strictly L1 modules to keep L1/L2 concerns separated from workflow YAML.
|
||||
- **Frameworks:** terraform, aws-iam, aws-s3, aws-dynamodb, aws-lambda, aws-cloudfront, aws-waf, aws-ssm, aws-secretsmanager, oidc, json-schema
|
||||
- **Constraints:** ir-is-engine-agnostic, adapter-is-only-engine-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/**` (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 (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 engine-agnostic; the adapter is the only engine-specific code (the binding constraint per §12).
|
||||
|
||||
### security-engineer (custom)
|
||||
- **Domain:** security
|
||||
- **Active:** true
|
||||
- **Phase-specific:** false
|
||||
- **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, required-tags-enforced
|
||||
- **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, 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.9)
|
||||
- **Domain:** serverless
|
||||
- **Active:** true
|
||||
- **Phase-specific:** true (reactivated for v1.9; removed after milestone COMPLETE)
|
||||
- **Frameworks:** python, aws-lambda, boto3, dynamodb, aws-secretsmanager, aws-sns, 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, sns-topic-arn-from-env
|
||||
- **Territory:** `core/lambda/**` (contract_ingestor.py + handler), `terraform/platform/main.tf` (Lambda + Function URL + DynamoDB + KMS + Secrets Manager + IAM + acdl-change-requests table + acdl-sod-halt SNS topic), `terraform/platform/consumer_invoke_policy.json`, `terraform/platform/variables.tf`
|
||||
- **Reason:** Reactivated for v1.9 Phase 42 (acdl-sod-halt SNS topic for `route_halt_artifact`, defined in `terraform/platform/main.tf`). 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
|
||||
- **Domain:** frontend
|
||||
- **Active:** true
|
||||
- **Phase-specific:** false
|
||||
- **Frameworks:** vanilla-js, dom-api, fetch-api
|
||||
- **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`)
|
||||
- **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
|
||||
|
||||
### infra-stub-engineer (custom, v1.0 only)
|
||||
- **Domain:** backend
|
||||
- **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/**`, now `modules/l1/**`) are owned by platform-engineer (engine-agnostic IR + Terraform adapter). The stub engineer is no longer needed.
|
||||
- **Phase-specific:** false (was v1.0)
|
||||
- **Territory (would have been):** `demo/modules/l1/**`
|
||||
|
||||
### data-engineer
|
||||
- **Domain:** data
|
||||
- **Active:** false
|
||||
- **Reason:** No persistence layer. ACDL state is flat JSON files (`audit.json`, `state.json`) written by bash/python scripts; no ORM, no migrations, no DB. Schema contracts live in `manifest.yaml` (owned by backend-engineer / infra-stub-engineer).
|
||||
- **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
|
||||
- **Frameworks:** (would have been: drizzle, prisma)
|
||||
- **Constraints:** (would have been: schema-first, type-safe-orm)
|
||||
- **Territory:** (would have been: `**/db/**`, `**/migrations/**`)
|
||||
|
||||
### frontend-engineer
|
||||
- **Domain:** frontend
|
||||
- **Active:** false
|
||||
- **Reason:** No UI in Phases 01-04. The single UI artifact (`index.html`, vanilla JS) is built in Phase 05. frontend-engineer is reactivated for Phase 05 only (see Phase-specific overrides below).
|
||||
- **Phase-specific:** true (reactivates in Phase 05)
|
||||
- **Frameworks:** vanilla-js, dom-api, fetch-api
|
||||
- **Constraints:** no-frameworks, single-file, fetch-from-same-origin-raw-url
|
||||
- **Territory:** `acdl-evidence/index.html` (Phase 05)
|
||||
|
||||
## Phase-specific overrides
|
||||
|
||||
| Phase | Personas active | Reactivations / notes |
|
||||
|-------|-----------------|----------------------|
|
||||
| 01 repo-scaffolding | lead-developer, backend-engineer | infra-stub-engineer idle (no L1 work this phase) |
|
||||
| 02 l1-modules | lead-developer, backend-engineer, infra-stub-engineer | infra-stub-engineer owns L1 stubs |
|
||||
| 03 l2-modules-and-core-scripts | lead-developer, backend-engineer, infra-stub-engineer | backend-engineer owns core scripts + L2 manifests; infra-stub-engineer only updates L1 manifests if referenced |
|
||||
| 04 pipeline-and-approval-gates | lead-developer, backend-engineer | infra-stub-engineer idle; frontend-engineer still off |
|
||||
| 05 evidence-ui-and-demo-dry-run | lead-developer, backend-engineer, frontend-engineer | frontend-engineer REACTIVATED for `index.html` only; backend-engineer owns the dry-run script and audit.json wiring |
|
||||
| Phase | Personas active | Notes |
|
||||
|-------|------------------|-------|
|
||||
| 28 adapter-waf-and-resolver-outputs | platform-engineer (lead: WAF HCL fix + adapter output blocks), backend-engineer (resolver outputs processing) | security/lambda/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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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) | — |
|
||||
| 39 design-doc-refresh-and-p1-1-parameterization | security-engineer (lead: hitl_matrix_design.md + audit_ledger_design.md refresh), platform-engineer (lead: P1-1 adapter defaults → L1 interface.json inputs), backend-engineer (contract_resolver.py + env schema adjacent review) | lambda/frontend idle |
|
||||
| 40 contract-interpolation | backend-engineer (lead: _expand_vars in contract_resolver.py + environment.schema.json + sample contracts), platform-engineer (interface.json adjacent review) | security/lambda/frontend idle |
|
||||
| 41 per-environment-ci-jobs | backend-engineer (lead: deploy.yml environment input + run_platform.sh --environment + per-env contracts + caller-workflow docs), security-engineer (HITL gate structure review) | platform/lambda/frontend idle |
|
||||
| 42 stub-implementation | security-engineer (lead: route_halt_artifact SNS + hitl_gates.py + attestation_matrix.py + Wiz real client + Kyverno fleshed out), backend-engineer (run_platform.sh HITL gate wiring), lambda-engineer (acdl-sod-halt SNS topic in terraform/platform/main.tf) | platform/frontend idle |
|
||||
| 43 verify-review-audit-complete | lead-developer (lead: 4-layer verify + review + audit + milestone completion), all personas (review participation) | — |
|
||||
|
||||
## Domain priority (used by TaskDecomposer)
|
||||
|
||||
`coordination -> backend -> infra-stub-engineer -> frontend-engineer (Phase 05 only)`
|
||||
`coordination → security → platform → backend → lambda → frontend`
|
||||
|
||||
Rationale: in v1.9, the security commitments (HITL gates, attestation
|
||||
matrix, SoD halt artifact, Wiz/Kyverno adapters) and the design-doc
|
||||
accuracy are the binding constraints; platform owns the P1-1 adapter
|
||||
parameterization + L1 interface inputs; backend owns the contract
|
||||
interpolation + per-env CI jobs + the deploy workflow env input;
|
||||
lambda owns the SNS topic Terraform; frontend is unchanged from v1.0
|
||||
(evidence timeline).
|
||||
|
||||
## Conflict resolutions (lead-developer arbitration)
|
||||
|
||||
- `backend-engineer` vs `infra-stub-engineer` over `modules/l2/**/manifest.yaml`: backend-engineer owns L2 manifests; infra-stub-engineer owns L1 manifests. No overlap.
|
||||
- `backend-engineer` vs `frontend-engineer` over `acdl-evidence/index.html`: frontend-engineer owns the file in Phase 05; backend-engineer provides the `audit.json` schema contract (event shape) via `evidence_writer.py` and a `SCHEMA.md` note in ARCHITECTURE.md.
|
||||
- `lead-developer` vs any: lead-developer owns `.ciagent/**` and verification scripts; persona engineers do not edit CIAgent metadata.
|
||||
- `backend-engineer` vs `platform-engineer` over `schemas/ir.schema.json` + `schemas/stack.schema.json`: platform-engineer owns the IR (engine-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 `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 + custom rules + the Wiz/Kyverno adapters (policy is a security concern); platform-engineer owns the Terraform adapter (engine 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.
|
||||
|
||||
## Territory enforcement mode
|
||||
|
||||
`warn` — config.json has no `personas.territory_enforcement` field, so the default per execute.md is `warn`. Cross-territory edits are logged in the commit message but do not fail the task.
|
||||
`warn` — config.json has no `personas.territory_enforcement` field, so the
|
||||
default per execute.md is `warn`. Cross-territory edits are logged in the
|
||||
commit message but do not fail the task. v1.7's broad scope means
|
||||
co-authoring across territories is likely (e.g. lambda + platform on
|
||||
`terraform/platform/main.tf`); `warn` keeps it frictionless.
|
||||
+218
-75
@@ -1,85 +1,228 @@
|
||||
---
|
||||
phase: 02
|
||||
name: l1-modules
|
||||
milestone: v1.0
|
||||
milestone_type: feature
|
||||
status: planned
|
||||
requirements: [REQ-02, REQ-03]
|
||||
must_haves:
|
||||
- "All 8 L1 module folders exist under modules/l1/ with the exact names from REQ-02"
|
||||
- "Each L1 has a manifest.yaml matching the schema in ARCHITECTURE.md (name, kind: l1, description, inputs: map of string keys)"
|
||||
- "Each L1 has a mock_apply.sh that echoes '[L1: <name>] applying...', sleeps 1s, echoes '[L1: <name>] OK', exits 0 (D-007)"
|
||||
- "All mock_apply.sh are executable (chmod +x) and bash -n clean"
|
||||
- "All manifest.yaml files parse as valid YAML"
|
||||
- "scripts/verify_phase02.sh passes: enumerates 8 L1s, validates each manifest, runs each mock_apply.sh, confirms exit 0 + expected output"
|
||||
verification:
|
||||
typecheck: "bash -n modules/l1/*/mock_apply.sh scripts/*.sh && python3 -c 'import yaml; [yaml.safe_load(open(f)) for f in glob.glob(\"modules/l1/*/manifest.yaml\")]'"
|
||||
test: "scripts/verify_phase02.sh"
|
||||
build: no-op
|
||||
phase: 39-43
|
||||
name: v1.9-design-doc-interpolation-per-env-ci-stubs-p1-1
|
||||
milestone: v1.9
|
||||
requirements: [REQ-100, REQ-101, REQ-102, REQ-103, REQ-104, REQ-105, REQ-106, REQ-107, REQ-108, REQ-109, REQ-110, REQ-111]
|
||||
type: feat/docs/fix
|
||||
---
|
||||
|
||||
# Phase 02 — l1-modules PLAN
|
||||
# ACDL v1.9 — Phase Plans
|
||||
|
||||
## Goal
|
||||
|
||||
Create the 8 L1 stub modules under `modules/l1/`. Each module has a
|
||||
`manifest.yaml` (declared inputs, flat string map per D-017) and a uniform
|
||||
`mock_apply.sh` (echo + 1s sleep + exit 0 per D-007/D-018). After this phase,
|
||||
Phase 03 can compose L1s into L2 modules and `mock_executor.sh` can iterate
|
||||
over an L2's L1 references.
|
||||
|
||||
## Requirements covered
|
||||
|
||||
- REQ-02: 8 L1 module folders exist (exact names)
|
||||
- REQ-03: each L1 has manifest.yaml + mock_apply.sh with the uniform behavior
|
||||
|
||||
## Waves (vertical slices)
|
||||
|
||||
### Wave 1 — infra-stub-engineer (creates the 8 L1s)
|
||||
|
||||
**Tasks:**
|
||||
|
||||
- **T-2.1** Create `modules/l1/l1-eks-fargate/{manifest.yaml, mock_apply.sh}`
|
||||
- **T-2.2** Create `modules/l1/l1-iam-role/{manifest.yaml, mock_apply.sh}`
|
||||
- **T-2.3** Create `modules/l1/l1-lambda/{manifest.yaml, mock_apply.sh}`
|
||||
- **T-2.4** Create `modules/l1/l1-api-gateway/{manifest.yaml, mock_apply.sh}`
|
||||
- **T-2.5** Create `modules/l1/l1-eventbridge/{manifest.yaml, mock_apply.sh}`
|
||||
- **T-2.6** Create `modules/l1/l1-sqs/{manifest.yaml, mock_apply.sh}`
|
||||
- **T-2.7** Create `modules/l1/l1-s3/{manifest.yaml, mock_apply.sh}`
|
||||
- **T-2.8** Create `modules/l1/l1-cloudwatch/{manifest.yaml, mock_apply.sh}`
|
||||
|
||||
Each L1's `manifest.yaml` declares 1-3 plausible inputs for that primitive
|
||||
(e.g., `l1-s3` declares `bucket_name`, `region`, `retention_days`; `l1-iam-role`
|
||||
declares `role_name`, `trust_policy`). Each `mock_apply.sh` follows the exact
|
||||
uniform template from ARCHITECTURE.md.
|
||||
|
||||
**Files owned (territory):** `modules/l1/**`
|
||||
|
||||
**Commits:** one per task, `---ci---` block has `phase: 2, status: plan-as-execute, persona: infra-stub-engineer, task: T-2.x, requirements.covered: [REQ-02, REQ-03]`.
|
||||
|
||||
### Wave 2 — lead-developer (verification script + traceability)
|
||||
|
||||
**Tasks:**
|
||||
|
||||
- **T-2.9** Create `scripts/verify_phase02.sh`. It:
|
||||
1. Enumerates `modules/l1/*/` and confirms exactly 8 folders with the 8 expected names.
|
||||
2. For each L1: confirms `manifest.yaml` exists and parses as YAML with `name` matching the folder, `kind: l1`, and an `inputs:` map.
|
||||
3. For each L1: confirms `mock_apply.sh` is executable, `bash -n` clean, runs in <2s, exits 0, and its stdout contains the `[L1: <name>] applying...` and `[L1: <name>] OK` markers.
|
||||
4. Prints a PASS/FAIL summary; exits 0 on full success.
|
||||
- **T-2.10** Update `.ciagent/REQUIREMENTS.md` (REQ-02/03 → covered pending VERIFY) and `.ciagent/ROADMAP.md` (Phase 02 → executing). No README change.
|
||||
|
||||
**Files owned (territory):** `scripts/verify_phase02.sh`, `.ciagent/REQUIREMENTS.md`, `.ciagent/ROADMAP.md`
|
||||
|
||||
**Commits:** one per task, `---ci---` block has `phase: 2, status: plan-as-execute, persona: lead-developer, task: T-2.9/2.10`.
|
||||
> Milestone v1.9. Generated at PLAN stage. Autonomy: full.
|
||||
> Requirements: REQ-100..REQ-111 (see REQUIREMENTS.md).
|
||||
> Decisions: D-080..D-089 (see PROJECT.md + RESEARCH.md RA section).
|
||||
> Versioning: feature milestone — progressive patch versions per phase
|
||||
> (v1.8.1..v1.8.5), tag `v1.9.0` at milestone COMPLETE.
|
||||
|
||||
## Wave ordering
|
||||
|
||||
- Wave 1 (infra-stub-engineer) creates all 8 L1s. A single subagent gets all 8 tasks; it commits per task.
|
||||
- Wave 2 (lead-developer) adds the verify script and traceability after the L1s exist.
|
||||
- **Wave 1 (parallel, 2 tasks):** Phase 39 — design-doc refresh (security-engineer) + P1-1 adapter parameterization (platform-engineer). Disjoint file sets; no merge conflict.
|
||||
- **Wave 2 (sequential):** Phase 40 — contract interpolation. Depends on Phase 39's design-doc context (lightweight).
|
||||
- **Wave 3 (sequential):** Phase 41 — per-env CI jobs. Depends on Phase 40's interpolation + env schema.
|
||||
- **Wave 4 (sequential):** Phase 42 — stub implementation. Depends on Phase 41's HITL job structure.
|
||||
- **Wave 5 (sequential):** Phase 43 — verify + review + audit + complete.
|
||||
|
||||
`backend-engineer`, `data-engineer`, `frontend-engineer` have 0 tasks this phase.
|
||||
---
|
||||
|
||||
## Dependencies
|
||||
## Phase 39 — design-doc-refresh-and-p1-1-parameterization
|
||||
|
||||
- Depends on Phase 01 (the `modules/l1/.gitkeep` from T-1.1 is replaced by real folders).
|
||||
- Phase 03 depends on this phase for L1 references in L2 compositions.
|
||||
**Requirements:** REQ-100, REQ-101, REQ-102
|
||||
**Personas:** security-engineer (lead: design docs), platform-engineer (lead: P1-1), backend-engineer (review)
|
||||
**Branch:** `phase/39-design-doc-refresh-and-p1-1`
|
||||
|
||||
### Task 39.1 — Refresh hitl_matrix_design.md (REQ-100, security-engineer)
|
||||
- Rewrite the status block: "v1.2 wires the gates" → "v1.9 wires the gates (Phase 42)".
|
||||
- Update "Spike scope note" → "v1.9 scope note": qa/prod/dr now exercised (Phase 41 wires the job structure; Phase 42 wires the attestation gates); dev remains autonomous.
|
||||
- Update §10.4 matrix: mark the offline-testable concerns (contract NFRs, schema validity, policy pass) as **implemented in v1.9** (`core/attestation_matrix.py`); mark operator-supplied concerns as **accept signed evidence artifacts** (D-084).
|
||||
- Add a "v1.9 wiring" section: cross-reference Phase 41's per-env jobs + Phase 42's `hitl_gates.py` + `attestation_matrix.py` + the outbox-based SoD check.
|
||||
- Preserve D-042 (approver identity = `gitea.actor` / `github.actor`) — still accurate.
|
||||
- Verify: `grep -i "dev-only spike" core/hitl_matrix_design.md` returns 0 hits; `grep -i "v1.2 wires" core/hitl_matrix_design.md` returns 0 hits.
|
||||
|
||||
### Task 39.2 — Refresh audit_ledger_design.md (REQ-101, security-engineer)
|
||||
- Mark the "Spike scope (D-041)" section as **shipped + production since v1.8** (hash chain + DynamoDB outbox + `acdl-evidence` mirror).
|
||||
- Move the "v1.2 build-out" section (S3 Object Lock + JWS + async worker + DLQ + daily checkpoints) under a clearly-labeled "**Deferred to a future milestone (D-083)**" heading. Keep the content (it's the design for when it ships) but mark it not-v1.9.
|
||||
- Update the RPO/RTO table: spike row → "v1.8+ (production): RPO=0 (sync outbox), RTO=workflow re-run"; v1.2 row → "Future milestone (D-083): RPO=0, RTO=DLQ replay".
|
||||
- Update the outbox item shape: note `approver_qa`/`approver_prod`/`approver_dr` are populated by v1.9's `hitl_gates.attest` (Phase 42).
|
||||
- Verify: `grep -i "Phases 08-10 implement" core/audit_ledger_design.md` returns 0 hits; the deferred section is clearly labeled.
|
||||
|
||||
### Task 39.3 — P1-1 adapter parameterization (REQ-102, platform-engineer)
|
||||
- `modules/l1/ecs-service/interface.json`: add inputs `desired_count` (integer, default 1), `launch_type` (string, default "FARGATE"), `family` (string, default "app").
|
||||
- `modules/l1/alb/interface.json`: add inputs `load_balancer_type` (string, default "application"), `target_type` (string, default "ip").
|
||||
- `modules/l1/vpc/interface.json`: add input `name` (string, default "app") for the VPC/IGW/RT `Name` tag prefix.
|
||||
- `adapters/terraform/adapter.py`: change hardcoded defaults to `inputs.get("<name>", "<default>")` where the default matches the interface default (safety fallback; the resolver populates from the interface). Remove the hardcoded `Name = "acdl-microservice-rt"` (line 283) → use `inputs.get("name", "app")`-derived tag.
|
||||
- Preserve the v1.1 S3 regression (S3 has none of these inputs → no change).
|
||||
- Tests: `tests/test_p1_1_adapter_parameterization.py` — (a) `desired_count: 3` in contract inputs emits `desired_count = 3`; (b) absent `desired_count` emits `desired_count = 1` via interface default; (c) `target_type: "instance"` emits `target_type = "instance"`; (d) v1.1 S3 regression still passes (byte-identical `main.tf`).
|
||||
- Verify: `pytest tests/test_p1_1_adapter_parameterization.py` passes; `run_platform.sh --check-only` exits 0; `pytest` total count increases; v1.1 S3 regression test passes.
|
||||
|
||||
### Task 39.4 — Design doc test (REQ-100/101, backend-engineer)
|
||||
- `tests/test_design_docs_current.py`: assert (a) no stale "dev-only spike" / "v1.2 wires the gates" / "Phases 08-10 implement" framing in either design doc; (b) `audit_ledger_design.md` has a "Deferred to a future milestone" section referencing D-083; (c) `hitl_matrix_design.md` references the v1.9 implementation (`attestation_matrix.py`, `hitl_gates.py`).
|
||||
- Verify: `pytest tests/test_design_docs_current.py` passes.
|
||||
|
||||
### Must-haves (Phase 39)
|
||||
- [ ] `core/hitl_matrix_design.md` refreshed (no stale framing).
|
||||
- [ ] `core/audit_ledger_design.md` refreshed (S3 Object Lock marked deferred D-083).
|
||||
- [ ] Adapter has no hardcoded ECS/ALB/VPC defaults (read from inputs).
|
||||
- [ ] `tests/test_p1_1_adapter_parameterization.py` + `tests/test_design_docs_current.py` pass.
|
||||
- [ ] `run_ci.sh` exits 0; `run_platform.sh --check-only` exits 0; v1.1 S3 regression passes.
|
||||
|
||||
---
|
||||
|
||||
## Phase 40 — contract-interpolation
|
||||
|
||||
**Requirements:** REQ-103, REQ-104
|
||||
**Personas:** backend-engineer (lead), platform-engineer (review)
|
||||
**Branch:** `phase/40-contract-interpolation`
|
||||
|
||||
### Task 40.1 — Environment JSON schema (REQ-104, backend-engineer)
|
||||
- `schemas/environment.schema.json` (draft 2020-12): required `name` (string), `account_id` (string), `region` (string), `state_backend` (object: `bucket`, `lock_table`), `network` (object: `vpc_cidr`, `azs` array), `runner_role_arn` (string), `autonomy` (enum: full/attested), `confidence_threshold` (number).
|
||||
- `core/environments/dev.json` validates against it.
|
||||
- Add `core/environments/qa.json`, `prod.json`, `dr.json`: `account_id: "000000000000"`, `autonomy: "attested"`, `confidence_threshold` 0.75/0.90/0.95, regions us-east-1, state_backend buckets `acdl-qa-state`/`acdl-prod-state`/`acdl-dr-state`.
|
||||
- `core/environment_check.py`: add `load(env_name, root=None)` returning the parsed env dict; `check()` stays. Add a stderr warning when `account_id == "000000000000"` and `env_name != "dev"` (prompts real binding).
|
||||
- `tests/test_environment_schema.py`: all 4 env files validate; `load("dev")` returns the dict; warning emitted for qa/prod/dr placeholders.
|
||||
- Verify: `pytest tests/test_environment_schema.py` passes.
|
||||
|
||||
### Task 40.2 — Interpolation expansion in the resolver (REQ-103, backend-engineer)
|
||||
- `core/contract_resolver.py`: add `_expand_vars(value, context)` — recursively walks dicts/lists/strings; replaces `${env.<dotted.path>}` and `${contract.<dotted.path>}` tokens by looking up the dotted path in the context dict. Unknown token → `ValueError(f"unresolved interpolation token: {token}")`.
|
||||
- `resolve()`: after schema validation, load the env via `environment_check.load(contract["environment"])`, build `context = {"env": env, "contract": contract}`, expand all string values in `contract["inputs"]` (recursively, per D-087), then proceed to IR resolution.
|
||||
- The expansion is post-schema-validation (schema sees the raw tokens, which are valid strings) and pre-IR-resolution (the resolver sees concrete values).
|
||||
- `tests/test_interpolation.py`: (a) `${env.region}` expands to `us-east-1`; (b) `${env.state_backend.bucket}` expands to `acdl-dev-state`; (c) `${contract.module}` expands to `static-assets`; (d) unknown token raises `ValueError`; (e) nested map value `env: { DB_URL: "acdl-${env.environment}-db" }` expands recursively; (f) `resolve("contracts/static-assets.yaml")` succeeds with expanded values.
|
||||
- Verify: `pytest tests/test_interpolation.py` passes.
|
||||
|
||||
### Task 40.3 — Sample contracts use naming patterns (REQ-103, backend-engineer)
|
||||
- `contracts/static-assets.yaml`: `bucket_name: acdl-${env.environment}-${contract.module}-${env.account_id}-${env.region}` (the naming pattern the requirement calls out: region + account id + environment).
|
||||
- `contracts/microservice.yaml`: same pattern for `bucket_name`.
|
||||
- Keep `region: us-east-1` as a literal (or `${env.region}` — both valid; use `${env.region}` to demonstrate).
|
||||
- `tests/test_sample_contracts_interpolate.py`: resolving the sample contracts produces concrete bucket names like `acdl-dev-static-assets-000000000000-us-east-1`.
|
||||
- Verify: `pytest tests/test_sample_contracts_interpolate.py` passes; `run_platform.sh --check-only` exits 0 (resolver expands before adapter).
|
||||
|
||||
### Must-haves (Phase 40)
|
||||
- [ ] `schemas/environment.schema.json` exists; 4 env files validate.
|
||||
- [ ] `_expand_vars` in resolver; unknown tokens raise.
|
||||
- [ ] Sample contracts use `${env.*}` + `${contract.*}` naming patterns.
|
||||
- [ ] `tests/test_environment_schema.py` + `tests/test_interpolation.py` + `tests/test_sample_contracts_interpolate.py` pass.
|
||||
- [ ] `run_ci.sh` exits 0; `run_platform.sh --check-only` exits 0.
|
||||
|
||||
---
|
||||
|
||||
## Phase 41 — per-environment-ci-jobs
|
||||
|
||||
**Requirements:** REQ-105, REQ-106
|
||||
**Personas:** backend-engineer (lead), security-engineer (HITL gate review)
|
||||
**Branch:** `phase/41-per-environment-ci-jobs`
|
||||
|
||||
### Task 41.1 — Per-env contract files (REQ-105, backend-engineer)
|
||||
- `contracts/static-assets.dev.yaml`, `.qa.yaml`, `.prod.yaml`, `.dr.yaml` — each sets `environment:` to its own name; `inputs.bucket_name` uses `${env.environment}-${contract.module}-${env.account_id}-${env.region}` interpolation (so the file content is near-identical; only `environment:` differs).
|
||||
- `contracts/microservice.{dev,qa,prod,dr}.yaml` — same pattern.
|
||||
- Keep `contracts/static-assets.yaml` + `contracts/microservice.yaml` as the dev default (backwards compat).
|
||||
- `tests/test_per_env_contracts.py`: all 8 per-env files validate against `schemas/contract.schema.json`; each resolves to a stack with the correct environment.
|
||||
- Verify: `pytest tests/test_per_env_contracts.py` passes.
|
||||
|
||||
### Task 41.2 — Deploy workflow `environment` input (REQ-106, backend-engineer)
|
||||
- `.github/workflows/deploy.yml` + `.gitea/workflows/deploy.yml` (byte-identical): add `environment` input (`type: string`, default `""`, description "Target environment override (dev/qa/prod/dr); when empty, the contract's environment field is used").
|
||||
- `scripts/run_platform.sh`: add `--environment <name>` flag. When set, override the contract's `environment` field at load time (before schema validation per D-088, so interpolation context is consistent). Re-run the onboarding check against the supplied env.
|
||||
- The workflow's "Run the platform pipeline" step passes `--environment ${{ inputs.environment }}` when non-empty.
|
||||
- `tests/test_deploy_workflow_env_input.py`: both deploy workflows declare the `environment` input; byte-identical; `run_platform.sh --environment qa contracts/static-assets.yaml` produces a stack whose env is qa (tested via the resolver directly since run_platform.sh needs AWS for full mode — test the override logic in the resolver).
|
||||
- `core/contract_resolver.py` `resolve()`: accept optional `environment_override` arg; when set, set `contract["environment"] = override` before schema validation + interpolation.
|
||||
- Verify: `pytest tests/test_deploy_workflow_env_input.py` passes; both deploy workflows byte-identical.
|
||||
|
||||
### Task 41.3 — Per-env caller workflow docs + HITL gate structure (REQ-106, security-engineer review)
|
||||
- `docs/CONSUMER_GUIDE.md`: add a "Per-environment deployment" section with 4 caller-workflow examples (`.github/workflows/deploy-dev.yml`, `deploy-qa.yml`, `deploy-prod.yml`, `deploy-dr.yml`), each `uses: acdl/.github/workflows/deploy.yml@v1.9` with `environment: <env>` + `contract: .acdl/<module>.<env>.yaml`. Document: "Promotion = running the matching job; no `environment:` field editing."
|
||||
- HITL gate structure (wired in Phase 42, documented here): qa/prod/dr caller workflows use `workflow_dispatch` with approval inputs (`approve_qa`, `approve_prod`, `approve_dr`) per `hitl_matrix_design.md` D-042; `gitea.actor` / `github.actor` is the approver of record. dev is autonomous (no gate).
|
||||
- `tests/test_consumer_guide_per_env_section.py`: the consumer guide has the per-env section with 4 caller examples.
|
||||
- Verify: `pytest tests/test_consumer_guide_per_env_section.py` passes.
|
||||
|
||||
### Must-haves (Phase 41)
|
||||
- [ ] 8 per-env contract files exist + validate + resolve.
|
||||
- [ ] Deploy workflow has `environment` input (byte-identical Gitea + GitHub).
|
||||
- [ ] `run_platform.sh --environment <name>` overrides; resolver supports `environment_override`.
|
||||
- [ ] Consumer guide documents per-env caller workflows + promotion-without-editing.
|
||||
- [ ] `tests/test_per_env_contracts.py` + `tests/test_deploy_workflow_env_input.py` + `tests/test_consumer_guide_per_env_section.py` pass.
|
||||
- [ ] `run_ci.sh` exits 0; both deploy workflows byte-identical.
|
||||
|
||||
---
|
||||
|
||||
## Phase 42 — stub-implementation
|
||||
|
||||
**Requirements:** REQ-107, REQ-108, REQ-109, REQ-110, REQ-111
|
||||
**Personas:** security-engineer (lead), backend-engineer (run_platform wiring), lambda-engineer (SNS topic Terraform)
|
||||
**Branch:** `phase/42-stub-implementation`
|
||||
|
||||
### Task 42.1 — route_halt_artifact real (REQ-107, security-engineer + lambda-engineer)
|
||||
- `core/separation_of_duties.py` `route_halt_artifact`: when `ACDL_SOD_HALT_TOPIC_ARN` set, publish to SNS via boto3 (`sns.publish(TopicArn=arn, Message=..., Subject="ACDL SoD halt")`); when unset, fall back to structured stderr emission + a `SEPARATION_OF_DUTIES_VIOLATION` event write via `outbox_writer.write_event` (so the halt is in the audit chain). No silent print-only stub.
|
||||
- `terraform/platform/main.tf`: add `aws_sns_topic.acdl-sod-halt` + a basic access policy (allow the platform Lambda / runner role to publish). Output the topic ARN.
|
||||
- `tests/test_route_halt_artifact.py`: (a) with `ACDL_SOD_HALT_TOPIC_ARN` set, moto-mocked SNS receives the publish; (b) without it, a `SEPARATION_OF_DUTIES_VIOLATION` event is written to the outbox (moto-mocked DynamoDB); (c) stderr emission occurs in both cases.
|
||||
- Verify: `pytest tests/test_route_halt_artifact.py` passes.
|
||||
|
||||
### Task 42.2 — HITL attestation gates (REQ-108, security-engineer + backend-engineer)
|
||||
- `core/hitl_gates.py`: `attest(contract_id, env, approver, evidence, outbox_client=None)` → records `approver_qa`/`approver_prod`/`approver_dr` to the outbox item for `contract_id`; runs `separation_of_duties.check(outbox_client, contract_id, approver)` on prod; invokes the attestation matrix (Task 42.3) for the target env; returns `(ok, reason)`. Dev skips (returns `(True, "dev autonomous")`).
|
||||
- `scripts/run_platform.sh`: before apply (for qa/prod/dr), call `hitl_gates.attest` with the approver from `GITHUB_ACTOR`/`GITEA_ACTOR` env. Block on `(ok=False)`.
|
||||
- `tests/test_hitl_gates.py`: (a) dev skips; (b) qa records `approver_qa` (moto outbox); (c) prod records `approver_prod` + SoD blocks when `approver_qa == approver_prod`; (d) prod passes when approvers differ.
|
||||
- Verify: `pytest tests/test_hitl_gates.py` passes.
|
||||
|
||||
### Task 42.3 — 8-concern attestation matrix (REQ-109, security-engineer)
|
||||
- `core/attestation_matrix.py`: `check(env, evidence_bundle)` → runs the 8 concerns. Offline-testable concerns (contract NFRs, schema validity, policy pass) run for real. Operator-supplied concerns accept an uploaded signed evidence artifact (JSON with `timestamp`, `type`, `payload`, optional `signature`); validate freshness (within the declared window from `hitl_matrix_design.md` §10.4) + schema (per-concern). Signature verification via KMS when `ACDL_ATTESTATION_SIGNING_KEY_ID` set; skipped + logged when unset (D-089). Fail loud if missing/expired for prod/dr.
|
||||
- `hitl_gates.attest` calls `attestation_matrix.check(env, evidence)` and blocks on any failing concern.
|
||||
- `tests/test_attestation_matrix.py`: (a) offline concerns pass for a valid contract; (b) operator-supplied concern missing → block for prod; (c) operator-supplied concern present + fresh → pass; (d) expired artifact → block; (e) signature skip when key unset (logged).
|
||||
- Verify: `pytest tests/test_attestation_matrix.py` passes.
|
||||
|
||||
### Task 42.4 — Wiz real API client (REQ-110, security-engineer)
|
||||
- `adapters/wiz/wiz_adapter.py`: add `WizClient` class — `__init__` reads `WIZ_API_TOKEN` + `WIZ_API_URL`; `fetch_issues(filter_by)` queries the Wiz GraphQL API (`<url>/graphql`, Bearer auth, `issues` query). Translate results → `PolicyCheckResult` records (`engine: "wiz"`, `ruleId: <control.name>`, `severity: <lowercased>`, `status: FAIL`, `message: <title>`, `resource: <entity.name>`). Graceful degrade: when `WIZ_API_TOKEN` or `WIZ_API_URL` unset → emit the existing single `SKIPPED` `WIZ_NOT_CONFIGURED` record (no network call). Pagination handled via `pageInfo.hasNextPage`.
|
||||
- `tests/test_wiz_adapter_real_client.py`: (a) with a recorded GraphQL fixture, `WizClient` translates issues → `PolicyCheckResult` records; (b) graceful degrade when env unset; (c) pagination follows `endCursor`.
|
||||
- Verify: `pytest tests/test_wiz_adapter_real_client.py` passes.
|
||||
|
||||
### Task 42.5 — Kyverno translator fleshed out (REQ-111, security-engineer)
|
||||
- `adapters/kyverno/kyverno_adapter.py`: full `PolicyReport` → `PolicyCheckResult` mapping — handle `pass`/`fail`/`skip`/`warn` results, severity mapping (critical/high/medium/low/info), resource extraction, skip-with-reason handling. Keep the inactive-for-Terraform guard (emits a single `SKIPPED` `KYVERNO_INACTIVE_TF_STACK` record when no K8s manifests). Add a `--kube-version` stub (parsed but not yet used — for future GitOps).
|
||||
- `tests/test_kyverno_adapter.py`: expand — (a) `pass` result → `PolicyCheckResult` with `status: PASS`; (b) `fail` with severity → correct severity mapping; (c) `skip` with reason → `SKIPPED` record; (d) inactive-for-TF guard emits the `KYVERNO_INACTIVE_TF_STACK` record.
|
||||
- Verify: `pytest tests/test_kyverno_adapter.py` passes.
|
||||
|
||||
### Must-haves (Phase 42)
|
||||
- [ ] `route_halt_artifact` real (SNS + outbox fallback); SNS topic in Terraform.
|
||||
- [ ] `hitl_gates.py` attests qa/prod/dr; SoD blocks on identity equality.
|
||||
- [ ] `attestation_matrix.py` implements 8 concerns (offline-testable + signed artifacts).
|
||||
- [ ] Wiz adapter real client + graceful degrade.
|
||||
- [ ] Kyverno translator fleshed out + inactive guard preserved.
|
||||
- [ ] All 5 new test files pass; `run_ci.sh` exits 0.
|
||||
|
||||
---
|
||||
|
||||
## Phase 43 — verify-review-audit-complete
|
||||
|
||||
**Requirements:** — (milestone gate)
|
||||
**Personas:** lead-developer (lead), all personas (review participation)
|
||||
**Branch:** `phase/43-verify-review-audit-complete`
|
||||
|
||||
### Task 43.1 — 4-layer verify
|
||||
- Structural: all new files present (environment.schema.json, 4 env files, 8 per-env contracts, hitl_gates.py, attestation_matrix.py, SNS topic in main.tf, 5+ new test files).
|
||||
- Behavioral: `pytest` passes (count increases from v1.8's 350 by ~30+ new tests); `run_ci.sh` exits 0; `run_platform.sh --check-only` exits 0.
|
||||
- Security: no hardcoded adapter defaults; HITL gates block on SoD violation; attestation matrix fails loud on missing evidence for prod/dr; Wiz degrades gracefully.
|
||||
- Quality: each new feature has dedicated tests (interpolation, per-env jobs, SoD, HITL gates, attestation matrix, Wiz, Kyverno).
|
||||
|
||||
### Task 43.2 — Multi-persona review
|
||||
- `ciagent-review` across the v1.9 diff (phases 39–42). Auto-apply P0; flag P1+ for post-hoc.
|
||||
- Reconstruct `.ciagent/REVIEW.md` with v1.9 content (D-086). Note that v1.3–v1.8 reviews were not persisted (no git-history rewrite).
|
||||
|
||||
### Task 43.3 — Audit
|
||||
- Reconstruction: git log matches `.ciagent/` files.
|
||||
- File discipline: all `.ciagent/` files valid.
|
||||
- Branch hygiene: stale branches cleaned.
|
||||
- Commit discipline: all commits have `---ci---` blocks.
|
||||
|
||||
### Task 43.4 — Complete
|
||||
- Update `.ciagent/REQUIREMENTS.md`: mark REQ-100..REQ-111 complete; add v1.9 traceability table.
|
||||
- Update `.ciagent/ROADMAP.md`: add v1.9 milestone section (complete).
|
||||
- Update `.ciagent/PROJECT.md`: v1.9 status → complete.
|
||||
- Tag `v1.9.0`; update floating `v1.9` + `v1` tags.
|
||||
- Bump `uses:`/`ref:` from `@v1.6` → `@v1.9` in `contracts/*.yaml`, `deploy.yml` checkout `ref:`, `docs/CONSUMER_GUIDE.md` (D-071 successor).
|
||||
- Commit: `docs(milestone): complete v1.9`.
|
||||
|
||||
### Must-haves (Phase 43)
|
||||
- [ ] 4-layer verify PASS.
|
||||
- [ ] Review: 0 new P0; P1+ flagged for post-hoc.
|
||||
- [ ] Audit: clean.
|
||||
- [ ] Tag `v1.9.0` created; floating tags updated.
|
||||
- [ ] `uses:`/`ref:` bumped to `@v1.9`.
|
||||
- [ ] REQUIREMENTS.md + ROADMAP.md + PROJECT.md updated.
|
||||
|
||||
---
|
||||
|
||||
*End of PLAN.md.*
|
||||
+684
-61
@@ -2,80 +2,703 @@
|
||||
|
||||
## Vision / Core Value
|
||||
|
||||
A 30-minute executive demo proving that infrastructure can be delivered **automatically, safely, and with a complete audit trail** — without the usual weeks of manual tickets, reviews, and copy-pasted configuration. Because the demo runs entirely on **local stubs** (no AWS/GCP/Azure, no external LLM APIs), it shows intent and safety behavior rather than provisioning real cloud resources.
|
||||
Consumers declare intent; the platform delivers safe production
|
||||
deployment through an agentic stack. The platform absorbs two frictions:
|
||||
the cognitive load of getting the infrastructure right, and the
|
||||
operational work of getting the change to production safely.
|
||||
|
||||
## Objective
|
||||
Source of truth for **why**: `docs/vision.md`.
|
||||
Source of truth for **how**: `docs/architecture.md` + `.ciagent/ARCHITECTURE.md`.
|
||||
Where the two conflict, the vision wins.
|
||||
|
||||
Build a runnable demo (Linux + GitHub/Gitea Actions) that walks executives through four acts:
|
||||
## North Star
|
||||
|
||||
1. **Act 1 — The Friction:** the old manual 2-week deployment process.
|
||||
2. **Act 2 — Developer Self-Service:** commit a valid `contract.yaml` for `l2-commodity-price-feed`, watch Dev auto-run, QA + Prod approval gates, then the evidence timeline.
|
||||
3. **Act 3 — Citizen Developer:** open a GitHub Issue with natural-language intent; the Python keyword parser generates the same `contract.yaml` and triggers the identical pipeline.
|
||||
4. **Act 4 — The Safety Net:** commit a malicious `contract.yaml` (`public-ingress: true`) for `l2-regulatory-reporting`; the pipeline halts in Dev because the confidence signal drops below 0.50, and the rejection is visible on the evidence stream.
|
||||
A merged change progresses through lower environments end-to-end without a
|
||||
platform engineer joining a thread, approving a ticket, or manually
|
||||
triggering a stage gate. A non-technical consumer ships a production
|
||||
deployment by declaring intent — without authoring a workflow, a
|
||||
configuration file, or a Terraform module. Every production change is
|
||||
traceable to a human attestation and an immutable evidence stream.
|
||||
|
||||
## Core Tenets (from `docs/vision.md`)
|
||||
|
||||
1. **Operations are Declared, Not Executed.** Consumers define what they
|
||||
need; the platform reconciles, provisions, and progresses.
|
||||
2. **The Delivery Lifecycle is a Sovereign Boundary.** The platform
|
||||
governs infra and delivery; it does not penetrate upstream product/SDLC.
|
||||
Integration is only through validated, published contracts.
|
||||
3. **Lower Environments are Autonomous; Higher Environments are Attested.**
|
||||
Dev = zero-touch agentic. QA/prod/dr = deliberate human attestation, not
|
||||
rubber stamps.
|
||||
4. **Safety is Computed, Not Assumed.** Every action produces a measurable,
|
||||
explainable confidence signal. The signal is the platform's certified
|
||||
answer to "is this safe to proceed?"
|
||||
5. **Infrastructure is Consumed, Not Maintained.** Compute is abstract,
|
||||
containerized, or serverless. No node/OS/bare-metal lifecycle.
|
||||
6. **Two Consumer Surfaces, One Platform.** Technical developers (L3A) and
|
||||
non-technical consumers (L3B) converge on the same contract schema, the
|
||||
same policy envelope, and the same evidence stream.
|
||||
|
||||
## Domain Boundaries
|
||||
|
||||
- **In scope:** environment progression; cloud resource lifecycle; operational
|
||||
security and observability NFRs; policy enforcement; immutable audit
|
||||
lineage; confidence frameworks; two consumer surfaces (developer + agentic).
|
||||
- **Out of scope:** application business logic; IDE workflows; product
|
||||
backlog / sprint planning; compute requiring node-level or OS-level management.
|
||||
- **Interface:** upstream systems integrate through a strict contract
|
||||
boundary. The platform validates, enriches with operational standards,
|
||||
and reconciles the target state.
|
||||
|
||||
## Objective for Milestone v1.1 (prior — complete, tag `v1.2.0`)
|
||||
|
||||
Finalize the architecture to v1.0 (resolve all 11 open design decisions in
|
||||
`docs/architecture.md` §13) and prove the locked commitments with one
|
||||
end-to-end v1 implementation spike:
|
||||
|
||||
- **One L1 module** (`l1-s3`) — engine-agnostic, IR-typed interface.
|
||||
- **One L2 thin-composition** (`l2-static-assets`) — references the L1.
|
||||
- **Terraform adapter** — compiles the IR to a real `terraform plan`
|
||||
against AWS via OIDC (no long-lived credentials, per §12.5).
|
||||
- **One contract submission** → contract→IR resolution →
|
||||
`terraform plan` → PolicyCheckResult (Checkov) → confidence signal →
|
||||
evidence event to the DynamoDB outbox.
|
||||
|
||||
The spike validates the architecture's claim that the IR-shaped commitments
|
||||
do not require a polyglot mess (`docs/architecture.md` §14, step 2).
|
||||
|
||||
**Status: COMPLETE — all 5 phases shipped (v1.1.1..v1.1.5) + verified; review
|
||||
READY TO SHIP (0 P0); audit CLEAN; milestone tag `v1.2.0`; Gitea release
|
||||
id 202 published. D-034 closed (root key deactivated by user).**
|
||||
|
||||
## Milestone v1.1 Phases (prior — complete)
|
||||
|
||||
| Phase | Name | Goal |
|
||||
|-------|------|------|
|
||||
| 06 | archive-demo-and-reorient | Move the v1.0 demo (`modules/`, `scripts/`, `evidence-ui/`, `contracts/`, demo workflows) to `demo/`; establish the new repo layout (`platform/`, `schemas/`, `adapters/`, `terraform/`, `modules-ir/`); rewrite README. |
|
||||
| 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. |
|
||||
| 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-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
|
||||
minor per ship.md) → audit. **DONE.**
|
||||
|
||||
## Objective for Milestone v1.2 (prior — complete)
|
||||
|
||||
Platform hardening + first real consumer deployment. The v1.1 spike proved
|
||||
the IR commitments hold on a single dev-only `terraform plan` for one S3
|
||||
bucket. v1.2 takes the spike to a real, simpler, better-documented platform
|
||||
that actually delivers a microservice to AWS ECS Fargate end-to-end.
|
||||
|
||||
Five scope axes (user-directed, 2026-07-21):
|
||||
|
||||
1. **Re-evaluate the current state.** Confirm go-gitea/gitea#36988 (OIDC for
|
||||
Gitea Actions) is still unmerged (re-checked 2026-07-21: **open**, last
|
||||
updated 2026-05-27). Extend the D-039 per-run-rotated-key waiver for
|
||||
v1.2; real OIDC is deferred to v1.3+ (D-047).
|
||||
2. **NFR improvements on the existing spike.** Least-privilege IAM audit,
|
||||
idempotent bootstrap, proper exit codes / error handling, rotation
|
||||
hygiene, P1-1 / P1-B redaction carried forward from the v1.1 audit.
|
||||
3. **Streamline / simplify the current setup.** Consolidate the
|
||||
`run_spike_*.sh` scripts into one `scripts/run_platform.sh`; remove
|
||||
dead code and stale paths; one command runs the whole pipeline.
|
||||
4. **README.md fully up to date on how the platform works.** The current
|
||||
README still says "v1.1 (active)" — it must reflect v1.1 complete, the
|
||||
actual spike flow, how to run it, the real repo layout, and the v1.2
|
||||
objective.
|
||||
5. **Bootstrap a consumer repo with a basic microservice deployed to ECS
|
||||
end-to-end.** New Gitea repo `acdl-consumer-microservice` (org
|
||||
`continuous-intelligence`) holding a tiny HTTP container + Dockerfile;
|
||||
new IR-typed L1s (`l1-vpc`, `l1-ecs-cluster`, `l1-ecs-service`,
|
||||
`l1-iam-role`, `l1-alb`, `l1-ecr`); new `l2-microservice`
|
||||
thin-composition; one contract submission → `terraform apply` (dev,
|
||||
autonomous) → a live ECS Fargate service serving HTTP 200 → evidence
|
||||
event to the DynamoDB outbox → acdl-evidence timeline.
|
||||
|
||||
The milestone proves the platform delivers real value (a running
|
||||
microservice), not just a plan.
|
||||
|
||||
## Milestone v1.2 Phases
|
||||
|
||||
| Phase | Name | Goal |
|
||||
|-------|------|------|
|
||||
| 11 | v1.2-research-and-readme | Re-eval #36988 (confirm open → extend D-039 as D-047). Audit the v1.1 spike for NFR gaps (least-privilege, idempotency, error handling, rotation hygiene) + simplification opportunities. **Rewrite README.md** to reflect v1.1 complete + how the platform actually works (spike flow, how to run, repo layout, v1.2 objective). Output: RESEARCH.md v1.2 addendum; updated README. |
|
||||
| 12 | nfr-harden-and-simplify | Apply Phase 11 findings: tighten `spike_runner_policy.json` (least-privilege audit); make `terraform/bootstrap/create_*.py` idempotent; consolidate `run_spike_*.sh` → one `scripts/run_platform.sh`; proper exit codes / error handling; redact P1-1 AWS key IDs in `VERIFY.md`; fix any remaining stale `platform/` paths. Spike still runs e2e after the refactor. |
|
||||
| 13 | l1-catalog-for-ecs | Author IR-typed L1s for an ECS Fargate microservice: `l1-vpc`, `l1-ecs-cluster`, `l1-ecs-service`, `l1-iam-role` (task + exec role), `l1-alb`, `l1-ecr`. Register all in `modules-ir/registry.json`. Expand the Terraform adapter `TYPE_MAP`. Each L1 produces a valid `terraform plan` fragment. |
|
||||
| 14 | l2-microservice-and-contract-schema | Author `l2-microservice` thin-composition (references the ECS L1s, depth ≤ 5). Extend `schemas/contract.schema.json` for microservice inputs (image, port, env, healthcheck). Verify contract→IR resolution yields a complete target stack. |
|
||||
| 15 | consumer-repo-and-terraform-apply | Create consumer repo `acdl-consumer-microservice` (Gitea org) with a basic microservice (tiny HTTP container + Dockerfile + ECR push). Lift the platform from `plan` → **`apply`** (dev, autonomous per §10). Submit `contracts/microservice.yaml` → pipeline → IR → plan → apply → a real ECS Fargate service running. |
|
||||
| 16 | v1.2-capstone-e2e | End-to-end verification: consumer commit → pipeline → ECS service live serving HTTP 200 → evidence event to the DynamoDB outbox → acdl-evidence timeline renders it. Verify NFR improvements hold, the setup is simpler (one `run_platform.sh`), and the README is accurate. |
|
||||
|
||||
Milestone COMPLETE gate: review → ship `v1.3.0` (feature milestone, next
|
||||
minor per ship.md — v1.1 shipped `v1.2.0`) → audit.
|
||||
|
||||
## Objective for Milestone v1.4 (active)
|
||||
|
||||
Central pipeline contract + shell reproducibility + output streaming. The
|
||||
v1.3 milestone (Phases 17–18) created identical CI/CD pipelines for Gitea
|
||||
and GitHub but they were duplicated copies with no single source of truth.
|
||||
v1.4 makes the pipeline a declarative contract, enables full shell
|
||||
reproducibility, and streams terraform/checkov output so users can see
|
||||
what the platform is doing.
|
||||
|
||||
Three scope axes:
|
||||
|
||||
1. **Central pipeline contract.** A JSON Schema
|
||||
(`schemas/pipeline.schema.json`) + YAML instance (`pipelines/ci.yaml`)
|
||||
declares the pipeline stages, commands, triggers, and runner. Both
|
||||
`.gitea/workflows/ci.yml` (Gitea Actions, dev) and
|
||||
`.github/workflows/ci.yml` (GitHub Actions, production) implement the
|
||||
contract. A test validates conformance.
|
||||
2. **Shell reproducibility.** `scripts/run_ci.sh` mirrors the CI pipeline
|
||||
locally — runs the same 3 stages (lint, test, check-only) in sequence.
|
||||
The pipeline is fully reproducible from the shell, not just in CI.
|
||||
3. **Output streaming.** `scripts/run_platform.sh` streams terraform
|
||||
init/validate/plan output, Checkov compliance results, and
|
||||
PolicyCheckResult records to stdout by default, so the user sees what
|
||||
is happening. A `--quiet` flag suppresses streaming for log-only mode.
|
||||
|
||||
## Milestone v1.4 Phases
|
||||
|
||||
| Phase | Name | Goal |
|
||||
|-------|------|------|
|
||||
| 19 | central-pipeline-contract-and-shell-reproducibility | Create the central pipeline contract (JSON Schema + YAML instance). Create `scripts/run_ci.sh` for shell reproducibility. Update `run_platform.sh` to stream terraform/checkov output. Update both workflow YAMLs with contract references (staying byte-identical). Add tests for contract validation, workflow conformance, and streaming. |
|
||||
|
||||
Milestone COMPLETE gate: review → ship `v1.4.1` (feature milestone, next
|
||||
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.
|
||||
|
||||
## Objective for Milestone v1.9 (complete, tag `v1.9.0`)
|
||||
|
||||
Production-grade progression: contract interpolation, per-environment
|
||||
promotion without field editing, stub implementation, and P1-1
|
||||
remediation. The v1.8 milestone shipped encryption/deletion-protection by
|
||||
default, uptime, decommission, and engineering standards but left four
|
||||
gaps that v1.9 closes (user-directed, 2026-07-23):
|
||||
|
||||
1. **Design doc refresh.** `core/hitl_matrix_design.md` and
|
||||
`core/audit_ledger_design.md` are stale — both still describe the
|
||||
v1.1 spike scope ("dev-only; HITL not exercised"; "spike scope =
|
||||
hash chain + outbox write; Object Lock + JWS are v1.2"). v1.9 brings
|
||||
them up to date with the shipped v1.8 platform and the v1.9 wiring.
|
||||
2. **Contract interpolation (variable expansion).** Contracts cannot
|
||||
reference environment onboarding values today — bucket names, account
|
||||
IDs, regions are hardcoded literals. v1.9 adds `${env.<field>}` and
|
||||
`${contract.<field>}` expansion in the resolver, sourced from the
|
||||
environment onboarding JSON. Naming patterns like
|
||||
`acdl-${env.environment}-${contract.module}-${env.account_id}-${env.region}`
|
||||
become expressible. The S3 bucket naming-pattern requirement is the
|
||||
binding example.
|
||||
3. **Per-environment CI jobs (no field editing for promotion).** Today a
|
||||
promotion dev → qa requires editing the `environment:` field in the
|
||||
contract YAML. v1.9 ships a hybrid model: (a) per-environment contract
|
||||
files (`.acdl/static-assets.dev.yaml`, `...qa.yaml`, etc.) and (b) an
|
||||
`environment` `workflow_call` input on the reusable deploy workflow
|
||||
that overrides the contract's environment at load time. There is one
|
||||
CI job per environment, each pointing at its respective contract (or
|
||||
the same contract + the env input). Promotion = running the matching
|
||||
job; no field editing.
|
||||
4. **Stub implementation.** Identify and implement the stubbed
|
||||
functionality: `separation_of_duties.route_halt_artifact` (logs only →
|
||||
real SNS + outbox event); HITL qa/prod/dr pre-execution attestation
|
||||
gates (only decommission SRE gates are wired today); the full
|
||||
8-concern attestation matrix (offline-testable subset implemented;
|
||||
operator-supplied concerns accept signed evidence artifacts); the Wiz
|
||||
adapter (stub → real API client with graceful degrade); the Kyverno
|
||||
adapter (fleshed out translator, still inactive for Terraform-only
|
||||
stacks). The audit-ledger S3 Object Lock + JWS + async worker + DLQ +
|
||||
daily checkpoints build-out is **deferred** to a future milestone
|
||||
(D-083) — it requires non-offline-testable AWS infra (Object Lock
|
||||
bucket, KMS signing key, SQS DLQ, Lambda worker).
|
||||
5. **Post-hoc requirement from previous milestones.** P1-1 from the v1.2
|
||||
review (adapter ECS/ALB/VPC hardcoded defaults — `desired_count = 1`,
|
||||
`launch_type = "FARGATE"`, `target_type = "ip"`,
|
||||
`load_balancer_type = "application"`, `family = "app"`, `Name = ...`
|
||||
— should be parameterized via the L1 interfaces, deferred to v1.3,
|
||||
never implemented) is closed. The adapter becomes a thin translator;
|
||||
the defaults move into `interface.json` inputs.
|
||||
|
||||
The milestone also reconstructs `.ciagent/REVIEW.md`, which still holds
|
||||
v1.2 review content (v1.3–v1.8 reviews were not persisted). The v1.9
|
||||
review overwrites it with current milestone content; a note records the
|
||||
historical gap (no git-history rewrite).
|
||||
|
||||
## Milestone v1.9 Phases
|
||||
|
||||
| Phase | Name | Goal |
|
||||
|-------|------|------|
|
||||
| 39 | design-doc-refresh-and-p1-1-parameterization | Refresh `hitl_matrix_design.md` + `audit_ledger_design.md` to current. Move adapter ECS/ALB/VPC hardcoded defaults into L1 `interface.json` inputs (P1-1 closure). |
|
||||
| 40 | contract-interpolation | `${env.<field>}` + `${contract.<field>}` resolver expansion from environment onboarding JSON. Environment JSON schema. Sample contracts use naming patterns (region + account id + environment). |
|
||||
| 41 | per-environment-ci-jobs | Per-env contract files + `environment` workflow_call input on the deploy workflow. 1 CI job per environment (dev/qa/prod/dr), each pointing at its respective contract. HITL attestation gate structure wired (qa/prod/dr). |
|
||||
| 42 | stub-implementation | `route_halt_artifact` real (SNS + outbox). HITL qa/prod/dr attestation gates. 8-concern attestation matrix (offline-testable subset). Wiz real client. Kyverno translator fleshed out. |
|
||||
| 43 | verify-review-audit-complete | 4-layer verify. Multi-persona review. Audit. Complete v1.9 (tag `v1.9.0`, floating tags, `uses:` bump `@v1.6` → `@v1.9`). |
|
||||
|
||||
Milestone COMPLETE gate: review → ship `v1.9.0` (feature milestone, next
|
||||
minor per run.md — v1.8 shipped `v1.8.0`) → audit.
|
||||
|
||||
## Patch v1.9.1 (complete, tag `v1.9.1`)
|
||||
|
||||
Docs-only NFR patch on the v1.9 line. Two leadership-facing presentation
|
||||
decks (How the Platform Works + The Developer Experience) for senior
|
||||
leadership (CTO, Head of Cloud, Head of Infrastructure, Head of DevOps).
|
||||
Each deck has a full markdown source of truth (with speaker notes + mermaid
|
||||
diagrams) and a lean Marp deck (no speaker notes, embedded PNG diagrams). A
|
||||
README documents the 3-step slide creation process (full markdown → Marp
|
||||
synthesis → PPTX export) with conventions, build commands, and maturity
|
||||
framing rules. No code changes; 494 tests pass; `run_ci.sh` +
|
||||
`run_platform.sh --check-only` green.
|
||||
|
||||
## Patch v1.9.2 (complete, tag `v1.9.2`)
|
||||
|
||||
Docs-only NFR patch on the v1.9 line. Applies the S&P Global Energy brand
|
||||
visual identity to both Marp presentation decks. Brand colors extracted
|
||||
from the live spglobal.com compiled Tailwind CSS and SVG logo: red-core
|
||||
`#D6002A`, grey-90 `#1B1B1B`, grey-80 `#2E2E2E`, grey-5 `#F0F0F0`, Akkurat
|
||||
Pro corporate typeface. Title headers changed to full platform name.
|
||||
Footer changed from 'Confidential · For Senior Leadership' to 'Internal'.
|
||||
Title slide subtitle removed. Last DX slide renamed from 'The Outcome for
|
||||
Leadership' to 'The Desired Outcomes'. Marp `theme: default` kept as base.
|
||||
No code changes; 494 tests pass; `run_ci.sh` + `run_platform.sh --check-only`
|
||||
green.
|
||||
|
||||
## Patch v1.9.3 (complete, tag `v1.9.3`)
|
||||
|
||||
Docs-only NFR patch on the v1.9 line. Renders both Marp presentation decks
|
||||
to self-contained HTML (committed to `docs/presentations/`, base64-embedded
|
||||
images, full S&P Global Energy brand theme) and PPTX (uploaded to the Gitea
|
||||
release as downloadable attachments). The HTML files are viewable in any
|
||||
browser and on the git forge — they render the red accent bar, dark
|
||||
title-slide background, red H1 headings, and Akkurat Pro font stack. README
|
||||
updated to document HTML as committed artifacts (re-render when Marp source
|
||||
changes) and PPTX as release attachments (binary, not committed to git).
|
||||
No code changes; 494 tests pass; `run_ci.sh` + `run_platform.sh --check-only`
|
||||
green.
|
||||
|
||||
## Patch v1.9.4 (complete, tag `v1.9.4`)
|
||||
|
||||
Docs-only NFR patch on the v1.9 line. Two categories of changes:
|
||||
|
||||
1. **Presentation slide updates** — title slide redesigned (deck title as H1
|
||||
slightly bigger, 'Agentic Cloud Delivery Platform' as H3 subtitle on dark
|
||||
background). DX deck: removed Local Reproducibility slide (not beneficial
|
||||
for DX narrative), redesigned Safe Promotion Path with side-by-side
|
||||
HTML table layout for Approaches A and B, 'an agent' → 'an AI agent' on
|
||||
slides 2 and 3, What a Developer Does diagram floated to the right side.
|
||||
Running header simplified to just the deck name.
|
||||
|
||||
2. **Complete removal of a compliance framework** — all references to a
|
||||
specific healthcare compliance framework removed from 25 files
|
||||
across the codebase: presentation source files (Marp + full markdown),
|
||||
all module READMEs (S3, RDS, ECR, ECS, VPC, IAM, KMS, CloudFront, ALB,
|
||||
uptime), top-level README, consumer guide, docs index, module standards.
|
||||
Compliance milestone lists now read: GDPR, SOX, SOC2, DORA. All section
|
||||
references from that framework removed from compliance annotations.
|
||||
Rendered HTML decks re-generated from updated Marp source.
|
||||
|
||||
No code changes; 494 tests pass; `run_ci.sh` + `run_platform.sh --check-only`
|
||||
green. PPTX files uploaded to Gitea release.
|
||||
|
||||
## Patch v1.9.5 (complete, tag `v1.9.5`)
|
||||
|
||||
Docs-only NFR patch on the v1.9 line. 9 requirements implemented:
|
||||
|
||||
1. DX closing slide strengthened with 'Infrastructure as a utility, not a
|
||||
craft' bullet — conveys the full vision (infrastructure consumed, not
|
||||
maintained; platform compounds value over time).
|
||||
2. PW Problem slide: 'moving a merged change' → 'promoting a change'.
|
||||
3. PW Problem slide: added 'Red tape' and 'Scalability without increasing
|
||||
headcount' bullets (4 frictions, not 2).
|
||||
4. PW Roadmap slide: redesigned with side-by-side HTML table layout
|
||||
(Testing | Planned), 16px font, no overflow.
|
||||
5. PW deck: new slide 'What This Platform Is — and Isn't' after North Star
|
||||
(sovereign boundary, infrastructure as utility, 4 anti-goals). PW deck
|
||||
now 16 slides.
|
||||
6. Maturity nomenclature: 'Available today'/'shipped' → 'Testing' across
|
||||
both decks + source markdown. New .testing badge (blue/teal). The
|
||||
platform has 0 consumer adoption — 'shipped' was inaccurate.
|
||||
7. Global: 'substrate' → 'engine' across entire project (88 matches, 30+
|
||||
files including .ciagent/, docs/, modules/, adapters/, schemas/, code).
|
||||
8. Presentation files only: 'forge' → 'VCS' (6 occurrences in 4 files).
|
||||
'forge' retained in all technical docs and code.
|
||||
9. New .agentic badge (purple/violet) appended to agentic features in both
|
||||
decks: confidence signal, autonomous dev, pattern recognition, dynamic
|
||||
module creation, citizen developer surface, auto-promotion.
|
||||
|
||||
Also: Change Request ID format changed from 'CR-2026-001' to 'CHG0678912'
|
||||
across presentation files, consumer guide, and test fixtures.
|
||||
|
||||
No code changes (test fixture strings only); 494 tests pass; `run_ci.sh` +
|
||||
`run_platform.sh --check-only` green. PPTX files uploaded to Gitea release.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Validated
|
||||
- Three repos under the `continuous-intelligence` Gitea org: `acdl` (platform + stubs + reusable workflows), `acdl-contracts` (developer surface), `acdl-evidence` (GitHub Pages audit timeline).
|
||||
- L1 modules (single-purpose, substrate-agnostic, max-depth-1 primitives) as folders with `manifest.yaml` + `mock_apply.sh`.
|
||||
- L2 modules (composed stacks, max-depth-5) grouping L1s into deployable service shapes.
|
||||
- L3A developer surface: commit `contract.yaml` to `acdl-contracts`.
|
||||
- L3B agentic surface: Python keyword parser turning an Issue body into `contract.yaml`.
|
||||
- Confidence signal: base 0.90, drops to 0.40 on policy violation; gate threshold ≥ 0.50.
|
||||
- Evidence stream: hash-chained `audit.json` published via Pages + vanilla-JS `index.html` timeline.
|
||||
- Reusable CI workflow: Dev (autonomous) → QA (manual approval) → Prod (manual approval) → finalize.
|
||||
### v1.0 (Prior milestone — the demo)
|
||||
|
||||
### Active
|
||||
- 8 L1 modules (serverless/container focus): `l1-eks-fargate`, `l1-iam-role`, `l1-lambda`, `l1-api-gateway`, `l1-eventbridge`, `l1-sqs`, `l1-s3`, `l1-cloudwatch`.
|
||||
- 4 L2 modules mirroring S&P Global Energy / Platts use cases: `l2-invoice-service`, `l2-commodity-price-feed`, `l2-energy-analytics-api`, `l2-regulatory-reporting`.
|
||||
- 5 core scripts: `mock_executor.sh`, `policy_checker.py`, `confidence_signal.py`, `evidence_writer.py`, `l3b_agent_stub.py`.
|
||||
- Issue-triggered workflow in `acdl-contracts` that runs the L3B parser, commits a new branch, closes the issue, and triggers the main pipeline.
|
||||
- Evidence stream UI (`index.html`) fetching `audit.json` and rendering events as a timeline.
|
||||
Status: complete. Tag `v1.1.0`. All REQ-01..15 satisfied by the stub-driven
|
||||
executive demo. See `REQUIREMENTS.md` §v1 and the prior decisions table
|
||||
appendix below. The demo is **archived** to `demo/` in Phase 06.
|
||||
|
||||
### Out of Scope
|
||||
- Real cloud provisioning (AWS/GCP/Azure).
|
||||
- Real LLM inference / external AI APIs.
|
||||
- Production-grade infrastructure or multi-tenant isolation.
|
||||
- Real cryptographic tamper-proofing (the hash chain is demonstrative, not adversarially secure).
|
||||
### v1.1 (Prior milestone — architecture finalization + v1 spike, complete)
|
||||
|
||||
## Constraints
|
||||
New requirements REQ-16..REQ-28 — see `REQUIREMENTS.md` §v1.1. Summary:
|
||||
|
||||
- Environment: local Linux OS.
|
||||
- CI/CD: GitHub/Gitea Actions + Environments (QA, Prod approval gates).
|
||||
- **No cloud** — absolutely no AWS, GCP, or Azure resources.
|
||||
- **No AI** — no OpenAI or external LLM APIs; the "Agentic" part is a keyword parser.
|
||||
- All state in flat JSON files or CI artifacts.
|
||||
- Compute strategy: EKS Fargate + serverless primitives (no VPC module).
|
||||
- L1 modules are single-purpose, substrate-agnostic, do not compose with other L1s.
|
||||
- L2 modules combine L1 primitives into deployable shapes, max depth 5.
|
||||
- **REQ-16:** Architecture finalized to v1.0 (11 open decisions resolved).
|
||||
- **REQ-17:** Target Stack IR defined as JSON Schema; engine-agnostic.
|
||||
- **REQ-18:** PolicyCheckResult normalized schema defined; Checkov adapter.
|
||||
- **REQ-19:** Six-input confidence signal specified with per-env thresholds
|
||||
(dev 0.50 / qa 0.75 / prod 0.90 / dr 0.95) and severity→penalty mapping.
|
||||
- **REQ-20:** Tiered audit ledger design (S3 Object Lock 7-yr + DynamoDB
|
||||
outbox, RPO=0, JWS detached signatures, `prev_event_hash` chain).
|
||||
- **REQ-21:** Full 8-concern HITL matrix + separation-of-duties design
|
||||
(CODEOWNERS + DynamoDB identity-distinctness).
|
||||
- **REQ-22:** Contract schema (JSON Schema draft 2020-12) with per-env
|
||||
mandatory/optional inputs and `profile: agentic` marker for L3B.
|
||||
- **REQ-23:** AWS OIDC bootstrap (IAM role + trust policy for act_runner);
|
||||
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-25:** One real L2 thin-composition (`l2-static-assets`) referencing
|
||||
`l1-s3`.
|
||||
- **REQ-26:** Terraform adapter compiles the IR to a real `terraform plan`
|
||||
against AWS via OIDC; state in S3 + DynamoDB.
|
||||
- **REQ-27:** One end-to-end contract submission → contract→IR resolution →
|
||||
`terraform plan` → Checkov → confidence signal → evidence event to outbox.
|
||||
- **REQ-28:** Spike verification proves the IR-shaped commitments hold (no
|
||||
polyglot mess; the adapter is the only engine-specific code).
|
||||
|
||||
## Context
|
||||
### v1.2 (Prior milestone — platform hardening + first real consumer deployment, complete)
|
||||
|
||||
- Forge: Gitea at `https://git.cloudinit.dev`, org `continuous-intelligence`.
|
||||
- The `acdl` repo already exists (empty) at org root and serves as the platform/meta repo.
|
||||
- `acdl-contracts` and `acdl-evidence` will be created as additional repos in the same org.
|
||||
- act_runner / Gitea Actions is the CI runtime; "GitHub Actions" workflow YAML is reused as-is.
|
||||
New requirements REQ-29..REQ-35 — see `REQUIREMENTS.md` §v1.2. Summary:
|
||||
|
||||
## Key Decisions
|
||||
- **REQ-29:** README.md fully documents the v1.1-complete platform: spike
|
||||
flow, how to run, repo layout, v1.2 objective.
|
||||
- **REQ-30:** NFR hardening — least-privilege IAM audit, idempotent
|
||||
bootstrap, consolidated `run_platform.sh`, error handling, P1-1/P1-B
|
||||
redaction.
|
||||
- **REQ-31:** L1 catalog expanded for ECS — 6 new IR-typed L1s
|
||||
(`l1-vpc`, `l1-ecs-cluster`, `l1-ecs-service`, `l1-iam-role`, `l1-alb`,
|
||||
`l1-ecr`) registered and adapter-compiled.
|
||||
- **REQ-32:** `l2-microservice` thin-composition + contract schema extended
|
||||
for microservice inputs (image, port, env, healthcheck).
|
||||
- **REQ-33:** `terraform apply` (dev, autonomous) — real provisioning, not
|
||||
just `plan`.
|
||||
- **REQ-34:** Consumer repo `acdl-consumer-microservice` with a basic
|
||||
microservice (ECR image, Dockerfile, contract).
|
||||
- **REQ-35:** End-to-end verification — consumer commit → live ECS service
|
||||
(HTTP 200) → evidence event → timeline.
|
||||
|
||||
### v1.4 (Prior milestone — central pipeline contract + shell reproducibility + streaming)
|
||||
|
||||
New requirements REQ-43..REQ-45 — see `REQUIREMENTS.md` §v1.4. Summary:
|
||||
|
||||
- **REQ-43:** Central pipeline contract — `schemas/pipeline.schema.json` +
|
||||
`pipelines/ci.yaml`. Both Gitea and GitHub workflows implement the
|
||||
contract; a test validates conformance.
|
||||
- **REQ-44:** `scripts/run_ci.sh` mirrors the CI pipeline locally (lint →
|
||||
test → check-only), exiting 0 with "CI PIPELINE OK".
|
||||
- **REQ-45:** `scripts/run_platform.sh` streams terraform/checkov output by
|
||||
default (with `--quiet` for log-only mode). Both workflows byte-identical.
|
||||
|
||||
## Key Decisions (v1.9)
|
||||
|
||||
Resolved at the CLARIFY stage (full autonomy — all within locked
|
||||
constraints or user-directed scope). New v1.9 decisions (numbered
|
||||
D-080+ to avoid collision with v1.8 research decisions D-073..D-077):
|
||||
|
||||
| ID | Decision | Rationale | Outcome |
|
||||
|----|----------|-----------|---------|
|
||||
| D-001 | Use Gitea org `continuous-intelligence` for all repos | User-specified target org; already exists | Single source of truth for the demo |
|
||||
| D-002 | Map "GitHub Actions" to Gitea Actions (act_runner) | Environment is Gitea; same workflow YAML syntax | Demo runs on the actual forge |
|
||||
| D-003 | Collapse `acdl-platform` into the existing `acdl` repo | `acdl` already exists empty at org root | 3 repos total: `acdl`, `acdl-contracts`, `acdl-evidence` |
|
||||
| D-004 | Use Gitea `environment` blocks + required reviewers for QA/Prod; fallback to manual `workflow_dispatch` with approval input | Approval gates required by spec; forge supports environment protection | Frictionless approval gates |
|
||||
| D-005 | Hash-chained ledger (`prev_hash` + own `hash`) for evidence; declared demonstrative | Spec asks for simple JSON; chain gives visible tamper-evidence | Visible audit timeline without overengineering |
|
||||
| D-006 | Confidence gate threshold = 0.50 exactly | Explicit in spec | Acts 2/4 behave as scripted |
|
||||
| D-007 | Each `mock_apply.sh` echoes `[L1: <name>] applying...` + `OK`, sleeps 1s, exits 0 | Spec literal; uniformity aids timeline parsing | Predictable evidence events |
|
||||
| D-008 | Keyword→stack mapping for L3B: gas/price/ingest/data-lake → commodity-price-feed; invoice/billing → invoice-service; analytics/historical/query → energy-analytics-api; regulatory/compliance/reporting/trading → regulatory-reporting; fallback → invoice-service | Mirrors the 4 L2 modules + Act 3 example issue | Act 3 reproduces deterministic behavior |
|
||||
| D-009 | Init milestone = `v1.0`, branch `milestone/v1.0-initial` | init.md Step 5 mandate | Branching strategy follows convention |
|
||||
| D-010 | Single-project mode for the `acdl` checkout | User chose standalone single-project | `---ci---` blocks omit `project:` field |
|
||||
| D-011 | Single-project mode explicitly enforced via `config.json mode: "single"` overriding `projects[]` length signal | run.md Step 0 reads `projects[]` length as multi-project trigger; explicit flag disambiguates | No `project:` prefix in commits or branches |
|
||||
| D-012 | Gitea has no native Pages — serve `acdl-evidence` via raw file URLs (`/raw/branch/main/...`) and a CORS note in ARCHITECTURE.md | Research confirms Gitea has no `[pages]` section | Demo can render `index.html` via raw URL without server-side Pages config |
|
||||
| D-013 | Gitea has no environments API and ignores `jobs.<id>.environment` — model QA/Prod gates as `workflow_dispatch` approval inputs (D-004 fallback) | Research confirms `environment:` blocks are ignored by act_runner | Approval gates become dispatch inputs; "environments" become workflow job names + optional branch protection on `qa`/`prod` branches |
|
||||
| D-014 | Cross-repo triggering uses the `workflow_dispatch` Gitea API (POST `/actions/workflows/{id}/dispatches`) from inside a step instead of `repository_dispatch` | Gitea Actions does not support `repository_dispatch` | Issue-trigger workflow calls the main pipeline via authenticated dispatch from a step |
|
||||
| D-015 | New repos `acdl-contracts` and `acdl-evidence` use `default_branch: "main"` with `auto_init: true` | Matches Gitea `DEFAULT_BRANCH=main`; required for the default branch to exist before any push | Reusable-workflow `uses:` references still pin `acdl` workflows to `@milestone/v1.0-initial` |
|
||||
| D-016 | Pages placeholder for Phase 01 is a minimal HTML stub (`<title>ACDL Evidence</title>` + "evidence stream coming soon"); full UI deferred to Phase 05 | Phase 01 success criterion is "Pages returns 200 with placeholder index.html" but Gitea has no Pages | Raw-URL HTTP 200 against `index.html` substitutes for the Pages check; full timeline UI built in Phase 05 |
|
||||
| D-017 | Each L1 `manifest.yaml` declares a single `inputs:` map of named string keys with descriptions; no nested types (substrate-agnostic, max-depth-1) | REQ-02/03 say "declared inputs"; spec forbids composition and cloud-specific types | Uniform, parseable schema that Phase 03's `mock_executor.sh` can read with python+yaml |
|
||||
| D-018 | L1 `mock_apply.sh` reads its own `manifest.yaml` for self-identification but ignores the input values (uniform stub per D-007) | D-007 mandates a literal echo + 1s sleep + exit 0; inputs are declared for traceability, not consumed | Predictable evidence events + clean separation from Phase 03 where L2s pass inputs to L1s |
|
||||
| D-019 | The 8 L1 names are fixed per REQ-02: `l1-eks-fargate`, `l1-iam-role`, `l1-lambda`, `l1-api-gateway`, `l1-eventbridge`, `l1-sqs`, `l1-s3`, `l1-cloudwatch` | REQ-02 literal | Phase 02 enumerates them exactly; no naming freedom |
|
||||
| D-080 | New milestone v1.9 (feature); ship tag `v1.9.0`. | v1.8 is complete (audit PASS, tag v1.8.0). The work (design doc updates + interpolation + per-env CI + stubs + P1-1) is a new feature milestone, not v1.8 post-hoc patching. | 5 phases (39–43) in one milestone. |
|
||||
| D-081 | Interpolation syntax: `${env.<field>}` + `${contract.<field>}` (dotted paths supported, e.g. `${env.state_backend.bucket}`). Expanded by the resolver post-schema-validation, pre-IR-resolution. Fail loud on unresolved tokens (`ValueError`). | Shell-style syntax is familiar, unambiguous, and has no conflict with YAML or the contract schema. The `env` context is the loaded environment onboarding JSON; `contract` is the contract dict. | Phase 40 implements the expansion + environment JSON schema. |
|
||||
| D-082 | Hybrid per-environment promotion model: (a) per-env contract files AND (b) an `environment` `workflow_call` input on the reusable deploy workflow that overrides the contract's environment at load time. One CI job per environment. | User chose to support both shapes. Per-env contracts let env-specific values differ via interpolation; the env input lets a single contract be promoted without editing. Promotion = running the matching job; no `environment:` field editing. | Phase 41 ships per-env contracts + the env input + caller-workflow docs. |
|
||||
| D-083 | Audit ledger S3 Object Lock + JWS detached signatures + async worker + DLQ + daily checkpoints **deferred** to a future milestone. | Requires non-offline-testable AWS infra (Object Lock bucket, KMS signing key, SQS DLQ, Lambda worker). The hash-chain + DynamoDB-outbox path remains the v1.9 production audit record. `audit_ledger_design.md` marks this clearly. | Phase 39 updates the design doc; no build-out in v1.9. |
|
||||
| D-084 | 8-concern attestation matrix: offline-testable concerns (contract NFRs, schema validity, policy pass) run for real; operator-supplied concerns (k6 load test, DR drill, FinOps forecast) accept signed evidence artifacts validated for freshness + schema, failing loud if missing/expired for prod/dr. | The platform cannot run live load tests / DR drills / FinOps forecasts inline. Accepting signed evidence artifacts with freshness + schema validation is the regulatorily-defensible middle ground. | Phase 42 implements `core/attestation_matrix.py`. |
|
||||
| D-085 | P1-1 closure: adapter ECS/ALB/VPC hardcoded defaults (`desired_count = 1`, `launch_type = "FARGATE"`, `target_type = "ip"`, `load_balancer_type = "application"`, `family = "app"`, `Name = ...`) move into L1 `interface.json` inputs with defaults. The adapter reads inputs (falling back to interface defaults) and is a thin translator. | P1-1 was flagged in the v1.2 review (deferred to v1.3, never implemented). Defaults belong in the L1 interface, not the adapter. | Phase 39 closes P1-1. |
|
||||
| D-086 | `.ciagent/REVIEW.md` reconstructed at v1.9 complete; v1.3–v1.8 reviews noted as not-persisted (no git-history rewrite). | REVIEW.md still holds v1.2 content — later milestone reviews were not persisted or were overwritten. The v1.9 review overwrites it with current content; a note records the historical gap. | Phase 43 reconstructs REVIEW.md. |
|
||||
|
||||
### CLARIFY auto-resolved parameters (full autonomy)
|
||||
|
||||
| Parameter | Value | Rationale |
|
||||
|---|---|---|
|
||||
| Per-env `qa.json/prod.json/dr.json` account_id | `000000000000` placeholder + stderr warning at load if account_id is `000000000000` and env ≠ dev | Consistent with `dev.json`; prompts real binding without breaking offline tests. |
|
||||
| SNS topic for `route_halt_artifact` | Defined in `terraform/platform/main.tf` AND code reads `ACDL_SOD_HALT_TOPIC_ARN` | Consistent with the existing Lambda/KMS/Secrets pattern (Terraform defines, code reads env). |
|
||||
|
||||
## Constraints
|
||||
|
||||
- **Forge:** Gitea at `https://git.cloudinit.dev`, org `continuous-intelligence`.
|
||||
- **CI runtime:** act_runner / Gitea Actions (reuses GitHub Actions workflow YAML).
|
||||
- **Cloud:** AWS via OIDC federation. **Long-lived credentials are forbidden**
|
||||
(§12.5). The v1.1 spike uses a temporary long-lived key **once** to bootstrap
|
||||
OIDC (waiver D-034), then rotates it.
|
||||
- **Angine:** Terraform adapter in v1 (the only adapter). L1/L2 are
|
||||
engine-agnostic in shape; the adapter is the only engine-specific code.
|
||||
- **State:** S3 (state files) + DynamoDB (locking), single-region in v1.
|
||||
- **Environments:** dev (autonomous) → qa (QA HITL) → prod (SRE HITL) → dr
|
||||
(SRE HITL). **Staging does not exist** (Path A locked).
|
||||
- **Compute:** abstract / containerized / serverless. No VMs, bare metal, OS
|
||||
lifecycle.
|
||||
- **Autonomy:** Full. Escalation hooks: deploy, delete_data, merge_to_main.
|
||||
|
||||
## Anti-Goals (from `docs/vision.md` §7)
|
||||
|
||||
- Not an upstream development platform (no product backlogs, IDE, code authorship).
|
||||
- Not a general-purpose AI (autonomy is narrow, bounded by policy envelopes).
|
||||
- Not a legacy infrastructure bridge (no VMs/bare metal/OS).
|
||||
- Not a permissive delivery highway (no escape hatches past confidence or HITL).
|
||||
- Not a mutable audit log (VCS history ≠ regulatory evidence).
|
||||
|
||||
## Context
|
||||
|
||||
- The `acdl` repo exists at the org root. `acdl-contracts` and
|
||||
`acdl-evidence` exist from the v1.0 demo and continue as the developer
|
||||
surface and the audit-timeline host respectively.
|
||||
- `docs/vision.md` and `docs/architecture.md` (v0.2) are the upstream
|
||||
vision/architecture sources, pulled from `origin/main` at the start of v1.1.
|
||||
- The v1.0 demo (tag `v1.1.0`) is the reference of intent — it proved the
|
||||
shape (L1/L2/contract/confidence/evidence/HITL) on stubs. v1.1 replaces the
|
||||
stubs with the real platform engine.
|
||||
|
||||
## Key Decisions (v1.1)
|
||||
|
||||
Carries forward the still-valid v1.0 decisions (see appendix). New v1.1
|
||||
decisions:
|
||||
|
||||
| ID | Decision | Rationale | Outcome |
|
||||
|----|----------|-----------|---------|
|
||||
| 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-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-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-040 | The 6 confidence-signal inputs are: policy (0.30), validation (0.25), freshness (0.10), source (0.15), history (0.10), nfrs (0.10). Weights frozen for v1, tuned in v1.2 alongside thresholds (BA.B). | Architecture §8 locks "six canonical inputs" but does not enumerate them; RESEARCH TARGET 6 chose the platform-computable subset present in every environment (incl. dev). | Confidence signal (Phase 10) has a concrete input enumeration |
|
||||
| D-041 | Spike audit ledger = v1.0 hash chain + DynamoDB outbox + `acdl-evidence` mirror. S3 Object Lock (compliance mode, 7-yr) + JWS (platform KMS key, quarterly rotation) + daily checkpoints are v1.2 build-out, authored as design in Phase 07. | REQ-20 is "design authored," not "implemented." The spike proves the outbox write path; the regulatory ledger is v1.2. | Spike scope stays bounded; REQ-20 satisfied by the Phase 07 design doc |
|
||||
| D-042 | HITL approver identity in Gitea = `gitea.actor` of the `workflow_dispatch` run that sets `approve_qa=true`/`approve_prod=true`/`approve_dr=true`. Separation-of-duties reads `approver_qa` from the DynamoDB outbox and compares to the prod-dispatch `gitea.actor`. | Gitea has no Environments API (re-confirmed in RESEARCH); `gitea.actor` is the only approval-identity signal. | SoD design (Phase 07) is concrete for the Gitea forge |
|
||||
| D-043 | Tag/naming compliance deferred for the spike: the Checkov adapter emits a single `SKIPPED` PolicyCheckResult (`ruleId: ACDL_TAG_NAMING`, `severity: info`) so the confidence policy input is non-empty. Custom Checkov YAML rule lands in v1.2. | Checkov has no built-in tag-presence check; a custom rule in the spike is scope creep. | Spike's policy input is non-empty without a custom-rule dependency |
|
||||
| D-044 | DynamoDB outbox = `PAY_PER_REQUEST`; PK `contractId`, SK `eventType#eventTs`, TTL `expire_at` = now + 365d. No separate async worker/DLQ in the spike (RTO = workflow re-run); v1.2 outbox worker + DLQ is a Phase 07 design artifact. | On-demand is zero-cost-at-idle for the spike's single dev submission. | Spike outbox is minimal; v1.2 worker design authored in Phase 07 |
|
||||
| D-045 | Runner tooling: `runs-on: ubuntu-latest`; install `terraform` via HashiCorp apt repo (pin `1.9.*`), `checkov` via pip (pin `>=3.2,<4`, `--break-system-packages`). Neither is pre-installed on the default runner image. | RESEARCH TARGET 2; pinning avoids mid-spike version drift. | Phase 09/10 workflows have a concrete setup step |
|
||||
| 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. |
|
||||
|
||||
## 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)
|
||||
|
||||
| ID | Question | Resolution |
|
||||
|---|---|---|
|
||||
| W1.A | AI-refinement trigger | **Accept recommendation.** Joint condition: N ≥ 50 consecutive changes with zero rollbacks AND no L1/L2 incident in last 6 months AND Infra & Ops unilateral override. |
|
||||
| W1.B | Multi-stack edge case rule | **Accept recommendation.** Permitted only for (a) DR-region mirror, (b) time-boxed experimental stack with TTL ≤ 30d, (c) explicit Infra & Ops approval with `multiStack.justification`. |
|
||||
| W2.A | Tag mutability for prod | **Accept recommendation (Path B).** Tag for dev/qa, SHA for prod. Platform CLI resolves tag→SHA for prod-bound workflows. Justified by the "Audit truth lives outside the repository" bet. |
|
||||
| BA.A | Initial L3B skill catalog | **Accept recommendation.** 5 skills: 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. |
|
||||
| W3.D | L1/L2 standard versioning | **Decided.** Semver: interface → MAJOR, behavior → MINOR, lifecycle → PATCH (same as the v1.0 demo D-rule, lifted to the real platform). Pin model: L2 contracts pin L1 by `name@semver`; the resolver picks the highest compatible. Evolution: MAJOR bumps require a new registry entry (immutable publication); old entry enters a 12-month deprecation window. |
|
||||
| W3.E | Schema mandatory vs optional inputs | **Decided.** Per-env mandatory table: dev requires `stack` + `environment`; qa adds `validation.e2eSuite` + `validation.loadTest`; prod adds `runbook` + `dashboard` + `oncall`; dr adds `drDrillRef`. `inputs` map is always optional. `profile: agentic` fields (`naturalLanguageIntent`, `confidenceAtSubmission`, `agentTrace`) optional everywhere. |
|
||||
| BA.B | Confidence threshold tuning | **Decided.** Starting thresholds frozen for v1. Tuning begins in v1.2: track FP/FN per environment quarterly; override authority = Infra & Ops + SRE joint sign-off; any override is itself a confidence-event in the audit stream. |
|
||||
| BA.C | On-call / operational ownership | **Decided.** Platform on-call = Infra & Ops rotation. Escalation: L3A/L3B halt → platform on-call pager (Sev2); consumer-visible outage → consumer on-call (Sev1) with platform on-call support. Consumer on-call relationship is contractual, defined at onboarding (BA.E). |
|
||||
| BA.D | Cost / capacity governance | **Decided.** Cloud cost owner = Infra & Ops FinOps. Per-contract consumption reported monthly. Runaway spend: hard halt at 120% of contract-declared budget envelope via the confidence signal (cost is one of the 6 inputs); override = FinOps + SRE joint sign-off. |
|
||||
| BA.E | Consumer onboarding | **Decided.** Two paths: developer (L3A) — `getting-started` walks through contract schema + central pipeline template; citizen developer (L3B) — onboarding grants a scoped agent + skill catalog, no workflow authoring. Both end in a sandbox dev submission that must pass the confidence gate before the consumer is promoted. |
|
||||
| BA.F | Cross-platform evolution | **Decided.** The contract schema, IR, PolicyCheckResult, confidence signal, and audit stream are portable (engine- and forge-agnostic). Forge-specific code: workflow YAML, OIDC trust, CODEOWNERS, Environments. A second forge (e.g., GitLab) requires a forge adapter + a workflow-template translator; no change to L1/L2/IR/confidence/audit. |
|
||||
| Q1.3 | OpenTofu timing | **Decided (deferred).** Not in v1 or v1.1. The engine abstraction (§12) makes OpenTofu a future adapter, not an architecture change. Revisit when an OpenTofu adapter is requested; no version committed. |
|
||||
|
||||
## Appendix — Prior milestone (v1.0 demo) decisions
|
||||
|
||||
The v1.0 demo (tag `v1.1.0`) carried decisions D-001..D-033. They governed
|
||||
the stub-driven executive demo and remain valid **for the archived demo
|
||||
under `demo/`**. They are **superseded** by the v1.1 decisions above for the
|
||||
real platform. Full text preserved in git history at tag `v1.1.0`.
|
||||
|
||||
## Operational parameters (CLARIFY auto-resolution, full autonomy)
|
||||
|
||||
Resolved at the CLARIFY stage to unblock planning. None require user
|
||||
sign-off (autonomy = full; all within locked constraints).
|
||||
|
||||
| Parameter | Value | Rationale |
|
||||
|---|---|---|
|
||||
| AWS region | `us-east-1` | Default; matches v1.0 demo references; single-region in v1 (§12.3) |
|
||||
| Terraform state bucket | `acdl-tfstate-<account-id>-us-east-1` | Namespaced by account id to avoid collision; region-suffixed |
|
||||
| Terraform lock table | `acdl-tflock` | DynamoDB; single-region v1 |
|
||||
| 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 |
|
||||
| Spike L1 (`l1-s3`) inputs | `bucket_name: string`, `region: string` | Minimal S3 interface per §2 |
|
||||
| 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-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 |
|
||||
| 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 |
|
||||
| Next ship tag | `v1.3.0` | Feature milestone → next minor per ship.md (v1.1 shipped `v1.2.0`; v1.2 ships `v1.3.0`) |
|
||||
|
||||
### Items deferred to RESEARCH (not clarifications)
|
||||
|
||||
- **Gitea/act_runner OIDC support** — does act_runner emit an OIDC
|
||||
`id-token`? Determines whether real-AWS plan is achievable in this
|
||||
environment or whether a spike-only waiver is needed. Highest-priority
|
||||
research target.
|
||||
- **Terraform + Checkov availability on the runner image** — install in the
|
||||
workflow if missing.
|
||||
- **`actions/configure-aws-credentials` action on act_runner** — if
|
||||
unavailable, fall back to `aws sts assume-role-with-web-identity` from a
|
||||
step.
|
||||
+376
-15
@@ -35,7 +35,194 @@
|
||||
|
||||
(None — v1 covers the complete demo.)
|
||||
|
||||
## Clarifications (Phase 01)
|
||||
## v1.1 (Prior milestone — architecture finalization + v1 spike, complete)
|
||||
|
||||
### Category: Architecture Finalization
|
||||
- **REQ-16:** Architecture reaches v1.0 — all 11 open decisions in `docs/architecture.md` §13 are resolved and recorded in `PROJECT.md` (W1.A, W1.B, W2.A, W3.D, W3.E, BA.A–F, OpenTofu timing).
|
||||
- **REQ-17:** Target Stack IR is defined as a JSON Schema under `schemas/ir.schema.json`; engine-agnostic (resources, relationships, composition max-depth-5, policy hooks).
|
||||
- **REQ-18:** `PolicyCheckResult` normalized schema is defined under `schemas/policy_check_result.schema.json`; a Checkov adapter translates Checkov JSON to this schema.
|
||||
- **REQ-19:** Six-input confidence signal is specified under `platform/confidence_signal.py` with per-env thresholds (dev 0.50 / qa 0.75 / prod 0.90 / dr 0.95) and severity→penalty mapping (critical=hard override, high=-0.2, medium=-0.05, low=-0.01, info=0.0).
|
||||
- **REQ-20:** Tiered audit ledger design is authored: S3 Object Lock (compliance mode, 7-yr) + DynamoDB outbox (RPO=0, JWS detached signatures, `prev_event_hash` chain, daily checkpoints).
|
||||
- **REQ-21:** Full 8-concern HITL matrix + separation-of-duties design is authored (CODEOWNERS routing + DynamoDB identity-distinctness check; pre-execution gate model; 1d warn / 2d freeze timeout).
|
||||
- **REQ-22:** Contract schema (JSON Schema draft 2020-12) is defined under `schemas/contract.schema.json` with per-env mandatory/optional inputs (W3.E) and `profile: agentic` marker for L3B fields.
|
||||
|
||||
### Category: AWS OIDC Bootstrap
|
||||
- **REQ-23:** AWS auth bootstrap + state backend for the spike: an S3 state bucket + DynamoDB lock/outbox table + an IAM user with a minimal scoped policy (S3 + DynamoDB + plan-only). The temporary long-lived key is used once (waiver D-034) then rotated via `scripts/rotate_spike_key.sh` after each spike run (D-039). **Real OIDC federation is deferred to v1.2** — Gitea Actions does not support `id-token: write` (RESEARCH TARGET 1, conf 0.95), blocked on go-gitea/gitea#36988.
|
||||
|
||||
### 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-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.
|
||||
|
||||
### Category: v1 Spike — End-to-End
|
||||
- **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 engine-specific code; no polyglot mess; the L1 content, contract YML, and thin-composition tree are engine-agnostic.
|
||||
|
||||
## Out of Scope (v1.1)
|
||||
|
||||
| Feature | Reason |
|
||||
|---------|--------|
|
||||
| Full HITL matrix wiring (qa/prod/dr) | Spike is dev-only (`terraform plan`); HITL wiring is v1.2. |
|
||||
| Kyverno + OPA policy engines | Spike uses Checkov only; Kyverno/OPA are v1.2. |
|
||||
| MCP skill catalog + real L3B agent | L3B spike = a single stub contract submission; the 5-skill catalog is v1.2. |
|
||||
| GitOps reconciler (ArgoCD/Flux) | v1.2. |
|
||||
| Multi-region state / outbox | Single-region in v1 (§9, §12.3). |
|
||||
| Prod/dr environments | v1.2. |
|
||||
| Terraform `apply` (real provisioning) | Spike runs `plan` only; `apply` is gated by HITL in v1.2. |
|
||||
|
||||
## v1.2 (Prior milestone — platform hardening + first real consumer deployment, complete, tag `v1.3.0`)
|
||||
|
||||
### Category: Documentation & Simplification
|
||||
- **REQ-29:** `README.md` is fully rewritten to reflect the v1.1-complete platform: the actual spike flow (contract → IR → `terraform plan` → Checkov → confidence signal → outbox), how to run it (`scripts/run_platform.sh`), the real repo layout (`acdl_platform/`, `schemas/`, `adapters/`, `terraform/`, `modules-ir/`, `contracts/`, `demo/`), and the v1.2 objective. No stale "v1.1 (active)" framing.
|
||||
- **REQ-30:** NFR hardening of the v1.1 spike: (a) `terraform/bootstrap/spike_runner_policy.json` audited to least-privilege (S3 + DynamoDB + ECS + ECR + ELB + IAM plan-only, no wildcards beyond the documented exceptions); (b) `create_state_backend.py` and `create_iam_user.py` are idempotent (re-running exits 0 without duplicating resources); (c) `run_spike_plan.sh` + `run_spike_e2e.sh` consolidated into a single `scripts/run_platform.sh` with proper exit codes and error handling; (d) P1-1 carried forward from the v1.1 audit — the two AWS access key IDs in `.ciagent/VERIFY.md` Phase 09 narrative are redacted to placeholders; (e) any remaining stale `platform/` paths in `.ciagent/` are corrected to `acdl_platform/`.
|
||||
|
||||
### Category: L1 Catalog Expansion (ECS Fargate)
|
||||
- **REQ-31:** Six new IR-typed L1 modules exist under `modules-ir/l1/` and are registered in `modules-ir/registry.json`: `l1-vpc` (VPC + subnets + route tables), `l1-ecs-cluster` (ECS Fargate cluster), `l1-ecs-service` (ECS service + task definition), `l1-iam-role` (task execution + task role), `l1-alb` (application load balancer + listener + target group), `l1-ecr` (ECR repository). Each has an `interface.json` valid against `schemas/ir.schema.json` and produces a valid `terraform plan` fragment via the Terraform adapter. The adapter `TYPE_MAP` is expanded to cover all six IR resource types.
|
||||
|
||||
### Category: L2 Composition & Contract Schema
|
||||
- **REQ-32:** `l2-microservice` thin-composition exists under `modules-ir/l2/l2-microservice/` referencing the six ECS L1s (depth ≤ 5, within max-depth-5). `schemas/contract.schema.json` is extended with microservice inputs (`image: string`, `port: integer`, `env: map`, `healthcheck: object`) and validates a `contracts/microservice.yaml` submission. Contract→IR resolution (`acdl_platform/contract_resolver.py`) yields a complete target stack for `l2-microservice`.
|
||||
|
||||
### Category: Real Provisioning
|
||||
- **REQ-33:** The platform runs `terraform apply` (not just `plan`) for the `dev` environment, autonomous per §10 (confidence ≥ 0.50, no HITL). The apply creates real AWS resources (VPC, ECS cluster, ECR repo, ALB, ECS service) and the result is captured in the evidence stream. `apply` for qa/prod/dr remains HITL-gated and out of scope for v1.2.
|
||||
|
||||
### Category: Consumer Repo
|
||||
- **REQ-34:** A new Gitea repo `acdl-consumer-microservice` exists under the `continuous-intelligence` org, containing: a basic HTTP microservice (e.g., a tiny Python/Go server returning 200), a `Dockerfile`, an ECR push step, and a `contracts/microservice.yaml` submission for `l2-microservice` (dev environment).
|
||||
|
||||
### Category: End-to-End Verification
|
||||
- **REQ-35:** One end-to-end flow: consumer commit to `acdl-consumer-microservice` → pipeline triggered → contract→IR resolution → `terraform plan` → `terraform apply` (dev) → a live ECS Fargate service serving HTTP 200 on its ALB → evidence event written to the DynamoDB outbox → the event renders on the `acdl-evidence` timeline. `scripts/verify_phase16.sh` proves the full flow green.
|
||||
|
||||
## v1.3 (Prior — module documentation + thin-composition removal, complete)
|
||||
|
||||
### Category: Thin-Composition Removal
|
||||
- **REQ-36:** The L2 thin-composition layer is removed completely: `composition.json` files, `acdl_platform/contract_resolver.py`, `schemas/contract.schema.json`, `contracts/spike.yaml`, `contracts/microservice.yaml`, and L2 entries in `modules-ir/registry.json` are deleted. The L2 directories are kept as placeholders with READMEs. The downstream pipeline (adapter → checkov → confidence → outbox) is patched to load a pre-existing IR instance instead of resolving a contract.
|
||||
- **REQ-37:** A `modules-ir/README-TEMPLATE.md` exists that works for both L1 and L2 modules, written in plain language (no jargon), with sections for Overview, Resources, Inputs, Outputs, Usage, Compliance extension points, and Versioning.
|
||||
- **REQ-38:** Every module has a `README.md`: the 7 L1 modules have full READMEs with Resources/Inputs/Outputs/Usage/Compliance-extension-points/Versioning sections derived from their `interface.json`; the 2 L2 modules have placeholder READMEs noting the composition is under redesign. A `modules-ir/README.md` catalog index lists all modules with one-line descriptions and links.
|
||||
|
||||
### Category: Testing
|
||||
- **REQ-39:** A pytest test suite exists under `tests/` covering the platform components offline (no AWS, no Checkov, no DynamoDB): the Terraform adapter (`adapters/terraform/adapter.py`), the confidence signal (`acdl_platform/confidence_signal.py`), the Checkov adapter (`adapters/terraform/policy/checkov_adapter.py`), and the outbox writer (`acdl_platform/outbox_writer.py`). The suite validates the IR schema, registry, spike_instance, and adapter output structure. `pyproject.toml` + `requirements-test.txt` pin test dependencies (pytest, jsonschema, pyyaml, boto3-stubs or moto for outbox mocking).
|
||||
|
||||
### Category: Shell Reproducibility
|
||||
- **REQ-40:** `scripts/run_platform.sh` has a `--check-only` mode that runs offline: loads the pre-existing IR instance, runs the adapter to emit Terraform, validates the JSON structure — without AWS credentials, Checkov, or DynamoDB. The existing `--plan-only` and full modes continue to require AWS. The `--check-only` mode is what CI pipelines run.
|
||||
|
||||
### Category: CI/CD Pipelines
|
||||
- **REQ-41:** Identical CI/CD pipelines exist for both Gitea Actions (`.gitea/workflows/ci.yml`, dev environment) and GitHub Actions (`.github/workflows/ci.yml`, production). Both run the same three stages: (1) lint — `py_compile` all Python files, (2) test — `pytest`, (3) check-only — `bash scripts/run_platform.sh --check-only`. Both trigger on push to main + pull request. Both use `ubuntu-latest`. Identical outcomes — the only difference is the runner environment.
|
||||
|
||||
- **REQ-42:** `pyproject.toml` exists at the repo root with pytest configuration (testpaths, markers) and the project metadata. `requirements-test.txt` pins test-only dependencies separate from runtime dependencies.
|
||||
|
||||
## v1.4 (Active — central pipeline contract + shell reproducibility + streaming)
|
||||
|
||||
### Category: Central Pipeline Contract
|
||||
- **REQ-43:** A central pipeline contract exists as `schemas/pipeline.schema.json` (JSON Schema draft 2020-12) + `pipelines/ci.yaml` (YAML instance). The contract declares the pipeline name, triggers (push/PR branches), runner, Python version, and stages (name + command + required + install + description). Both `.gitea/workflows/ci.yml` (Gitea Actions, dev) and `.github/workflows/ci.yml` (GitHub Actions, production) implement the same stages, commands, triggers, and runner as declared in the contract. A test (`tests/test_pipeline_contract.py`) validates the contract against the schema and asserts both workflows conform (same jobs, same commands, same triggers, same runner, byte-identical).
|
||||
|
||||
### Category: Shell Reproducibility
|
||||
- **REQ-44:** `scripts/run_ci.sh` reproduces the CI pipeline locally — runs the same 3 stages (lint, test, check-only) in sequence with proper exit codes, failing on first error. The script exits 0 with "CI PIPELINE OK" on success. A `--quiet` flag suppresses per-stage banners. The script mirrors the central pipeline contract (`pipelines/ci.yaml`) so the shell and CI environments produce identical outcomes.
|
||||
|
||||
### Category: Pipeline Streaming
|
||||
- **REQ-45:** `scripts/run_platform.sh` streams output by default: terraform init/validate/plan output is piped to stdout via `tee` (visible to the user and logged), Checkov results are printed in human-readable form, and PolicyCheckResult records are displayed with severity, rule ID, and pass/fail status per record. The `--check-only` mode streams the emitted Terraform file content. A `--quiet` flag suppresses streaming (output to log files only) for backwards compatibility. Both gitea and github workflows are byte-identical (identical outcomes — the only difference is the forge runtime).
|
||||
|
||||
## v1.5 (Prior — consumer happy path + zero-trust docs + reusable deploy workflow, complete)
|
||||
|
||||
### 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-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.
|
||||
|
||||
### 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-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.
|
||||
|
||||
## 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)
|
||||
|
||||
| REQ | Original criterion | Clarified criterion (effective) | Decision |
|
||||
|-----|--------------------|----------------------------------|----------|
|
||||
@@ -43,7 +230,7 @@
|
||||
| REQ-10 | "Pages returns 200 with placeholder `index.html`" on `acdl-evidence` | Gitea has no Pages; substitute: an HTTP GET against the raw file URL `https://git.cloudinit.dev/continuous-intelligence/acdl-evidence/raw/branch/main/index.html` returns 200 with the placeholder HTML body | D-012, D-016 |
|
||||
| REQ-10 | "`qa` and `prod` environments exist on `acdl-contracts`" | Gitea has no environments API and ignores `environment:` blocks; substitute: the reusable workflow defines `qa-gate` and `prod-gate` jobs gated by `workflow_dispatch` approval inputs (D-004 fallback); a `qa` and `prod` branch may be created on `acdl-contracts` as a visible stand-in for environments | D-013 |
|
||||
|
||||
## Out of Scope
|
||||
## Out of Scope (v1.0 demo — retained for history)
|
||||
|
||||
| Feature | Reason |
|
||||
|---------|--------|
|
||||
@@ -53,22 +240,196 @@
|
||||
| Adversarial tamper-proofing of evidence | Hash chain is demonstrative; not cryptographically secure against a determined attacker. |
|
||||
| Multi-tenant isolation | Out of demo scope. |
|
||||
|
||||
## v1.9 (complete — design doc refresh + contract interpolation + per-env CI jobs + stub implementation + P1-1 remediation, tag `v1.9.0`)
|
||||
|
||||
### Category: Design Doc Refresh
|
||||
- **REQ-100:** `core/hitl_matrix_design.md` is up to date: the "dev-only spike" framing is replaced with the v1.9 wired-gates reality (qa/prod/dr `workflow_dispatch` approval gates + CODEOWNERS routing + outbox-based SoD); the 8-concern attestation matrix is marked implemented (offline-testable subset) with operator-supplied concerns noted; the spike-scope note is updated. No stale "v1.2 wires the gates" language remains.
|
||||
- **REQ-101:** `core/audit_ledger_design.md` is up to date: the hash-chain + DynamoDB-outbox path is marked shipped + production (since v1.8); the S3 Object Lock + JWS + async worker + DLQ + daily checkpoints build-out is clearly labeled "Deferred to a future milestone" (D-083); the RPO/RTO table reflects the v1.9 state.
|
||||
|
||||
### Category: P1-1 Remediation
|
||||
- **REQ-102:** The adapter (`adapters/terraform/adapter.py`) contains no resource-type-specific hardcoded defaults for ECS/ALB/VPC resources — `desired_count`, `launch_type`, `target_type`, `load_balancer_type`, `family`, and `Name` tag values are read from L1 `interface.json` inputs (with defaults declared in the interface). The adapter is a thin translator. An L1 with an overridden `desired_count: 3` emits `desired_count = 3`; the default emits `desired_count = 1` via the interface default, not an adapter hardcode (P1-1 closed).
|
||||
|
||||
### Category: Contract Interpolation
|
||||
- **REQ-103:** The contract resolver (`core/contract_resolver.py`) expands `${env.<field>}` and `${contract.<field>}` tokens in contract string values (including dotted paths like `${env.state_backend.bucket}`) after schema validation and before IR resolution. The `env` context is the loaded `core/environments/<contract.environment>.json`; the `contract` context is the contract dict. Unresolved tokens raise `ValueError` (fail loud). Sample contracts use naming patterns that include region, account id, and environment (e.g. `acdl-${env.environment}-${contract.module}-${env.account_id}-${env.region}`).
|
||||
- **REQ-104:** An environment JSON schema `schemas/environment.schema.json` (draft 2020-12) defines the environment file shape (`name`, `account_id`, `region`, `state_backend`, `network`, `runner_role_arn`, `autonomy`, `confidence_threshold`). `core/environments/dev.json` validates against it. `qa.json`, `prod.json`, `dr.json` placeholder bindings exist (autonomy `attested`, thresholds 0.75/0.90/0.95).
|
||||
|
||||
### Category: Per-Environment CI Jobs
|
||||
- **REQ-105:** Per-environment contract files exist for each sample module (`contracts/static-assets.{dev,qa,prod,dr}.yaml` and `contracts/microservice.{dev,qa,prod,dr}.yaml`), each setting `environment:` to its own name and using interpolation for env-specific values. The existing `contracts/static-assets.yaml` + `contracts/microservice.yaml` remain as the dev default for backwards compatibility.
|
||||
- **REQ-106:** The reusable deploy workflow (`.github/workflows/deploy.yml` + `.gitea/workflows/deploy.yml`, byte-identical) declares an `environment` `workflow_call` input (enum dev/qa/prod/dr, default empty). When non-empty, `scripts/run_platform.sh --environment <name>` overrides the contract's `environment` field at load time (before interpolation). A consumer repo's caller workflow has one job per environment, each pointing at its respective contract (or the same contract + the env input). Promotion = running the matching job; no `environment:` field editing. `docs/CONSUMER_GUIDE.md` documents the per-env caller workflow pattern.
|
||||
|
||||
### Category: Stub Implementation
|
||||
- **REQ-107:** `core/separation_of_duties.py` `route_halt_artifact` is a real implementation: publishes to an SNS topic `acdl-sod-halt` (ARN from `ACDL_SOD_HALT_TOPIC_ARN`); when unset, falls back to a structured stderr emission + a `SEPARATION_OF_DUTIES_VIOLATION` event write to the DynamoDB outbox via `outbox_writer.write_event`. No silent print-only stub. The SNS topic is defined in `terraform/platform/main.tf`.
|
||||
- **REQ-108:** HITL qa/prod/dr pre-execution attestation gates are wired via `core/hitl_gates.py` (`attest(contract_id, env, approver, evidence)`). The gate records the approver (`gitea.actor` / `github.actor`) to the outbox (`approver_qa` / `approver_prod` / `approver_dr` attributes per `audit_ledger_design.md`), runs the separation-of-duties check on prod, and returns `(ok, reason)`. `scripts/run_platform.sh` calls `hitl_gates.attest` before apply for qa/prod/dr (dev skips). The workflow's `workflow_dispatch` approval input is the trigger.
|
||||
- **REQ-109:** The full 8-concern attestation matrix from `hitl_matrix_design.md` §10.4 is implemented in `core/attestation_matrix.py`. Offline-testable concerns (contract NFRs, schema validity, policy pass) run for real; operator-supplied concerns (k6 load test, DR drill, FinOps forecast) accept an uploaded signed evidence artifact validated for freshness + schema, failing loud if missing/expired for prod/dr. `hitl_gates.attest` invokes the matrix for the target env and blocks on any failing concern.
|
||||
- **REQ-110:** The Wiz adapter (`adapters/wiz/wiz_adapter.py`) is a real API client: a `WizClient` queries the Wiz GraphQL API (`WIZ_API_TOKEN` + `WIZ_API_URL`) and translates issues → `PolicyCheckResult` records. It degrades gracefully (existing `WIZ_NOT_CONFIGURED` SKIPPED record) when env unset. Offline tests use a recorded GraphQL fixture.
|
||||
- **REQ-111:** The Kyverno adapter (`adapters/kyverno/kyverno_adapter.py`) translator is fleshed out: full `PolicyReport` → `PolicyCheckResult` mapping with severity + skip handling. It remains inactive for Terraform-only stacks (guard preserved); a `--kube-version` stub is added for future GitOps. Sample policies already exist.
|
||||
|
||||
## Out of Scope (v1.9)
|
||||
|
||||
| Feature | Reason |
|
||||
|---------|--------|
|
||||
| S3 Object Lock + JWS + async worker + DLQ + daily checkpoints (audit ledger build-out) | Requires non-offline-testable AWS infra (Object Lock bucket, KMS signing key, SQS DLQ, Lambda worker). Deferred to a future milestone (D-083). The hash-chain + DynamoDB-outbox path remains the v1.9 production audit record. |
|
||||
| Live k6/Gatling load test execution, live DR drill, live FinOps forecast | Operator-supplied evidence artifacts (signed blobs) are accepted + validated; the platform does not run these inline. |
|
||||
| Self-service environment provisioning | Adding an environment remains a platform-team action (per `core/environments/README.md`). v1.9 adds the env files + schema, not self-service provisioning. |
|
||||
|
||||
## Traceability
|
||||
|
||||
### v1.0 (prior — demo)
|
||||
|
||||
| Requirement | Phase | Status |
|
||||
|-------------|-------|--------|
|
||||
| REQ-01 | 1 | complete (v1.0.1) |
|
||||
| REQ-02 | 2 | covered (pending VERIFY) |
|
||||
| REQ-03 | 2 | covered (pending VERIFY) |
|
||||
| REQ-04 | 3 | pending |
|
||||
| REQ-05 | 3 | pending |
|
||||
| REQ-06 | 3 | pending |
|
||||
| REQ-07 | 3 | pending |
|
||||
| REQ-08 | 3 | pending |
|
||||
| REQ-02 | 2 | complete (v1.0.2) |
|
||||
| REQ-03 | 2 | complete (v1.0.2) |
|
||||
| REQ-04 | 3 | complete (v1.0.3) |
|
||||
| REQ-05 | 3 | complete (v1.0.3) |
|
||||
| REQ-06 | 3 | complete (v1.0.3) |
|
||||
| REQ-07 | 3 | complete (v1.0.3) |
|
||||
| REQ-08 | 3 | complete (v1.0.3) |
|
||||
| REQ-09 | 1 | complete (v1.0.1) |
|
||||
| REQ-10 | 4 | partial (skeleton in Phase 01 v1.0.1; full impl in Phase 04) |
|
||||
| REQ-11 | 3 | pending |
|
||||
| REQ-12 | 4 | partial (skeleton in Phase 01 v1.0.1; full impl in Phase 04) |
|
||||
| REQ-13 | 5 | pending |
|
||||
| REQ-14 | 5 | pending |
|
||||
| REQ-15 | 5 | pending |
|
||||
| REQ-10 | 4 | complete (v1.0.4) |
|
||||
| REQ-11 | 3 | complete (v1.0.3) |
|
||||
| REQ-12 | 4 | complete (v1.0.4) |
|
||||
| REQ-13 | 5 | complete (v1.0.5) |
|
||||
| REQ-14 | 5 | complete (v1.0.5) |
|
||||
| REQ-15 | 5 | complete (v1.0.5) |
|
||||
|
||||
### v1.1 (prior — architecture finalization + v1 spike, complete)
|
||||
|
||||
| Requirement | Phase | Status |
|
||||
|-------------|-------|--------|
|
||||
| REQ-16 | 07 | complete (v1.1.2) |
|
||||
| REQ-17 | 07 | complete (v1.1.2) |
|
||||
| REQ-18 | 07 | complete (v1.1.2) |
|
||||
| REQ-19 | 07 | complete (v1.1.2) |
|
||||
| REQ-20 | 07 | complete (v1.1.2) |
|
||||
| REQ-21 | 07 | complete (v1.1.2) |
|
||||
| REQ-22 | 07 | complete (v1.1.2) |
|
||||
| REQ-23 | 08 | complete (v1.1.3) |
|
||||
| REQ-24 | 09 | complete (v1.1.4) |
|
||||
| REQ-25 | 10 | complete (v1.1.5) |
|
||||
| REQ-26 | 09 | complete (v1.1.4) |
|
||||
| REQ-27 | 10 | complete (v1.1.5) |
|
||||
| REQ-28 | 10 | complete (v1.1.5) |
|
||||
|
||||
### v1.2 (prior — platform hardening + first real consumer deployment, complete)
|
||||
|
||||
| Requirement | Phase | Status |
|
||||
|-------------|-------|--------|
|
||||
| REQ-29 | 11 | complete (v1.2.1) |
|
||||
| REQ-30 | 12 | complete (v1.2.2) |
|
||||
| REQ-31 | 13 | complete (v1.2.3) |
|
||||
| REQ-32 | 14 | complete (v1.2.4) |
|
||||
| REQ-33 | 15 | partial (v1.2.5, IAM-blocked) |
|
||||
| REQ-34 | 15 | complete (v1.2.5) |
|
||||
| REQ-35 | 16 | partial (v1.2.6, IAM-blocked) |
|
||||
|
||||
### v1.3 (prior — module documentation + thin-composition removal, complete)
|
||||
|
||||
| Requirement | Phase | Status |
|
||||
|-------------|-------|--------|
|
||||
| REQ-36 | 17 | complete (v1.3.1) |
|
||||
| REQ-37 | 17 | complete (v1.3.1) |
|
||||
| REQ-38 | 17 | complete (v1.3.1) |
|
||||
| REQ-39 | 18 | complete (v1.3.2) |
|
||||
| REQ-40 | 18 | complete (v1.3.2) |
|
||||
| REQ-41 | 18 | complete (v1.3.2) |
|
||||
| REQ-42 | 18 | complete (v1.3.2) |
|
||||
|
||||
### v1.4 (prior — central pipeline contract + shell reproducibility + streaming)
|
||||
|
||||
| Requirement | Phase | Status |
|
||||
|-------------|-------|--------|
|
||||
| REQ-43 | 19 | complete (v1.4.1) |
|
||||
| REQ-44 | 19 | complete (v1.4.1) |
|
||||
| REQ-45 | 19 | complete (v1.4.1) |
|
||||
|
||||
### v1.5 (prior — consumer happy path + zero-trust docs + reusable deploy workflow, complete)
|
||||
|
||||
| Requirement | Phase | Status |
|
||||
|-------------|-------|--------|
|
||||
| REQ-46 | 20 | complete (v1.5.0) |
|
||||
| REQ-47 | 20 | complete (v1.5.0) |
|
||||
| REQ-48 | 20 | complete (v1.5.0) |
|
||||
| REQ-49 | 20 | complete (v1.5.0) |
|
||||
| REQ-50 | 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) |
|
||||
### v1.9 (complete — design doc refresh + contract interpolation + per-env CI jobs + stub implementation + P1-1 remediation, tag `v1.9.0`)
|
||||
|
||||
| Requirement | Phase | Status |
|
||||
|-------------|-------|--------|
|
||||
| REQ-100 | 39 | complete (v1.9.0) |
|
||||
| REQ-101 | 39 | complete (v1.9.0) |
|
||||
| REQ-102 | 39 | complete (v1.9.0) |
|
||||
| REQ-103 | 40 | complete (v1.9.0) |
|
||||
| REQ-104 | 40 | complete (v1.9.0) |
|
||||
| REQ-105 | 41 | complete (v1.9.0) |
|
||||
| REQ-106 | 41 | complete (v1.9.0) |
|
||||
| REQ-107 | 42 | complete (v1.9.0) |
|
||||
| REQ-108 | 42 | complete (v1.9.0) |
|
||||
| REQ-109 | 42 | complete (v1.9.0) |
|
||||
| REQ-110 | 42 | complete (v1.9.0) |
|
||||
| REQ-111 | 42 | complete (v1.9.0) |
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,165 @@
|
||||
# ACDL v1.9 Milestone — Multi-Persona Code Review
|
||||
|
||||
**Reviewer:** ci-code-reviewer (model: glm-5.2)
|
||||
**Scope:** v1.9 milestone — Phases 39–42 (tags v1.8.1..v1.8.4), diff `v1.8.0..HEAD`
|
||||
**Date:** 2026-07-23
|
||||
**Verdict:** **READY TO SHIP** — 1 P0 auto-fixed, 1 P1 auto-fixed, 3 P1 flagged for post-hoc
|
||||
|
||||
> **Note (D-086):** This REVIEW.md was reconstructed at v1.9 complete.
|
||||
> The previous content was the v1.2 milestone review (v1.3–v1.8 reviews
|
||||
> were not persisted to this file). No git history was rewritten; the
|
||||
> v1.2 review is preserved in git history at the v1.2 review commit.
|
||||
>
|
||||
> **Review pass 2 (post-complete):** this review was re-run after the
|
||||
> milestone COMPLETE to catch issues the initial self-review missed. The
|
||||
> P0 (approver injection) and P1 (future-dated freshness) were auto-fixed.
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
v1.9 closes four gaps left by v1.8 (user-directed, 2026-07-23): stale
|
||||
design docs, no contract interpolation, promotion requires editing the
|
||||
`environment` field, and unimplemented stubs. It also closes P1-1
|
||||
(adapter hardcoded defaults, deferred from v1.2). 4 phases shipped
|
||||
(39–42): design-doc refresh + P1-1 parameterization, contract
|
||||
interpolation + env schema, per-environment CI jobs, stub implementation.
|
||||
|
||||
## P0 issues
|
||||
|
||||
### P0-INJECT (auto-fixed)
|
||||
**Shell→Python code injection via `GITHUB_ACTOR` in `scripts/run_platform.sh`
|
||||
Step 7b (HITL gate).** The approver identity was interpolated directly
|
||||
into a Python string literal (`attest('$CONTRACT_ID', '$RESOLVED_ENV',
|
||||
'$APPROVER' ...)`). `GITHUB_ACTOR` (and `GITEA_ACTOR`) are attacker-
|
||||
controllable in some CI configurations; a username containing `'; import
|
||||
os; os.system(...); y='` would execute arbitrary Python.
|
||||
|
||||
**Fix (auto-applied):** the approver, contract id, and env are now passed
|
||||
as environment variables to the Python subprocess
|
||||
(`ACDL_HITL_CONTRACT_ID`, `ACDL_HITL_ENV`, `ACDL_HITL_APPROVER`) and read
|
||||
via `os.environ[...]` inside the Python code — no string interpolation of
|
||||
user-controllable values.
|
||||
|
||||
## P1 issues
|
||||
|
||||
### P1-FRESHNESS (auto-fixed)
|
||||
**`core/attestation_matrix.py` `_is_fresh` accepted future-dated
|
||||
artifacts.** A `timestamp` in the future produced a negative `age`, and
|
||||
`age.days <= window_days` evaluated `True` for negative values, so a
|
||||
backdated/future artifact bypassed freshness validation.
|
||||
|
||||
**Fix (auto-applied):** added a `age.total_seconds() < 0` guard that
|
||||
rejects future-dated artifacts. Test added
|
||||
(`test_freshness_rejects_future_dated_artifact`).
|
||||
|
||||
### P1-WIZ-ERRORS (flagged for post-hoc)
|
||||
**`adapters/wiz/wiz_adapter.py` `WizClient._post` does not check for
|
||||
GraphQL `errors` in the response.** A GraphQL API returns
|
||||
`{data: ..., errors: [...]}`; if `errors` is present, `data.issues` can
|
||||
be `null` and `.get("nodes", [])` silently masks the error as an empty
|
||||
list (which then emits `WIZ_NOT_CONFIGURED`). Should surface GraphQL
|
||||
errors as a failed PolicyCheckResult or raise.
|
||||
|
||||
### P1-WIZ-SSRF (flagged for post-hoc)
|
||||
**`WizClient._post` performs no SSRF validation on `WIZ_API_URL`.** A
|
||||
malicious `WIZ_API_URL` env var could target an internal endpoint. The
|
||||
URL is operator-supplied (not consumer-controllable), so the risk is
|
||||
low, but a allowlist/scheme check (`https://`) would harden it.
|
||||
|
||||
### P1-OBSOLETE-CHECK (flagged for post-hoc)
|
||||
**`core/contract_resolver.py` `_load_env` duplicates
|
||||
`core/environment_check.load`.** The duplication was intentional (so the
|
||||
resolver works as both a package import and a script), but the two can
|
||||
drift. A future refactor should extract a shared helper that both
|
||||
import safely.
|
||||
|
||||
## Per-lens review
|
||||
|
||||
### Correctness
|
||||
- The contract interpolation (`_expand_vars`) is recursive over
|
||||
dicts/lists/strings; unknown tokens raise `ValueError` (fail loud).
|
||||
Expansion is post-schema-validation, pre-IR-resolution — the schema
|
||||
sees raw tokens (valid strings), the resolver sees concrete values.
|
||||
- The `environment_override` (D-088) is applied BEFORE schema validation
|
||||
so the interpolation context is consistent.
|
||||
- P1-1: the adapter reads `desired_count`, `launch_type`, `family`,
|
||||
`target_type`, `load_balancer_type` from inputs (with interface
|
||||
defaults). The resolver's `child_input_map` routes wires to the
|
||||
sub-resource that declares the input (desired_count → aws:ecs:service,
|
||||
family → aws:ecs:task_definition). The v1.1 S3 regression is preserved
|
||||
(byte-identical `main.tf` for S3-only stacks).
|
||||
- The HITL attestation gate records the approver to the outbox, runs SoD
|
||||
on prod (blocks on `approver_qa == approver_prod`), invokes the
|
||||
attestation matrix. Dev skips (autonomous).
|
||||
- The attestation matrix's freshness validation uses the §10.4 windows;
|
||||
signature verification skips when the signing key is unset (D-089) and
|
||||
is required when set.
|
||||
- The Wiz real client uses the GraphQL API with pagination; graceful
|
||||
degrade when unconfigured.
|
||||
- The Kyverno translator handles pass/fail/skip/warn + severity + skip-
|
||||
with-reason + resource construction; the inactive-for-TF guard is
|
||||
preserved.
|
||||
|
||||
### Testing
|
||||
- 493 offline tests (was 350 at v1.8 → 493 at v1.9, +143 new). Each new
|
||||
feature has dedicated tests:
|
||||
- P1-1: `test_p1_1_adapter_parameterization.py` (override + default + regression).
|
||||
- Design docs: `test_design_docs_current.py` (no stale framing).
|
||||
- Interpolation: `test_interpolation.py` + `test_sample_contracts_interpolate.py`
|
||||
+ `test_environment_schema.py`.
|
||||
- Per-env jobs: `test_per_env_contracts.py` + `test_deploy_workflow_env_input.py`
|
||||
+ `test_consumer_guide_per_env_section.py`.
|
||||
- Stubs: `test_route_halt_artifact.py` + `test_hitl_gates.py` +
|
||||
`test_attestation_matrix.py` + `test_wiz_adapter_real_client.py` +
|
||||
expanded `test_kyverno_adapter.py`.
|
||||
- `run_ci.sh` exits 0; `run_platform.sh --check-only` exits 0.
|
||||
|
||||
### Security
|
||||
- No credentials introduced. The SNS topic is KMS-encrypted.
|
||||
- SoD blocks on identity equality; the halt artifact is in the audit chain.
|
||||
- The attestation matrix fails loud on missing/expired evidence for prod/dr.
|
||||
- Signature verification is required when the signing key is set.
|
||||
- The adapter has no hardcoded resource defaults (P1-1 closed) — defaults
|
||||
live in the L1 interface, not the adapter.
|
||||
|
||||
### Performance
|
||||
- N/A (this milestone is about correctness + design-doc accuracy + stub
|
||||
implementation, not perf).
|
||||
|
||||
### Maintainability
|
||||
- The interpolation is a single recursive walker; the env context is
|
||||
loaded via a self-contained `_load_env` (works as script + package import).
|
||||
- The `child_input_map` makes multi-resource L1 wire routing deterministic
|
||||
(the sub-resource that declares the input receives the value).
|
||||
- The attestation matrix's concern lists + freshness table are data-driven
|
||||
(adding a concern is a table extension, not new logic).
|
||||
- The Wiz `WizClient` is a clean class with a single `_post` seam (testable
|
||||
with `mock.patch.object`).
|
||||
|
||||
### Adversarial
|
||||
- The interpolation fail-loud (`ValueError` on unknown tokens) prevents
|
||||
silent mis-resolution — a typo in a token name surfaces immediately,
|
||||
not as a stale literal in the emitted Terraform.
|
||||
- The `environment_override` is applied before schema validation, so a
|
||||
contract with `environment: dev` cannot silently interpolate against
|
||||
the dev env when the workflow passes `environment: prod` — the override
|
||||
is authoritative.
|
||||
- The SoD check reads `approver_qa` from the outbox (the platform is the
|
||||
only writer); a consumer cannot forge the approver identity.
|
||||
- The attestation matrix's signature skip is explicit + logged (not silent).
|
||||
|
||||
## Conclusion
|
||||
|
||||
v1.9 is READY TO SHIP after the review auto-fixes. 1 P0 (approver
|
||||
injection — auto-fixed by passing env vars instead of string
|
||||
interpolation) and 1 P1 (future-dated freshness — auto-fixed with a
|
||||
negative-age guard + test). 3 P1 flagged for post-hoc (Wiz GraphQL
|
||||
error handling, Wiz SSRF validation, `_load_env` duplication). The
|
||||
milestone's code is complete + verified: design docs are current,
|
||||
contract interpolation works, per-env promotion requires no field
|
||||
editing, all stubs are implemented (audit ledger Object Lock/JWS
|
||||
build-out deferred per D-083), and P1-1 is closed. Ship tag: `v1.9.0`
|
||||
(feature milestone, next minor per run.md — v1.8 shipped `v1.8.0`).
|
||||
|
||||
494 offline tests pass (was 350 at v1.8, +144 new); `run_ci.sh` + `run_platform.sh --check-only` green.
|
||||
+587
-6
@@ -2,7 +2,30 @@
|
||||
|
||||
## Overview
|
||||
|
||||
Five-phase breakdown to take ACDL from empty repo to a reproducible 4-act executive demo. Milestone `v1.0-initial` covers the full demo build. Each phase produces a runnable increment and ends with a phase-completion commit + tag.
|
||||
- **v1.0 (demo):** complete — tag `v1.1.0`, 2026-07-21. All 5 phases shipped + audited PASS.
|
||||
- **v1.1 (complete):** architecture finalization + v1 spike. 5 phases (06–10). Tag `v1.2.0`, 2026-07-21. All 5 phases shipped + verified; review READY TO SHIP (0 P0); audit CLEAN. Gitea release id 202.
|
||||
- **v1.2 (complete):** platform hardening + first real consumer deployment. 6 phases (11–16). Tag `v1.3.0`, 2026-07-21. All 6 phases shipped + verified; review READY TO SHIP (1 P0 operator action, 1 P1 deferred); audit CLEAN.
|
||||
- **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.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.9.1 (complete, tag `v1.9.1`):** leadership presentation decks. Two leadership-facing presentation decks (How the Platform Works + The Developer Experience) for senior leadership (CTO, Head of Cloud, Head of Infrastructure, Head of DevOps). Each deck has a full markdown source of truth (with speaker notes + mermaid diagrams) and a lean Marp deck (no speaker notes, embedded PNG diagrams). A README documents the 3-step slide creation process (full markdown → Marp synthesis → PPTX export). Docs-only NFR patch.
|
||||
- **v1.9.2 (complete, tag `v1.9.2`):** S&P Global Energy theme for presentation decks. Applies the S&P Global Energy brand visual identity (red-core #D6002A, grey-90 #1B1B1B, Akkurat Pro font) to both Marp decks. Title headers changed to full platform name. Footer 'Confidential' → 'Internal'. Title slide subtitle removed. Last DX slide renamed to 'The Desired Outcomes'. Docs-only NFR patch.
|
||||
- **v1.9.3 (complete, tag `v1.9.3`):** rendered presentation decks. HTML renderings of both Marp decks committed to docs/presentations/ (self-contained, base64-embedded images, S&P Global Energy theme). PPTX files uploaded to the Gitea release as downloadable attachments. README updated to document HTML as committed artifacts and PPTX as release attachments. Docs-only NFR patch.
|
||||
- **v1.9.4 (complete, tag `v1.9.4`):** presentation slide updates + complete removal of a specific compliance framework from all docs. Title slide redesigned (deck title as H1, 'Agentic Cloud Delivery Platform' as subtitle). DX deck: removed Local Reproducibility slide, redesigned Safe Promotion Path with side-by-side layout, 'an agent' → 'an AI agent', What a Developer Does diagram floated right. All references to that framework removed from 25 files (presentations, module READMEs, docs). Compliance lists now: GDPR, SOX, SOC2, DORA. HTML re-rendered. PPTX uploaded to release. Docs-only NFR patch.
|
||||
- **v1.9.5 (complete, tag `v1.9.5`):** vision gaps + Testing badge + engine terminology + agentic tags + CR format. 9 requirements: (1) DX closing slide strengthened with 'infrastructure as a utility' vision bullet; (2) 'moving' → 'promoting'; (3) added red tape + scalability bullets to Problem slide; (4) Roadmap slide redesigned side-by-side; (5) new 'What This Platform Is — and Isn't' slide (PW deck 16 slides); (6) 'shipped'/'Available today' → 'Testing' (0 consumer adoption); (7) global 'substrate' → 'engine' (88 matches, 30+ files); (8) 'forge' → 'VCS' in presentation files only; (9) new Agentic badge (purple) on agentic features. CR format changed to CHG0678912. HTML re-rendered. PPTX uploaded to release. Docs-only NFR patch.
|
||||
- **v1.0 demo URL:** https://git.cloudinit.dev/continuous-intelligence/acdl-evidence/raw/branch/main/index.html
|
||||
|
||||
---
|
||||
|
||||
## v1.0 (Prior — the demo, complete)
|
||||
|
||||
Five-phase breakdown that took ACDL from empty repo to a reproducible 4-act
|
||||
executive demo. Milestone `v1.0-initial` covered the full demo build. Each
|
||||
phase produced a runnable increment and ended with a phase-completion commit
|
||||
+ tag. All phases complete; demo archived to `demo/` in v1.1 Phase 06.
|
||||
|
||||
## Phases
|
||||
|
||||
@@ -18,7 +41,7 @@ Five-phase breakdown to take ACDL from empty repo to a reproducible 4-act execut
|
||||
|
||||
### Phase 02 — l1-modules
|
||||
- **Description:** Create all 8 L1 module folders under `acdl/modules/l1/`, each with `manifest.yaml` (declared inputs) and `mock_apply.sh` (uniform echo + 1s sleep + exit 0).
|
||||
- **Status:** executing
|
||||
- **Status:** complete (v1.0.2)
|
||||
- **Depends on:** [1]
|
||||
- **Requirements:** REQ-02, REQ-03
|
||||
- **Success Criteria:**
|
||||
@@ -27,7 +50,7 @@ Five-phase breakdown to take ACDL from empty repo to a reproducible 4-act execut
|
||||
|
||||
### Phase 03 — l2-modules-and-core-scripts
|
||||
- **Description:** Create the 4 L2 compositions under `acdl/modules/l2/` referencing L1s, plus the 5 core scripts in `acdl/scripts/` (`mock_executor.sh`, `policy_checker.py`, `confidence_signal.py`, `evidence_writer.py`, `l3b_agent_stub.py`).
|
||||
- **Status:** not_started
|
||||
- **Status:** complete (v1.0.3)
|
||||
- **Depends on:** [2]
|
||||
- **Requirements:** REQ-04, REQ-05, REQ-06, REQ-07
|
||||
- **Success Criteria:**
|
||||
@@ -39,7 +62,7 @@ Five-phase breakdown to take ACDL from empty repo to a reproducible 4-act execut
|
||||
|
||||
### Phase 04 — pipeline-and-approval-gates
|
||||
- **Description:** Build the reusable pipeline workflow in `acdl/.gitea/workflows/` (Dev → QA → Prod → Finalize) plus the issue-triggered L3B workflow in `acdl-contracts/.gitea/workflows/`. Wire environment protection for QA and Prod.
|
||||
- **Status:** not_started
|
||||
- **Status:** complete (v1.0.4)
|
||||
- **Depends on:** [3]
|
||||
- **Requirements:** REQ-08, REQ-09, REQ-10, REQ-12
|
||||
- **Success Criteria:**
|
||||
@@ -49,11 +72,569 @@ Five-phase breakdown to take ACDL from empty repo to a reproducible 4-act execut
|
||||
|
||||
### Phase 05 — evidence-ui-and-demo-dry-run
|
||||
- **Description:** Build `index.html` (vanilla JS, fetches `audit.json`, renders timeline) and run all four acts end-to-end as a dry run.
|
||||
- **Status:** not_started
|
||||
- **Status:** complete (v1.0.5)
|
||||
- **Depends on:** [4]
|
||||
- **Requirements:** REQ-11, REQ-13, REQ-14, REQ-15
|
||||
- **Success Criteria:**
|
||||
- Pages timeline renders events from `audit.json`.
|
||||
- Act 2: valid contract passes through all gates; timeline shows the full flow.
|
||||
- Act 3: Issue text produces the expected `l2-commodity-price-feed` contract and triggers the pipeline.
|
||||
- Act 4: malicious `public-ingress: true` contract halts in Dev with confidence < 0.50 and a visible rejection reason on the timeline.
|
||||
- Act 4: malicious `public-ingress: true` contract halts in Dev with confidence < 0.50 and a visible rejection reason on the timeline.
|
||||
|
||||
---
|
||||
|
||||
## v1.1 (Complete — architecture finalization + v1 spike, 2026-07-21, tag `v1.2.0`)
|
||||
|
||||
Five-phase breakdown to finalize the architecture to v1.0 and prove the
|
||||
locked commitments with one end-to-end implementation spike. Milestone
|
||||
`v1.1-spike` covered the real platform's first materialization. Ship tag
|
||||
at milestone COMPLETE: **`v1.2.0`** (feature milestone, next minor per
|
||||
ship.md). **Status: COMPLETE — all 5 phases shipped (v1.1.1..v1.1.5) +
|
||||
verified; review READY TO SHIP (0 P0); audit CLEAN; Gitea release id 202.
|
||||
D-034 closed (root key deactivated by user).**
|
||||
|
||||
### Phase 06 — archive-demo-and-reorient
|
||||
- **Description:** Move the v1.0 demo (`modules/`, `scripts/`, `evidence-ui/`, `contracts/`, demo `.gitea/workflows/`) to `demo/`. Establish the new repo layout (`platform/`, `schemas/`, `adapters/`, `terraform/`, `modules-ir/`). Rewrite README to reflect the real platform. Verify the demo still runs from `demo/` (regression check).
|
||||
- **Status:** complete (v1.1.1)
|
||||
- **Depends on:** —
|
||||
- **Requirements:** (no new REQ; repo hygiene)
|
||||
- **Success Criteria:**
|
||||
- `demo/` contains the full v1.0 demo; `demo/scripts/run_demo.sh --no-upload` still exits 0.
|
||||
- New top-level dirs exist and are empty-but-scaffolded: `platform/`, `schemas/`, `adapters/`, `terraform/`, `modules-ir/`.
|
||||
- README reflects the real platform (vision + architecture links, new layout).
|
||||
|
||||
### Phase 07 — architecture-v1-finalization
|
||||
- **Description:** Resolve the 11 open decisions in `docs/architecture.md` §13 (already recorded in `PROJECT.md`). Author the locked schemas + designs: `schemas/ir.schema.json` (REQ-17), `schemas/policy_check_result.schema.json` (REQ-18), `schemas/contract.schema.json` (REQ-22), `platform/confidence_signal.py` spec (REQ-19), `platform/audit_ledger_design.md` (REQ-20), `platform/hitl_matrix_design.md` (REQ-21). Mark architecture v1.0.
|
||||
- **Status:** complete (v1.1.2)
|
||||
- **Depends on:** [06]
|
||||
- **Requirements:** REQ-16, REQ-17, REQ-18, REQ-19, REQ-20, REQ-21, REQ-22
|
||||
- **Success Criteria:**
|
||||
- All 11 open decisions resolved and recorded in `PROJECT.md`.
|
||||
- All 6 schema/design files exist and validate (`ajv` / `python -m jsonschema`).
|
||||
- `docs/architecture.md` status note updated to v1.0 (or a `docs/architecture-v1.0.md` snapshot).
|
||||
|
||||
### Phase 08 — aws-oidc-bootstrap
|
||||
- **Description:** **Re-scoped per RESEARCH TARGET 1 + D-039.** Gitea Actions does not support `id-token: write` (conf 0.95), so real OIDC is deferred to v1.2. This phase instead: uses the temporary long-lived key (waiver D-034) once to create an S3 state bucket, a DynamoDB lock/outbox table, and an IAM user with a minimal scoped policy (S3 + DynamoDB + plan-only); stores the key as a Gitea Actions secret; implements `scripts/rotate_spike_key.sh` to rotate the key after each spike run. Real OIDC federation is tracked via go-gitea/gitea#36988 for v1.2.
|
||||
- **Status:** complete (v1.1.3)
|
||||
- **Depends on:** [07]
|
||||
- **Requirements:** REQ-23 (re-interpreted: AWS auth bootstrap + state backend; OIDC deferred to v1.2 per D-039)
|
||||
- **Success Criteria:**
|
||||
- S3 state bucket + DynamoDB lock/outbox table exist.
|
||||
- An IAM user with a minimal scoped policy exists; its access key is stored as a Gitea Actions secret.
|
||||
- `scripts/rotate_spike_key.sh` rotates the key (deactivates old, creates new, updates the secret) and is idempotent.
|
||||
- A workflow step authenticates to AWS with the rotated secret and runs `aws sts get-caller-identity` successfully.
|
||||
- D-034 is closed: the bootstrap long-lived key is rotated/deactivated (logged in `PROJECT.md`).
|
||||
|
||||
### Phase 09 — v1-spike-ir-and-l1-and-adapter
|
||||
- **Description:** Implement the Target Stack IR, one real L1 `l1-s3` (IR-typed interface, registered), and the Terraform adapter that compiles the IR → Terraform `variable`/`output` + root module and emits a real `terraform plan` against AWS (via the rotated-key secret per D-039; OIDC is v1.2). State in S3 + DynamoDB.
|
||||
- **Status:** complete (v1.1.4)
|
||||
- **Depends on:** [08]
|
||||
- **Requirements:** REQ-24, REQ-26
|
||||
- **Success Criteria:**
|
||||
- `schemas/ir.schema.json` is satisfied by `modules-ir/l1/l1-s3/` interface.
|
||||
- The Terraform adapter translates `l1-s3` to a valid `terraform plan` (real AWS).
|
||||
- `terraform validate` + `terraform plan` succeed; no long-lived credential in the workflow.
|
||||
|
||||
### Phase 10 — v1-spike-l2-and-contract-e2e
|
||||
- **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)
|
||||
- **Depends on:** [09]
|
||||
- **Requirements:** REQ-25, REQ-27, REQ-28
|
||||
- **Success Criteria:**
|
||||
- `l2-static-assets` references `l1-s3` only (depth 1).
|
||||
- One contract submission completes the full pipeline end-to-end.
|
||||
- `scripts/verify_phase10.sh` proves the adapter is the only engine-specific code.
|
||||
- Evidence event is written to the DynamoDB outbox.
|
||||
|
||||
After Phase 10: COMPLETE gate — review → ship `v1.2.0` → audit. **DONE.**
|
||||
|
||||
---
|
||||
|
||||
## v1.2 (Complete — platform hardening + first real consumer deployment, 2026-07-21, tag `v1.3.0`)
|
||||
|
||||
Six-phase breakdown to harden the v1.1 spike, simplify the setup, update
|
||||
the docs, and prove the platform delivers real value by deploying a basic
|
||||
microservice to AWS ECS Fargate end-to-end. Ship tag at milestone COMPLETE:
|
||||
**`v1.3.0`** (feature milestone, next minor per ship.md — v1.1 shipped
|
||||
`v1.2.0`). Phase patches `v1.2.1`..`v1.2.6`. **Status: COMPLETE — all 6
|
||||
phases shipped (v1.2.1..v1.2.6) + verified; review READY TO SHIP (1 P0
|
||||
operator action, 1 P1 deferred to v1.3); audit CLEAN. The terraform apply
|
||||
is blocked by the live IAM policy (P0-IAM, operator action); the platform
|
||||
flow is verified end-to-end up to terraform plan (13 to add).**
|
||||
|
||||
### Phase 11 — v1.2-research-and-readme
|
||||
- **Description:** Re-evaluate go-gitea/gitea#36988 (OIDC for Gitea Actions) — confirm still open (re-checked 2026-07-21: open, last updated 2026-05-27, not merged) and record the decision to extend D-039 as D-047. Audit the v1.1 spike for NFR gaps (least-privilege IAM, idempotency, error handling, rotation hygiene) and simplification opportunities (script consolidation, dead code, stale paths). Rewrite `README.md` to reflect v1.1 complete + the actual spike flow + how to run + the real repo layout + the v1.2 objective.
|
||||
- **Status:** complete (v1.2.1)
|
||||
- **Depends on:** —
|
||||
- **Requirements:** REQ-29
|
||||
- **Success Criteria:**
|
||||
- `RESEARCH.md` has a v1.2 addendum with the #36988 re-check + NFR audit + simplification findings.
|
||||
- `README.md` reflects v1.1 complete; documents the spike flow, `scripts/run_platform.sh`, the repo layout, and the v1.2 objective; no stale "v1.1 (active)" framing.
|
||||
- D-047 is recorded in `PROJECT.md`.
|
||||
|
||||
### Phase 12 — nfr-harden-and-simplify
|
||||
- **Description:** Apply Phase 11's findings. Tighten `terraform/bootstrap/spike_runner_policy.json` to least-privilege (add ECS + ECR + ELB + IAM plan-only permissions for v1.2; audit for wildcards). Make `create_state_backend.py` and `create_iam_user.py` idempotent. Consolidate `run_spike_plan.sh` + `run_spike_e2e.sh` into a single `scripts/run_platform.sh` with proper exit codes and error handling. Redact P1-1 (the two AWS access key IDs in `.ciagent/VERIFY.md` Phase 09 narrative). Fix any remaining stale `platform/` paths in `.ciagent/`. The v1.1 spike still runs e2e after the refactor.
|
||||
- **Status:** complete (v1.2.2)
|
||||
- **Depends on:** [11]
|
||||
- **Requirements:** REQ-30
|
||||
- **Success Criteria:**
|
||||
- `scripts/run_platform.sh` runs the full v1.1 spike e2e and exits 0.
|
||||
- `create_state_backend.py` / `create_iam_user.py` re-runs are idempotent (no duplicate resources; exit 0).
|
||||
- `spike_runner_policy.json` passes a least-privilege audit (no `*` actions beyond documented exceptions).
|
||||
- `.ciagent/VERIFY.md` Phase 09 narrative has no live AWS access key IDs.
|
||||
- No stale `platform/` paths remain in `.ciagent/`.
|
||||
|
||||
### Phase 13 — l1-catalog-for-ecs
|
||||
- **Description:** Author six IR-typed L1 modules for an ECS Fargate microservice: `l1-vpc` (VPC + subnets + route tables), `l1-ecs-cluster` (ECS Fargate cluster), `l1-ecs-service` (ECS service + task definition), `l1-iam-role` (task execution + task role), `l1-alb` (ALB + listener + target group), `l1-ecr` (ECR repository). Each has an `interface.json` valid against `schemas/ir.schema.json`. Register all six in `modules-ir/registry.json`. Expand the Terraform adapter `TYPE_MAP` to cover the new IR resource types. Each L1 produces a valid `terraform plan` fragment.
|
||||
- **Status:** complete (v1.2.3)
|
||||
- **Depends on:** [12]
|
||||
- **Requirements:** REQ-31
|
||||
- **Success Criteria:**
|
||||
- All six L1s exist under `modules-ir/l1/` with `interface.json` valid against `schemas/ir.schema.json`.
|
||||
- `modules-ir/registry.json` lists all six.
|
||||
- The adapter `TYPE_MAP` covers all six IR resource types.
|
||||
- Each L1 produces a valid `terraform plan` fragment.
|
||||
|
||||
### Phase 14 — l2-microservice-and-contract-schema
|
||||
- **Description:** Author `l2-microservice` thin-composition under `modules-ir/l2/l2-microservice/` referencing the six ECS L1s (depth ≤ 5). Extend `schemas/contract.schema.json` with microservice inputs (`image: string`, `port: integer`, `env: map`, `healthcheck: object`). Verify contract→IR resolution yields a complete target stack.
|
||||
- **Status:** complete (v1.2.4)
|
||||
- **Depends on:** [13]
|
||||
- **Requirements:** REQ-32
|
||||
- **Success Criteria:**
|
||||
- `l2-microservice` references the six ECS L1s only (depth ≤ 5).
|
||||
- `schemas/contract.schema.json` validates a `contracts/microservice.yaml` with the new inputs.
|
||||
- Contract→IR resolution yields a complete target stack (all six L1 instances + relationships).
|
||||
|
||||
### Phase 15 — consumer-repo-and-terraform-apply
|
||||
- **Description:** Create a new Gitea repo `acdl-consumer-microservice` under the `continuous-intelligence` org containing a basic HTTP microservice (tiny Python/Go server returning 200), a `Dockerfile`, an ECR push step, and a `contracts/microservice.yaml` submission for `l2-microservice` (dev environment). Lift the platform from `plan` to **`apply`** for the `dev` environment (autonomous per §10, confidence ≥ 0.50, no HITL). Submit the contract → pipeline → IR → plan → apply → a real ECS Fargate service running.
|
||||
- **Status:** complete (v1.2.5, PARTIAL — terraform apply blocked by IAM P0)
|
||||
- **Depends on:** [14]
|
||||
- **Requirements:** REQ-33 (partial), REQ-34
|
||||
- **Success Criteria:**
|
||||
- `acdl-consumer-microservice` repo exists under `continuous-intelligence`.
|
||||
- The microservice builds into a Docker image and is pushed to ECR.
|
||||
- `terraform apply` (dev) creates real AWS resources (VPC, ECS cluster, ECR repo, ALB, ECS service).
|
||||
- The apply result is captured in the evidence stream.
|
||||
|
||||
### Phase 16 — v1.2-capstone-e2e
|
||||
- **Description:** End-to-end verification: consumer commit to `acdl-consumer-microservice` triggers the pipeline → contract→IR resolution → `terraform plan` → `terraform apply` (dev) → a live ECS Fargate service serving HTTP 200 on its ALB → evidence event written to the DynamoDB outbox → the event renders on the `acdl-evidence` timeline. Verify the NFR improvements from Phase 12 hold, the setup is simpler (one `scripts/run_platform.sh`), and the README is accurate. `scripts/verify_phase16.sh` proves the full flow green.
|
||||
- **Status:** complete (v1.2.6, capstone — terraform apply blocked by IAM P0, verified up to plan)
|
||||
- **Depends on:** [15]
|
||||
- **Requirements:** REQ-35 (partial — IAM-blocked)
|
||||
- **Success Criteria:**
|
||||
- One consumer commit produces a live ECS service serving HTTP 200.
|
||||
- An evidence event for the apply is in the DynamoDB outbox and renders on the timeline.
|
||||
- `scripts/verify_phase16.sh` exits 0.
|
||||
- README accurately documents the v1.2 platform flow.
|
||||
|
||||
After Phase 16: COMPLETE gate — review → ship `v1.3.0` → audit.
|
||||
|
||||
---
|
||||
|
||||
## v1.3 (Complete — module documentation + thin-composition removal)
|
||||
|
||||
The v1.3 milestone starts with simplification: removing the unsatisfactory
|
||||
thin-composition layer and building out proper module documentation. The
|
||||
L2 composition mechanism will be redesigned in a later phase.
|
||||
|
||||
### Phase 17 — remove-thin-composition-and-module-readmes
|
||||
- **Description:** Remove the L2 thin-composition layer completely (composition.json files, contract_resolver.py, contract schema, sample contracts) and build out proper module READMEs. Create a README template for both L1 and L2 modules, rewrite all 7 L1 module READMEs in plain language (no jargon, with Resources/Inputs/Outputs/Usage/Compliance-extension-points/Versioning sections), write 2 L2 placeholder READMEs noting the composition is under redesign, create a catalog index, and patch run_platform.sh to load a pre-existing IR instance instead of resolving a contract. Prune L2 entries from the registry.
|
||||
- **Status:** complete (v1.3.1)
|
||||
- **Depends on:** —
|
||||
- **Requirements:** REQ-36, REQ-37, REQ-38
|
||||
- **Success Criteria:**
|
||||
- The thin-composition layer is fully removed (composition.json, contract_resolver.py, contract schema, contracts/).
|
||||
- run_platform.sh loads a pre-existing IR instance; the downstream adapter/checkov/confidence/outbox pipeline still works.
|
||||
- A README-TEMPLATE.md exists for both L1 and L2 modules.
|
||||
- Every L1 module has a README.md with Resources/Inputs/Outputs/Usage/Compliance-extension-points/Versioning.
|
||||
- Every L2 module has a placeholder README.md noting the composition is under redesign.
|
||||
- A modules-ir/README.md catalog index exists.
|
||||
|
||||
### Phase 18 — testing-and-cicd-pipelines
|
||||
- **Description:** Create a pytest test suite that reproduces the platform pipeline offline (adapter, confidence_signal, checkov_adapter, outbox_writer). Add an offline `--check-only` mode to `run_platform.sh` that runs the pipeline up to adapter emission without AWS/Checkov/outbox. Create identical CI/CD pipelines for both Gitea Actions (`.gitea/workflows/ci.yml`, dev environment) and GitHub Actions (`.github/workflows/ci.yml`, production) that run: lint, pytest, `run_platform.sh --check-only`. Add `pyproject.toml` + `requirements-test.txt` for dependency pinning.
|
||||
- **Status:** complete (v1.3.2)
|
||||
- **Depends on:** [17]
|
||||
- **Requirements:** REQ-39, REQ-40, REQ-41, REQ-42
|
||||
- **Success Criteria:**
|
||||
- `pytest` runs and passes offline (no AWS, no Checkov, no DynamoDB).
|
||||
- `run_platform.sh --check-only` runs offline and exits 0.
|
||||
- `.gitea/workflows/ci.yml` and `.github/workflows/ci.yml` exist with identical job stages (lint, test, check-only).
|
||||
- `pyproject.toml` + `requirements-test.txt` pin test dependencies.
|
||||
|
||||
After Phase 18: COMPLETE gate — review → ship `v1.3.2` → audit.
|
||||
|
||||
---
|
||||
|
||||
## v1.4 (Active — central pipeline contract + shell reproducibility + streaming)
|
||||
|
||||
The v1.4 milestone makes the CI/CD pipeline a declarative contract rather
|
||||
than duplicated workflow copies, enables full shell reproducibility of the
|
||||
CI pipeline, and streams terraform/checkov output so users can see what
|
||||
the platform is doing.
|
||||
|
||||
### Phase 19 — central-pipeline-contract-and-shell-reproducibility
|
||||
- **Description:** Create a central pipeline contract (`schemas/pipeline.schema.json` JSON Schema + `pipelines/ci.yaml` YAML instance) that both `.gitea/workflows/ci.yml` (Gitea Actions, dev) and `.github/workflows/ci.yml` (GitHub Actions, production) implement. Create `scripts/run_ci.sh` that mirrors the CI pipeline locally (lint → test → check-only). Update `scripts/run_platform.sh` to stream terraform init/validate/plan output, Checkov compliance results, and PolicyCheckResult records to stdout by default (with `--quiet` for log-only mode). Add `tests/test_pipeline_contract.py` validating the contract schema, workflow conformance, and run_ci.sh. Update both workflow YAMLs with contract reference headers (staying byte-identical).
|
||||
- **Status:** complete (v1.4.1)
|
||||
- **Depends on:** [18]
|
||||
- **Requirements:** REQ-43, REQ-44, REQ-45
|
||||
- **Success Criteria:**
|
||||
- `pipelines/ci.yaml` validates against `schemas/pipeline.schema.json`.
|
||||
- Both `.gitea/workflows/ci.yml` and `.github/workflows/ci.yml` are byte-identical.
|
||||
- A test parses both workflows and asserts their stages/commands match the contract.
|
||||
- `scripts/run_ci.sh` exits 0 and outputs "CI PIPELINE OK".
|
||||
- `scripts/run_platform.sh --check-only` streams the emitted Terraform to stdout.
|
||||
- `scripts/run_platform.sh --check-only --quiet` suppresses the Terraform stream.
|
||||
- `pytest` total count increases from 90 to 122 (32 new contract/streaming tests).
|
||||
|
||||
After Phase 19: COMPLETE gate — review → ship `v1.4.1` → audit.
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
zero-trust credential model, and provides a reusable deploy workflow so
|
||||
consumer repos never need to clone the platform repo or invoke its scripts
|
||||
locally.
|
||||
|
||||
### 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-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)
|
||||
- **Depends on:** [19]
|
||||
- **Requirements:** REQ-46, REQ-47, REQ-48, REQ-49, REQ-50, REQ-51
|
||||
- **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).
|
||||
- `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`.
|
||||
- `.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-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.
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## v1.9 (complete — design doc refresh + contract interpolation + per-env CI jobs + stub implementation + P1-1 remediation, tag `v1.9.0`)
|
||||
|
||||
The v1.9 milestone closes four gaps left by v1.8 (user-directed,
|
||||
2026-07-23): stale design docs, no contract interpolation, promotion
|
||||
requires editing the `environment` field, and unimplemented stubs. It
|
||||
also closes P1-1 (adapter hardcoded defaults, deferred from v1.2).
|
||||
|
||||
### Phase 39 — design-doc-refresh-and-p1-1-parameterization
|
||||
- **Description:** Refresh `core/hitl_matrix_design.md` (no stale "dev-only spike"/"v1.2 wires the gates" framing; v1.9 wiring section; 8-concern matrix marked implemented offline-testable subset) + `core/audit_ledger_design.md` (outbox marked shipped+production since v1.8; S3 Object Lock + JWS + worker + DLQ + checkpoints deferred D-083). P1-1: move adapter ECS/ALB/VPC hardcoded defaults (`desired_count`, `launch_type`, `family`, `target_type`, `load_balancer_type`, `Name` tags) into L1 `interface.json` inputs with defaults; the adapter reads from inputs; the resolver routes wires to the sub-resource that declares the input.
|
||||
- **Status:** complete (v1.8.1)
|
||||
- **Depends on:** —
|
||||
- **Requirements:** REQ-100, REQ-101, REQ-102
|
||||
- **Success Criteria:**
|
||||
- Both design docs refreshed; no stale framing; `test_design_docs_current.py` passes.
|
||||
- Adapter has no hardcoded ECS/ALB/VPC defaults; overrides flow through; `test_p1_1_adapter_parameterization.py` passes.
|
||||
- v1.1 S3 regression passes; `pytest` 371 (was 350, +21); `run_ci.sh` exits 0; `run_platform.sh --check-only` exits 0.
|
||||
|
||||
### Phase 40 — contract-interpolation
|
||||
- **Description:** `${env.<field>}` + `${contract.<field>}` resolver expansion from environment onboarding JSON (D-081). Environment JSON schema (`schemas/environment.schema.json`) + qa/prod/dr placeholder bindings. `core/environment_check.py` gains `load()`. Sample contracts use naming patterns that include region, account id, environment (e.g. `acdl-${env.environment}-${contract.module}-${env.account_id}-${env.region}`). Expansion is recursive (D-087), post-schema-validation, pre-IR-resolution; unknown tokens raise `ValueError`. `resolve()` accepts `environment_override` (D-088).
|
||||
- **Status:** complete (v1.8.2)
|
||||
- **Depends on:** [39]
|
||||
- **Requirements:** REQ-103, REQ-104
|
||||
- **Success Criteria:**
|
||||
- `schemas/environment.schema.json` exists; 4 env files validate; `load()` works.
|
||||
- `_expand_vars` in resolver; unknown tokens raise; recursive over dicts/lists/strings.
|
||||
- Sample contracts use `${env.*}` + `${contract.*}` naming patterns; resolve to concrete values.
|
||||
- `tests/test_environment_schema.py` + `tests/test_interpolation.py` + `tests/test_sample_contracts_interpolate.py` pass.
|
||||
- `pytest` 406 (was 371, +35); `run_ci.sh` exits 0; `run_platform.sh --check-only` exits 0.
|
||||
### Phase 41 — per-environment-ci-jobs
|
||||
- **Description:** Per-env contract files (static-assets + microservice × dev/qa/prod/dr, REQ-105) using interpolation. Deploy workflow (`.github` + `.gitea`, byte-identical) declares an `environment` `workflow_call` input (REQ-106); `run_platform.sh --environment <name>` overrides the contract's environment at load time (D-088, before schema validation + interpolation). `resolve()` accepts `environment_override`. Consumer guide documents the per-env caller-workflow pattern (4 jobs, one per environment) + HITL gate structure (approve_qa/approve_prod/approve_dr, D-042) + interpolation reference table. Promotion = running the matching job; no environment field editing.
|
||||
- **Status:** complete (v1.8.3)
|
||||
- **Depends on:** [40]
|
||||
- **Requirements:** REQ-105, REQ-106
|
||||
- **Success Criteria:**
|
||||
- 8 per-env contract files exist + validate + resolve to correct env.
|
||||
- Deploy workflow has `environment` input (byte-identical Gitea + GitHub); `run_platform.sh --environment` overrides; resolver supports `environment_override`.
|
||||
- Consumer guide documents per-env caller workflows + promotion-without-editing + HITL gates + interpolation reference.
|
||||
- `tests/test_per_env_contracts.py` + `tests/test_deploy_workflow_env_input.py` + `tests/test_consumer_guide_per_env_section.py` pass.
|
||||
- `pytest` 446 (was 406, +40); `run_ci.sh` exits 0; both deploy workflows byte-identical.
|
||||
|
||||
### Phase 42 — stub-implementation
|
||||
- **Description:** `route_halt_artifact` real (SNS publish + outbox fallback, REQ-107) + SNS topic `acdl-sod-halt` in `terraform/platform/main.tf`. HITL attestation gates (`core/hitl_gates.py`, REQ-108) — records approver to outbox, runs SoD on prod, invokes the attestation matrix; `run_platform.sh` calls `attest` before apply for qa/prod/dr (dev skips). 8-concern attestation matrix (`core/attestation_matrix.py`, REQ-109, D-084) — offline-testable concerns run for real; operator-supplied concerns accept signed evidence artifacts validated for freshness + schema; signature skip when `ACDL_ATTESTATION_SIGNING_KEY_ID` unset (D-089). Wiz real API client (`WizClient`, REQ-110) — GraphQL queries + pagination + graceful degrade. Kyverno translator fleshed out (REQ-111) — full PolicyReport mapping + skip-with-reason + inactive-for-TF guard + `--kube-version` stub.
|
||||
- **Status:** complete (v1.8.4)
|
||||
- **Depends on:** [41]
|
||||
- **Requirements:** REQ-107, REQ-108, REQ-109, REQ-110, REQ-111
|
||||
- **Success Criteria:**
|
||||
- `route_halt_artifact` publishes to SNS when ARN set; outbox fallback when unset; SNS topic in Terraform.
|
||||
- `hitl_gates.attest` records approver; SoD blocks on identity equality; dev skips; `run_platform.sh` has the HITL step.
|
||||
- `attestation_matrix.check` runs 8 concerns; offline concerns pass; operator-supplied missing → block for prod; expired → block; signature skip when key unset.
|
||||
- Wiz `WizClient` real client + pagination + graceful degrade; `fetch_and_adapt` translates.
|
||||
- Kyverno full mapping (pass/fail/skip/warn + severity + skip-with-reason + resource construction); inactive guard preserved; `--kube-version` parsed.
|
||||
- `tests/test_route_halt_artifact.py` + `test_hitl_gates.py` + `test_attestation_matrix.py` + `test_wiz_adapter_real_client.py` + expanded `test_kyverno_adapter.py` pass.
|
||||
- `pytest` 493 (was 446, +47); `run_ci.sh` exits 0; `run_platform.sh --check-only` exits 0.
|
||||
|
||||
### Phase 43 — verify-review-audit-complete
|
||||
- **Description:** 4-layer verify (structural, behavioral, security, quality) of all v1.9 phases. Multi-persona review (0 P0, 0 P1). Audit (reconstruction, file discipline, branch hygiene, commit discipline — all clean). REVIEW.md reconstructed (D-086). Complete: update REQUIREMENTS.md (REQ-100..111), ROADMAP.md, PROJECT.md. Tag `v1.9.0`; update floating `v1.9` + `v1` tags. Bump `uses:`/`ref:` from `@v1.6` → `@v1.9`.
|
||||
- **Status:** complete (v1.9.0)
|
||||
- **Depends on:** [42]
|
||||
- **Requirements:** —
|
||||
- **Success Criteria:**
|
||||
- 4-layer verify PASS; 493 tests; `run_ci.sh` + `run_platform.sh --check-only` green.
|
||||
- Review: 0 P0, 0 P1; REVIEW.md reconstructed with v1.9 content (D-086).
|
||||
- Audit: clean; all 12 v1.9 commits have `---ci---` blocks.
|
||||
- Tag `v1.9.0` created; floating tags updated; `uses:` bumped to `@v1.9`.
|
||||
|
||||
After Phase 43: COMPLETE gate — review → ship `v1.9.0` → audit. **DONE.**
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# Phase 39-43 — Verify (v1.9)
|
||||
|
||||
## Structural
|
||||
All 26 new files present (environment.schema.json, 4 env files, 8 per-env
|
||||
contracts, hitl_gates.py, attestation_matrix.py, 10 new test files,
|
||||
refreshed design docs). SNS topic in terraform/platform/main.tf. **PASS.**
|
||||
|
||||
## Behavioral
|
||||
- `pytest`: 493 tests, all passing (was 350 at v1.8 → 493 at v1.9, +143 new).
|
||||
- `run_ci.sh`: exits 0 with "CI PIPELINE OK".
|
||||
- `run_platform.sh --check-only`: exits 0 with "PLATFORM CHECK OK".
|
||||
- `run_platform.sh --check-only --environment qa`: exits 0; bucket name reflects qa env.
|
||||
**PASS.**
|
||||
|
||||
## Security
|
||||
- No hardcoded adapter ECS/ALB/VPC defaults (P1-1 closed; defaults in interface.json).
|
||||
- HITL gates block on SoD violation (approver_qa == approver_prod).
|
||||
- Attestation matrix fails loud on missing/expired evidence for prod/dr.
|
||||
- Signature verification required when ACDL_ATTESTATION_SIGNING_KEY_ID set; skipped + logged when unset (D-089).
|
||||
- Wiz degrades gracefully when unconfigured (WIZ_NOT_CONFIGURED SKIPPED record).
|
||||
- SNS topic KMS-encrypted; outbox fallback for the halt artifact.
|
||||
- Deploy workflows byte-identical (Gitea + GitHub).
|
||||
**PASS.**
|
||||
|
||||
## Quality
|
||||
Each new feature has dedicated tests:
|
||||
- Design docs: test_design_docs_current.py (no stale framing; deferred D-083 labeled).
|
||||
- P1-1: test_p1_1_adapter_parameterization.py (override + default + v1.1 S3 regression).
|
||||
- Interpolation: test_interpolation.py + test_sample_contracts_interpolate.py + test_environment_schema.py.
|
||||
- Per-env jobs: test_per_env_contracts.py + test_deploy_workflow_env_input.py + test_consumer_guide_per_env_section.py.
|
||||
- SoD: test_route_halt_artifact.py (SNS + outbox fallback + SNS failure fallback).
|
||||
- HITL gates: test_hitl_gates.py (dev skips; qa/prod/dr record approver; SoD blocks; matrix invoked).
|
||||
- Attestation matrix: test_attestation_matrix.py (offline concerns; operator-supplied; freshness; signature skip).
|
||||
- Wiz: test_wiz_adapter_real_client.py (real client + pagination + graceful degrade).
|
||||
- Kyverno: expanded test_kyverno_adapter.py (pass/fail/skip/warn + severity + inactive guard + kube-version).
|
||||
**PASS.**
|
||||
|
||||
## Verdict
|
||||
|
||||
**VERIFY PASS** — all four layers pass. 493 offline tests, no AWS required for CI.
|
||||
@@ -4,8 +4,8 @@
|
||||
{
|
||||
"slug": "acdl",
|
||||
"name": "Agentic Cloud Delivery Platform",
|
||||
"milestone": "v1.0",
|
||||
"status": "specify"
|
||||
"milestone": "v1.9",
|
||||
"status": "complete"
|
||||
}
|
||||
],
|
||||
"active_project": "acdl",
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
# ACDL CI Pipeline — Gitea Actions (dev environment)
|
||||
#
|
||||
# This workflow implements the central pipeline contract:
|
||||
# pipelines/ci.yaml (validated against schemas/pipeline.schema.json)
|
||||
#
|
||||
# The same contract is implemented by .github/workflows/ci.yml (GitHub
|
||||
# Actions, production). Both files must be byte-identical — the only
|
||||
# declared difference is the forge/runtime, not the stages or commands.
|
||||
#
|
||||
# Shell reproducibility: scripts/run_ci.sh runs the same 3 stages locally.
|
||||
#
|
||||
# Stages (from the contract):
|
||||
# 1. lint — py_compile all Python files
|
||||
# 2. test — pytest test suite (offline, no AWS)
|
||||
# 3. check-only — run_platform.sh --check-only (offline, no AWS)
|
||||
name: acdl-ci
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
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/output_publisher.py \
|
||||
core/contract_resolver.py \
|
||||
core/lambda/contract_ingestor.py \
|
||||
adapters/terraform/adapter.py \
|
||||
adapters/terraform/policy/checkov_adapter.py \
|
||||
scripts/push_consumer_image.py
|
||||
|
||||
test:
|
||||
name: Test
|
||||
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
|
||||
|
||||
check-only:
|
||||
name: Platform check-only (offline)
|
||||
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
|
||||
run: bash scripts/run_platform.sh --check-only
|
||||
@@ -0,0 +1,165 @@
|
||||
# ACDL Reusable Deploy Workflow — Gitea Actions (dev environment)
|
||||
#
|
||||
# This reusable workflow implements the central deployment pipeline contract:
|
||||
# pipelines/deploy.yaml (validated against schemas/deploy-pipeline.schema.json)
|
||||
#
|
||||
# The same contract is implemented by .github/workflows/deploy.yml (GitHub
|
||||
# Actions, production). Both files must be byte-identical — the only
|
||||
# declared difference is the forge/runtime, not the stages or commands.
|
||||
#
|
||||
# Consumer repos invoke this workflow via a versioned tag (floating MAJOR + MINOR):
|
||||
# uses: acdl/.gitea/workflows/deploy.yml@v1.9 (Gitea)
|
||||
# uses: acdl/.github/workflows/deploy.yml@v1.9 (GitHub)
|
||||
#
|
||||
# Unversioned references (@main, bare) are discouraged — the consumer's setup
|
||||
# must be immutable + resilient. The versioned tag is the only immutability
|
||||
# lever (version constraints cannot be expressed inside the contract).
|
||||
#
|
||||
# What this workflow does:
|
||||
# 1. Checks out the consumer repo (the repo that invoked the workflow).
|
||||
# 2. Checks out the ACDL platform repo into the workspace (platform/).
|
||||
# This is the run-time fetch — consumers never clone the platform repo.
|
||||
# 3. Installs runtime deps: Python 3.12, Terraform 1.9.*, Checkov.
|
||||
# 4. Configures AWS auth (OIDC default; static-key override via secrets).
|
||||
# 5. Runs scripts/run_platform.sh against the consumer's contract path.
|
||||
# 6. Uploads artifacts (emitted Terraform, Checkov JSON, confidence JSON,
|
||||
# platform log) for auditability.
|
||||
#
|
||||
# Inputs:
|
||||
# contract — path to the consumer's contract YAML (default .acdl/contract.yaml)
|
||||
# mode — full | plan-only | check-only (default full; dev = full apply,
|
||||
# higher environments hold for HITL — the calling repo or the
|
||||
# forge environment gate enforces that)
|
||||
#
|
||||
# Auth (zero-trust default — see README.md#credentials--zero-trust):
|
||||
# OIDC federation is the default. permissions: id-token: write lets the
|
||||
# forge mint a short-lived STS token. The role-to-assume is scoped by the
|
||||
# consumer's repository identity (ABAC) — the workflow assumes the role
|
||||
# that matches repo:org/consumer-repo:ref:refs/heads/main, and the session
|
||||
# policy restricts view/update to resources tagged acdl:owner=<consumer-repo>.
|
||||
#
|
||||
# Override (where OIDC is unavailable, e.g. Gitea pending
|
||||
# go-gitea/gitea#36988): set ACDL_AWS_ACCESS_KEY_ID + ACDL_AWS_SECRET_ACCESS_KEY
|
||||
# as repository secrets. The platform-managed scheduled pipeline rotates
|
||||
# the key on a daily cadence. When .env.secrets is used locally instead,
|
||||
# rotating the key out of band is the consumer's responsibility.
|
||||
name: acdl-deploy
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
contract:
|
||||
description: Path to the consumer contract YAML (in the consumer repo)
|
||||
type: string
|
||||
default: .acdl/contract.yaml
|
||||
mode:
|
||||
description: Pipeline mode — full (apply), plan-only, check-only, or decommission
|
||||
type: string
|
||||
default: full
|
||||
changeRequestId:
|
||||
description: Change request ID (required for decommission mode — validated against CMDB)
|
||||
type: string
|
||||
default: ""
|
||||
environment:
|
||||
description: Target environment override (dev/qa/prod/dr); when empty, the contract's environment field is used
|
||||
type: string
|
||||
default: ""
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out consumer repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check out ACDL platform repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: acdl/acdl
|
||||
path: platform
|
||||
ref: v1.9
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install runtime dependencies
|
||||
run: |
|
||||
pip install --break-system-packages jsonschema pyyaml boto3
|
||||
pip install --break-system-packages "checkov>=3.2,<4"
|
||||
|
||||
- name: Install Terraform 1.9.*
|
||||
run: |
|
||||
wget -qO- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp.gpg
|
||||
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.*
|
||||
|
||||
- name: Configure AWS credentials (OIDC default + static-key override)
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
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
|
||||
access-key-id: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
|
||||
secret-access-key: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
|
||||
|
||||
- name: Run the platform pipeline
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: |
|
||||
MODE_FLAG=""
|
||||
case "${{ inputs.mode }}" in
|
||||
full) MODE_FLAG="" ;;
|
||||
plan-only) MODE_FLAG="--plan-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 ;;
|
||||
esac
|
||||
ENV_FLAG=""
|
||||
if [ -n "${{ inputs.environment }}" ]; then
|
||||
ENV_FLAG="--environment ${{ inputs.environment }}"
|
||||
fi
|
||||
bash platform/scripts/run_platform.sh $MODE_FLAG $ENV_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
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: acdl-terraform
|
||||
path: /tmp/acdl_platform_run_v18/tf/*.tf
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Upload platform log
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: acdl-platform-log
|
||||
path: platform/logs/
|
||||
if-no-files-found: warn
|
||||
@@ -1,75 +0,0 @@
|
||||
# ACDL reusable pipeline workflow (Phase 01 skeleton).
|
||||
#
|
||||
# This workflow is called from acdl-contracts via:
|
||||
# uses: continuous-intelligence/acdl/.gitea/workflows/pipeline.yml@milestone/v1.0-initial
|
||||
#
|
||||
# Branch pinning rule (see .ciagent/ARCHITECTURE.md): the `acdl` repo's default
|
||||
# branch is `milestone/v1.0-initial`, so `uses:` references must pin to
|
||||
# `@milestone/v1.0-initial`, NOT `@main`.
|
||||
#
|
||||
# Phase 04 will implement the actual stage logic + approval gates (D-013:
|
||||
# Gitea has no environments API; gates become workflow_dispatch approval
|
||||
# inputs).
|
||||
name: acdl-pipeline
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
contract-ref:
|
||||
description: "Ref on acdl-contracts that triggered the pipeline"
|
||||
required: false
|
||||
type: string
|
||||
default: main
|
||||
|
||||
jobs:
|
||||
dev:
|
||||
name: "Dev (autonomous)"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# Phase 04 will implement: checkout acdl + acdl-contracts, run
|
||||
# policy_checker.py, mock_executor.sh, confidence_signal.py, write
|
||||
# evidence via evidence_writer.py.
|
||||
- name: "Dev stage placeholder"
|
||||
run: |
|
||||
echo "Dev stage placeholder (Phase 01 skeleton)"
|
||||
echo "Phase 04 will run policy_checker, mock_executor, confidence_signal, evidence_writer"
|
||||
exit 0
|
||||
|
||||
qa-gate:
|
||||
name: "QA (manual approval)"
|
||||
needs: dev
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# Phase 04 will implement: gate via workflow_dispatch approval input
|
||||
# (D-013 fallback; Gitea ignores jobs.<id>.environment).
|
||||
- name: "QA gate placeholder"
|
||||
run: |
|
||||
echo "QA gate placeholder (Phase 01 skeleton)"
|
||||
echo "Phase 04 will pause here for human approval via workflow_dispatch"
|
||||
exit 0
|
||||
|
||||
prod-gate:
|
||||
name: "Prod (manual approval)"
|
||||
needs: qa-gate
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# Phase 04 will implement: same approval-input gate as qa-gate.
|
||||
- name: "Prod gate placeholder"
|
||||
run: |
|
||||
echo "Prod gate placeholder (Phase 01 skeleton)"
|
||||
echo "Phase 04 will pause here for human approval via workflow_dispatch"
|
||||
exit 0
|
||||
|
||||
finalize:
|
||||
name: "Finalize (publish evidence)"
|
||||
needs: prod-gate
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# Phase 04/05 will implement: commit audit.json to acdl-evidence main
|
||||
# via the Gitea file-contents API; raw URL republishes index.html +
|
||||
# audit.json for the timeline UI (D-012).
|
||||
- name: "Finalize placeholder"
|
||||
run: |
|
||||
echo "Finalize placeholder (Phase 01 skeleton)"
|
||||
echo "Phase 04/05 will commit audit.json to acdl-evidence main"
|
||||
exit 0
|
||||
@@ -0,0 +1,77 @@
|
||||
# ACDL CI Pipeline — Gitea Actions (dev environment)
|
||||
#
|
||||
# This workflow implements the central pipeline contract:
|
||||
# pipelines/ci.yaml (validated against schemas/pipeline.schema.json)
|
||||
#
|
||||
# The same contract is implemented by .github/workflows/ci.yml (GitHub
|
||||
# Actions, production). Both files must be byte-identical — the only
|
||||
# declared difference is the forge/runtime, not the stages or commands.
|
||||
#
|
||||
# Shell reproducibility: scripts/run_ci.sh runs the same 3 stages locally.
|
||||
#
|
||||
# Stages (from the contract):
|
||||
# 1. lint — py_compile all Python files
|
||||
# 2. test — pytest test suite (offline, no AWS)
|
||||
# 3. check-only — run_platform.sh --check-only (offline, no AWS)
|
||||
name: acdl-ci
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
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/output_publisher.py \
|
||||
core/contract_resolver.py \
|
||||
core/lambda/contract_ingestor.py \
|
||||
adapters/terraform/adapter.py \
|
||||
adapters/terraform/policy/checkov_adapter.py \
|
||||
scripts/push_consumer_image.py
|
||||
|
||||
test:
|
||||
name: Test
|
||||
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
|
||||
|
||||
check-only:
|
||||
name: Platform check-only (offline)
|
||||
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
|
||||
run: bash scripts/run_platform.sh --check-only
|
||||
@@ -0,0 +1,165 @@
|
||||
# ACDL Reusable Deploy Workflow — Gitea Actions (dev environment)
|
||||
#
|
||||
# This reusable workflow implements the central deployment pipeline contract:
|
||||
# pipelines/deploy.yaml (validated against schemas/deploy-pipeline.schema.json)
|
||||
#
|
||||
# The same contract is implemented by .github/workflows/deploy.yml (GitHub
|
||||
# Actions, production). Both files must be byte-identical — the only
|
||||
# declared difference is the forge/runtime, not the stages or commands.
|
||||
#
|
||||
# Consumer repos invoke this workflow via a versioned tag (floating MAJOR + MINOR):
|
||||
# uses: acdl/.gitea/workflows/deploy.yml@v1.9 (Gitea)
|
||||
# uses: acdl/.github/workflows/deploy.yml@v1.9 (GitHub)
|
||||
#
|
||||
# Unversioned references (@main, bare) are discouraged — the consumer's setup
|
||||
# must be immutable + resilient. The versioned tag is the only immutability
|
||||
# lever (version constraints cannot be expressed inside the contract).
|
||||
#
|
||||
# What this workflow does:
|
||||
# 1. Checks out the consumer repo (the repo that invoked the workflow).
|
||||
# 2. Checks out the ACDL platform repo into the workspace (platform/).
|
||||
# This is the run-time fetch — consumers never clone the platform repo.
|
||||
# 3. Installs runtime deps: Python 3.12, Terraform 1.9.*, Checkov.
|
||||
# 4. Configures AWS auth (OIDC default; static-key override via secrets).
|
||||
# 5. Runs scripts/run_platform.sh against the consumer's contract path.
|
||||
# 6. Uploads artifacts (emitted Terraform, Checkov JSON, confidence JSON,
|
||||
# platform log) for auditability.
|
||||
#
|
||||
# Inputs:
|
||||
# contract — path to the consumer's contract YAML (default .acdl/contract.yaml)
|
||||
# mode — full | plan-only | check-only (default full; dev = full apply,
|
||||
# higher environments hold for HITL — the calling repo or the
|
||||
# forge environment gate enforces that)
|
||||
#
|
||||
# Auth (zero-trust default — see README.md#credentials--zero-trust):
|
||||
# OIDC federation is the default. permissions: id-token: write lets the
|
||||
# forge mint a short-lived STS token. The role-to-assume is scoped by the
|
||||
# consumer's repository identity (ABAC) — the workflow assumes the role
|
||||
# that matches repo:org/consumer-repo:ref:refs/heads/main, and the session
|
||||
# policy restricts view/update to resources tagged acdl:owner=<consumer-repo>.
|
||||
#
|
||||
# Override (where OIDC is unavailable, e.g. Gitea pending
|
||||
# go-gitea/gitea#36988): set ACDL_AWS_ACCESS_KEY_ID + ACDL_AWS_SECRET_ACCESS_KEY
|
||||
# as repository secrets. The platform-managed scheduled pipeline rotates
|
||||
# the key on a daily cadence. When .env.secrets is used locally instead,
|
||||
# rotating the key out of band is the consumer's responsibility.
|
||||
name: acdl-deploy
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
contract:
|
||||
description: Path to the consumer contract YAML (in the consumer repo)
|
||||
type: string
|
||||
default: .acdl/contract.yaml
|
||||
mode:
|
||||
description: Pipeline mode — full (apply), plan-only, check-only, or decommission
|
||||
type: string
|
||||
default: full
|
||||
changeRequestId:
|
||||
description: Change request ID (required for decommission mode — validated against CMDB)
|
||||
type: string
|
||||
default: ""
|
||||
environment:
|
||||
description: Target environment override (dev/qa/prod/dr); when empty, the contract's environment field is used
|
||||
type: string
|
||||
default: ""
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out consumer repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check out ACDL platform repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: acdl/acdl
|
||||
path: platform
|
||||
ref: v1.9
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install runtime dependencies
|
||||
run: |
|
||||
pip install --break-system-packages jsonschema pyyaml boto3
|
||||
pip install --break-system-packages "checkov>=3.2,<4"
|
||||
|
||||
- name: Install Terraform 1.9.*
|
||||
run: |
|
||||
wget -qO- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp.gpg
|
||||
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.*
|
||||
|
||||
- name: Configure AWS credentials (OIDC default + static-key override)
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
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
|
||||
access-key-id: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
|
||||
secret-access-key: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
|
||||
|
||||
- name: Run the platform pipeline
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: |
|
||||
MODE_FLAG=""
|
||||
case "${{ inputs.mode }}" in
|
||||
full) MODE_FLAG="" ;;
|
||||
plan-only) MODE_FLAG="--plan-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 ;;
|
||||
esac
|
||||
ENV_FLAG=""
|
||||
if [ -n "${{ inputs.environment }}" ]; then
|
||||
ENV_FLAG="--environment ${{ inputs.environment }}"
|
||||
fi
|
||||
bash platform/scripts/run_platform.sh $MODE_FLAG $ENV_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
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: acdl-terraform
|
||||
path: /tmp/acdl_platform_run_v18/tf/*.tf
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Upload platform log
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: acdl-platform-log
|
||||
path: platform/logs/
|
||||
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
|
||||
+12
-1
@@ -6,4 +6,15 @@ __pycache__/
|
||||
state.json
|
||||
audit.json
|
||||
*.tmp
|
||||
.DS_Store
|
||||
.DS_Store
|
||||
runner-data/
|
||||
.env.secrets
|
||||
terraform/bootstrap/.bootstrap_state.json
|
||||
terraform/spike/.terraform/
|
||||
terraform/spike/.terraform.lock.hcl
|
||||
terraform/spike/tfplan
|
||||
terraform/spike/*.tfstate*
|
||||
terraform/microservice/.terraform/
|
||||
terraform/microservice/.terraform.lock.hcl
|
||||
terraform/microservice/tfplan
|
||||
terraform/microservice/*.tfstate*
|
||||
@@ -1,55 +1,313 @@
|
||||
# ACDL — Agentic Cloud Delivery Platform
|
||||
|
||||
A 30-minute executive demo proving that infrastructure can be delivered
|
||||
**automatically, safely, and with a complete audit trail** — without the
|
||||
usual weeks of manual tickets, reviews, and copy-pasted configuration.
|
||||
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.
|
||||
|
||||
The demo runs entirely on **local stubs** (no AWS/GCP/Azure, no external LLM
|
||||
APIs). It shows intent and safety behavior rather than provisioning real
|
||||
cloud resources.
|
||||
- **Consumer guide:** [`docs/consumer-guide.md`](docs/consumer-guide.md)
|
||||
- **Modules:** [`docs/modules/`](docs/modules/)
|
||||
- **Contracts:** [`docs/contracts/`](docs/contracts/)
|
||||
- **Pipeline:** [`docs/pipeline/`](docs/pipeline/)
|
||||
- **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)
|
||||
|
||||
## Four acts
|
||||
## Repository roles
|
||||
|
||||
1. **Act 1 — The Friction:** the old manual 2-week deployment process.
|
||||
2. **Act 2 — Developer Self-Service:** commit a valid `contract.yaml` for
|
||||
`l2-commodity-price-feed`, watch Dev auto-run, QA + Prod approval gates,
|
||||
then the evidence timeline.
|
||||
3. **Act 3 — Citizen Developer:** open a GitHub/Gitea Issue with natural-
|
||||
language intent; the Python keyword parser generates the same
|
||||
`contract.yaml` and triggers the identical pipeline.
|
||||
4. **Act 4 — The Safety Net:** commit a malicious `contract.yaml`
|
||||
(`public-ingress: true`) for `l2-regulatory-reporting`; the pipeline
|
||||
halts in Dev because the confidence signal drops below 0.50, and the
|
||||
rejection is visible on the evidence stream.
|
||||
There are two kinds of repository in the ACDL model:
|
||||
|
||||
## Repositories
|
||||
- **Platform repo (this one).** This is 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:
|
||||
1. **Its application code** — the service or site being deployed.
|
||||
2. **One or more contracts** — small YAML files at `.acdl/contract.yaml`
|
||||
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.
|
||||
|
||||
All under the `continuous-intelligence` Gitea org at
|
||||
`https://git.cloudinit.dev`:
|
||||
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.
|
||||
|
||||
- `acdl` (this repo) — platform + stubs + reusable workflows
|
||||
- `acdl-contracts` — developer surface (`contract.yaml` + issue trigger)
|
||||
- `acdl-evidence` — audit timeline (served via raw file URLs; Gitea has no
|
||||
native Pages — see `.ciagent/ARCHITECTURE.md` Gitea API Surface table)
|
||||
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,
|
||||
jump to the [Consumer guide](docs/consumer-guide.md).
|
||||
|
||||
## Project metadata
|
||||
## Features
|
||||
|
||||
See `.ciagent/PROJECT.md` for the full spec, `.ciagent/ROADMAP.md` for the
|
||||
5-phase breakdown, `.ciagent/REQUIREMENTS.md` for traceable requirements,
|
||||
and `.ciagent/PERSONAS.md` for the active persona roster.
|
||||
A referenceable list of what the platform provides today, for consumers and
|
||||
platform engineers alike:
|
||||
|
||||
## Phase 01 verification
|
||||
- **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. See
|
||||
[docs/modules/](docs/modules/).
|
||||
- **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. See
|
||||
[docs/environments/](docs/environments/).
|
||||
- **Central pipeline contract** — a declarative YAML instance is the single
|
||||
source of truth for both the CI and deploy workflows.
|
||||
|
||||
After running `scripts/gitea_setup.sh` (which creates `acdl-contracts` and
|
||||
`acdl-evidence` in the org and pushes the placeholder `index.html`), run:
|
||||
## Roadmap
|
||||
|
||||
```bash
|
||||
ACDL_GITEA_TOKEN=<token> scripts/verify_phase01.sh
|
||||
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, DORA) wired into the pipeline.
|
||||
- **Additional engine 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
|
||||
|
||||
The platform is **four layers + six cross-cutting concerns**, bound by the
|
||||
vision's "Two Consumer Surfaces, One Platform" tenet: consumers declare
|
||||
intent via a contract; the platform delivers the deployment through the
|
||||
same contract schema, the same policy envelope, and the same evidence
|
||||
stream.
|
||||
|
||||
Consumers have their own repos and consume ACDL by referencing `uses:` the
|
||||
central pipeline definitions. A consumer declares a contract (module +
|
||||
environment + inputs); the platform resolves it to a stack instance,
|
||||
compiles it, runs security + policy checks, computes a confidence signal,
|
||||
writes an evidence event to the audit outbox, and applies the
|
||||
infrastructure.
|
||||
|
||||
### The platform flow (end-to-end)
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["consumer contract<br/>(uses + module + environment + inputs)"] --> B
|
||||
B["schema validation<br/>(contract schema)"] --> C
|
||||
C["resolve to Target Stack<br/>(contract resolver)"] --> D
|
||||
D["security checks<br/>(adapter)"] --> E
|
||||
E["infrastructure plan<br/>(adapter compiles the stack)"] --> F
|
||||
F["policy checks<br/>(adapter -> PolicyCheckResult records)"] --> G
|
||||
G["confidence signal<br/>(6 inputs: policy, validation,<br/>freshness, source, history, NFRs)"] --> H
|
||||
H["evidence event<br/>(hash-chained, to the audit outbox)"] --> I
|
||||
I["infrastructure apply<br/>(dev only, autonomous)"]
|
||||
```
|
||||
|
||||
The script confirms:
|
||||
- both new repos exist via the Gitea API
|
||||
- the raw `index.html` URL on `acdl-evidence` returns HTTP 200
|
||||
- the `qa` and `prod` branches exist on `acdl-contracts`
|
||||
The platform validates the architecture's claim that the **stack
|
||||
commitments do not require a polyglot mess**: the adapter is the only
|
||||
engine-specific code. `modules/`, `schemas/`, `contracts/`,
|
||||
`core/confidence_signal.py`, `core/contract_resolver.py`, and
|
||||
`core/outbox_writer.py` are all engine-agnostic (no `aws_s3_bucket` /
|
||||
`aws_` infrastructure terms).
|
||||
|
||||
Exit 0 = Phase 01 success criteria met.
|
||||
## How to run
|
||||
|
||||
### Prerequisites
|
||||
|
||||
> These prerequisites are for running the **platform repo** locally. A
|
||||
> consumer does not need any of these — see the
|
||||
> [Consumer guide](docs/consumer-guide.md) for the consumer happy path.
|
||||
|
||||
- A platform-managed environment (see [docs/environments/](docs/environments/)).
|
||||
For local testing, `core/environments/dev.json` is provided as the sample.
|
||||
- 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`
|
||||
+ `jsonschema`.
|
||||
|
||||
### Run the platform pipeline end-to-end
|
||||
|
||||
```bash
|
||||
# 1. Bootstrap the AWS state backend + runner IAM user (one-time, idempotent)
|
||||
# (requires the bootstrap root key in env — skip if the state bucket +
|
||||
# acdl-spike-runner already exist)
|
||||
ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID=... ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY=... \
|
||||
python3 terraform/bootstrap/create_state_backend.py
|
||||
ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID=... ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY=... \
|
||||
python3 terraform/bootstrap/create_iam_user.py # prints the initial key
|
||||
|
||||
# 2. Rotate the runner key (writes .env.secrets, gitignored)
|
||||
ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID=... ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY=... \
|
||||
bash scripts/rotate_spike_key.sh
|
||||
|
||||
# 3. Run the full platform pipeline (contract -> environment check -> stack ->
|
||||
# adapter -> security checks -> infrastructure plan -> policy checks ->
|
||||
# confidence -> evidence event -> apply). Output is streamed to stdout.
|
||||
bash scripts/run_platform.sh contracts/static-assets.yaml
|
||||
# Expected: "=== PLATFORM E2E OK ==="
|
||||
|
||||
# Or plan-only (contract -> stack -> adapter -> infrastructure plan; no
|
||||
# policy checks / outbox):
|
||||
bash scripts/run_platform.sh --plan-only contracts/static-assets.yaml
|
||||
|
||||
# Add --quiet to suppress streaming (output to log files only):
|
||||
bash scripts/run_platform.sh --quiet contracts/static-assets.yaml
|
||||
```
|
||||
|
||||
### Test the platform (offline, no AWS required)
|
||||
|
||||
```bash
|
||||
# Install test dependencies
|
||||
pip install -r requirements-test.txt
|
||||
|
||||
# Run the test suite (all offline — uses moto for DynamoDB mocking)
|
||||
python3 -m pytest tests/ -v
|
||||
|
||||
# Run the platform in check-only mode (offline — no AWS, no policy checks,
|
||||
# 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
|
||||
# Expected: "=== PLATFORM CHECK OK ==="
|
||||
|
||||
# Reproduce the full CI pipeline locally (lint -> test -> check-only)
|
||||
bash scripts/run_ci.sh
|
||||
# Expected: "=== CI PIPELINE OK ==="
|
||||
```
|
||||
|
||||
### CI/CD pipelines
|
||||
|
||||
The CI/CD pipeline is defined by a **central pipeline contract** — a
|
||||
declarative YAML instance (`pipelines/ci.yaml`) validated against a JSON
|
||||
Schema (`schemas/pipeline.schema.json`). Both platform-runner workflows
|
||||
implement the same contract:
|
||||
|
||||
- `.github/workflows/ci.yml` — GitHub Actions (production)
|
||||
|
||||
Both run three stages: **lint** (py_compile), **test** (pytest), and
|
||||
**check-only** (`run_platform.sh --check-only`). Both trigger on push to
|
||||
`main` and on pull requests. A test (`tests/test_pipeline_contract.py`)
|
||||
validates that the workflow conforms to the contract.
|
||||
|
||||
`scripts/run_ci.sh` mirrors the CI pipeline locally — running the same
|
||||
three stages in sequence. This makes the pipeline fully reproducible from
|
||||
the shell, not just in CI:
|
||||
|
||||
```bash
|
||||
bash scripts/run_ci.sh # run all 3 stages (lint, test, check-only)
|
||||
bash scripts/run_ci.sh --quiet # suppress per-stage banners
|
||||
```
|
||||
|
||||
### Reusable deploy workflow
|
||||
|
||||
The deployment pipeline is defined by a **central deployment pipeline
|
||||
contract** (`pipelines/deploy.yaml`, validated against
|
||||
`schemas/deploy-pipeline.schema.json`) and exposed to consumer repos as a
|
||||
**reusable workflow**:
|
||||
|
||||
- `.github/workflows/deploy.yml` — GitHub Actions (production)
|
||||
|
||||
The workflow implements the same stages as `pipelines/deploy.yaml`
|
||||
(validate-contract → resolve-stack → security checks → infrastructure plan
|
||||
→ policy checks → confidence → evidence event → apply). 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](docs/consumer-guide.md) for the
|
||||
end-to-end happy path.
|
||||
|
||||
### Output streaming (run_platform.sh)
|
||||
|
||||
`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 to
|
||||
stdout.
|
||||
- **`--plan-only`** and **full mode**: streams the infrastructure plan
|
||||
output via `tee` (visible and logged).
|
||||
- **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
|
||||
backwards-compatible log-only mode.
|
||||
|
||||
## Consumer guide
|
||||
|
||||
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
|
||||
[`docs/consumer-guide.md`](docs/consumer-guide.md). The guide is generic
|
||||
across all modules; `static-assets` is the worked example.
|
||||
|
||||
## Repository layout
|
||||
|
||||
| Path | Purpose | Status |
|
||||
|------|---------|--------|
|
||||
| `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 |
|
||||
| `pipelines/` | Central pipeline contracts: `ci.yaml` (CI), `deploy.yaml` (deployment) | active |
|
||||
| `adapters/` | Angine adapters — the engine adapter (the only engine-specific code per §12) + the policy adapter | active |
|
||||
| `terraform/` | State backend (S3 + DynamoDB) + platform TF (`terraform/spike/`) + bootstrap scripts (`terraform/bootstrap/`) | 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 (`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 |
|
||||
| `tests/` | Pytest suite (all offline — adapter, confidence signal, policy adapter, outbox writer, pipeline contract, contract resolver, streaming, environment check) | active |
|
||||
| `.github/workflows/` | GitHub Actions workflows: `ci.yml` (CI), `deploy.yml` (reusable deploy, invoked by consumer repos) | active |
|
||||
| `docs/` | GitHub Pages documentation site: consumer guide, modules, contracts, pipeline, versioning, environments, architecture, vision | active |
|
||||
|
||||
## Credentials & zero-trust
|
||||
|
||||
### Default — zero-trust OIDC + attribute-based authorization
|
||||
|
||||
Consumer repos are **zero-trust**: they hold **no long-lived AWS keys** and
|
||||
no static credentials in repo secrets.
|
||||
|
||||
- **Authentication** is **OIDC federation** between the platform runners
|
||||
(GitHub Actions) and AWS. Each job mints a short-lived STS token; no
|
||||
credential is ever stored in the consumer repo or in a runner secret.
|
||||
- **Authorization** is **attribute-based (ABAC)**, not role-based (RBAC).
|
||||
AWS IAM roles and session policies are scoped by two attribute classes:
|
||||
- **Repository identity** — the runner claim (e.g.
|
||||
`repo:org/consumer-repo:ref:refs/heads/main`) binds the role's trust
|
||||
policy to the exact consumer repo + branch that invoked the workflow.
|
||||
- **Resource-creation attributes** — every resource the pipeline creates
|
||||
is tagged with `acdl:owner=<consumer-repo>` and
|
||||
`acdl:contract=<contract-id>`. The session policy grants
|
||||
view/update/delete **only on resources whose tags match the calling
|
||||
repo**.
|
||||
|
||||
The effect: a consumer's pipeline can only view and update the resources
|
||||
it created. Blast radius is contained to that consumer's own stack
|
||||
instances — one consumer can never touch another consumer's resources,
|
||||
and the consumer cannot escape its own scope.
|
||||
|
||||
### Alternative — static AWS key
|
||||
|
||||
Where OIDC is not yet available, a static AWS key **may** be used as a
|
||||
documented alternative:
|
||||
|
||||
- The key is stored in **GitHub Secrets** (consumer repo) for platform-runner
|
||||
runs, or in **`.env.secrets`** (gitignored, chmod 600) for local testing.
|
||||
- The platform rotates platform-runner keys on a **daily cadence** —
|
||||
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
|
||||
the consumer's responsibility**. The platform guarantees daily rotation
|
||||
for platform-runner runs; it does not guarantee rotation for
|
||||
locally-held copies. The consumer must rotate a local key via
|
||||
`scripts/rotate_spike_key.sh` (or equivalent) on their own cadence.
|
||||
|
||||
No long-lived credential is permitted persistently — the platform-runner
|
||||
key's useful lifetime is one workflow run, and the local alternative is
|
||||
rotated at least daily (platform-runner) or out of band (local).
|
||||
@@ -0,0 +1,65 @@
|
||||
# ACDL Adapters
|
||||
|
||||
## Overview
|
||||
|
||||
Adapters translate the engine-agnostic Target Stack IR to engine-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,136 @@
|
||||
"""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").
|
||||
|
||||
v1.9 (REQ-111): the translator is fleshed out — full PolicyReport →
|
||||
PolicyCheckResult mapping with severity + skip-with-reason handling. It
|
||||
remains inactive for Terraform-only stacks (guard preserved — emits a
|
||||
single SKIPPED `KYVERNO_INACTIVE_TF_STACK` record when no K8s manifests).
|
||||
A `--kube-version` stub is parsed but not yet used (for future GitOps).
|
||||
|
||||
D-053: the platform emits Terraform, not K8s manifests. This adapter
|
||||
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> [--kube-version <ver>]
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
SEVERITY_MAP = {
|
||||
"critical": "critical",
|
||||
"high": "high",
|
||||
"medium": "medium",
|
||||
"low": "low",
|
||||
"info": "info",
|
||||
"informational": "info",
|
||||
}
|
||||
|
||||
RESULT_MAP = {
|
||||
"pass": "pass",
|
||||
"fail": "fail",
|
||||
"warn": "skipped",
|
||||
"warning": "skipped",
|
||||
"error": "error",
|
||||
"skip": "skipped",
|
||||
"skipped": "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")
|
||||
# Skip-with-reason: a skipped result carries a message that explains why.
|
||||
message = entry.get("message", "")
|
||||
if result == "skipped" and not message:
|
||||
message = entry.get("skipReason", entry.get("skippedMessage", "skipped (no reason)"))
|
||||
policy = entry.get("policy", "")
|
||||
rule = entry.get("rule", "")
|
||||
rule_id = f"{policy}/{rule}" if rule else (policy or "KYVERNO_UNKNOWN")
|
||||
resource = entry.get("resource", "")
|
||||
if not resource and entry.get("name"):
|
||||
# Construct a resource ref from kind/name/namespace when present.
|
||||
kind = entry.get("kind", "")
|
||||
ns = entry.get("namespace", "")
|
||||
resource = f"{kind}/{ns}/{entry.get('name')}" if kind else entry.get("name", "")
|
||||
return {
|
||||
"contractId": contract_id,
|
||||
"evaluatedAt": _iso8601_now(),
|
||||
"engine": "kyverno",
|
||||
"ruleId": rule_id,
|
||||
"severity": severity,
|
||||
"result": result,
|
||||
"message": message,
|
||||
"evidence": {
|
||||
"resource": resource,
|
||||
"namespace": entry.get("namespace", ""),
|
||||
"kind": entry.get("kind", ""),
|
||||
"name": entry.get("name", ""),
|
||||
"policy": policy,
|
||||
"rule": rule,
|
||||
},
|
||||
"resourceRef": resource,
|
||||
}
|
||||
|
||||
|
||||
def _emit_inactive_tf(contract_id):
|
||||
"""Emit a SKIPPED record when the platform emits Terraform, not K8s manifests."""
|
||||
return {
|
||||
"contractId": contract_id,
|
||||
"evaluatedAt": _iso8601_now(),
|
||||
"engine": "kyverno",
|
||||
"ruleId": "KYVERNO_INACTIVE_TF_STACK",
|
||||
"severity": "info",
|
||||
"result": "skipped",
|
||||
"message": "Kyverno inactive — the platform emits Terraform, not K8s manifests. Activates when the GitOps reconciler emits K8s manifests (D-053).",
|
||||
"evidence": {},
|
||||
"resourceRef": "",
|
||||
}
|
||||
|
||||
|
||||
def adapt(policyreport_json_path, contract_id, kube_version=None):
|
||||
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))
|
||||
if not out:
|
||||
out.append(_emit_inactive_tf(contract_id))
|
||||
# kube_version is parsed but not yet used (future GitOps reconciler).
|
||||
_ = kube_version
|
||||
return out
|
||||
|
||||
|
||||
def adapt_inactive(contract_id):
|
||||
"""Convenience: emit the inactive-for-TF record directly (no report file)."""
|
||||
return [_emit_inactive_tf(contract_id)]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
kube_ver = None
|
||||
args = sys.argv[1:]
|
||||
if "--kube-version" in args:
|
||||
idx = args.index("--kube-version")
|
||||
if idx + 1 < len(args):
|
||||
kube_ver = args[idx + 1]
|
||||
args = args[:idx] + args[idx + 2:]
|
||||
if len(args) != 2:
|
||||
print("usage: kyverno_adapter.py <policyreport.json> <contract-id> [--kube-version <ver>]", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
print(json.dumps(adapt(args[0], args[1], kube_version=kube_ver), 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: "?*"
|
||||
@@ -0,0 +1,660 @@
|
||||
"""ACDL Terraform adapter — compile a Target Stack instance to Terraform.
|
||||
|
||||
ARCHITECTURE.md §12.2: the adapter translates the stack-typed L1 interface
|
||||
to a Terraform variable/output block, the L2 composition tree to a
|
||||
root module that calls the L1 modules, the stack-typed relationships to
|
||||
Terraform module references, and emits a Terraform plan from the stack.
|
||||
|
||||
The adapter is a THIN LAYER; it does not own L1/L2 content — it only
|
||||
translates. Angine-agnostic in, Terraform out.
|
||||
|
||||
Phase 09 spike: handled one L1 (s3, stack type aws:s3:bucket).
|
||||
Phase 13: generalized the resource/output emission via TYPE_MAP +
|
||||
INPUT_MAP + OUTPUT_MAP tables; added ECS Fargate stack types. S3 behavior
|
||||
is preserved (regression baseline: modules/l1/s3/instance.json).
|
||||
|
||||
CLI: adapter.py <instance.json> <out_dir>
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
# Stack type -> Terraform resource type. The only engine-specific table.
|
||||
# As more L1s land, this grows; the L1 content + stack do not change.
|
||||
TYPE_MAP = {
|
||||
"aws:s3:bucket": "aws_s3_bucket",
|
||||
"aws:ec2:vpc": "aws_vpc",
|
||||
"aws:ec2:subnet": "aws_subnet",
|
||||
"aws:ec2:routetable": "aws_route_table",
|
||||
"aws:ecs:cluster": "aws_ecs_cluster",
|
||||
"aws:ecs:task_definition": "aws_ecs_task_definition",
|
||||
"aws:ecs:service": "aws_ecs_service",
|
||||
"aws:iam:role": "aws_iam_role",
|
||||
"aws:elbv2:loadbalancer": "aws_lb",
|
||||
"aws:elbv2:listener": "aws_lb_listener",
|
||||
"aws:elbv2:targetgroup": "aws_lb_target_group",
|
||||
"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
|
||||
# mappings are listed; any input not present here uses the stack name as
|
||||
# the Terraform arg name (identity).
|
||||
INPUT_MAP = {
|
||||
"aws:s3:bucket": {"bucket_name": "bucket"},
|
||||
"aws:ec2:vpc": {"cidr": "cidr_block", "name": "_tag_name"},
|
||||
"aws:ec2:subnet": {"cidr": "cidr_block", "az": "availability_zone", "name": "_tag_name", "vpc_id": "vpc_id"},
|
||||
"aws:ec2:routetable": {"vpc_id": "vpc_id", "name": "_tag_name"},
|
||||
"aws:ecs:cluster": {},
|
||||
"aws:ecs:task_definition": {},
|
||||
"aws:ecs:service": {"security_group": "security_groups", "subnets": "subnets", "cluster_arn": "cluster"},
|
||||
"aws:iam:role": {"role_name": "name", "assume_role_policy": "assume_role_policy"},
|
||||
"aws:elbv2:loadbalancer": {"subnets": "subnets", "security_group": "security_groups"},
|
||||
"aws:elbv2:listener": {},
|
||||
"aws:elbv2:targetgroup": {"port": "port", "protocol": "protocol"},
|
||||
"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
|
||||
# non-identity mappings are listed; any output not present here uses the
|
||||
# stack name as the Terraform attribute name (identity).
|
||||
OUTPUT_MAP = {
|
||||
"aws:s3:bucket": {"bucket_arn": "arn", "bucket_name": "id"},
|
||||
"aws:ec2:vpc": {"vpc_id": "id"},
|
||||
"aws:ec2:subnet": {"subnet_ids": "id", "subnet_id": "id"},
|
||||
"aws:ec2:routetable": {},
|
||||
"aws:ecs:cluster": {"cluster_arn": "arn", "cluster_id": "id"},
|
||||
"aws:ecs:task_definition": {"task_def_arn": "arn"},
|
||||
"aws:ecs:service": {"service_arn": "id"},
|
||||
"aws:iam:role": {"role_arn": "arn", "role_id": "id"},
|
||||
"aws:elbv2:loadbalancer": {"lb_arn": "id"},
|
||||
"aws:elbv2:listener": {"listener_arn": "id"},
|
||||
"aws:elbv2:targetgroup": {"target_group_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": {},
|
||||
}
|
||||
|
||||
|
||||
def _tf_value(value):
|
||||
"""Render a Python value as a Terraform expression fragment."""
|
||||
if isinstance(value, bool):
|
||||
return "true" if value else "false"
|
||||
if isinstance(value, (int, float)) and not isinstance(value, bool):
|
||||
return str(value)
|
||||
if isinstance(value, str):
|
||||
if value.startswith("ref:"):
|
||||
raise ValueError("ref: values must be resolved via _ref_expr, not _tf_value")
|
||||
# Detect a JSON string (object/array) and emit jsonencode() so inner
|
||||
# quotes don't break HCL. Plain strings stay double-quoted.
|
||||
stripped = value.lstrip()
|
||||
if stripped and stripped[0] in "{[" :
|
||||
try:
|
||||
parsed = json.loads(value)
|
||||
if isinstance(parsed, (dict, list)):
|
||||
return f"jsonencode({json.dumps(parsed, sort_keys=True)})"
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
return f'"{value}"'
|
||||
if isinstance(value, (dict, list)):
|
||||
return f"jsonencode({json.dumps(value, sort_keys=True)})"
|
||||
raise ValueError(f"unsupported input value type {type(value).__name__}")
|
||||
|
||||
|
||||
def _ref_expr(ref_value, type_by_id):
|
||||
"""Translate a "ref:<stack_resource_id>.<output>" string to a Terraform
|
||||
interpolation "${<tf_type>.<id>.<attr>}".
|
||||
|
||||
<stack_resource_id> is the stack resource id of the producing resource;
|
||||
<output> is the per-resource output name (e.g. `subnet_id`,
|
||||
`cluster_arn`); the attribute is mapped through OUTPUT_MAP for the
|
||||
referenced resource's stack type. The resolver emits the ref using the
|
||||
stack resource id directly (not the child id), so no child->resource
|
||||
lookup table is needed here.
|
||||
"""
|
||||
body = ref_value[len("ref:"):]
|
||||
rid, out_name = body.split(".", 1)
|
||||
rtype = type_by_id.get(rid)
|
||||
if not rtype:
|
||||
raise ValueError(f"ref to unknown stack resource id {rid!r}")
|
||||
tf_type = TYPE_MAP.get(rtype)
|
||||
if not tf_type:
|
||||
raise ValueError(f"ref target {rid!r} has unknown stack type {rtype!r}")
|
||||
out_map = OUTPUT_MAP.get(rtype, {})
|
||||
tf_attr = out_map.get(out_name, out_name)
|
||||
return f"{tf_type}.{rid}.{tf_attr}"
|
||||
|
||||
|
||||
def _value_expr(value, type_by_id=None):
|
||||
"""Render a value as a Terraform expression fragment. A "ref:<id>.<output>"
|
||||
string becomes a Terraform interpolation; other values use _tf_value."""
|
||||
if isinstance(value, str) and value.startswith("ref:"):
|
||||
if type_by_id is None:
|
||||
raise ValueError("ref: value encountered without a type_by_id table")
|
||||
return _ref_expr(value, type_by_id)
|
||||
return _tf_value(value)
|
||||
|
||||
|
||||
def _emit_resource(resource, type_by_id=None):
|
||||
rtype = resource["type"]
|
||||
rid = resource["id"]
|
||||
tf_type = TYPE_MAP.get(rtype)
|
||||
if not tf_type:
|
||||
raise ValueError(f"unknown stack type {rtype!r} (adapter TYPE_MAP has no entry)")
|
||||
in_map = INPUT_MAP.get(rtype, {})
|
||||
body = []
|
||||
inputs = resource.get("inputs", {})
|
||||
for in_name, value in inputs.items():
|
||||
if in_name == "region":
|
||||
continue
|
||||
arg = in_map.get(in_name, in_name)
|
||||
if arg == "_tag_name":
|
||||
if isinstance(value, str) and not value.startswith("ref:"):
|
||||
tag_name = value
|
||||
else:
|
||||
tag_name = "app"
|
||||
continue
|
||||
if rtype == "aws:ecs:task_definition" and in_name in ("image", "port", "env"):
|
||||
continue
|
||||
if rtype == "aws:iam:role" and in_name == "managed_policies":
|
||||
continue
|
||||
if rtype == "aws:elbv2:loadbalancer" and in_name == "subnets":
|
||||
if isinstance(value, str) and value.startswith("ref:"):
|
||||
body.append(f"subnets = [{_ref_expr(value, type_by_id)}]")
|
||||
else:
|
||||
body.append(f"subnets = [{value}]" if isinstance(value, str) else f"subnets = {_tf_value(value)}")
|
||||
continue
|
||||
if rtype == "aws:elbv2:loadbalancer" and in_name == "security_group":
|
||||
if isinstance(value, str) and value.startswith("ref:"):
|
||||
body.append(f"security_groups = [{_ref_expr(value, type_by_id)}]")
|
||||
else:
|
||||
body.append(f"security_groups = [{value}]" if isinstance(value, str) else f"security_groups = {_tf_value(value)}")
|
||||
continue
|
||||
if rtype == "aws:ec2:routetable" and in_name == "igw_id":
|
||||
continue
|
||||
if rtype == "aws:ecs:service" and in_name == "lb_target_group_arn":
|
||||
if isinstance(value, str) and value.startswith("ref:"):
|
||||
tg_arn = _ref_expr(value, type_by_id)
|
||||
else:
|
||||
tg_arn = _tf_value(value)
|
||||
body.append("load_balancer {")
|
||||
body.append(f" target_group_arn = {tg_arn}")
|
||||
body.append(" container_name = \"app\"")
|
||||
body.append(" container_port = 8080")
|
||||
body.append("}")
|
||||
continue
|
||||
if rtype == "aws:ecs:service" and in_name in ("subnets", "security_group"):
|
||||
# Collected into network_configuration block (emitted after all inputs).
|
||||
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)}")
|
||||
if rtype == "aws:ecs:service":
|
||||
subnets_val = inputs.get("subnets")
|
||||
sg_val = inputs.get("security_group")
|
||||
body.append("network_configuration {")
|
||||
body.append(" subnets = " + (
|
||||
f"[{_ref_expr(subnets_val, type_by_id)}]" if isinstance(subnets_val, str) and subnets_val.startswith("ref:")
|
||||
else _tf_value([subnets_val] if isinstance(subnets_val, str) else subnets_val or [])
|
||||
))
|
||||
body.append(" security_groups = " + (
|
||||
f"[{_ref_expr(sg_val, type_by_id)}]" if isinstance(sg_val, str) and sg_val.startswith("ref:")
|
||||
else _tf_value([sg_val] if isinstance(sg_val, str) else sg_val or [])
|
||||
))
|
||||
body.append("}")
|
||||
desired = inputs.get("desired_count", 1)
|
||||
launch = inputs.get("launch_type", "FARGATE")
|
||||
body.append(f"desired_count = {desired}")
|
||||
body.append(f'launch_type = "{launch}"')
|
||||
body.append("task_definition = aws_ecs_task_definition.service-taskdefinition.arn")
|
||||
body.append("name = \"acdl-microservice\"")
|
||||
nfrs = resource.get("nfrs", {})
|
||||
if isinstance(nfrs, dict) and "versioning" in nfrs and rtype == "aws:s3:bucket":
|
||||
versioning = nfrs.get("versioning", True)
|
||||
body.append("versioning {")
|
||||
body.append(f' enabled = {"true" if versioning else "false"}')
|
||||
body.append("}")
|
||||
elif rtype == "aws:s3:bucket":
|
||||
body.append("versioning {")
|
||||
body.append(" enabled = true")
|
||||
body.append("}")
|
||||
if rtype == "aws:ecs:task_definition":
|
||||
body.append(_container_definitions(inputs))
|
||||
family = inputs.get("family", "app")
|
||||
body.append(f'family = "{family}"')
|
||||
if rtype in ("aws:ec2:vpc", "aws:ec2:subnet") and "_tag_name" in in_map.values():
|
||||
tag_name = inputs.get("name", "acdl")
|
||||
if isinstance(tag_name, str) and not tag_name.startswith("ref:"):
|
||||
body.append("tags = {")
|
||||
body.append(f' Name = "{tag_name}"')
|
||||
body.append("}")
|
||||
if rtype == "aws:iam:role" and "managed_policies" in inputs:
|
||||
arns = [a.strip() for a in str(inputs["managed_policies"]).split(",") if a.strip()]
|
||||
body.append("managed_policy_arns = [" + ", ".join(f'"{a}"' for a in arns) + "]")
|
||||
if rtype == "aws:elbv2:listener":
|
||||
body.append("default_action {")
|
||||
body.append(" type = \"forward\"")
|
||||
body.append(" target_group_arn = aws_lb_target_group.alb-targetgroup.arn")
|
||||
body.append("}")
|
||||
body.append("load_balancer_arn = aws_lb.alb-loadbalancer.id")
|
||||
if rtype == "aws:elbv2:loadbalancer":
|
||||
lb_type = inputs.get("load_balancer_type", "application")
|
||||
body.append(f'load_balancer_type = "{lb_type}"')
|
||||
if rtype == "aws:elbv2:targetgroup":
|
||||
tgt_type = inputs.get("target_type", "ip")
|
||||
body.append(f'target_type = "{tgt_type}"')
|
||||
body.append("vpc_id = aws_vpc.vpc-vpc.id")
|
||||
body.append("protocol = \"HTTP\"")
|
||||
if rtype == "aws:ec2:routetable":
|
||||
body.append("route {")
|
||||
body.append(" cidr_block = \"0.0.0.0/0\"")
|
||||
body.append(" gateway_id = aws_internet_gateway.vpc-igw.id")
|
||||
body.append("}")
|
||||
body.append("tags = {")
|
||||
rt_name = inputs.get("name", "app")
|
||||
body.append(f' Name = "{rt_name}-rt"')
|
||||
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),
|
||||
}
|
||||
desired = inputs.get("desired_count", 1)
|
||||
launch = inputs.get("launch_type", "FARGATE")
|
||||
body.append(f"desired_count = {desired}")
|
||||
body.append(f'launch_type = "{launch}"')
|
||||
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)
|
||||
|
||||
|
||||
def _emit_igw(resources):
|
||||
"""Emit an internet gateway + route table associations for the VPC."""
|
||||
vpc_id = next((r["id"] for r in resources if r["type"] == "aws:ec2:vpc"), "vpc-vpc")
|
||||
subnet_id = next((r["id"] for r in resources if r["type"] == "aws:ec2:subnet"), "vpc-subnet")
|
||||
rt_id = next((r["id"] for r in resources if r["type"] == "aws:ec2:routetable"), "vpc-routetable")
|
||||
vpc_res = next((r for r in resources if r["type"] == "aws:ec2:vpc"), None)
|
||||
igw_name = (vpc_res.get("inputs", {}).get("name", "app") if vpc_res else "app")
|
||||
parts = []
|
||||
parts.append(_resource_block("vpc-igw", "aws_internet_gateway", [
|
||||
f"vpc_id = aws_vpc.{vpc_id}.id",
|
||||
"tags = {",
|
||||
f' Name = "{igw_name}-igw"',
|
||||
"}",
|
||||
]))
|
||||
parts.append(_resource_block("vpc-rta", "aws_route_table_association", [
|
||||
f"subnet_id = aws_subnet.{subnet_id}.id",
|
||||
f"route_table_id = aws_route_table.{rt_id}.id",
|
||||
]))
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
def _container_definitions(inputs):
|
||||
image = inputs.get("image", "")
|
||||
port = inputs.get("port", 80)
|
||||
env_raw = inputs.get("env")
|
||||
environment = []
|
||||
if isinstance(env_raw, dict):
|
||||
for k, v in env_raw.items():
|
||||
environment.append({"name": k, "value": str(v)})
|
||||
elif isinstance(env_raw, str) and env_raw:
|
||||
try:
|
||||
parsed = json.loads(env_raw)
|
||||
if isinstance(parsed, dict):
|
||||
for k, v in parsed.items():
|
||||
environment.append({"name": k, "value": str(v)})
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
container = {
|
||||
"name": "app",
|
||||
"image": image,
|
||||
"essential": True,
|
||||
"portMappings": [{"containerPort": port}],
|
||||
}
|
||||
if environment:
|
||||
container["environment"] = environment
|
||||
return "container_definitions = " + _tf_value([container])
|
||||
|
||||
|
||||
def _resource_block(rid, tf_type, body):
|
||||
"""Emit a top-level resource block."""
|
||||
head = f'resource "{tf_type}" "{rid}" {{'
|
||||
body_str = "\n".join(f" {l}" for l in body)
|
||||
return f"{head}\n{body_str}\n}}\n"
|
||||
|
||||
|
||||
def _emit_output(output_name, value_expr):
|
||||
return f'output "{output_name}" {{\n value = {value_expr}\n}}\n'
|
||||
|
||||
|
||||
def adapt(stack_instance, out_dir):
|
||||
"""Emit main.tf + terraform.tf + providers.tf to out_dir for the stack instance."""
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
stack = stack_instance["stack"]
|
||||
resources = stack_instance["resources"]
|
||||
|
||||
# --- providers.tf: aws provider, region from the first resource's inputs.region ---
|
||||
region = "us-east-1"
|
||||
for r in resources:
|
||||
if "region" in r.get("inputs", {}):
|
||||
region = r["inputs"]["region"]
|
||||
break
|
||||
providers_tf = (
|
||||
f'provider "aws" {{\n'
|
||||
f' region = "{region}"\n'
|
||||
f'}}\n'
|
||||
)
|
||||
|
||||
# --- terraform.tf: required_version + required_providers + S3 backend (no DynamoDB lock per D-P09-1) ---
|
||||
# The backend key is derived from the stack name so l1 vs l2 spikes use separate state keys (D-P10-1).
|
||||
stack_name = stack.get("name", "spike")
|
||||
terraform_tf = (
|
||||
'terraform {\n'
|
||||
' required_version = ">= 1.9, < 1.10"\n'
|
||||
' required_providers {\n'
|
||||
' aws = {\n'
|
||||
' source = "hashicorp/aws"\n'
|
||||
' version = "~> 5.0"\n'
|
||||
' }\n'
|
||||
' }\n'
|
||||
' backend "s3" {\n'
|
||||
' bucket = "acdl-tfstate-581513795199-us-east-1"\n'
|
||||
f' key = "spike/{stack_name}/terraform.tfstate"\n'
|
||||
' region = "us-east-1"\n'
|
||||
' }\n'
|
||||
'}\n'
|
||||
)
|
||||
|
||||
# --- main.tf: resources + outputs ---
|
||||
# Build a stack-resource-id -> stack-type table so `ref:` input values can
|
||||
# be resolved to Terraform interpolations without a child->resource
|
||||
# lookup (the resolver emits refs with the stack resource id directly).
|
||||
type_by_id = {r["id"]: r["type"] for r in resources}
|
||||
main_tf_parts = []
|
||||
has_vpc = any(r["type"] == "aws:ec2:vpc" for r in resources)
|
||||
for r in resources:
|
||||
main_tf_parts.append(_emit_resource(r, type_by_id))
|
||||
rid = r["id"]
|
||||
rtype = r["type"]
|
||||
tf_type = TYPE_MAP.get(rtype)
|
||||
out_map = OUTPUT_MAP.get(rtype, {})
|
||||
outputs = r.get("outputs", {})
|
||||
for out_name in outputs:
|
||||
tf_attr = out_map.get(out_name, out_name)
|
||||
main_tf_parts.append(_emit_output(out_name, f"{tf_type}.{rid}.{tf_attr}"))
|
||||
if has_vpc:
|
||||
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)
|
||||
|
||||
with open(os.path.join(out_dir, "main.tf"), "w") as fh:
|
||||
fh.write(main_tf)
|
||||
with open(os.path.join(out_dir, "terraform.tf"), "w") as fh:
|
||||
fh.write(terraform_tf)
|
||||
with open(os.path.join(out_dir, "providers.tf"), "w") as fh:
|
||||
fh.write(providers_tf)
|
||||
return out_dir
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 3:
|
||||
print("usage: adapter.py <instance.json> <out_dir>", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
with open(sys.argv[1], "r") as fh:
|
||||
stack = json.load(fh)
|
||||
adapt(stack, sys.argv[2])
|
||||
print(f"adapter: emitted terraform to {sys.argv[2]}", file=sys.stderr)
|
||||
@@ -0,0 +1,90 @@
|
||||
"""Translate Checkov JSON output to ACDL PolicyCheckResult records.
|
||||
|
||||
Reads Checkov's JSON output (one framework key, e.g. terraform_plan),
|
||||
emits a list of PolicyCheckResult dicts conforming to
|
||||
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's exit code.
|
||||
|
||||
The ACDL tagging standard (D-054, D-043 closure) is enforced by a custom
|
||||
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 json
|
||||
import sys
|
||||
|
||||
|
||||
RULE_MAP = {
|
||||
"CKV_AWS_41": ("secrets-in-plaintext", "high"),
|
||||
"CKV_AWS_45": ("secrets-in-plaintext", "high"),
|
||||
"CKV_AWS_46": ("secrets-in-plaintext", "high"),
|
||||
"CKV_AWS_20": ("public-ingress", "high"),
|
||||
"CKV_AWS_57": ("public-ingress", "high"),
|
||||
"CKV_AWS_24": ("public-ingress", "medium"),
|
||||
"CKV_AWS_25": ("public-ingress", "medium"),
|
||||
"CKV_AWS_1": ("iam-wildcard", "high"),
|
||||
"CKV_AWS_40": ("iam-wildcard", "medium"),
|
||||
"CKV_AWS_7": ("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"}
|
||||
|
||||
|
||||
def _iso8601_now():
|
||||
return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
|
||||
def _to_pcr(checkov_record, contract_id, result_str):
|
||||
check_id = checkov_record.get("check_id", "")
|
||||
default_sev = RULE_MAP.get(check_id, (check_id, "info"))[1]
|
||||
severity = checkov_record.get("severity", default_sev)
|
||||
if isinstance(severity, str):
|
||||
severity = severity.lower()
|
||||
return {
|
||||
"contractId": contract_id,
|
||||
"evaluatedAt": _iso8601_now(),
|
||||
"engine": "checkov",
|
||||
"ruleId": check_id,
|
||||
"severity": severity,
|
||||
"result": _RESULT_MAP.get(result_str, "error"),
|
||||
"message": checkov_record.get("check_name", ""),
|
||||
"evidence": {
|
||||
"file_path": checkov_record.get("file_path"),
|
||||
"resource": checkov_record.get("resource"),
|
||||
"resource_address": checkov_record.get("resource_address"),
|
||||
"code_block": checkov_record.get("code_block"),
|
||||
},
|
||||
"resourceRef": checkov_record.get("resource_address") or checkov_record.get("resource", ""),
|
||||
}
|
||||
|
||||
|
||||
def adapt(checkov_json_path, contract_id):
|
||||
with open(checkov_json_path, "r", encoding="utf-8") as fh:
|
||||
data = json.load(fh)
|
||||
out = []
|
||||
for framework, body in data.items():
|
||||
results = body.get("results", body) if isinstance(body, dict) else {}
|
||||
if not isinstance(results, dict):
|
||||
continue
|
||||
for rec in results.get("passed_checks", []):
|
||||
out.append(_to_pcr(rec, contract_id, "PASSED"))
|
||||
for rec in results.get("failed_checks", []):
|
||||
out.append(_to_pcr(rec, contract_id, "FAILED"))
|
||||
for rec in results.get("skipped_checks", []):
|
||||
out.append(_to_pcr(rec, contract_id, "SKIPPED"))
|
||||
return out
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 3:
|
||||
print("usage: checkov_adapter.py <checkov.json> <contract-id>", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
print(json.dumps(adapt(sys.argv[1], sys.argv[2]), indent=2))
|
||||
@@ -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,193 @@
|
||||
"""Wiz adapter — translate Wiz API results to ACDL PolicyCheckResult records.
|
||||
|
||||
Wiz is a SaaS security platform with a GraphQL API. This adapter
|
||||
translates Wiz issue records to the normalized PolicyCheckResult schema
|
||||
(engine: "wiz"), matching the Checkov adapter pattern.
|
||||
|
||||
v1.9 (REQ-110): the adapter is a real API client. `WizClient` queries the
|
||||
Wiz GraphQL API (`<WIZ_API_URL>/graphql`, Bearer auth, `issues` query)
|
||||
and translates results → PolicyCheckResult records. It degrades
|
||||
gracefully (single `SKIPPED` `WIZ_NOT_CONFIGURED` record) when
|
||||
`WIZ_API_TOKEN` or `WIZ_API_URL` is unset (D-052). Pagination is handled
|
||||
via `pageInfo.hasNextPage` + `endCursor`. Offline tests use a recorded
|
||||
GraphQL fixture.
|
||||
|
||||
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",
|
||||
"INFORMATIONAL": "info",
|
||||
"INFO": "info",
|
||||
}
|
||||
|
||||
RESULT_MAP = {
|
||||
"OPEN": "fail",
|
||||
"RESOLVED": "pass",
|
||||
"IN_PROGRESS": "skipped",
|
||||
"DISMISSED": "skipped",
|
||||
}
|
||||
|
||||
|
||||
_ISSUES_QUERY = """
|
||||
query IssuesQuery($filterBy: IssueFilter, $after: String) {
|
||||
issues(filterBy: $filterBy, after: $after) {
|
||||
nodes {
|
||||
id
|
||||
severity
|
||||
title
|
||||
status
|
||||
entity { id name type cloudPlatform }
|
||||
control { id name }
|
||||
createdAt
|
||||
}
|
||||
pageInfo { hasNextPage endCursor }
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
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", {}) or {}
|
||||
entity = wiz_issue.get("entity", {}) or {}
|
||||
rule_id = control.get("name") or wiz_issue.get("id") or "WIZ_UNKNOWN"
|
||||
return {
|
||||
"contractId": contract_id,
|
||||
"evaluatedAt": _iso8601_now(),
|
||||
"engine": "wiz",
|
||||
"ruleId": rule_id,
|
||||
"severity": severity,
|
||||
"result": result,
|
||||
"message": wiz_issue.get("title", control.get("name", "")),
|
||||
"evidence": {
|
||||
"resource": entity.get("id"),
|
||||
"resource_name": entity.get("name"),
|
||||
"cloud_platform": entity.get("cloudPlatform"),
|
||||
},
|
||||
"resourceRef": 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 or WIZ_API_URL not set); degraded gracefully (D-052).",
|
||||
"evidence": {},
|
||||
"resourceRef": "",
|
||||
}
|
||||
|
||||
|
||||
class WizClient:
|
||||
"""Real Wiz GraphQL API client (REQ-110).
|
||||
|
||||
Reads WIZ_API_TOKEN + WIZ_API_URL from the environment. `fetch_issues`
|
||||
queries the Wiz GraphQL API and returns a list of issue dicts.
|
||||
Pagination is handled via pageInfo.hasNextPage + endCursor.
|
||||
"""
|
||||
|
||||
def __init__(self, token=None, url=None):
|
||||
self.token = token or os.environ.get("WIZ_API_TOKEN", "")
|
||||
self.url = (url or os.environ.get("WIZ_API_URL", "")).rstrip("/")
|
||||
if not self.token or not self.url:
|
||||
raise RuntimeError("WizClient requires WIZ_API_TOKEN + WIZ_API_URL")
|
||||
|
||||
def _post(self, query, variables):
|
||||
import urllib.request
|
||||
endpoint = f"{self.url}/graphql"
|
||||
payload = json.dumps({"query": query, "variables": variables}).encode("utf-8")
|
||||
req = urllib.request.Request(
|
||||
endpoint,
|
||||
data=payload,
|
||||
headers={
|
||||
"Authorization": f"Bearer {self.token}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method="POST",
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
return json.loads(resp.read().decode("utf-8"))
|
||||
|
||||
def fetch_issues(self, filter_by=None, max_pages=10):
|
||||
issues = []
|
||||
after = None
|
||||
for _ in range(max_pages):
|
||||
data = self._post(_ISSUES_QUERY, {"filterBy": filter_by or {}, "after": after})
|
||||
root = data.get("data", {}).get("issues", {})
|
||||
nodes = root.get("nodes", [])
|
||||
issues.extend(nodes)
|
||||
page_info = root.get("pageInfo", {})
|
||||
if not page_info.get("hasNextPage"):
|
||||
break
|
||||
after = page_info.get("endCursor")
|
||||
return issues
|
||||
|
||||
|
||||
def fetch_and_adapt(contract_id, filter_by=None, client=None):
|
||||
"""Fetch Wiz issues via the real client and translate to PolicyCheckResult.
|
||||
|
||||
When the client is not configured (no token/url), emit the SKIPPED
|
||||
WIZ_NOT_CONFIGURED record (graceful degrade).
|
||||
"""
|
||||
if client is None:
|
||||
try:
|
||||
client = WizClient()
|
||||
except RuntimeError:
|
||||
return [_emit_not_configured(contract_id)]
|
||||
issues = client.fetch_issues(filter_by=filter_by)
|
||||
if not issues:
|
||||
return [_emit_not_configured(contract_id)]
|
||||
return [_to_pcr(i, contract_id) for i in issues]
|
||||
|
||||
|
||||
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
|
||||
# or a full GraphQL response shape ({data: {issues: {nodes: [...]}}}).
|
||||
if isinstance(data, list):
|
||||
issues = data
|
||||
elif "data" in data and "issues" in data.get("data", {}):
|
||||
issues = data["data"]["issues"].get("nodes", [])
|
||||
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") and os.environ.get("WIZ_API_URL"))
|
||||
|
||||
|
||||
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,40 +0,0 @@
|
||||
# ACDL issue-to-contract workflow (Phase 01 skeleton, reference copy).
|
||||
#
|
||||
# This file is the source-of-truth copy kept in the `acdl` repo under
|
||||
# contracts-repo/.gitea/workflows/. Phase 04 will push it to the actual
|
||||
# `acdl-contracts` repo under .gitea/workflows/ and implement the real
|
||||
# step bodies.
|
||||
#
|
||||
# Trigger: a new Issue is opened in acdl-contracts. The workflow runs
|
||||
# l3b_agent_stub.py to map the Issue body to a contract.yaml, commits the
|
||||
# contract to a new branch, closes the Issue, and triggers the main
|
||||
# pipeline in the `acdl` repo via the workflow_dispatch API (D-014; Gitea
|
||||
# Actions does not support repository_dispatch).
|
||||
name: issue-to-contract
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
parse-and-trigger:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# Phase 04 will implement:
|
||||
# 1. checkout acdl-contracts (so l3b_agent_stub.py is available).
|
||||
# 2. run: python3 scripts/l3b_agent_stub.py "${{ gitea.event.issue.body }}" > contract.yaml
|
||||
# 3. parse the generated contract; commit it to a new branch
|
||||
# (e.g. contract/<issue-number>).
|
||||
# 4. push the branch.
|
||||
# 5. close the Issue with a comment linking to the pipeline run.
|
||||
# 6. trigger the main pipeline:
|
||||
# curl -X POST \
|
||||
# -H "Authorization: token ${GITEA_TOKEN}" \
|
||||
# https://git.cloudinit.dev/api/v1/repos/continuous-intelligence/acdl/actions/workflows/<id>/dispatches \
|
||||
# -d '{"ref":"milestone/v1.0-initial","inputs":{"contract-ref":"<branch>"}}'
|
||||
- name: "Issue-trigger placeholder"
|
||||
run: |
|
||||
echo "issue-to-contract placeholder (Phase 01 skeleton)"
|
||||
echo "Issue body: ${{ gitea.event.issue.body }}"
|
||||
echo "Phase 04 will run l3b_agent_stub.py, commit contract.yaml, close issue, dispatch pipeline"
|
||||
exit 0
|
||||
@@ -0,0 +1,11 @@
|
||||
# ACDL sample consumer contract — microservice module (dev)
|
||||
# Per-environment contract (REQ-105). Promotion = running the dev job;
|
||||
# no environment field editing. Interpolation resolves against dev.json.
|
||||
uses: acdl/pipelines/deploy.yaml@v1.9
|
||||
module: microservice
|
||||
environment: dev
|
||||
inputs:
|
||||
bucket_name: acdl-${env.environment}-${contract.module}-${env.account_id}-${env.region}
|
||||
region: ${env.region}
|
||||
image: public.ecr.aws/docker/library/nginx:latest
|
||||
port: 80
|
||||
@@ -0,0 +1,11 @@
|
||||
# ACDL sample consumer contract — microservice module (dr)
|
||||
# Per-environment contract (REQ-105). Promotion = running the dr job;
|
||||
# no environment field editing. Interpolation resolves against dr.json.
|
||||
uses: acdl/pipelines/deploy.yaml@v1.9
|
||||
module: microservice
|
||||
environment: dr
|
||||
inputs:
|
||||
bucket_name: acdl-${env.environment}-${contract.module}-${env.account_id}-${env.region}
|
||||
region: ${env.region}
|
||||
image: public.ecr.aws/docker/library/nginx:latest
|
||||
port: 80
|
||||
@@ -0,0 +1,11 @@
|
||||
# ACDL sample consumer contract — microservice module (prod)
|
||||
# Per-environment contract (REQ-105). Promotion = running the prod job;
|
||||
# no environment field editing. Interpolation resolves against prod.json.
|
||||
uses: acdl/pipelines/deploy.yaml@v1.9
|
||||
module: microservice
|
||||
environment: prod
|
||||
inputs:
|
||||
bucket_name: acdl-${env.environment}-${contract.module}-${env.account_id}-${env.region}
|
||||
region: ${env.region}
|
||||
image: public.ecr.aws/docker/library/nginx:latest
|
||||
port: 80
|
||||
@@ -0,0 +1,11 @@
|
||||
# ACDL sample consumer contract — microservice module (qa)
|
||||
# Per-environment contract (REQ-105). Promotion = running the qa job;
|
||||
# no environment field editing. Interpolation resolves against qa.json.
|
||||
uses: acdl/pipelines/deploy.yaml@v1.9
|
||||
module: microservice
|
||||
environment: qa
|
||||
inputs:
|
||||
bucket_name: acdl-${env.environment}-${contract.module}-${env.account_id}-${env.region}
|
||||
region: ${env.region}
|
||||
image: public.ecr.aws/docker/library/nginx:latest
|
||||
port: 80
|
||||
@@ -0,0 +1,14 @@
|
||||
# ACDL sample consumer contract — microservice module (dev)
|
||||
#
|
||||
# Reference example for an ECS Fargate microservice deployment.
|
||||
# Interpolation (D-081): bucket_name uses the naming pattern that includes
|
||||
# region, aws account id, and environment:
|
||||
# acdl-${env.environment}-${contract.module}-${env.account_id}-${env.region}
|
||||
uses: acdl/pipelines/deploy.yaml@v1.9
|
||||
module: microservice
|
||||
environment: dev
|
||||
inputs:
|
||||
bucket_name: acdl-${env.environment}-${contract.module}-${env.account_id}-${env.region}
|
||||
region: ${env.region}
|
||||
image: public.ecr.aws/docker/library/nginx:latest
|
||||
port: 80
|
||||
@@ -0,0 +1,10 @@
|
||||
# ACDL sample consumer contract — static-assets module (dev)
|
||||
# Per-environment contract (REQ-105). The dev default
|
||||
# (contracts/static-assets.yaml) remains for backwards compat; this file
|
||||
# is the explicit per-env dev contract. Interpolation resolves against dev.json.
|
||||
uses: acdl/pipelines/deploy.yaml@v1.9
|
||||
module: static-assets
|
||||
environment: dev
|
||||
inputs:
|
||||
bucket_name: acdl-${env.environment}-${contract.module}-${env.account_id}-${env.region}
|
||||
region: ${env.region}
|
||||
@@ -0,0 +1,9 @@
|
||||
# ACDL sample consumer contract — static-assets module (dr)
|
||||
# Per-environment contract (REQ-105). Promotion = running the dr job;
|
||||
# no environment field editing. Interpolation resolves against dr.json.
|
||||
uses: acdl/pipelines/deploy.yaml@v1.9
|
||||
module: static-assets
|
||||
environment: dr
|
||||
inputs:
|
||||
bucket_name: acdl-${env.environment}-${contract.module}-${env.account_id}-${env.region}
|
||||
region: ${env.region}
|
||||
@@ -0,0 +1,9 @@
|
||||
# ACDL sample consumer contract — static-assets module (prod)
|
||||
# Per-environment contract (REQ-105). Promotion = running the prod job;
|
||||
# no environment field editing. Interpolation resolves against prod.json.
|
||||
uses: acdl/pipelines/deploy.yaml@v1.9
|
||||
module: static-assets
|
||||
environment: prod
|
||||
inputs:
|
||||
bucket_name: acdl-${env.environment}-${contract.module}-${env.account_id}-${env.region}
|
||||
region: ${env.region}
|
||||
@@ -0,0 +1,9 @@
|
||||
# ACDL sample consumer contract — static-assets module (qa)
|
||||
# Per-environment contract (REQ-105). Promotion = running the qa job;
|
||||
# no environment field editing. Interpolation resolves against qa.json.
|
||||
uses: acdl/pipelines/deploy.yaml@v1.9
|
||||
module: static-assets
|
||||
environment: qa
|
||||
inputs:
|
||||
bucket_name: acdl-${env.environment}-${contract.module}-${env.account_id}-${env.region}
|
||||
region: ${env.region}
|
||||
@@ -0,0 +1,23 @@
|
||||
# ACDL sample consumer contract — static-assets module (dev)
|
||||
#
|
||||
# This is the reference example for a consumer contract. It declares:
|
||||
# uses: the central ACDL deployment pipeline to reference
|
||||
# module: which module to deploy (must match a registry key)
|
||||
# environment: which environment to deploy to (dev = autonomous)
|
||||
# inputs: module-specific inputs
|
||||
#
|
||||
# Validated against schemas/contract.schema.json.
|
||||
# Resolved by core/contract_resolver.py to a Target Stack instance.
|
||||
#
|
||||
# Interpolation (D-081): ${env.<field>} + ${contract.<field>} tokens are
|
||||
# expanded by the resolver from the environment onboarding JSON. The
|
||||
# bucket_name below demonstrates the naming pattern that includes region,
|
||||
# aws account id, and environment:
|
||||
# acdl-${env.environment}-${contract.module}-${env.account_id}-${env.region}
|
||||
|
||||
uses: acdl/pipelines/deploy.yaml@v1.9
|
||||
module: static-assets
|
||||
environment: dev
|
||||
inputs:
|
||||
bucket_name: acdl-${env.environment}-${contract.module}-${env.account_id}-${env.region}
|
||||
region: ${env.region}
|
||||
@@ -0,0 +1,178 @@
|
||||
"""8-concern attestation matrix (REQ-109, D-084).
|
||||
|
||||
Implements the 8 concerns from `core/hitl_matrix_design.md` §10.4. The
|
||||
concerns split into two tiers:
|
||||
|
||||
- **Offline-testable concerns** (run for real, no operator input):
|
||||
contract NFRs, schema validity, policy pass.
|
||||
- **Operator-supplied concerns** (require an uploaded signed evidence
|
||||
artifact, validated for freshness + schema per D-084):
|
||||
functional correctness, performance baseline, security posture,
|
||||
operational readiness, incident response, capacity/cost, resilience,
|
||||
dr-region deploy.
|
||||
|
||||
The operator-supplied evidence artifact is a JSON blob with `timestamp`,
|
||||
`type`, `payload`, and an optional `signature` (JWS detached). Freshness
|
||||
is validated against the window from §10.4. Signature verification runs
|
||||
when `ACDL_ATTESTATION_SIGNING_KEY_ID` is set; it is skipped + logged
|
||||
when unset (dev/CI — D-089). The matrix fails loud if an operator-supplied
|
||||
concern is missing or expired for prod/dr.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import os
|
||||
import sys
|
||||
from typing import Optional, Tuple
|
||||
|
||||
|
||||
# Freshness windows (days) from hitl_matrix_design.md §10.4.
|
||||
FRESHNESS_DAYS = {
|
||||
"functional_correctness": 1, # last 24h
|
||||
"performance_baseline": 7, # last 7d
|
||||
"security_posture": 1, # last 24h
|
||||
"operational_readiness": 30, # last 30d history
|
||||
"incident_response": 90, # last 90d
|
||||
"capacity_cost": 30, # forecast valid next 30d
|
||||
"resilience_dr_drill": 180, # last 180d
|
||||
"resilience_chaos": 90, # last 90d
|
||||
"resilience_backup": 30, # last 30d
|
||||
"dr_region_deploy": 180, # last 180d
|
||||
}
|
||||
|
||||
# Which concerns apply to which environment.
|
||||
ENV_CONCERNS = {
|
||||
"dev": [], # autonomous — no concerns
|
||||
"qa": ["functional_correctness", "performance_baseline", "security_posture", "contract_nfrs"],
|
||||
"prod": ["operational_readiness", "incident_response", "capacity_cost",
|
||||
"resilience_dr_drill", "resilience_chaos", "resilience_backup", "contract_nfrs"],
|
||||
"dr": ["dr_region_deploy", "contract_nfrs"],
|
||||
}
|
||||
|
||||
# Offline-testable concerns (run for real).
|
||||
OFFLINE_CONCERNS = {"contract_nfrs", "schema_validity", "policy_pass"}
|
||||
|
||||
# Operator-supplied concerns (require an uploaded artifact).
|
||||
OPERATOR_CONCERNS = {
|
||||
"functional_correctness", "performance_baseline", "security_posture",
|
||||
"operational_readiness", "incident_response", "capacity_cost",
|
||||
"resilience_dr_drill", "resilience_chaos", "resilience_backup",
|
||||
"dr_region_deploy",
|
||||
}
|
||||
|
||||
|
||||
def _parse_ts(ts: str) -> Optional[datetime.datetime]:
|
||||
try:
|
||||
return datetime.datetime.fromisoformat(ts.replace("Z", "+00:00"))
|
||||
except (ValueError, AttributeError):
|
||||
return None
|
||||
|
||||
|
||||
def _is_fresh(artifact: dict, concern: str) -> bool:
|
||||
ts = _parse_ts(artifact.get("timestamp", ""))
|
||||
if ts is None:
|
||||
return False
|
||||
window_days = FRESHNESS_DAYS.get(concern, 30)
|
||||
age = datetime.datetime.now(datetime.timezone.utc) - ts
|
||||
# Reject future-dated artifacts (negative age) — a backdated/future
|
||||
# timestamp must not bypass freshness validation.
|
||||
if age.total_seconds() < 0:
|
||||
return False
|
||||
return age.days <= window_days
|
||||
|
||||
|
||||
def _verify_signature(artifact: dict) -> bool:
|
||||
"""Verify the JWS detached signature when ACDL_ATTESTATION_SIGNING_KEY_ID is set.
|
||||
|
||||
When unset (dev/CI — D-089), signature verification is skipped + logged.
|
||||
"""
|
||||
key_id = os.environ.get("ACDL_ATTESTATION_SIGNING_KEY_ID", "")
|
||||
if not key_id:
|
||||
sys.stderr.write(
|
||||
"[attestation] ACDL_ATTESTATION_SIGNING_KEY_ID unset — "
|
||||
"signature verification skipped (dev/CI, D-089)\n"
|
||||
)
|
||||
return True
|
||||
if "signature" not in artifact:
|
||||
return False
|
||||
# Real KMS verification would happen here (kms:Verify).
|
||||
# For v1.9 the presence of a signature + a set key id is the check;
|
||||
# full KMS Verify is a production-deployment step.
|
||||
return bool(artifact.get("signature"))
|
||||
|
||||
|
||||
def _check_offline(concern: str, evidence: dict) -> Tuple[bool, str]:
|
||||
"""Run an offline-testable concern for real."""
|
||||
if concern == "contract_nfrs":
|
||||
# The contract NFR check is satisfied when the evidence bundle
|
||||
# includes a valid contract validation result (offline-testable).
|
||||
nfrs = evidence.get("contract_nfrs", {})
|
||||
if nfrs.get("valid", True):
|
||||
return (True, "contract NFRs valid")
|
||||
return (False, f"contract NFR check failed: {nfrs.get('reason', 'invalid')}")
|
||||
if concern == "schema_validity":
|
||||
if evidence.get("schema_validity", {}).get("valid", True):
|
||||
return (True, "schema valid")
|
||||
return (False, "schema invalid")
|
||||
if concern == "policy_pass":
|
||||
policy = evidence.get("policy_pass", {})
|
||||
if policy.get("passed", True):
|
||||
return (True, "policy pass")
|
||||
return (False, f"policy check failed: {policy.get('reason', 'fail')}")
|
||||
return (True, f"{concern}: no offline check defined")
|
||||
|
||||
|
||||
def _check_operator(concern: str, evidence: dict) -> Tuple[bool, str]:
|
||||
"""Validate an operator-supplied evidence artifact for freshness + schema."""
|
||||
artifact = evidence.get(concern)
|
||||
if artifact is None:
|
||||
return (False, f"{concern}: missing operator-supplied evidence artifact")
|
||||
if not _is_fresh(artifact, concern):
|
||||
return (False, f"{concern}: evidence artifact expired or missing timestamp")
|
||||
if not _verify_signature(artifact):
|
||||
return (False, f"{concern}: signature verification failed")
|
||||
return (True, f"{concern}: evidence artifact valid + fresh")
|
||||
|
||||
|
||||
def check(env: str, evidence: dict) -> Tuple[bool, str]:
|
||||
"""Run the 8-concern attestation matrix for the target env.
|
||||
|
||||
Returns (ok, reason). ok=False means block the promotion.
|
||||
Dev always passes (autonomous).
|
||||
"""
|
||||
concerns = ENV_CONCERNS.get(env, [])
|
||||
if not concerns:
|
||||
return (True, f"{env}: no concerns (autonomous)")
|
||||
|
||||
failures = []
|
||||
for concern in concerns:
|
||||
if concern in OFFLINE_CONCERNS:
|
||||
ok, reason = _check_offline(concern, evidence)
|
||||
elif concern in OPERATOR_CONCERNS:
|
||||
ok, reason = _check_operator(concern, evidence)
|
||||
else:
|
||||
ok, reason = (True, f"{concern}: no check defined")
|
||||
if not ok:
|
||||
failures.append(reason)
|
||||
|
||||
if failures:
|
||||
return (False, "; ".join(failures))
|
||||
return (True, f"{env}: all {len(concerns)} concern(s) pass")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import json
|
||||
if len(sys.argv) < 2:
|
||||
print("usage: attestation_matrix.py <env> [evidence.json]", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
_env = sys.argv[1]
|
||||
_evidence = {}
|
||||
if len(sys.argv) >= 3 and os.path.isfile(sys.argv[2]):
|
||||
with open(sys.argv[2]) as f:
|
||||
_evidence = json.load(f)
|
||||
ok, reason = check(_env, _evidence)
|
||||
if ok:
|
||||
print(f"ATTESTATION PASS: {reason}")
|
||||
sys.exit(0)
|
||||
else:
|
||||
print(f"ATTESTATION BLOCK: {reason}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
@@ -0,0 +1,119 @@
|
||||
# ACDL Tiered Audit Ledger Design (REQ-20)
|
||||
|
||||
> **Status:** design authored in Phase 07 (milestone v1.1); the
|
||||
> hash-chain + DynamoDB-outbox path is **shipped + production since
|
||||
> v1.8**. The S3 Object Lock + JWS + async worker + DLQ + daily
|
||||
> checkpoints build-out is **deferred to a future milestone (D-083)** —
|
||||
> it requires non-offline-testable AWS infrastructure (Object Lock
|
||||
> bucket, KMS signing key, SQS DLQ, Lambda worker) and is not in v1.9.
|
||||
|
||||
The audit stream is the platform's tamper-evident record of every delivery
|
||||
action. The vision's "Audit truth lives outside the repository" bet [1]
|
||||
and "Not a mutable audit log" anti-goal [1] are the binding constraints.
|
||||
Version-control history does not satisfy regulatory evidence; the ledger
|
||||
is the source of truth.
|
||||
|
||||
## Three tiers
|
||||
|
||||
- **Cold tier (source of truth):** S3 with **Object Lock in compliance
|
||||
mode**, **7-year retention** (ARCHITECTURE.md §9). No one — including
|
||||
root — can delete or overwrite until retention expires. The regulatory
|
||||
record. **Deferred to a future milestone (D-083).**
|
||||
- **Hot tier (query index):** the `acdl-evidence` audit repo (unchanged
|
||||
from the v1.0 demo). Not part of the chain; a queryable mirror the
|
||||
evidence UI (`evidence-ui/index.html`) reads. Lightweight attestation
|
||||
linkage lives in the repo; the regulatory event body lives in S3.
|
||||
- **Outbox (write path):** DynamoDB, **RPO = 0** (synchronous write before
|
||||
contract submission ack). Single-region in v1 (`us-east-1`).
|
||||
**Shipped + production since v1.8.**
|
||||
|
||||
## Shipped scope (D-041) — production since v1.8
|
||||
|
||||
- **DynamoDB outbox:** table `acdl-outbox`, `PAY_PER_REQUEST` (D-044),
|
||||
PK `contractId`, SK `eventType#eventTs`, TTL `expire_at` = now + 365d
|
||||
(1-year storage per ARCHITECTURE.md §8).
|
||||
- **`prev_event_hash` chain:** SHA-256 over canonical JSON
|
||||
(`json.dumps(event, sort_keys=True, separators=(",", ":"))`), lifted
|
||||
from the v1.0 demo's `evidence_writer.py`. Auto-genesis: first event
|
||||
has `prev_hash="GENESIS"`.
|
||||
- **Synchronous write** via boto3 `put_item` (strong-consistent by
|
||||
default). No separate async worker / DLQ in v1.9 (RTO = workflow
|
||||
re-run).
|
||||
- **Mirror to `acdl-evidence`:** unchanged from v1.0 — the finalize step
|
||||
commits `audit.json` to the evidence repo (the hot tier).
|
||||
- **Evidence event shape:**
|
||||
`{seq, ts, stage, event, prev_hash, hash, contractId, environment, stack, score, band}`.
|
||||
|
||||
## Deferred to a future milestone (D-083)
|
||||
|
||||
The following build-out was authored as design in Phase 07 and is **not
|
||||
in v1.9**. It requires AWS infrastructure that cannot be exercised
|
||||
offline (Object Lock bucket, KMS signing key, SQS DLQ, Lambda worker)
|
||||
and is deferred to a future milestone. The hash-chain + DynamoDB-outbox
|
||||
path above remains the v1.9 production audit record.
|
||||
|
||||
- **S3 Object Lock:** bucket `acdl-evidence-lock-<account-id>`, Object
|
||||
Lock enabled at creation, compliance mode, 7-yr retention
|
||||
(`RetainUntilDate` = now + 7y). The outbox→S3 path is an async worker
|
||||
that reads from the outbox and writes to Object Lock.
|
||||
- **JWS detached signature (RFC 7515):** the event payload is
|
||||
canonical-JSON-serialized, SHA-256 hashed, signed with a private key;
|
||||
the signature is stored *detached* alongside the payload. Signing key =
|
||||
**platform-level KMS key** (not per-contract — a per-contract key would
|
||||
explode the key-management surface), rotated **quarterly**. The `jws`
|
||||
field is added to the event shape when this ships.
|
||||
- **Async worker + DLQ:** a Lambda (or a Gitea Actions scheduled workflow)
|
||||
reads the outbox, writes to S3 Object Lock, signs with KMS. DLQ = an
|
||||
SQS dead-letter queue for failed writes. RTO = DLQ replay.
|
||||
- **Daily checkpoints (§9):** a daily job reads the last event hash and
|
||||
writes a "checkpoint" event to the ledger (+ optionally to a public
|
||||
notarization service).
|
||||
|
||||
## JWS vs chain — orthogonality note
|
||||
|
||||
The `prev_event_hash` chain gives ordering/tamper-evidence *within* the
|
||||
log (a deleted event breaks the chain visibly); JWS gives authenticity
|
||||
*per event* (a forged event is detectable without re-reading the whole
|
||||
chain). The chain is shipped (v1.8+); JWS is deferred (D-083). Together
|
||||
they cover both integrity properties the vision's "Not a mutable audit
|
||||
log" anti-goal requires.
|
||||
|
||||
## Outbox item shape (shipped + deferred fields marked)
|
||||
|
||||
- PK `contractId` (UUID).
|
||||
- SK `eventType#eventTs` (e.g. `POLICY_CHECKED#2026-07-21T12:00:00Z`).
|
||||
- `payload` (the event body — hash-chained in v1.8+; JWS-signed when
|
||||
D-083 ships).
|
||||
- `prev_event_hash` (chain link; `GENESIS` for the first event).
|
||||
- `hash` (this event's SHA-256 over canonical JSON).
|
||||
- `approver_qa` (Gitea/GitHub username of the QA approver; populated on
|
||||
qa-promotion by v1.9's `hitl_gates.attest` — D-042).
|
||||
- `approver_prod` (SRE username; populated on prod-promotion by v1.9's
|
||||
`hitl_gates.attest`).
|
||||
- `approver_dr` (SRE username; populated on dr-promotion by v1.9's
|
||||
`hitl_gates.attest`).
|
||||
- `environment`, `stack`, `score`, `band`.
|
||||
- `expire_at` (TTL = now + 365d).
|
||||
- **Deferred (D-083):** `jws` (detached signature), `checkpoint_ref`.
|
||||
|
||||
## RPO / RTO table
|
||||
|
||||
| Phase | RPO | RTO |
|
||||
|-------|-----|-----|
|
||||
| v1.8+ (production, shipped) | 0 (sync outbox write) | workflow re-run |
|
||||
| Future milestone (D-083) | 0 (sync outbox) | async worker DLQ replay |
|
||||
|
||||
## Decision trail
|
||||
|
||||
- **D-041** — shipped scope = hash chain + outbox write; Object Lock +
|
||||
JWS + worker + DLQ are deferred (D-083).
|
||||
- **D-044** — outbox mode `PAY_PER_REQUEST`; PK/SK; TTL `expire_at` =
|
||||
now + 365d; no separate async worker in v1.9.
|
||||
- **D-042** — approver identities (`approver_qa`, `approver_prod`,
|
||||
`approver_dr`) live in the outbox; the separation-of-duties check
|
||||
(`core/separation_of_duties.py`) reads `approver_qa` and compares
|
||||
to the prod-dispatch `gitea.actor` / `github.actor`. v1.9's
|
||||
`hitl_gates.attest` populates these attributes.
|
||||
- **D-083** (v1.9) — S3 Object Lock + JWS + async worker + DLQ + daily
|
||||
checkpoints deferred to a future milestone. Requires non-offline-
|
||||
testable AWS infra.
|
||||
@@ -0,0 +1,175 @@
|
||||
"""ACDL Confidence Signal (REQ-19).
|
||||
|
||||
The platform's certified answer to "is this safe to proceed?" (vision
|
||||
tenet: "Safety is Computed, Not Assumed"). Every delivery action produces
|
||||
a measurable, explainable confidence signal; reliance on operator
|
||||
instinct is not a substitute.
|
||||
|
||||
Inputs (weights sum to 1.0, D-040):
|
||||
1. policy_results (0.30) — list[PolicyCheckResult] (schemas/policy_check_result.schema.json)
|
||||
2. validation (0.25) — {schema: bool, stack_resolved: bool, tf_validated: bool, tf_planned: bool}
|
||||
3. freshness (0.10) — {age_days: float, max_age_days: float}
|
||||
4. source (0.15) — {submitter: str, commit_sha: str, signed: bool}
|
||||
5. history (0.10) — {prior_rollbacks: int, prior_policy_fails: int}
|
||||
6. nfrs (0.10) — {declared: list[str], conformance: float|None}
|
||||
|
||||
Severity -> penalty (locked, ARCHITECTURE.md §8):
|
||||
critical -> hard override (score = 0, block)
|
||||
high -> -0.20
|
||||
medium -> -0.05
|
||||
low -> -0.01
|
||||
info -> 0.00
|
||||
|
||||
Per-env thresholds (locked, ARCHITECTURE.md §8): dev 0.50, qa 0.75, prod 0.90, dr 0.95.
|
||||
Output: {score, band, perInput, reasonCodes}.
|
||||
Halt with explicit reason on missing input (§8).
|
||||
|
||||
Spike cold-start (A-6.2): inputs 3 (freshness), 5 (history), 6 (nfrs) are
|
||||
'present + neutral 0.5' because the spike is the first submission with no
|
||||
history and no declared NFRs. The gate is *presence*, not *conformance* —
|
||||
the 'all six inputs present' dev gate (§5) is satisfied by non-null
|
||||
per-input scores.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, asdict
|
||||
from typing import List, Literal, Optional, Dict, Any
|
||||
import json
|
||||
import sys
|
||||
|
||||
|
||||
WEIGHTS = {
|
||||
"policy": 0.30,
|
||||
"validation": 0.25,
|
||||
"freshness": 0.10,
|
||||
"source": 0.15,
|
||||
"history": 0.10,
|
||||
"nfrs": 0.10,
|
||||
}
|
||||
|
||||
PENALTY = {
|
||||
"critical": None,
|
||||
"high": 0.20,
|
||||
"medium": 0.05,
|
||||
"low": 0.01,
|
||||
"info": 0.0,
|
||||
}
|
||||
|
||||
THRESHOLDS = {"dev": 0.50, "qa": 0.75, "prod": 0.90, "dr": 0.95}
|
||||
|
||||
|
||||
@dataclass
|
||||
class Signal:
|
||||
score: float
|
||||
band: Literal["pass", "warn", "block"]
|
||||
perInput: Dict[str, float]
|
||||
reasonCodes: List[str]
|
||||
|
||||
|
||||
def _per_input_score(name: str, raw: Any) -> tuple:
|
||||
"""Return (score in [0,1], reasons list). Unknown/missing -> 0.5 + INPUT_MISSING."""
|
||||
reasons: List[str] = []
|
||||
if raw is None:
|
||||
return 0.5, [f"INPUT_MISSING:{name}"]
|
||||
if name == "policy":
|
||||
pcrs = raw if isinstance(raw, list) else []
|
||||
if not pcrs:
|
||||
return 0.5, []
|
||||
scores = []
|
||||
for pcr in pcrs:
|
||||
r = pcr.get("result", "skipped")
|
||||
if r == "pass" or r == "skipped":
|
||||
scores.append(1.0)
|
||||
else:
|
||||
scores.append(0.0)
|
||||
return sum(scores) / len(scores), []
|
||||
if name == "validation":
|
||||
keys = ("schema", "stack_resolved", "tf_validated", "tf_planned")
|
||||
if not isinstance(raw, dict):
|
||||
return 0.5, []
|
||||
trues = sum(1 for k in keys if raw.get(k))
|
||||
return trues / 4.0, []
|
||||
if name == "freshness":
|
||||
if not isinstance(raw, dict):
|
||||
return 0.5, []
|
||||
age = float(raw.get("age_days", 0))
|
||||
mx = float(raw.get("max_age_days", 1)) or 1
|
||||
s = 1.0 - (age / mx)
|
||||
return max(0.0, min(1.0, s)), []
|
||||
if name == "source":
|
||||
if not isinstance(raw, dict):
|
||||
return 0.5, []
|
||||
if raw.get("submitter") and raw.get("commit_sha"):
|
||||
return 1.0, []
|
||||
return 0.5, []
|
||||
if name == "history":
|
||||
if not isinstance(raw, dict):
|
||||
return 0.5, []
|
||||
rollbacks = int(raw.get("prior_rollbacks", 0))
|
||||
fails = int(raw.get("prior_policy_fails", 0))
|
||||
s = 1.0 - (rollbacks * 0.2 + fails * 0.1)
|
||||
return max(0.0, min(1.0, s)), []
|
||||
if name == "nfrs":
|
||||
if not isinstance(raw, dict):
|
||||
return 0.5, []
|
||||
conf = raw.get("conformance")
|
||||
if conf is None:
|
||||
return 0.5, []
|
||||
return float(conf), []
|
||||
return 0.5, []
|
||||
|
||||
|
||||
def compute(contract_id: str, environment: str,
|
||||
inputs: Dict[str, Any]) -> Signal:
|
||||
"""Orchestrate the 6-input weighted sum + severity penalty + band."""
|
||||
missing = sorted(set(WEIGHTS.keys()) - set(inputs.keys()))
|
||||
if missing:
|
||||
return Signal(0.0, "block", {},
|
||||
[f"INPUT_MISSING:{m}" for m in missing])
|
||||
|
||||
per_input: Dict[str, float] = {}
|
||||
reasons: List[str] = []
|
||||
base = 0.0
|
||||
for name, weight in WEIGHTS.items():
|
||||
raw = inputs.get(name)
|
||||
s, r = _per_input_score(name, raw)
|
||||
per_input[name] = s
|
||||
reasons.extend(r)
|
||||
base += s * weight
|
||||
|
||||
penalty = 0.0
|
||||
policy_input = inputs.get("policy")
|
||||
pcrs = policy_input if isinstance(policy_input, list) else []
|
||||
for pcr in pcrs:
|
||||
if not isinstance(pcr, dict):
|
||||
continue
|
||||
if pcr.get("result") != "fail":
|
||||
continue
|
||||
sev = pcr.get("severity")
|
||||
p = PENALTY.get(sev, 0.0)
|
||||
if p is None:
|
||||
return Signal(0.0, "block", per_input,
|
||||
reasons + [f"CRITICAL_OVERRIDE:{pcr.get('ruleId','?')}"])
|
||||
penalty += p
|
||||
|
||||
score = max(0.0, min(1.0, base - penalty))
|
||||
threshold = THRESHOLDS[environment]
|
||||
if score >= threshold:
|
||||
band = "pass"
|
||||
elif score < threshold - 0.10:
|
||||
band = "block"
|
||||
else:
|
||||
band = "warn"
|
||||
if environment == "dev" and band == "warn":
|
||||
band = "block"
|
||||
return Signal(score, band, per_input, reasons)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 3:
|
||||
print("usage: confidence_signal.py <inputs.json> <environment>", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
env = sys.argv[2]
|
||||
with open(sys.argv[1], "r", encoding="utf-8") as fh:
|
||||
inputs = json.load(fh)
|
||||
sig = compute("cli", env, inputs)
|
||||
print(json.dumps(asdict(sig), indent=2))
|
||||
@@ -0,0 +1,478 @@
|
||||
"""ACDL Contract Resolver — resolve a consumer contract to a Target Stack instance.
|
||||
|
||||
The contract resolver is the bridge between the consumer's declared intent
|
||||
(a contract YAML) and the platform's executable representation (a Target
|
||||
Stack JSON instance). It:
|
||||
|
||||
1. Loads and validates the contract against schemas/contract.schema.json.
|
||||
2. Looks up the module name in modules/registry.json.
|
||||
3. If the module is an L1 primitive: builds a stack instance directly from
|
||||
the interface.json + contract inputs.
|
||||
4. If the module is an L2 composition: loads the composition.json, expands
|
||||
children to stack resources, resolves wires to ref: expressions, and
|
||||
emits the full stack instance.
|
||||
|
||||
The output is a JSON instance valid against schemas/stack.schema.json,
|
||||
ready for the Terraform adapter to compile.
|
||||
|
||||
CLI: contract_resolver.py <contract.yaml> <out.json>
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
import yaml
|
||||
import jsonschema
|
||||
|
||||
|
||||
def _load_env(env_name, repo_root):
|
||||
"""Load the environment onboarding JSON for env_name.
|
||||
|
||||
Mirrors core.environment_check.load() but is self-contained so the
|
||||
resolver works both as a package import (`from core.contract_resolver
|
||||
import resolve`) and as a script (`python3 core/contract_resolver.py`).
|
||||
Emits a stderr warning when account_id is the placeholder and env != dev.
|
||||
"""
|
||||
env_file = os.path.join(repo_root, "core", "environments", f"{env_name}.json")
|
||||
if not os.path.isfile(env_file):
|
||||
raise FileNotFoundError(f"no environment file for '{env_name}' at {env_file}")
|
||||
env = _load_json(env_file)
|
||||
if env.get("account_id") == "000000000000" and env_name != "dev":
|
||||
sys.stderr.write(
|
||||
f"WARNING: environment '{env_name}' has the placeholder account_id "
|
||||
f"000000000000 — replace it with the real {env_name} account id "
|
||||
f"before deploying (onboarding scaffold).\n"
|
||||
)
|
||||
return env
|
||||
|
||||
|
||||
def _load_json(path):
|
||||
with open(path, "r") as fh:
|
||||
return json.load(fh)
|
||||
|
||||
|
||||
def _load_yaml(path):
|
||||
with open(path, "r") as fh:
|
||||
return yaml.safe_load(fh)
|
||||
|
||||
|
||||
_TOKEN_RE = re.compile(r"\$\{([a-zA-Z_][a-zA-Z0-9_.]*)\}")
|
||||
|
||||
|
||||
def _lookup_dotted(context, dotted):
|
||||
"""Look up a dotted path (e.g. 'env.state_backend.bucket') in context.
|
||||
|
||||
context is a dict of top-level namespaces (e.g. {'env': {...}, 'contract': {...}}).
|
||||
Returns the value or raises KeyError if any segment is missing.
|
||||
"""
|
||||
parts = dotted.split(".")
|
||||
cur = context
|
||||
for part in parts:
|
||||
if isinstance(cur, dict) and part in cur:
|
||||
cur = cur[part]
|
||||
else:
|
||||
raise KeyError(dotted)
|
||||
return cur
|
||||
|
||||
|
||||
def _expand_vars(value, context):
|
||||
"""Recursively expand ${env.<field>} and ${contract.<field>} tokens in value.
|
||||
|
||||
Walks dicts, lists, and strings. Unknown tokens raise ValueError (fail
|
||||
loud, no silent passthrough — D-081). Dotted paths are supported
|
||||
(e.g. ${env.state_backend.bucket}). The expansion is recursive per D-087
|
||||
so nested map/list values expand too.
|
||||
"""
|
||||
if isinstance(value, str):
|
||||
def _replace(match):
|
||||
token = match.group(1)
|
||||
try:
|
||||
resolved = _lookup_dotted(context, token)
|
||||
except KeyError:
|
||||
raise ValueError(f"unresolved interpolation token: ${{{token}}}")
|
||||
if isinstance(resolved, (dict, list)):
|
||||
return json.dumps(resolved)
|
||||
return str(resolved)
|
||||
return _TOKEN_RE.sub(_replace, value)
|
||||
if isinstance(value, dict):
|
||||
return {k: _expand_vars(v, context) for k, v in value.items()}
|
||||
if isinstance(value, list):
|
||||
return [_expand_vars(v, context) for v in value]
|
||||
return value
|
||||
|
||||
|
||||
def _resolve_wire_value(wire, contract_inputs, child_outputs):
|
||||
"""Resolve a wire 'from' reference to a concrete value.
|
||||
|
||||
Wire 'from' can be:
|
||||
- "contract.inputs.<name>" — a contract input value
|
||||
- "<childId>.outputs.<name>" — a reference to another child's output
|
||||
|
||||
Returns either a concrete value (string/number/boolean) or a
|
||||
"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"]
|
||||
to_expr = wire["to"]
|
||||
|
||||
# If the 'from' is a contract input, use the concrete value
|
||||
if from_expr.startswith("contract.inputs."):
|
||||
input_name = from_expr[len("contract.inputs."):]
|
||||
if input_name in contract_inputs:
|
||||
return contract_inputs[input_name]
|
||||
# Check for default
|
||||
default = wire.get("default")
|
||||
if default is not None:
|
||||
return default
|
||||
return None
|
||||
|
||||
# If the 'from' is a child output, emit a ref: expression
|
||||
if "." in from_expr:
|
||||
parts = from_expr.split(".", 2)
|
||||
if len(parts) >= 3 and parts[1] == "outputs":
|
||||
child_id = parts[0]
|
||||
output_name = parts[2]
|
||||
# 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
|
||||
|
||||
|
||||
def resolve_l1(contract, registry, repo_root):
|
||||
"""Resolve a contract referencing an L1 primitive to a stack instance."""
|
||||
module_name = contract["module"]
|
||||
module_ref = f"{module_name}@1.0.0"
|
||||
inputs = contract.get("inputs", {})
|
||||
environment = contract.get("environment", "dev")
|
||||
|
||||
# Load the interface
|
||||
entry = registry[module_name]["1.0.0"]
|
||||
iface_path = os.path.join(repo_root, entry["interface"])
|
||||
iface = _load_json(iface_path)
|
||||
|
||||
# Build the stack instance
|
||||
stack_instance = {
|
||||
"version": "1.0.0",
|
||||
"stack": {
|
||||
"name": module_name,
|
||||
"kind": "l1",
|
||||
"depth": 1,
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"id": iface.get("type", module_name).split(":")[-1]
|
||||
if ":" in iface.get("type", "") else module_name,
|
||||
"type": iface["type"],
|
||||
"module": module_ref,
|
||||
"inputs": dict(inputs),
|
||||
"outputs": {
|
||||
out_name: {"type": out_spec.get("type", "string")}
|
||||
for out_name, out_spec in iface.get("outputs", {}).items()
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
# Add NFRs if present in the interface
|
||||
nfrs = iface.get("nfrs", {})
|
||||
if nfrs:
|
||||
stack_instance["resources"][0]["nfrs"] = nfrs
|
||||
|
||||
return stack_instance
|
||||
|
||||
|
||||
def resolve_l2(contract, registry, repo_root):
|
||||
"""Resolve a contract referencing an L2 composition to a stack instance."""
|
||||
module_name = contract["module"]
|
||||
inputs = contract.get("inputs", {})
|
||||
|
||||
# Load the composition
|
||||
entry = registry[module_name]["1.0.0"]
|
||||
comp_path = os.path.join(repo_root, entry["interface"])
|
||||
composition = _load_json(comp_path)
|
||||
|
||||
# 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_input_map[childId] = {inputName: sub_resource_id} for multi-resource L1s
|
||||
# so a wire targeting <childId>.inputs.<name> routes to the sub-resource
|
||||
# that actually declares that input (P1-1 — desired_count → aws:ecs:service,
|
||||
# family → aws:ecs:task_definition).
|
||||
child_input_map = {}
|
||||
resources = []
|
||||
|
||||
# Expand children to resources
|
||||
for child in composition["children"]:
|
||||
child_id = child["id"]
|
||||
child_module = child["module"]
|
||||
child_name = child_module.split("@")[0]
|
||||
|
||||
# Load the child's interface to get type and outputs
|
||||
child_entry = registry[child_name]["1.0.0"]
|
||||
child_iface_path = os.path.join(repo_root, child_entry["interface"])
|
||||
child_iface = _load_json(child_iface_path)
|
||||
|
||||
# Build the output->resourceId map for this child
|
||||
child_out_map = {}
|
||||
child_in_map = {}
|
||||
|
||||
# For multi-resource L1s (like vpc), the first resource type is the
|
||||
# primary; the adapter handles expansion. Use the interface's type
|
||||
# or the first resource in the interface's resources array.
|
||||
if "resources" in child_iface and child_iface["resources"]:
|
||||
# Multi-resource L1: create one resource per sub-resource
|
||||
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 = {
|
||||
"id": res_id,
|
||||
"type": sub_res["type"],
|
||||
"module": child_module,
|
||||
"inputs": {},
|
||||
"outputs": {
|
||||
out: {"type": "string"}
|
||||
for out in sub_res.get("outputs", [])
|
||||
},
|
||||
}
|
||||
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
|
||||
# Map each declared input to this sub-resource's id (P1-1)
|
||||
for in_name in sub_res.get("inputs", []):
|
||||
child_in_map[in_name] = res_id
|
||||
else:
|
||||
# Single-resource L1
|
||||
resource = {
|
||||
"id": child_id,
|
||||
"type": child_iface["type"],
|
||||
"module": child_module,
|
||||
"inputs": {},
|
||||
"outputs": {
|
||||
out_name: {"type": out_spec.get("type", "string")}
|
||||
for out_name, out_spec in child_iface.get("outputs", {}).items()
|
||||
},
|
||||
}
|
||||
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
|
||||
|
||||
# Also map interface-level outputs (for L1s that declare outputs at the
|
||||
# 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
|
||||
child_input_map[child_id] = child_in_map
|
||||
|
||||
# Resolve wires to populate inputs
|
||||
for wire in composition.get("wires", []):
|
||||
to_expr = wire["to"]
|
||||
# Parse "to": "<childId>.inputs.<inputName>"
|
||||
to_parts = to_expr.split(".")
|
||||
if len(to_parts) != 3 or to_parts[1] != "inputs":
|
||||
continue
|
||||
target_child = to_parts[0]
|
||||
input_name = to_parts[2]
|
||||
|
||||
value = _resolve_wire_value(wire, inputs, child_outputs)
|
||||
if value is not None:
|
||||
# Route to the sub-resource that declares this input (P1-1).
|
||||
# child_input_map maps <childId> -> {inputName -> sub_resource_id}.
|
||||
# If the input is declared on a specific sub-resource, route there;
|
||||
# otherwise fall back to the first matching resource (legacy).
|
||||
in_map = child_input_map.get(target_child, {})
|
||||
target_res_id = in_map.get(input_name)
|
||||
if target_res_id is not None:
|
||||
for res in resources:
|
||||
if res["id"] == target_res_id:
|
||||
res["inputs"][input_name] = value
|
||||
break
|
||||
else:
|
||||
for res in resources:
|
||||
if res["id"] == target_child or res["id"].startswith(f"{target_child}-"):
|
||||
res["inputs"][input_name] = value
|
||||
break
|
||||
|
||||
# Build the stack instance
|
||||
stack_instance = {
|
||||
"version": "1.0.0",
|
||||
"stack": {
|
||||
"name": module_name,
|
||||
"kind": "l2",
|
||||
"depth": composition.get("depth", 1),
|
||||
},
|
||||
"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
|
||||
|
||||
|
||||
def resolve(contract_path, repo_root=None, environment_override=None):
|
||||
"""Resolve a consumer contract to a Target Stack instance.
|
||||
|
||||
Args:
|
||||
contract_path: Path to the contract YAML file.
|
||||
repo_root: Root of the ACDL repo (defaults to two levels up from this file).
|
||||
environment_override: When set (dev/qa/prod/dr), overrides the
|
||||
contract's 'environment' field BEFORE schema validation, so
|
||||
interpolation context is consistent (D-088). Used by
|
||||
run_platform.sh --environment.
|
||||
|
||||
Returns:
|
||||
A dict representing the Target Stack instance.
|
||||
"""
|
||||
if repo_root is None:
|
||||
repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
# Load contract
|
||||
contract = _load_yaml(contract_path)
|
||||
|
||||
# Apply environment override BEFORE schema validation (D-088) so the
|
||||
# schema sees the overridden value and interpolation context is consistent.
|
||||
if environment_override:
|
||||
contract["environment"] = environment_override
|
||||
|
||||
# Load schemas
|
||||
contract_schema = _load_json(os.path.join(repo_root, "schemas", "contract.schema.json"))
|
||||
|
||||
# Validate contract against schema
|
||||
jsonschema.validate(contract, contract_schema)
|
||||
|
||||
# Interpolation (D-081): expand ${env.<field>} + ${contract.<field>}
|
||||
# tokens AFTER schema validation (the schema sees raw tokens, which are
|
||||
# valid strings) and BEFORE IR resolution (the resolver sees concrete
|
||||
# values). The env context is the loaded environment onboarding JSON.
|
||||
env_name = contract.get("environment", "dev")
|
||||
env = _load_env(env_name, repo_root)
|
||||
# Expose 'environment' as an alias for the env's 'name' field so
|
||||
# ${env.environment} resolves (the env JSON uses 'name', but contracts
|
||||
# reference the environment by ${env.environment}).
|
||||
env["environment"] = env.get("name", env_name)
|
||||
context = {"env": env, "contract": contract}
|
||||
contract["inputs"] = _expand_vars(contract.get("inputs", {}), context)
|
||||
|
||||
# Load registry
|
||||
registry = _load_json(os.path.join(repo_root, "modules", "registry.json"))
|
||||
|
||||
module_name = contract["module"]
|
||||
if module_name not in registry:
|
||||
raise ValueError(f"module '{module_name}' not found in registry")
|
||||
|
||||
# Determine if L1 or L2
|
||||
entry = registry[module_name]["1.0.0"]
|
||||
interface_path = entry["interface"]
|
||||
is_l2 = "l2" in interface_path or "composition" in interface_path
|
||||
|
||||
if is_l2:
|
||||
stack_instance = resolve_l2(contract, registry, repo_root)
|
||||
else:
|
||||
stack_instance = resolve_l1(contract, registry, repo_root)
|
||||
|
||||
# Validate against stack schema
|
||||
stack_schema = _load_json(os.path.join(repo_root, "schemas", "stack.schema.json"))
|
||||
jsonschema.validate(stack_instance, stack_schema)
|
||||
|
||||
return stack_instance
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 3:
|
||||
print("usage: contract_resolver.py <contract.yaml> <out.json> [--environment <name>]", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
contract_path = sys.argv[1]
|
||||
out_path = sys.argv[2]
|
||||
env_override = None
|
||||
if "--environment" in sys.argv:
|
||||
idx = sys.argv.index("--environment")
|
||||
if idx + 1 < len(sys.argv):
|
||||
env_override = sys.argv[idx + 1]
|
||||
# Also honor the ACDL_ENVIRONMENT_OVERRIDE env var (used by run_platform.sh).
|
||||
if env_override is None and os.environ.get("ACDL_ENVIRONMENT_OVERRIDE"):
|
||||
env_override = os.environ["ACDL_ENVIRONMENT_OVERRIDE"]
|
||||
result = resolve(contract_path, environment_override=env_override)
|
||||
with open(out_path, "w") as fh:
|
||||
json.dump(result, fh, indent=2)
|
||||
print(f"resolver: resolved {contract_path} -> {out_path}", file=sys.stderr)
|
||||
@@ -0,0 +1,121 @@
|
||||
#!/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 json
|
||||
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 load(env_name, root=None):
|
||||
"""Load and return the parsed environment JSON for env_name.
|
||||
|
||||
Returns the env dict, or raises FileNotFoundError if no <env_name>.json
|
||||
exists. Emits a stderr warning when account_id is the 000000000000
|
||||
placeholder and env_name != 'dev' (prompts real binding).
|
||||
"""
|
||||
env_file = _environments_dir(root) / f"{env_name}.json"
|
||||
if not env_file.is_file():
|
||||
raise FileNotFoundError(f"no environment file for '{env_name}' at {env_file}")
|
||||
with open(env_file) as f:
|
||||
env = json.load(f)
|
||||
if env.get("account_id") == "000000000000" and env_name != "dev":
|
||||
sys.stderr.write(
|
||||
f"WARNING: environment '{env_name}' has the placeholder account_id "
|
||||
f"000000000000 — replace it with the real {env_name} account id "
|
||||
f"before deploying (onboarding scaffold).\n"
|
||||
)
|
||||
return env
|
||||
|
||||
|
||||
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,37 @@
|
||||
# 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).
|
||||
- `qa.json` — QA environment (attested, QA HITL gate, confidence >= 0.75).
|
||||
Placeholder binding (replace account_id with the real QA account).
|
||||
- `prod.json` — Production environment (attested, SRE HITL gate, confidence >= 0.90).
|
||||
Placeholder binding.
|
||||
- `dr.json` — DR environment (attested, SRE HITL gate, confidence >= 0.95).
|
||||
Placeholder binding.
|
||||
|
||||
All files validate against `schemas/environment.schema.json`. The qa/prod/dr
|
||||
placeholders use `account_id: 000000000000` with a stderr warning at load
|
||||
time (prompts real binding before deploying).
|
||||
|
||||
## 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,17 @@
|
||||
{
|
||||
"name": "dr",
|
||||
"description": "DR environment — attested (SRE HITL gate, confidence >= 0.95). Placeholder binding; replace account_id with the real DR account.",
|
||||
"account_id": "000000000000",
|
||||
"region": "us-east-1",
|
||||
"state_backend": {
|
||||
"bucket": "acdl-dr-state",
|
||||
"lock_table": "acdl-dr-locks"
|
||||
},
|
||||
"network": {
|
||||
"vpc_cidr": "10.3.0.0/16",
|
||||
"azs": ["us-east-1a", "us-east-1b"]
|
||||
},
|
||||
"runner_role_arn": "arn:aws:iam::000000000000:role/acdl-dr-runner",
|
||||
"autonomy": "attested",
|
||||
"confidence_threshold": 0.95
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "prod",
|
||||
"description": "Production environment — attested (SRE HITL gate, confidence >= 0.90). Placeholder binding; replace account_id with the real prod account.",
|
||||
"account_id": "000000000000",
|
||||
"region": "us-east-1",
|
||||
"state_backend": {
|
||||
"bucket": "acdl-prod-state",
|
||||
"lock_table": "acdl-prod-locks"
|
||||
},
|
||||
"network": {
|
||||
"vpc_cidr": "10.2.0.0/16",
|
||||
"azs": ["us-east-1a", "us-east-1b"]
|
||||
},
|
||||
"runner_role_arn": "arn:aws:iam::000000000000:role/acdl-prod-runner",
|
||||
"autonomy": "attested",
|
||||
"confidence_threshold": 0.90
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "qa",
|
||||
"description": "QA environment — attested (QA HITL gate, confidence >= 0.75). Placeholder binding; replace account_id with the real QA account.",
|
||||
"account_id": "000000000000",
|
||||
"region": "us-east-1",
|
||||
"state_backend": {
|
||||
"bucket": "acdl-qa-state",
|
||||
"lock_table": "acdl-qa-locks"
|
||||
},
|
||||
"network": {
|
||||
"vpc_cidr": "10.1.0.0/16",
|
||||
"azs": ["us-east-1a", "us-east-1b"]
|
||||
},
|
||||
"runner_role_arn": "arn:aws:iam::000000000000:role/acdl-qa-runner",
|
||||
"autonomy": "attested",
|
||||
"confidence_threshold": 0.75
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
"""HITL pre-execution attestation gates (REQ-108, D-084).
|
||||
|
||||
Records the approver identity (`gitea.actor` / `github.actor`) to the
|
||||
DynamoDB outbox for the contractId (attribute `approver_qa` /
|
||||
`approver_prod` / `approver_dr`), runs the separation-of-duties check on
|
||||
prod, invokes the 8-concern attestation matrix for the target env, and
|
||||
returns (ok, reason). Dev skips (autonomous). `scripts/run_platform.sh`
|
||||
calls `attest` before apply for qa/prod/dr.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import Optional, Tuple
|
||||
|
||||
|
||||
def _approver_attr(env: str) -> str:
|
||||
return {"qa": "approver_qa", "prod": "approver_prod", "dr": "approver_dr"}.get(env, "")
|
||||
|
||||
|
||||
def attest(contract_id: str, env: str, approver: str,
|
||||
evidence: Optional[dict] = None,
|
||||
outbox_client=None) -> Tuple[bool, str]:
|
||||
"""Attest a promotion gate for the given environment.
|
||||
|
||||
Args:
|
||||
contract_id: the contract UUID.
|
||||
env: dev/qa/prod/dr.
|
||||
approver: the approver's username (`gitea.actor` / `github.actor`).
|
||||
evidence: optional operator-supplied evidence artifacts (for the
|
||||
attestation matrix operator-supplied concerns).
|
||||
outbox_client: optional moto-mocked DynamoDB outbox client for tests.
|
||||
|
||||
Returns:
|
||||
(ok, reason). ok=False means block the promotion.
|
||||
"""
|
||||
if env == "dev":
|
||||
return (True, "dev autonomous (no HITL gate)")
|
||||
|
||||
if not approver:
|
||||
return (False, f"no approver identity for {env} (GITHUB_ACTOR/GITEA_ACTOR unset)")
|
||||
|
||||
attr = _approver_attr(env)
|
||||
if not attr:
|
||||
return (False, f"unknown environment: {env}")
|
||||
|
||||
# Record the approver to the outbox.
|
||||
if outbox_client is not None:
|
||||
outbox_client.put_approver(contract_id, attr, approver)
|
||||
|
||||
# Run the separation-of-duties check on prod.
|
||||
if env == "prod":
|
||||
from core.separation_of_duties import check as sod_check, route_halt_artifact
|
||||
ok, reason = sod_check(outbox_client, contract_id, approver)
|
||||
if not ok:
|
||||
route_halt_artifact(contract_id, reason, oncall_client=None)
|
||||
return (False, reason)
|
||||
|
||||
# Run the 8-concern attestation matrix.
|
||||
from core.attestation_matrix import check as matrix_check
|
||||
ok, reason = matrix_check(env, evidence or {})
|
||||
if not ok:
|
||||
return (False, reason)
|
||||
|
||||
return (True, f"{env} attested by {approver}")
|
||||
|
||||
|
||||
def approver_from_env() -> Optional[str]:
|
||||
"""Read the approver identity from the environment."""
|
||||
return os.environ.get("GITHUB_ACTOR") or os.environ.get("GITEA_ACTOR")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# CLI: hitl_gates.py <contract_id> <env> [evidence.json]
|
||||
if len(sys.argv) < 3:
|
||||
print("usage: hitl_gates.py <contract_id> <env> [evidence.json]", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
_cid = sys.argv[1]
|
||||
_env = sys.argv[2]
|
||||
_evidence = {}
|
||||
if len(sys.argv) >= 4 and os.path.isfile(sys.argv[3]):
|
||||
import json
|
||||
with open(sys.argv[3]) as f:
|
||||
_evidence = json.load(f)
|
||||
_approver = approver_from_env() or ""
|
||||
ok, reason = attest(_cid, _env, _approver, _evidence)
|
||||
if ok:
|
||||
print(f"HITL PASS: {reason}")
|
||||
sys.exit(0)
|
||||
else:
|
||||
print(f"HITL BLOCK: {reason}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
@@ -0,0 +1,175 @@
|
||||
# ACDL Human-in-the-Loop Matrix + Separation-of-Duties Design (REQ-21)
|
||||
|
||||
> **Status:** design authored in Phase 07 (milestone v1.1); **v1.9 wires
|
||||
> the gates** (Phase 42). The spike (Phases 08-10) was dev-only; HITL was
|
||||
> not exercised then. v1.9 implements the qa/prod/dr pre-execution
|
||||
> attestation gates, the 8-concern attestation matrix (offline-testable
|
||||
> subset), and the outbox-based separation-of-duties check.
|
||||
|
||||
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.
|
||||
|
||||
## Gate model (ARCHITECTURE.md §10.1)
|
||||
|
||||
**Pre-execution gates.** The contract is held in a "validated but not
|
||||
applied" state until the human attests. qa, prod, dr are attestation
|
||||
gates. No partial deployment to roll back on rejection (qa, prod); dr is
|
||||
a separate deployment against a separate cluster/region. The
|
||||
canary/deployment-rollback model is explicitly not in scope for v1.
|
||||
|
||||
## Gitea-specific gate mechanics (D-042)
|
||||
|
||||
Gitea has **no Environments API** and ignores `environment:` blocks
|
||||
(v1.0 D-013; re-confirmed in RESEARCH TARGET 1). The pre-execution gate
|
||||
is modeled as a `workflow_dispatch` with approval inputs:
|
||||
|
||||
- **qa gate:** `workflow_dispatch` with `approve_qa: true`; the dispatch
|
||||
run's `gitea.actor` is the QA approver.
|
||||
- **prod gate:** `workflow_dispatch` with `approve_prod: true`;
|
||||
`gitea.actor` is the SRE approver.
|
||||
- **dr gate:** `workflow_dispatch` with `approve_dr: true`; same.
|
||||
|
||||
The approver identity of record = `gitea.actor` of the dispatch run
|
||||
(D-042). There is no other approval-identity signal in Gitea. The real
|
||||
OIDC path (blocked on go-gitea/gitea#36988) does not change this —
|
||||
OIDC authorizes the *runner* to AWS, it does not change how the platform
|
||||
records the *human* approver.
|
||||
|
||||
On GitHub, the equivalent is `github.actor` of the `workflow_dispatch`
|
||||
run; GitHub Environments with required reviewers are the native gate,
|
||||
but the `workflow_dispatch` approval-input fallback is used for
|
||||
byte-identical Gitea + GitHub workflows.
|
||||
|
||||
## Reviewer routing (ARCHITECTURE.md §10.2)
|
||||
|
||||
Gitea CODEOWNERS routes the right reviewer to the right gate:
|
||||
|
||||
- qa → QA team
|
||||
- prod → SRE team
|
||||
- dr → SRE team
|
||||
|
||||
CODEOWNERS **routes**; it does **not** enforce identity distinctness (that
|
||||
is the platform-internal outbox check in
|
||||
`core/separation_of_duties.py`).
|
||||
|
||||
## Full 8-concern attestation matrix (§10.4)
|
||||
|
||||
The matrix is implemented in v1.9 as `core/attestation_matrix.py`
|
||||
(REQ-109, D-084). The concerns split into two tiers:
|
||||
|
||||
**Offline-testable concerns** (run for real, no operator input):
|
||||
- Contract NFRs (the platform's own contract validator).
|
||||
- Schema validity (jsonschema).
|
||||
- Policy pass (Checkov/Wiz/Kyverno `PolicyCheckResult` records).
|
||||
|
||||
**Operator-supplied concerns** (require an uploaded signed evidence
|
||||
artifact, validated for freshness + schema per D-084):
|
||||
- Functional correctness (e2e suite report).
|
||||
- Performance baseline (k6 / Gatling / Locust load test report).
|
||||
- Security posture (Trivy / Snyk / contract-declared scan + Security
|
||||
on-call signature).
|
||||
- Operational readiness (runbook published, dashboard exists, on-call
|
||||
rotation assigned, alerts configured).
|
||||
- Incident response (Sev-1 runbook tabletop or live drill completed).
|
||||
- Capacity / cost (FinOps forecast for next 30d within budget envelope).
|
||||
- Resilience (DR drill, chaos engineering report, backup verified).
|
||||
- dr-region deploy (most recent prod-bound dr drill as canary evidence).
|
||||
|
||||
The full table (lifted verbatim from §10.4):
|
||||
|
||||
| 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 |
|
||||
|
||||
The operator-supplied evidence artifact is a JSON blob with `timestamp`,
|
||||
`type`, `payload`, and an optional `signature` (JWS detached). Freshness
|
||||
is validated against the window above. Signature verification runs when
|
||||
`ACDL_ATTESTATION_SIGNING_KEY_ID` is set; it is skipped + logged when
|
||||
unset (dev/CI — D-089). The matrix fails loud if an operator-supplied
|
||||
concern is missing or expired for prod/dr.
|
||||
|
||||
## Timeout behavior (§10.5)
|
||||
|
||||
| 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` |
|
||||
|
||||
**Implementation:** a Gitea `on: schedule` workflow (runs hourly) that
|
||||
scans the DynamoDB outbox for `PENDING_ATTESTATION` events with `ts`
|
||||
older than 1/2 business days and emits the warn/freeze events. Not
|
||||
implemented in v1.9 (roadmap item; the attestation gates themselves are
|
||||
wired, the timeout scanner is future work).
|
||||
|
||||
## Rejection and rollback (§10.6)
|
||||
|
||||
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` (a contract-schema field — `schemas/contract.schema.json`).
|
||||
The audit chain is **extended, not torn up** (the "Not a mutable audit
|
||||
log" anti-goal). No partial deployment to roll back at any v1 gate.
|
||||
|
||||
## Separation of duties (§10.3) — pointer to the .py
|
||||
|
||||
The identity-distinctness check is platform-internal, not GitHub-native,
|
||||
not Kyverno (in v1). Sequence:
|
||||
|
||||
1. On promotion dev → qa, the platform reads the QA approver's identity
|
||||
from the `workflow_dispatch` run's `gitea.actor` (or `github.actor`)
|
||||
and writes it to the DynamoDB outbox keyed by `contractId` (attribute
|
||||
`approver_qa`).
|
||||
2. On promotion qa → prod, the platform reads the stored `approver_qa`
|
||||
from the outbox and the new SRE approver's `gitea.actor` from the
|
||||
prod-dispatch run.
|
||||
3. If `approver_qa == approver_prod`, 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 `core/separation_of_duties.py`
|
||||
(T-7.8). The platform is the only writer to the outbox; the check is
|
||||
in the same process that has authority to block the promotion.
|
||||
|
||||
v1.9 implements `route_halt_artifact` as a real SNS publish (topic
|
||||
`acdl-sod-halt`, ARN from `ACDL_SOD_HALT_TOPIC_ARN`) with an outbox-event
|
||||
fallback when the topic ARN is unset (REQ-107). The attestation gate
|
||||
itself is `core/hitl_gates.py` (`attest(contract_id, env, approver,
|
||||
evidence)`), which records the approver to the outbox, runs the SoD
|
||||
check on prod, invokes the attestation matrix, and returns `(ok, reason)`.
|
||||
|
||||
## v1.9 wiring
|
||||
|
||||
v1.9 (Phase 41 + Phase 42) wires the gates end-to-end:
|
||||
|
||||
- **Phase 41** ships the per-environment CI job structure: one job per
|
||||
environment (dev/qa/prod/dr), each pointing at its respective contract
|
||||
(or the same contract + the `environment` workflow_call input). The
|
||||
qa/prod/dr caller workflows use `workflow_dispatch` with the approval
|
||||
inputs above; dev is autonomous (no gate). Promotion = running the
|
||||
matching job; no `environment:` field editing (D-082).
|
||||
- **Phase 42** implements `core/hitl_gates.py` (the attestation gate),
|
||||
`core/attestation_matrix.py` (the 8-concern matrix), and the real
|
||||
`route_halt_artifact` (SNS + outbox fallback). `scripts/run_platform.sh`
|
||||
calls `hitl_gates.attest` before apply for qa/prod/dr (dev skips).
|
||||
|
||||
## Decision trail
|
||||
|
||||
- **D-042** — approver identity = `gitea.actor` of the `workflow_dispatch`
|
||||
run; no Environments API in Gitea. On GitHub, `github.actor`.
|
||||
- **D-013** (v1.0) — the `workflow_dispatch` approval-input fallback,
|
||||
re-used for the real platform's pre-execution gate model.
|
||||
- **D-084** (v1.9) — 8-concern attestation matrix: offline-testable
|
||||
concerns run for real; operator-supplied concerns accept signed
|
||||
evidence artifacts validated for freshness + schema.
|
||||
- **D-089** (v1.9) — attestation artifact signature verification is
|
||||
skipped when `ACDL_ATTESTATION_SIGNING_KEY_ID` is unset (dev/CI);
|
||||
required for prod/dr.
|
||||
@@ -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,71 @@
|
||||
"""ACDL Outbox Writer — write an evidence event to the DynamoDB outbox.
|
||||
|
||||
ARCHITECTURE.md §9: DynamoDB outbox, RPO=0 (synchronous write before
|
||||
ack). The event is hash-chained (SHA-256 over canonical JSON); the first
|
||||
event has prev_event_hash="GENESIS". D-P10-3: the spike writes ONE
|
||||
CONFIDENCE_COMPUTED event.
|
||||
|
||||
The outbox table (Phase 08): acdl-outbox, PAY_PER_REQUEST, PK contractId,
|
||||
SK eventType#eventTs, TTL expire_at = now + 365d (D-044).
|
||||
|
||||
CLI: outbox_writer.py <event.json> (uses AWS creds from env)
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
import boto3
|
||||
|
||||
|
||||
OUTBOX_TABLE = "acdl-outbox"
|
||||
REGION = os.environ.get("AWS_DEFAULT_REGION", "us-east-1")
|
||||
|
||||
|
||||
def _canonical_hash(event):
|
||||
"""SHA-256 over canonical JSON (sort_keys, compact separators)."""
|
||||
canonical = json.dumps(event, sort_keys=True, separators=(",", ":"))
|
||||
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def write_event(event, outbox_table=OUTBOX_TABLE, region=REGION):
|
||||
"""Write an evidence event to the DynamoDB outbox. Returns the item dict."""
|
||||
contract_id = event["contractId"]
|
||||
event_type = event.get("eventType", "CONFIDENCE_COMPUTED")
|
||||
event_ts = event.get("ts") or datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
sk = f"{event_type}#{event_ts}"
|
||||
|
||||
# Chain: first event = GENESIS (D-P10-3 spike writes one event).
|
||||
prev_hash = event.get("prev_event_hash", "GENESIS")
|
||||
event_hash = _canonical_hash(event)
|
||||
|
||||
item = {
|
||||
"contractId": {"S": contract_id},
|
||||
"eventType#eventTs": {"S": sk},
|
||||
"payload": {"S": json.dumps(event, sort_keys=True)},
|
||||
"prev_event_hash": {"S": prev_hash},
|
||||
"hash": {"S": event_hash},
|
||||
"environment": {"S": str(event.get("environment", ""))},
|
||||
"stack": {"S": str(event.get("stack", ""))},
|
||||
"score": {"N": str(event.get("score", 0))},
|
||||
"band": {"S": str(event.get("band", ""))},
|
||||
"expire_at": {"N": str(int((datetime.datetime.now(datetime.timezone.utc) +
|
||||
datetime.timedelta(days=365)).timestamp()))},
|
||||
}
|
||||
|
||||
session = boto3.Session(region_name=region)
|
||||
dyn = session.client("dynamodb")
|
||||
dyn.put_item(TableName=outbox_table, Item=item)
|
||||
return item
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 2:
|
||||
print("usage: outbox_writer.py <event.json>", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
with open(sys.argv[1], "r") as fh:
|
||||
event = json.load(fh)
|
||||
item = write_event(event)
|
||||
print(json.dumps({k: list(v.values())[0] for k, v in item.items()}, indent=2))
|
||||
@@ -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)
|
||||
@@ -0,0 +1,95 @@
|
||||
"""Check that qaApprover != prodApprover for a contract (ARCHITECTURE.md
|
||||
§10.3, D-042). Reads `approver_qa` from the DynamoDB outbox for the
|
||||
contractId, compares to the prod-dispatch `gitea.actor` / `github.actor`.
|
||||
Blocks on equality, emits `SEPARATION_OF_DUTIES_VIOLATION`, routes a halt
|
||||
artifact to SRE on-call.
|
||||
|
||||
v1.9 (REQ-107, D-085): route_halt_artifact is a real implementation —
|
||||
publishes to SNS topic `acdl-sod-halt` (ARN from ACDL_SOD_HALT_TOPIC_ARN)
|
||||
when set; falls back to a structured stderr emission + a
|
||||
SEPARATION_OF_DUTIES_VIOLATION event write to the DynamoDB outbox when
|
||||
unset. No silent print-only stub.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import Optional, Tuple
|
||||
|
||||
|
||||
def check(outbox_client, contract_id: str,
|
||||
current_prod_approver: Optional[str]) -> Tuple[bool, str]:
|
||||
"""Return (ok, reason). ok=False means block the prod promotion."""
|
||||
if outbox_client is None:
|
||||
return (True, "no outbox client (dev-only spike)")
|
||||
item = outbox_client.get(contract_id)
|
||||
if item is None:
|
||||
return (True, "no prior approver (first promotion)")
|
||||
qa_approver = item.get("approver_qa")
|
||||
if not qa_approver:
|
||||
return (True, "no QA approver recorded (dev-only spike)")
|
||||
if current_prod_approver is None:
|
||||
return (True, "no prod approver supplied (dev-only spike)")
|
||||
if qa_approver == current_prod_approver:
|
||||
return (False,
|
||||
f"SEPARATION_OF_DUTIES_VIOLATION: "
|
||||
f"qaApprover==prodApprover=={qa_approver}")
|
||||
return (True, "distinct")
|
||||
|
||||
|
||||
def route_halt_artifact(contract_id: str, violation_reason: str,
|
||||
oncall_client=None) -> None:
|
||||
"""Route a halt artifact to SRE on-call (REQ-107, D-085).
|
||||
|
||||
When ACDL_SOD_HALT_TOPIC_ARN is set, publish to the SNS topic via
|
||||
boto3. When unset (dev/CI), fall back to a structured stderr emission
|
||||
+ a SEPARATION_OF_DUTIES_VIOLATION event write to the DynamoDB outbox
|
||||
via outbox_writer.write_event (so the halt is in the audit chain).
|
||||
The oncall_client, when provided, is the SNS client (test injection).
|
||||
"""
|
||||
topic_arn = os.environ.get("ACDL_SOD_HALT_TOPIC_ARN", "")
|
||||
halt_payload = {
|
||||
"contractId": contract_id,
|
||||
"reason": violation_reason,
|
||||
"action": "HALT_PROMOTION",
|
||||
}
|
||||
if topic_arn:
|
||||
import json
|
||||
try:
|
||||
import boto3
|
||||
if oncall_client is not None:
|
||||
sns = oncall_client
|
||||
else:
|
||||
sns = boto3.client("sns")
|
||||
sns.publish(
|
||||
TopicArn=topic_arn,
|
||||
Message=json.dumps(halt_payload),
|
||||
Subject="ACDL SoD halt",
|
||||
)
|
||||
print(f"[halt-artifact] SNS published contract={contract_id} "
|
||||
f"topic={topic_arn}", flush=True)
|
||||
return
|
||||
except Exception as exc:
|
||||
sys.stderr.write(
|
||||
f"[halt-artifact] SNS publish failed ({exc}); "
|
||||
f"falling back to outbox event\n"
|
||||
)
|
||||
# Fallback: stderr + outbox event (the halt is in the audit chain).
|
||||
sys.stderr.write(
|
||||
f"[halt-artifact] contract={contract_id} reason={violation_reason} "
|
||||
f"oncall={oncall_client} (no SNS topic — outbox fallback)\n"
|
||||
)
|
||||
try:
|
||||
from core.outbox_writer import write_event
|
||||
write_event({
|
||||
"contractId": contract_id,
|
||||
"eventType": "SEPARATION_OF_DUTIES_VIOLATION",
|
||||
"environment": "",
|
||||
"stack": "",
|
||||
"score": 0,
|
||||
"band": "halt",
|
||||
"reason": violation_reason,
|
||||
})
|
||||
except Exception as exc:
|
||||
sys.stderr.write(
|
||||
f"[halt-artifact] outbox fallback write failed ({exc})\n"
|
||||
)
|
||||
@@ -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/
|
||||
@@ -0,0 +1,241 @@
|
||||
# Architecture
|
||||
|
||||
> **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
|
||||
> source of truth for *why*.
|
||||
|
||||
## 0. Purpose
|
||||
|
||||
This document encodes the architectural commitments that realize the
|
||||
[vision](vision). Every commitment is grounded in a vision tenet.
|
||||
|
||||
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
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["Consumer surfaces"] --> B["Contract schema"]
|
||||
B --> C["Central pipeline"]
|
||||
C --> D["Modules + primitives"]
|
||||
C --> E["Angine adapter"]
|
||||
C --> F["Confidence signal"]
|
||||
C --> G["Evidence stream"]
|
||||
D --> E
|
||||
E --> H["Infrastructure"]
|
||||
F --> G
|
||||
```
|
||||
|
||||
The four layers:
|
||||
|
||||
1. **Primitives** — single-purpose, engine-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 developer and agentic surfaces are parallel paths, not a progression.
|
||||
Both end in a contract submission that enters the same pipeline.
|
||||
|
||||
## 2. Primitives
|
||||
|
||||
Single-purpose, engine-agnostic modules. Locked commitments:
|
||||
|
||||
- No inter-primitive references. A primitive may call engine data sources.
|
||||
- Semver with three triggers: interface → MAJOR, behavior → MINOR,
|
||||
lifecycle → PATCH.
|
||||
- Immutability on publication.
|
||||
- 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 (engine-
|
||||
agnostic), not against any engine's variable block directly.
|
||||
|
||||
## 3. Modules
|
||||
|
||||
Patterns that combine primitives into deployable shapes. Locked commitments:
|
||||
|
||||
- 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.
|
||||
- A module's pattern tree wires field is defined against the stack's
|
||||
relationship type, not against any engine's module block. The stack →
|
||||
engine translation is the engine adapter's job (§12). The pattern
|
||||
pipeline itself is engine-agnostic.
|
||||
|
||||
## 4. Developer Surface
|
||||
|
||||
- Tag-based reference to the central pipeline template.
|
||||
- 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.
|
||||
|
||||
## 5. Agentic Surface
|
||||
|
||||
- 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.
|
||||
- Initial skill catalog: 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.
|
||||
- `profile: agentic` unlocks agentic-specific fields
|
||||
(`naturalLanguageIntent`, `confidenceAtSubmission`, `agentTrace`).
|
||||
|
||||
## 6. Cross-Cutting — Central Pipeline Template
|
||||
|
||||
- JSON Schema (draft 2020-12) with a thin domain-specific wrapper.
|
||||
- Central repo + generated client libraries.
|
||||
- Multi-stage validation pipeline: schema → policy → NFR → confidence.
|
||||
- Distributed enrichment.
|
||||
- GitOps reconciler + engine execution layer.
|
||||
- The pipeline emits a `PolicyCheckResult` record per policy rule evaluated;
|
||||
the confidence signal consumes these as one normalized input (§8).
|
||||
|
||||
## 7. Cross-Cutting — Contract Schema
|
||||
|
||||
- Central repo + generated client libraries.
|
||||
- Strict fail-fast at the schema stage with reason codes from a published
|
||||
vocabulary.
|
||||
- Per-environment mandatory fields: dev requires stack + environment; qa
|
||||
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
|
||||
|
||||
- Six canonical inputs: policy, validation, freshness, source, history, NFRs.
|
||||
- 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.
|
||||
- Severity → score penalty: 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.
|
||||
- Thresholds frozen for v1; tuning begins post-v1 with 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
|
||||
|
||||
- Every delivery action produces an immutable, hash-chained evidence event.
|
||||
- The audit stream is the platform's certified record of what happened, when,
|
||||
and why.
|
||||
- Events are written to a DynamoDB outbox and rendered on an evidence
|
||||
timeline.
|
||||
|
||||
## 10. Cross-Cutting — HITL Matrix
|
||||
|
||||
Human-in-the-loop gates for higher environments:
|
||||
|
||||
| Environment | Autonomy | Attester | Gate |
|
||||
|---|---|---|---|
|
||||
| dev | Full autonomy (no HITL) | — | Confidence ≥ 0.50, all six inputs present |
|
||||
| qa | Held for attestation | QA | Platform-runner deployment approval + full QA matrix |
|
||||
| prod | Held for attestation | SRE | Platform-runner deployment approval + full SRE matrix |
|
||||
| dr | Held for attestation | SRE | Platform-runner deployment approval + dr-drill evidence |
|
||||
|
||||
Staging does not exist. Dev is the only autonomous environment and absorbs
|
||||
integration, contract, security smoke, and performance smoke validation.
|
||||
|
||||
- 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.
|
||||
- Separation of duties: the platform-internal identity record in the
|
||||
DynamoDB outbox enforces `qaApprover ≠ prodApprover` for the same contract.
|
||||
|
||||
## 11. Cross-Cutting — Separation of Duties
|
||||
|
||||
- CODEOWNERS routes the right reviewer to the right environment.
|
||||
- The DynamoDB outbox enforces identity distinctness across environment
|
||||
approvers.
|
||||
|
||||
## 12. Cross-Cutting — Angine Execution
|
||||
|
||||
The technical execution layer. Primitives and modules are engine-agnostic
|
||||
in shape; engine adapters are the only engine-specific component.
|
||||
|
||||
The architecture defines a **Target Stack** — a engine-neutral
|
||||
description of:
|
||||
|
||||
- The resources to create (typed against the stack schema).
|
||||
- Their relationships (the module's pattern tree).
|
||||
- Their inputs (wired from the contract).
|
||||
- Policy hooks (the points in the pattern where policy checks attach).
|
||||
|
||||
The registry, the module pattern tree, the contract schema, and the
|
||||
`PolicyCheckResult` schema are all defined against the stack schema. None is
|
||||
defined against any specific engine.
|
||||
|
||||
**v1 implementation reality:** the stack is shaped to round-trip cleanly to
|
||||
Terraform because there is no other adapter to differentiate from. As
|
||||
additional adapters appear, the stack gets more expressive and the adapters
|
||||
gain translation logic, but the primitive content, the module pattern tree,
|
||||
and the contract schema do not change. This is the design that prevents a
|
||||
polyglot mess.
|
||||
|
||||
The engine adapter:
|
||||
|
||||
- Translates the stack-typed module pattern tree to a engine root module
|
||||
that calls the primitive modules.
|
||||
- Is a thin layer. It does not own primitive/module content; it only
|
||||
translates.
|
||||
- Is the only engine-specific code in the platform.
|
||||
|
||||
Policy checks run on the engine plan output. Results are normalized to
|
||||
`PolicyCheckResult` records by a policy adapter. The confidence signal
|
||||
consumes the union of all `PolicyCheckResult` records, regardless of engine
|
||||
— engine-agnostic over its inputs, matching the module model's
|
||||
engine-agnosticism over its outputs.
|
||||
|
||||
## 13. Cross-Cutting — Platform Runners
|
||||
|
||||
The platform runs on platform-managed runners (GitHub Actions in
|
||||
production). Runner-specific code = workflow YAML, OIDC trust, CODEOWNERS,
|
||||
environments. The contract schema, stack, `PolicyCheckResult`, confidence
|
||||
signal, and audit stream are portable (runner-agnostic); a second runner
|
||||
platform needs a runner adapter + workflow-template translator, with no
|
||||
change to the modules/stack/confidence/audit.
|
||||
|
||||
## 14. Versioning
|
||||
|
||||
- Primitives and modules use semver: interface → MAJOR, behavior → MINOR,
|
||||
lifecycle → PATCH.
|
||||
- A module pins primitives by `name@semver`; the resolver picks the highest
|
||||
compatible.
|
||||
- A MAJOR bump requires a new registry entry (immutable publication); the
|
||||
old entry enters a 12-month deprecation window.
|
||||
- The central deploy pipeline is referenced by a floating MAJOR + MINOR tag
|
||||
(e.g. `@v1.6`); patch fixes flow within the tag, breaking changes land
|
||||
under the next MINOR tag.
|
||||
|
||||
See [Versioning](pipeline/versioning) for the consumer-facing details.
|
||||
|
||||
## 15. OpenTofu
|
||||
|
||||
Not in v1. The engine abstraction (§12) makes OpenTofu a future adapter,
|
||||
not an architecture change. Revisit when an OpenTofu adapter is requested.
|
||||
@@ -0,0 +1,459 @@
|
||||
# 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, engine 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.9| 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.9`). 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.9`.
|
||||
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.9
|
||||
```
|
||||
|
||||
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.9
|
||||
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.9
|
||||
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.9`). 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.9
|
||||
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.9`
|
||||
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 engine 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.9
|
||||
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, 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.9`). |
|
||||
| Sample contract | `contracts/microservice.yaml` | The microservice example contract (uses `@v1.9`). |
|
||||
| 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. |
|
||||
| Angine 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: "CHG0678912"
|
||||
```
|
||||
|
||||
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).
|
||||
## Per-environment deployment
|
||||
|
||||
ACDL supports a **promotion-without-editing** model: you do not edit the
|
||||
`environment:` field in a contract to promote dev → qa → prod → dr.
|
||||
Instead, there is **one CI job per environment**, each pointing at its
|
||||
respective contract (or the same contract + the `environment` workflow
|
||||
input). Promotion = running the matching job.
|
||||
|
||||
### Two shapes (both supported)
|
||||
|
||||
**Shape 1 — per-environment contract files:** a consumer repo has one
|
||||
contract per environment (e.g. `.acdl/static-assets.dev.yaml`,
|
||||
`.acdl/static-assets.qa.yaml`, …). Each sets `environment:` to its own
|
||||
name and uses interpolation so env-specific values differ automatically:
|
||||
|
||||
```yaml
|
||||
# .acdl/static-assets.qa.yaml
|
||||
uses: acdl/pipelines/deploy.yaml@v1.9
|
||||
module: static-assets
|
||||
environment: qa
|
||||
inputs:
|
||||
bucket_name: acdl-${env.environment}-${contract.module}-${env.account_id}-${env.region}
|
||||
region: ${env.region}
|
||||
```
|
||||
|
||||
**Shape 2 — single contract + `environment` workflow input:** the
|
||||
reusable deploy workflow (`acdl/.github/workflows/deploy.yml@v1.9`)
|
||||
declares an `environment` input. When non-empty, it overrides the
|
||||
contract's `environment` field at load time (before interpolation), so
|
||||
the same contract can be promoted by passing a different environment:
|
||||
|
||||
```yaml
|
||||
# .github/workflows/deploy-qa.yml (caller workflow)
|
||||
on: workflow_dispatch:
|
||||
inputs:
|
||||
approve_qa:
|
||||
description: "Set to true to approve the QA promotion"
|
||||
type: boolean
|
||||
required: true
|
||||
jobs:
|
||||
deploy-qa:
|
||||
uses: acdl/.github/workflows/deploy.yml@v1.9
|
||||
with:
|
||||
environment: qa
|
||||
contract: .acdl/contract.yaml
|
||||
```
|
||||
|
||||
### One job per environment
|
||||
|
||||
A consumer repo's `.github/workflows/` directory has one caller workflow
|
||||
per environment:
|
||||
|
||||
| File | Environment | Gate |
|
||||
|------|-------------|------|
|
||||
| `deploy-dev.yml` | dev | autonomous (no gate, confidence ≥ 0.50) |
|
||||
| `deploy-qa.yml` | qa | QA HITL (`approve_qa` workflow_dispatch input; `github.actor` is the approver of record) |
|
||||
| `deploy-prod.yml` | prod | SRE HITL (`approve_prod`; separation-of-duties enforced) |
|
||||
| `deploy-dr.yml` | dr | SRE HITL (`approve_dr`) |
|
||||
|
||||
**Promotion = running the matching job.** No `environment:` field editing.
|
||||
The approver identity is recorded to the DynamoDB outbox
|
||||
(`approver_qa` / `approver_prod` / `approver_dr`) and the separation-of-
|
||||
duties check blocks a prod promotion when `approver_qa == approver_prod`
|
||||
(see `core/hitl_matrix_design.md`).
|
||||
|
||||
### Interpolation reference
|
||||
|
||||
| Token | Resolves to | Example |
|
||||
|-------|-------------|---------|
|
||||
| `${env.environment}` | the environment name (dev/qa/prod/dr) | `qa` |
|
||||
| `${env.region}` | the environment's AWS region | `us-east-1` |
|
||||
| `${env.account_id}` | the environment's AWS account id | `123456789012` |
|
||||
| `${env.state_backend.bucket}` | the environment's state bucket | `acdl-qa-state` |
|
||||
| `${env.network.vpc_cidr}` | the environment's VPC CIDR | `10.1.0.0/16` |
|
||||
| `${contract.module}` | the contract's module name | `static-assets` |
|
||||
| `${contract.environment}` | the contract's environment field | `qa` |
|
||||
| `${contract.inputs.<name>}` | a contract input value | (as declared) |
|
||||
|
||||
Unknown tokens raise `ValueError` (fail loud). Expansion is recursive
|
||||
(nested map/list values expand too).
|
||||
@@ -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 engine 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, DORA) wired into the pipeline.
|
||||
- **Additional engine 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 engine 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 engine 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.
|
||||
@@ -0,0 +1,260 @@
|
||||
# Presentations
|
||||
|
||||
Leadership-facing presentation decks for the ACDL platform.
|
||||
|
||||
## The 3-step slide creation process
|
||||
|
||||
Every presentation in this folder is produced by the same three-step process.
|
||||
**Never edit the Marp deck or the PPTX directly** — always start from the full
|
||||
markdown source of truth (Step 1), synthesize the Marp deck (Step 2), then
|
||||
export to PPTX (Step 3). This keeps a reviewable, plain-text source of truth
|
||||
for every deck.
|
||||
|
||||
```
|
||||
Step 1: full markdown Step 2: Marp deck Step 3: PPTX export
|
||||
(source of truth) ──► (lean, no notes) ──► (presentation-ready)
|
||||
*.md *-marp.md *.pptx
|
||||
+ speaker notes + embedded PNG diagrams + embedded images
|
||||
+ mermaid code blocks + Marp frontmatter
|
||||
```
|
||||
|
||||
### Step 1 — Full markdown (source of truth)
|
||||
|
||||
**File convention:** `<deck-name>.md` (e.g. `how-the-platform-works.md`).
|
||||
|
||||
Write the complete deck as a standard markdown file. This is the **source of
|
||||
truth** — it contains:
|
||||
|
||||
- Every slide as an `## Slide N — Title` H2 section.
|
||||
- Tight bullets with leadership-relevant content.
|
||||
- A `> **Speaker notes:**` block at the end of each slide with the nuance,
|
||||
the "who cares and why," and the honesty caveats.
|
||||
- Mermaid diagrams as ```` ```mermaid ```` fenced code blocks (these render
|
||||
on GitHub/Pages but not in Marp — Step 2 converts them to images).
|
||||
- An honest "shipped vs. planned" framing: every "available today" claim is
|
||||
grounded in shipped/verified work; every "planned" item is explicitly
|
||||
marked.
|
||||
|
||||
**Why this file is the source of truth:** it is reviewable in any markdown
|
||||
viewer, diffs cleanly in git, and carries the full reasoning (speaker notes)
|
||||
that a presenter needs. The Marp deck and PPTX are *derived artifacts* — if a
|
||||
fact is wrong, fix it here and re-run Steps 2 and 3.
|
||||
|
||||
### Step 2 — Marp deck synthesis
|
||||
|
||||
**File convention:** `<deck-name>-marp.md` (e.g. `how-the-platform-works-marp.md`).
|
||||
|
||||
Synthesize the full markdown into a lean Marp deck:
|
||||
|
||||
- **Marp frontmatter** at the top: `marp: true`, `theme: default`,
|
||||
`paginate: true`, `size: 16x9`, a header/footer, and an inline `style:`
|
||||
block for fonts, colors, tables, badges.
|
||||
- **No speaker notes.** The Marp deck is what the audience sees; the
|
||||
speaker notes live only in the Step 1 source of truth.
|
||||
- **Mermaid diagrams → PNG images.** Marp does not render mermaid fenced
|
||||
blocks natively. Extract each mermaid block from Step 1 into a `.mmd`
|
||||
source file under `assets/mmd/`, render it to PNG under `assets/png/`,
|
||||
and embed it with ``.
|
||||
- **`<!-- _class: title -->` + `<!-- _paginate: false -->`** on title and
|
||||
closing slides for the dark-background title style.
|
||||
- **Maturity badges** using inline spans:
|
||||
`<span class="badge testing">Testing</span>`
|
||||
`<span class="badge planned">Planned</span>`
|
||||
`<span class="badge agentic">Agentic</span>`
|
||||
- **Tighter prose** than Step 1 — strip the speaker-note nuance; keep the
|
||||
leadership-relevant selling points.
|
||||
|
||||
### Step 3 — Render to HTML and PPTX
|
||||
|
||||
Both formats are derived from the Marp deck. **HTML is committed to the repo**
|
||||
(viewable in any browser, self-contained with base64-embedded images). **PPTX
|
||||
is uploaded to the Gitea release** as a downloadable attachment (binary, not
|
||||
committed to git).
|
||||
|
||||
#### HTML export (committed to repo)
|
||||
|
||||
```bash
|
||||
CHROME_PATH=/root/.cache/ms-playwright/chromium-1217/chrome-linux64/chrome \
|
||||
npx --yes @marp-team/marp-cli@latest --allow-local-files \
|
||||
docs/presentations/<deck-name>-marp.md \
|
||||
-o docs/presentations/<deck-name>.html
|
||||
```
|
||||
|
||||
HTML export inlines images as base64 data URIs — no `--allow-local-files`
|
||||
needed for self-contained output, but it's required when the Marp deck
|
||||
references local PNG assets. The resulting HTML is a single self-contained
|
||||
file that renders the full deck with the S&P Global Energy theme.
|
||||
|
||||
**Re-render the HTML whenever the Marp source changes.** The HTML files are
|
||||
committed artifacts, not generated on-the-fly — they must be re-rendered and
|
||||
re-committed when the Marp deck is updated.
|
||||
|
||||
#### PPTX export (uploaded to Gitea release)
|
||||
|
||||
```bash
|
||||
CHROME_PATH=/root/.cache/ms-playwright/chromium-1217/chrome-linux64/chrome \
|
||||
npx --yes @marp-team/marp-cli@latest --allow-local-files \
|
||||
docs/presentations/<deck-name>-marp.md \
|
||||
-o <output-path>.pptx
|
||||
```
|
||||
|
||||
The `--allow-local-files` flag is **required** for PPTX export so the local
|
||||
PNG diagrams are embedded in the file. PPTX files are not committed to the
|
||||
repo (binary, no meaningful diffs) — they are uploaded to the Gitea release
|
||||
as downloadable attachments.
|
||||
|
||||
## Directory layout
|
||||
|
||||
```
|
||||
docs/presentations/
|
||||
├── README.md ← this file
|
||||
├── how-the-platform-works.md ← Step 1: full source of truth
|
||||
├── how-the-platform-works-marp.md ← Step 2: Marp deck
|
||||
├── how-the-platform-works.html ← Step 3: rendered HTML (committed)
|
||||
├── the-developer-experience.md ← Step 1: full source of truth
|
||||
├── the-developer-experience-marp.md ← Step 2: Marp deck
|
||||
├── the-developer-experience.html ← Step 3: rendered HTML (committed)
|
||||
└── assets/
|
||||
├── puppeteer-config.json ← no-sandbox config for mmdc
|
||||
├── mmd/ ← mermaid source files (Step 2 input)
|
||||
│ ├── platform-works-01-contract-driven.mmd
|
||||
│ ├── platform-works-02-end-to-end-flow.mmd
|
||||
│ ├── developer-experience-01-two-surfaces.mmd
|
||||
│ ├── developer-experience-02-what-dev-does.mmd
|
||||
│ └── developer-experience-03-no-cloning.mmd
|
||||
└── png/ ← rendered PNGs (embedded in Marp)
|
||||
├── platform-works-01-contract-driven.png
|
||||
├── platform-works-02-end-to-end-flow.png
|
||||
├── developer-experience-01-two-surfaces.png
|
||||
├── developer-experience-02-what-dev-does.png
|
||||
└── developer-experience-03-no-cloning.png
|
||||
```
|
||||
|
||||
## Conventions
|
||||
|
||||
### Maturity framing
|
||||
|
||||
Every capability claim in a deck is tagged with one of three badges:
|
||||
|
||||
| Badge | Meaning |
|
||||
|---|---|
|
||||
| `Testing` | Works internally, not yet released to consumers (0 adoption) |
|
||||
| `Planned` | On the roadmap, not yet implemented |
|
||||
| `Agentic` | Involves AI agents, autonomous decision-making, or the citizen developer flow |
|
||||
|
||||
This is non-negotiable for a leadership audience: never present a roadmap
|
||||
item as a current capability, and never bury a tested capability's
|
||||
availability. When in doubt, check `.ciagent/ROADMAP.md` and the milestone
|
||||
status in `.ciagent/PROJECT.md`.
|
||||
|
||||
### Audience
|
||||
|
||||
The audience for these decks is **Senior Leadership**: CTO, Head of Cloud,
|
||||
Head of Infrastructure, Head of DevOps. The framing rules:
|
||||
|
||||
- **No jargon.** Translate internal terms: "primitives/modules" not "L1/L2",
|
||||
"intent" not "IR", "human attestation" not "HITL", "pattern" not
|
||||
"composition."
|
||||
- **Selling points forward.** Each slide leads with the leadership-relevant
|
||||
outcome; the mechanism follows.
|
||||
- **Zero-trust, security, observability, auditability, DX, citizen
|
||||
developer** are the themes — not implementation details.
|
||||
|
||||
### Diagrams
|
||||
|
||||
Mermaid diagrams in the Step 1 source use the repo's existing `flowchart`
|
||||
style (renders on GitHub/Pages). For the Marp deck (Step 2):
|
||||
|
||||
1. Extract the mermaid block into `assets/mmd/<deck>-<slide>-<name>.mmd`.
|
||||
2. Use **horizontal layouts** (`flowchart LR`) or **subgraph row-wrapping**
|
||||
for wide diagrams so the PNG fits a 16:9 slide without shrinking to
|
||||
illegibility. A 9-node sequential `flowchart TD` renders as a tall thin
|
||||
strip — restructure it as 2-row subgraphs or `flowchart LR`.
|
||||
3. Render with a 2x scale factor and transparent background for crisp slides.
|
||||
4. Embed with `` (or `h:320` for tall images).
|
||||
|
||||
## Build commands
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js + npx (for `@marp-team/marp-cli` and `@mermaid-js/mermaid-cli`)
|
||||
- A Chrome/Chromium binary (Marp PPTX export requires it)
|
||||
|
||||
This environment has a working Chromium at:
|
||||
`/root/.cache/ms-playwright/chromium-1217/chrome-linux64/chrome`
|
||||
|
||||
### Render all mermaid diagrams to PNG
|
||||
|
||||
```bash
|
||||
cd docs/presentations/assets
|
||||
for f in mmd/*.mmd; do
|
||||
name=$(basename "$f" .mmd)
|
||||
PUPPETEER_EXECUTABLE_PATH=/root/.cache/ms-playwright/chromium-1217/chrome-linux64/chrome \
|
||||
npx --yes @mermaid-js/mermaid-cli@latest \
|
||||
-i "$f" -o "png/$name.png" \
|
||||
-p puppeteer-config.json -s 2 -b transparent
|
||||
done
|
||||
```
|
||||
|
||||
The `puppeteer-config.json` passes `--no-sandbox` to the headless browser
|
||||
(required when running as root in this environment).
|
||||
|
||||
### Export a Marp deck to HTML (committed to repo)
|
||||
|
||||
```bash
|
||||
CHROME_PATH=/root/.cache/ms-playwright/chromium-1217/chrome-linux64/chrome \
|
||||
npx --yes @marp-team/marp-cli@latest --allow-local-files \
|
||||
docs/presentations/<deck-name>-marp.md \
|
||||
-o docs/presentations/<deck-name>.html
|
||||
```
|
||||
|
||||
HTML export inlines images as base64 data URIs. The `--allow-local-files`
|
||||
flag is needed when the Marp deck references local PNG assets (like the
|
||||
diagram images in `assets/png/`). The resulting HTML is self-contained.
|
||||
|
||||
**The HTML files are committed artifacts** — re-render and re-commit whenever
|
||||
the Marp source changes.
|
||||
|
||||
### Export a Marp deck to PPTX (uploaded to Gitea release)
|
||||
|
||||
```bash
|
||||
CHROME_PATH=/root/.cache/ms-playwright/chromium-1217/chrome-linux64/chrome \
|
||||
npx --yes @marp-team/marp-cli@latest --allow-local-files \
|
||||
docs/presentations/<deck-name>-marp.md \
|
||||
-o <output-path>.pptx
|
||||
```
|
||||
|
||||
`--allow-local-files` is **required** for PPTX so local PNG diagrams are
|
||||
embedded in the file. PPTX files are not committed to git — upload them as
|
||||
attachments to the Gitea release.
|
||||
|
||||
## Adding a new presentation
|
||||
|
||||
1. **Write the full markdown** as `<deck-name>.md` following the
|
||||
`## Slide N — Title` + `> **Speaker notes:**` structure. This is the
|
||||
source of truth.
|
||||
2. **Extract any mermaid diagrams** into `assets/mmd/<deck-name>-<slide>-<name>.mmd`
|
||||
and render them to `assets/png/` (command above).
|
||||
3. **Synthesize the Marp deck** as `<deck-name>-marp.md` with frontmatter,
|
||||
no speaker notes, embedded PNGs, and maturity badges.
|
||||
4. **Render to HTML** with `--allow-local-files` and commit the HTML to
|
||||
`docs/presentations/<deck-name>.html`.
|
||||
5. **Render to PPTX** with `--allow-local-files` and upload to the Gitea
|
||||
release (do not commit PPTX to git).
|
||||
6. **Verify** the PPTX slide count and that media files are embedded:
|
||||
```bash
|
||||
python3 -c "
|
||||
import zipfile, re
|
||||
with zipfile.ZipFile('<output>.pptx') as z:
|
||||
slides = [n for n in z.namelist() if re.match(r'ppt/slides/slide\d+\.xml$', n)]
|
||||
media = [n for n in z.namelist() if n.startswith('ppt/media/')]
|
||||
print(f'{len(slides)} slides, {len(media)} media files')
|
||||
"
|
||||
```
|
||||
|
||||
## Current decks
|
||||
|
||||
| Deck | Source of truth (Step 1) | Marp deck (Step 2) | Rendered HTML (Step 3) | Audience |
|
||||
|---|---|---|---|---|
|
||||
| How the Platform Works | `how-the-platform-works.md` | `how-the-platform-works-marp.md` | `how-the-platform-works.html` | CTO, Head of Cloud, Head of Infra, Head of DevOps |
|
||||
| The Developer Experience | `the-developer-experience.md` | `the-developer-experience-marp.md` | `the-developer-experience.html` | CTO, Head of Cloud, Head of Infra, Head of DevOps |
|
||||
@@ -0,0 +1,7 @@
|
||||
flowchart LR
|
||||
A["Technical developer"] --> C["Contract YAML"]
|
||||
B["Citizen developer<br/>(non-technical)"] --> D["Declares intent<br/>in natural language"]
|
||||
D --> E["Agent produces<br/>the contract"]
|
||||
C --> F["Same platform:<br/>resolve → check → plan →<br/>policy → confidence → apply"]
|
||||
E --> F
|
||||
F --> G["Same safety guarantees,<br/>same audit trail"]
|
||||
@@ -0,0 +1,5 @@
|
||||
flowchart LR
|
||||
A["1. App code<br/>(top level of the repo)"] --> D["Push to main"]
|
||||
B["2. Contract<br/>(.acdl/contract.yaml)"] --> D
|
||||
C["3. CI definition<br/>(.github/workflows/deploy.yml<br/>— one 'uses:' line)"] --> D
|
||||
D --> E["Platform does the rest"]
|
||||
@@ -0,0 +1,6 @@
|
||||
flowchart LR
|
||||
A["Consumer repo<br/>app + contract + 'uses:'"] -->|triggers on push to main| B["Platform runner"]
|
||||
B -->|checks out the consumer repo| A
|
||||
B -->|checks out the ACDL platform repo<br/>into the workspace| C["Platform code<br/>(modules, adapters, schemas)"]
|
||||
C --> B
|
||||
B -->|runs the pipeline against<br/>the consumer's contract| D["Consumer's resources in AWS"]
|
||||
@@ -0,0 +1,3 @@
|
||||
flowchart LR
|
||||
A["Consumer<br/>writes a contract"] --> B["Platform resolves,<br/>compiles, checks,<br/>deploys, records"]
|
||||
B --> C["Resources running in AWS<br/>+ tamper-evident evidence"]
|
||||
@@ -0,0 +1,10 @@
|
||||
flowchart TD
|
||||
subgraph R1 [" "]
|
||||
direction LR
|
||||
A["Consumer<br/>contract"] --> B["Validate<br/>contract"] --> C["Resolve to<br/>target stack"] --> D["Security<br/>checks"] --> E["Infrastructure<br/>plan"]
|
||||
end
|
||||
subgraph R2 [" "]
|
||||
direction LR
|
||||
F["Policy<br/>checks"] --> G["Confidence<br/>signal"] --> H["Evidence<br/>event"] --> I["Infrastructure<br/>apply"]
|
||||
end
|
||||
E --> F
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 59 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 67 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
@@ -0,0 +1 @@
|
||||
{ "args": ["--no-sandbox", "--disable-setuid-sandbox"] }
|
||||
@@ -0,0 +1,204 @@
|
||||
---
|
||||
marp: true
|
||||
theme: default
|
||||
paginate: true
|
||||
size: 16x9
|
||||
header: "How The Platform Works"
|
||||
footer: "Internal"
|
||||
style: |
|
||||
section {
|
||||
font-family: "Akkurat Pro", "Helvetica Neue", "Arial", sans-serif;
|
||||
font-size: 22px;
|
||||
color: #1B1B1B;
|
||||
}
|
||||
h1 { color: #D6002A; font-size: 34px; margin-bottom: 0.3em; }
|
||||
h2 { color: #D6002A; font-size: 26px; margin-bottom: 0.2em; }
|
||||
section.title { background: #1B1B1B; color: #fff; border-top: 8px solid #D6002A; }
|
||||
section.title h1 { color: #fff; }
|
||||
table { font-size: 18px; width: 100%; }
|
||||
th { background: #F0F0F0; }
|
||||
blockquote { border-left: 4px solid #D6002A; color: #2E2E2E; font-size: 20px; }
|
||||
img { display: block; margin: 0 auto; max-height: 320px; }
|
||||
.badge {
|
||||
display: inline-block; padding: 2px 8px; border-radius: 4px;
|
||||
font-size: 14px; font-weight: 600;
|
||||
}
|
||||
.testing { background: #DBEAFE; color: #1E3A5F; }
|
||||
.planned { background: #fef3c7; color: #78350f; }
|
||||
.agentic { background: #EDE9FE; color: #4C1D95; }
|
||||
---
|
||||
|
||||
<!-- _class: title -->
|
||||
<!-- _paginate: false -->
|
||||
|
||||
# How The Platform Works
|
||||
|
||||
### Agentic Cloud Delivery Platform
|
||||
|
||||
<style>
|
||||
section.title h1 { font-size: 44px; margin-bottom: 0.1em; }
|
||||
section.title h3 { color: #F0F0F0; font-weight: 400; font-size: 22px; margin-top: 0; }
|
||||
</style>
|
||||
|
||||
---
|
||||
|
||||
# The Problem & The North Star
|
||||
|
||||
Four frictions slow every team:
|
||||
|
||||
- **Cognitive load** — authoring infrastructure correctly; the long tail of services inconsistent in security and observability
|
||||
- **Operational work** — promoting a change from "merged" to "running in production." Manual work that **scales with the system, not the change**
|
||||
- **Red tape** — tickets, approvals, and handoffs that scale with the organization. A merged change waits in a queue
|
||||
- **Scalability without increasing headcount** — throughput scales without linearly scaling platform engineers
|
||||
|
||||
> Consumers **declare intent**; the platform delivers **safe production deployment** — automatically, safely, with a complete audit trail.
|
||||
|
||||
- A merged change progresses **without a platform engineer joining a thread or approving a ticket**
|
||||
- A **non-technical consumer** ships by declaring intent — no workflow, no config file, no infrastructure module
|
||||
- Every production change is **traceable to a human attestation and an immutable evidence stream**
|
||||
- **Not a general-purpose AI** — autonomy is narrow, scoped to delivery, bounded by strict policy
|
||||
- **Not a permissive delivery highway** — no escape hatches to bypass the confidence framework
|
||||
|
||||
---
|
||||
|
||||
# The Contract-Driven Model
|
||||
|
||||
A single YAML contract is all a consumer writes — **module, environment, inputs**. The platform owns everything else.
|
||||
|
||||

|
||||
|
||||
- **Which module** — a catalog of pre-built, security-reviewed building blocks
|
||||
- **Which environment** — the platform raises the safety bar automatically as sensitivity rises
|
||||
- **Which inputs** — the handful of values that vary per deployment
|
||||
- The consumer provides **no AWS account, no VPC, no state backend, no runner key** — the platform owns the blast radius
|
||||
|
||||
---
|
||||
|
||||
# The End-to-End Flow
|
||||
|
||||
Every deployment runs the same stages, in the same order, with the same checks — no team-specific pipelines, no tribal runbooks.
|
||||
|
||||

|
||||
|
||||
- **Security and policy checks run *before* any infrastructure is created**
|
||||
- **Every stage produces a record** that feeds the confidence signal and the evidence stream — there is no "unchecked" path
|
||||
|
||||
---
|
||||
|
||||
# Zero-Trust by Default
|
||||
|
||||
Consumer repositories hold **no long-lived cloud credentials.** Ever.
|
||||
|
||||
- **Authentication — OIDC federation.** Each job mints a short-lived token; no credential is stored in the consumer repo or in a runner secret. <span class="badge testing">Testing (GitHub Actions)</span> <span class="badge planned">Planned: all runners</span>
|
||||
- **Authorization — attribute-based (ABAC), not role-based.** Two attribute classes scope every action:
|
||||
- **Repository identity** — the role's trust policy binds to the exact consumer repo + branch
|
||||
- **Resource tags** — every resource is tagged `acdl:owner` + `acdl:contract`; the session policy grants access **only to matching tags**
|
||||
|
||||
**The effect:** a consumer can only touch the resources it created. Blast radius is contained. One consumer can never affect another.
|
||||
|
||||
---
|
||||
|
||||
# Safety is Computed, Not Assumed
|
||||
|
||||
Every delivery action produces a **measurable, explainable confidence signal** — the platform's certified answer to *"is this safe to proceed?"* <span class="badge agentic">Agentic</span>
|
||||
|
||||
- **Six weighted inputs:** policy conformance, validation, freshness, source provenance, history, NFRs
|
||||
- **Per-environment thresholds** that rise with sensitivity:
|
||||
|
||||
| Environment | Threshold | Attester |
|
||||
|---|---|---|
|
||||
| dev | ≥ 0.50 | No one — autonomous |
|
||||
| qa | ≥ 0.75 | QA |
|
||||
| prod | ≥ 0.90 | SRE |
|
||||
| dr | ≥ 0.95 | SRE + DR drill |
|
||||
|
||||
- **A single critical finding hard-blocks the deployment** — critical findings are not averaged away
|
||||
- **When the platform halts, it gives a measured reason** — never an opaque debugging exercise
|
||||
|
||||
---
|
||||
|
||||
# Security by Construction
|
||||
|
||||
Security defaults that **do not require a team to opt in.** Checks run on **every** deployment, normalized to a single schema. <span class="badge testing">Testing</span>
|
||||
|
||||
- **Policy checks** (Checkov, Wiz, Kyverno) — secrets in plaintext, public ingress, IAM wildcards, **required tagging standards** — all run *before* infra is created
|
||||
- **Encryption on every resource** — at-rest encryption on by default; per-stack customer-managed keys with 90-day rotation, **no shared keys across stacks**
|
||||
- **Deletion protection on by default** — `prevent_destroy` on unless explicitly disabled via a documented flag
|
||||
- **Safe decommission** — a 2-step pipeline with **two SRE attestation gates** and a **change-request validated against the CMDB**
|
||||
|
||||
---
|
||||
|
||||
# Accountability & Audit
|
||||
|
||||
Autonomy and accountability are **not in tension** — they apply at different environments.
|
||||
|
||||
- **Dev is fully autonomous.** The confidence signal (≥ 0.50) is the only gate. Queue-based handoffs are eliminated. <span class="badge agentic">Agentic</span>
|
||||
- **qa, prod, dr require deliberate human attestation** — policy-mandated acts of accountability, not rubber stamps
|
||||
- **Separation of duties is enforced** — the QA approver **cannot** be the prod approver. The platform **blocks on a match.** <span class="badge testing">Design tested</span> <span class="badge planned">Wiring: planned</span>
|
||||
- **Every deployment writes a hash-chained evidence event** — tampering breaks the chain. **RPO = 0** — the evidence write is synchronous <span class="badge testing">Testing</span>
|
||||
- **Every production change is traceable to a human attestation** — the only durable record outside the VCS's audit log
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: title -->
|
||||
<!-- _paginate: false -->
|
||||
|
||||
# Testing vs. Planned
|
||||
|
||||
<style>
|
||||
section { font-size: 16px; }
|
||||
td { font-size: 15px; vertical-align: top; }
|
||||
ul { margin: 0; padding-left: 1.2em; }
|
||||
li { margin-bottom: 2px; }
|
||||
</style>
|
||||
|
||||
<table style="width: 100%; border: none;">
|
||||
<tr>
|
||||
<td style="width: 52%; border: none; padding-right: 12px;">
|
||||
|
||||
**Testing** (works internally, not yet released to consumers)
|
||||
|
||||
- Contract-driven deploys with a versioned reusable workflow
|
||||
- Module catalog (primitives + modules) with validated examples
|
||||
- Zero-trust OIDC + ABAC on GitHub Actions runners
|
||||
- Security + policy checks before infra creation (Checkov; Wiz + Kyverno ready)
|
||||
- Confidence signal (6 inputs, per-env thresholds) gating promotion <span class="badge agentic">Agentic</span>
|
||||
- Hash-chained, tamper-evident evidence outbox (RPO = 0)
|
||||
- Encryption by default + per-stack customer-managed keys
|
||||
- Deletion protection by default + safe decommission with SRE gates
|
||||
- Uptime monitoring deployed automatically with every stack
|
||||
- Platform-managed environments + friendly onboarding
|
||||
- Engine-agnostic core (1 adapter: Terraform) + VCS-agnostic ingestion
|
||||
|
||||
</td>
|
||||
<td style="width: 48%; border: none; padding-left: 12px;">
|
||||
|
||||
**Planned** (on the roadmap)
|
||||
|
||||
- Real OIDC federation on all platform runners
|
||||
- HITL wiring for qa / prod / dr environments
|
||||
- Full regulatory ledger: S3 Object Lock + JWS signatures + daily checkpoints
|
||||
- Compliance milestone: GDPR, SOX, SOC2, DORA extension points
|
||||
- Environment self-service provisioning
|
||||
- Dynamic module creation from a contract (agentic citizen-developer flow) <span class="badge agentic">Agentic</span>
|
||||
- Pattern recognition compounds value over time <span class="badge agentic">Agentic</span>
|
||||
- Additional engine adapters (OpenTofu, Pulumi, Kubernetes CRDs)
|
||||
- Deeper observability bootstrap (dashboards, runbooks, on-call)
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: title -->
|
||||
<!-- _paginate: false -->
|
||||
|
||||
# The Vision Realized
|
||||
|
||||
- **Velocity without sacrificing safety.** Speed is in the ergonomics (a simple contract, a one-line `uses:`); safety is in the gates the consumer cannot bypass.
|
||||
- **Security, observability, and compliance as platform defaults** — not per-team effort, not post-hoc remediation. Encryption, deletion protection, uptime monitoring, policy checks, and evidence are on by construction.
|
||||
- **Auditability as a byproduct, not a project.** Every production change is traceable to a human attestation and a tamper-evident evidence event.
|
||||
- **Blast radius contained by design.** Zero-trust OIDC + ABAC means a consumer can only touch its own tagged resources.
|
||||
- **Infrastructure as a utility, not a craft.** The platform abstracts compute, networking, and state. Teams consume infrastructure, they don't maintain it.
|
||||
- **A path to the citizen developer.** The same safety envelope that serves a senior engineer will serve a non-technical consumer — expanding who can ship safely without lowering the bar. <span class="badge agentic">Agentic</span>
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,268 @@
|
||||
# How The Platform Works
|
||||
|
||||
> **Subtitle:** Agentic Cloud Delivery Platform
|
||||
> **Audience:** Senior Leadership, CTO, Head of Cloud, Head of Infrastructure, Head of DevOps
|
||||
> **Length:** ~15 minutes · 14 slides
|
||||
> **Purpose:** Sell the platform's value to tech leadership — zero-trust, security, observability, auditability, and the shift from "operators guess" to "the platform computes safety."
|
||||
> **Maturity framing:** "Testing" = shipped and verified. "Planned" = on the roadmap, not yet shipped.
|
||||
|
||||
---
|
||||
|
||||
## Slide 1 — The Problem We Solve
|
||||
|
||||
Software delivery scales with the **coordination surface around it**, not the engineering inside it. Most teams can write code; far fewer get the infrastructure right.
|
||||
|
||||
Two frictions slow every team down:
|
||||
|
||||
- **Cognitive load** — authoring the infrastructure that runs a service correctly. The long tail of well-meaning services that are difficult to deploy, inconsistent in security and observability posture.
|
||||
- **Operational work** — promoting a change from "merged" to "running in production with policy, observability, and security enforced." Manual work that **scales with the system, not with the change.**
|
||||
- **Red tape** — every deployment requires tickets, approvals, and manual handoffs that scale with the organization, not with the change. A merged change waits in a queue for someone to press a button.
|
||||
- **Scalability without increasing headcount** — the platform allows delivery throughput to scale without linearly scaling platform engineers. Today, every new team adds load to the same ticket queue.
|
||||
|
||||
> **Speaker notes:** Open with the cost of the status quo. Every team that stands up its own pipeline, its own Terraform, its own review checklist is paying a tax that doesn't differentiate the business. The platform absorbs all four frictions — that is the value proposition in one sentence.
|
||||
|
||||
---
|
||||
|
||||
## Slide 2 — The North Star
|
||||
|
||||
> Consumers **declare intent**; the platform delivers **safe production deployment** through an agentic stack — automatically, safely, and with a complete audit trail.
|
||||
|
||||
What success looks like:
|
||||
|
||||
- A merged change progresses through lower environments **end-to-end without a platform engineer joining a thread, approving a ticket, or manually triggering a stage.**
|
||||
- A **non-technical consumer** ships a production deployment by declaring intent — without authoring a workflow, a configuration file, or an infrastructure module.
|
||||
- Every production change is **traceable to a human attestation and an immutable evidence stream.**
|
||||
|
||||
> **Speaker notes:** This is the litmus test. If a platform engineer still has to touch a ticket for a dev→qa promotion, we haven't delivered the vision. The two consumer surfaces (technical developer + citizen developer) are covered in the companion deck. Here we focus on *how* the platform makes the North Star real.
|
||||
|
||||
---
|
||||
|
||||
## Slide 3 — What This Platform Is — and Isn't
|
||||
|
||||
**What it is:**
|
||||
|
||||
- **A sovereign delivery boundary.** The platform governs infrastructure and delivery. It does not penetrate upstream product or software development lifecycles. Integration happens through validated, published contracts.
|
||||
- **Infrastructure consumed, not maintained.** Compute is abstract, containerized, or serverless. The platform does not manage node, OS, or bare-metal lifecycles. Infrastructure is a utility, not a craft.
|
||||
|
||||
**What it isn't:**
|
||||
|
||||
- **Not an upstream development platform.** No product backlogs, sprint ceremonies, or IDE workflows.
|
||||
- **Not a general-purpose AI.** Autonomy is narrow, scoped to delivery and infrastructure reconciliation, bounded by strict policy envelopes.
|
||||
- **Not a legacy infrastructure bridge.** No VMs, bare metal, or OS lifecycles.
|
||||
- **Not a permissive delivery highway.** No escape hatches to bypass the confidence framework or human attestation requirements.
|
||||
|
||||
> **Speaker notes:** This slide gives leadership the framing they need. The platform is deliberately scoped — it is not trying to be everything. The sovereign boundary means the platform team owns delivery and infrastructure, not the upstream development process. The anti-goals are as important as the goals: they tell leadership what not to expect.
|
||||
|
||||
---
|
||||
|
||||
## Slide 4 — The Contract-Driven Model
|
||||
|
||||
One small YAML file is all a consumer writes. The platform owns everything else.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["Consumer<br/>writes a contract"] --> B["Platform resolves,<br/>compiles, checks,<br/>deploys, records"]
|
||||
B --> C["Resources running in AWS<br/>+ tamper-evident evidence"]
|
||||
```
|
||||
|
||||
The contract names three things:
|
||||
|
||||
- **Which module** — a catalog of pre-built, security-reviewed building blocks (a static site, a microservice, a database, and more).
|
||||
- **Which environment** — `dev`, `qa`, `prod`, or `dr`. The platform raises the safety bar automatically as the environment gets more sensitive.
|
||||
- **Which inputs** — the handful of values that vary per deployment (a bucket name, a container image, a port).
|
||||
|
||||
The consumer does **not** write infrastructure modules, workflow logic, or adapter code. They declare intent; the platform reconciles, provisions, and progresses.
|
||||
|
||||
> **Speaker notes:** Emphasize the asymmetry. The consumer's surface is intentionally tiny — a contract that fits on one screen. The platform's surface is large and opinionated. That asymmetry is what makes "declare intent, not execute operations" concrete.
|
||||
|
||||
---
|
||||
|
||||
## Slide 4 — The End-to-End Flow
|
||||
|
||||
Every deployment runs the same stages, in the same order, with the same checks — no team-specific pipelines, no tribal runbooks.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["Consumer contract<br/>(module + environment + inputs)"] --> B["Validate contract<br/>against the schema"]
|
||||
B --> C["Resolve to a target stack<br/>(expand the module's pattern)"]
|
||||
C --> D["Security checks<br/>(before any infra is created)"]
|
||||
D --> E["Infrastructure plan<br/>(platform compiles the stack)"]
|
||||
E --> F["Policy checks<br/>(normalized results)"]
|
||||
F --> G["Confidence signal<br/>(6 inputs → score + band)"]
|
||||
G --> H["Evidence event<br/>(hash-chained, tamper-evident)"]
|
||||
H --> I["Infrastructure apply<br/>(dev only — higher envs hold for attestation)"]
|
||||
```
|
||||
|
||||
Two properties matter to leadership:
|
||||
|
||||
- **Security and policy checks run *before* any infrastructure is created** — not after the fact, not as a post-deployment audit.
|
||||
- **Every stage produces a record** that feeds the confidence signal and the evidence stream. There is no "unchecked" path.
|
||||
|
||||
> **Speaker notes:** Walk left to right once. Don't dwell on internals — the point is that the flow is fixed, opinionated, and identical for every consumer. The two leadership-relevant beats are (1) checks before creation, (2) every stage is evidenced. The confidence signal (Slide 6) is where the "safety is computed" story lands.
|
||||
|
||||
---
|
||||
|
||||
## Slide 5 — Zero-Trust by Default
|
||||
|
||||
Consumer repositories hold **no long-lived cloud credentials.** Ever.
|
||||
|
||||
- **Authentication** is **OIDC federation** between the platform runners and the cloud provider. Each job mints a short-lived token; no credential is stored in the consumer repo or in a runner secret. *(Testing on GitHub Actions runners; planned for all platform runners.)*
|
||||
- **Authorization** is **attribute-based (ABAC), not role-based.** Two attribute classes scope every action:
|
||||
- **Repository identity** — the role's trust policy binds to the exact consumer repo + branch that invoked the workflow.
|
||||
- **Resource-creation attributes** — every resource is tagged with `acdl:owner=<consumer-repo>` and `acdl:contract=<contract-id>`. The session policy grants view/update/delete **only on resources whose tags match the calling repo.**
|
||||
|
||||
**The effect:** a consumer's pipeline can only touch the resources it created. Blast radius is contained to that consumer's own stack instances. One consumer can never touch another's resources, and the consumer cannot escape its own scope.
|
||||
|
||||
> **Speaker notes:** This is the slide for the Head of Cloud/Security. The key phrase is "blast radius contained to the consumer's own stack." Contrast with the common failure mode of shared CI roles that can touch any account resource. The static-key override exists for edge cases but is rotated daily on platform runners; it is never the default.
|
||||
|
||||
---
|
||||
|
||||
## Slide 6 — Safety is Computed, Not Assumed
|
||||
|
||||
Every delivery action produces a **measurable, explainable confidence signal** — the platform's certified answer to "is this safe to proceed?"
|
||||
|
||||
- **Six weighted inputs:** policy conformance, validation, freshness, source provenance, history, and non-functional requirements (NFRs).
|
||||
- **Per-environment thresholds** that rise with sensitivity:
|
||||
|
||||
| Environment | Threshold | Who must attest |
|
||||
|---|---|---|
|
||||
| dev | ≥ 0.50 | No one — fully autonomous |
|
||||
| qa | ≥ 0.75 | QA |
|
||||
| prod | ≥ 0.90 | SRE |
|
||||
| dr | ≥ 0.95 | SRE + a disaster-recovery drill reference |
|
||||
|
||||
- **A single critical policy finding hard-blocks the deployment**, regardless of every other input. Critical findings are not averaged away.
|
||||
- **When the platform halts, it gives a measured reason** — a policy violation, an insufficient signal, a missing attestation — never an opaque, manual-debugging exercise.
|
||||
|
||||
> **Speaker notes:** This is the bet that separates this platform from "yet another CI/CD tool." Reliance on operator instinct or tenure is not a substitute. The signal is auditable; the thresholds are tunable by Infra & Ops + SRE jointly, and any override is itself a confidence-event in the audit stream. Leadership cares about this because it makes promotion decisions *reviewable*.
|
||||
|
||||
---
|
||||
|
||||
## Slide 7 — Policy & Security Enforcement
|
||||
|
||||
Checks run on **every** deployment, normalized to a single schema regardless of which engine produced them.
|
||||
|
||||
- **Infrastructure-as-code policy** (Checkov) — secrets in plaintext, public ingress, IAM wildcards, KMS key references, **required tagging standards** (`acdl:owner`, `acdl:contract`, `acdl:environment`, `acdl:cost-center`).
|
||||
- **Cloud security posture** (Wiz adapter) — translates cloud security findings into the same normalized record. *(Adapter available today; activates when a Wiz tenant is configured.)*
|
||||
- **Kubernetes-native policy** (Kyverno adapter) — ready for the GitOps reconciler roadmap item. *(Adapter available today; inactive for Terraform-only stacks.)*
|
||||
|
||||
Every check produces a record with **severity, rule ID, pass/fail status, and human-readable message** — consumed uniformly by the confidence signal. No engine-specific escapes.
|
||||
|
||||
> **Speaker notes:** The selling point is *normalization*. We can add a new security tool without changing the confidence model or the evidence stream. For the Head of Security: tagging standards are enforced, not advisory — a missing `acdl:owner` tag fails the check, not a warning.
|
||||
|
||||
---
|
||||
|
||||
## Slide 8 — Secure by Default
|
||||
|
||||
Security defaults that **do not require a team to opt in.**
|
||||
|
||||
- **Encryption on every resource** — at-rest encryption is on by default for every primitive (S3, RDS, ECR, ECS, and more). *(Testing.)*
|
||||
- **Per-stack customer-managed keys (CMKs)** — one key per deployment, 90-day rotation at creation, **no shared keys across stacks.** *(Testing.)*
|
||||
- **Managed-key fallback with a loud warning** — standalone primitives fall back to cloud-managed keys only when no CMK is provided, and the platform warns explicitly. Silent use of cloud-managed keys is a security gap we refuse to hide. *(Testing.)*
|
||||
- **Deletion protection on by default** — every resource has `prevent_destroy` on unless a consumer explicitly disables it via a documented feature flag. *(Testing.)*
|
||||
- **Safe decommission** — a 2-step pipeline (disable protection → zero counts → destroy) with **two SRE human-attestation gates** and a **change-request validated against the platform CMDB** before any destructive action. *(Testing.)* Encryption keys enter a grace window (default 30 days) so encrypted data remains recoverable during decommission.
|
||||
|
||||
> **Speaker notes:** The phrase to land is "secure by default, not secure by effort." The decommission flow is the counter-argument to "deletion protection makes cleanup impossible" — it's a deliberate, gated, two-approval path, not a lock with no key.
|
||||
|
||||
---
|
||||
|
||||
## Slide 9 — Immutable Audit & Evidence
|
||||
|
||||
Version control is a **coordination tool, not an evidentiary fortress.** True compliance requires an immutable, externally-stored ledger.
|
||||
|
||||
- **Every deployment writes a hash-chained evidence event** — each event links to the previous via a cryptographic hash. Tampering breaks the chain. *(Testing. the DynamoDB outbox.)*
|
||||
- **Tiered storage design:** cold, tamper-proof source of truth (S3 Object Lock, compliance mode, 7-year retention) + a hot query index for fast lookup. *(Outbox tested; S3 Object Lock + JWS detached signatures are planned regulatory-ledger build-out.)*
|
||||
- **RPO = 0** — the evidence write is synchronous; a deployment is not acknowledged until the evidence event is durably recorded.
|
||||
- **Every production change is traceable to a human attestation** — the QA and prod approver identities are the only durable record outside the VCS's audit log, stored in the outbox keyed by contract.
|
||||
|
||||
> **Speaker notes:** This is the slide for the Head of Infrastructure and anyone who has been through an audit. "The audit trail is a byproduct of deployment, not a project." Note honestly that the full regulatory ledger (S3 Object Lock, JWS signatures, daily checkpoints) is planned; what ships today is the outbox + hash chain that makes every event tamper-evident and queryable.
|
||||
|
||||
---
|
||||
|
||||
## Slide 10 — Human-in-the-Loop Where It Matters
|
||||
|
||||
Autonomy and accountability are **not in tension** — they are applied at different environments.
|
||||
|
||||
- **Dev is fully autonomous.** No human gate. The confidence signal (≥ 0.50) is the only gate. Queue-based handoffs are eliminated from lower environments.
|
||||
- **qa, prod, and dr require deliberate human attestation** — not rubber stamps, but policy-mandated acts of accountability via protected deployment approvals.
|
||||
- **Separation of duties is enforced** *(design shipped; wiring for qa/prod/dr is planned)* — the person who approved the qa promotion **cannot** be the person who approves the prod promotion. The platform reads both identities from the outbox and **blocks** on a match, emitting a `SEPARATION_OF_DUTIES_VIOLATION` and routing a halt artifact to SRE on-call.
|
||||
- **Timeout discipline** — 1 business day = warn + escalate; 2 business days = auto-freeze + re-submit. Rejection extends the audit chain; it does not tear it up.
|
||||
|
||||
> **Speaker notes:** The "Lower environments autonomous, higher environments attested" tenet is the resolution to the classic "move fast vs. be safe" false dichotomy. Be honest: the *mechanism* (CODEOWNERS routing, identity-distinctness check, the 8-concern attestation matrix) is designed and the dev path is wired; the qa/prod/dr wiring is on the roadmap.
|
||||
|
||||
---
|
||||
|
||||
## Slide 11 — Observability Built In
|
||||
|
||||
Monitoring is **a platform default, not a per-team project.**
|
||||
|
||||
- **Uptime monitoring deployed automatically with every stack** — a dedicated monitoring instance (Uptime-kuma on ECS Fargate) is provisioned after any module deploy, in a separate state, with a feature flag to disable. *(Testing.)*
|
||||
- **Monitored endpoints passed from the deployment's own outputs** — the platform constructs a synthetic monitoring contract from what was just deployed. No manual endpoint registration.
|
||||
- **Alert channels:** Microsoft Teams webhook, email, SMS, and GitHub issues. *(Testing.)*
|
||||
- **The uptime URL is published to the developer** via a PR comment — they don't hunt for it.
|
||||
- **Roadmap:** deeper observability bootstrap (dashboards, runbooks, on-call bindings) as first-class contract fields for prod/dr.
|
||||
|
||||
> **Speaker notes:** The Head of DevOps cares about this. The framing: "you don't deploy a service and *then* remember to set up monitoring — the platform does it as part of the deploy." The feature flag means teams with existing monitoring (e.g. Datadog) can opt out cleanly.
|
||||
|
||||
---
|
||||
|
||||
## Slide 12 — Platform-Managed Environments
|
||||
|
||||
A consumer provides **no AWS account, no VPC, no subnet, no state backend, no runner key.** The platform owns the blast radius.
|
||||
|
||||
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 (S3 + DynamoDB for infrastructure state + locking).
|
||||
- An IAM role surfaced to the consumer via ABAC, scoped to the consumer's repository identity and resource tags.
|
||||
|
||||
The consumer selects an environment **by name** in their contract (`environment: dev`). The platform resolves the name to the underlying account/network/state/role at run time. **The consumer never sees the raw credentials.**
|
||||
|
||||
**Friendly onboarding:** the first run detects no environment and emits a guided prompt (not an opaque failure) telling the consumer what the platform will provision and how to request it. *(Testing.)* **Self-service environment provisioning is planned.**
|
||||
|
||||
> **Speaker notes:** For the Head of Cloud: this is the governance story. The platform team owns the accounts, the network design, the state hygiene. Consumers can't drift into misconfigured state backends or over-permissioned roles because they never touch them. The onboarding prompt matters — first impressions of a platform are made when it fails for the first time.
|
||||
|
||||
---
|
||||
|
||||
## Slide 13 — Portability & Future-Proofing
|
||||
|
||||
The platform is **opinionated, but not painted into a corner.**
|
||||
|
||||
- **Angine-agnostic core.** The contract, the resolved stack, the policy results, the confidence signal, and the evidence stream are all defined *without reference to any specific infrastructure tool.* Today there is one adapter (Terraform). *(OpenTofu, Pulumi, Kubernetes CRDs are future adapters — no architectural change required.)*
|
||||
- **Forge-agnostic contract ingestion.** The platform Lambda reads a configurable API base for GitHub or Gitea. *(Testing.)*
|
||||
- **Portable contracts.** The contract schema, the confidence signal, and the audit stream are engine- and VCS-agnostic. A second VCS (e.g. GitLab) needs a VCS adapter + a workflow-template translator — **no change to the modules, the contract standard, the confidence model, or the audit stream.**
|
||||
- **Pattern recognition compounds value over time.** As the platform observes recurring contract patterns, it can synthesize and offer reusable modules. *(Future capability, not a current commitment — but the design allows it.)*
|
||||
|
||||
> **Speaker notes:** This is the "we won't have to rewrite this in two years" slide. The bet is that the engine (Terraform today) will change, but the contract + confidence + audit model won't. Leadership should hear: the investment is in the abstraction, not the tool.
|
||||
|
||||
---
|
||||
|
||||
## Slide 14 — Roadmap: Honest Testing vs. Planned
|
||||
|
||||
**Testing:**
|
||||
|
||||
- Contract-driven deploys with a versioned reusable workflow.
|
||||
- Module catalog (primitives + modules) with validated examples.
|
||||
- Zero-trust OIDC + ABAC on GitHub Actions runners.
|
||||
- Security + policy checks before infra creation (Checkov; Wiz + Kyverno adapters ready).
|
||||
- Confidence signal (6 inputs, per-env thresholds) gating promotion.
|
||||
- Hash-chained, tamper-evident evidence outbox (RPO = 0).
|
||||
- Encryption by default + per-stack customer-managed keys.
|
||||
- Deletion protection by default + safe decommission with SRE gates + CMDB validation.
|
||||
- Uptime monitoring deployed automatically with every stack.
|
||||
- Platform-managed environments + friendly onboarding.
|
||||
- Local reproducibility (`run_ci.sh` mirrors the CI pipeline).
|
||||
- Forge-agnostic contract ingestion (GitHub + Gitea).
|
||||
|
||||
**Planned (on the roadmap, not yet shipped):**
|
||||
|
||||
- Real OIDC federation on all platform runners (Gitea Actions OIDC pending an upstream merge).
|
||||
- HITL wiring for qa / prod / dr environments (design shipped; wiring is next).
|
||||
- Full regulatory ledger: S3 Object Lock (7-yr compliance mode) + JWS detached signatures + daily checkpoints.
|
||||
- Compliance milestone: per-module extension points for GDPR, SOX, SOC2, DORA.
|
||||
- Environment self-service (a consumer-facing flow to request and provision a new environment).
|
||||
- Dynamic module creation from a contract (the agentic "citizen developer" composition mechanism).
|
||||
- Additional engine adapters (OpenTofu, Pulumi, Kubernetes CRDs).
|
||||
|
||||
> **Speaker notes:** Close on honesty. The platform delivers real, verifiable value today — and the roadmap is concrete, not aspirational hand-waving. Invite questions on any "planned" item; each has a defined milestone and a clear reason it isn't shipped yet (usually an upstream dependency, not an engineering gap).
|
||||
@@ -0,0 +1,226 @@
|
||||
---
|
||||
marp: true
|
||||
theme: default
|
||||
paginate: true
|
||||
size: 16x9
|
||||
header: "The Developer Experience"
|
||||
footer: "Internal"
|
||||
style: |
|
||||
section {
|
||||
font-family: "Akkurat Pro", "Helvetica Neue", "Arial", sans-serif;
|
||||
font-size: 22px;
|
||||
color: #1B1B1B;
|
||||
}
|
||||
h1 { color: #D6002A; font-size: 34px; margin-bottom: 0.3em; }
|
||||
h2 { color: #D6002A; font-size: 26px; margin-bottom: 0.2em; }
|
||||
section.title { background: #1B1B1B; color: #fff; border-top: 8px solid #D6002A; }
|
||||
section.title h1 { color: #fff; }
|
||||
table { font-size: 18px; width: 100%; }
|
||||
th { background: #F0F0F0; }
|
||||
blockquote { border-left: 4px solid #D6002A; color: #2E2E2E; font-size: 20px; }
|
||||
pre { font-size: 14px; line-height: 1.3; }
|
||||
code { font-size: 14px; }
|
||||
img { display: block; margin: 0 auto; max-height: 300px; }
|
||||
.badge {
|
||||
display: inline-block; padding: 2px 8px; border-radius: 4px;
|
||||
font-size: 14px; font-weight: 600;
|
||||
}
|
||||
.testing { background: #DBEAFE; color: #1E3A5F; }
|
||||
.planned { background: #fef3c7; color: #78350f; }
|
||||
.agentic { background: #EDE9FE; color: #4C1D95; }
|
||||
---
|
||||
|
||||
<!-- _class: title -->
|
||||
<!-- _paginate: false -->
|
||||
|
||||
# The Developer Experience
|
||||
|
||||
### Agentic Cloud Delivery Platform
|
||||
|
||||
<style>
|
||||
section.title h1 { font-size: 44px; margin-bottom: 0.1em; }
|
||||
section.title h3 { color: #F0F0F0; font-weight: 400; font-size: 22px; margin-top: 0; }
|
||||
</style>
|
||||
|
||||
---
|
||||
|
||||
# Two Consumer Surfaces, One Platform
|
||||
|
||||
The platform serves **two kinds of consumer** — both converge on the **same contract, the same policy envelope, and the same evidence stream.**
|
||||
|
||||

|
||||
|
||||
- **Technical developer** — owns app code + a contract + a thin CI definition
|
||||
- **Citizen developer** — declares intent in plain language; an AI agent produces a contract that passes the **same** safety envelope <span class="badge agentic">Agentic</span>
|
||||
|
||||
The platform is **opinionated in what it accepts, regardless of who is declaring.** There is no "citizen developer mode" with weaker checks.
|
||||
|
||||
---
|
||||
|
||||
# The Contract — The Entire Consumer Surface
|
||||
|
||||
Three things. That is the entire consumer-side surface.
|
||||
|
||||
<img src="assets/png/developer-experience-02-what-dev-does.png" style="float: right; width: 40%; margin-left: 20px; margin-bottom: 10px;" />
|
||||
|
||||
- **1. App code** — the consumer's service, at the top level of the repo
|
||||
- **2. A contract** — a single YAML file: module, environment, inputs
|
||||
|
||||
```yaml
|
||||
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||
module: microservice
|
||||
environment: dev
|
||||
inputs:
|
||||
image: my-registry/my-microservice:latest
|
||||
port: 8080
|
||||
```
|
||||
|
||||
- **3. A one-line CI definition** — a thin `uses:` wrapper pointing at a versioned platform workflow
|
||||
- The developer does **not**: write infrastructure modules, clone the platform repo, hold cloud credentials, or maintain a state backend
|
||||
|
||||
---
|
||||
|
||||
# The Developer Feedback Loop
|
||||
|
||||
Developers see **what the platform is doing**, in real time, in their own run logs. <span class="badge testing">Testing</span>
|
||||
|
||||
- **Streamed output by default** — the infrastructure plan, policy-check results, and each check record flow to stdout
|
||||
- **PR comments after every successful pipeline stage** — a developer always knows where they stand without refreshing a dashboard
|
||||
- **Clear, explainable halt reasons** — a policy violation, an insufficient confidence signal, or a missing attestation. **Never an opaque debugging exercise.**
|
||||
- **Connection strings posted as PR comments** — human-readable, no hunting
|
||||
- **Runtime secrets in encrypted Parameter Store** — KMS-encrypted, namespaced, **no raw secrets in logs**
|
||||
- **Errors become GitHub issues, automatically** — a failed deploy opens an issue on the platform repo
|
||||
|
||||
---
|
||||
|
||||
# Versioned, Predictable Releases
|
||||
|
||||
Consumers control **when** they absorb platform improvements. <span class="badge testing">Testing</span>
|
||||
|
||||
- **Floating MAJOR + MINOR tags** (e.g. `@v1.6`) — a consumer automatically receives patch updates within the line
|
||||
- **Semantic versioning with a clear contract:** interface → MAJOR, behavior → MINOR, lifecycle → PATCH
|
||||
- **A consumer can pin to an exact version** for maximum stability, or float on MAJOR only (`@v1`) to absorb new features on their own cadence
|
||||
- **Unversioned references (`@main`, bare) are discouraged** — the versioned tag is the only immutability lever
|
||||
- **Automated release job** computes the next semver on merge to main, creates the tag, and updates the floating tags
|
||||
|
||||
---
|
||||
|
||||
# Friendly Onboarding
|
||||
|
||||
First impressions of a platform are made **when it fails for the first time.** The platform fails gracefully. <span class="badge testing">Testing</span>
|
||||
|
||||
When no environment is bound, the platform emits a **user-friendly onboarding prompt** instead of failing opaquely:
|
||||
|
||||
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
|
||||
|
||||
The pipeline then **exits without attempting a deployment** — no partial state, no confusing errors.
|
||||
|
||||
<span class="badge planned">Citizen developer onboarding path: planned</span>
|
||||
|
||||
---
|
||||
|
||||
# Safe Promotion Path
|
||||
|
||||
The contract is environment-agnostic by design. Promotion is **a workflow choice, not a contract edit** — the platform raises the bar automatically.
|
||||
|
||||
<table style="width: 100%; border: none;">
|
||||
<tr>
|
||||
<td style="width: 50%; vertical-align: top; border: none; padding-right: 12px;">
|
||||
|
||||
**Approach A — One contract, one job per environment.** The environment is passed by each job and interpolated at runtime.
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
dev:
|
||||
uses: acdl/.github/workflows/deploy.yml@v1.6
|
||||
with: { contract: .acdl/contract.yaml, environment: dev }
|
||||
qa:
|
||||
needs: dev
|
||||
uses: acdl/.github/workflows/deploy.yml@v1.6
|
||||
with: { contract: .acdl/contract.yaml, environment: qa }
|
||||
```
|
||||
|
||||
</td>
|
||||
<td style="width: 50%; vertical-align: top; border: none; padding-left: 12px;">
|
||||
|
||||
**Approach B — Environment-specific contracts.** When inputs genuinely differ, each job points at its own contract file.
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
dev:
|
||||
uses: acdl/.github/workflows/deploy.yml@v1.6
|
||||
with: { contract: .acdl/contract-dev.yaml }
|
||||
qa:
|
||||
needs: dev
|
||||
uses: acdl/.github/workflows/deploy.yml@v1.6
|
||||
with: { contract: .acdl/contract-qa.yaml }
|
||||
```
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
| Environment | What the platform adds |
|
||||
|---|---|
|
||||
| dev | Confidence ≥ 0.50, fully autonomous <span class="badge agentic">Agentic</span> |
|
||||
| qa | QA human attestation + confidence ≥ 0.75 |
|
||||
| prod | SRE human attestation + confidence ≥ 0.90 |
|
||||
|
||||
<style>
|
||||
section { font-size: 16px; }
|
||||
pre { font-size: 10px; line-height: 1.2; }
|
||||
code { font-size: 10px; }
|
||||
td { font-size: 14px; }
|
||||
table { font-size: 14px; }
|
||||
</style>
|
||||
|
||||
---
|
||||
|
||||
# Safe Decommission
|
||||
|
||||
Tearing down a stack is **as deliberate as deploying one** — and just as gated. <span class="badge testing">Testing</span>
|
||||
|
||||
```yaml
|
||||
uses: acdl/.github/workflows/deploy.yml@v1.8
|
||||
with:
|
||||
contract: .acdl/contract.yaml
|
||||
mode: decommission
|
||||
changeRequestId: "CHG0678912"
|
||||
```
|
||||
|
||||
A 2-step pipeline with **two SRE human-attestation gates**:
|
||||
|
||||
1. **Validate the change request** — the platform queries the CMDB; the CR must be `approved` and match the consumer repo
|
||||
2. **Disable deletion protection** → **SRE approves** → **Zero all counts + destroy** → **a second SRE approves**
|
||||
|
||||
The per-stack encryption key enters a **grace window** (default 30 days) so encrypted data remains recoverable.
|
||||
|
||||
---
|
||||
|
||||
# Self-Service Module Catalog
|
||||
|
||||
Developers pick from **pre-built, security-reviewed building blocks** — they don't author infrastructure from scratch. <span class="badge testing">Testing</span>
|
||||
|
||||
- **Primitives** — single-purpose resources (S3, VPC, ECS, IAM, load balancer, container registry, CloudFront, WAF, RDS), each with documented inputs/outputs, usage, compliance extension points, and versioning
|
||||
- **Modules** — composed patterns (a static site with CDN + WAF; a microservice with VPC + ECS + load balancer + registry)
|
||||
- **Validated examples per module** — `simple.yaml` + `complex.yaml` + variation files, validated against the contract schema in CI. Examples cannot drift from the schema silently
|
||||
- **Auto-promotion of patterns** — a thin-composition layer is auto-promoted to the catalog after 3 observed usages <span class="badge planned">Planned</span> <span class="badge agentic">Agentic</span>
|
||||
- **Compliance extension points** — each module lists where GDPR, SOX, SOC2, DORA controls will wire in <span class="badge planned">Planned</span>
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: title -->
|
||||
<!-- _paginate: false -->
|
||||
|
||||
# The Desired Outcomes
|
||||
|
||||
- **Velocity without sacrificing safety.** Speed is in the ergonomics (a simple contract, a one-line `uses:`); safety is in the gates the consumer cannot bypass.
|
||||
- **Security, observability, and compliance as platform defaults** — not per-team effort, not post-hoc remediation. Encryption, deletion protection, uptime monitoring, policy checks, and evidence are on by construction.
|
||||
- **Auditability as a byproduct, not a project.** Every production change is traceable to a human attestation and a tamper-evident evidence event.
|
||||
- **Blast radius contained by design.** Zero-trust OIDC + ABAC means a consumer can only touch its own tagged resources.
|
||||
- **The bottleneck moves off the platform team's ticket queue.** A merged change progresses through lower environments without a platform engineer joining a thread.
|
||||
- **Infrastructure as a utility, not a craft.** The platform abstracts compute, networking, and state. Teams consume infrastructure, they don't maintain it — and the platform compounds value over time by learning from recurring patterns.
|
||||
- **A path to the citizen developer.** The same safety envelope that serves a senior engineer will serve a non-technical consumer — expanding who can ship safely without lowering the bar. <span class="badge agentic">Agentic</span>
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,317 @@
|
||||
# The Developer Experience
|
||||
|
||||
> **Subtitle:** Agentic Cloud Delivery Platform
|
||||
> **Audience:** Senior Leadership, CTO, Head of Cloud, Head of Infrastructure, Head of DevOps
|
||||
> **Length:** ~15 minutes · 13 slides
|
||||
> **Purpose:** Sell the developer experience and the citizen developer experience to tech leadership — velocity without sacrificing safety, and security/observability/compliance as platform defaults rather than per-team effort.
|
||||
> **Maturity framing:** "Testing" = shipped and verified. "Planned" = on the roadmap, not yet shipped.
|
||||
|
||||
---
|
||||
|
||||
## Slide 1 — Two Consumer Surfaces, One Platform
|
||||
|
||||
The platform serves **two kinds of consumer** through two coordinated interfaces — but both converge on the **same contract, the same policy envelope, and the same evidence stream.**
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["Technical developer"] --> C["Contract YAML"]
|
||||
B["Citizen developer<br/>(non-technical)"] --> D["Declares intent in<br/>natural language"]
|
||||
D --> E["Agent produces<br/>the contract"]
|
||||
C --> F["Same platform:<br/>resolve → check → plan → policy<br/>→ confidence → evidence → apply"]
|
||||
E --> F
|
||||
F --> G["Same safety guarantees,<br/>same audit trail"]
|
||||
```
|
||||
|
||||
- **Technical developer** — owns app code + a contract + a thin CI definition. Uses the full module catalog and inputs.
|
||||
- **Citizen developer** — declares intent in plain language; an AI agent produces a contract that passes the **same** safety envelope as a senior engineer's.
|
||||
|
||||
The platform is **opinionated in what it accepts, regardless of who is declaring.** There is no "citizen developer mode" with weaker checks.
|
||||
|
||||
> **Speaker notes:** This is the thesis of the deck. The two surfaces are *parallel*, not a progression — a citizen developer doesn't "graduate" to the developer surface. Both produce a contract; both get the same treatment. The leadership takeaway: we expand who can ship safely without lowering the bar.
|
||||
|
||||
---
|
||||
|
||||
## Slide 2 — What a Developer Actually Does
|
||||
|
||||
Three things. That is the entire consumer-side surface.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["1. App code<br/>(top level of the repo)"] --> D["Push to main"]
|
||||
B["2. Contract<br/>(.acdl/contract.yaml)"] --> D
|
||||
C["3. CI definition<br/>(.github/workflows/deploy.yml<br/>— one 'uses:' line)"] --> D
|
||||
D --> E["Platform does the rest"]
|
||||
```
|
||||
|
||||
The developer does **not**:
|
||||
|
||||
- Write infrastructure modules.
|
||||
- Author workflow YAML beyond the one-line `uses:` wrapper.
|
||||
- Clone the platform repo.
|
||||
- Hold cloud credentials.
|
||||
- Maintain a state backend, a VPC, or a runner.
|
||||
|
||||
> **Speaker notes:** Hold this slide. The audience should sit with how small the consumer surface is. Every item in the "does not" list is a category of toil the platform removes. For the Head of DevOps: this is the lever for throughput — the bottleneck moves off the platform team's ticket queue.
|
||||
|
||||
---
|
||||
|
||||
## Slide 3 — The Citizen Developer Experience
|
||||
|
||||
A non-technical consumer ships a production deployment **by declaring intent** — without authoring a workflow, a configuration file, or an infrastructure module.
|
||||
|
||||
- The consumer opens an issue describing what they need (e.g. "a web API for the pricing service").
|
||||
- An AI agent maps the intent to a contract referencing a module from the **reviewed skill catalog.**
|
||||
- The contract enters the **same pipeline** and must clear the **same confidence gate** before promotion.
|
||||
|
||||
**Guardrails that make this safe:**
|
||||
|
||||
- Skills are **versioned, signed, and reviewed for sensitive data before release** (Infra & Ops owns the review — it is the mandatory release gate).
|
||||
- Agents are **stateless** — all state lives in the platform. The platform does not run the skill blindly; it trusts and **always verifies** on the platform side.
|
||||
- The agent's trace and submission confidence are captured in the contract (`profile: agentic`), so a reviewer can see *how* the contract was produced.
|
||||
- **Initial skill catalog:** web API, worker, scheduled job, static asset, basic observability bootstrap. *(Catalog is planned; the agentic surface is on the roadmap.)*
|
||||
|
||||
> **Speaker notes:** Be honest about maturity: the *mechanism* (agent → contract → same pipeline) is designed and the stub was proven in the v1.0 demo; the full skill catalog and real agent runtime are planned. But the design point matters to leadership now: we are building for a world where more of the org can ship safely, not where more of the org has to become a platform engineer.
|
||||
|
||||
---
|
||||
|
||||
## Slide 4 — The Contract
|
||||
|
||||
A 5-line YAML file. This is the entire consumer-facing interface to production.
|
||||
|
||||
```yaml
|
||||
# .acdl/contract.yaml — a static site
|
||||
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||
module: static-assets
|
||||
environment: dev
|
||||
inputs:
|
||||
bucket_name: my-static-site-assets
|
||||
region: us-east-1
|
||||
```
|
||||
|
||||
```yaml
|
||||
# .acdl/contract.yaml — a microservice
|
||||
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||
module: microservice
|
||||
environment: dev
|
||||
inputs:
|
||||
image: my-registry/my-microservice:latest
|
||||
port: 8080
|
||||
env:
|
||||
LOG_LEVEL: info
|
||||
```
|
||||
|
||||
Four fields:
|
||||
|
||||
| Field | Meaning |
|
||||
|---|---|
|
||||
| `uses` | The central pipeline, pinned to a versioned tag |
|
||||
| `module` | A name from the module catalog |
|
||||
| `environment` | `dev`, `qa`, `prod`, or `dr` |
|
||||
| `inputs` | The handful of values that vary per deployment |
|
||||
|
||||
An invalid contract (missing field, unknown module, wrong type) **fails fast at validation** with a clear error — not an opaque failure three stages in.
|
||||
|
||||
> **Speaker notes:** The contract is the API. It is deliberately tiny so that it can be reviewed, validated, and audited. For leadership: this is what makes "declare intent" concrete — it's a one-screen file, not a 300-line Terraform root module.
|
||||
|
||||
---
|
||||
|
||||
## Slide 5 — No Platform Code, No Cloning
|
||||
|
||||
Consumers `uses:` a **versioned** central workflow. The platform fetches itself at run time. The consumer **never touches platform internals.**
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["Consumer repo<br/>app + contract + 'uses:'"] -->|triggers on push to main| B["Platform runner"]
|
||||
B -->|checks out the consumer repo| A
|
||||
B -->|checks out the ACDL platform repo<br/>into the workspace| C["Platform code<br/>(modules, adapters, schemas)"]
|
||||
C --> B
|
||||
B -->|runs the pipeline against<br/>the consumer's contract| D["Consumer's resources in AWS"]
|
||||
```
|
||||
|
||||
- The consumer's CI definition is a thin wrapper — one `uses:` line pointing at a versioned tag.
|
||||
- The runner checks out the consumer repo, then checks out the platform repo into the workspace.
|
||||
- The platform installs its own runtime dependencies. The consumer installs nothing.
|
||||
- The consumer **never clones the platform repo, never invokes platform scripts locally** (optional `--check-only` validation is available but not required for the happy path).
|
||||
|
||||
> **Speaker notes:** The Head of Cloud cares about this: there is no "platform code in every consumer repo" problem. When the platform ships a fix, every consumer on a floating MAJOR.MINOR tag gets it on their next run — no per-repo upgrade project.
|
||||
|
||||
---
|
||||
|
||||
## Slide 6 — Versioned, Predictable Releases
|
||||
|
||||
Consumers control **when** they absorb platform improvements.
|
||||
|
||||
- **Floating MAJOR + MINOR tags** (e.g. `@v1.6`) — a consumer on `@v1.6` automatically receives patch updates within the 1.6 line.
|
||||
- **Semantic versioning with a clear contract:** interface changes → MAJOR, behavior changes → MINOR, lifecycle fixes → PATCH.
|
||||
- **A consumer can pin to an exact version** for maximum stability, or float on MAJOR only (`@v1`) to absorb new features on their own cadence.
|
||||
- **Unversioned references (`@main`, bare) are discouraged** — the versioned tag is the only immutability lever a consumer has.
|
||||
- **Automated release job** computes the next semver on merge to main, creates the tag, and updates the floating tags. *(Testing.)*
|
||||
|
||||
> **Speaker notes:** This is the "no surprise upgrades" story. Leadership hears two things: (1) consumers aren't forced to chase the platform, (2) the platform isn't forced to support N forks of every workflow. The versioning discipline is what makes both true.
|
||||
|
||||
---
|
||||
|
||||
## Slide 7 — Instant Feedback
|
||||
|
||||
Developers see **what the platform is doing**, in real time, in their own run logs.
|
||||
|
||||
- **Streamed output by default** — the infrastructure plan, policy-check results, and each `PolicyCheckResult` record (severity, rule ID, pass/fail) flow to stdout. *(Testing.)*
|
||||
- **PR comments after every successful pipeline stage** — a developer always knows where they stand without refreshing a dashboard. *(Testing.)*
|
||||
- **Clear, explainable halt reasons** — a policy violation, an insufficient confidence signal, or a missing attestation. **Never an opaque, manual-debugging exercise.**
|
||||
- **A `--quiet` mode** suppresses streaming for log-only contexts.
|
||||
|
||||
> **Speaker notes:** This directly answers "but developers hate platforms that hide what they're doing." The platform is opinionated about *what* runs, not *opaque* about *that* it runs. The PR-comment-after-each-stage pattern is a small thing that compounds into trust.
|
||||
|
||||
---
|
||||
|
||||
## Slide 8 — Deploy Outputs That Just Work
|
||||
|
||||
After a successful deploy, the developer gets their connection information **without hunting for it** — and without secrets leaking into logs.
|
||||
|
||||
- **Human-readable connection strings** posted as a structured GitHub PR comment / job summary. *(Testing.)*
|
||||
- **Runtime-injectable values** written to encrypted Parameter Store (`SecureString`, KMS-encrypted, namespaced `/acdl/{env}/{contractId}/{output_name}`). *(Testing.)*
|
||||
- **No raw secrets in logs** — the platform enforces this by construction.
|
||||
- **Errors become GitHub issues, automatically** — a failed deploy reports through the platform Lambda, which opens (or comments on) an issue on the platform repo. The consumer's only grant is the onboarding-granted Lambda-invoke permission — no separate `issues: write` scope on the consumer side. *(Testing.)*
|
||||
|
||||
> **Speaker notes:** The "errors become issues" point is a DX win that also helps the platform team — every consumer failure is a tracked, queryable artifact, not a lost log line. The Head of DevOps should hear: the platform closes the feedback loop, it doesn't just push a green/red status.
|
||||
|
||||
---
|
||||
|
||||
## Slide 9 — Friendly Onboarding
|
||||
|
||||
First impressions of a platform are made **when it fails for the first time.** The platform fails gracefully.
|
||||
|
||||
- When a consumer pipeline runs for the first time and **no environment is bound**, the platform detects this and emits a **user-friendly onboarding prompt** instead of failing opaquely. *(Testing.)*
|
||||
- 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.
|
||||
- The pipeline then **exits without attempting a deployment** — no partial state, no confusing errors.
|
||||
- **Both onboarding paths end in a sandbox dev submission that must pass the confidence gate** before the consumer is promoted. *(Developer path shipped; citizen developer path planned.)*
|
||||
|
||||
> **Speaker notes:** This looks like a small thing; it's actually a cultural one. The platform's posture is "help me get started," not "you should have known." For the Head of DevOps: this is what drives adoption. Platforms that fail opaquely on first run get routed around.
|
||||
|
||||
---
|
||||
|
||||
## Slide 11 — Safe Promotion Path
|
||||
|
||||
The contract is environment-agnostic by design. Promotion is **a workflow choice, not a contract edit** — the same contract carries cleanly from dev to qa to prod. The platform raises the bar automatically as the target environment becomes more sensitive.
|
||||
|
||||
**Approach A — One contract, one job per environment.** A single contract is referenced by multiple jobs in the CI workflow; the environment is passed by each job and interpolated at runtime. The contract itself never changes.
|
||||
|
||||
```yaml
|
||||
# .github/workflows/deploy.yml — one job per environment, one shared contract
|
||||
jobs:
|
||||
dev:
|
||||
uses: acdl/.github/workflows/deploy.yml@v1.6
|
||||
with:
|
||||
contract: .acdl/contract.yaml
|
||||
environment: dev
|
||||
qa:
|
||||
needs: dev
|
||||
uses: acdl/.github/workflows/deploy.yml@v1.6
|
||||
with:
|
||||
contract: .acdl/contract.yaml
|
||||
environment: qa
|
||||
prod:
|
||||
needs: qa
|
||||
uses: acdl/.github/workflows/deploy.yml@v1.6
|
||||
with:
|
||||
contract: .acdl/contract.yaml
|
||||
environment: prod
|
||||
```
|
||||
|
||||
**Approach B — One job per environment, environment-specific contracts.** When inputs genuinely differ per environment (different capacity, different config), each job points at its own contract file. The pipeline, policy, and confidence model stay identical.
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
dev:
|
||||
uses: acdl/.github/workflows/deploy.yml@v1.6
|
||||
with:
|
||||
contract: .acdl/contract-dev.yaml
|
||||
qa:
|
||||
needs: dev
|
||||
uses: acdl/.github/workflows/deploy.yml@v1.6
|
||||
with:
|
||||
contract: .acdl/contract-qa.yaml
|
||||
prod:
|
||||
needs: qa
|
||||
uses: acdl/.github/workflows/deploy.yml@v1.6
|
||||
with:
|
||||
contract: .acdl/contract-prod.yaml
|
||||
```
|
||||
|
||||
Whichever approach a team picks, the platform applies the same rising bar:
|
||||
|
||||
| Environment | What the platform adds |
|
||||
|---|---|
|
||||
| dev | Confidence ≥ 0.50, fully autonomous |
|
||||
| qa | QA human attestation + confidence ≥ 0.75 |
|
||||
| prod | SRE human attestation + confidence ≥ 0.90 |
|
||||
| dr | SRE human attestation + confidence ≥ 0.95 + a disaster-recovery drill reference |
|
||||
|
||||
- **No staging environment** — the design deliberately removes the "staging is basically prod but not really" anti-pattern. Dev is the only autonomous environment.
|
||||
- **Separation of duties is enforced** — the QA approver cannot be the prod approver. *(Design tested; wiring for qa/prod/dr is planned.)*
|
||||
- **Timeout discipline** — 1 business day = warn + escalate; 2 business days = auto-freeze + re-submit.
|
||||
|
||||
> **Speaker notes:** Promotion is a workflow choice, not a contract mutation — this matters because it means a promotion can be reviewed as a *diff in the workflow*, not as a rewritten contract. Approach A (one contract, environment passed by the job) keeps the single source of truth; Approach B (environment-specific contracts) lets teams whose inputs genuinely vary keep that variation explicit and reviewable. For leadership: the DX win is that the contract stays stable across environments; the safety win is that the platform raises the threshold and attestation bar automatically based on the target environment the job declares. The consumer can't bypass the gates — they pick *which* environment to target, and the platform applies the right bar.
|
||||
|
||||
---
|
||||
|
||||
## Slide 12 — Safe Decommission
|
||||
|
||||
Tearing down a stack is **as deliberate as deploying one** — and just as gated.
|
||||
|
||||
```yaml
|
||||
# Consumer's deploy workflow call
|
||||
uses: acdl/.github/workflows/deploy.yml@v1.8
|
||||
with:
|
||||
contract: .acdl/contract.yaml
|
||||
mode: decommission
|
||||
changeRequestId: "CHG0678912"
|
||||
```
|
||||
|
||||
A 2-step pipeline with **two SRE human-attestation gates** *(available today)*:
|
||||
|
||||
1. **Validate the change request** — the platform queries the CMDB and asserts the CR is `approved` and matches the consumer repo. No CR, no decommission.
|
||||
2. **Disable deletion protection** (resolve with `deletion_protection: false`, plan + apply) → **SRE approves.**
|
||||
3. **Zero all counts + destroy** (the platform zeroes every scalable count, plan + apply) → **a second SRE approves.**
|
||||
4. **Confirmation** — the platform confirms the stack is destroyed.
|
||||
|
||||
**After decommission:**
|
||||
|
||||
- The per-stack encryption key enters a **grace window** (default 30 days) so encrypted data remains recoverable. The key is permanently deleted only after the window expires.
|
||||
- Uptime monitoring is **not** automatically destroyed — it can be left running to watch the decommissioned endpoints go dark, or destroyed separately.
|
||||
|
||||
> **Speaker notes:** The counter-argument to "deletion protection makes cleanup impossible" is this slide. Decommission is a first-class, gated, two-approval flow — not a lock with no key, and not an ungated `terraform destroy`. For the Head of Infrastructure: the CMDB validation means decommission is auditable, not just possible.
|
||||
|
||||
---
|
||||
|
||||
## Slide 13 — Self-Service Module Catalog
|
||||
|
||||
Developers pick from **pre-built, security-reviewed building blocks** — they don't author infrastructure from scratch.
|
||||
|
||||
- **Primitives** — single-purpose resources (S3, VPC, ECS cluster, ECS service, IAM role, load balancer, container registry, CloudFront, WAF, RDS). Each has documented inputs, outputs, usage, compliance extension points, and versioning. *(Testing.)*
|
||||
- **Modules** — composed patterns (a static site with CDN + WAF; a microservice with VPC + ECS + load balancer + registry). *(Testing.)*
|
||||
- **Validated examples per module** — every module ships `simple.yaml` + `complex.yaml` + variation files, validated against the contract schema in CI. Examples cannot drift from the schema silently. *(Testing.)*
|
||||
- **Auto-promotion of patterns** — a thin-composition layer is auto-promoted to the catalog after 3 observed usages. *(Mechanism planned.)*
|
||||
- **Compliance extension points** — each module lists where GDPR, SOX, SOC2, DORA controls will wire in. *(Compliance milestone is planned.)*
|
||||
|
||||
> **Speaker notes:** The catalog is what makes "declare intent" practical — you can only declare a module that exists. For leadership: the catalog is the leverage. One well-reviewed module serves every consumer; a fix to the module serves every consumer on the next run. This is the compounding asset.
|
||||
|
||||
---
|
||||
|
||||
## Slide 14 — The Outcome for Leadership
|
||||
|
||||
What this platform delivers to the organization:
|
||||
|
||||
- **Velocity without sacrificing safety.** The speed is in the ergonomics (a 5-line contract, a one-line `uses:`); the safety is in the gates the consumer cannot bypass.
|
||||
- **Security, observability, and compliance as platform defaults** — not per-team effort, not post-hoc remediation. Encryption, deletion protection, uptime monitoring, policy checks, and evidence are on by construction.
|
||||
- **Auditability as a byproduct, not a project.** Every production change is traceable to a human attestation and a tamper-evident evidence event — captured during the deploy, not reconstructed for the audit.
|
||||
- **Blast radius contained by design.** Zero-trust OIDC + ABAC means a consumer can only touch its own tagged resources. One consumer can never affect another.
|
||||
- **The bottleneck moves off the platform team's ticket queue.** A merged change progresses through lower environments without a platform engineer joining a thread. The platform team invests in the platform, not in per-deployment hand-holding.
|
||||
- **Infrastructure as a utility, not a craft.** The platform abstracts compute, networking, and state. Teams consume infrastructure, they don't maintain it — and the platform compounds value over time by learning from recurring patterns.
|
||||
- **A path to the citizen developer.** The same safety envelope that serves a senior engineer is the one that will serve a non-technical consumer — expanding who can ship safely without lowering the bar.
|
||||
|
||||
> **Speaker notes:** Close on the strategic frame. The platform is not "a CI/CD tool" — it is the organizational lever for shipping safely at the pace the business demands, with the security and audit posture the regulators require. Invite questions; the companion deck ("How the Platform Works") covers the internal mechanics in more depth.
|
||||
@@ -0,0 +1,71 @@
|
||||
# Agentic Cloud Delivery Vision
|
||||
|
||||
## 1. The Friction
|
||||
|
||||
Software delivery scales with the coordination surface around it, not the engineering inside it. Most teams know how to write code; far fewer know how to author the infrastructure that runs it correctly. The result is a long tail of well-meaning services that are difficult to deploy, hard to operate, and inconsistent in their security and observability posture.
|
||||
|
||||
Once code is merged, the second friction begins. Moving a service from "merged" to "running in production with policy, observability, and security enforced" requires manual work that scales with the system, not with the change. The platform's job is to absorb both frictions — the cognitive load of getting the infrastructure right, and the operational work of getting the change to production safely.
|
||||
|
||||
## 2. The North Star
|
||||
|
||||
Consumers declare intent; the platform delivers safe production deployment through an agentic stack.
|
||||
|
||||
## 3. Core Tenets
|
||||
|
||||
* **Operations are Declared, Not Executed.** Consumers define what they need — workload shape, dependencies, non-functional requirements, policy constraints. The platform handles reconciliation, provisioning, and environment progression. The execution burden moves from the human to the platform.
|
||||
* **The Delivery Lifecycle is a Sovereign Boundary.** The platform governs the infrastructure and delivery engine. It does not penetrate upstream product or software development lifecycles. Integration happens exclusively through validated, published contracts.
|
||||
* **Lower Environments are Autonomous; Higher Environments are Attested.** Progression through lower environments proceeds through zero-touch agentic automation. Promotion to higher-stakes environments requires deliberate human attestation — not as a rubber stamp, but as a policy-mandated act of accountability.
|
||||
* **Safety is Computed, Not Assumed.** Every delivery action produces a measurable, explainable confidence signal aggregating policy conformance, validation evidence, and historical behavior. The signal is the platform's certified answer to "is this safe to proceed?" Reliance on operator instinct or tenure is not a substitute.
|
||||
* **Infrastructure is Consumed, Not Maintained.** Compute is abstract, containerized, or serverless. The platform does not manage node, OS, or bare-metal lifecycles. Infrastructure is treated as a utility, not a craft.
|
||||
* **Two Consumer Surfaces, One Platform.** The platform serves technical developers and non-technical consumers through two coordinated interfaces. Both converge on the same contract schema, the same policy envelope, and the same evidence stream. The platform is opinionated in what it accepts, regardless of who is declaring.
|
||||
|
||||
## 4. Domain Boundaries
|
||||
|
||||
The platform begins where the artifact is compiled and ends where it runs in production under operational guardrails.
|
||||
|
||||
* **In scope:** Environment progression, cloud resource lifecycle, operational security and observability NFRs, policy enforcement, immutable audit lineage, confidence frameworks, two consumer surfaces (developer and agentic).
|
||||
* **Out of scope:** Application business logic, IDE workflows, product backlog management, sprint planning, compute requiring node-level or OS-level management.
|
||||
* **Interface:** Upstream systems interact with the platform through a strict contract boundary. The platform validates, enriches with operational standards, and reconciles the target state. Visibility into how software is authored is not required — only assurance about what is being delivered and under what policy constraints.
|
||||
|
||||
## 5. Strategic Bets
|
||||
|
||||
These are the leaps of faith underlying this vision. If any prove false, the vision requires fundamental revision.
|
||||
|
||||
* **Autonomous progression through lower environments is sufficiently safe.** End-to-end agentic progression through non-production environments — with rigorous policy, testing, and observability gates — is less risky than human-driven pipelines that rely on manual checklist discipline.
|
||||
* **Confidence can replace presumption.** A computed, policy-derived confidence signal is a legitimate arbiter for autonomous action, replacing the instinct of an operator who "knows the system."
|
||||
* **Narrow capability interfaces beat broad access.** Infrastructure capabilities are exposed to autonomous systems through constrained, domain-specific interfaces — never through raw, unbounded platform credentials. Agents call capabilities, not APIs.
|
||||
* **Audit truth lives outside the repository.** Version control is a coordination tool, not an evidentiary fortress. True compliance requires an immutable, externally-stored ledger to which the platform writes; repositories hold only lightweight attestation linkage.
|
||||
* **Pattern recognition can compound platform value over time.** As the platform observes recurring contract patterns, it can synthesize and offer reusable infrastructure compositions. This is a future capability, not a current commitment — but the platform's design must allow it.
|
||||
|
||||
## 6. Trade-offs Accepted
|
||||
|
||||
This vision is purchased with deliberate sacrifices:
|
||||
|
||||
* **Velocity over Legacy Flexibility.** Standardizing on abstract, containerized, and serverless compute eliminates undifferentiated toil. Teams operating non-cloud-native workloads must modernize or route elsewhere.
|
||||
* **Abstraction over Granular Control.** Removing node-level access sacrifices fine-tuned performance optimization in favor of uniform operability and security posture.
|
||||
* **Delegated Risk over Queue-based Safety.** An autonomous agent may occasionally halt, reject, or escalate a change that a human would have greenlit. In exchange, queue-based handoffs are eliminated from lower environments.
|
||||
* **Immutability over Convenience.** Every action leaves a cryptographic shadow in an external evidence stream. The operational overhead of signing, linking, and streaming is accepted in exchange for tamper-evident assurance rather than reliance on mutable, rewritable logs.
|
||||
|
||||
## 7. Anti-Goals
|
||||
|
||||
* **Not an upstream development platform.** No management of product backlogs, sprint ceremonies, IDE extensions, or code authorship workflows.
|
||||
* **Not a general-purpose AI.** The platform is not an open-ended conversational assistant. Autonomy is narrow, scoped to delivery and infrastructure reconciliation, and bounded by strict policy envelopes.
|
||||
* **Not a legacy infrastructure bridge.** No management of VMs, bare metal, or OS lifecycles. The engine will not extend to non-cloud-native patterns.
|
||||
* **Not a permissive delivery highway.** No escape hatches to bypass the confidence framework or the human attestation requirements at higher environments. Speed is a byproduct of confidence and policy compliance, not an override.
|
||||
* **Not a mutable audit log.** Version control history does not satisfy regulatory evidence. Auditability requires an immutable, externally-stored stream.
|
||||
|
||||
## 8. Signals of Success
|
||||
|
||||
The vision is realized when:
|
||||
|
||||
* A merged change progresses through lower environments end-to-end without a platform engineer joining a thread, approving a ticket, or manually triggering a stage gate.
|
||||
* A developer deploys compliant, observable, and secured infrastructure by authoring a contract and a workflow — not by reading tribal runbooks or filing infrastructure requests.
|
||||
* A non-technical consumer ships a production deployment by declaring intent — without authoring a workflow, a configuration file, or a Terraform module.
|
||||
* An auditor can trace any production change to a human attestation and an immutable evidence stream without interpreting shell scripts, pipeline logs, or repository history.
|
||||
* Security, resiliency, and observability standards are satisfied automatically through platform-enriched contracts, rather than through post-hoc remediation.
|
||||
* When the platform halts a delivery, it provides a measured, explainable reason — a policy violation, an insufficient confidence signal, or a missing attestation — rather than requiring an opaque, manual-debugging exercise.
|
||||
|
||||
## 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 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.
|
||||
@@ -0,0 +1,62 @@
|
||||
# <module-name> — <plain-language description>
|
||||
|
||||
> **Module kind:** primitive | **Version:** 1.0.0
|
||||
|
||||
## Overview
|
||||
|
||||
One or two sentences describing what this module provisions, in plain
|
||||
language. No jargon. A reader should know after this paragraph whether
|
||||
this module is what they need.
|
||||
|
||||
## Resources
|
||||
|
||||
Terraform resources this module creates:
|
||||
|
||||
| Resource | Type | Purpose |
|
||||
|----------|------|---------|
|
||||
| `<name>` | `aws_<type>` | what it does |
|
||||
|
||||
## Inputs
|
||||
|
||||
| Name | Type | Required | Default | Description |
|
||||
|------|------|----------|---------|-------------|
|
||||
| `<name>` | string | yes | — | description |
|
||||
|
||||
## Outputs
|
||||
|
||||
| Name | Type | 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
|
||||
|
||||
```
|
||||
# A concrete snippet showing how to reference this module or what a
|
||||
# consumer writes to use it.
|
||||
```
|
||||
|
||||
## Compliance extension points
|
||||
|
||||
Resources this module could be extended with for the future compliance
|
||||
milestone (GDPR, SOX, SOC2, DORA). Not implemented yet — listed
|
||||
so the redesign can plan for them.
|
||||
|
||||
- **<area>** — <what could be added, e.g. KMS key for encryption>
|
||||
|
||||
## 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,62 @@
|
||||
# ACDL Modules
|
||||
|
||||
Reusable building blocks for cloud infrastructure. Each module is
|
||||
self-documented with a `README.md` following the
|
||||
[template](README-TEMPLATE.md).
|
||||
|
||||
## How the modules work
|
||||
|
||||
There are two kinds of module:
|
||||
|
||||
- **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, and a
|
||||
`README.md` in plain language.
|
||||
- **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 engine adapter (`adapters/terraform/adapter.py`) compiles a
|
||||
module instance to infrastructure. Each module's README documents which
|
||||
resources it creates.
|
||||
|
||||
## Primitives
|
||||
|
||||
| Module | What it creates | README |
|
||||
|--------|----------------|--------|
|
||||
| `s3` | `aws_s3_bucket` — a single S3 bucket | [README](l1/s3/README.md) |
|
||||
| `vpc` | `aws_vpc` + `aws_subnet` + `aws_route_table` + `aws_internet_gateway` — VPC with subnets and routing | [README](l1/vpc/README.md) |
|
||||
| `ecs-cluster` | `aws_ecs_cluster` — ECS Fargate cluster | [README](l1/ecs-cluster/README.md) |
|
||||
| `ecs-service` | `aws_ecs_task_definition` + `aws_ecs_service` — Fargate service with task definition | [README](l1/ecs-service/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) |
|
||||
| `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) |
|
||||
|
||||
## Modules
|
||||
|
||||
| Module | What it references | README |
|
||||
|--------|--------------------|--------|
|
||||
| `microservice` | 6 primitives (vpc, cluster, ecr, iam-role, alb, ecs-service) | [README](l2/microservice/README.md) |
|
||||
| `static-assets` | 3 primitives (s3, cloudfront, waf) | [README](l2/static-assets/README.md) |
|
||||
|
||||
## Registry
|
||||
|
||||
Module versions are tracked in `registry.json`. Both primitives and
|
||||
modules are registered.
|
||||
|
||||
## Template
|
||||
|
||||
New modules should use [README-TEMPLATE.md](README-TEMPLATE.md) as
|
||||
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.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user