Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a90a7562b9 | |||
| a07a61bf3e | |||
| edc695592a | |||
| df7b40b435 | |||
| 553caf8f1d | |||
| 4e495e5648 | |||
| d830357230 | |||
| b758a7c242 | |||
| c5745de37c | |||
| 8d5c56b88e | |||
| f68f85c9fd | |||
| 75c227429a | |||
| 04bf6bc31a | |||
| 9a1ea04f93 | |||
| 2a84c0047b | |||
| 895a2f3806 | |||
| e050e65158 | |||
| 6e23c168f1 | |||
| c816493e7e |
+19
-34
@@ -1,58 +1,43 @@
|
|||||||
# Phase 17 — Audit (v1.3.1)
|
# Phase 18 — Audit (v1.3.2)
|
||||||
|
|
||||||
**Auditor:** ci-audit-verifier (model: glm-5.2)
|
|
||||||
**Date:** 2026-07-22
|
**Date:** 2026-07-22
|
||||||
**Phase:** 17 — remove-thin-composition-and-module-readmes
|
**Phase:** 18 — testing-and-cicd-pipelines
|
||||||
**Milestone:** v1.3 (active, NFR)
|
**Milestone:** v1.3 (active, NFR)
|
||||||
**Tag:** v1.3.1
|
**Tag:** v1.3.2
|
||||||
|
|
||||||
## 1. Reconstruction Test
|
## 1. Reconstruction Test
|
||||||
|
|
||||||
Git log (3 commits for phase 17) matches `.ciagent/` files:
|
Git log (2 commits for phase 18) matches `.ciagent/` files:
|
||||||
|
|
||||||
| Commit | Status | .ciagent match |
|
| Commit | Status | .ciagent match |
|
||||||
|--------|--------|----------------|
|
|--------|--------|----------------|
|
||||||
| 3508671 | execute | (execute commit, no .ciagent update needed) |
|
| 1598c54 | verify | VERIFY.md updated, ROADMAP/REQUIREMENTS marked complete |
|
||||||
| ae86a29 | specify | ROADMAP.md + REQUIREMENTS.md + config.json updated |
|
| (specify was done in prior commit ae86a29 for phase 17) | | |
|
||||||
| 431341a | verify | VERIFY.md updated |
|
|
||||||
|
|
||||||
ROADMAP.md has Phase 17 with `Status: complete (v1.3.1)`.
|
ROADMAP.md has Phase 18 with `Status: complete (v1.3.2)`.
|
||||||
REQUIREMENTS.md has REQ-36, REQ-37, REQ-38 marked `complete (v1.3.1)`.
|
REQUIREMENTS.md has REQ-39, REQ-40, REQ-41, REQ-42 marked `complete (v1.3.2)`.
|
||||||
VERIFY.md has `VERIFY PASS` verdict.
|
VERIFY.md has `VERIFY PASS` verdict.
|
||||||
Tag `v1.3.1` exists. **PASS.**
|
Tag `v1.3.2` exists. **PASS.**
|
||||||
|
|
||||||
## 2. File Discipline
|
## 2. File Discipline
|
||||||
|
|
||||||
- Working tree clean (no uncommitted changes).
|
Working tree clean. All new files present (pyproject.toml,
|
||||||
- All expected `.ciagent/` files present: ARCHITECTURE.md, AUDIT.md,
|
requirements-test.txt, 7 test files, 2 workflow YAMLs). Modified files
|
||||||
PERSONAS.md, PLAN.md, PROJECT.md, REQUIREMENTS.md, RESEARCH.md,
|
(run_platform.sh, README.md, terraform/spike/terraform.tf) are expected.
|
||||||
REVIEW.md, ROADMAP.md, VERIFY.md, config.json.
|
**PASS.**
|
||||||
- Deleted files are gone (6 files: composition.json x2, contract_resolver.py,
|
|
||||||
contracts x2, contract.schema.json).
|
|
||||||
- New files are present (11: README-TEMPLATE.md, README.md catalog, 7 L1
|
|
||||||
READMEs, 2 L2 placeholder READMEs).
|
|
||||||
- `contracts/` directory removed (was empty after file deletion).
|
|
||||||
- L2 directories kept as placeholders with READMEs only (no
|
|
||||||
composition.json). **PASS.**
|
|
||||||
|
|
||||||
## 3. Branch Hygiene
|
## 3. Branch Hygiene
|
||||||
|
|
||||||
- On `main`, no stale phase branches.
|
On `main`, no stale phase branches. `milestone/v1.0-initial` is
|
||||||
- `milestone/v1.0-initial` is a historical milestone branch (v1.0 demo).
|
historical. **PASS.**
|
||||||
- No phase/NN-* branches (phase 17 committed directly to main per the
|
|
||||||
NFR single-phase flow). **PASS.**
|
|
||||||
|
|
||||||
## 4. Commit Discipline
|
## 4. Commit Discipline
|
||||||
|
|
||||||
- All 3 phase-17 commits have `---ci---` blocks with project, phase,
|
All phase-18 commits have `---ci---` blocks with correct closing
|
||||||
milestone, status fields.
|
`---/ci---` tag. Tag `v1.3.2` follows NFR patch versioning (v1.3.1 →
|
||||||
- Commit messages follow the convention: `<type>(scope): description`.
|
v1.3.2). **PASS.**
|
||||||
- Tag `v1.3.1` follows NFR patch versioning (v1.3.0 → v1.3.1, no
|
|
||||||
separate milestone tag per the versioning logic). **PASS.**
|
|
||||||
|
|
||||||
## Verdict
|
## Verdict
|
||||||
|
|
||||||
**AUDIT CLEAN** — reconstruction, file discipline, branch hygiene, and
|
**AUDIT CLEAN** — reconstruction, file discipline, branch hygiene, and
|
||||||
commit discipline all pass. No critical issues. One P1 (AWS account ID
|
commit discipline all pass. No critical issues.
|
||||||
in l1-ecs-service README usage example) deferred to post-hoc review —
|
|
||||||
not an audit blocker.
|
|
||||||
+48
-2
@@ -84,7 +84,7 @@ id 202 published. D-034 closed (root key deactivated by user).**
|
|||||||
Milestone COMPLETE gate: review → ship `v1.2.0` (feature milestone, next
|
Milestone COMPLETE gate: review → ship `v1.2.0` (feature milestone, next
|
||||||
minor per ship.md) → audit. **DONE.**
|
minor per ship.md) → audit. **DONE.**
|
||||||
|
|
||||||
## Objective for Milestone v1.2 (active)
|
## Objective for Milestone v1.2 (prior — complete)
|
||||||
|
|
||||||
Platform hardening + first real consumer deployment. The v1.1 spike proved
|
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
|
the IR commitments hold on a single dev-only `terraform plan` for one S3
|
||||||
@@ -133,6 +133,40 @@ microservice), not just a plan.
|
|||||||
Milestone COMPLETE gate: review → ship `v1.3.0` (feature milestone, next
|
Milestone COMPLETE gate: review → ship `v1.3.0` (feature milestone, next
|
||||||
minor per ship.md — v1.1 shipped `v1.2.0`) → audit.
|
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.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
### v1.0 (Prior milestone — the demo)
|
### v1.0 (Prior milestone — the demo)
|
||||||
@@ -168,7 +202,7 @@ New requirements REQ-16..REQ-28 — see `REQUIREMENTS.md` §v1.1. Summary:
|
|||||||
- **REQ-28:** Spike verification proves the IR-shaped commitments hold (no
|
- **REQ-28:** Spike verification proves the IR-shaped commitments hold (no
|
||||||
polyglot mess; the adapter is the only substrate-specific code).
|
polyglot mess; the adapter is the only substrate-specific code).
|
||||||
|
|
||||||
### v1.2 (Active milestone — platform hardening + first real consumer deployment)
|
### v1.2 (Prior milestone — platform hardening + first real consumer deployment, complete)
|
||||||
|
|
||||||
New requirements REQ-29..REQ-35 — see `REQUIREMENTS.md` §v1.2. Summary:
|
New requirements REQ-29..REQ-35 — see `REQUIREMENTS.md` §v1.2. Summary:
|
||||||
|
|
||||||
@@ -189,6 +223,18 @@ New requirements REQ-29..REQ-35 — see `REQUIREMENTS.md` §v1.2. Summary:
|
|||||||
- **REQ-35:** End-to-end verification — consumer commit → live ECS service
|
- **REQ-35:** End-to-end verification — consumer commit → live ECS service
|
||||||
(HTTP 200) → evidence event → timeline.
|
(HTTP 200) → evidence event → timeline.
|
||||||
|
|
||||||
|
### v1.4 (Active milestone — central pipeline contract + shell reproducibility + streaming)
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
## Constraints
|
## Constraints
|
||||||
|
|
||||||
- **Forge:** Gitea at `https://git.cloudinit.dev`, org `continuous-intelligence`.
|
- **Forge:** Gitea at `https://git.cloudinit.dev`, org `continuous-intelligence`.
|
||||||
|
|||||||
@@ -91,7 +91,7 @@
|
|||||||
### Category: End-to-End Verification
|
### 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.
|
- **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 (Active — module documentation + thin-composition removal)
|
## v1.3 (Prior — module documentation + thin-composition removal, complete)
|
||||||
|
|
||||||
### Category: Thin-Composition Removal
|
### 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-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.
|
||||||
@@ -109,6 +109,51 @@
|
|||||||
|
|
||||||
- **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.
|
- **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-asset.md`. It is generic across all L2 modules (`static-asset` as the worked example), uses mermaid diagrams (model + pipeline flow), documents versioned `uses:` references (floating MAJOR+MINOR tags — bare/`@main` discouraged), scopes prerequisites to consumer-repo bootstrap only (no Terraform/Checkov/boto3/runner-key — those are platform-repo concerns), and documents that the pipeline fetches the ACDL repo at run time via a reusable workflow (consumers never invoke `scripts/run_platform.sh` locally for the happy path).
|
||||||
|
- **REQ-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-asset.yaml` uses a versioned `uses:` reference (`@v1.4`, MAJOR+MINOR) — not bare `@v1` or `@main` — as the canonical example the consumer guide points at.
|
||||||
|
- **REQ-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 `platform/` across the directory, all imports in tests/scripts/pipelines/workflows, and all doc references. `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.
|
||||||
|
|
||||||
## Out of Scope (v1.2)
|
## Out of Scope (v1.2)
|
||||||
|
|
||||||
| REQ | Original criterion | Clarified criterion (effective) | Decision |
|
| REQ | Original criterion | Clarified criterion (effective) | Decision |
|
||||||
@@ -179,7 +224,7 @@
|
|||||||
| REQ-34 | 15 | complete (v1.2.5) |
|
| REQ-34 | 15 | complete (v1.2.5) |
|
||||||
| REQ-35 | 16 | partial (v1.2.6, IAM-blocked) |
|
| REQ-35 | 16 | partial (v1.2.6, IAM-blocked) |
|
||||||
|
|
||||||
### v1.3 (active — module documentation + thin-composition removal)
|
### v1.3 (prior — module documentation + thin-composition removal, complete)
|
||||||
|
|
||||||
| Requirement | Phase | Status |
|
| Requirement | Phase | Status |
|
||||||
|-------------|-------|--------|
|
|-------------|-------|--------|
|
||||||
@@ -189,4 +234,38 @@
|
|||||||
| REQ-39 | 18 | complete (v1.3.2) |
|
| REQ-39 | 18 | complete (v1.3.2) |
|
||||||
| REQ-40 | 18 | complete (v1.3.2) |
|
| REQ-40 | 18 | complete (v1.3.2) |
|
||||||
| REQ-41 | 18 | complete (v1.3.2) |
|
| REQ-41 | 18 | complete (v1.3.2) |
|
||||||
| REQ-42 | 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) |
|
||||||
+86
-3
@@ -5,7 +5,10 @@
|
|||||||
- **v1.0 (demo):** complete — tag `v1.1.0`, 2026-07-21. All 5 phases shipped + audited PASS.
|
- **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.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.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 (active):** module documentation + thin-composition removal. The L2 composition layer is removed; module READMEs are built out.
|
- **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.0 demo URL:** https://git.cloudinit.dev/continuous-intelligence/acdl-evidence/raw/branch/main/index.html
|
- **v1.0 demo URL:** https://git.cloudinit.dev/continuous-intelligence/acdl-evidence/raw/branch/main/index.html
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -221,7 +224,7 @@ After Phase 16: COMPLETE gate — review → ship `v1.3.0` → audit.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## v1.3 (Active — module documentation + thin-composition removal)
|
## v1.3 (Complete — module documentation + thin-composition removal)
|
||||||
|
|
||||||
The v1.3 milestone starts with simplification: removing the unsatisfactory
|
The v1.3 milestone starts with simplification: removing the unsatisfactory
|
||||||
thin-composition layer and building out proper module documentation. The
|
thin-composition layer and building out proper module documentation. The
|
||||||
@@ -249,4 +252,84 @@ L2 composition mechanism will be redesigned in a later phase.
|
|||||||
- `pytest` runs and passes offline (no AWS, no Checkov, no DynamoDB).
|
- `pytest` runs and passes offline (no AWS, no Checkov, no DynamoDB).
|
||||||
- `run_platform.sh --check-only` runs offline and exits 0.
|
- `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).
|
- `.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.
|
- `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-asset.md` with a generic `docs/CONSUMER_GUIDE.md` (all L2 modules, mermaid diagrams, versioned `uses:` floating MAJOR+MINOR, consumer-scoped prerequisites, run-time platform fetch via a reusable workflow). Create byte-identical `.gitea/workflows/deploy.yml` + `.github/workflows/deploy.yml` implementing `pipelines/deploy.yaml` — a reusable workflow invoked by consumer repos via `uses: acdl/.gitea/workflows/deploy.yml@v1.4` that checks out the consumer repo + the ACDL platform repo and runs `scripts/run_platform.sh`. Update `contracts/static-asset.yaml` to `uses: acdl/pipelines/deploy.yaml@v1.4`. Extend `tests/test_pipeline_contract.py` to validate the new deploy workflows (byte-identical, schema-conformant).
|
||||||
|
- **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-asset.md` is deleted; `grep -R consumer-guide-static-asset` returns 0 dangling references; guide is generic (static-asset is the worked example, not the scope); diagrams are mermaid; `uses:` references use `@v1.4`.
|
||||||
|
- `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-asset.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 `platform/`, 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/` → `platform/` (directory + all code/test/script/pipeline/workflow references; tests green). 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: `platform/environments/` dir + sample `dev.json` + README, `platform/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; `platform/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.**
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
{
|
{
|
||||||
"slug": "acdl",
|
"slug": "acdl",
|
||||||
"name": "Agentic Cloud Delivery Platform",
|
"name": "Agentic Cloud Delivery Platform",
|
||||||
"milestone": "v1.3",
|
"milestone": "v1.6",
|
||||||
"status": "active"
|
"status": "active"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
+18
-2
@@ -1,3 +1,18 @@
|
|||||||
|
# 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
|
name: acdl-ci
|
||||||
|
|
||||||
on:
|
on:
|
||||||
@@ -20,8 +35,9 @@ jobs:
|
|||||||
- name: Compile all Python files
|
- name: Compile all Python files
|
||||||
run: |
|
run: |
|
||||||
python3 -m py_compile \
|
python3 -m py_compile \
|
||||||
acdl_platform/confidence_signal.py \
|
core/confidence_signal.py \
|
||||||
acdl_platform/outbox_writer.py \
|
core/outbox_writer.py \
|
||||||
|
core/contract_resolver.py \
|
||||||
adapters/terraform/adapter.py \
|
adapters/terraform/adapter.py \
|
||||||
adapters/terraform/policy/checkov_adapter.py \
|
adapters/terraform/policy/checkov_adapter.py \
|
||||||
scripts/push_consumer_image.py
|
scripts/push_consumer_image.py
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
# 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.4 (Gitea)
|
||||||
|
# uses: acdl/.github/workflows/deploy.yml@v1.4 (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, or check-only
|
||||||
|
type: string
|
||||||
|
default: full
|
||||||
|
|
||||||
|
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.4
|
||||||
|
|
||||||
|
- 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)
|
||||||
|
uses: aws-actions/configure-aws-credentials@v4
|
||||||
|
with:
|
||||||
|
role-to-assume: arn:aws:iam::${{ secrets.ACDL_AWS_ACCOUNT_ID }}:role/acdl-deploy-${{ github.repository_id }}
|
||||||
|
aws-region: us-east-1
|
||||||
|
env:
|
||||||
|
ACDL_AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
|
||||||
|
ACDL_AWS_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" ;;
|
||||||
|
*) echo "Unknown mode: ${{ inputs.mode }}"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
bash platform/scripts/run_platform.sh $MODE_FLAG "${{ inputs.contract }}"
|
||||||
|
|
||||||
|
- name: Upload emitted Terraform
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: acdl-terraform
|
||||||
|
path: platform/terraform/spike/*.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,3 +1,18 @@
|
|||||||
|
# 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
|
name: acdl-ci
|
||||||
|
|
||||||
on:
|
on:
|
||||||
@@ -20,8 +35,9 @@ jobs:
|
|||||||
- name: Compile all Python files
|
- name: Compile all Python files
|
||||||
run: |
|
run: |
|
||||||
python3 -m py_compile \
|
python3 -m py_compile \
|
||||||
acdl_platform/confidence_signal.py \
|
core/confidence_signal.py \
|
||||||
acdl_platform/outbox_writer.py \
|
core/outbox_writer.py \
|
||||||
|
core/contract_resolver.py \
|
||||||
adapters/terraform/adapter.py \
|
adapters/terraform/adapter.py \
|
||||||
adapters/terraform/policy/checkov_adapter.py \
|
adapters/terraform/policy/checkov_adapter.py \
|
||||||
scripts/push_consumer_image.py
|
scripts/push_consumer_image.py
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
# 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.4 (Gitea)
|
||||||
|
# uses: acdl/.github/workflows/deploy.yml@v1.4 (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, or check-only
|
||||||
|
type: string
|
||||||
|
default: full
|
||||||
|
|
||||||
|
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.4
|
||||||
|
|
||||||
|
- 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)
|
||||||
|
uses: aws-actions/configure-aws-credentials@v4
|
||||||
|
with:
|
||||||
|
role-to-assume: arn:aws:iam::${{ secrets.ACDL_AWS_ACCOUNT_ID }}:role/acdl-deploy-${{ github.repository_id }}
|
||||||
|
aws-region: us-east-1
|
||||||
|
env:
|
||||||
|
ACDL_AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
|
||||||
|
ACDL_AWS_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" ;;
|
||||||
|
*) echo "Unknown mode: ${{ inputs.mode }}"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
bash platform/scripts/run_platform.sh $MODE_FLAG "${{ inputs.contract }}"
|
||||||
|
|
||||||
|
- name: Upload emitted Terraform
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: acdl-terraform
|
||||||
|
path: platform/terraform/spike/*.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
|
||||||
@@ -5,118 +5,165 @@ through an agentic stack — automatically, safely, and with a complete audit
|
|||||||
trail. A merged change progresses through lower environments end-to-end
|
trail. A merged change progresses through lower environments end-to-end
|
||||||
without a platform engineer joining a thread; a non-technical consumer ships
|
without a platform engineer joining a thread; a non-technical consumer ships
|
||||||
a production deployment by declaring intent, without authoring a workflow,
|
a production deployment by declaring intent, without authoring a workflow,
|
||||||
a configuration file, or a Terraform module.
|
a configuration file, or an infrastructure module.
|
||||||
|
|
||||||
- **Vision** (the why): [`docs/vision.md`](docs/vision.md)
|
- **Consumer guide:** [`docs/consumer-guide.md`](docs/consumer-guide.md)
|
||||||
- **Architecture** (the how): [`docs/architecture.md`](docs/architecture.md) + [`.ciagent/ARCHITECTURE.md`](.ciagent/ARCHITECTURE.md)
|
- **Modules:** [`docs/modules/`](docs/modules/)
|
||||||
- **Decisions**: [`.ciagent/PROJECT.md`](.ciagent/PROJECT.md)
|
- **Contracts:** [`docs/contracts/`](docs/contracts/)
|
||||||
- **Phase plan**: [`.ciagent/ROADMAP.md`](.ciagent/ROADMAP.md)
|
- **Pipeline:** [`docs/pipeline/`](docs/pipeline/)
|
||||||
|
- **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)
|
||||||
|
|
||||||
## Status
|
## Repository roles
|
||||||
|
|
||||||
- **v1.2 (active):** platform hardening + first real consumer deployment.
|
There are two kinds of repository in the ACDL model:
|
||||||
Harden the v1.1 spike's NFRs, simplify the setup, rewrite the docs, and
|
|
||||||
prove the platform delivers real value by deploying a basic microservice
|
- **Platform repo (this one).** This is the **source code of the platform**.
|
||||||
to AWS ECS Fargate end-to-end (`terraform apply`, dev autonomous). Ship
|
It owns `modules/`, `adapters/`, `core/`, `schemas/`, `pipelines/`,
|
||||||
tag `v1.3.0`.
|
`scripts/`, and the reusable workflow files. Platform engineers work here.
|
||||||
- **v1.1 (complete, tag `v1.2.0`):** architecture finalization + v1 spike.
|
A **consumer never clones it.**
|
||||||
Finalized the architecture to v1.0 (resolved all 11 open design
|
- **Consumer repo (yours).** A consumer repo contains only:
|
||||||
decisions) and proved the IR commitments hold with one end-to-end spike
|
1. **Its application code** — the service or site being deployed.
|
||||||
(`l1-s3` + `l2-static-asset` + Terraform adapter → real `terraform plan`
|
2. **One or more contracts** — small YAML files at `.acdl/contract.yaml`
|
||||||
against AWS). Gitea release id 202.
|
that reference the central pipeline, name a module, select an
|
||||||
- **v1.0 demo (complete, archived under `demo/`, tag `v1.1.0`):** the
|
environment, and supply module-specific inputs.
|
||||||
30-minute stub-driven executive demo. Preserved as the intent reference;
|
3. **One or more CI definitions** — thin `.github/workflows/*.yml` files
|
||||||
it is not the platform.
|
that `uses:` the central reusable deploy workflow, pointing at the
|
||||||
|
appropriate environment + contract.
|
||||||
|
|
||||||
|
The consumer does not write infrastructure modules, workflow YAML beyond
|
||||||
|
the thin `uses:` wrapper, or adapter code — they write a contract YAML
|
||||||
|
file and the platform does the rest.
|
||||||
|
|
||||||
|
The rest of this README describes the **platform repo** (how the platform
|
||||||
|
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).
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
A referenceable list of what the platform provides today, for consumers and
|
||||||
|
platform engineers alike:
|
||||||
|
|
||||||
|
- **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.
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
|
||||||
|
Planned future features (no dates; tracked in the internal roadmap):
|
||||||
|
|
||||||
|
- **Dynamic module creation from a contract** — an agentic flow where a
|
||||||
|
consumer creates a module directly from the contract file (the
|
||||||
|
"composition" mechanism, redesigned).
|
||||||
|
- **Compliance milestone** — per-module compliance extension points (GDPR,
|
||||||
|
SOX, SOC2, HIPAA, DORA) wired into the pipeline.
|
||||||
|
- **Additional substrate adapters** — beyond the Terraform adapter.
|
||||||
|
- **Environment self-service** — a consumer-facing flow to request and
|
||||||
|
provision a new platform-managed environment (today it is a platform-team
|
||||||
|
action).
|
||||||
|
- **HITL gates for qa / prod / dr** — human attestation + higher confidence
|
||||||
|
thresholds for higher environments.
|
||||||
|
- **OIDC for all platform runners** — zero-trust credentials everywhere.
|
||||||
|
|
||||||
## How the platform works
|
## How the platform works
|
||||||
|
|
||||||
The platform is **four layers + six cross-cutting concerns**, bound by the
|
The platform is **four layers + six cross-cutting concerns**, bound by the
|
||||||
vision's "Two Consumer Surfaces, One Platform" tenet: technical developers
|
vision's "Two Consumer Surfaces, One Platform" tenet: consumers declare
|
||||||
(L3A) and non-technical consumers (L3B) converge on the same contract
|
intent via a contract; the platform delivers the deployment through the
|
||||||
schema, the same policy envelope, and the same evidence stream.
|
same contract schema, the same policy envelope, and the same evidence
|
||||||
|
stream.
|
||||||
|
|
||||||
### The v1.1 spike flow (end-to-end)
|
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)
|
||||||
contracts/spike.yaml
|
|
||||||
│ (contract schema validation)
|
```mermaid
|
||||||
▼
|
flowchart TD
|
||||||
acdl_platform/contract_resolver.py ──▶ Target Stack IR (JSON)
|
A["consumer contract<br/>(uses + module + environment + inputs)"] --> B
|
||||||
│ (IR schema validation)
|
B["schema validation<br/>(contract schema)"] --> C
|
||||||
▼
|
C["resolve to Target Stack<br/>(contract resolver)"] --> D
|
||||||
adapters/terraform/adapter.py ──▶ terraform/spike/{main,terraform,providers}.tf
|
D["security checks<br/>(adapter)"] --> E
|
||||||
│ (the only substrate-specific code)
|
E["infrastructure plan<br/>(adapter compiles the stack)"] --> F
|
||||||
▼
|
F["policy checks<br/>(adapter -> PolicyCheckResult records)"] --> G
|
||||||
terraform plan (real AWS, via the rotated spike key — D-039/D-047)
|
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)"]
|
||||||
adapters/terraform/policy/checkov_adapter.py ──▶ PolicyCheckResult (JSON list)
|
|
||||||
│ (normalized, engine-agnostic)
|
|
||||||
▼
|
|
||||||
acdl_platform/confidence_signal.py ──▶ { score, band, perInput, reasonCodes }
|
|
||||||
│ (6 inputs: policy, validation, freshness, source, history, nfrs)
|
|
||||||
▼
|
|
||||||
acdl_platform/outbox_writer.py ──▶ DynamoDB outbox (acdl-outbox)
|
|
||||||
│ (hash-chained evidence event)
|
|
||||||
▼
|
|
||||||
acdl-evidence timeline (acdl-evidence repo, raw-file served)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The spike validates the architecture's claim that the **IR-shaped
|
The platform validates the architecture's claim that the **stack
|
||||||
commitments do not require a polyglot mess**: the adapter is the only
|
commitments do not require a polyglot mess**: the adapter is the only
|
||||||
substrate-specific code. `modules-ir/`, `schemas/`, `contracts/`,
|
substrate-specific code. `modules/`, `schemas/`, `contracts/`,
|
||||||
`acdl_platform/confidence_signal.py`, `acdl_platform/contract_resolver.py`,
|
`core/confidence_signal.py`, `core/contract_resolver.py`, and
|
||||||
and `acdl_platform/outbox_writer.py` are all substrate-agnostic (no
|
`core/outbox_writer.py` are all substrate-agnostic (no `aws_s3_bucket` /
|
||||||
`aws_s3_bucket` / `aws_` Terraform terms).
|
`aws_` infrastructure terms).
|
||||||
|
|
||||||
### What's different in v1.2
|
|
||||||
|
|
||||||
v1.2 extends the spike to a real, simpler, better-documented platform that
|
|
||||||
**deploys a microservice to ECS Fargate**:
|
|
||||||
|
|
||||||
- Six new IR-typed L1s: `l1-vpc`, `l1-ecs-cluster`, `l1-ecs-service`,
|
|
||||||
`l1-iam-role`, `l1-alb`, `l1-ecr`.
|
|
||||||
- One new L2 thin-composition: `l2-microservice` (references the six L1s).
|
|
||||||
- `terraform apply` (dev, autonomous per §10, confidence ≥ 0.50) — real
|
|
||||||
provisioning, not just `plan`.
|
|
||||||
- A new consumer repo `acdl-consumer-microservice` with a basic HTTP
|
|
||||||
container + Dockerfile + ECR push + contract submission.
|
|
||||||
- One `scripts/run_platform.sh` (consolidated from the v1.1 spike scripts).
|
|
||||||
- NFR hardening: least-privilege IAM (expanded for ECS), idempotent
|
|
||||||
bootstrap, proper error handling, P1-1 redaction.
|
|
||||||
|
|
||||||
## How to run
|
## How to run
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
- AWS account + the rotated spike key in `.env.secrets` (see
|
> These prerequisites are for running the **platform repo** locally. A
|
||||||
`scripts/rotate_spike_key.sh`; the bootstrap root key was deactivated
|
> consumer does not need any of these — see the
|
||||||
per D-034 closure).
|
> [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`
|
- `terraform` (pin `1.9.*`), `checkov` (pin `>=3.2,<4`), `python3` + `boto3`
|
||||||
+ `jsonschema`.
|
+ `jsonschema`.
|
||||||
|
|
||||||
### Run the platform pipeline end-to-end
|
### Run the platform pipeline end-to-end
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Bootstrap the AWS state backend + spike IAM user (one-time, idempotent)
|
# 1. Bootstrap the AWS state backend + runner IAM user (one-time, idempotent)
|
||||||
# (requires the bootstrap root key in env — now deactivated; skip if
|
# (requires the bootstrap root key in env — skip if the state bucket +
|
||||||
# the state bucket + acdl-spike-runner already exist)
|
# acdl-spike-runner already exist)
|
||||||
ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID=... ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY=... \
|
ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID=... ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY=... \
|
||||||
python3 terraform/bootstrap/create_state_backend.py
|
python3 terraform/bootstrap/create_state_backend.py
|
||||||
ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID=... ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY=... \
|
ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID=... ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY=... \
|
||||||
python3 terraform/bootstrap/create_iam_user.py # prints the initial key
|
python3 terraform/bootstrap/create_iam_user.py # prints the initial key
|
||||||
|
|
||||||
# 2. Rotate the spike key (writes .env.secrets, gitignored)
|
# 2. Rotate the runner key (writes .env.secrets, gitignored)
|
||||||
ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID=... ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY=... \
|
ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID=... ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY=... \
|
||||||
bash scripts/rotate_spike_key.sh
|
bash scripts/rotate_spike_key.sh
|
||||||
|
|
||||||
# 3. Run the full platform pipeline (IR -> adapter -> plan -> Checkov ->
|
# 3. Run the full platform pipeline (contract -> environment check -> stack ->
|
||||||
# confidence -> outbox)
|
# adapter -> security checks -> infrastructure plan -> policy checks ->
|
||||||
bash scripts/run_platform.sh
|
# confidence -> evidence event -> apply). Output is streamed to stdout.
|
||||||
|
bash scripts/run_platform.sh contracts/static-asset.yaml
|
||||||
# Expected: "=== PLATFORM E2E OK ==="
|
# Expected: "=== PLATFORM E2E OK ==="
|
||||||
|
|
||||||
# Or plan-only (IR -> adapter -> terraform plan; no Checkov/outbox):
|
# Or plan-only (contract -> stack -> adapter -> infrastructure plan; no
|
||||||
bash scripts/run_platform.sh --plan-only
|
# policy checks / outbox):
|
||||||
|
bash scripts/run_platform.sh --plan-only contracts/static-asset.yaml
|
||||||
|
|
||||||
|
# Add --quiet to suppress streaming (output to log files only):
|
||||||
|
bash scripts/run_platform.sh --quiet contracts/static-asset.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
### Test the platform (offline, no AWS required)
|
### Test the platform (offline, no AWS required)
|
||||||
@@ -125,68 +172,142 @@ bash scripts/run_platform.sh --plan-only
|
|||||||
# Install test dependencies
|
# Install test dependencies
|
||||||
pip install -r requirements-test.txt
|
pip install -r requirements-test.txt
|
||||||
|
|
||||||
# Run the test suite (90 tests, all offline — uses moto for DynamoDB mocking)
|
# Run the test suite (all offline — uses moto for DynamoDB mocking)
|
||||||
python3 -m pytest tests/ -v
|
python3 -m pytest tests/ -v
|
||||||
|
|
||||||
# Run the platform in check-only mode (offline — no AWS, no Checkov, no outbox)
|
# Run the platform in check-only mode (offline — no AWS, no policy checks,
|
||||||
|
# no outbox). Uses the default sample contract (contracts/static-asset.yaml)
|
||||||
|
# and the sample dev environment (core/environments/dev.json).
|
||||||
bash scripts/run_platform.sh --check-only
|
bash scripts/run_platform.sh --check-only
|
||||||
# Expected: "=== PLATFORM CHECK OK ==="
|
# Expected: "=== PLATFORM CHECK OK ==="
|
||||||
|
|
||||||
|
# Reproduce the full CI pipeline locally (lint -> test -> check-only)
|
||||||
|
bash scripts/run_ci.sh
|
||||||
|
# Expected: "=== CI PIPELINE OK ==="
|
||||||
```
|
```
|
||||||
|
|
||||||
### CI/CD pipelines
|
### CI/CD pipelines
|
||||||
|
|
||||||
Identical pipelines run on both Gitea Actions (dev) and GitHub Actions
|
The CI/CD pipeline is defined by a **central pipeline contract** — a
|
||||||
(production):
|
declarative YAML instance (`pipelines/ci.yaml`) validated against a JSON
|
||||||
|
Schema (`schemas/pipeline.schema.json`). Both platform-runner workflows
|
||||||
|
implement the same contract:
|
||||||
|
|
||||||
- `.gitea/workflows/ci.yml` — Gitea Actions (dev environment)
|
|
||||||
- `.github/workflows/ci.yml` — GitHub Actions (production)
|
- `.github/workflows/ci.yml` — GitHub Actions (production)
|
||||||
|
|
||||||
Both run three stages: **lint** (py_compile), **test** (pytest), and
|
Both run three stages: **lint** (py_compile), **test** (pytest), and
|
||||||
**check-only** (`run_platform.sh --check-only`). Both trigger on push to
|
**check-only** (`run_platform.sh --check-only`). Both trigger on push to
|
||||||
`main` and on pull requests.
|
`main` and on pull requests. A test (`tests/test_pipeline_contract.py`)
|
||||||
|
validates that the workflow conforms to the contract.
|
||||||
|
|
||||||
### Re-run the archived v1.0 demo (stubs only, no AWS)
|
`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
|
||||||
bash demo/scripts/run_demo.sh --no-upload
|
bash scripts/run_ci.sh # run all 3 stages (lint, test, check-only)
|
||||||
|
bash scripts/run_ci.sh --quiet # suppress per-stage banners
|
||||||
```
|
```
|
||||||
|
|
||||||
The demo deck is at [`demo/ACDL_DEMO.md`](demo/ACDL_DEMO.md). It runs
|
### Reusable deploy workflow
|
||||||
entirely on local stubs — no AWS, no AI — and shows intent and safety
|
|
||||||
behavior rather than provisioning real cloud resources.
|
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.4`). 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-asset` is the worked example.
|
||||||
|
|
||||||
## Repository layout
|
## Repository layout
|
||||||
|
|
||||||
| Path | Purpose | Status |
|
| Path | Purpose | Status |
|
||||||
|------|---------|--------|
|
|------|---------|--------|
|
||||||
| `acdl_platform/` | Platform code: confidence signal, contract resolver, outbox writer, HITL/ledger/SoD designs (renamed from `platform/` in Phase 08 to avoid shadowing the stdlib `platform` module) | v1.1 complete; v1.2 extends |
|
| `core/` | Platform code: contract resolver, confidence signal, outbox writer, environment check, environments, separation of duties, HITL/ledger designs | active |
|
||||||
| `schemas/` | JSON Schemas: IR, PolicyCheckResult, contract (draft 2020-12) | v1.1 complete; v1.2 extends contract schema |
|
| `schemas/` | JSON Schemas: stack, contract, PolicyCheckResult, pipeline contract, deploy pipeline contract (draft 2020-12) | active |
|
||||||
| `adapters/` | Substrate adapters — Terraform adapter (the only substrate-specific code per §12) + Checkov policy adapter | v1.1 complete; v1.2 expands `TYPE_MAP` |
|
| `pipelines/` | Central pipeline contracts: `ci.yaml` (CI), `deploy.yaml` (deployment) | active |
|
||||||
| `terraform/` | State backend (S3 + DynamoDB) + spike TF (`terraform/spike/`) + bootstrap scripts (`terraform/bootstrap/`) | v1.1 complete; v1.2 adds ECS apply |
|
| `adapters/` | Substrate adapters — the substrate adapter (the only substrate-specific code per §12) + the policy adapter | active |
|
||||||
| `modules-ir/` | IR-typed L1/L2 modules + `registry.json`. v1.1: `l1-s3`, `l2-static-asset`. v1.2: + 6 ECS L1s, `l2-microservice` | v1.1 complete; v1.2 expands |
|
| `terraform/` | State backend (S3 + DynamoDB) + platform TF (`terraform/spike/`) + bootstrap scripts (`terraform/bootstrap/`) | active |
|
||||||
| `contracts/` | Sample contracts (`spike.yaml` for `l2-static-asset`) | v1.1 complete; v1.2 adds `microservice.yaml` |
|
| `modules/` | Primitives + modules + `registry.json`. Primitives: s3, vpc, ecs-cluster, ecs-service, iam-role, alb, ecr. Modules: microservice, static-asset | active |
|
||||||
| `scripts/` | Verify scripts (`verify_phaseNN.sh`), platform run script (`run_platform.sh`; `--plan-only` for plan subset), key rotation | v1.1 complete; v1.2 consolidates |
|
| `contracts/` | Sample consumer contracts (e.g. `static-asset.yaml`) | active |
|
||||||
| `demo/` | Archived v1.0 executive demo (tag `v1.1.0`); runs locally via `demo/scripts/run_demo.sh --no-upload` | complete (archived) |
|
| `scripts/` | Platform run script (`run_platform.sh` with `--check-only`/`--plan-only`/`--quiet`), CI pipeline script (`run_ci.sh`), key rotation | active |
|
||||||
| `.ciagent/` | CIAgent metadata (config, project, architecture, requirements, roadmap, personas, plans, research, verify, review, audit) | active |
|
| `tests/` | Pytest suite (all offline — adapter, confidence signal, policy adapter, outbox writer, pipeline contract, contract resolver, streaming, environment check) | active |
|
||||||
| `docs/` | Upstream vision + architecture sources (`vision.md`, `architecture.md`) | 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 |
|
||||||
|
|
||||||
## Environments
|
## Credentials & zero-trust
|
||||||
|
|
||||||
| Environment | Autonomy | Gate | Status |
|
### Default — zero-trust OIDC + attribute-based authorization
|
||||||
|---|---|---|---|
|
|
||||||
| dev | Full autonomy (no HITL) | Confidence ≥ 0.50 | v1.1 spike (`plan`); v1.2 (`apply`) |
|
|
||||||
| qa | Held for attestation | QA HITL + confidence ≥ 0.75 | v1.3+ |
|
|
||||||
| prod | Held for attestation | SRE HITL + confidence ≥ 0.90 | v1.3+ |
|
|
||||||
| dr | Held for attestation | SRE HITL + confidence ≥ 0.95 + dr-drill | v1.3+ |
|
|
||||||
|
|
||||||
**Staging does not exist** (Path A locked).
|
Consumer repos are **zero-trust**: they hold **no long-lived AWS keys** and
|
||||||
|
no static credentials in repo secrets.
|
||||||
|
|
||||||
## Credentials
|
- **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**.
|
||||||
|
|
||||||
**Long-lived AWS credentials are forbidden** (§12.5). The v1.1 spike uses a
|
The effect: a consumer's pipeline can only view and update the resources
|
||||||
temporary long-lived key **once** to bootstrap (waiver D-034, now closed —
|
it created. Blast radius is contained to that consumer's own stack
|
||||||
the root key was deactivated by the user), then rotates the spike key
|
instances — one consumer can never touch another consumer's resources,
|
||||||
per-run via `scripts/rotate_spike_key.sh` (waiver D-039, extended for v1.2
|
and the consumer cannot escape its own scope.
|
||||||
as D-047). Real OIDC federation is deferred to v1.3+, blocked on
|
|
||||||
[go-gitea/gitea#36988](https://github.com/go-gitea/gitea/pull/36988) (still
|
### Alternative — static AWS key
|
||||||
open as of 2026-07-21).
|
|
||||||
|
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).
|
||||||
@@ -1,19 +1,19 @@
|
|||||||
"""ACDL Terraform adapter — compile a Target Stack IR instance to Terraform.
|
"""ACDL Terraform adapter — compile a Target Stack instance to Terraform.
|
||||||
|
|
||||||
ARCHITECTURE.md §12.2: the adapter translates the IR-typed L1 interface
|
ARCHITECTURE.md §12.2: the adapter translates the stack-typed L1 interface
|
||||||
to a Terraform variable/output block, the L2 thin-composition tree to a
|
to a Terraform variable/output block, the L2 composition tree to a
|
||||||
root module that calls the L1 modules, the IR-typed relationships to
|
root module that calls the L1 modules, the stack-typed relationships to
|
||||||
Terraform module references, and emits a Terraform plan from the IR.
|
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
|
The adapter is a THIN LAYER; it does not own L1/L2 content — it only
|
||||||
translates. Substrate-agnostic in, Terraform out.
|
translates. Substrate-agnostic in, Terraform out.
|
||||||
|
|
||||||
Phase 09 spike: handled one L1 (l1-s3, IR type aws:s3:bucket).
|
Phase 09 spike: handled one L1 (s3, stack type aws:s3:bucket).
|
||||||
Phase 13: generalized the resource/output emission via TYPE_MAP +
|
Phase 13: generalized the resource/output emission via TYPE_MAP +
|
||||||
INPUT_MAP + OUTPUT_MAP tables; added ECS Fargate IR types. S3 behavior
|
INPUT_MAP + OUTPUT_MAP tables; added ECS Fargate stack types. S3 behavior
|
||||||
is preserved (regression baseline: modules-ir/l1/l1-s3/spike_instance.json).
|
is preserved (regression baseline: modules/l1/s3/instance.json).
|
||||||
|
|
||||||
CLI: adapter.py <ir_instance.json> <out_dir>
|
CLI: adapter.py <instance.json> <out_dir>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
@@ -21,8 +21,8 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
|
||||||
# IR type -> Terraform resource type. The only substrate-specific table.
|
# Stack type -> Terraform resource type. The only substrate-specific table.
|
||||||
# As more L1s land, this grows; the L1 content + IR do not change.
|
# As more L1s land, this grows; the L1 content + stack do not change.
|
||||||
TYPE_MAP = {
|
TYPE_MAP = {
|
||||||
"aws:s3:bucket": "aws_s3_bucket",
|
"aws:s3:bucket": "aws_s3_bucket",
|
||||||
"aws:ec2:vpc": "aws_vpc",
|
"aws:ec2:vpc": "aws_vpc",
|
||||||
@@ -38,8 +38,8 @@ TYPE_MAP = {
|
|||||||
"aws:ecr:repository": "aws_ecr_repository",
|
"aws:ecr:repository": "aws_ecr_repository",
|
||||||
}
|
}
|
||||||
|
|
||||||
# IR input name -> Terraform arg name, per IR type. Only non-identity
|
# Stack input name -> Terraform arg name, per stack type. Only non-identity
|
||||||
# mappings are listed; any input not present here uses the IR name as
|
# mappings are listed; any input not present here uses the stack name as
|
||||||
# the Terraform arg name (identity).
|
# the Terraform arg name (identity).
|
||||||
INPUT_MAP = {
|
INPUT_MAP = {
|
||||||
"aws:s3:bucket": {"bucket_name": "bucket"},
|
"aws:s3:bucket": {"bucket_name": "bucket"},
|
||||||
@@ -56,9 +56,9 @@ INPUT_MAP = {
|
|||||||
"aws:ecr:repository": {},
|
"aws:ecr:repository": {},
|
||||||
}
|
}
|
||||||
|
|
||||||
# IR output name -> Terraform attribute name, per IR type. Only
|
# Stack output name -> Terraform attribute name, per stack type. Only
|
||||||
# non-identity mappings are listed; any output not present here uses the
|
# non-identity mappings are listed; any output not present here uses the
|
||||||
# IR name as the Terraform attribute name (identity).
|
# stack name as the Terraform attribute name (identity).
|
||||||
OUTPUT_MAP = {
|
OUTPUT_MAP = {
|
||||||
"aws:s3:bucket": {"bucket_arn": "arn", "bucket_name": "id"},
|
"aws:s3:bucket": {"bucket_arn": "arn", "bucket_name": "id"},
|
||||||
"aws:ec2:vpc": {"vpc_id": "id"},
|
"aws:ec2:vpc": {"vpc_id": "id"},
|
||||||
@@ -101,24 +101,24 @@ def _tf_value(value):
|
|||||||
|
|
||||||
|
|
||||||
def _ref_expr(ref_value, type_by_id):
|
def _ref_expr(ref_value, type_by_id):
|
||||||
"""Translate a "ref:<ir_resource_id>.<output>" string to a Terraform
|
"""Translate a "ref:<stack_resource_id>.<output>" string to a Terraform
|
||||||
interpolation "${<tf_type>.<id>.<attr>}".
|
interpolation "${<tf_type>.<id>.<attr>}".
|
||||||
|
|
||||||
<ir_resource_id> is the IR resource id of the producing resource;
|
<stack_resource_id> is the stack resource id of the producing resource;
|
||||||
<output> is the per-resource output name (e.g. `subnet_id`,
|
<output> is the per-resource output name (e.g. `subnet_id`,
|
||||||
`cluster_arn`); the attribute is mapped through OUTPUT_MAP for the
|
`cluster_arn`); the attribute is mapped through OUTPUT_MAP for the
|
||||||
referenced resource's IR type. The resolver emits the ref using the
|
referenced resource's stack type. The resolver emits the ref using the
|
||||||
IR resource id directly (not the child id), so no child->resource
|
stack resource id directly (not the child id), so no child->resource
|
||||||
lookup table is needed here.
|
lookup table is needed here.
|
||||||
"""
|
"""
|
||||||
body = ref_value[len("ref:"):]
|
body = ref_value[len("ref:"):]
|
||||||
rid, out_name = body.split(".", 1)
|
rid, out_name = body.split(".", 1)
|
||||||
rtype = type_by_id.get(rid)
|
rtype = type_by_id.get(rid)
|
||||||
if not rtype:
|
if not rtype:
|
||||||
raise ValueError(f"ref to unknown IR resource id {rid!r}")
|
raise ValueError(f"ref to unknown stack resource id {rid!r}")
|
||||||
tf_type = TYPE_MAP.get(rtype)
|
tf_type = TYPE_MAP.get(rtype)
|
||||||
if not tf_type:
|
if not tf_type:
|
||||||
raise ValueError(f"ref target {rid!r} has unknown IR type {rtype!r}")
|
raise ValueError(f"ref target {rid!r} has unknown stack type {rtype!r}")
|
||||||
out_map = OUTPUT_MAP.get(rtype, {})
|
out_map = OUTPUT_MAP.get(rtype, {})
|
||||||
tf_attr = out_map.get(out_name, out_name)
|
tf_attr = out_map.get(out_name, out_name)
|
||||||
return f"{tf_type}.{rid}.{tf_attr}"
|
return f"{tf_type}.{rid}.{tf_attr}"
|
||||||
@@ -139,7 +139,7 @@ def _emit_resource(resource, type_by_id=None):
|
|||||||
rid = resource["id"]
|
rid = resource["id"]
|
||||||
tf_type = TYPE_MAP.get(rtype)
|
tf_type = TYPE_MAP.get(rtype)
|
||||||
if not tf_type:
|
if not tf_type:
|
||||||
raise ValueError(f"unknown IR type {rtype!r} (adapter TYPE_MAP has no entry)")
|
raise ValueError(f"unknown stack type {rtype!r} (adapter TYPE_MAP has no entry)")
|
||||||
in_map = INPUT_MAP.get(rtype, {})
|
in_map = INPUT_MAP.get(rtype, {})
|
||||||
body = []
|
body = []
|
||||||
inputs = resource.get("inputs", {})
|
inputs = resource.get("inputs", {})
|
||||||
@@ -306,11 +306,11 @@ def _emit_output(output_name, value_expr):
|
|||||||
return f'output "{output_name}" {{\n value = {value_expr}\n}}\n'
|
return f'output "{output_name}" {{\n value = {value_expr}\n}}\n'
|
||||||
|
|
||||||
|
|
||||||
def adapt(ir_instance, out_dir):
|
def adapt(stack_instance, out_dir):
|
||||||
"""Emit main.tf + terraform.tf + providers.tf to out_dir for the IR instance."""
|
"""Emit main.tf + terraform.tf + providers.tf to out_dir for the stack instance."""
|
||||||
os.makedirs(out_dir, exist_ok=True)
|
os.makedirs(out_dir, exist_ok=True)
|
||||||
stack = ir_instance["stack"]
|
stack = stack_instance["stack"]
|
||||||
resources = ir_instance["resources"]
|
resources = stack_instance["resources"]
|
||||||
|
|
||||||
# --- providers.tf: aws provider, region from the first resource's inputs.region ---
|
# --- providers.tf: aws provider, region from the first resource's inputs.region ---
|
||||||
region = "us-east-1"
|
region = "us-east-1"
|
||||||
@@ -345,9 +345,9 @@ def adapt(ir_instance, out_dir):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# --- main.tf: resources + outputs ---
|
# --- main.tf: resources + outputs ---
|
||||||
# Build an IR-resource-id -> IR-type table so `ref:` input values can
|
# Build a stack-resource-id -> stack-type table so `ref:` input values can
|
||||||
# be resolved to Terraform interpolations without a child->resource
|
# be resolved to Terraform interpolations without a child->resource
|
||||||
# lookup (the resolver emits refs with the IR resource id directly).
|
# lookup (the resolver emits refs with the stack resource id directly).
|
||||||
type_by_id = {r["id"]: r["type"] for r in resources}
|
type_by_id = {r["id"]: r["type"] for r in resources}
|
||||||
main_tf_parts = []
|
main_tf_parts = []
|
||||||
has_vpc = any(r["type"] == "aws:ec2:vpc" for r in resources)
|
has_vpc = any(r["type"] == "aws:ec2:vpc" for r in resources)
|
||||||
@@ -376,9 +376,9 @@ def adapt(ir_instance, out_dir):
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
if len(sys.argv) != 3:
|
if len(sys.argv) != 3:
|
||||||
print("usage: adapter.py <ir_instance.json> <out_dir>", file=sys.stderr)
|
print("usage: adapter.py <instance.json> <out_dir>", file=sys.stderr)
|
||||||
sys.exit(2)
|
sys.exit(2)
|
||||||
with open(sys.argv[1], "r") as fh:
|
with open(sys.argv[1], "r") as fh:
|
||||||
ir = json.load(fh)
|
stack = json.load(fh)
|
||||||
adapt(ir, sys.argv[2])
|
adapt(stack, sys.argv[2])
|
||||||
print(f"adapter: emitted terraform to {sys.argv[2]}", file=sys.stderr)
|
print(f"adapter: emitted terraform to {sys.argv[2]}", file=sys.stderr)
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
A basic HTTP microservice for the ACDL v1.2 milestone. Returns 200 on `/`
|
A basic HTTP microservice for the ACDL v1.2 milestone. Returns 200 on `/`
|
||||||
and `/health` with a JSON status body. Deployed to AWS ECS Fargate via the
|
and `/health` with a JSON status body. Deployed to AWS ECS Fargate via the
|
||||||
ACDL platform's `l2-microservice` contract.
|
ACDL platform's `microservice` contract.
|
||||||
|
|
||||||
## Build + push to ECR
|
## Build + push to ECR
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
This is the reference consumer microservice for the v1.2 milestone. It's
|
This is the reference consumer microservice for the v1.2 milestone. It's
|
||||||
intentionally minimal: stdlib only, no framework, no dependencies. The
|
intentionally minimal: stdlib only, no framework, no dependencies. The
|
||||||
platform deploys it to ECS Fargate via the l2-microservice contract.
|
platform deploys it to ECS Fargate via the microservice contract.
|
||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# ACDL sample consumer contract — static-asset 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.
|
||||||
|
|
||||||
|
uses: acdl/pipelines/deploy.yaml@v1.4
|
||||||
|
module: static-asset
|
||||||
|
environment: dev
|
||||||
|
inputs:
|
||||||
|
bucket_name: acdl-spike-bucket
|
||||||
|
region: us-east-1
|
||||||
@@ -7,7 +7,7 @@ instinct is not a substitute.
|
|||||||
|
|
||||||
Inputs (weights sum to 1.0, D-040):
|
Inputs (weights sum to 1.0, D-040):
|
||||||
1. policy_results (0.30) — list[PolicyCheckResult] (schemas/policy_check_result.schema.json)
|
1. policy_results (0.30) — list[PolicyCheckResult] (schemas/policy_check_result.schema.json)
|
||||||
2. validation (0.25) — {schema: bool, ir_resolved: bool, tf_validated: bool, tf_planned: bool}
|
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}
|
3. freshness (0.10) — {age_days: float, max_age_days: float}
|
||||||
4. source (0.15) — {submitter: str, commit_sha: str, signed: bool}
|
4. source (0.15) — {submitter: str, commit_sha: str, signed: bool}
|
||||||
5. history (0.10) — {prior_rollbacks: int, prior_policy_fails: int}
|
5. history (0.10) — {prior_rollbacks: int, prior_policy_fails: int}
|
||||||
@@ -83,7 +83,7 @@ def _per_input_score(name: str, raw: Any) -> tuple:
|
|||||||
scores.append(0.0)
|
scores.append(0.0)
|
||||||
return sum(scores) / len(scores), []
|
return sum(scores) / len(scores), []
|
||||||
if name == "validation":
|
if name == "validation":
|
||||||
keys = ("schema", "ir_resolved", "tf_validated", "tf_planned")
|
keys = ("schema", "stack_resolved", "tf_validated", "tf_planned")
|
||||||
if not isinstance(raw, dict):
|
if not isinstance(raw, dict):
|
||||||
return 0.5, []
|
return 0.5, []
|
||||||
trues = sum(1 for k in keys if raw.get(k))
|
trues = sum(1 for k in keys if raw.get(k))
|
||||||
@@ -0,0 +1,262 @@
|
|||||||
|
"""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 sys
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
import jsonschema
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
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:<childId>.<outputName>" string for cross-child references.
|
||||||
|
"""
|
||||||
|
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]
|
||||||
|
return f"ref:{child_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 = {}
|
||||||
|
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)
|
||||||
|
|
||||||
|
# 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"]:
|
||||||
|
resource = {
|
||||||
|
"id": f"{child_id}-{sub_res['type'].split(':')[-1].replace('_', '-')}"
|
||||||
|
if len(child_iface["resources"]) > 1 else child_id,
|
||||||
|
"type": sub_res["type"],
|
||||||
|
"module": child_module,
|
||||||
|
"inputs": {},
|
||||||
|
"outputs": {
|
||||||
|
out: {"type": "string"}
|
||||||
|
for out in sub_res.get("outputs", [])
|
||||||
|
},
|
||||||
|
}
|
||||||
|
resources.append(resource)
|
||||||
|
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)
|
||||||
|
|
||||||
|
# Track outputs for this child
|
||||||
|
child_outputs[child_id] = child_iface.get("outputs", {})
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
# Find the target resource and set the input
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
return stack_instance
|
||||||
|
|
||||||
|
|
||||||
|
def resolve(contract_path, repo_root=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).
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
# Load schemas
|
||||||
|
contract_schema = _load_json(os.path.join(repo_root, "schemas", "contract.schema.json"))
|
||||||
|
|
||||||
|
# Validate contract against schema
|
||||||
|
jsonschema.validate(contract, contract_schema)
|
||||||
|
|
||||||
|
# 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>", file=sys.stderr)
|
||||||
|
sys.exit(2)
|
||||||
|
result = resolve(sys.argv[1])
|
||||||
|
with open(sys.argv[2], "w") as fh:
|
||||||
|
json.dump(result, fh, indent=2)
|
||||||
|
print(f"resolver: resolved {sys.argv[1]} -> {sys.argv[2]}", file=sys.stderr)
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Environment onboarding check.
|
||||||
|
|
||||||
|
Reads a contract's `environment` field and looks up the matching
|
||||||
|
`core/environments/<name>.json`. If no matching file exists, prints a
|
||||||
|
friendly onboarding prompt and exits non-zero, halting the pipeline before
|
||||||
|
any work is done.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 core/environment_check.py <contract.yaml>
|
||||||
|
python3 core/environment_check.py --env dev
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
try:
|
||||||
|
import yaml
|
||||||
|
except ImportError:
|
||||||
|
sys.stderr.write("PyYAML is required (pip install pyyaml)\n")
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
|
||||||
|
def _environments_dir(root=None):
|
||||||
|
if root is None:
|
||||||
|
root = Path(__file__).resolve().parent.parent
|
||||||
|
return Path(root) / "core" / "environments"
|
||||||
|
|
||||||
|
|
||||||
|
def _contract_environment(contract_path):
|
||||||
|
with open(contract_path) as f:
|
||||||
|
contract = yaml.safe_load(f)
|
||||||
|
return contract.get("environment")
|
||||||
|
|
||||||
|
|
||||||
|
def _onboarding_message(env_name):
|
||||||
|
return (
|
||||||
|
"=== ACDL Environment Onboarding ===\n"
|
||||||
|
f"No environment named '{env_name}' is bound to this repository.\n\n"
|
||||||
|
"ACDL environments are platform-managed. The platform provisions on\n"
|
||||||
|
"your behalf:\n"
|
||||||
|
" - an AWS account (or a scoped partition of one)\n"
|
||||||
|
" - a network (VPC + subnets)\n"
|
||||||
|
" - a state backend (an S3 bucket + DynamoDB lock table)\n"
|
||||||
|
" - an IAM role surfaced to your repo via attribute-based\n"
|
||||||
|
" authorization (ABAC)\n\n"
|
||||||
|
"You do not provide an AWS account, VPC, subnet, or state bucket.\n\n"
|
||||||
|
"To request an environment:\n"
|
||||||
|
" 1. Contact the platform team with your repo name + the\n"
|
||||||
|
" environment name you need (e.g. 'dev').\n"
|
||||||
|
" 2. The platform team provisions the account/network/state/role\n"
|
||||||
|
" and binds the environment to your repo.\n"
|
||||||
|
" 3. Your next pipeline run will proceed normally.\n\n"
|
||||||
|
"Expected turnaround: contact the platform team for current SLA.\n"
|
||||||
|
"===================================\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def check(contract_path=None, env_name=None, root=None):
|
||||||
|
"""Return (ok: bool, message: str).
|
||||||
|
|
||||||
|
If env_name is None it is read from the contract at contract_path.
|
||||||
|
ok is True when an environment definition exists; False otherwise.
|
||||||
|
On False, message is the friendly onboarding prompt.
|
||||||
|
"""
|
||||||
|
if env_name is None:
|
||||||
|
if contract_path is None:
|
||||||
|
return (False, "no contract or environment name supplied")
|
||||||
|
env_name = _contract_environment(contract_path)
|
||||||
|
if env_name is None:
|
||||||
|
return (False, "contract has no 'environment' field")
|
||||||
|
|
||||||
|
env_file = _environments_dir(root) / f"{env_name}.json"
|
||||||
|
if env_file.is_file():
|
||||||
|
return (True, f"environment '{env_name}' is bound ({env_file})")
|
||||||
|
return (False, _onboarding_message(env_name))
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv):
|
||||||
|
contract_path = None
|
||||||
|
env_name = None
|
||||||
|
for arg in argv[1:]:
|
||||||
|
if arg.startswith("--env="):
|
||||||
|
env_name = arg.split("=", 1)[1]
|
||||||
|
elif arg.startswith("--"):
|
||||||
|
sys.stderr.write(f"unknown flag: {arg}\n")
|
||||||
|
return 2
|
||||||
|
else:
|
||||||
|
contract_path = arg
|
||||||
|
|
||||||
|
ok, message = check(contract_path=contract_path, env_name=env_name)
|
||||||
|
if ok:
|
||||||
|
print(message)
|
||||||
|
return 0
|
||||||
|
sys.stdout.write(message)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main(sys.argv))
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# Platform-managed environments
|
||||||
|
|
||||||
|
This directory holds environment definitions used by the onboarding scaffold.
|
||||||
|
Each file is a named environment the platform owns (an AWS account or
|
||||||
|
scoped partition, a network, a state backend, and an IAM role surfaced to
|
||||||
|
the consumer via ABAC).
|
||||||
|
|
||||||
|
A consumer never provides an AWS account, VPC, subnet, S3 state bucket, or
|
||||||
|
runner key — the platform manages all of that here.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- `dev.json` — the default dev environment (autonomous, confidence ≥ 0.50).
|
||||||
|
|
||||||
|
## How it is used
|
||||||
|
|
||||||
|
`core/environment_check.py` reads a contract's `environment` field and
|
||||||
|
looks up the matching `<name>.json` in this directory. If no matching file
|
||||||
|
exists, the check prints a friendly onboarding prompt and exits non-zero,
|
||||||
|
halting the pipeline before any work is done.
|
||||||
|
|
||||||
|
## Adding an environment
|
||||||
|
|
||||||
|
A new environment is a platform-team action: provision the AWS account /
|
||||||
|
network / state backend / IAM role, then add a `<name>.json` here and bind
|
||||||
|
it to the consumer repo. Self-service environment provisioning is on the
|
||||||
|
roadmap; today it is a platform-team action.
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "dev",
|
||||||
|
"description": "Default platform-managed dev environment for onboarding demos.",
|
||||||
|
"account_id": "000000000000",
|
||||||
|
"region": "us-east-1",
|
||||||
|
"state_backend": {
|
||||||
|
"bucket": "acdl-dev-state",
|
||||||
|
"lock_table": "acdl-dev-locks"
|
||||||
|
},
|
||||||
|
"network": {
|
||||||
|
"vpc_cidr": "10.0.0.0/16",
|
||||||
|
"azs": ["us-east-1a", "us-east-1b"]
|
||||||
|
},
|
||||||
|
"runner_role_arn": "arn:aws:iam::000000000000:role/acdl-dev-runner",
|
||||||
|
"autonomy": "full",
|
||||||
|
"confidence_threshold": 0.50
|
||||||
|
}
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
# ACDL pipeline workflow (Phase 04 implementation).
|
|
||||||
#
|
|
||||||
# 3-dispatch approval-gate topology (D-027 / D-028; ARCHITECTURE.md
|
|
||||||
# "Phase 04 pipeline topology"):
|
|
||||||
#
|
|
||||||
# Dispatch 1 (initial): approve_qa=false, approve_prod=false
|
|
||||||
# -> runs the `dev` job (policy check, confidence
|
|
||||||
# gate, mock_executor, evidence + finalize).
|
|
||||||
# Dispatch 2 (QA approve): approve_qa=true, approve_prod=false
|
|
||||||
# -> runs the `qa-gate` job (records QA approval
|
|
||||||
# in the audit chain via evidence_writer +
|
|
||||||
# finalize_evidence).
|
|
||||||
# Dispatch 3 (Prod approve): approve_prod=true
|
|
||||||
# -> runs the `prod-gate` job, then the `finalize`
|
|
||||||
# job (needs: prod-gate) which writes the final
|
|
||||||
# evidence event and commits audit.json to
|
|
||||||
# acdl-evidence.
|
|
||||||
#
|
|
||||||
# Gitea Actions limitations driving this design:
|
|
||||||
# - No `repository_dispatch` trigger (D-014).
|
|
||||||
# - No environments API / `environment:` blocks are ignored (D-013).
|
|
||||||
# - Re-dispatch starts a NEW run; artifacts do NOT survive between runs,
|
|
||||||
# so state is persisted to acdl-evidence via the file-contents API
|
|
||||||
# (D-028 / finalize_evidence.py) instead of via artifacts.
|
|
||||||
#
|
|
||||||
# Branch-pin rule (ARCHITECTURE.md "Branch pinning rule"):
|
|
||||||
# This workflow lives on `acdl`'s default branch `milestone/v1.0-initial`.
|
|
||||||
# Cross-repo `uses:` references (e.g. the issue-trigger's checkout of
|
|
||||||
# l3b_agent_stub.py) MUST pin to `@milestone/v1.0-initial`, NOT `@main`
|
|
||||||
# (the `acdl` repo has no `main` branch). This workflow is invoked via
|
|
||||||
# the workflow_dispatch API (D-014), NOT via `workflow_call`, so the
|
|
||||||
# `uses:` rule applies to the issue-trigger's checkout of the acdl repo,
|
|
||||||
# not to this file itself.
|
|
||||||
name: acdl-pipeline
|
|
||||||
|
|
||||||
"on":
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
contract-ref:
|
|
||||||
description: "Ref on acdl-contracts that carries the contract"
|
|
||||||
required: false
|
|
||||||
type: string
|
|
||||||
default: main
|
|
||||||
approve_qa:
|
|
||||||
description: "Human approval to advance past QA"
|
|
||||||
required: false
|
|
||||||
type: boolean
|
|
||||||
default: false
|
|
||||||
approve_prod:
|
|
||||||
description: "Human approval to advance past Prod"
|
|
||||||
required: false
|
|
||||||
type: boolean
|
|
||||||
default: false
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
dev:
|
|
||||||
name: "Dev (autonomous)"
|
|
||||||
if: inputs.approve_qa != true && inputs.approve_prod != true
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: "Checkout acdl (this repo, pinned to milestone/v1.0-initial)"
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
ref: milestone/v1.0-initial
|
|
||||||
|
|
||||||
- name: "Checkout acdl-contracts at contract-ref"
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
repository: continuous-intelligence/acdl-contracts
|
|
||||||
ref: ${{ inputs.contract-ref }}
|
|
||||||
token: ${{ secrets.GITEA_TOKEN }}
|
|
||||||
path: acdl-contracts
|
|
||||||
|
|
||||||
- name: "Policy check"
|
|
||||||
run: |
|
|
||||||
python3 scripts/policy_checker.py acdl-contracts/contract.yaml
|
|
||||||
|
|
||||||
- name: "Confidence signal"
|
|
||||||
id: confidence
|
|
||||||
run: |
|
|
||||||
set +e
|
|
||||||
SCORE_JSON=$(python3 scripts/confidence_signal.py acdl-contracts/contract.yaml)
|
|
||||||
echo "$SCORE_JSON"
|
|
||||||
echo "score_json=$SCORE_JSON" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
- name: "Apply or reject based on confidence (gate < 0.50)"
|
|
||||||
run: |
|
|
||||||
set +e
|
|
||||||
SCORE=$(python3 -c "import json,sys; print(json.load(sys.stdin)['score'])" <<< '${{ steps.confidence.outputs.score_json }}')
|
|
||||||
python3 -c "import sys; sys.exit(0 if float('${SCORE}') >= 0.50 else 1)"
|
|
||||||
THRESHOLD_RC=$?
|
|
||||||
if [ "$THRESHOLD_RC" -ne 0 ]; then
|
|
||||||
python3 scripts/evidence_writer.py --stage dev --event "dev rejected: confidence < 0.50" --audit audit.json
|
|
||||||
python3 scripts/finalize_evidence.py --audit audit.json
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
STACK=$(python3 -c 'import yaml; print(yaml.safe_load(open("acdl-contracts/contract.yaml"))["stack"])')
|
|
||||||
bash scripts/mock_executor.sh acdl-contracts/contract.yaml
|
|
||||||
python3 scripts/evidence_writer.py --stage dev --event "dev applied: ${STACK}" --audit audit.json
|
|
||||||
python3 scripts/finalize_evidence.py --audit audit.json
|
|
||||||
|
|
||||||
- name: "Upload dev state artifacts (best-effort)"
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: dev-state
|
|
||||||
path: |
|
|
||||||
audit.json
|
|
||||||
state.json
|
|
||||||
|
|
||||||
qa-gate:
|
|
||||||
name: "QA (manual approval)"
|
|
||||||
if: inputs.approve_qa == true && inputs.approve_prod != true
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: "Checkout acdl (this repo, pinned to milestone/v1.0-initial)"
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
ref: milestone/v1.0-initial
|
|
||||||
|
|
||||||
- name: "Record QA approval in evidence"
|
|
||||||
run: |
|
|
||||||
python3 scripts/evidence_writer.py --stage qa --event "qa approved" --audit audit.json
|
|
||||||
python3 scripts/finalize_evidence.py --audit audit.json
|
|
||||||
|
|
||||||
prod-gate:
|
|
||||||
name: "Prod (manual approval)"
|
|
||||||
if: inputs.approve_prod == true
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: "Checkout acdl (this repo, pinned to milestone/v1.0-initial)"
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
ref: milestone/v1.0-initial
|
|
||||||
|
|
||||||
- name: "Record Prod approval in evidence"
|
|
||||||
run: |
|
|
||||||
python3 scripts/evidence_writer.py --stage prod --event "prod approved" --audit audit.json
|
|
||||||
python3 scripts/finalize_evidence.py --audit audit.json
|
|
||||||
|
|
||||||
finalize:
|
|
||||||
name: "Finalize (publish evidence)"
|
|
||||||
needs: [prod-gate]
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: "Checkout acdl (this repo, pinned to milestone/v1.0-initial)"
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
ref: milestone/v1.0-initial
|
|
||||||
|
|
||||||
- name: "Write finalize event + commit audit.json to acdl-evidence"
|
|
||||||
run: |
|
|
||||||
python3 scripts/evidence_writer.py --stage finalize --event "pipeline complete: audit.json committed to acdl-evidence" --audit audit.json
|
|
||||||
python3 scripts/finalize_evidence.py --audit audit.json
|
|
||||||
@@ -1,368 +0,0 @@
|
|||||||
---
|
|
||||||
marp: true
|
|
||||||
theme: default
|
|
||||||
paginate: true
|
|
||||||
size: 16:9
|
|
||||||
header: 'ACDL · Agentic Cloud Delivery Platform'
|
|
||||||
footer: 'Executive Demo · v1.0'
|
|
||||||
style: |
|
|
||||||
/* S&P Global-inspired palette */
|
|
||||||
:root {
|
|
||||||
--sp-red: #C8102E;
|
|
||||||
--sp-red-dark: #8E0B20;
|
|
||||||
--sp-ink: #1A1A1A;
|
|
||||||
--sp-slate: #4A4A4A;
|
|
||||||
--sp-gray: #6E6E6E;
|
|
||||||
--sp-line: #D6D6D6;
|
|
||||||
--sp-bg: #FFFFFF;
|
|
||||||
--sp-tint: #F4F4F4;
|
|
||||||
}
|
|
||||||
section {
|
|
||||||
font-size: 24px;
|
|
||||||
color: var(--sp-ink);
|
|
||||||
background: var(--sp-bg);
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
|
||||||
padding: 50px 60px;
|
|
||||||
}
|
|
||||||
section.title {
|
|
||||||
text-align: center;
|
|
||||||
background: var(--sp-red);
|
|
||||||
color: #FFFFFF;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
section.title h1 {
|
|
||||||
color: #FFFFFF;
|
|
||||||
font-size: 64px;
|
|
||||||
margin-bottom: 0;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
section.title h2 {
|
|
||||||
color: #FFFFFF;
|
|
||||||
border: none;
|
|
||||||
font-weight: 400;
|
|
||||||
}
|
|
||||||
section.title strong { color: #FFFFFF; }
|
|
||||||
h1 {
|
|
||||||
color: var(--sp-red);
|
|
||||||
font-size: 40px;
|
|
||||||
font-weight: 700;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
h2 {
|
|
||||||
color: var(--sp-red);
|
|
||||||
border-bottom: 3px solid var(--sp-red);
|
|
||||||
padding-bottom: 6px;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
h3 {
|
|
||||||
color: var(--sp-red-dark);
|
|
||||||
font-weight: 600;
|
|
||||||
margin-top: 24px;
|
|
||||||
}
|
|
||||||
ul, ol { color: var(--sp-slate); }
|
|
||||||
li { margin-bottom: 6px; }
|
|
||||||
strong { color: var(--sp-ink); }
|
|
||||||
table {
|
|
||||||
font-size: 18px;
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
margin: 12px 0;
|
|
||||||
}
|
|
||||||
th {
|
|
||||||
background: var(--sp-red);
|
|
||||||
color: #FFFFFF;
|
|
||||||
text-align: left;
|
|
||||||
padding: 10px 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
border: 1px solid var(--sp-red-dark);
|
|
||||||
}
|
|
||||||
td {
|
|
||||||
padding: 8px 12px;
|
|
||||||
border: 1px solid var(--sp-line);
|
|
||||||
color: var(--sp-slate);
|
|
||||||
}
|
|
||||||
tr:nth-child(even) td { background: var(--sp-tint); }
|
|
||||||
pre {
|
|
||||||
font-size: 13px;
|
|
||||||
background: var(--sp-tint);
|
|
||||||
border-left: 4px solid var(--sp-red);
|
|
||||||
padding: 14px 16px;
|
|
||||||
border-radius: 0;
|
|
||||||
color: var(--sp-ink);
|
|
||||||
}
|
|
||||||
code {
|
|
||||||
background: var(--sp-tint);
|
|
||||||
color: var(--sp-red-dark);
|
|
||||||
padding: 1px 5px;
|
|
||||||
border-radius: 2px;
|
|
||||||
font-family: 'Menlo', 'Consolas', monospace;
|
|
||||||
}
|
|
||||||
pre code {
|
|
||||||
background: none;
|
|
||||||
color: var(--sp-ink);
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
blockquote {
|
|
||||||
border-left: 5px solid var(--sp-red);
|
|
||||||
background: var(--sp-tint);
|
|
||||||
padding: 10px 16px;
|
|
||||||
color: var(--sp-slate);
|
|
||||||
font-style: italic;
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
header {
|
|
||||||
color: var(--sp-red);
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
footer {
|
|
||||||
color: var(--sp-gray);
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
section::after {
|
|
||||||
color: var(--sp-red);
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
---
|
|
||||||
|
|
||||||
<!-- _class: title -->
|
|
||||||
|
|
||||||
# ACDL
|
|
||||||
## Agentic Cloud Delivery Platform
|
|
||||||
|
|
||||||
Automatic. Safe. Audited. — in 30 minutes, on stubs.
|
|
||||||
|
|
||||||
v1.0 · GitHub Actions · stub-driven
|
|
||||||
|
|
||||||
<!--
|
|
||||||
30-min exec demo. Emphasize: we prove INTENT and SAFETY, not provision real infra.
|
|
||||||
Everything runs as local stubs on Linux via GitHub Actions. No AWS.
|
|
||||||
-->
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# The Problem
|
|
||||||
|
|
||||||
### Today, deploying infrastructure takes **weeks**
|
|
||||||
|
|
||||||
- Ticket → triage queue → copy-paste config → peer review → security review → waiting for central IT to release
|
|
||||||
|
|
||||||
> Two weeks of human latency for a service that should take minutes.
|
|
||||||
|
|
||||||
### What we want instead
|
|
||||||
|
|
||||||
- Developer commits a **contract** → pipeline runs
|
|
||||||
- Safety **computed** automatically (confidence signal)
|
|
||||||
- Manual gates only where they matter (QA, Prod)
|
|
||||||
- Every step written to a tamper-evident **evidence stream**
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Set the pain. Ask: who has lived this? Then pivot to the vision.
|
|
||||||
-->
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# How It Works
|
|
||||||
|
|
||||||
```
|
|
||||||
┌────────────── acdl-contracts ──────────────┐
|
|
||||||
Developer ──▶│ commit contract.yaml │
|
|
||||||
└───────┬───────────────────────────────────┘
|
|
||||||
│ (push)
|
|
||||||
Citizen ┌─────────┴──────────┐
|
|
||||||
│ "ingest gas pricing into data lake"
|
|
||||||
▼
|
|
||||||
Claude agent ──▶ contract.yaml ─┘
|
|
||||||
│ (push)
|
|
||||||
▼
|
|
||||||
┌─────────────────┐
|
|
||||||
│ reusable │
|
|
||||||
│ GitHub Actions │
|
|
||||||
│ pipeline │
|
|
||||||
└────────┬────────┘
|
|
||||||
│
|
|
||||||
┌─────────────┼─────────────┐
|
|
||||||
▼ ▼ ▼
|
|
||||||
Dev (auto) QA (approval) Prod (approval)
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
evidence_writer ─▶ audit.json (hash-chained) ─▶ Pages timeline
|
|
||||||
```
|
|
||||||
|
|
||||||
Two entry paths, **one** pipeline, **one** audit trail — developer via GitHub, citizen developer via their own Claude agent.
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Two surfaces: developers push contracts directly to GitHub; citizen developers prompt a Claude agent they own, which writes and pushes the contract for them.
|
|
||||||
Both converge on one GitHub Actions pipeline with three stages.
|
|
||||||
-->
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# The Safety Story
|
|
||||||
|
|
||||||
### Computed, not requested
|
|
||||||
|
|
||||||
| Signal | Behavior |
|
|
||||||
|--------|----------|
|
|
||||||
| **Base confidence** | 0.90 |
|
|
||||||
| **On policy violation** | drop to 0.40 + reason code |
|
|
||||||
| **Gate threshold** | ≥ 0.50 to proceed past Dev |
|
|
||||||
|
|
||||||
### Policy (v1)
|
|
||||||
- `public-ingress: true` → `POLICY_VIOLATION:PUBLIC_INGRESS`
|
|
||||||
|
|
||||||
### Evidence
|
|
||||||
- Each event appended to `audit.json` with SHA-256 link to previous (`prev_hash` + `hash`)
|
|
||||||
- Published to Pages → vanilla-JS timeline
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Safety is computed by the platform, not asked of the developer.
|
|
||||||
Threshold 0.50 is exact → 0.90 passes, 0.40 halts. That's what makes the three scenarios deterministic.
|
|
||||||
-->
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Scenario 1 — Developer Self-Service
|
|
||||||
|
|
||||||
### Trigger
|
|
||||||
Developer commits a valid `contract.yaml` requesting **`l2-commodity-price-feed`** via **GitHub**.
|
|
||||||
|
|
||||||
### What you'll see
|
|
||||||
- **Dev:** policy ✅ → apply api-gateway, lambda, s3 → confidence **0.90** → proceed
|
|
||||||
- **QA:** pipeline pauses → click **Approve**
|
|
||||||
- **Prod:** pipeline pauses → click **Approve**
|
|
||||||
- **Finalize:** `audit.json` committed → Pages timeline updates
|
|
||||||
|
|
||||||
### Evidence outcome
|
|
||||||
Timeline shows: contract received → policy pass → apply × 3 → confidence 0.90 → QA → Prod → published.
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Normal developer flow: human writes the contract, pushes to GitHub, the GitHub Action pipeline runs.
|
|
||||||
Narrate: developer commits and walks away; platform does the rest.
|
|
||||||
Pause visibly at each gate so the audience sees human-in-the-loop.
|
|
||||||
End by refreshing the Pages timeline.
|
|
||||||
-->
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Scenario 1 — Journey
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart LR
|
|
||||||
classDef gh fill:#F4F4F4,stroke:#1A1A1A,stroke-width:2px,color:#1A1A1A
|
|
||||||
classDef stage fill:#C8102E,stroke:#8E0B20,stroke-width:1px,color:#FFFFFF
|
|
||||||
classDef gate fill:#FFFFFF,stroke:#1A1A1A,stroke-width:2px,color:#1A1A1A
|
|
||||||
classDef evidence fill:#F4F4F4,stroke:#C8102E,stroke-width:1px,color:#1A1A1A
|
|
||||||
|
|
||||||
D["Developer"]:::gh -->|"writes contract.yaml"| GH["GitHub<br/>acdl-contracts"]:::gh
|
|
||||||
GH -->|"push triggers<br/>GitHub Action"| DEV["Dev<br/>(autonomous)"]:::stage
|
|
||||||
DEV -->|"policy ✅ · confidence 0.90"| QA["QA<br/>approval gate"]:::gate
|
|
||||||
QA -->|"approve"| PROD["Prod<br/>approval gate"]:::gate
|
|
||||||
PROD -->|"approve"| FIN["Finalize<br/>commit audit.json"]:::stage
|
|
||||||
FIN --> TL["GitHub Pages<br/>timeline"]:::evidence
|
|
||||||
```
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Point to the two approval gates — QA and Prod — both human clicks.
|
|
||||||
The whole chain from commit to timeline is one GitHub Actions workflow.
|
|
||||||
-->
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Scenario 2 — Citizen Developer
|
|
||||||
|
|
||||||
### Trigger
|
|
||||||
Non-technical user prompts their **own Claude agent** in natural language:
|
|
||||||
|
|
||||||
> "I need a new service to ingest real-time natural gas pricing data into our data lake."
|
|
||||||
|
|
||||||
### What you'll see
|
|
||||||
- Claude agent parses intent, writes `contract.yaml` for **`l2-commodity-price-feed`**, pushes a branch
|
|
||||||
- Issue **closed**; branch push triggers the **identical** pipeline from Scenario 1
|
|
||||||
- Citizen developer follows the run all the way to **Prod**
|
|
||||||
|
|
||||||
### Evidence outcome
|
|
||||||
Timeline is **indistinguishable** from Scenario 1 — the agentic surface is first-class, not a bolt-on.
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Punchline: same timeline, same safety, same audit — different entry.
|
|
||||||
The citizen developer owns and drives their own Claude agent; they are the actor, not the platform.
|
|
||||||
Normal developers (Scenario 1) keep using GitHub directly — two surfaces, one pipeline, one audit.
|
|
||||||
-->
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Scenario 2 — Journey
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart LR
|
|
||||||
classDef cit fill:#F4F4F4,stroke:#C8102E,stroke-width:2px,color:#1A1A1A
|
|
||||||
classDef agent fill:#C8102E,stroke:#8E0B20,stroke-width:1px,color:#FFFFFF
|
|
||||||
classDef stage fill:#1A1A1A,stroke:#1A1A1A,stroke-width:1px,color:#FFFFFF
|
|
||||||
classDef gate fill:#FFFFFF,stroke:#1A1A1A,stroke-width:2px,color:#1A1A1A
|
|
||||||
classDef evidence fill:#F4F4F4,stroke:#C8102E,stroke-width:1px,color:#1A1A1A
|
|
||||||
|
|
||||||
CD["Citizen developer"]:::cit -->|"natural-language<br/>prompt"| CL["Claude agent<br/>(citizen-owned)"]:::agent
|
|
||||||
CL -->|"generates<br/>contract.yaml"| GH["GitHub<br/>acdl-contracts"]:::cit
|
|
||||||
GH -->|"push triggers<br/>GitHub Action"| DEV["Dev<br/>(autonomous)"]:::stage
|
|
||||||
DEV -->|"policy ✅ · confidence 0.90"| QA["QA<br/>approval gate"]:::gate
|
|
||||||
QA -->|"approve"| PROD["Prod<br/>approval gate"]:::gate
|
|
||||||
PROD -->|"approve"| FIN["Finalize<br/>commit audit.json"]:::stage
|
|
||||||
FIN --> TL["GitHub Pages<br/>timeline"]:::evidence
|
|
||||||
```
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Highlight the red Claude-agent node — owned by the citizen, not by the platform.
|
|
||||||
From GitHub onward the journey is identical to Scenario 1.
|
|
||||||
-->
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Scenario 3 — The Safety Net
|
|
||||||
|
|
||||||
### Trigger
|
|
||||||
Developer commits a **malicious** `contract.yaml` for `l2-regulatory-reporting` via **GitHub**:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
stack: l2-regulatory-reporting
|
|
||||||
public-ingress: true
|
|
||||||
```
|
|
||||||
|
|
||||||
### What you'll see
|
|
||||||
- **Dev:** `policy_checker` → `POLICY_VIOLATION:PUBLIC_INGRESS`
|
|
||||||
- `confidence_signal` drops 0.90 → **0.40**
|
|
||||||
- `0.40 < 0.50` → pipeline **halts in Dev**
|
|
||||||
- Rejection reason written to the evidence stream
|
|
||||||
|
|
||||||
### Evidence outcome
|
|
||||||
Timeline shows the attempted deploy, the violation, the confidence drop, and the **halt** — visible and explained.
|
|
||||||
|
|
||||||
<!--
|
|
||||||
The safety money shot. The platform said NO, and said WHY, on the record.
|
|
||||||
No human had to catch it — the confidence signal computed the risk.
|
|
||||||
Contrast with The Problem's old-world review queue.
|
|
||||||
-->
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Scenario 3 — Journey
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart LR
|
|
||||||
classDef gh fill:#F4F4F4,stroke:#1A1A1A,stroke-width:2px,color:#1A1A1A
|
|
||||||
classDef stage fill:#C8102E,stroke:#8E0B20,stroke-width:1px,color:#FFFFFF
|
|
||||||
classDef halt fill:#1A1A1A,stroke:#1A1A1A,stroke-width:1px,color:#FFFFFF
|
|
||||||
classDef evidence fill:#F4F4F4,stroke:#C8102E,stroke-width:1px,color:#1A1A1A
|
|
||||||
|
|
||||||
D["Developer"]:::gh -->|"writes malicious<br/>contract.yaml"| GH["GitHub<br/>acdl-contracts"]:::gh
|
|
||||||
GH -->|"push triggers<br/>GitHub Action"| DEV["Dev<br/>(autonomous)"]:::stage
|
|
||||||
DEV -->|"POLICY_VIOLATION:PUBLIC_INGRESS<br/>confidence 0.90 → 0.40"| HALT["Halt in Dev<br/>+ rejection reason"]:::halt
|
|
||||||
HALT --> TL["GitHub Pages<br/>timeline"]:::evidence
|
|
||||||
```
|
|
||||||
|
|
||||||
<!--
|
|
||||||
The black halt node is the whole point — pipeline stops, evidence records why.
|
|
||||||
Notice there are no QA/Prod gates on this path; the journey ends at Dev.
|
|
||||||
-->
|
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
# ACDL issue-to-contract workflow (Phase 04 implementation).
|
|
||||||
#
|
|
||||||
# Trigger: a new Issue is opened in acdl-contracts. The workflow runs
|
|
||||||
# l3b_agent_stub.py (checked out from the `acdl` repo, pinned to
|
|
||||||
# @milestone/v1.0-initial) to map the Issue body to a contract.yaml, commits
|
|
||||||
# the contract to a new branch `contract/<issue-number>` on acdl-contracts
|
|
||||||
# via the Gitea file-contents API, closes the Issue with a comment, and
|
|
||||||
# dispatches the main pipeline in the `acdl` repo via the workflow_dispatch
|
|
||||||
# API (D-014; Gitea Actions does not support repository_dispatch).
|
|
||||||
#
|
|
||||||
# Cross-repo trigger (D-014):
|
|
||||||
# The final step POSTs to
|
|
||||||
# /api/v1/repos/continuous-intelligence/acdl/actions/workflows/pipeline.yml/dispatches
|
|
||||||
# with body {"ref": "milestone/v1.0-initial",
|
|
||||||
# "inputs": {"contract-ref": "contract/<issue-number>"}}.
|
|
||||||
#
|
|
||||||
# Branch-pin rule (ARCHITECTURE.md):
|
|
||||||
# The `acdl` repo's default branch is `milestone/v1.0-initial`, so the
|
|
||||||
# checkout step pins `ref: milestone/v1.0-initial`. The pipeline dispatch
|
|
||||||
# also pins `ref: milestone/v1.0-initial` (the workflow file lives on
|
|
||||||
# that branch). The new `contract/<n>` branch is created on acdl-contracts
|
|
||||||
# (whose default branch is `main`, per D-015).
|
|
||||||
#
|
|
||||||
# File-contents POST with `new_branch` (D-030):
|
|
||||||
# The POST to /repos/.../contents/contract.yaml includes
|
|
||||||
# `new_branch: contract/<n>`, which tells Gitea to create the file on a
|
|
||||||
# NEW branch off the current head of `branch: main` instead of committing
|
|
||||||
# directly to main. This avoids a separate branch-create + commit round
|
|
||||||
# trip.
|
|
||||||
name: issue-to-contract
|
|
||||||
|
|
||||||
"on":
|
|
||||||
issues:
|
|
||||||
types: [opened]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
parse-and-trigger:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: "Checkout acdl (pinned to milestone/v1.0-initial for l3b_agent_stub.py)"
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
repository: continuous-intelligence/acdl
|
|
||||||
ref: milestone/v1.0-initial
|
|
||||||
token: ${{ secrets.GITEA_TOKEN }}
|
|
||||||
|
|
||||||
- name: "Parse Issue body into contract.yaml"
|
|
||||||
env:
|
|
||||||
ISSUE_BODY: ${{ gitea.event.issue.body }}
|
|
||||||
run: |
|
|
||||||
# Pass the Issue body via an env var to avoid shell injection from
|
|
||||||
# arbitrary Issue text. l3b_agent_stub.py reads argv[1]; we pass
|
|
||||||
# the env var quoted so no metacharacter interpretation happens.
|
|
||||||
python3 scripts/l3b_agent_stub.py "$ISSUE_BODY" -o contract.yaml
|
|
||||||
echo "--- generated contract.yaml ---"
|
|
||||||
cat contract.yaml
|
|
||||||
|
|
||||||
- name: "Commit contract.yaml to new branch contract/${{ gitea.event.issue.number }} on acdl-contracts"
|
|
||||||
env:
|
|
||||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
STACK=$(python3 -c 'import yaml; print(yaml.safe_load(open("contract.yaml"))["stack"])')
|
|
||||||
ISSUE_NUMBER="${{ gitea.event.issue.number }}"
|
|
||||||
BRANCH="contract/${ISSUE_NUMBER}"
|
|
||||||
HOST="https://git.cloudinit.dev"
|
|
||||||
API="${HOST}/api/v1/repos/continuous-intelligence/acdl-contracts/contents/contract.yaml"
|
|
||||||
B64=$(base64 -w 0 contract.yaml)
|
|
||||||
BODY=$(python3 -c "
|
|
||||||
import json
|
|
||||||
print(json.dumps({
|
|
||||||
'content': '${B64}',
|
|
||||||
'message': 'l3b: contract for issue #${ISSUE_NUMBER}',
|
|
||||||
'branch': 'main',
|
|
||||||
'new_branch': '${BRANCH}'
|
|
||||||
}))
|
|
||||||
")
|
|
||||||
STATUS=$(curl -sS -o /tmp/contract_post.json -w "%{http_code}" \
|
|
||||||
-X POST \
|
|
||||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "$BODY" \
|
|
||||||
"$API")
|
|
||||||
echo "POST contract.yaml -> HTTP ${STATUS}"
|
|
||||||
cat /tmp/contract_post.json || true
|
|
||||||
case "$STATUS" in
|
|
||||||
201) echo "contract.yaml committed on branch ${BRANCH}" ;;
|
|
||||||
*) echo "ERROR: file-contents POST failed (HTTP ${STATUS})" >&2; exit 1 ;;
|
|
||||||
esac
|
|
||||||
echo "STACK=${STACK}" >> "$GITHUB_ENV"
|
|
||||||
echo "BRANCH=${BRANCH}" >> "$GITHUB_ENV"
|
|
||||||
|
|
||||||
- name: "Comment on Issue + close it"
|
|
||||||
env:
|
|
||||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
ISSUE_NUMBER="${{ gitea.event.issue.number }}"
|
|
||||||
HOST="https://git.cloudinit.dev"
|
|
||||||
ISSUES_API="${HOST}/api/v1/repos/continuous-intelligence/acdl-contracts/issues/${ISSUE_NUMBER}"
|
|
||||||
COMMENT_BODY=$(python3 -c "
|
|
||||||
import json
|
|
||||||
print(json.dumps({'body': 'Generated contract.yaml for stack \`' + '${STACK}' + '\` on branch \`' + '${BRANCH}' + '\`. Pipeline dispatched.'}))
|
|
||||||
")
|
|
||||||
curl -sS -o /tmp/comment.json -w "comment HTTP %{http_code}\n" \
|
|
||||||
-X POST \
|
|
||||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "$COMMENT_BODY" \
|
|
||||||
"${ISSUES_API}/comments"
|
|
||||||
CLOSE_BODY='{"state":"closed"}'
|
|
||||||
curl -sS -o /tmp/close.json -w "close HTTP %{http_code}\n" \
|
|
||||||
-X PATCH \
|
|
||||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "$CLOSE_BODY" \
|
|
||||||
"${ISSUES_API}"
|
|
||||||
|
|
||||||
- name: "Dispatch the pipeline on acdl (contract-ref = contract/${{ gitea.event.issue.number }})"
|
|
||||||
env:
|
|
||||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
ISSUE_NUMBER="${{ gitea.event.issue.number }}"
|
|
||||||
HOST="https://git.cloudinit.dev"
|
|
||||||
DISPATCH_URL="${HOST}/api/v1/repos/continuous-intelligence/acdl/actions/workflows/pipeline.yml/dispatches"
|
|
||||||
BODY=$(python3 -c "
|
|
||||||
import json
|
|
||||||
print(json.dumps({
|
|
||||||
'ref': 'milestone/v1.0-initial',
|
|
||||||
'inputs': {'contract-ref': 'contract/${ISSUE_NUMBER}'}
|
|
||||||
}))
|
|
||||||
")
|
|
||||||
STATUS=$(curl -sS -o /tmp/dispatch.json -w "%{http_code}" \
|
|
||||||
-X POST \
|
|
||||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "$BODY" \
|
|
||||||
"$DISPATCH_URL")
|
|
||||||
echo "pipeline dispatch -> HTTP ${STATUS}"
|
|
||||||
cat /tmp/dispatch.json || true
|
|
||||||
case "$STATUS" in
|
|
||||||
201|202|204) echo "pipeline dispatched (contract-ref=contract/${ISSUE_NUMBER})" ;;
|
|
||||||
*) echo "ERROR: pipeline dispatch failed (HTTP ${STATUS})" >&2; exit 1 ;;
|
|
||||||
esac
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
# Elaborate developer contract — energy trading price feed.
|
|
||||||
#
|
|
||||||
# Schema (D-021): stack + inputs (open-ended string map) + public-ingress.
|
|
||||||
# The `stack` field MUST match an L2 folder name under modules/l2/.
|
|
||||||
# The `inputs` map is free-form string values; these are L2-level params
|
|
||||||
# that travel with the deployment into state.json and the audit trail.
|
|
||||||
# The L1 input values are declared by the L2's manifest.yaml, not here.
|
|
||||||
#
|
|
||||||
# Commit this to acdl-contracts as contract.yaml to trigger the pipeline:
|
|
||||||
# git add contract.yaml && git commit -m "feat: deploy price feed (prod)" && git push
|
|
||||||
|
|
||||||
stack: l2-commodity-price-feed
|
|
||||||
|
|
||||||
inputs:
|
|
||||||
# --- Environment + ownership ---
|
|
||||||
environment: prod
|
|
||||||
owner: commodity-trading-platform-team
|
|
||||||
team: power-and-gas-desk
|
|
||||||
cost_center: CC-TRD-4471
|
|
||||||
change_ticket: CHG-2026-07-21-093
|
|
||||||
|
|
||||||
# --- Business context (rides into the audit timeline) ---
|
|
||||||
business_owner: kchen@jccapital.xyz
|
|
||||||
oncall_email: sre-commodity@example.com
|
|
||||||
sla_tier: T1
|
|
||||||
business_hours: "Mon-Fri 07:00-19:00 ET"
|
|
||||||
data_classification: internal
|
|
||||||
|
|
||||||
# --- Source feed contract (business-facing) ---
|
|
||||||
feed_vendor: Platts
|
|
||||||
feed_name: natural-gas-daily-settlement
|
|
||||||
feed_cadence: daily
|
|
||||||
feed_timezone: US/Eastern
|
|
||||||
symbols: "NG-WTI-HH,NG-HH-M,NG-PJM"
|
|
||||||
retry_policy: backoff-3x-15min
|
|
||||||
dead_letter_queue: commodity-price-dlq
|
|
||||||
|
|
||||||
# --- Deployment knobs (consumed by the pipeline; passed to L1s via L2 manifest) ---
|
|
||||||
replicas: "3"
|
|
||||||
cpu_request: "500m"
|
|
||||||
memory_request: "1Gi"
|
|
||||||
autoscale_min: "2"
|
|
||||||
autoscale_max: "8"
|
|
||||||
log_retention_days: "90"
|
|
||||||
archive_retention_days: "2555"
|
|
||||||
|
|
||||||
# --- Operational flags ---
|
|
||||||
enable_canary: "true"
|
|
||||||
canary_percentage: "10"
|
|
||||||
enable_pagerduty: "true"
|
|
||||||
enable_cost_alerts: "true"
|
|
||||||
cost_alert_threshold_usd: "500"
|
|
||||||
|
|
||||||
# Policy-gated field. true -> POLICY_VIOLATION:PUBLIC_INGRESS -> confidence 0.40 < 0.50 -> Dev rejects (Act 4).
|
|
||||||
public-ingress: false
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
# Elaborate developer contract — regulatory reporting (with policy violation).
|
|
||||||
#
|
|
||||||
# Same schema as the price-feed example, but with public-ingress: true,
|
|
||||||
# which triggers Act 4: the policy_checker fails, the confidence_signal
|
|
||||||
# drops to 0.40, the 0.50 gate halts the pipeline in Dev, and the
|
|
||||||
# rejection appears on the evidence timeline.
|
|
||||||
#
|
|
||||||
# Commit this to acdl-contracts as contract.yaml to reproduce Act 4:
|
|
||||||
# git add contract.yaml && git commit -m "feat: deploy regulatory reporting" && git push
|
|
||||||
|
|
||||||
stack: l2-regulatory-reporting
|
|
||||||
|
|
||||||
inputs:
|
|
||||||
# --- Environment + ownership ---
|
|
||||||
environment: prod
|
|
||||||
owner: compliance-and-controls-team
|
|
||||||
team: regulatory-reporting-desk
|
|
||||||
cost_center: CC-CMP-9902
|
|
||||||
change_ticket: CHG-2026-07-21-118
|
|
||||||
business_owner: compliance@jccapital.xyz
|
|
||||||
oncall_email: sre-regulatory@example.com
|
|
||||||
sla_tier: T0
|
|
||||||
business_hours: "24x7"
|
|
||||||
data_classification: confidential
|
|
||||||
|
|
||||||
# --- Regulatory context ---
|
|
||||||
regulator: FERC
|
|
||||||
filing_frequency: monthly
|
|
||||||
filing_deadline_day_of_month: "15"
|
|
||||||
reporting_period: 2026-Q3
|
|
||||||
jurisdiction: US-Federal
|
|
||||||
legal_hold: "false"
|
|
||||||
|
|
||||||
# --- Deployment knobs ---
|
|
||||||
replicas: "2"
|
|
||||||
cpu_request: "1000m"
|
|
||||||
memory_request: "2Gi"
|
|
||||||
autoscale_min: "2"
|
|
||||||
autoscale_max: "4"
|
|
||||||
log_retention_days: "365"
|
|
||||||
archive_retention_days: "2555"
|
|
||||||
enable_encryption_at_rest: "true"
|
|
||||||
enable_kms_rotation: "true"
|
|
||||||
|
|
||||||
# --- Operational flags ---
|
|
||||||
enable_canary: "false"
|
|
||||||
enable_pagerduty: "true"
|
|
||||||
enable_cost_alerts: "true"
|
|
||||||
cost_alert_threshold_usd: "1000"
|
|
||||||
|
|
||||||
# POLICY VIOLATION — this is the Act 4 trigger.
|
|
||||||
# The policy_checker.py will emit: POLICY_VIOLATION:PUBLIC_INGRESS
|
|
||||||
# The confidence_signal.py will return: {"score": 0.40, "reason": "POLICY_VIOLATION:PUBLIC_INGRESS"}
|
|
||||||
# The 0.50 gate halts the pipeline in Dev; mock_executor never runs.
|
|
||||||
public-ingress: true
|
|
||||||
@@ -1,334 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<title>ACDL Evidence Timeline</title>
|
|
||||||
<style>
|
|
||||||
:root {
|
|
||||||
--stage-dev: #2563eb;
|
|
||||||
--stage-qa: #ca8a04;
|
|
||||||
--stage-prod: #ea580c;
|
|
||||||
--stage-finalize: #16a34a;
|
|
||||||
--stage-genesis: #6b7280;
|
|
||||||
--stage-rejected: #dc2626;
|
|
||||||
--bg: #f8fafc;
|
|
||||||
--card-bg: #ffffff;
|
|
||||||
--text: #0f172a;
|
|
||||||
--muted: #64748b;
|
|
||||||
--border: #e2e8f0;
|
|
||||||
}
|
|
||||||
* { box-sizing: border-box; }
|
|
||||||
html, body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
background: var(--bg);
|
|
||||||
color: var(--text);
|
|
||||||
font-family: system-ui, -apple-system, sans-serif;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
header {
|
|
||||||
padding: 24px 32px 16px;
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
background: var(--card-bg);
|
|
||||||
}
|
|
||||||
header h1 {
|
|
||||||
margin: 0 0 6px;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
header p {
|
|
||||||
margin: 0;
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
|
||||||
.toolbar {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 16px 32px;
|
|
||||||
background: var(--card-bg);
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
button#refresh {
|
|
||||||
appearance: none;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
background: var(--text);
|
|
||||||
color: #fff;
|
|
||||||
padding: 8px 16px;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
font-family: inherit;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
button#refresh:hover { opacity: 0.9; }
|
|
||||||
button#refresh:active { transform: translateY(1px); }
|
|
||||||
.toolbar .status {
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 0.85rem;
|
|
||||||
}
|
|
||||||
main {
|
|
||||||
padding: 24px 32px 48px;
|
|
||||||
max-width: 900px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
.empty {
|
|
||||||
padding: 48px 24px;
|
|
||||||
text-align: center;
|
|
||||||
color: var(--muted);
|
|
||||||
background: var(--card-bg);
|
|
||||||
border: 1px dashed var(--border);
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
ol.timeline {
|
|
||||||
list-style: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
ol.timeline::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
left: 11px;
|
|
||||||
top: 6px;
|
|
||||||
bottom: 6px;
|
|
||||||
width: 2px;
|
|
||||||
background: var(--border);
|
|
||||||
}
|
|
||||||
li.event {
|
|
||||||
position: relative;
|
|
||||||
padding: 12px 0 12px 40px;
|
|
||||||
}
|
|
||||||
li.event::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
left: 6px;
|
|
||||||
top: 18px;
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: var(--dot, var(--muted));
|
|
||||||
border: 2px solid var(--card-bg);
|
|
||||||
box-shadow: 0 0 0 1px var(--border);
|
|
||||||
}
|
|
||||||
.card {
|
|
||||||
background: var(--card-bg);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-left: 4px solid var(--dot, var(--muted));
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 12px 16px;
|
|
||||||
}
|
|
||||||
.card .row {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
.seq {
|
|
||||||
display: inline-block;
|
|
||||||
min-width: 28px;
|
|
||||||
padding: 2px 6px;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 600;
|
|
||||||
text-align: center;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: #eef2ff;
|
|
||||||
color: #3730a3;
|
|
||||||
border: 1px solid #c7d2fe;
|
|
||||||
}
|
|
||||||
.chip {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 2px 8px;
|
|
||||||
font-size: 0.72rem;
|
|
||||||
font-weight: 600;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.04em;
|
|
||||||
border-radius: 999px;
|
|
||||||
color: #fff;
|
|
||||||
background: var(--dot, var(--muted));
|
|
||||||
}
|
|
||||||
.ts {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: var(--muted);
|
|
||||||
font-variant-numeric: tabular-nums;
|
|
||||||
}
|
|
||||||
.event-text {
|
|
||||||
margin: 4px 0 6px;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
|
||||||
.hash {
|
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: var(--muted);
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
footer {
|
|
||||||
padding: 16px 32px 24px;
|
|
||||||
border-top: 1px solid var(--border);
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 0.8rem;
|
|
||||||
max-width: 900px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
footer code {
|
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
||||||
background: #f1f5f9;
|
|
||||||
padding: 1px 4px;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header>
|
|
||||||
<h1>ACDL Evidence Timeline</h1>
|
|
||||||
<p>ACDL — Agentic Cloud Delivery Platform · Audit Timeline</p>
|
|
||||||
</header>
|
|
||||||
<div class="toolbar">
|
|
||||||
<button id="refresh" type="button">Refresh</button>
|
|
||||||
<span class="status" id="status"></span>
|
|
||||||
</div>
|
|
||||||
<main>
|
|
||||||
<div id="container">
|
|
||||||
<div class="empty">Loading…</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
<footer>
|
|
||||||
<div id="footer"></div>
|
|
||||||
</footer>
|
|
||||||
<script>
|
|
||||||
(function () {
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
var AUDIT_URL = "./audit.json";
|
|
||||||
var STAGE_COLORS = {
|
|
||||||
dev: "var(--stage-dev)",
|
|
||||||
qa: "var(--stage-qa)",
|
|
||||||
prod: "var(--stage-prod)",
|
|
||||||
finalize: "var(--stage-finalize)",
|
|
||||||
genesis: "var(--stage-genesis)"
|
|
||||||
};
|
|
||||||
|
|
||||||
function $(id) { return document.getElementById(id); }
|
|
||||||
|
|
||||||
function stageColor(stage, eventText) {
|
|
||||||
var evt = (eventText || "").toString().toLowerCase();
|
|
||||||
if (evt.indexOf("rejected") !== -1) {
|
|
||||||
return "var(--stage-rejected)";
|
|
||||||
}
|
|
||||||
return STAGE_COLORS[stage] || "var(--stage-genesis)";
|
|
||||||
}
|
|
||||||
|
|
||||||
function dash(v) {
|
|
||||||
return (v === null || v === undefined || v === "") ? "—" : v;
|
|
||||||
}
|
|
||||||
|
|
||||||
function hashPreview(hash) {
|
|
||||||
if (hash === null || hash === undefined || hash === "") return "—";
|
|
||||||
var s = String(hash);
|
|
||||||
return s.slice(0, 12) + "…";
|
|
||||||
}
|
|
||||||
|
|
||||||
function esc(s) {
|
|
||||||
return String(s)
|
|
||||||
.replace(/&/g, "&")
|
|
||||||
.replace(/</g, "<")
|
|
||||||
.replace(/>/g, ">")
|
|
||||||
.replace(/"/g, """)
|
|
||||||
.replace(/'/g, "'");
|
|
||||||
}
|
|
||||||
|
|
||||||
function auditUrlDisplay() {
|
|
||||||
try {
|
|
||||||
var href = window.location.href;
|
|
||||||
var slash = href.lastIndexOf("/");
|
|
||||||
if (slash >= 0) {
|
|
||||||
return href.slice(0, slash + 1) + "audit.json";
|
|
||||||
}
|
|
||||||
} catch (e) {}
|
|
||||||
return AUDIT_URL;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderEmpty(msg) {
|
|
||||||
$("container").innerHTML =
|
|
||||||
'<div class="empty">' + esc(msg) + "</div>";
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderTimeline(events) {
|
|
||||||
if (!Array.isArray(events)) {
|
|
||||||
renderEmpty("No audit data yet");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (events.length === 0) {
|
|
||||||
renderEmpty("No audit data yet");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var sorted = events.slice().sort(function (a, b) {
|
|
||||||
var sa = (a && typeof a.seq === "number") ? a.seq : 0;
|
|
||||||
var sb = (b && typeof b.seq === "number") ? b.seq : 0;
|
|
||||||
return sa - sb;
|
|
||||||
});
|
|
||||||
var html = '<ol class="timeline">';
|
|
||||||
for (var i = 0; i < sorted.length; i++) {
|
|
||||||
var e = sorted[i] || {};
|
|
||||||
var stage = dash(e.stage);
|
|
||||||
var color = stageColor(e.stage, e.event);
|
|
||||||
html += '<li class="event" style="--dot:' + color + ';">';
|
|
||||||
html += '<div class="card" style="--dot:' + color + ';">';
|
|
||||||
html += '<div class="row">';
|
|
||||||
html += '<span class="seq">#' + esc(dash(e.seq)) + "</span>";
|
|
||||||
html += '<span class="chip">' + esc(stage) + "</span>";
|
|
||||||
html += '<span class="ts">' + esc(dash(e.ts)) + "</span>";
|
|
||||||
html += "</div>";
|
|
||||||
html += '<div class="event-text">' + esc(dash(e.event)) + "</div>";
|
|
||||||
html += '<div class="hash">' + esc(hashPreview(e.hash)) + "</div>";
|
|
||||||
html += "</div>";
|
|
||||||
html += "</li>";
|
|
||||||
}
|
|
||||||
html += "</ol>";
|
|
||||||
$("container").innerHTML = html;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderFooter(ok) {
|
|
||||||
var when = new Date().toISOString();
|
|
||||||
var url = auditUrlDisplay();
|
|
||||||
var prefix = "Fetched at " + when + " · audit.json: ";
|
|
||||||
$("footer").innerHTML =
|
|
||||||
esc(prefix) + '<code>' + esc(url) + "</code>" +
|
|
||||||
(ok ? "" : " (fetch failed)");
|
|
||||||
}
|
|
||||||
|
|
||||||
function setStatus(msg) {
|
|
||||||
$("status").textContent = msg || "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function fetchAudit() {
|
|
||||||
setStatus("Fetching…");
|
|
||||||
fetch(AUDIT_URL, { cache: "no-store" })
|
|
||||||
.then(function (res) {
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error("HTTP " + res.status);
|
|
||||||
}
|
|
||||||
return res.json();
|
|
||||||
})
|
|
||||||
.then(function (data) {
|
|
||||||
if (!Array.isArray(data)) {
|
|
||||||
throw new Error("not an array");
|
|
||||||
}
|
|
||||||
renderTimeline(data);
|
|
||||||
renderFooter(true);
|
|
||||||
setStatus("Loaded " + data.length + " event(s)");
|
|
||||||
})
|
|
||||||
.catch(function (err) {
|
|
||||||
renderEmpty("No audit data yet");
|
|
||||||
renderFooter(false);
|
|
||||||
setStatus("Fetch failed: " + (err && err.message ? err.message : "error"));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$("refresh").addEventListener("click", fetchAudit);
|
|
||||||
fetchAudit();
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
name: l1-api-gateway
|
|
||||||
kind: l1
|
|
||||||
description: HTTP routing primitive
|
|
||||||
inputs:
|
|
||||||
api_name:
|
|
||||||
description: Name of the API Gateway REST/HTTP API
|
|
||||||
type: string
|
|
||||||
stage_name:
|
|
||||||
description: Name of the deployment stage (e.g. dev, prod)
|
|
||||||
type: string
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
echo "[L1: l1-api-gateway] applying..."
|
|
||||||
sleep 1
|
|
||||||
echo "[L1: l1-api-gateway] OK"
|
|
||||||
exit 0
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
name: l1-cloudwatch
|
|
||||||
kind: l1
|
|
||||||
description: Observability primitive
|
|
||||||
inputs:
|
|
||||||
log_group_name:
|
|
||||||
description: Name of the CloudWatch log group
|
|
||||||
type: string
|
|
||||||
metric_namespace:
|
|
||||||
description: Namespace under which custom metrics are emitted
|
|
||||||
type: string
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
echo "[L1: l1-cloudwatch] applying..."
|
|
||||||
sleep 1
|
|
||||||
echo "[L1: l1-cloudwatch] OK"
|
|
||||||
exit 0
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
name: l1-eks-fargate
|
|
||||||
kind: l1
|
|
||||||
description: Serverless container compute substrate
|
|
||||||
inputs:
|
|
||||||
cluster_name:
|
|
||||||
description: Name of the EKS cluster to target
|
|
||||||
type: string
|
|
||||||
region:
|
|
||||||
description: AWS region the cluster runs in
|
|
||||||
type: string
|
|
||||||
cpu_arch:
|
|
||||||
description: CPU architecture for Fargate pods (x86_64 or arm64)
|
|
||||||
type: string
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
echo "[L1: l1-eks-fargate] applying..."
|
|
||||||
sleep 1
|
|
||||||
echo "[L1: l1-eks-fargate] OK"
|
|
||||||
exit 0
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
name: l1-eventbridge
|
|
||||||
kind: l1
|
|
||||||
description: Event bus primitive
|
|
||||||
inputs:
|
|
||||||
bus_name:
|
|
||||||
description: Name of the EventBridge bus
|
|
||||||
type: string
|
|
||||||
rule_name:
|
|
||||||
description: Name of the event rule on the bus
|
|
||||||
type: string
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
echo "[L1: l1-eventbridge] applying..."
|
|
||||||
sleep 1
|
|
||||||
echo "[L1: l1-eventbridge] OK"
|
|
||||||
exit 0
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
name: l1-iam-role
|
|
||||||
kind: l1
|
|
||||||
description: Identity and access role primitive
|
|
||||||
inputs:
|
|
||||||
role_name:
|
|
||||||
description: Name of the IAM role to create
|
|
||||||
type: string
|
|
||||||
trust_policy:
|
|
||||||
description: JSON trust policy document defining who can assume the role
|
|
||||||
type: string
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
echo "[L1: l1-iam-role] applying..."
|
|
||||||
sleep 1
|
|
||||||
echo "[L1: l1-iam-role] OK"
|
|
||||||
exit 0
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
name: l1-lambda
|
|
||||||
kind: l1
|
|
||||||
description: Event-driven function primitive
|
|
||||||
inputs:
|
|
||||||
function_name:
|
|
||||||
description: Name of the Lambda function
|
|
||||||
type: string
|
|
||||||
runtime:
|
|
||||||
description: Lambda runtime identifier (e.g. python3.12, nodejs20.x)
|
|
||||||
type: string
|
|
||||||
handler:
|
|
||||||
description: Handler entrypoint in the form module.function
|
|
||||||
type: string
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
echo "[L1: l1-lambda] applying..."
|
|
||||||
sleep 1
|
|
||||||
echo "[L1: l1-lambda] OK"
|
|
||||||
exit 0
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
name: l1-s3
|
|
||||||
kind: l1
|
|
||||||
description: Object store primitive
|
|
||||||
inputs:
|
|
||||||
bucket_name:
|
|
||||||
description: Globally unique name of the S3 bucket
|
|
||||||
type: string
|
|
||||||
region:
|
|
||||||
description: AWS region the bucket lives in
|
|
||||||
type: string
|
|
||||||
retention_days:
|
|
||||||
description: Number of days to retain objects before expiration
|
|
||||||
type: string
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
echo "[L1: l1-s3] applying..."
|
|
||||||
sleep 1
|
|
||||||
echo "[L1: l1-s3] OK"
|
|
||||||
exit 0
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
name: l1-sqs
|
|
||||||
kind: l1
|
|
||||||
description: Queue primitive
|
|
||||||
inputs:
|
|
||||||
queue_name:
|
|
||||||
description: Name of the SQS queue
|
|
||||||
type: string
|
|
||||||
visibility_timeout:
|
|
||||||
description: Visibility timeout in seconds for in-flight messages
|
|
||||||
type: string
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
echo "[L1: l1-sqs] applying..."
|
|
||||||
sleep 1
|
|
||||||
echo "[L1: l1-sqs] OK"
|
|
||||||
exit 0
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
name: l2-commodity-price-feed
|
|
||||||
kind: l2
|
|
||||||
description: Real-time commodity price ingestion from Platts
|
|
||||||
l1s:
|
|
||||||
- name: l1-eks-fargate
|
|
||||||
inputs:
|
|
||||||
cluster_name: price-feed-cluster
|
|
||||||
region: us-east-1
|
|
||||||
cpu_arch: arm64
|
|
||||||
- name: l1-lambda
|
|
||||||
inputs:
|
|
||||||
function_name: price-ingest
|
|
||||||
runtime: python3.11
|
|
||||||
handler: index.handler
|
|
||||||
- name: l1-api-gateway
|
|
||||||
inputs:
|
|
||||||
api_name: platts-price-api
|
|
||||||
stage_name: dev
|
|
||||||
- name: l1-eventbridge
|
|
||||||
inputs:
|
|
||||||
bus_name: price-events
|
|
||||||
rule_name: price-publish-rule
|
|
||||||
- name: l1-s3
|
|
||||||
inputs:
|
|
||||||
bucket_name: acdl-price-archive
|
|
||||||
region: us-east-1
|
|
||||||
retention_days: "90"
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
name: l2-energy-analytics-api
|
|
||||||
kind: l2
|
|
||||||
description: Historical energy analytics query API
|
|
||||||
l1s:
|
|
||||||
- name: l1-eks-fargate
|
|
||||||
inputs:
|
|
||||||
cluster_name: analytics-cluster
|
|
||||||
region: us-east-1
|
|
||||||
cpu_arch: arm64
|
|
||||||
- name: l1-api-gateway
|
|
||||||
inputs:
|
|
||||||
api_name: energy-analytics-api
|
|
||||||
stage_name: dev
|
|
||||||
- name: l1-lambda
|
|
||||||
inputs:
|
|
||||||
function_name: analytics-query
|
|
||||||
runtime: python3.11
|
|
||||||
handler: index.handler
|
|
||||||
- name: l1-s3
|
|
||||||
inputs:
|
|
||||||
bucket_name: acdl-analytics-data
|
|
||||||
region: us-east-1
|
|
||||||
retention_days: "2555"
|
|
||||||
- name: l1-cloudwatch
|
|
||||||
inputs:
|
|
||||||
log_group_name: /acdl/analytics-api
|
|
||||||
metric_namespace: acdl/analytics
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
name: l2-invoice-service
|
|
||||||
kind: l2
|
|
||||||
description: Billing and invoicing microservice for energy trades
|
|
||||||
l1s:
|
|
||||||
- name: l1-eks-fargate
|
|
||||||
inputs:
|
|
||||||
cluster_name: invoice-cluster
|
|
||||||
region: us-east-1
|
|
||||||
cpu_arch: arm64
|
|
||||||
- name: l1-iam-role
|
|
||||||
inputs:
|
|
||||||
role_name: invoice-service-role
|
|
||||||
trust_policy: '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"eks.amazonaws.com"},"Action":"sts:AssumeRole"}]}'
|
|
||||||
- name: l1-lambda
|
|
||||||
inputs:
|
|
||||||
function_name: invoice-generator
|
|
||||||
runtime: python3.11
|
|
||||||
handler: index.handler
|
|
||||||
- name: l1-sqs
|
|
||||||
inputs:
|
|
||||||
queue_name: invoice-queue
|
|
||||||
visibility_timeout: "60"
|
|
||||||
- name: l1-s3
|
|
||||||
inputs:
|
|
||||||
bucket_name: acdl-invoice-archive
|
|
||||||
region: us-east-1
|
|
||||||
retention_days: "365"
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
name: l2-regulatory-reporting
|
|
||||||
kind: l2
|
|
||||||
description: Regulatory compliance and reporting for energy trading
|
|
||||||
l1s:
|
|
||||||
- name: l1-eks-fargate
|
|
||||||
inputs:
|
|
||||||
cluster_name: regulatory-cluster
|
|
||||||
region: us-east-1
|
|
||||||
cpu_arch: arm64
|
|
||||||
- name: l1-iam-role
|
|
||||||
inputs:
|
|
||||||
role_name: regulatory-reporting-role
|
|
||||||
trust_policy: '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"eks.amazonaws.com"},"Action":"sts:AssumeRole"}]}'
|
|
||||||
- name: l1-lambda
|
|
||||||
inputs:
|
|
||||||
function_name: regulatory-reporter
|
|
||||||
runtime: python3.11
|
|
||||||
handler: index.handler
|
|
||||||
- name: l1-sqs
|
|
||||||
inputs:
|
|
||||||
queue_name: regulatory-queue
|
|
||||||
visibility_timeout: "120"
|
|
||||||
- name: l1-s3
|
|
||||||
inputs:
|
|
||||||
bucket_name: acdl-regulatory-archive
|
|
||||||
region: us-east-1
|
|
||||||
retention_days: "2555"
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""confidence_signal.py — REQ-08 / D-024
|
|
||||||
|
|
||||||
Reads a contract.yaml, invokes policy_checker.py as a subprocess, and emits
|
|
||||||
a deterministic JSON confidence score.
|
|
||||||
|
|
||||||
policy pass -> {"score": 0.90, "reason": "POLICY_PASS"}
|
|
||||||
policy fail -> {"score": 0.40, "reason": "<violation code>"}
|
|
||||||
|
|
||||||
Exit 0 ALWAYS (per D-024): the pipeline decides the gate, not this script's
|
|
||||||
exit code.
|
|
||||||
|
|
||||||
Input: argv[1] = path to a contract.yaml file.
|
|
||||||
"""
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
if len(sys.argv) < 2:
|
|
||||||
print("usage: confidence_signal.py <contract.yaml>", file=sys.stderr)
|
|
||||||
return 1
|
|
||||||
|
|
||||||
contract_path = sys.argv[1]
|
|
||||||
|
|
||||||
# Resolve policy_checker.py relative to this script so it works regardless
|
|
||||||
# of cwd. Use python3 + script path (not ./) per the contract.
|
|
||||||
here = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
policy_checker = os.path.join(here, "policy_checker.py")
|
|
||||||
|
|
||||||
proc = subprocess.run(
|
|
||||||
["python3", policy_checker, contract_path],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
if proc.returncode == 0:
|
|
||||||
score = "0.90"
|
|
||||||
# POLICY_PASS is the expected stdout; strip any trailing whitespace.
|
|
||||||
reason = proc.stdout.strip() or "POLICY_PASS"
|
|
||||||
else:
|
|
||||||
score = "0.40"
|
|
||||||
# The violation code (e.g. "POLICY_VIOLATION:PUBLIC_INGRESS") is on stdout.
|
|
||||||
reason = proc.stdout.strip() or "POLICY_VIOLATION:UNKNOWN"
|
|
||||||
|
|
||||||
# Emit with literal score (two-decimal form per the contract) and a quoted
|
|
||||||
# reason. Constructed manually so json.dumps does not collapse 0.90 -> 0.9.
|
|
||||||
print('{"score": ' + score + ', "reason": ' + json.dumps(reason) + '}')
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""evidence_writer.py — REQ-11 / D-023 / D-005
|
|
||||||
|
|
||||||
Appends a hash-chained event to audit.json.
|
|
||||||
|
|
||||||
Each event: {"seq": N, "ts": <iso8601 UTC>, "stage": "...", "event": "...",
|
|
||||||
"prev_hash": "<sha256 or GENESIS>", "hash": "<sha256 of canonical json of this event with hash empty>"}
|
|
||||||
|
|
||||||
Hash chain (D-023):
|
|
||||||
1. Build event dict with hash = "" (empty string).
|
|
||||||
2. canonical = json.dumps(event, sort_keys=True, separators=(",", ":"))
|
|
||||||
3. hash = sha256(canonical.encode("utf-8")).hexdigest()
|
|
||||||
4. event["hash"] = hash
|
|
||||||
5. append to audit.json
|
|
||||||
|
|
||||||
Auto-genesis: if audit.json is empty/missing and --stage is not "genesis",
|
|
||||||
a genesis event (seq 0, prev_hash "GENESIS") is inserted first.
|
|
||||||
|
|
||||||
Input:
|
|
||||||
--stage <dev|qa|prod|finalize|genesis> (required)
|
|
||||||
--event "<text>" (required)
|
|
||||||
--audit <path> (optional, default ./audit.json)
|
|
||||||
Output: stdout {"seq": N, "hash": "..."}
|
|
||||||
Exit: 0 on success, 1 on I/O error.
|
|
||||||
"""
|
|
||||||
import argparse
|
|
||||||
import datetime
|
|
||||||
import hashlib
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
GENESIS_EVENT_TEXT = "audit log initialized"
|
|
||||||
|
|
||||||
|
|
||||||
def now_iso8601_utc() -> str:
|
|
||||||
return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
||||||
|
|
||||||
|
|
||||||
def compute_hash(event: dict) -> str:
|
|
||||||
"""Compute the sha256 hash of an event using canonical JSON (D-023)."""
|
|
||||||
tmp = dict(event)
|
|
||||||
tmp["hash"] = ""
|
|
||||||
canonical = json.dumps(tmp, sort_keys=True, separators=(",", ":"))
|
|
||||||
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
|
|
||||||
|
|
||||||
|
|
||||||
def make_event(seq: int, stage: str, event_text: str, prev_hash: str) -> dict:
|
|
||||||
event = {
|
|
||||||
"seq": seq,
|
|
||||||
"ts": now_iso8601_utc(),
|
|
||||||
"stage": stage,
|
|
||||||
"event": event_text,
|
|
||||||
"prev_hash": prev_hash,
|
|
||||||
"hash": "",
|
|
||||||
}
|
|
||||||
event["hash"] = compute_hash(event)
|
|
||||||
return event
|
|
||||||
|
|
||||||
|
|
||||||
def load_audit(audit_path: str) -> list:
|
|
||||||
if not os.path.exists(audit_path):
|
|
||||||
return []
|
|
||||||
try:
|
|
||||||
with open(audit_path, "r", encoding="utf-8") as fh:
|
|
||||||
data = json.load(fh)
|
|
||||||
except (json.JSONDecodeError, ValueError):
|
|
||||||
return []
|
|
||||||
if not isinstance(data, list):
|
|
||||||
return []
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
def atomic_write(audit_path: str, data: list) -> None:
|
|
||||||
tmp_path = audit_path + ".tmp"
|
|
||||||
with open(tmp_path, "w", encoding="utf-8") as fh:
|
|
||||||
json.dump(data, fh, indent=2)
|
|
||||||
fh.write("\n")
|
|
||||||
os.replace(tmp_path, audit_path)
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
parser = argparse.ArgumentParser(description="Append a hash-chained event to audit.json")
|
|
||||||
parser.add_argument("--stage", required=True,
|
|
||||||
choices=["dev", "qa", "prod", "finalize", "genesis"])
|
|
||||||
parser.add_argument("--event", required=True)
|
|
||||||
parser.add_argument("--audit", default="./audit.json")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
events = load_audit(args.audit)
|
|
||||||
|
|
||||||
# Auto-genesis: if the log is empty and the caller did not ask for a
|
|
||||||
# genesis event, seed one first.
|
|
||||||
if len(events) == 0 and args.stage != "genesis":
|
|
||||||
genesis = make_event(seq=0, stage="genesis", event_text=GENESIS_EVENT_TEXT,
|
|
||||||
prev_hash="GENESIS")
|
|
||||||
events.append(genesis)
|
|
||||||
|
|
||||||
# Determine the new seq + prev_hash.
|
|
||||||
if events:
|
|
||||||
last = events[-1]
|
|
||||||
seq = last["seq"] + 1
|
|
||||||
prev_hash = last["hash"]
|
|
||||||
else:
|
|
||||||
seq = 0
|
|
||||||
prev_hash = "GENESIS"
|
|
||||||
|
|
||||||
new_event = make_event(seq=seq, stage=args.stage, event_text=args.event,
|
|
||||||
prev_hash=prev_hash)
|
|
||||||
events.append(new_event)
|
|
||||||
|
|
||||||
try:
|
|
||||||
atomic_write(args.audit, events)
|
|
||||||
except OSError as exc:
|
|
||||||
print(f"evidence_writer: I/O error: {exc}", file=sys.stderr)
|
|
||||||
return 1
|
|
||||||
|
|
||||||
print(json.dumps({"seq": new_event["seq"], "hash": new_event["hash"]}))
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
@@ -1,182 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""finalize_evidence.py — REQ-10 / D-028 / D-029
|
|
||||||
|
|
||||||
Uploads (PUT or POST) a local `audit.json` to the `acdl-evidence` repo on
|
|
||||||
Gitea via the file-contents API. Used by the pipeline workflow steps to
|
|
||||||
persist the hash-chained audit trail to `acdl-evidence` between dispatches
|
|
||||||
(D-028 state-persistence across re-dispatches; D-029 finalize step).
|
|
||||||
|
|
||||||
Uses only the Python standard library (urllib.request) so it has no
|
|
||||||
external dependency on `requests`. Auth header: `Authorization: token <token>`.
|
|
||||||
|
|
||||||
Input (argv flags):
|
|
||||||
--audit <path> (required) local audit.json file to upload
|
|
||||||
--owner <org> (optional, default continuous-intelligence)
|
|
||||||
--repo <name> (optional, default acdl-evidence)
|
|
||||||
--branch <name> (optional, default main)
|
|
||||||
--path <remote path> (optional, default audit.json) path in the repo
|
|
||||||
--token-env <env var> (optional, default ACDL_GITEA_TOKEN)
|
|
||||||
--host <url> (optional, default https://git.cloudinit.dev)
|
|
||||||
--message <commit msg> (optional, default chore(evidence): update audit.json)
|
|
||||||
|
|
||||||
Behavior:
|
|
||||||
1. Read the token from os.environ[token_env]. Missing -> stderr + exit 1.
|
|
||||||
2. Read the local audit file; base64-encode it.
|
|
||||||
3. GET the current file at .../contents/<path>?ref=<branch> to discover
|
|
||||||
the existing `sha`. 200 -> capture sha (update mode). 404 -> no sha
|
|
||||||
(create mode). Other errors -> exit 1.
|
|
||||||
4. If sha set: PUT with body {content, message, branch, sha}.
|
|
||||||
If no sha: POST with body {content, message, branch}.
|
|
||||||
5. Print {"uploaded": true, "path": "<path>", "sha": "<new sha>"} to
|
|
||||||
stdout and exit 0.
|
|
||||||
6. On any HTTP error: print
|
|
||||||
{"uploaded": false, "status": <code>, "body": "<body>"} to stdout
|
|
||||||
and exit 1.
|
|
||||||
"""
|
|
||||||
import argparse
|
|
||||||
import base64
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import urllib.error
|
|
||||||
import urllib.parse
|
|
||||||
import urllib.request
|
|
||||||
|
|
||||||
|
|
||||||
def _request(method: str, url: str, token: str, body: dict = None):
|
|
||||||
"""Perform an HTTP request with the Gitea auth header. Returns
|
|
||||||
(status_code, response_body_text). Raises URLError on network failure."""
|
|
||||||
data = None
|
|
||||||
headers = {"Authorization": f"token {token}",
|
|
||||||
"Accept": "application/json"}
|
|
||||||
if body is not None:
|
|
||||||
data = json.dumps(body).encode("utf-8")
|
|
||||||
headers["Content-Type"] = "application/json"
|
|
||||||
req = urllib.request.Request(url, data=data, method=method, headers=headers)
|
|
||||||
try:
|
|
||||||
with urllib.request.urlopen(req) as resp:
|
|
||||||
return resp.getcode(), resp.read().decode("utf-8", "replace")
|
|
||||||
except urllib.error.HTTPError as exc:
|
|
||||||
# HTTPError carries the response body
|
|
||||||
try:
|
|
||||||
body_text = exc.read().decode("utf-8", "replace")
|
|
||||||
except Exception:
|
|
||||||
body_text = ""
|
|
||||||
return exc.code, body_text
|
|
||||||
except urllib.error.URLError as exc:
|
|
||||||
# Network-level failure (connection refused, DNS, timeout). Return
|
|
||||||
# a synthetic 0 status + the reason so callers can report cleanly
|
|
||||||
# without a stack trace.
|
|
||||||
return 0, f"URLError: {exc.reason}"
|
|
||||||
|
|
||||||
|
|
||||||
def get_existing_sha(host: str, owner: str, repo: str, path: str,
|
|
||||||
branch: str, token: str):
|
|
||||||
"""Return (sha-or-None, error_status_or_None). On 200 returns the sha.
|
|
||||||
On 404 returns (None, None). Other codes return (None, (status, body))."""
|
|
||||||
qs = urllib.parse.urlencode({"ref": branch})
|
|
||||||
url = f"{host}/api/v1/repos/{owner}/{repo}/contents/{path}?{qs}"
|
|
||||||
status, body = _request("GET", url, token)
|
|
||||||
if status == 200:
|
|
||||||
try:
|
|
||||||
data = json.loads(body)
|
|
||||||
return data.get("sha"), None
|
|
||||||
except (ValueError, TypeError):
|
|
||||||
return None, (status, body)
|
|
||||||
if status == 404:
|
|
||||||
return None, None
|
|
||||||
return None, (status, body)
|
|
||||||
|
|
||||||
|
|
||||||
def upload(host: str, owner: str, repo: str, path: str, branch: str,
|
|
||||||
message: str, content_b64: str, sha, token: str):
|
|
||||||
"""PUT (update) or POST (create) the file. Returns (new_sha, None) on
|
|
||||||
success or (None, (status, body)) on HTTP error."""
|
|
||||||
url = f"{host}/api/v1/repos/{owner}/{repo}/contents/{path}"
|
|
||||||
if sha:
|
|
||||||
body = {"content": content_b64, "message": message,
|
|
||||||
"branch": branch, "sha": sha}
|
|
||||||
status, resp = _request("PUT", url, token, body)
|
|
||||||
else:
|
|
||||||
body = {"content": content_b64, "message": message, "branch": branch}
|
|
||||||
status, resp = _request("POST", url, token, body)
|
|
||||||
if status in (200, 201):
|
|
||||||
try:
|
|
||||||
data = json.loads(resp)
|
|
||||||
# The file-contents API returns the new content object either at
|
|
||||||
# top-level `content` (POST create) or `content` (PUT update).
|
|
||||||
new_sha = None
|
|
||||||
if isinstance(data, dict):
|
|
||||||
content_obj = data.get("content") or data
|
|
||||||
if isinstance(content_obj, dict):
|
|
||||||
new_sha = content_obj.get("sha")
|
|
||||||
return new_sha, None
|
|
||||||
except (ValueError, TypeError):
|
|
||||||
return None, None
|
|
||||||
return None, (status, resp)
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="Upload a local audit.json to the acdl-evidence Gitea "
|
|
||||||
"repo via the file-contents API (D-028/D-029).")
|
|
||||||
parser.add_argument("--audit", required=True,
|
|
||||||
help="Local audit.json file to upload")
|
|
||||||
parser.add_argument("--owner", default="continuous-intelligence",
|
|
||||||
help="Gitea org (default: continuous-intelligence)")
|
|
||||||
parser.add_argument("--repo", default="acdl-evidence",
|
|
||||||
help="Gitea repo (default: acdl-evidence)")
|
|
||||||
parser.add_argument("--branch", default="main",
|
|
||||||
help="Target branch (default: main)")
|
|
||||||
parser.add_argument("--path", default="audit.json",
|
|
||||||
help="Remote path in the repo (default: audit.json)")
|
|
||||||
parser.add_argument("--token-env", default="ACDL_GITEA_TOKEN",
|
|
||||||
help="Env var name holding the Gitea token "
|
|
||||||
"(default: ACDL_GITEA_TOKEN)")
|
|
||||||
parser.add_argument("--host", default="https://git.cloudinit.dev",
|
|
||||||
help="Gitea host URL (default: https://git.cloudinit.dev)")
|
|
||||||
parser.add_argument("--message", default="chore(evidence): update audit.json",
|
|
||||||
help="Commit message (default: chore(evidence): "
|
|
||||||
"update audit.json)")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
token = os.environ.get(args.token_env)
|
|
||||||
if not token:
|
|
||||||
print(f"finalize_evidence: required env var {args.token_env} is not "
|
|
||||||
f"set", file=sys.stderr)
|
|
||||||
return 1
|
|
||||||
|
|
||||||
# Read + base64-encode the local audit file. Missing/unreadable file is
|
|
||||||
# a clean exit 1 (no stack trace).
|
|
||||||
try:
|
|
||||||
with open(args.audit, "rb") as fh:
|
|
||||||
raw = fh.read()
|
|
||||||
except OSError as exc:
|
|
||||||
print(f"finalize_evidence: cannot read {args.audit}: {exc}",
|
|
||||||
file=sys.stderr)
|
|
||||||
return 1
|
|
||||||
content_b64 = base64.b64encode(raw).decode("ascii")
|
|
||||||
|
|
||||||
# Discover existing sha (update vs create).
|
|
||||||
sha, err = get_existing_sha(args.host, args.owner, args.repo,
|
|
||||||
args.path, args.branch, token)
|
|
||||||
if err is not None:
|
|
||||||
status, body = err
|
|
||||||
print(json.dumps({"uploaded": False, "status": status, "body": body}))
|
|
||||||
return 1
|
|
||||||
|
|
||||||
# Upload (PUT if sha, POST otherwise).
|
|
||||||
new_sha, err = upload(args.host, args.owner, args.repo, args.path,
|
|
||||||
args.branch, args.message, content_b64, sha, token)
|
|
||||||
if err is not None:
|
|
||||||
status, body = err
|
|
||||||
print(json.dumps({"uploaded": False, "status": status, "body": body}))
|
|
||||||
return 1
|
|
||||||
|
|
||||||
print(json.dumps({"uploaded": True, "path": args.path,
|
|
||||||
"sha": new_sha}))
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
@@ -1,228 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Phase 01 Gitea scaffolding. Idempotent.
|
|
||||||
#
|
|
||||||
# Creates the two new repos under the continuous-intelligence org, pushes a
|
|
||||||
# placeholder index.html to acdl-evidence, and creates qa + prod branches on
|
|
||||||
# acdl-contracts. Running against existing repos / branches / files is a
|
|
||||||
# no-op (409 or 422 is treated as success).
|
|
||||||
#
|
|
||||||
# Usage: ACDL_GITEA_TOKEN=<token> scripts/gitea_setup.sh
|
|
||||||
# Exit codes: 0 = success (created or already existed); 1 = unrecoverable error.
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
GITEA_HOST="${GITEA_HOST:-https://git.cloudinit.dev}"
|
|
||||||
ORG="continuous-intelligence"
|
|
||||||
TOKEN="${ACDL_GITEA_TOKEN:?ACDL_GITEA_TOKEN is required}"
|
|
||||||
API="${GITEA_HOST}/api/v1"
|
|
||||||
|
|
||||||
AUTH=(-H "Authorization: token ${TOKEN}" -H "Content-Type: application/json")
|
|
||||||
|
|
||||||
log() { printf '[setup] %s\n' "$*"; }
|
|
||||||
warn() { printf '[setup][WARN] %s\n' "$*" >&2; }
|
|
||||||
err() { printf '[setup][ERROR] %s\n' "$*" >&2; }
|
|
||||||
|
|
||||||
# --- helpers ----------------------------------------------------------------
|
|
||||||
|
|
||||||
# http_status_code URL
|
|
||||||
http_get_status() {
|
|
||||||
local url="$1"
|
|
||||||
curl -sS -o /dev/null -w "%{http_code}" "${AUTH[@]}" "$url"
|
|
||||||
}
|
|
||||||
|
|
||||||
# repo_exists NAME -> 0 if exists, 1 otherwise
|
|
||||||
repo_exists() {
|
|
||||||
local name="$1"
|
|
||||||
local status
|
|
||||||
status=$(http_get_status "${API}/repos/${ORG}/${name}")
|
|
||||||
[ "$status" = "200" ]
|
|
||||||
}
|
|
||||||
|
|
||||||
# create_repo NAME DESCRIPTION
|
|
||||||
create_repo() {
|
|
||||||
local name="$1"
|
|
||||||
local description="$2"
|
|
||||||
local body
|
|
||||||
body=$(python3 -c "
|
|
||||||
import json, sys
|
|
||||||
print(json.dumps({
|
|
||||||
'name': '${name}',
|
|
||||||
'description': ${description@Q},
|
|
||||||
'private': True,
|
|
||||||
'default_branch': 'main',
|
|
||||||
'auto_init': True,
|
|
||||||
'gitignores': 'Python',
|
|
||||||
'license': '',
|
|
||||||
'readme': 'Default'
|
|
||||||
}))
|
|
||||||
")
|
|
||||||
log "Creating repo ${ORG}/${name} (default_branch=main, auto_init=true)"
|
|
||||||
local status body_out
|
|
||||||
status=$(curl -sS -o /tmp/setup_repo_create.json -w "%{http_code}" \
|
|
||||||
"${AUTH[@]}" -X POST -d "$body" \
|
|
||||||
"${API}/orgs/${ORG}/repos")
|
|
||||||
case "$status" in
|
|
||||||
201) log " created (HTTP 201)" ;;
|
|
||||||
409) log " already exists (HTTP 409); skipping" ;;
|
|
||||||
*)
|
|
||||||
err "create_repo ${name} failed: HTTP ${status}"
|
|
||||||
cat /tmp/setup_repo_create.json >&2 || true
|
|
||||||
return 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
# set_repo_visibility REPO VISIBILITY (public|private)
|
|
||||||
set_repo_visibility() {
|
|
||||||
local repo="$1"
|
|
||||||
local visibility="$2"
|
|
||||||
local body
|
|
||||||
body=$(python3 -c "
|
|
||||||
import json
|
|
||||||
is_private = ('${visibility}' == 'private')
|
|
||||||
print(json.dumps({'private': is_private, 'visibility': '${visibility}'}))
|
|
||||||
")
|
|
||||||
log "Setting ${repo} visibility to ${visibility}"
|
|
||||||
local status
|
|
||||||
status=$(curl -sS -o /tmp/setup_vis.json -w "%{http_code}" \
|
|
||||||
"${AUTH[@]}" -X PATCH -d "$body" \
|
|
||||||
"${API}/repos/${ORG}/${repo}")
|
|
||||||
case "$status" in
|
|
||||||
200) log " ok (HTTP 200)" ;;
|
|
||||||
*) warn "set_repo_visibility ${repo} -> ${visibility} returned HTTP ${status} (continuing)"; cat /tmp/setup_vis.json >&2 || true ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
# file_exists REPO PATH -> 0 if the file already exists on the default branch
|
|
||||||
file_exists_on_default() {
|
|
||||||
local repo="$1"
|
|
||||||
local path="$2"
|
|
||||||
local status
|
|
||||||
status=$(http_get_status "${API}/repos/${ORG}/${repo}/contents/${path}?ref=main")
|
|
||||||
[ "$status" = "200" ]
|
|
||||||
}
|
|
||||||
|
|
||||||
# create_placeholder_index REPO
|
|
||||||
create_placeholder_index() {
|
|
||||||
local repo="$1"
|
|
||||||
local path="index.html"
|
|
||||||
local placeholder
|
|
||||||
placeholder='<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>ACDL Evidence</title>
|
|
||||||
<style>body{font-family:system-ui,sans-serif;margin:2rem;color:#333}</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>ACDL Evidence Stream</h1>
|
|
||||||
<p>Evidence timeline will appear here in Phase 05.</p>
|
|
||||||
<p>Placeholder served via Gitea raw file URL (D-012; Gitea has no native Pages).</p>
|
|
||||||
</body>
|
|
||||||
</html>'
|
|
||||||
|
|
||||||
if file_exists_on_default "$repo" "$path"; then
|
|
||||||
log "index.html already exists on ${repo} main; skipping"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
local body
|
|
||||||
body=$(python3 -c "
|
|
||||||
import json, base64
|
|
||||||
content = '''${placeholder}'''
|
|
||||||
print(json.dumps({
|
|
||||||
'content': base64.b64encode(content.encode('utf-8')).decode('ascii'),
|
|
||||||
'message': 'Initial placeholder index.html (Phase 01, D-016)',
|
|
||||||
'branch': 'main'
|
|
||||||
}))
|
|
||||||
")
|
|
||||||
log "Pushing placeholder index.html to ${repo} main"
|
|
||||||
local status
|
|
||||||
status=$(curl -sS -o /tmp/setup_index_push.json -w "%{http_code}" \
|
|
||||||
"${AUTH[@]}" -X POST -d "$body" \
|
|
||||||
"${API}/repos/${ORG}/${repo}/contents/${path}")
|
|
||||||
case "$status" in
|
|
||||||
201) log " pushed (HTTP 201)" ;;
|
|
||||||
409|422) log " already exists or conflict (HTTP ${status}); skipping" ;;
|
|
||||||
*)
|
|
||||||
err "create_placeholder_index on ${repo} failed: HTTP ${status}"
|
|
||||||
cat /tmp/setup_index_push.json >&2 || true
|
|
||||||
return 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
# branch_exists REPO BRANCH -> 0 if exists
|
|
||||||
branch_exists() {
|
|
||||||
local repo="$1"
|
|
||||||
local branch="$2"
|
|
||||||
local status
|
|
||||||
status=$(http_get_status "${API}/repos/${ORG}/${repo}/branches/${branch}")
|
|
||||||
[ "$status" = "200" ]
|
|
||||||
}
|
|
||||||
|
|
||||||
# create_branch REPO BRANCH FROM_REF
|
|
||||||
create_branch() {
|
|
||||||
local repo="$1"
|
|
||||||
local branch="$2"
|
|
||||||
local from_ref="$3"
|
|
||||||
if branch_exists "$repo" "$branch"; then
|
|
||||||
log "Branch ${branch} already exists on ${repo}; skipping"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
local body
|
|
||||||
body=$(python3 -c "
|
|
||||||
import json
|
|
||||||
print(json.dumps({'new_branch_name': '${branch}', 'old_branch_name': '${from_ref}'}))
|
|
||||||
")
|
|
||||||
log "Creating branch ${branch} on ${repo} from ${from_ref}"
|
|
||||||
local status
|
|
||||||
status=$(curl -sS -o /tmp/setup_branch.json -w "%{http_code}" \
|
|
||||||
"${AUTH[@]}" -X POST -d "$body" \
|
|
||||||
"${API}/repos/${ORG}/${repo}/branches")
|
|
||||||
case "$status" in
|
|
||||||
201) log " created (HTTP 201)" ;;
|
|
||||||
409) log " already exists (HTTP 409); skipping" ;;
|
|
||||||
*)
|
|
||||||
err "create_branch ${branch} on ${repo} failed: HTTP ${status}"
|
|
||||||
cat /tmp/setup_branch.json >&2 || true
|
|
||||||
return 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- main -------------------------------------------------------------------
|
|
||||||
|
|
||||||
log "Host: ${GITEA_HOST}"
|
|
||||||
log "Org: ${ORG}"
|
|
||||||
log "Token: <set, ${#TOKEN} chars>"
|
|
||||||
|
|
||||||
# Step 1: create acdl-contracts
|
|
||||||
if ! repo_exists acdl-contracts; then
|
|
||||||
create_repo acdl-contracts "ACDL developer + agentic entry surface (contract.yaml + issue trigger)" || exit 1
|
|
||||||
else
|
|
||||||
log "acdl-contracts already exists; skipping create"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Step 2: create acdl-evidence
|
|
||||||
if ! repo_exists acdl-evidence; then
|
|
||||||
create_repo acdl-evidence "ACDL hash-chained audit timeline served as a static site via raw file URLs" || exit 1
|
|
||||||
else
|
|
||||||
log "acdl-evidence already exists; skipping create"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Step 2b: make acdl-evidence public so the Phase 05 UI (index.html) can
|
|
||||||
# fetch audit.json from a browser without exposing the API token (D-012
|
|
||||||
# raw-URL approach). acdl-contracts stays private.
|
|
||||||
set_repo_visibility acdl-evidence public
|
|
||||||
|
|
||||||
# Step 3: push placeholder index.html to acdl-evidence
|
|
||||||
create_placeholder_index acdl-evidence || exit 1
|
|
||||||
|
|
||||||
# Step 4: create qa + prod branches on acdl-contracts (visible stand-in for
|
|
||||||
# the unsupported Gitea environments API; per D-013).
|
|
||||||
create_branch acdl-contracts qa main || exit 1
|
|
||||||
create_branch acdl-contracts prod main || exit 1
|
|
||||||
|
|
||||||
log "Done. Run scripts/verify_phase01.sh to confirm success criteria."
|
|
||||||
exit 0
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""l3b_agent_stub.py — D-008 / D-026 / D-021
|
|
||||||
|
|
||||||
Parses a GitHub/Gitea Issue body by keywords and emits a contract.yaml that
|
|
||||||
selects an L2 stack. This is the agentic (L3B) entry surface: deterministic
|
|
||||||
keyword matching, no external AI APIs.
|
|
||||||
|
|
||||||
D-008 keyword map (priority order — first match wins):
|
|
||||||
gas, price, ingest, data-lake -> l2-commodity-price-feed
|
|
||||||
invoice, billing -> l2-invoice-service
|
|
||||||
analytics, historical, query -> l2-energy-analytics-api
|
|
||||||
regulatory, compliance, reporting, trading
|
|
||||||
-> l2-regulatory-reporting
|
|
||||||
(no match) -> l2-invoice-service (fallback)
|
|
||||||
|
|
||||||
Output contract.yaml (D-021 schema):
|
|
||||||
stack: <mapped L2 name>
|
|
||||||
inputs:
|
|
||||||
environment: dev
|
|
||||||
owner: citizen-developer
|
|
||||||
source: l3b-agent-stub
|
|
||||||
public-ingress: false
|
|
||||||
|
|
||||||
Input:
|
|
||||||
argv[1] = issue body text (or stdin if argv[1] absent/empty)
|
|
||||||
-o <path> = write the contract to a file (default: stdout)
|
|
||||||
Exit:
|
|
||||||
0 on success, 1 on empty input
|
|
||||||
"""
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
# Ordered keyword groups -> L2 stack mapping (D-008). First match wins.
|
|
||||||
KEYWORD_MAP = [
|
|
||||||
(("gas", "price", "ingest", "data-lake"), "l2-commodity-price-feed"),
|
|
||||||
(("invoice", "billing"), "l2-invoice-service"),
|
|
||||||
(("analytics", "historical", "query"), "l2-energy-analytics-api"),
|
|
||||||
(("regulatory", "compliance", "reporting", "trading"), "l2-regulatory-reporting"),
|
|
||||||
]
|
|
||||||
|
|
||||||
FALLBACK_STACK = "l2-invoice-service"
|
|
||||||
|
|
||||||
|
|
||||||
def map_issue_to_stack(text: str) -> str:
|
|
||||||
lowered = text.lower()
|
|
||||||
for keywords, stack in KEYWORD_MAP:
|
|
||||||
for kw in keywords:
|
|
||||||
if kw in lowered:
|
|
||||||
return stack
|
|
||||||
return FALLBACK_STACK
|
|
||||||
|
|
||||||
|
|
||||||
def render_contract(stack: str) -> str:
|
|
||||||
# Fixed-schema YAML (D-021). Emitted as text (no yaml dependency needed).
|
|
||||||
return (
|
|
||||||
f"stack: {stack}\n"
|
|
||||||
"inputs:\n"
|
|
||||||
" environment: dev\n"
|
|
||||||
" owner: citizen-developer\n"
|
|
||||||
" source: l3b-agent-stub\n"
|
|
||||||
"public-ingress: false\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def read_issue_body(args: list) -> str:
|
|
||||||
"""Read issue body from args[0] (already-stripped argv, no script name)
|
|
||||||
or stdin. Empty -> error."""
|
|
||||||
if len(args) >= 1 and args[0].strip():
|
|
||||||
return args[0]
|
|
||||||
# Fall back to stdin if argv body is absent or empty.
|
|
||||||
if not sys.stdin.isatty():
|
|
||||||
data = sys.stdin.read()
|
|
||||||
if data.strip():
|
|
||||||
return data
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
def parse_output_flag(argv: list):
|
|
||||||
"""Extract -o <path> from argv (returns (rest, output_path))."""
|
|
||||||
output_path = None
|
|
||||||
rest = []
|
|
||||||
i = 1
|
|
||||||
while i < len(argv):
|
|
||||||
arg = argv[i]
|
|
||||||
if arg == "-o":
|
|
||||||
if i + 1 < len(argv):
|
|
||||||
output_path = argv[i + 1]
|
|
||||||
i += 2
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
print("l3b_agent_stub: -o requires a path argument", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
rest.append(arg)
|
|
||||||
i += 1
|
|
||||||
return rest, output_path
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
rest, output_path = parse_output_flag(sys.argv)
|
|
||||||
body = read_issue_body(rest)
|
|
||||||
if not body.strip():
|
|
||||||
print("l3b_agent_stub: empty issue body (no argv[1] and no stdin)", file=sys.stderr)
|
|
||||||
return 1
|
|
||||||
|
|
||||||
stack = map_issue_to_stack(body)
|
|
||||||
contract = render_contract(stack)
|
|
||||||
|
|
||||||
if output_path:
|
|
||||||
with open(output_path, "w", encoding="utf-8") as fh:
|
|
||||||
fh.write(contract)
|
|
||||||
else:
|
|
||||||
sys.stdout.write(contract)
|
|
||||||
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# mock_executor.sh — REQ-06 / D-022
|
|
||||||
#
|
|
||||||
# Reads a contract.yaml, resolves the L2 composition, invokes each L1's
|
|
||||||
# mock_apply.sh in order, and writes state.json to the current working
|
|
||||||
# directory.
|
|
||||||
#
|
|
||||||
# Input: argv[1] = path to a contract.yaml file.
|
|
||||||
# Output:
|
|
||||||
# - stdout: per-L1 progress (echoed from each mock_apply.sh)
|
|
||||||
# - state.json in cwd: {"l2": "...", "l1s": [...], "contract": {...}}
|
|
||||||
# Exit:
|
|
||||||
# 0 if all L1s exit 0; 1 if any L1 exited non-zero (state.json is still
|
|
||||||
# written with the recorded exit codes).
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
if [[ $# -lt 1 ]]; then
|
|
||||||
echo "usage: mock_executor.sh <contract.yaml>" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
CONTRACT_PATH="$1"
|
|
||||||
|
|
||||||
if [[ ! -f "$CONTRACT_PATH" ]]; then
|
|
||||||
echo "contract not found: $CONTRACT_PATH" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- Parse the contract (stack + full contract dict) via python3 + yaml. ---
|
|
||||||
# Emit stack on line 1 and the full contract JSON on line 2, then read both
|
|
||||||
# lines into separate bash variables (so the JSON's internal spaces survive).
|
|
||||||
CONTRACT_PARSED=$(python3 - "$CONTRACT_PATH" <<'PY'
|
|
||||||
import sys, json, yaml
|
|
||||||
path = sys.argv[1]
|
|
||||||
with open(path, "r", encoding="utf-8") as fh:
|
|
||||||
contract = yaml.safe_load(fh)
|
|
||||||
if not isinstance(contract, dict):
|
|
||||||
sys.stderr.write("contract is not a mapping\n")
|
|
||||||
sys.exit(2)
|
|
||||||
stack = contract.get("stack", "")
|
|
||||||
# Use a compact JSON (no spaces) so the single-line contract survives bash
|
|
||||||
# variable capture cleanly.
|
|
||||||
print(stack)
|
|
||||||
print(json.dumps(contract, sort_keys=True, separators=(",", ":")))
|
|
||||||
PY
|
|
||||||
)
|
|
||||||
|
|
||||||
STACK=$(printf '%s\n' "$CONTRACT_PARSED" | sed -n '1p')
|
|
||||||
CONTRACT_JSON=$(printf '%s\n' "$CONTRACT_PARSED" | sed -n '2p')
|
|
||||||
|
|
||||||
if [[ -z "$STACK" ]]; then
|
|
||||||
echo "contract missing 'stack' key" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- Resolve the L2 manifest. ---
|
|
||||||
L2_MANIFEST="modules/l2/${STACK}/manifest.yaml"
|
|
||||||
if [[ ! -f "$L2_MANIFEST" ]]; then
|
|
||||||
echo "L2_NOT_FOUND: ${STACK}" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- Read the L2's l1s: list (ordered names) via python. ---
|
|
||||||
L1_NAMES_JSON=$(python3 - "$L2_MANIFEST" <<'PY'
|
|
||||||
import sys, json, yaml
|
|
||||||
path = sys.argv[1]
|
|
||||||
with open(path, "r", encoding="utf-8") as fh:
|
|
||||||
manifest = yaml.safe_load(fh)
|
|
||||||
l1s = manifest.get("l1s", []) if isinstance(manifest, dict) else []
|
|
||||||
names = [entry.get("name", "") for entry in l1s if isinstance(entry, dict)]
|
|
||||||
print(json.dumps(names))
|
|
||||||
PY
|
|
||||||
)
|
|
||||||
|
|
||||||
# --- Invoke each L1's mock_apply.sh in order, recording exit codes. ---
|
|
||||||
# Build the l1s results array in JSON via python, appending as we go.
|
|
||||||
RESULTS_JSON="[]"
|
|
||||||
|
|
||||||
ALL_OK=0
|
|
||||||
while IFS= read -r L1_NAME; do
|
|
||||||
L1_SCRIPT="modules/l1/${L1_NAME}/mock_apply.sh"
|
|
||||||
if [[ ! -f "$L1_SCRIPT" ]]; then
|
|
||||||
echo "L1_NOT_FOUND: ${L1_NAME}" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Capture stdout + exit code. stderr passes through.
|
|
||||||
L1_OUT=$(bash "$L1_SCRIPT")
|
|
||||||
L1_RC=$?
|
|
||||||
|
|
||||||
# Echo the L1's stdout so the pipeline sees the progress lines.
|
|
||||||
printf '%s\n' "$L1_OUT"
|
|
||||||
|
|
||||||
# Record {"name": ..., "applied": true, "exit_code": ...}.
|
|
||||||
RESULTS_JSON=$(python3 - "$RESULTS_JSON" "$L1_NAME" "$L1_RC" <<'PY'
|
|
||||||
import sys, json
|
|
||||||
results = json.loads(sys.argv[1])
|
|
||||||
name = sys.argv[2]
|
|
||||||
rc = int(sys.argv[3])
|
|
||||||
results.append({"name": name, "applied": True, "exit_code": rc})
|
|
||||||
print(json.dumps(results))
|
|
||||||
PY
|
|
||||||
)
|
|
||||||
|
|
||||||
if [[ $L1_RC -ne 0 ]]; then
|
|
||||||
ALL_OK=1
|
|
||||||
fi
|
|
||||||
done < <(python3 -c "import sys, json; print('\n'.join(json.loads(sys.argv[1])))" "$L1_NAMES_JSON")
|
|
||||||
|
|
||||||
# --- Write state.json to the current working directory (D-022). ---
|
|
||||||
python3 - "$RESULTS_JSON" "$STACK" "$CONTRACT_JSON" <<'PY'
|
|
||||||
import sys, json
|
|
||||||
results = json.loads(sys.argv[1])
|
|
||||||
stack = sys.argv[2]
|
|
||||||
contract = json.loads(sys.argv[3])
|
|
||||||
state = {
|
|
||||||
"l2": stack,
|
|
||||||
"l1s": results,
|
|
||||||
"contract": contract,
|
|
||||||
}
|
|
||||||
with open("state.json", "w", encoding="utf-8") as fh:
|
|
||||||
json.dump(state, fh, indent=2)
|
|
||||||
fh.write("\n")
|
|
||||||
PY
|
|
||||||
|
|
||||||
exit "$ALL_OK"
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""policy_checker.py — REQ-07 / D-025
|
|
||||||
|
|
||||||
Reads a contract.yaml and enforces the single Phase-03 policy rule:
|
|
||||||
`public-ingress: true` is forbidden.
|
|
||||||
|
|
||||||
Input: argv[1] = path to a contract.yaml file.
|
|
||||||
Output: stdout "POLICY_PASS" or "POLICY_VIOLATION:PUBLIC_INGRESS"
|
|
||||||
Exit: 0 on pass, 1 on violation.
|
|
||||||
|
|
||||||
Idempotent, no side effects (no file writes). Treats an absent or falsy
|
|
||||||
`public-ingress` key as a pass.
|
|
||||||
"""
|
|
||||||
import sys
|
|
||||||
import yaml
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
if len(sys.argv) < 2:
|
|
||||||
print("usage: policy_checker.py <contract.yaml>", file=sys.stderr)
|
|
||||||
return 2
|
|
||||||
|
|
||||||
contract_path = sys.argv[1]
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(contract_path, "r", encoding="utf-8") as fh:
|
|
||||||
contract = yaml.safe_load(fh)
|
|
||||||
except FileNotFoundError:
|
|
||||||
print(f"contract not found: {contract_path}", file=sys.stderr)
|
|
||||||
return 2
|
|
||||||
except yaml.YAMLError as exc:
|
|
||||||
print(f"invalid yaml: {exc}", file=sys.stderr)
|
|
||||||
return 2
|
|
||||||
|
|
||||||
# Treat missing/non-mapping as no policy violation.
|
|
||||||
if not isinstance(contract, dict):
|
|
||||||
print("POLICY_PASS")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
public_ingress = contract.get("public-ingress", False)
|
|
||||||
|
|
||||||
if public_ingress is True:
|
|
||||||
print("POLICY_VIOLATION:PUBLIC_INGRESS")
|
|
||||||
return 1
|
|
||||||
|
|
||||||
print("POLICY_PASS")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
@@ -1,258 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# scripts/run_demo.sh — Phase 05 dry-run simulation of the 4 demo acts (T-5.2).
|
|
||||||
#
|
|
||||||
# Simulates the full 4-act demo locally (no act_runner) by calling the core
|
|
||||||
# scripts in sequence and writing hash-chained evidence events to audit.json,
|
|
||||||
# then optionally uploads audit.json + evidence-ui/index.html to acdl-evidence
|
|
||||||
# main via finalize_evidence.py (D-031, D-033).
|
|
||||||
#
|
|
||||||
# Usage: scripts/run_demo.sh [--no-upload]
|
|
||||||
# --no-upload skip the Gitea API calls (useful for testing without a token)
|
|
||||||
|
|
||||||
set -uo pipefail
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# Parse args
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
UPLOAD=1
|
|
||||||
for arg in "$@"; do
|
|
||||||
case "$arg" in
|
|
||||||
--no-upload)
|
|
||||||
UPLOAD=0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "run_demo.sh: unknown argument: $arg" >&2
|
|
||||||
echo "usage: scripts/run_demo.sh [--no-upload]" >&2
|
|
||||||
exit 2
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# Paths
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# Repo root = location of this script's parent dir.
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
||||||
|
|
||||||
WORKDIR="/tmp/acdl_demo_run"
|
|
||||||
AUDIT="$WORKDIR/audit.json"
|
|
||||||
CONTRACTS="$WORKDIR/contracts"
|
|
||||||
|
|
||||||
# Track failures so we can return non-zero at the end (we do NOT use set -e
|
|
||||||
# because policy_checker intentionally exits 1 on Act 4).
|
|
||||||
FAIL=0
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# Helpers
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
# Write one evidence event. Args: <stage> <event-text>
|
|
||||||
ev() {
|
|
||||||
local stage="$1"
|
|
||||||
local text="$2"
|
|
||||||
if ! python3 "$SCRIPT_DIR/evidence_writer.py" --stage "$stage" --event "$text" --audit "$AUDIT"; then
|
|
||||||
echo "run_demo.sh: evidence_writer failed for stage=$stage text=$text" >&2
|
|
||||||
FAIL=1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Run a contract through the Act 2/3 pipeline (policy -> confidence -> executor).
|
|
||||||
# Assumes the contract already passed policy (caller verifies). Writes the
|
|
||||||
# standard 4-event sequence. Args: <act-label> <dev-applied-event-text>
|
|
||||||
run_passing_pipeline() {
|
|
||||||
local dev_event="$1"
|
|
||||||
|
|
||||||
ev dev "$dev_event"
|
|
||||||
ev qa "qa approved"
|
|
||||||
ev prod "prod approved"
|
|
||||||
ev finalize "finalize: audit.json committed to acdl-evidence"
|
|
||||||
}
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# Setup working directory
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
mkdir -p "$CONTRACTS"
|
|
||||||
rm -f "$AUDIT"
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# Initialize audit (genesis)
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
echo "== run_demo.sh: initializing audit at $AUDIT =="
|
|
||||||
ev genesis "audit log initialized"
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# Act 1 — Friction
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
echo "== Act 1 — Friction =="
|
|
||||||
ev dev "Act 1 Friction: manual 2-week deployment (legacy process)"
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# Act 2 — Developer Self-Service
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
echo "== Act 2 — Developer Self-Service =="
|
|
||||||
cat > "$CONTRACTS/act2.yaml" <<'YAML'
|
|
||||||
stack: l2-commodity-price-feed
|
|
||||||
inputs:
|
|
||||||
environment: dev
|
|
||||||
owner: platform-team
|
|
||||||
public-ingress: false
|
|
||||||
YAML
|
|
||||||
|
|
||||||
ACT2_POLICY="$(python3 "$SCRIPT_DIR/policy_checker.py" "$CONTRACTS/act2.yaml")"
|
|
||||||
ACT2_POLICY_RC=$?
|
|
||||||
echo " policy_checker: $ACT2_POLICY (rc=$ACT2_POLICY_RC)"
|
|
||||||
if [ "$ACT2_POLICY" != "POLICY_PASS" ]; then
|
|
||||||
echo "run_demo.sh: Act 2 expected POLICY_PASS, got '$ACT2_POLICY'" >&2
|
|
||||||
FAIL=1
|
|
||||||
fi
|
|
||||||
|
|
||||||
ACT2_CONF="$(python3 "$SCRIPT_DIR/confidence_signal.py" "$CONTRACTS/act2.yaml")"
|
|
||||||
echo " confidence_signal: $ACT2_CONF"
|
|
||||||
# Expected: {"score": 0.90, "reason": "POLICY_PASS"}
|
|
||||||
|
|
||||||
# mock_executor.sh resolves modules/l2/<stack>/manifest.yaml relative to its
|
|
||||||
# cwd, so it must run from the repo root. It writes state.json to its cwd;
|
|
||||||
# clean it up from the repo root afterward so no stray file is left there.
|
|
||||||
(
|
|
||||||
cd "$REPO_ROOT" && bash "$SCRIPT_DIR/mock_executor.sh" "$CONTRACTS/act2.yaml"
|
|
||||||
)
|
|
||||||
MOCK_RC=$?
|
|
||||||
rm -f "$REPO_ROOT/state.json"
|
|
||||||
if [ "$MOCK_RC" -ne 0 ]; then
|
|
||||||
echo "run_demo.sh: Act 2 mock_executor failed (rc=$MOCK_RC)" >&2
|
|
||||||
FAIL=1
|
|
||||||
fi
|
|
||||||
|
|
||||||
run_passing_pipeline "dev applied: l2-commodity-price-feed"
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# Act 3 — Citizen Developer
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
echo "== Act 3 — Citizen Developer =="
|
|
||||||
ISSUE_BODY="We need to ingest natural gas prices from Platts and report on compliance for the trading desk."
|
|
||||||
if ! python3 "$SCRIPT_DIR/l3b_agent_stub.py" "$ISSUE_BODY" -o "$CONTRACTS/act3.yaml"; then
|
|
||||||
echo "run_demo.sh: l3b_agent_stub failed for Act 3" >&2
|
|
||||||
FAIL=1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Confirm the generated contract's stack (D-008: gas/price matches first).
|
|
||||||
ACT3_STACK="$(python3 -c "import yaml,sys; print(yaml.safe_load(open('$CONTRACTS/act3.yaml'))['stack'])" 2>/dev/null || echo "")"
|
|
||||||
echo " l3b generated stack: $ACT3_STACK"
|
|
||||||
if [ "$ACT3_STACK" != "l2-commodity-price-feed" ]; then
|
|
||||||
echo "run_demo.sh: WARNING Act 3 expected stack l2-commodity-price-feed, got '$ACT3_STACK'" >&2
|
|
||||||
# Continue anyway per the task spec.
|
|
||||||
fi
|
|
||||||
|
|
||||||
ACT3_POLICY="$(python3 "$SCRIPT_DIR/policy_checker.py" "$CONTRACTS/act3.yaml")"
|
|
||||||
ACT3_POLICY_RC=$?
|
|
||||||
echo " policy_checker: $ACT3_POLICY (rc=$ACT3_POLICY_RC)"
|
|
||||||
if [ "$ACT3_POLICY" != "POLICY_PASS" ]; then
|
|
||||||
echo "run_demo.sh: Act 3 expected POLICY_PASS, got '$ACT3_POLICY'" >&2
|
|
||||||
FAIL=1
|
|
||||||
fi
|
|
||||||
|
|
||||||
ACT3_CONF="$(python3 "$SCRIPT_DIR/confidence_signal.py" "$CONTRACTS/act3.yaml")"
|
|
||||||
echo " confidence_signal: $ACT3_CONF"
|
|
||||||
|
|
||||||
(
|
|
||||||
cd "$REPO_ROOT" && bash "$SCRIPT_DIR/mock_executor.sh" "$CONTRACTS/act3.yaml"
|
|
||||||
)
|
|
||||||
MOCK_RC=$?
|
|
||||||
rm -f "$REPO_ROOT/state.json"
|
|
||||||
if [ "$MOCK_RC" -ne 0 ]; then
|
|
||||||
echo "run_demo.sh: Act 3 mock_executor failed (rc=$MOCK_RC)" >&2
|
|
||||||
FAIL=1
|
|
||||||
fi
|
|
||||||
|
|
||||||
run_passing_pipeline "dev applied: l2-commodity-price-feed (Act 3 from issue)"
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# Act 4 — Safety Net
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
echo "== Act 4 — Safety Net =="
|
|
||||||
cat > "$CONTRACTS/act4.yaml" <<'YAML'
|
|
||||||
stack: l2-regulatory-reporting
|
|
||||||
inputs:
|
|
||||||
environment: dev
|
|
||||||
owner: platform-team
|
|
||||||
public-ingress: true
|
|
||||||
YAML
|
|
||||||
|
|
||||||
# policy_checker exits 1 on violation; capture without failing the script.
|
|
||||||
ACT4_POLICY="$(python3 "$SCRIPT_DIR/policy_checker.py" "$CONTRACTS/act4.yaml" 2>&1 || true)"
|
|
||||||
echo " policy_checker: $ACT4_POLICY"
|
|
||||||
if [ "$ACT4_POLICY" != "POLICY_VIOLATION:PUBLIC_INGRESS" ]; then
|
|
||||||
echo "run_demo.sh: Act 4 expected POLICY_VIOLATION:PUBLIC_INGRESS, got '$ACT4_POLICY'" >&2
|
|
||||||
FAIL=1
|
|
||||||
fi
|
|
||||||
|
|
||||||
ACT4_CONF="$(python3 "$SCRIPT_DIR/confidence_signal.py" "$CONTRACTS/act4.yaml")"
|
|
||||||
echo " confidence_signal: $ACT4_CONF"
|
|
||||||
# Expected: {"score": 0.40, "reason": "POLICY_VIOLATION:PUBLIC_INGRESS"}
|
|
||||||
|
|
||||||
# Score < 0.50 -> dev rejects. Do NOT run mock_executor, do NOT write qa/prod/finalize.
|
|
||||||
ev dev "dev rejected: POLICY_VIOLATION:PUBLIC_INGRESS (confidence 0.40 < 0.50)"
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# Summary
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
echo "== Summary =="
|
|
||||||
python3 - "$AUDIT" <<'PY'
|
|
||||||
import json, sys
|
|
||||||
audit = json.load(open(sys.argv[1]))
|
|
||||||
for e in audit:
|
|
||||||
print(f"{e['seq']} | {e['stage']} | {e['event']} | {e['hash'][:12]}")
|
|
||||||
print(f"total events: {len(audit)}")
|
|
||||||
PY
|
|
||||||
|
|
||||||
EVENT_COUNT="$(python3 -c "import json; print(len(json.load(open('$AUDIT'))))")"
|
|
||||||
echo "event count: $EVENT_COUNT"
|
|
||||||
|
|
||||||
if [ "$EVENT_COUNT" -lt 11 ]; then
|
|
||||||
echo "run_demo.sh: expected >= 11 events, got $EVENT_COUNT" >&2
|
|
||||||
FAIL=1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# Upload (optional)
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
if [ "$UPLOAD" -eq 1 ]; then
|
|
||||||
echo "== Upload =="
|
|
||||||
if [ -z "${ACDL_GITEA_TOKEN:-}" ]; then
|
|
||||||
echo "run_demo.sh: ACDL_GITEA_TOKEN not set; skipping upload (use --no-upload to silence)" >&2
|
|
||||||
else
|
|
||||||
# Upload audit.json to acdl-evidence main.
|
|
||||||
if python3 "$SCRIPT_DIR/finalize_evidence.py" --audit "$AUDIT"; then
|
|
||||||
echo " audit.json uploaded"
|
|
||||||
else
|
|
||||||
echo "run_demo.sh: finalize_evidence failed for audit.json" >&2
|
|
||||||
FAIL=1
|
|
||||||
fi
|
|
||||||
# Upload index.html (the --audit flag accepts any local file path; --path
|
|
||||||
# sets the remote destination).
|
|
||||||
if python3 "$SCRIPT_DIR/finalize_evidence.py" \
|
|
||||||
--audit "$REPO_ROOT/evidence-ui/index.html" \
|
|
||||||
--path index.html \
|
|
||||||
--message "chore(ui): update index.html (demo dry run)"; then
|
|
||||||
echo " index.html uploaded"
|
|
||||||
else
|
|
||||||
echo "run_demo.sh: finalize_evidence failed for index.html" >&2
|
|
||||||
FAIL=1
|
|
||||||
fi
|
|
||||||
echo "Uploaded audit.json + index.html to acdl-evidence main"
|
|
||||||
echo " raw URL: https://git.cloudinit.dev/continuous-intelligence/acdl-evidence/raw/branch/main/index.html"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "== Upload skipped (--no-upload) =="
|
|
||||||
fi
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# Exit
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
if [ "$FAIL" -ne 0 ]; then
|
|
||||||
echo "run_demo.sh: one or more steps failed (see warnings above)" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "run_demo.sh: OK ($EVENT_COUNT events)"
|
|
||||||
exit 0
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Phase 01 verification script.
|
|
||||||
# Confirms the three-repo scaffold exists under the continuous-intelligence
|
|
||||||
# Gitea org and that the Phase 01 visible artifacts (placeholder index.html on
|
|
||||||
# acdl-evidence; qa + prod branches on acdl-contracts) are present.
|
|
||||||
#
|
|
||||||
# Usage: ACDL_GITEA_TOKEN=<token> scripts/verify_phase01.sh
|
|
||||||
# Exit codes: 0 = all checks passed; 1 = one or more checks failed.
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
GITEA_HOST="${GITEA_HOST:-https://git.cloudinit.dev}"
|
|
||||||
ORG="continuous-intelligence"
|
|
||||||
TOKEN="${ACDL_GITEA_TOKEN:-}"
|
|
||||||
|
|
||||||
fail_count=0
|
|
||||||
note() { printf ' [%s] %s\n' "$1" "$2"; }
|
|
||||||
pass() { note "PASS" "$1"; }
|
|
||||||
fail() { note "FAIL" "$1"; fail_count=$((fail_count + 1)); }
|
|
||||||
warn() { printf ' [WARN] %s\n' "$1" >&2; }
|
|
||||||
|
|
||||||
echo "== Phase 01 verification =="
|
|
||||||
echo "Host: $GITEA_HOST"
|
|
||||||
echo "Org: $ORG"
|
|
||||||
if [ -n "$TOKEN" ]; then
|
|
||||||
echo "Token: <set, ${#TOKEN} chars>"
|
|
||||||
else
|
|
||||||
echo "Token: <unset>"
|
|
||||||
fi
|
|
||||||
echo
|
|
||||||
|
|
||||||
# --- Check 1: acdl-contracts repo exists ---
|
|
||||||
echo "-- Check 1: acdl-contracts repo exists --"
|
|
||||||
status=$(curl -sS -o /tmp/p01_contracts.json -w "%{http_code}" \
|
|
||||||
-H "Authorization: token ${TOKEN}" \
|
|
||||||
"${GITEA_HOST}/api/v1/repos/${ORG}/acdl-contracts")
|
|
||||||
if [ "$status" = "200" ]; then
|
|
||||||
default_branch=$(python3 -c "import json; print(json.load(open('/tmp/p01_contracts.json')).get('default_branch','?'))")
|
|
||||||
pass "acdl-contracts exists (default_branch=${default_branch})"
|
|
||||||
else
|
|
||||||
fail "acdl-contracts GET returned HTTP ${status}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- Check 2: acdl-evidence repo exists ---
|
|
||||||
echo "-- Check 2: acdl-evidence repo exists --"
|
|
||||||
status=$(curl -sS -o /tmp/p01_evidence.json -w "%{http_code}" \
|
|
||||||
-H "Authorization: token ${TOKEN}" \
|
|
||||||
"${GITEA_HOST}/api/v1/repos/${ORG}/acdl-evidence")
|
|
||||||
if [ "$status" = "200" ]; then
|
|
||||||
default_branch=$(python3 -c "import json; print(json.load(open('/tmp/p01_evidence.json')).get('default_branch','?'))")
|
|
||||||
pass "acdl-evidence exists (default_branch=${default_branch})"
|
|
||||||
else
|
|
||||||
fail "acdl-evidence GET returned HTTP ${status}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- Check 3: acdl-evidence raw index.html returns 200 (Pages substitute per D-012/D-016) ---
|
|
||||||
# acdl-evidence is public per gitea_setup.sh step 2b, so the raw URL should
|
|
||||||
# work without auth. We also try with the auth header as a fallback so the
|
|
||||||
# check does not spuriously fail if the repo visibility was reset.
|
|
||||||
echo "-- Check 3: acdl-evidence raw index.html returns 200 --"
|
|
||||||
index_url="${GITEA_HOST}/${ORG}/acdl-evidence/raw/branch/main/index.html"
|
|
||||||
status=$(curl -sS -o /tmp/p01_index.html -w "%{http_code}" "${index_url}")
|
|
||||||
if [ "$status" != "200" ] && [ -n "$TOKEN" ]; then
|
|
||||||
warn "raw URL returned ${status} unauth; retrying with Authorization header"
|
|
||||||
status=$(curl -sS -o /tmp/p01_index.html -w "%{http_code}" \
|
|
||||||
-H "Authorization: token ${TOKEN}" "${index_url}")
|
|
||||||
fi
|
|
||||||
if [ "$status" = "200" ]; then
|
|
||||||
body_size=$(wc -c < /tmp/p01_index.html)
|
|
||||||
if grep -q "ACDL Evidence" /tmp/p01_index.html; then
|
|
||||||
pass "raw index.html returns 200 with placeholder body (${body_size} bytes)"
|
|
||||||
else
|
|
||||||
fail "raw index.html returns 200 but body does not contain 'ACDL Evidence' marker"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
fail "GET ${index_url} returned HTTP ${status}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- Check 4: qa + prod branches exist on acdl-contracts ---
|
|
||||||
echo "-- Check 4: qa + prod branches exist on acdl-contracts --"
|
|
||||||
status=$(curl -sS -o /tmp/p01_branches.json -w "%{http_code}" \
|
|
||||||
-H "Authorization: token ${TOKEN}" \
|
|
||||||
"${GITEA_HOST}/api/v1/repos/${ORG}/acdl-contracts/branches?limit=50")
|
|
||||||
if [ "$status" != "200" ]; then
|
|
||||||
fail "list branches on acdl-contracts returned HTTP ${status}"
|
|
||||||
else
|
|
||||||
for want in qa prod; do
|
|
||||||
if python3 -c "
|
|
||||||
import json, sys
|
|
||||||
branches = json.load(open('/tmp/p01_branches.json'))
|
|
||||||
names = [b.get('name', '') for b in branches]
|
|
||||||
sys.exit(0 if '${want}' in names else 1)
|
|
||||||
"; then
|
|
||||||
pass "branch '${want}' exists on acdl-contracts"
|
|
||||||
else
|
|
||||||
fail "branch '${want}' missing on acdl-contracts"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "== Summary =="
|
|
||||||
if [ "$fail_count" -eq 0 ]; then
|
|
||||||
echo "Phase 01 verification PASSED (all checks ok)"
|
|
||||||
exit 0
|
|
||||||
else
|
|
||||||
echo "Phase 01 verification FAILED (${fail_count} check(s) failed)"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Phase 02 verification script.
|
|
||||||
# Confirms the 8 L1 module folders exist under modules/l1/ with the exact
|
|
||||||
# names from REQ-02, each containing a valid manifest.yaml (D-017 schema)
|
|
||||||
# and a uniform mock_apply.sh (D-007 + D-018) that exits 0 with the
|
|
||||||
# expected echo markers.
|
|
||||||
#
|
|
||||||
# Usage: scripts/verify_phase02.sh
|
|
||||||
# Exit codes: 0 = all checks passed; 1 = one or more checks failed.
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
||||||
L1_DIR="${ROOT}/modules/l1"
|
|
||||||
|
|
||||||
# Expected L1 names per REQ-02 / D-019.
|
|
||||||
EXPECTED_L1S=(
|
|
||||||
l1-eks-fargate
|
|
||||||
l1-iam-role
|
|
||||||
l1-lambda
|
|
||||||
l1-api-gateway
|
|
||||||
l1-eventbridge
|
|
||||||
l1-sqs
|
|
||||||
l1-s3
|
|
||||||
l1-cloudwatch
|
|
||||||
)
|
|
||||||
|
|
||||||
fail_count=0
|
|
||||||
pass() { printf ' [PASS] %s\n' "$1"; }
|
|
||||||
fail() { printf ' [FAIL] %s\n' "$1"; fail_count=$((fail_count + 1)); }
|
|
||||||
|
|
||||||
echo "== Phase 02 verification =="
|
|
||||||
echo "L1 dir: ${L1_DIR}"
|
|
||||||
echo
|
|
||||||
|
|
||||||
# --- Check 1: exactly 8 L1 folders with the expected names ---
|
|
||||||
echo "-- Check 1: 8 L1 folders with expected names --"
|
|
||||||
if [ ! -d "$L1_DIR" ]; then
|
|
||||||
fail "modules/l1/ does not exist"
|
|
||||||
echo
|
|
||||||
echo "== Summary =="
|
|
||||||
echo "Phase 02 verification FAILED (${fail_count} check(s) failed)"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
actual_folders=$(ls "$L1_DIR" | sort | tr '\n' ' ')
|
|
||||||
expected_folders=$(printf '%s\n' "${EXPECTED_L1S[@]}" | sort | tr '\n' ' ')
|
|
||||||
if [ "$actual_folders" = "$expected_folders" ]; then
|
|
||||||
pass "exactly 8 L1 folders present and named correctly"
|
|
||||||
else
|
|
||||||
fail "L1 folder list mismatch"
|
|
||||||
echo " expected: $expected_folders"
|
|
||||||
echo " actual: $actual_folders"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- Per-L1 checks ---
|
|
||||||
for l1 in "${EXPECTED_L1S[@]}"; do
|
|
||||||
echo "-- L1: ${l1} --"
|
|
||||||
dir="${L1_DIR}/${l1}"
|
|
||||||
|
|
||||||
# Check 2a: folder exists
|
|
||||||
if [ ! -d "$dir" ]; then
|
|
||||||
fail "${l1}: folder missing"
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
pass "${l1}: folder exists"
|
|
||||||
|
|
||||||
# Check 2b: manifest.yaml exists + parses + name matches folder + kind=l1
|
|
||||||
manifest="${dir}/manifest.yaml"
|
|
||||||
if [ ! -f "$manifest" ]; then
|
|
||||||
fail "${l1}: manifest.yaml missing"
|
|
||||||
else
|
|
||||||
manifest_ok=$(python3 -c "
|
|
||||||
import yaml, sys
|
|
||||||
try:
|
|
||||||
d = yaml.safe_load(open('${manifest}'))
|
|
||||||
name = d.get('name') == '${l1}'
|
|
||||||
kind = d.get('kind') == 'l1'
|
|
||||||
has_inputs = isinstance(d.get('inputs'), dict)
|
|
||||||
sys.exit(0 if (name and kind and has_inputs) else 1)
|
|
||||||
except Exception as e:
|
|
||||||
print(f' parse error: {e}', file=sys.stderr)
|
|
||||||
sys.exit(2)
|
|
||||||
" 2>/dev/null; echo $?)
|
|
||||||
if [ "$manifest_ok" = "0" ]; then
|
|
||||||
pass "${l1}: manifest.yaml valid (name=${l1}, kind=l1, inputs present)"
|
|
||||||
else
|
|
||||||
fail "${l1}: manifest.yaml invalid (name/kind/inputs check failed; rc=${manifest_ok})"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check 2c: mock_apply.sh exists + executable + bash -n clean
|
|
||||||
apply="${dir}/mock_apply.sh"
|
|
||||||
if [ ! -f "$apply" ]; then
|
|
||||||
fail "${l1}: mock_apply.sh missing"
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
if [ ! -x "$apply" ]; then
|
|
||||||
fail "${l1}: mock_apply.sh not executable"
|
|
||||||
else
|
|
||||||
pass "${l1}: mock_apply.sh is executable"
|
|
||||||
fi
|
|
||||||
if ! bash -n "$apply" 2>/dev/null; then
|
|
||||||
fail "${l1}: mock_apply.sh bash -n failed"
|
|
||||||
else
|
|
||||||
pass "${l1}: mock_apply.sh bash -n clean"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check 2d: end-to-end run: exit 0 + expected markers, completes in <2s
|
|
||||||
start=$(date +%s)
|
|
||||||
output=$("$apply" 2>&1)
|
|
||||||
rc=$?
|
|
||||||
elapsed=$(( $(date +%s) - start ))
|
|
||||||
if [ "$rc" -ne 0 ]; then
|
|
||||||
fail "${l1}: mock_apply.sh exited ${rc}"
|
|
||||||
elif ! echo "$output" | grep -qF "[L1: ${l1}] applying..."; then
|
|
||||||
fail "${l1}: missing '[L1: ${l1}] applying...' marker"
|
|
||||||
elif ! echo "$output" | grep -qF "[L1: ${l1}] OK"; then
|
|
||||||
fail "${l1}: missing '[L1: ${l1}] OK' marker"
|
|
||||||
elif [ "$elapsed" -lt 1 ] || [ "$elapsed" -gt 2 ]; then
|
|
||||||
fail "${l1}: run took ${elapsed}s (expected ~1s; 1<=t<=2 ok)"
|
|
||||||
else
|
|
||||||
pass "${l1}: mock_apply.sh runs, exits 0, markers correct (${elapsed}s)"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "== Summary =="
|
|
||||||
if [ "$fail_count" -eq 0 ]; then
|
|
||||||
echo "Phase 02 verification PASSED (8 L1 modules, all checks ok)"
|
|
||||||
exit 0
|
|
||||||
else
|
|
||||||
echo "Phase 02 verification FAILED (${fail_count} check(s) failed)"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
@@ -1,240 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Phase 03 verification script.
|
|
||||||
# Confirms the 4 L2 modules and the 5 core scripts conform to their contracts.
|
|
||||||
#
|
|
||||||
# Usage: scripts/verify_phase03.sh
|
|
||||||
# Exit codes: 0 = all checks passed; 1 = one or more checks failed.
|
|
||||||
|
|
||||||
set -uo pipefail
|
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
||||||
cd "$ROOT"
|
|
||||||
|
|
||||||
fail_count=0
|
|
||||||
pass() { printf ' [PASS] %s\n' "$1"; }
|
|
||||||
fail() { printf ' [FAIL] %s\n' "$1"; fail_count=$((fail_count + 1)); }
|
|
||||||
|
|
||||||
# Expected L2 names per REQ-04.
|
|
||||||
EXPECTED_L2S=(
|
|
||||||
l2-invoice-service
|
|
||||||
l2-commodity-price-feed
|
|
||||||
l2-energy-analytics-api
|
|
||||||
l2-regulatory-reporting
|
|
||||||
)
|
|
||||||
|
|
||||||
echo "== Phase 03 verification =="
|
|
||||||
echo "Root: ${ROOT}"
|
|
||||||
echo
|
|
||||||
|
|
||||||
# --- Check 1: exactly 4 L2 folders with the expected names ---
|
|
||||||
echo "-- Check 1: 4 L2 folders with expected names --"
|
|
||||||
actual=$(ls modules/l2/ 2>/dev/null | sort | tr '\n' ' ')
|
|
||||||
expected=$(printf '%s\n' "${EXPECTED_L2S[@]}" | sort | tr '\n' ' ')
|
|
||||||
if [ "$actual" = "$expected" ]; then
|
|
||||||
pass "exactly 4 L2 folders present and named correctly"
|
|
||||||
else
|
|
||||||
fail "L2 folder list mismatch"
|
|
||||||
echo " expected: $expected"
|
|
||||||
echo " actual: $actual"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- Check 2: each L2 manifest.yaml validates + references 5 existing L1s ---
|
|
||||||
echo "-- Check 2: L2 manifests reference 5 existing L1s --"
|
|
||||||
l2_validate=$(python3 << 'PYEOF' || true
|
|
||||||
import yaml, glob, os, sys
|
|
||||||
ok = True
|
|
||||||
l1s = set(os.listdir('modules/l1'))
|
|
||||||
for f in sorted(glob.glob('modules/l2/*/manifest.yaml')):
|
|
||||||
d = yaml.safe_load(open(f))
|
|
||||||
folder = os.path.basename(os.path.dirname(f))
|
|
||||||
problems = []
|
|
||||||
if d.get('name') != folder: problems.append(f"name != {folder}")
|
|
||||||
if d.get('kind') != 'l2': problems.append("kind != l2")
|
|
||||||
refs = [x.get('name') for x in d.get('l1s', [])]
|
|
||||||
if len(refs) != 5: problems.append(f"expected 5 l1s, got {len(refs)}")
|
|
||||||
unknown = [r for r in refs if r not in l1s]
|
|
||||||
if unknown: problems.append(f"unknown L1 refs: {unknown}")
|
|
||||||
# each l1 entry must have an inputs: map
|
|
||||||
for x in d.get('l1s', []):
|
|
||||||
if not isinstance(x.get('inputs'), dict): problems.append(f"l1 {x.get('name')} missing inputs map")
|
|
||||||
status = 'OK' if not problems else 'FAIL: ' + '; '.join(problems)
|
|
||||||
print(f' [{status}] {f}')
|
|
||||||
if problems: ok = False
|
|
||||||
sys.exit(0 if ok else 1)
|
|
||||||
PYEOF
|
|
||||||
)
|
|
||||||
echo "$l2_validate"
|
|
||||||
if [ "$l2_validate" = "" ] || echo "$l2_validate" | grep -q FAIL; then
|
|
||||||
if ! echo "$l2_validate" | grep -q PASS; then
|
|
||||||
fail "one or more L2 manifests invalid (see above)"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
pass "all 4 L2 manifests valid"
|
|
||||||
fi
|
|
||||||
# Re-run for the explicit pass/fail count
|
|
||||||
python3 << 'PYEOF' > /tmp/l2_check.txt 2>&1 || true
|
|
||||||
import yaml, glob, os, sys
|
|
||||||
ok = True
|
|
||||||
l1s = set(os.listdir('modules/l1'))
|
|
||||||
for f in sorted(glob.glob('modules/l2/*/manifest.yaml')):
|
|
||||||
d = yaml.safe_load(open(f))
|
|
||||||
folder = os.path.basename(os.path.dirname(f))
|
|
||||||
if d.get('name') != folder: ok = False
|
|
||||||
if d.get('kind') != 'l2': ok = False
|
|
||||||
refs = [x.get('name') for x in d.get('l1s', [])]
|
|
||||||
if len(refs) != 5: ok = False
|
|
||||||
if any(r not in l1s for r in refs): ok = False
|
|
||||||
for x in d.get('l1s', []):
|
|
||||||
if not isinstance(x.get('inputs'), dict): ok = False
|
|
||||||
sys.exit(0 if ok else 1)
|
|
||||||
PYEOF
|
|
||||||
if [ $? -eq 0 ]; then pass "all 4 L2 manifests pass structural + reference checks"; else fail "L2 manifest structural check"; fi
|
|
||||||
|
|
||||||
# --- Check 3: typecheck (bash -n + py_compile + yaml load) ---
|
|
||||||
echo "-- Check 3: typecheck --"
|
|
||||||
if bash -n scripts/mock_executor.sh; then pass "bash -n mock_executor.sh"; else fail "bash -n mock_executor.sh"; fi
|
|
||||||
if python3 -m py_compile scripts/policy_checker.py scripts/confidence_signal.py scripts/evidence_writer.py scripts/l3b_agent_stub.py 2>/dev/null; then
|
|
||||||
pass "py_compile all 4 python scripts"
|
|
||||||
else
|
|
||||||
fail "py_compile"
|
|
||||||
fi
|
|
||||||
if python3 -c "import yaml, glob; [yaml.safe_load(open(f)) for f in glob.glob('modules/l2/*/manifest.yaml')]" 2>/dev/null; then
|
|
||||||
pass "yaml load all L2 manifests"
|
|
||||||
else
|
|
||||||
fail "yaml load L2 manifests"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- Check 4: policy_checker (D-025) ---
|
|
||||||
echo "-- Check 4: policy_checker behavior (D-025) --"
|
|
||||||
WORK="$(mktemp -d)"
|
|
||||||
trap 'rm -rf "$WORK" "$ROOT/tmp_pass_contract.yaml" "$ROOT/tmp_fail_contract.yaml" "$ROOT/state.json" 2>/dev/null || true' EXIT
|
|
||||||
printf 'stack: l2-commodity-price-feed\npublic-ingress: false\n' > "$WORK/pass.yaml"
|
|
||||||
printf 'stack: l2-regulatory-reporting\npublic-ingress: true\n' > "$WORK/fail.yaml"
|
|
||||||
out=$(python3 scripts/policy_checker.py "$WORK/pass.yaml" 2>&1); rc=$?
|
|
||||||
if [ "$out" = "POLICY_PASS" ] && [ "$rc" = "0" ]; then
|
|
||||||
pass "policy_checker pass contract -> POLICY_PASS exit 0"
|
|
||||||
else
|
|
||||||
fail "policy_checker pass contract: got '$out' exit=$rc"
|
|
||||||
fi
|
|
||||||
out=$(python3 scripts/policy_checker.py "$WORK/fail.yaml" 2>&1); rc=$?
|
|
||||||
if [ "$out" = "POLICY_VIOLATION:PUBLIC_INGRESS" ] && [ "$rc" = "1" ]; then
|
|
||||||
pass "policy_checker fail contract -> POLICY_VIOLATION:PUBLIC_INGRESS exit 1"
|
|
||||||
else
|
|
||||||
fail "policy_checker fail contract: got '$out' exit=$rc"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- Check 5: confidence_signal (D-024) ---
|
|
||||||
echo "-- Check 5: confidence_signal behavior (D-024) --"
|
|
||||||
out=$(python3 scripts/confidence_signal.py "$WORK/pass.yaml" 2>&1); rc=$?
|
|
||||||
if echo "$out" | grep -q '"score": 0.90' && [ "$rc" = "0" ]; then
|
|
||||||
pass "confidence_signal pass -> score 0.90 exit 0"
|
|
||||||
else
|
|
||||||
fail "confidence_signal pass: got '$out' exit=$rc"
|
|
||||||
fi
|
|
||||||
out=$(python3 scripts/confidence_signal.py "$WORK/fail.yaml" 2>&1); rc=$?
|
|
||||||
if echo "$out" | grep -q '"score": 0.40' && [ "$rc" = "0" ]; then
|
|
||||||
pass "confidence_signal fail -> score 0.40 exit 0"
|
|
||||||
else
|
|
||||||
fail "confidence_signal fail: got '$out' exit=$rc"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- Check 6: evidence_writer hash chain (D-023) ---
|
|
||||||
echo "-- Check 6: evidence_writer hash chain (D-023) --"
|
|
||||||
rm -f "$WORK/audit.json"
|
|
||||||
python3 scripts/evidence_writer.py --stage dev --event "dev start" --audit "$WORK/audit.json" > /dev/null
|
|
||||||
python3 scripts/evidence_writer.py --stage qa --event "qa approved" --audit "$WORK/audit.json" > /dev/null
|
|
||||||
python3 scripts/evidence_writer.py --stage prod --event "prod approved" --audit "$WORK/audit.json" > /dev/null
|
|
||||||
chain_ok=$(python3 << PYEOF
|
|
||||||
import json, hashlib, sys
|
|
||||||
try:
|
|
||||||
events = json.load(open("$WORK/audit.json"))
|
|
||||||
assert len(events) == 4, f"expected 4 (genesis + 3), got {len(events)}"
|
|
||||||
assert events[0]['prev_hash'] == 'GENESIS', "genesis prev_hash"
|
|
||||||
for i in range(1, len(events)):
|
|
||||||
assert events[i]['prev_hash'] == events[i-1]['hash'], f"chain break at {i}"
|
|
||||||
e = dict(events[i]); h = e.pop('hash'); e['hash'] = ''
|
|
||||||
canon = json.dumps(e, sort_keys=True, separators=(',',':'))
|
|
||||||
assert hashlib.sha256(canon.encode()).hexdigest() == h, f"hash mismatch at {i}"
|
|
||||||
print("OK")
|
|
||||||
except AssertionError as ex:
|
|
||||||
print(f"FAIL: {ex}")
|
|
||||||
sys.exit(1)
|
|
||||||
PYEOF
|
|
||||||
)
|
|
||||||
if [ "$chain_ok" = "OK" ]; then
|
|
||||||
pass "evidence_writer: 4 events, GENESIS + 3, chain links + hashes valid"
|
|
||||||
else
|
|
||||||
fail "evidence_writer chain: $chain_ok"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- Check 7: mock_executor (D-022) ---
|
|
||||||
echo "-- Check 7: mock_executor writes state.json (D-022) --"
|
|
||||||
rm -f "$ROOT/state.json"
|
|
||||||
out=$(bash scripts/mock_executor.sh "$WORK/pass.yaml" 2>&1); rc=$?
|
|
||||||
if [ "$rc" != "0" ]; then
|
|
||||||
fail "mock_executor exit $rc (expected 0)"
|
|
||||||
else
|
|
||||||
me_ok=$(python3 << PYEOF
|
|
||||||
import json, sys
|
|
||||||
try:
|
|
||||||
s = json.load(open("$ROOT/state.json"))
|
|
||||||
assert s['l2'] == 'l2-commodity-price-feed', f"l2 mismatch: {s.get('l2')}"
|
|
||||||
assert 'l1s' in s and len(s['l1s']) == 5, f"expected 5 l1s, got {len(s.get('l1s', []))}"
|
|
||||||
assert all(x['applied'] is True and x['exit_code'] == 0 for x in s['l1s']), "l1 not all applied+0"
|
|
||||||
assert 'contract' in s, "missing contract field"
|
|
||||||
print("OK")
|
|
||||||
except Exception as ex:
|
|
||||||
print(f"FAIL: {ex}")
|
|
||||||
sys.exit(1)
|
|
||||||
PYEOF
|
|
||||||
)
|
|
||||||
if [ "$me_ok" = "OK" ]; then
|
|
||||||
pass "mock_executor: state.json with l2 + 5 l1s (all exit 0) + contract"
|
|
||||||
else
|
|
||||||
fail "mock_executor state.json: $me_ok"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
rm -f "$ROOT/state.json"
|
|
||||||
|
|
||||||
# --- Check 8: l3b_agent_stub D-008 keyword map ---
|
|
||||||
echo "-- Check 8: l3b_agent_stub keyword map (D-008) --"
|
|
||||||
act3=$(python3 scripts/l3b_agent_stub.py "We need to ingest natural gas prices from Platts and report on compliance." 2>&1)
|
|
||||||
if echo "$act3" | grep -q 'stack: l2-commodity-price-feed'; then
|
|
||||||
pass "l3b Act 3 example -> l2-commodity-price-feed"
|
|
||||||
else
|
|
||||||
fail "l3b Act 3 example: got '$act3'"
|
|
||||||
fi
|
|
||||||
fallback=$(python3 scripts/l3b_agent_stub.py "please deploy something" 2>&1)
|
|
||||||
if echo "$fallback" | grep -q 'stack: l2-invoice-service'; then
|
|
||||||
pass "l3b fallback (no keywords) -> l2-invoice-service"
|
|
||||||
else
|
|
||||||
fail "l3b fallback: got '$fallback'"
|
|
||||||
fi
|
|
||||||
regulatory=$(python3 scripts/l3b_agent_stub.py "regulatory compliance reporting for trading desk" 2>&1)
|
|
||||||
if echo "$regulatory" | grep -q 'stack: l2-regulatory-reporting'; then
|
|
||||||
pass "l3b regulatory keywords -> l2-regulatory-reporting"
|
|
||||||
else
|
|
||||||
fail "l3b regulatory: got '$regulatory'"
|
|
||||||
fi
|
|
||||||
invoice=$(python3 scripts/l3b_agent_stub.py "monthly invoice and billing reconciliation" 2>&1)
|
|
||||||
if echo "$invoice" | grep -q 'stack: l2-invoice-service'; then
|
|
||||||
pass "l3b invoice keywords -> l2-invoice-service"
|
|
||||||
else
|
|
||||||
fail "l3b invoice: got '$invoice'"
|
|
||||||
fi
|
|
||||||
analytics=$(python3 scripts/l3b_agent_stub.py "historical analytics and query API" 2>&1)
|
|
||||||
if echo "$analytics" | grep -q 'stack: l2-energy-analytics-api'; then
|
|
||||||
pass "l3b analytics keywords -> l2-energy-analytics-api"
|
|
||||||
else
|
|
||||||
fail "l3b analytics: got '$analytics'"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "== Summary =="
|
|
||||||
if [ "$fail_count" -eq 0 ]; then
|
|
||||||
echo "Phase 03 verification PASSED (4 L2s + 5 core scripts, all checks ok)"
|
|
||||||
exit 0
|
|
||||||
else
|
|
||||||
echo "Phase 03 verification FAILED (${fail_count} check(s) failed)"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
@@ -1,186 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Phase 04 verification script.
|
|
||||||
# Confirms the pipeline workflow + issue trigger + finalize_evidence.py
|
|
||||||
# conform to the Phase 04 plan and the Gitea Actions topology in
|
|
||||||
# ARCHITECTURE.md. Does NOT execute a real Gitea Actions run (act_runner
|
|
||||||
# is not registered in this environment); validates structure + syntax
|
|
||||||
# + a dry-run of finalize_evidence.py against a dead host.
|
|
||||||
#
|
|
||||||
# Usage: scripts/verify_phase04.sh
|
|
||||||
# Exit codes: 0 = all checks passed; 1 = one or more checks failed.
|
|
||||||
|
|
||||||
set -uo pipefail
|
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
||||||
cd "$ROOT"
|
|
||||||
|
|
||||||
fail_count=0
|
|
||||||
pass() { printf ' [PASS] %s\n' "$1"; }
|
|
||||||
fail() { printf ' [FAIL] %s\n' "$1"; fail_count=$((fail_count + 1)); }
|
|
||||||
|
|
||||||
echo "== Phase 04 verification =="
|
|
||||||
echo "Root: ${ROOT}"
|
|
||||||
echo
|
|
||||||
|
|
||||||
# --- Check 1: typecheck ---
|
|
||||||
echo "-- Check 1: typecheck --"
|
|
||||||
if bash -n scripts/finalize_evidence.py 2>/dev/null || python3 -m py_compile scripts/finalize_evidence.py 2>/dev/null; then
|
|
||||||
pass "py_compile finalize_evidence.py"
|
|
||||||
else
|
|
||||||
fail "py_compile finalize_evidence.py"
|
|
||||||
fi
|
|
||||||
if python3 -c "import yaml; yaml.safe_load(open('.gitea/workflows/pipeline.yml')); yaml.safe_load(open('contracts-repo/.gitea/workflows/issue-to-contract.yml'))" 2>/dev/null; then
|
|
||||||
pass "yaml load both workflows"
|
|
||||||
else
|
|
||||||
fail "yaml load workflows"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- Check 2: pipeline.yml structure ---
|
|
||||||
echo "-- Check 2: pipeline.yml structure (D-027, D-028) --"
|
|
||||||
p_struct=$(python3 << 'PYEOF'
|
|
||||||
import yaml, sys
|
|
||||||
try:
|
|
||||||
d = yaml.safe_load(open('.gitea/workflows/pipeline.yml'))
|
|
||||||
on = d.get('on', d.get(True)) or {}
|
|
||||||
assert 'workflow_dispatch' in on, 'no workflow_dispatch trigger'
|
|
||||||
inputs = on['workflow_dispatch']['inputs']
|
|
||||||
assert set(inputs.keys()) == {'contract-ref', 'approve_qa', 'approve_prod'}, f'inputs: {set(inputs.keys())}'
|
|
||||||
assert inputs['contract-ref']['type'] == 'string', 'contract-ref type'
|
|
||||||
assert inputs['approve_qa']['type'] == 'boolean', 'approve_qa type'
|
|
||||||
assert inputs['approve_prod']['type'] == 'boolean', 'approve_prod type'
|
|
||||||
jobs = d['jobs']
|
|
||||||
assert set(jobs.keys()) == {'dev', 'qa-gate', 'prod-gate', 'finalize'}, f'jobs: {set(jobs.keys())}'
|
|
||||||
dev_if = jobs['dev'].get('if', '')
|
|
||||||
assert 'approve_qa' in dev_if and 'approve_prod' in dev_if, f'dev.if: {dev_if}'
|
|
||||||
qa_if = jobs['qa-gate'].get('if', '')
|
|
||||||
assert 'approve_qa' in qa_if, f'qa-gate.if: {qa_if}'
|
|
||||||
prod_if = jobs['prod-gate'].get('if', '')
|
|
||||||
assert 'approve_prod' in prod_if, f'prod-gate.if: {prod_if}'
|
|
||||||
fin_needs = jobs['finalize'].get('needs', [])
|
|
||||||
assert fin_needs == ['prod-gate'] or fin_needs == 'prod-gate', f'finalize.needs: {fin_needs}'
|
|
||||||
# All jobs runs-on ubuntu-latest
|
|
||||||
for name, job in jobs.items():
|
|
||||||
assert job.get('runs-on') == 'ubuntu-latest', f'{name} runs-on: {job.get("runs-on")}'
|
|
||||||
print('OK')
|
|
||||||
except AssertionError as ex:
|
|
||||||
print(f'FAIL: {ex}')
|
|
||||||
sys.exit(1)
|
|
||||||
except Exception as ex:
|
|
||||||
print(f'FAIL: {ex}')
|
|
||||||
sys.exit(1)
|
|
||||||
PYEOF
|
|
||||||
)
|
|
||||||
if [ "$p_struct" = "OK" ]; then
|
|
||||||
pass "pipeline.yml: 3 inputs + 4 jobs + correct if: conditions + finalize.needs=prod-gate"
|
|
||||||
else
|
|
||||||
fail "pipeline.yml structure: $p_struct"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- Check 3: pipeline.yml references core scripts ---
|
|
||||||
echo "-- Check 3: pipeline.yml references core scripts (D-029) --"
|
|
||||||
text=$(cat .gitea/workflows/pipeline.yml)
|
|
||||||
missing=""
|
|
||||||
for ref in policy_checker.py confidence_signal.py mock_executor.sh evidence_writer.py finalize_evidence.py; do
|
|
||||||
if ! echo "$text" | grep -qF "$ref"; then
|
|
||||||
missing="$missing $ref"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
if [ -z "$missing" ]; then
|
|
||||||
pass "pipeline.yml references all 5 core scripts"
|
|
||||||
else
|
|
||||||
fail "pipeline.yml missing references:$missing"
|
|
||||||
fi
|
|
||||||
# Branch-pin documentation
|
|
||||||
if echo "$text" | grep -q 'milestone/v1.0-initial'; then
|
|
||||||
pass "pipeline.yml documents branch-pin to milestone/v1.0-initial"
|
|
||||||
else
|
|
||||||
fail "pipeline.yml missing branch-pin reference"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- Check 4: issue-to-contract.yml structure ---
|
|
||||||
echo "-- Check 4: issue-to-contract.yml structure (D-030) --"
|
|
||||||
i_struct=$(python3 << 'PYEOF'
|
|
||||||
import yaml, sys
|
|
||||||
try:
|
|
||||||
d = yaml.safe_load(open('contracts-repo/.gitea/workflows/issue-to-contract.yml'))
|
|
||||||
on = d.get('on', d.get(True)) or {}
|
|
||||||
assert 'issues' in on, 'no issues trigger'
|
|
||||||
assert on['issues']['types'] == ['opened'], f'types: {on["issues"]["types"]}'
|
|
||||||
assert 'parse-and-trigger' in d['jobs'], 'no parse-and-trigger job'
|
|
||||||
assert d['jobs']['parse-and-trigger'].get('runs-on') == 'ubuntu-latest', 'runs-on'
|
|
||||||
print('OK')
|
|
||||||
except AssertionError as ex:
|
|
||||||
print(f'FAIL: {ex}')
|
|
||||||
sys.exit(1)
|
|
||||||
PYEOF
|
|
||||||
)
|
|
||||||
if [ "$i_struct" = "OK" ]; then
|
|
||||||
pass "issue-to-contract.yml: issues[opened] + parse-and-trigger job"
|
|
||||||
else
|
|
||||||
fail "issue-to-contract.yml structure: $i_struct"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- Check 5: issue-to-contract.yml references + dispatch endpoint ---
|
|
||||||
echo "-- Check 5: issue-to-contract.yml references + dispatch (D-014, D-030) --"
|
|
||||||
text=$(cat contracts-repo/.gitea/workflows/issue-to-contract.yml)
|
|
||||||
missing=""
|
|
||||||
for ref in l3b_agent_stub.py 'actions/workflows/pipeline.yml/dispatches' 'contract-ref' 'gitea.event.issue.number' 'GITEA_TOKEN' 'new_branch'; do
|
|
||||||
if ! echo "$text" | grep -qF "$ref"; then
|
|
||||||
missing="$missing $ref"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
if [ -z "$missing" ]; then
|
|
||||||
pass "issue-to-contract.yml: l3b_agent_stub + dispatch + contract-ref + issue number + token + new_branch"
|
|
||||||
else
|
|
||||||
fail "issue-to-contract.yml missing references:$missing"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- Check 6: finalize_evidence.py --help + clean failure ---
|
|
||||||
echo "-- Check 6: finalize_evidence.py CLI + clean failure modes ---"
|
|
||||||
out=$(python3 scripts/finalize_evidence.py --help 2>&1); rc=$?
|
|
||||||
if [ "$rc" = "0" ] && echo "$out" | grep -qi 'usage\|--audit\|--owner'; then
|
|
||||||
pass "finalize_evidence.py --help exits 0 with usage"
|
|
||||||
else
|
|
||||||
fail "finalize_evidence.py --help: rc=$rc"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Missing audit file (with a fake token so it gets past the env check) → exit 1, no stack trace
|
|
||||||
out=$(ACDL_GITEA_TOKEN=fake python3 scripts/finalize_evidence.py --audit /tmp/definitely_nonexistent_audit.json 2>&1); rc=$?
|
|
||||||
if [ "$rc" = "1" ] && ! echo "$out" | grep -q 'Traceback'; then
|
|
||||||
pass "finalize_evidence.py missing file → exit 1, no stack trace"
|
|
||||||
else
|
|
||||||
fail "finalize_evidence.py missing file: rc=$rc, out='$out'"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Missing token env (audit file present) → exit 1, no stack trace
|
|
||||||
printf '[]\n' > /tmp/empty_audit.json
|
|
||||||
out=$(env -u ACDL_GITEA_TOKEN python3 scripts/finalize_evidence.py --audit /tmp/empty_audit.json 2>&1); rc=$?
|
|
||||||
if [ "$rc" = "1" ] && ! echo "$out" | grep -q 'Traceback'; then
|
|
||||||
pass "finalize_evidence.py missing token env → exit 1, no stack trace"
|
|
||||||
else
|
|
||||||
fail "finalize_evidence.py missing token: rc=$rc, out='$out'"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- Check 7: finalize_evidence.py dry-run against a dead host (clean failure) ---
|
|
||||||
echo "-- Check 7: finalize_evidence.py dry-run against dead host ---"
|
|
||||||
# Use a real audit.json but point at a host that will refuse the connection.
|
|
||||||
printf '[{"seq":0,"ts":"2026-07-21T00:00:00Z","stage":"genesis","event":"init","prev_hash":"GENESIS","hash":"x"}]\n' > /tmp/real_audit.json
|
|
||||||
out=$(ACDL_GITEA_TOKEN=fake GITEA_HOST=http://127.0.0.1:0 python3 scripts/finalize_evidence.py --audit /tmp/real_audit.json --host http://127.0.0.1:0 2>&1); rc=$?
|
|
||||||
if [ "$rc" = "1" ] && ! echo "$out" | grep -q 'Traceback'; then
|
|
||||||
pass "finalize_evidence.py dead host → exit 1, no stack trace (clean API failure)"
|
|
||||||
else
|
|
||||||
fail "finalize_evidence.py dead host: rc=$rc, out='$out'"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Cleanup
|
|
||||||
rm -f /tmp/empty_audit.json /tmp/real_audit.json
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "== Summary =="
|
|
||||||
if [ "$fail_count" -eq 0 ]; then
|
|
||||||
echo "Phase 04 verification PASSED (pipeline + issue trigger + finalize helper, all checks ok)"
|
|
||||||
exit 0
|
|
||||||
else
|
|
||||||
echo "Phase 04 verification FAILED (${fail_count} check(s) failed)"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
@@ -1,213 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Phase 05 verification script.
|
|
||||||
# Validates the evidence UI + the 4-act demo dry-run.
|
|
||||||
#
|
|
||||||
# Usage: scripts/verify_phase05.sh
|
|
||||||
# Exit codes: 0 = all checks passed; 1 = one or more checks failed.
|
|
||||||
|
|
||||||
set -uo pipefail
|
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
||||||
cd "$ROOT"
|
|
||||||
|
|
||||||
fail_count=0
|
|
||||||
pass() { printf ' [PASS] %s\n' "$1"; }
|
|
||||||
fail() { printf ' [FAIL] %s\n' "$1"; fail_count=$((fail_count + 1)); }
|
|
||||||
|
|
||||||
GITEA_HOST="${GITEA_HOST:-https://git.cloudinit.dev}"
|
|
||||||
ORG="continuous-intelligence"
|
|
||||||
EVIDENCE_REPO="acdl-evidence"
|
|
||||||
|
|
||||||
echo "== Phase 05 verification =="
|
|
||||||
echo "Root: ${ROOT}"
|
|
||||||
echo
|
|
||||||
|
|
||||||
# --- Check 1: evidence-ui/index.html structure ---
|
|
||||||
echo "-- Check 1: evidence-ui/index.html structure (D-032, REQ-14) --"
|
|
||||||
UI="evidence-ui/index.html"
|
|
||||||
if [ ! -f "$UI" ]; then
|
|
||||||
fail "$UI missing"
|
|
||||||
else
|
|
||||||
pass "$UI exists"
|
|
||||||
size=$(wc -c < "$UI")
|
|
||||||
if [ "$size" -ge 1000 ] && [ "$size" -le 30000 ]; then
|
|
||||||
pass "$UI size ${size} bytes (within 1-30 KB range)"
|
|
||||||
else
|
|
||||||
fail "$UI size ${size} bytes (expected 1-30 KB)"
|
|
||||||
fi
|
|
||||||
ui_check=$(python3 << 'PYEOF'
|
|
||||||
import re, sys
|
|
||||||
content = open('evidence-ui/index.html').read()
|
|
||||||
problems = []
|
|
||||||
if '<style>' not in content or '</style>' not in content: problems.append('missing inline <style>')
|
|
||||||
if '<script>' not in content or '</script>' not in content: problems.append('missing inline <script>')
|
|
||||||
if 'fetch(' not in content: problems.append('missing fetch call')
|
|
||||||
if "'./audit.json'" not in content and '"./audit.json"' not in content: problems.append('missing relative ./audit.json fetch')
|
|
||||||
external = re.findall(r'(?:src|href)\s*=\s*["\']https?://', content)
|
|
||||||
if external: problems.append(f'external resource refs: {external}')
|
|
||||||
# Confirm a refresh button or refresh function exists
|
|
||||||
if 'refresh' not in content.lower(): problems.append('no refresh button/function')
|
|
||||||
print('OK' if not problems else 'FAIL: ' + '; '.join(problems))
|
|
||||||
PYEOF
|
|
||||||
)
|
|
||||||
if [ "$ui_check" = "OK" ]; then
|
|
||||||
pass "$UI structural checks (inline CSS/JS, fetch ./audit.json, no external refs, refresh)"
|
|
||||||
else
|
|
||||||
fail "$UI structural: $ui_check"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- Check 2: run_demo.sh syntax ---
|
|
||||||
echo "-- Check 2: run_demo.sh syntax + flags --"
|
|
||||||
if bash -n scripts/run_demo.sh 2>/dev/null; then
|
|
||||||
pass "run_demo.sh bash -n clean"
|
|
||||||
else
|
|
||||||
fail "run_demo.sh bash -n"
|
|
||||||
fi
|
|
||||||
if grep -q -- '--no-upload' scripts/run_demo.sh; then
|
|
||||||
pass "run_demo.sh supports --no-upload flag"
|
|
||||||
else
|
|
||||||
fail "run_demo.sh missing --no-upload flag"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- Check 3: run_demo.sh dry-run (no upload) ---
|
|
||||||
echo "-- Check 3: run_demo.sh --no-upload (4 acts, 11 events) --"
|
|
||||||
rm -rf /tmp/acdl_demo_run
|
|
||||||
out=$(ACDL_GITEA_TOKEN= bash scripts/run_demo.sh --no-upload 2>&1); rc=$?
|
|
||||||
if [ "$rc" = "0" ]; then
|
|
||||||
pass "run_demo.sh --no-upload exits 0"
|
|
||||||
else
|
|
||||||
fail "run_demo.sh --no-upload exit $rc"
|
|
||||||
echo "$out" | tail -10
|
|
||||||
fi
|
|
||||||
audit="/tmp/acdl_demo_run/audit.json"
|
|
||||||
if [ -f "$audit" ]; then
|
|
||||||
pass "audit.json written to $audit"
|
|
||||||
else
|
|
||||||
fail "audit.json missing at $audit"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- Check 4: audit.json event count + Act 4 rejection ---
|
|
||||||
echo "-- Check 4: audit.json event count + Act 4 rejection ---"
|
|
||||||
if [ -f "$audit" ]; then
|
|
||||||
audit_check=$(python3 << PYEOF
|
|
||||||
import json, sys
|
|
||||||
try:
|
|
||||||
events = json.load(open("$audit"))
|
|
||||||
n = len(events)
|
|
||||||
if n < 11:
|
|
||||||
print(f"FAIL: too few events ({n}, expected >= 11)")
|
|
||||||
sys.exit(1)
|
|
||||||
if not any('POLICY_VIOLATION:PUBLIC_INGRESS' in x.get('event', '') for x in events):
|
|
||||||
print("FAIL: no Act 4 rejection event")
|
|
||||||
sys.exit(1)
|
|
||||||
if not any('Act 1 Friction' in x.get('event', '') for x in events):
|
|
||||||
print("FAIL: no Act 1 event")
|
|
||||||
sys.exit(1)
|
|
||||||
if not any('Act 3' in x.get('event', '') for x in events):
|
|
||||||
print("FAIL: no Act 3 event")
|
|
||||||
sys.exit(1)
|
|
||||||
if not any('l2-commodity-price-feed' in x.get('event', '') for x in events):
|
|
||||||
print("FAIL: no l2-commodity-price-feed event")
|
|
||||||
sys.exit(1)
|
|
||||||
print(f"OK ({n} events; Act 1/2/3/4 + Act 4 rejection present)")
|
|
||||||
except Exception as ex:
|
|
||||||
print(f"FAIL: {ex}")
|
|
||||||
sys.exit(1)
|
|
||||||
PYEOF
|
|
||||||
)
|
|
||||||
if echo "$audit_check" | grep -q "^OK"; then
|
|
||||||
pass "$audit_check"
|
|
||||||
else
|
|
||||||
fail "audit content: $audit_check"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- Check 5: audit.json hash chain integrity ---
|
|
||||||
echo "-- Check 5: audit.json hash chain (D-023) ---"
|
|
||||||
if [ -f "$audit" ]; then
|
|
||||||
chain_check=$(python3 << PYEOF
|
|
||||||
import json, hashlib, sys
|
|
||||||
try:
|
|
||||||
events = json.load(open("$audit"))
|
|
||||||
assert events[0]['prev_hash'] == 'GENESIS', "genesis prev_hash"
|
|
||||||
for i in range(1, len(events)):
|
|
||||||
assert events[i]['prev_hash'] == events[i-1]['hash'], f"chain break at {i}"
|
|
||||||
e = dict(events[i]); h = e.pop('hash'); e['hash'] = ''
|
|
||||||
canon = json.dumps(e, sort_keys=True, separators=(',',':'))
|
|
||||||
assert hashlib.sha256(canon.encode()).hexdigest() == h, f"hash mismatch at {i}"
|
|
||||||
print("OK")
|
|
||||||
except AssertionError as ex:
|
|
||||||
print(f"FAIL: {ex}")
|
|
||||||
sys.exit(1)
|
|
||||||
PYEOF
|
|
||||||
)
|
|
||||||
if [ "$chain_check" = "OK" ]; then
|
|
||||||
pass "audit.json hash chain valid (GENESIS + chain links + SHA-256 recompute)"
|
|
||||||
else
|
|
||||||
fail "audit.json hash chain: $chain_check"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- Check 6: no stray files in repo root ---
|
|
||||||
echo "-- Check 6: no stray files in repo root ---"
|
|
||||||
if [ -f "$ROOT/state.json" ]; then
|
|
||||||
fail "state.json left in repo root"
|
|
||||||
else
|
|
||||||
pass "no state.json in repo root"
|
|
||||||
fi
|
|
||||||
if [ -d "$ROOT/contracts" ]; then
|
|
||||||
fail "contracts/ directory left in repo root"
|
|
||||||
else
|
|
||||||
pass "no contracts/ directory in repo root"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- Check 7: real upload + raw URL fetch (if token available) ---
|
|
||||||
echo "-- Check 7: real upload + raw URL fetch (REQ-13) ---"
|
|
||||||
TOKEN="${ACDL_GITEA_TOKEN:-}"
|
|
||||||
if [ -z "$TOKEN" ]; then
|
|
||||||
echo " [SKIP] No ACDL_GITEA_TOKEN set; skipping real upload + raw URL fetch (Phase 05 dry-run is sufficient)"
|
|
||||||
else
|
|
||||||
echo " Running run_demo.sh (with upload)..."
|
|
||||||
upload_out=$(bash scripts/run_demo.sh 2>&1); upload_rc=$?
|
|
||||||
if [ "$upload_rc" = "0" ]; then
|
|
||||||
pass "run_demo.sh (with upload) exits 0"
|
|
||||||
else
|
|
||||||
fail "run_demo.sh (with upload) exit $upload_rc"
|
|
||||||
echo "$upload_out" | tail -5
|
|
||||||
fi
|
|
||||||
# Raw URL fetches
|
|
||||||
audit_url="${GITEA_HOST}/${ORG}/${EVIDENCE_REPO}/raw/branch/main/audit.json"
|
|
||||||
index_url="${GITEA_HOST}/${ORG}/${EVIDENCE_REPO}/raw/branch/main/index.html"
|
|
||||||
audit_status=$(curl -sS -o /tmp/p05_audit_remote.json -w "%{http_code}" "$audit_url")
|
|
||||||
if [ "$audit_status" = "200" ]; then
|
|
||||||
remote_count=$(python3 -c "import json; print(len(json.load(open('/tmp/p05_audit_remote.json'))))" 2>/dev/null || echo "?")
|
|
||||||
if [ "$remote_count" = "11" ] || [ "$remote_count" -ge 11 ] 2>/dev/null; then
|
|
||||||
pass "raw audit.json returns 200 with ${remote_count} events"
|
|
||||||
else
|
|
||||||
pass "raw audit.json returns 200 (events: ${remote_count})"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
fail "raw audit.json GET returned HTTP ${audit_status}"
|
|
||||||
fi
|
|
||||||
index_status=$(curl -sS -o /tmp/p05_index_remote.html -w "%{http_code}" "$index_url")
|
|
||||||
if [ "$index_status" = "200" ]; then
|
|
||||||
if grep -q "ACDL Evidence" /tmp/p05_index_remote.html && grep -q "audit.json" /tmp/p05_index_remote.html; then
|
|
||||||
pass "raw index.html returns 200 with ACDL Evidence + audit.json reference"
|
|
||||||
else
|
|
||||||
fail "raw index.html returns 200 but missing ACDL Evidence / audit.json markers"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
fail "raw index.html GET returned HTTP ${index_status}"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "== Summary =="
|
|
||||||
if [ "$fail_count" -eq 0 ]; then
|
|
||||||
echo "Phase 05 verification PASSED (UI + 4-act dry-run, all checks ok)"
|
|
||||||
exit 0
|
|
||||||
else
|
|
||||||
echo "Phase 05 verification FAILED (${fail_count} check(s) failed)"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
title: ACDL — Agentic Cloud Delivery Platform
|
||||||
|
description: Consumer + platform-engineer documentation for the ACDL platform.
|
||||||
|
remote_theme: mmistakes/minimal-mistakes@9.0.4
|
||||||
|
|
||||||
|
exclude:
|
||||||
|
- internal/
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
- scope:
|
||||||
|
path: ""
|
||||||
|
values:
|
||||||
|
layout: single
|
||||||
|
|
||||||
|
nav:
|
||||||
|
- title: Overview
|
||||||
|
url: /
|
||||||
|
- title: Consumer Guide
|
||||||
|
url: /consumer-guide/
|
||||||
|
- title: Modules
|
||||||
|
url: /modules/
|
||||||
|
- title: Contracts
|
||||||
|
url: /contracts/
|
||||||
|
- title: Pipeline
|
||||||
|
url: /pipeline/
|
||||||
|
- title: Versioning
|
||||||
|
url: /pipeline/versioning/
|
||||||
|
- title: Environments
|
||||||
|
url: /environments/
|
||||||
|
- title: Architecture
|
||||||
|
url: /architecture/
|
||||||
|
- title: Vision
|
||||||
|
url: /vision/
|
||||||
@@ -1,458 +0,0 @@
|
|||||||
# Architecture Document v1.0
|
|
||||||
|
|
||||||
> **Snapshot status:** v1.0 — taken in ACDL Phase 07 (milestone v1.1).
|
|
||||||
> All 11 open decisions in §13 are **resolved** — see `PROJECT.md`
|
|
||||||
> "Open-decision resolutions" table + decisions D-034..D-046.
|
|
||||||
> The body §§1-12 is copied verbatim from the upstream
|
|
||||||
> `docs/architecture.md` v0.2; only the header status line, the resolution
|
|
||||||
> session log, §13, §14, and the new §15 are Phase 07 additions. The
|
|
||||||
> `act_runner` → `gitea-runner` rename (D-046, 2026-04 in gitea/runner#850)
|
|
||||||
> is applied; `act_runner` appears only in a "formerly" note.
|
|
||||||
|
|
||||||
# Agentic Cloud Delivery Platform — Architecture Document
|
|
||||||
|
|
||||||
Status: **v1.0** (snapshot taken in ACDL Phase 07, milestone v1.1). All 11
|
|
||||||
open decisions in §13 are resolved — see `PROJECT.md` "Open-decision
|
|
||||||
resolutions" table + decisions D-034..D-046.
|
|
||||||
|
|
||||||
Companion to: Agentic Cloud Delivery Vision [1].
|
|
||||||
|
|
||||||
Authoring principle: The vision is the source of truth for why [1]; this document is the source of truth for how. Where the two conflict, the vision wins.
|
|
||||||
|
|
||||||
Resolution session log (v1.0 snapshot — see PROJECT.md for full text):
|
|
||||||
|
|
||||||
| ID | Question | Resolution (one-line — see PROJECT.md for rationale) |
|
|
||||||
|---|---|---|
|
|
||||||
| W1.A | AI-refinement trigger | ✅ RESOLVED — joint condition: N ≥ 50 consecutive zero-rollback changes AND no L1/L2 incident in 6 months AND Infra & Ops unilateral override. |
|
|
||||||
| W1.B | Multi-stack edge case rule | ✅ RESOLVED — permitted only for (a) DR-region mirror, (b) time-boxed experimental stack TTL ≤ 30d, (c) explicit Infra & Ops approval with `multiStack.justification`. |
|
|
||||||
| W2.A | Tag mutability for prod | ✅ RESOLVED — Path B: tag for dev/qa, SHA for prod; platform CLI resolves tag→SHA. |
|
|
||||||
| W3.D | L1/L2 standard versioning | ✅ RESOLVED — semver (interface→MAJOR, behavior→MINOR, lifecycle→PATCH); L2 pins L1 by `name@semver`; MAJOR bump = new registry entry + 12-month deprecation. |
|
|
||||||
| W3.E | Schema mandatory vs. optional inputs | ✅ RESOLVED — dev: stack+environment; qa adds validation.e2eSuite+loadTest; prod adds runbook+dashboard+oncall; dr adds drDrillRef; `inputs` always optional; `profile: agentic` fields optional everywhere (naturalLanguageIntent required when profile is agentic). |
|
|
||||||
| BA.A | Initial L3B skill catalog | ✅ RESOLVED — 5 skills: web API, worker, scheduled job, static asset, basic observability bootstrap; addition criteria: (a) sensitive-data reviewable, (b) single contract submission, (c) documented use case. |
|
|
||||||
| BA.B | Confidence threshold tuning | ✅ RESOLVED — thresholds frozen for v1; tuning begins v1.2 (quarterly FP/FN tracking; override = Infra & Ops + SRE joint sign-off, itself a confidence-event). |
|
|
||||||
| BA.C | On-call / operational ownership | ✅ RESOLVED — platform on-call = Infra & Ops; L3A/L3B halt → platform on-call (Sev2); consumer-visible outage → consumer on-call (Sev1) + platform support. |
|
|
||||||
| BA.D | Cost / capacity governance | ✅ RESOLVED — FinOps owns cloud cost; per-contract monthly reporting; runaway spend hard-halts at 120% of declared budget via the confidence signal; override = FinOps + SRE joint sign-off. |
|
|
||||||
| BA.E | Consumer onboarding | ✅ RESOLVED — developer (L3A): `getting-started` → contract schema + central pipeline template; citizen (L3B): scoped agent + skill catalog, no workflow authoring; both end in a sandbox dev submission that must pass the confidence gate. |
|
|
||||||
| BA.F | Cross-platform evolution | ✅ RESOLVED — contract schema, IR, PolicyCheckResult, confidence signal, audit stream are portable (forge-agnostic); forge-specific code = workflow YAML, OIDC trust, CODEOWNERS, Environments; a second forge needs a forge adapter + workflow-template translator, no change to L1/L2/IR/confidence/audit. |
|
|
||||||
| Q1.3 | OpenTofu timing | ✅ RESOLVED (deferred) — not in v1 or v1.1; the substrate abstraction (§12) makes OpenTofu a future adapter, not an architecture change; revisit when an OpenTofu adapter is requested. |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 0. Purpose
|
|
||||||
|
|
||||||
This document encodes the architectural commitments that realize the vision [1]. The resolution session has closed eight open items; the document is now at v0.2 with eleven open items remaining, listed in Section 13. Every locked commitment is grounded in either a vision tenet or a specific decision made during resolution.
|
|
||||||
|
|
||||||
The structure remains: four layers (L1 primitives, L2 composed stacks, L3A developer surface, L3B agentic surface) plus five cross-cutting concerns (central pipeline, contract schema, confidence signal, audit stream, HITL mechanics), with one addition: the substrate abstraction layer (Section 12) is now a first-class architectural concern, not an implementation detail.
|
|
||||||
|
|
||||||
## 1. Architectural Overview
|
|
||||||
|
|
||||||
The platform remains four layers and five cross-cutting concerns. The substrate abstraction is added as a sixth cross-cutting concern in Section 12 because it is the binding constraint for the L1/L2 model, the central pipeline, and the policy toolchain.
|
|
||||||
|
|
||||||
The vision's "Two Consumer Surfaces, One Platform" tenet [1] remains the constraint that binds all concerns: L3A and L3B converge on the same contract schema, the same policy envelope, and the same evidence stream.
|
|
||||||
|
|
||||||
Locked additions this revision:
|
|
||||||
|
|
||||||
- The environment model is dev (autonomous) → qa (QA HITL) → prod (SRE HITL) → dr (SRE HITL). Staging does not exist.
|
|
||||||
|
|
||||||
- L1/L2 are substrate-agnostic in shape; substrate adapters are the only substrate-specific component.
|
|
||||||
|
|
||||||
## 2. Layer 1 — Foundational Primitives
|
|
||||||
|
|
||||||
Purpose. Single-purpose, substrate-agnostic primitive modules representing the smallest reusable infrastructure pieces. L1 modules do not compose with other L1 modules; L1 takes its environment as input.
|
|
||||||
|
|
||||||
Locked commitments (unchanged from v0.1):
|
|
||||||
|
|
||||||
- No inter-L1 references. L1 may call Terraform data sources.
|
|
||||||
|
|
||||||
- Semver with three triggers (interface → MAJOR, behavior → MINOR, lifecycle → PATCH).
|
|
||||||
|
|
||||||
- Immutability on publication.
|
|
||||||
|
|
||||||
- 12-month deprecation window.
|
|
||||||
|
|
||||||
- AI refinement is a flag.
|
|
||||||
|
|
||||||
✅ RESOLVED (see PROJECT.md W1.A): AI-refinement operational trigger — joint condition: N ≥ 50 consecutive changes with zero rollbacks AND no L1/L2 incident in last 6 months AND Infra & Ops holds a unilateral override.
|
|
||||||
|
|
||||||
✅ RESOLVED (sub-decision): The L1 module's interface field is defined against the Target Stack IR, not against Terraform's variable block directly. In v1, the IR is shaped to round-trip cleanly to Terraform, but the schema is substrate-agnostic. Pending v1 implementation details in Section 12.
|
|
||||||
|
|
||||||
## 3. Layer 2 — Composed Stacks
|
|
||||||
|
|
||||||
Purpose. Combine L1 primitives into deployable infrastructure shapes. Each codebase maps to one canonical L2 stack; the stack is either a parameterized module (Shape X) or a thin-composition layer (Shape Y).
|
|
||||||
|
|
||||||
Locked commitments (unchanged from v0.1):
|
|
||||||
|
|
||||||
- 1 codebase = 1 L2 stack (default), with multiStack: true for exceptions.
|
|
||||||
|
|
||||||
- Shape X or Shape Y.
|
|
||||||
|
|
||||||
- Hierarchical composition, max depth 5, only registered L1s.
|
|
||||||
|
|
||||||
- Pipeline quality checks: secrets-in-plaintext, public ingress, IAM wildcard, KMS key reference, tag compliance, naming convention.
|
|
||||||
|
|
||||||
- Restricted from thin-composition: IAM principal creation, network boundary creation, key/secret creation, external data transfer.
|
|
||||||
|
|
||||||
- Auto-promote after 3 observed usages.
|
|
||||||
|
|
||||||
✅ RESOLVED (see PROJECT.md W1.B): Multi-stack edge case rule — permitted only for (a) DR-region mirror, (b) time-boxed experimental stack with TTL ≤ 30 days, (c) explicit Infra & Ops approval for a documented reason captured in multiStack.justification.
|
|
||||||
|
|
||||||
✅ RESOLVED (sub-decision): The L2 thin-composition tree's wires field is defined against the IR's relationship type, not against a Terraform module block. The IR → Terraform translation is the Terraform adapter's job (Section 12). The thin-composition pipeline itself is substrate-agnostic.
|
|
||||||
|
|
||||||
## 4. Layer 3A — Developer Consumer Surface
|
|
||||||
|
|
||||||
Locked commitments (unchanged from v0.1):
|
|
||||||
|
|
||||||
- Tag-based reference to the central pipeline template.
|
|
||||||
|
|
||||||
- Developer-owned workflow file, no platform auto-sync.
|
|
||||||
|
|
||||||
- L3A and L3B are parallel paths, not a progression.
|
|
||||||
|
|
||||||
✅ RESOLVED (see PROJECT.md W2.A): Tag mutability for production-bound references — Path B (tag for dev/qa, SHA for prod). The platform provides a CLI command that resolves the current tag to its SHA for prod-bound workflows.
|
|
||||||
|
|
||||||
## 5. Layer 3B — Agentic Consumer Surface
|
|
||||||
|
|
||||||
Locked commitments (unchanged from v0.1):
|
|
||||||
|
|
||||||
- Hybrid runtime, skill as markdown, agent as executor.
|
|
||||||
|
|
||||||
- Trust model: trust and always verify on the platform side.
|
|
||||||
|
|
||||||
- Skill envelope (4 dimensions).
|
|
||||||
|
|
||||||
- Stateless agents, all state in the platform.
|
|
||||||
|
|
||||||
Environment progression — locked (this revision):
|
|
||||||
|
|
||||||
| Environment | Autonomy | Attester | Gate |
|
|
||||||
|---|---|---|---|
|
|
||||||
| dev | Full autonomy (no HITL) | — | Confidence signal ≥ 0.50, all six inputs present |
|
|
||||||
| qa | Held for attestation | QA | GitHub Deployment approval + full QA matrix (see §10) |
|
|
||||||
| prod | Held for attestation | SRE | GitHub Deployment approval + full SRE matrix (see §10) |
|
|
||||||
| dr | Held for attestation | SRE | GitHub Deployment approval + dr-drill evidence (see §10) |
|
|
||||||
|
|
||||||
Staging is removed. Dev is the only autonomous environment and absorbs integration, contract, security smoke, and performance smoke validation. The CDLC reference document's environment model is a doc-sync item flagged at the top of this document.
|
|
||||||
|
|
||||||
Profile marker: profile: agentic unlocks L3B-specific fields naturalLanguageIntent, confidenceAtSubmission, agentTrace).
|
|
||||||
|
|
||||||
✅ RESOLVED (see PROJECT.md BA.A): Skill catalog — initial set: web API, worker, scheduled job, static asset, basic observability bootstrap. Addition criteria: (a) reviewable for sensitive data, (b) expressible as a single contract submission, (c) documented use case.
|
|
||||||
|
|
||||||
## 6. Cross-Cutting — Central Pipeline Template
|
|
||||||
|
|
||||||
Locked commitments (unchanged from v0.1):
|
|
||||||
|
|
||||||
- JSON Schema (draft 2020-12) with thin domain-specific wrapper.
|
|
||||||
|
|
||||||
- Central repo + generated client libraries.
|
|
||||||
|
|
||||||
- Multi-stage validation pipeline (schema → policy → NFR → confidence).
|
|
||||||
|
|
||||||
- Distributed enrichment.
|
|
||||||
|
|
||||||
- GitOps reconciler + Terraform execution layer.
|
|
||||||
|
|
||||||
Locked additions this revision:
|
|
||||||
|
|
||||||
- The GitOps reconciler is the platform's K8s API. The cdlc-gitops repository's state materializes into K8s CRDs (ArgoCD Applications or Flux Kustomizations) that the reconciler watches. This is the platform's internal state surface.
|
|
||||||
|
|
||||||
- The pipeline emits a PolicyCheckResult record per policy rule evaluated. The confidence signal consumes these as one normalized input (Section 8).
|
|
||||||
|
|
||||||
✅ RESOLVED (see PROJECT.md W3.D): L1/L2 standard versioning details — semver (interface→MAJOR, behavior→MINOR, lifecycle→PATCH); L2 contracts pin L1 by `name@semver`; the resolver picks the highest compatible; MAJOR bumps require a new registry entry (immutable publication); old entry enters a 12-month deprecation window.
|
|
||||||
|
|
||||||
✅ RESOLVED (see PROJECT.md W3.E): Schema mandatory vs. optional inputs — dev requires stack+environment; qa adds validation.e2eSuite + validation.loadTest; prod adds runbook + dashboard + oncall; dr adds drDrillRef; `inputs` always optional; `profile: agentic` fields optional everywhere (naturalLanguageIntent required when profile is agentic).
|
|
||||||
|
|
||||||
## 7. Cross-Cutting — Contract Schema
|
|
||||||
|
|
||||||
Locked commitments (unchanged from v0.1):
|
|
||||||
|
|
||||||
- Central repo + generated client libraries.
|
|
||||||
|
|
||||||
- Strict fail-fast at schema stage, multi-stage validation pipeline with reason codes from a published vocabulary.
|
|
||||||
|
|
||||||
✅ RESOLVED (see PROJECT.md W3.E): Schema mandatory vs. optional inputs. The CDLC reference contract example [1] is illustrative; the v1 contract schema has explicit per-field mandatory/optional declarations per environment.
|
|
||||||
|
|
||||||
## 8. Cross-Cutting — Confidence Signal
|
|
||||||
|
|
||||||
Locked commitments (unchanged from v0.1):
|
|
||||||
|
|
||||||
- Six canonical inputs.
|
|
||||||
|
|
||||||
- Weighted sum with per-input breakdown.
|
|
||||||
|
|
||||||
- Per-environment thresholds: dev ≥ 0.50, qa ≥ 0.75, prod ≥ 0.90, dr ≥ 0.95.
|
|
||||||
|
|
||||||
- Structured output { score, band, perInput, reasonCodes }.
|
|
||||||
|
|
||||||
- 1-year storage, no algorithm retraining in v1.
|
|
||||||
|
|
||||||
- Halt with explicit reason on missing input.
|
|
||||||
|
|
||||||
Locked additions this revision:
|
|
||||||
|
|
||||||
- The policy check results input is a list of PolicyCheckResult records from the normalized schema (Section 9, 12). The signal does not know which engine produced which result.
|
|
||||||
|
|
||||||
- Severity → score penalty mapping: critical → hard override to mandatory block, high → -0.2, medium → -0.05, low → -0.01, info → 0.0. One critical finding hard-overrides the score regardless of all other inputs.
|
|
||||||
|
|
||||||
✅ RESOLVED (see PROJECT.md BA.B): Threshold tuning policy. Thresholds frozen for v1. Tuning begins v1.2: quarterly FP/FN tracking per environment; override authority = Infra & Ops + SRE joint sign-off; any override is itself a confidence-event in the audit stream.
|
|
||||||
|
|
||||||
## 9. Cross-Cutting — Audit and Evidence Stream
|
|
||||||
|
|
||||||
Locked commitments (unchanged from v0.1):
|
|
||||||
|
|
||||||
- Tiered audit ledger: S3 with Object Lock in compliance mode (cold, source of truth, 7-year retention) + GitHub audit repo (hot, query index, not part of the chain).
|
|
||||||
|
|
||||||
- Daily checkpoints.
|
|
||||||
|
|
||||||
- Event schema: JWS detached signature, prev_event_hash chain, controlled-vocabulary event_type.
|
|
||||||
|
|
||||||
- Outbox pattern with local durable outbox + async worker.
|
|
||||||
|
|
||||||
- Linkage via workflow run ID or agent invocation ID.
|
|
||||||
|
|
||||||
Locked additions this revision:
|
|
||||||
|
|
||||||
- The outbox database is DynamoDB. RPO is zero (synchronous write to local outbox before contract submission ack); RTO is the async worker's recovery from the dead-letter queue. Single-region in v1; multi-region is a v2 concern.
|
|
||||||
|
|
||||||
- The outbox also stores the per-contract QA and prod approver identities (Section 10). The platform-internal identity-distinctness check reads from this outbox. This is the only durable record of the approver identities outside GitHub's audit log.
|
|
||||||
|
|
||||||
## 10. Cross-Cutting — Human-in-the-Loop Mechanics
|
|
||||||
|
|
||||||
Purpose. The human gates at higher environments. The vision's "Lower Environments are Autonomous; Higher Environments are Attested" tenet [1] and the "deliberate human attestation — not as a rubber stamp" requirement [1] are the binding constraints.
|
|
||||||
|
|
||||||
### 10.1 Gate model
|
|
||||||
|
|
||||||
Pre-execution gates. The contract is held in a "validated but not applied" state until the human attests. qa, prod, and dr are PR-based attestation gates backed by GitHub Environments with required reviewers.
|
|
||||||
|
|
||||||
For qa and prod, there is no partial deployment to roll back on rejection. For dr, the same model — promotion to the DR environment is a separate GitHub Deployment, gated by SRE, against a separate cluster/region. The canary/deployment-rollback model is explicitly not in scope for v1.
|
|
||||||
|
|
||||||
### 10.2 Reviewer routing
|
|
||||||
|
|
||||||
GitHub CODEOWNERS + GitHub Environment required reviewers. qa → QA team; prod → SRE team; dr → SRE team. CODEOWNERS is the routing layer; it does not enforce identity distinctness.
|
|
||||||
|
|
||||||
### 10.3 Separation of duties — identity distinctness
|
|
||||||
|
|
||||||
Mechanism is platform-internal, not GitHub-native, not Kyverno (in v1).
|
|
||||||
|
|
||||||
Sequence:
|
|
||||||
|
|
||||||
1. On promotion dev → qa, the platform reads the QA approver's GitHub identity from the GitHub Deployment approval event and writes it to the DynamoDB outbox keyed by contractId.
|
|
||||||
|
|
||||||
2. On promotion qa → prod, the platform reads the stored QA approver identity from the outbox and the new SRE approver identity from the GitHub Deployment approval event.
|
|
||||||
|
|
||||||
3. If qaApprover == prodApprover, the platform blocks the prod promotion, writes a SEPARATION_OF_DUTIES_VIOLATION event to the evidence stream, and routes a halt artifact to the SRE on-call.
|
|
||||||
|
|
||||||
4. The check is implemented in the central pipeline repo, not as an external policy. The platform is the only writer to the outbox; the check is in the same process that has authority to block the promotion.
|
|
||||||
|
|
||||||
### 10.4 Full HITL attestation matrix
|
|
||||||
|
|
||||||
| Env | Concern | Evidence artifact | Freshness | Source | Attester |
|
|
||||||
|---|---|---|---|---|---|
|
|
||||||
| qa | Functional correctness | Last successful run of contract-declared validation.e2eSuite with pass rate ≥ 99% | Last 24h | Test runner declared in contract | QA |
|
|
||||||
| qa | Performance baseline | Load test report (k6 / Gatling / Locust) showing p99 latency < declared NFR and throughput > declared minimum | Last 7d | Load test runner declared in contract | QA |
|
|
||||||
| qa | Security posture | Vulnerability scan (Trivy, Snyk, or contract-declared equivalent) with no criticals/highs, signed by Security on-call | Last 24h | Security scanner + Security team signature | QA |
|
|
||||||
| qa | Contract NFRs | Platform-generated report: schema valid, NFR assertions (latency, throughput, error rate) within declared bounds | At submission | Platform contract validator | QA |
|
|
||||||
| prod | Operational readiness | Runbook published, dashboard exists, on-call rotation assigned, alerts configured | At submission, validated against last 30d history | Platform + SRE | SRE |
|
|
||||||
| prod | Incident response | Sev-1 runbook tabletop or live drill completed | Last 90d | SRE drill record | SRE |
|
|
||||||
| prod | Capacity / cost | FinOps forecast for next 30d within budget envelope, cost anomaly baseline stored, budget alert configured | Forecast valid for next 30d | FinOps + SRE | SRE |
|
|
||||||
| prod | Resilience | DR drill, chaos engineering report, backup verified | DR: 180d; chaos: 90d; backup: 30d | SRE + Platform | SRE |
|
|
||||||
| dr | dr-region deploy with the most recent prod-bound dr drill as canary evidence | dr drill report | Last 180d | SRE | SRE |
|
|
||||||
|
|
||||||
### 10.5 Timeout behavior
|
|
||||||
|
|
||||||
| Time | State | Action |
|
|
||||||
|---|---|---|
|
|
||||||
| Submission | PENDING_ATTESTATION | Notify responsible team |
|
|
||||||
| 1 business day | PENDING_ATTESTATION_WARNING | Notify team + platform on-call (elevated path); emit PENDING_ATTESTATION_TIMEOUT_WARNING event |
|
|
||||||
| 2 business days | PENDING_ATTESTATION_AUTO_FREEZE | Auto-freeze; require re-submission; emit PENDING_ATTESTATION_AUTO_FREEZE event; new submission linked via supersedes |
|
|
||||||
|
|
||||||
### 10.6 Rejection and rollback
|
|
||||||
|
|
||||||
Rejection returns the contract to a HELD state with the rejection reason captured as a PROMOTION_REJECTED event. The consumer fixes the cause and re-submits; the new submission is linked to the rejected one via supersedes. The audit chain is extended, not torn up — matching the resolution session's answer.
|
|
||||||
|
|
||||||
There is no partial deployment to roll back at any v1 gate.
|
|
||||||
|
|
||||||
## 11. Cross-Cutting — Agentic Stack
|
|
||||||
|
|
||||||
Locked commitments (unchanged from v0.1):
|
|
||||||
|
|
||||||
- Hybrid runtime, platform-managed control plane + consumer-owned agent.
|
|
||||||
|
|
||||||
- Versioned, signed skill catalog over MCP.
|
|
||||||
|
|
||||||
- Skill envelope enforced on invocation and result submission.
|
|
||||||
|
|
||||||
- Consumer-owned skill execution environment. Platform does not run the skill.
|
|
||||||
|
|
||||||
- Stateless agents, all state in the platform.
|
|
||||||
|
|
||||||
Locked additions this revision:
|
|
||||||
|
|
||||||
- Skills are reviewed for sensitive data before release. Secrets, customer data, internal IPs, and other sensitive payloads are forbidden in skill markdown. The review is owned by Infra & Ops and is the mandatory release gate for any new skill. This is the trade-off for accepting the L3B runtime threat model (skill content is consumer-readable, so the platform must not put anything sensitive in it).
|
|
||||||
|
|
||||||
✅ RESOLVED (see PROJECT.md BA.A): Skill catalog — initial set, addition process, deprecation process per the resolution.
|
|
||||||
|
|
||||||
## 12. Cross-Cutting — L1/L2 Substrate Execution
|
|
||||||
|
|
||||||
Purpose. The technical execution layer for the L1/L2 substrate, including the substrate abstraction that protects v1 from polyglot mess while leaving v2+ room to grow.
|
|
||||||
|
|
||||||
### 12.1 Substrate abstraction (locked this revision)
|
|
||||||
|
|
||||||
L1/L2 are substrate-agnostic in shape. The architecture defines a Target Stack Intermediate Representation (IR) — a substrate-neutral description of:
|
|
||||||
|
|
||||||
- Resources with typed input contracts, typed output contracts, and declared NFRs.
|
|
||||||
|
|
||||||
- Relationships (single parent per child, with a shared keyword for multi-relationship dependencies).
|
|
||||||
|
|
||||||
- Composition (a tree of resources with max depth 5).
|
|
||||||
|
|
||||||
- Policy hooks (the points in the composition where policy checks attach).
|
|
||||||
|
|
||||||
The L1 registry, the L2 thin-composition tree, the YML standard, and the policy check result schema are all defined against the IR. None of them is defined against any specific substrate.
|
|
||||||
|
|
||||||
Substrate adapters are the only substrate-specific code. An adapter compiles the IR into a substrate execution plan. v1 ships exactly one adapter: the Terraform adapter. v2+ may add additional adapters (OpenTofu, Pulumi, K8s CRDs) without architectural change.
|
|
||||||
|
|
||||||
v1 implementation reality: the IR is shaped to round-trip cleanly to Terraform because there is no other adapter to differentiate from. The IR and the Terraform output are nearly isomorphic in v1. As additional adapters appear in v2+, the IR gets more expressive (e.g., substrate-specific output types) and the adapters gain translation logic, but the L1 module content, the YML standard, and the thin-composition tree do not change. This is the design that prevents the polyglot mess.
|
|
||||||
|
|
||||||
Why not build the abstraction earlier? Building a substrate-agnostic IR before there is a second adapter to test against is speculative generality. The v1 commitment is: (1) the L1 module interface is defined against the IR even though the only adapter is Terraform, and (2) the central pipeline, registry, and policy schema consume the IR-typed contracts. The adapter is the only place where substrate terminology appears in v1.
|
|
||||||
|
|
||||||
### 12.2 Terraform adapter (v1)
|
|
||||||
|
|
||||||
The Terraform adapter:
|
|
||||||
|
|
||||||
- Translates the IR-typed L1 module interface to a Terraform variable block and a Terraform output block.
|
|
||||||
|
|
||||||
- Translates the IR-typed L2 thin-composition tree to a Terraform root module that calls the L1 modules.
|
|
||||||
|
|
||||||
- Translates the IR-typed relationships to Terraform module references.
|
|
||||||
|
|
||||||
- Emits a Terraform plan from the IR.
|
|
||||||
|
|
||||||
The adapter is a thin layer. It does not own L1/L2 content; it only translates.
|
|
||||||
|
|
||||||
### 12.3 State storage
|
|
||||||
|
|
||||||
Locked: S3 (state files) + DynamoDB (state locking), cloud-managed. Single-region in v1.
|
|
||||||
|
|
||||||
### 12.4 Policy toolchain
|
|
||||||
|
|
||||||
Locked:
|
|
||||||
|
|
||||||
- Checkov for Terraform plan policy (the four L2 thin-composition checks: secrets-in-plaintext, public ingress, IAM wildcard, KMS key reference, plus tag and naming convention). Checkov is open-source, has a broad rule catalog, and is GitOps-friendly.
|
|
||||||
|
|
||||||
- Kyverno for K8s-native policy (platform-internal state in the GitOps reconciler, separation-of-dues-adjacent checks if any are added in v2, future CRD validation).
|
|
||||||
|
|
||||||
- OPA/Rego is reserved for cross-resource policy and is explicitly last resort due to Rego complexity.
|
|
||||||
|
|
||||||
### 12.5 Execution layer
|
|
||||||
|
|
||||||
Locked: GitHub Actions. terraform plan and terraform apply run in the central pipeline repo's GitHub Actions workflow. State locking via DynamoDB. AWS credentials via OIDC federation (long-lived credentials are forbidden). The platform does not run terraform apply against a developer's workstation; all execution is in the central pipeline.
|
|
||||||
|
|
||||||
> **ACDL Phase 07 note (D-039):** Gitea Actions (the ACDL forge) does not
|
|
||||||
> support `id-token: write` / OIDC token issuance as of Gitea 1.27.x /
|
|
||||||
> gitea-runner v2.1.0 (formerly `act_runner`, renamed 2026-04 in
|
|
||||||
> gitea/runner#850). The v1.1 spike uses a per-run-rotated long-lived key
|
|
||||||
> waiver; real OIDC federation is a v1.2 deliverable, blocked on
|
|
||||||
> go-gitea/gitea#36988. The §12.5 "long-lived credentials are forbidden"
|
|
||||||
> commitment is the locked target; the waiver is a time-boxed spike
|
|
||||||
> exception.
|
|
||||||
|
|
||||||
### 12.6 Policy result normalization (locked this revision)
|
|
||||||
|
|
||||||
The confidence signal does not consume raw Checkov or Kyverno output. It consumes a normalized PolicyCheckResult schema produced by substrate-specific adapters.
|
|
||||||
|
|
||||||
Schema (canonical form, lives in the central pipeline repo):
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"contractId": "uuid",
|
|
||||||
"evaluatedAt": "ISO-8601",
|
|
||||||
"engine": "checkov | kyverno | opa",
|
|
||||||
"ruleId": "CKV_AWS_24 | KYVERNO_NO_PRIVILEGED | ...",
|
|
||||||
"severity": "critical | high | medium | low | info",
|
|
||||||
"result": "pass | fail | skipped | error",
|
|
||||||
"message": "human-readable",
|
|
||||||
"evidence": { "...engine-specific payload, opaque to the signal..." },
|
|
||||||
"resourceRef": "IR-typed resource identifier"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The Checkov adapter runs in the same GitHub Actions step as Checkov itself and translates Checkov JSON to PolicyCheckResult records. The Kyverno adapter runs as a controller in the platform's K8s cluster and translates Kyverno PolicyReport CRDs to PolicyCheckResult records. The confidence signal's policy input component is the union of all PolicyCheckResult records, regardless of engine. The signal does not know which engine produced which result — substrate-agnostic over its inputs, matching the L1/L2 model's substrate-agnostic over its outputs.
|
|
||||||
|
|
||||||
### 12.7 Registry maintenance
|
|
||||||
|
|
||||||
Locked: L1 module publication updates the L1 registry in the same PR as the module. Registry and module land together. The registry is the IR-typed contract, not a Terraform-specific variable schema. The L1 registry, the central pipeline, and the policy schema all consume the same IR-typed contract — there is one source of truth for the L1 interface, not multiple substrate-specific copies.
|
|
||||||
|
|
||||||
### 12.8 Contract-schema-to-IR resolution
|
|
||||||
|
|
||||||
The contract schema declares the consumer's intent in IR-typed terms. The central pipeline resolves the contract to a target stack (a list of L1 module instances with their inputs and the relationships between them). The Terraform adapter compiles the target stack to a Terraform execution plan. This resolution is substrate-agnostic — the target stack is in the IR.
|
|
||||||
|
|
||||||
## 13. Consolidated Open Design Decisions
|
|
||||||
|
|
||||||
✅ **All 11 decisions are RESOLVED (see PROJECT.md).** The §13 subsections
|
|
||||||
below preserve the upstream structure with the `🟡 OPEN` markers replaced
|
|
||||||
by `✅ RESOLVED (see PROJECT.md)`.
|
|
||||||
|
|
||||||
### From Wave 1 (L1/L2 Substrate)
|
|
||||||
|
|
||||||
- (W1.A) AI-refinement trigger. ✅ RESOLVED (see PROJECT.md) — joint condition: N ≥ 50 consecutive zero-rollback changes AND no L1/L2 incident in 6 months AND Infra & Ops unilateral override.
|
|
||||||
|
|
||||||
- (W1.B) Multi-stack edge case rule. ✅ RESOLVED (see PROJECT.md) — permitted only for (a) DR-region mirror, (b) time-boxed experimental stack TTL ≤ 30d, (c) explicit Infra & Ops approval with `multiStack.justification`.
|
|
||||||
|
|
||||||
### From Wave 2 (L3A/L3B)
|
|
||||||
|
|
||||||
- (W2.A) Tag mutability for production-bound references. ✅ RESOLVED (see PROJECT.md) — Path B (tag for dev/qa, SHA for prod) with platform-provided CLI to resolve tag → SHA.
|
|
||||||
|
|
||||||
### From Wave 3 (Technical Execution)
|
|
||||||
|
|
||||||
- (W3.D) L1/L2 standard versioning details. ✅ RESOLVED (see PROJECT.md) — semver (interface→MAJOR, behavior→MINOR, lifecycle→PATCH); L2 pins L1 by `name@semver`; MAJOR bump = new registry entry + 12-month deprecation.
|
|
||||||
|
|
||||||
- (W3.E) Schema mandatory vs. optional inputs. ✅ RESOLVED (see PROJECT.md) — per-env mandatory table (dev: stack+environment; qa adds validation.e2eSuite+loadTest; prod adds runbook+dashboard+oncall; dr adds drDrillRef); `inputs` always optional; `profile: agentic` fields optional everywhere.
|
|
||||||
|
|
||||||
### From Beyond Architecture
|
|
||||||
|
|
||||||
- (BA.A) Skill catalog. ✅ RESOLVED (see PROJECT.md) — 5 skills (web API, worker, scheduled job, static asset, basic observability bootstrap); addition criteria locked.
|
|
||||||
|
|
||||||
- (BA.B) Confidence signal threshold tuning. ✅ RESOLVED (see PROJECT.md) — frozen for v1; tuning begins v1.2 (quarterly FP/FN; override = Infra & Ops + SRE joint sign-off).
|
|
||||||
|
|
||||||
- (BA.C) On-call and operational ownership. ✅ RESOLVED (see PROJECT.md) — platform on-call = Infra & Ops; L3A/L3B halt → Sev2; consumer outage → Sev1.
|
|
||||||
|
|
||||||
- (BA.D) Cost and capacity governance. ✅ RESOLVED (see PROJECT.md) — FinOps owns; per-contract monthly reporting; hard halt at 120% of declared budget via the confidence signal; override = FinOps + SRE joint sign-off.
|
|
||||||
|
|
||||||
- (BA.E) Consumer onboarding. ✅ RESOLVED (see PROJECT.md) — developer (L3A): getting-started → contract schema + central pipeline template; citizen (L3B): scoped agent + skill catalog; both end in a sandbox dev submission that must pass the confidence gate.
|
|
||||||
|
|
||||||
- (BA.F) Cross-platform evolution. ✅ RESOLVED (see PROJECT.md) — contract schema, IR, PolicyCheckResult, confidence signal, audit stream are portable; forge-specific code = workflow YAML, OIDC trust, CODEOWNERS, Environments; a second forge needs a forge adapter + workflow-template translator.
|
|
||||||
|
|
||||||
- (Q1.3) OpenTofu timing. ✅ RESOLVED (deferred — see PROJECT.md) — not in v1 or v1.1; the substrate abstraction makes OpenTofu a future adapter, not an architecture change.
|
|
||||||
|
|
||||||
## 14. Document Status and Next Steps
|
|
||||||
|
|
||||||
Status: **v1.0**. All 11 open items in §13 are resolved. The architecture is
|
|
||||||
internally consistent; the v1.1 implementation spike (ACDL Phases 08-10)
|
|
||||||
validates the locked substrate abstraction + contract→IR→adapter path
|
|
||||||
against real AWS via a per-run-rotated key (D-039; OIDC deferred to v1.2).
|
|
||||||
The v1.2 build-out (S3 Object Lock, JWS, HITL wiring, L3B skill catalog,
|
|
||||||
Kyverno/OPA, real OIDC federation, multi-region) is design-authored in
|
|
||||||
Phase 07 and implemented post-spike.
|
|
||||||
|
|
||||||
Doc-sync items (out of scope of this document but flagged for the same change set):
|
|
||||||
|
|
||||||
- The CDLC reference document's environment model assumes staging exists. Path A invalidates that. The CDLC contract example's targetEnvironments: [staging, production] must be revised to [dev, qa, prod, dr].
|
|
||||||
|
|
||||||
## 15. Phase 07 authored artifacts
|
|
||||||
|
|
||||||
The 11 resolutions are recorded in `PROJECT.md` (decisions D-034..D-046 +
|
|
||||||
the "Open-decision resolutions" table). Phase 07 formalizes the locked
|
|
||||||
commitments into these schema/design files (landed in Waves 2-4 of
|
|
||||||
Phase 07):
|
|
||||||
|
|
||||||
| REQ | File | Owner persona |
|
|
||||||
|-----|------|--------------|
|
|
||||||
| REQ-17 | `schemas/ir.schema.json` | platform-engineer |
|
|
||||||
| REQ-18 | `schemas/policy_check_result.schema.json` + `adapters/terraform/policy/checkov_adapter.py` | security-engineer |
|
|
||||||
| REQ-19 | `platform/confidence_signal.py` | backend-engineer + security-engineer (co-authored) |
|
|
||||||
| REQ-20 | `platform/audit_ledger_design.md` | security-engineer |
|
|
||||||
| REQ-21 | `platform/hitl_matrix_design.md` + `platform/separation_of_duties.py` | security-engineer |
|
|
||||||
| REQ-22 | `schemas/contract.schema.json` | backend-engineer |
|
|
||||||
|
|
||||||
The spike scope (D-041, D-043) vs v1.2 build-out boundary for each design
|
|
||||||
is documented in the respective file.
|
|
||||||
+195
-406
@@ -1,452 +1,241 @@
|
|||||||
# Architecture Document v0.2 + Executive Demo Plan
|
# Architecture
|
||||||
|
|
||||||
Three things to set up before I deliver the document, because they determine how I write the doc:
|
> **Status:** v1.0 (current). All design decisions are resolved. This is the
|
||||||
|
> source of truth for *how* the platform works; the [Vision](vision) is the
|
||||||
1. What is locked from the resolution session. Eight items: environment model (Path A — dev-only autonomous, no staging), substrate abstraction (Target Stack IR + adapter pattern, Terraform adapter in v1), policy toolchain (Checkov for Terraform plan, Kyverno for K8s, OPA last resort), separation of duties (CODEOWNERS for routing + DynamoDB outbox for identity distinctness), policy normalization PolicyCheckResult schema with engine adapters), HITL matrix (full 8-concern matrix with evidence, freshness, source), HITL timeout (1d warn, 2d freeze), HITL rollback (pre-execution model, audit chain extended, no partial deploy).
|
> source of truth for *why*.
|
||||||
|
|
||||||
2. What is still open after the session. Eleven items, listed in the updated Section 13. They are the gating items for v1.0.
|
|
||||||
|
|
||||||
3. Doc-sync item. The CDLC reference document (not in scope of this architecture) describes staging as an environment and uses targetEnvironments: [staging, production] in its contract example. Path A invalidates that. The CDLC document is the responsibility of its own owners, but this architecture flags the inconsistency so the CDLC revision can be scheduled in the same change set.
|
|
||||||
|
|
||||||
The architecture document is below, followed by the executive demo plan, followed by the final three questions I need resolved before v1.0.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Agentic Cloud Delivery Platform — Architecture Document
|
|
||||||
|
|
||||||
Status: Draft v0.2. Section 13 lists 11 open decisions. Eight of the original 15 open items are now locked; seven remain from Section 13 plus four new sub-decisions surfaced during resolution.
|
|
||||||
|
|
||||||
Companion to: Agentic Cloud Delivery Vision [1].
|
|
||||||
|
|
||||||
Authoring principle: The vision is the source of truth for why [1]; this document is the source of truth for how. Where the two conflict, the vision wins.
|
|
||||||
|
|
||||||
Resolution session log (this revision):
|
|
||||||
|
|
||||||
| ID | Question | Resolution |
|
|
||||||
|---|---|---|
|
|
||||||
| Q1 | Environment model | Path A locked. Dev is the only autonomous environment. QA HITL at qa. SRE HITL at prod and dr. Staging does not exist. |
|
|
||||||
| Q1.2 | Substrate trajectory | Substrate abstraction locked. L1/L2 are defined against a Target Stack IR. Substrate adapters compile the IR to a substrate execution plan. v1 ships only the Terraform adapter. |
|
|
||||||
| Q1.3 | OpenTofu timing | 🟡 OPEN (W3.D-adjacent). No specific version or trigger committed. |
|
|
||||||
| Q2.1 | Policy toolchain | Locked. Checkov for Terraform plan policy. Kyverno for K8s-native and platform-internal policy. OPA/Rego reserved for cross-resource cases; explicitly last resort due to Rego complexity. |
|
|
||||||
| Q2.2 | Separation of duties | Locked. GitHub CODEOWNERS routes the right reviewer to the right environment. Platform-internal identity record in DynamoDB outbox enforces qaApprover ≠ prodApprover for the same contract. |
|
|
||||||
| Q2.3 | Policy normalization | Locked. PolicyCheckResult JSON schema is the contract between engines and the confidence signal. Engine-specific adapters translate native output to the schema. |
|
|
||||||
| Q3 | HITL matrix + timeout + rollback | Locked (full 8-concern matrix in §10). Pre-execution gate model. 1 business day = warn + escalate. 2 business days = auto-freeze + re-submit. Rejection extends the audit chain, no partial deploy to roll back. |
|
|
||||||
| W1.A | AI-refinement trigger | 🟡 OPEN. Recommendation pending sign-off. |
|
|
||||||
| W1.B | Multi-stack edge case rule | 🟡 OPEN. Recommendation pending sign-off. |
|
|
||||||
| W2.A | Tag mutability for prod | 🟡 OPEN. Recommendation pending sign-off. |
|
|
||||||
| W3.D | L1/L2 standard versioning details | 🟡 OPEN. |
|
|
||||||
| W3.E | Schema mandatory vs. optional inputs | 🟡 OPEN. |
|
|
||||||
| BA.A–F | Beyond-architecture questions | 🟡 OPEN (6 items). |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 0. Purpose
|
## 0. Purpose
|
||||||
|
|
||||||
This document encodes the architectural commitments that realize the vision [1]. The resolution session has closed eight open items; the document is now at v0.2 with eleven open items remaining, listed in Section 13. Every locked commitment is grounded in either a vision tenet or a specific decision made during resolution.
|
This document encodes the architectural commitments that realize the
|
||||||
|
[vision](vision). Every commitment is grounded in a vision tenet.
|
||||||
|
|
||||||
The structure remains: four layers (L1 primitives, L2 composed stacks, L3A developer surface, L3B agentic surface) plus five cross-cutting concerns (central pipeline, contract schema, confidence signal, audit stream, HITL mechanics), with one addition: the substrate abstraction layer (Section 12) is now a first-class architectural concern, not an implementation detail.
|
The platform is **four layers + six cross-cutting concerns**, bound by the
|
||||||
|
vision's "Two Consumer Surfaces, One Platform" tenet: both surfaces converge
|
||||||
|
on the same contract schema, the same policy envelope, and the same evidence
|
||||||
|
stream.
|
||||||
|
|
||||||
## 1. Architectural Overview
|
## 1. Architectural Overview
|
||||||
|
|
||||||
The platform remains four layers and five cross-cutting concerns. The substrate abstraction is added as a sixth cross-cutting concern in Section 12 because it is the binding constraint for the L1/L2 model, the central pipeline, and the policy toolchain.
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A["Consumer surfaces"] --> B["Contract schema"]
|
||||||
|
B --> C["Central pipeline"]
|
||||||
|
C --> D["Modules + primitives"]
|
||||||
|
C --> E["Substrate adapter"]
|
||||||
|
C --> F["Confidence signal"]
|
||||||
|
C --> G["Evidence stream"]
|
||||||
|
D --> E
|
||||||
|
E --> H["Infrastructure"]
|
||||||
|
F --> G
|
||||||
|
```
|
||||||
|
|
||||||
The vision's "Two Consumer Surfaces, One Platform" tenet [1] remains the constraint that binds all concerns: L3A and L3B converge on the same contract schema, the same policy envelope, and the same evidence stream.
|
The four layers:
|
||||||
|
|
||||||
Locked additions this revision:
|
1. **Primitives** — single-purpose, substrate-agnostic modules representing
|
||||||
|
the smallest reusable infrastructure pieces (a VPC, an S3 bucket, an ECS
|
||||||
|
cluster). A primitive does not reference other primitives; it takes its
|
||||||
|
environment as input.
|
||||||
|
2. **Modules** — patterns that combine primitives into deployable
|
||||||
|
infrastructure shapes (an ECS Fargate microservice, a static-asset site).
|
||||||
|
A module references registered primitives (max depth 5).
|
||||||
|
3. **Developer surface** — the developer-owned workflow file + contract. The
|
||||||
|
developer references the central pipeline via a versioned tag and owns
|
||||||
|
their workflow file (no platform auto-sync).
|
||||||
|
4. **Agentic surface** — a hybrid runtime where a consumer declares intent
|
||||||
|
in natural language and an agent resolves it to a contract submission.
|
||||||
|
Trust model: trust and always verify on the platform side. Stateless
|
||||||
|
agents; all state lives in the platform.
|
||||||
|
|
||||||
- The environment model is dev (autonomous) → qa (QA HITL) → prod (SRE HITL) → dr (SRE HITL). Staging does not exist.
|
The developer and agentic surfaces are parallel paths, not a progression.
|
||||||
|
Both end in a contract submission that enters the same pipeline.
|
||||||
|
|
||||||
- L1/L2 are substrate-agnostic in shape; substrate adapters are the only substrate-specific component.
|
## 2. Primitives
|
||||||
|
|
||||||
## 2. Layer 1 — Foundational Primitives
|
Single-purpose, substrate-agnostic modules. Locked commitments:
|
||||||
|
|
||||||
Purpose. Single-purpose, substrate-agnostic primitive modules representing the smallest reusable infrastructure pieces. L1 modules do not compose with other L1 modules; L1 takes its environment as input.
|
|
||||||
|
|
||||||
Locked commitments (unchanged from v0.1):
|
|
||||||
|
|
||||||
- No inter-L1 references. L1 may call Terraform data sources.
|
|
||||||
|
|
||||||
- Semver with three triggers (interface → MAJOR, behavior → MINOR, lifecycle → PATCH).
|
|
||||||
|
|
||||||
|
- No inter-primitive references. A primitive may call substrate data sources.
|
||||||
|
- Semver with three triggers: interface → MAJOR, behavior → MINOR,
|
||||||
|
lifecycle → PATCH.
|
||||||
- Immutability on publication.
|
- Immutability on publication.
|
||||||
|
|
||||||
- 12-month deprecation window.
|
- 12-month deprecation window.
|
||||||
|
- AI refinement is a flag, triggered by a joint operational condition
|
||||||
|
(N ≥ 50 consecutive zero-rollback changes, no primitive/module incident in
|
||||||
|
6 months, Infra & Ops unilateral override).
|
||||||
|
- A primitive's interface is defined against the Target Stack (substrate-
|
||||||
|
agnostic), not against any substrate's variable block directly.
|
||||||
|
|
||||||
- AI refinement is a flag.
|
## 3. Modules
|
||||||
|
|
||||||
🟡 OPEN (W1.A): AI-refinement operational trigger. The criterion for flipping aiRefinement from false to true needs a falsifiable operational signal. Recommendation: joint condition — N ≥ 50 consecutive changes with zero rollbacks AND no L1/L2 incident in the last 6 months AND Infra & Ops holds a unilateral override. Pending sign-off.
|
Patterns that combine primitives into deployable shapes. Locked commitments:
|
||||||
|
|
||||||
🟡 OPEN (sub-decision surfaced this revision): The L1 module's interface field is defined against the Target Stack IR, not against Terraform's variable block directly. In v1, the IR is shaped to round-trip cleanly to Terraform, but the schema is substrate-agnostic. Pending v1 implementation details in Section 12.
|
|
||||||
|
|
||||||
## 3. Layer 2 — Composed Stacks
|
|
||||||
|
|
||||||
Purpose. Combine L1 primitives into deployable infrastructure shapes. Each codebase maps to one canonical L2 stack; the stack is either a parameterized module (Shape X) or a thin-composition layer (Shape Y).
|
|
||||||
|
|
||||||
Locked commitments (unchanged from v0.1):
|
|
||||||
|
|
||||||
- 1 codebase = 1 L2 stack (default), with multiStack: true for exceptions.
|
|
||||||
|
|
||||||
- Shape X or Shape Y.
|
|
||||||
|
|
||||||
- Hierarchical composition, max depth 5, only registered L1s.
|
|
||||||
|
|
||||||
- Pipeline quality checks: secrets-in-plaintext, public ingress, IAM wildcard, KMS key reference, tag compliance, naming convention.
|
|
||||||
|
|
||||||
- Restricted from thin-composition: IAM principal creation, network boundary creation, key/secret creation, external data transfer.
|
|
||||||
|
|
||||||
|
- One codebase maps to one canonical module (default); `multiStack: true`
|
||||||
|
is permitted only for (a) a DR-region mirror, (b) a time-boxed
|
||||||
|
experimental stack (TTL ≤ 30 days), or (c) explicit Infra & Ops approval
|
||||||
|
with a documented justification.
|
||||||
|
- A module references registered primitives only (max depth 5).
|
||||||
|
- Pipeline quality checks: secrets-in-plaintext, public ingress, IAM
|
||||||
|
wildcard, KMS key reference, tag compliance, naming convention.
|
||||||
|
- Restricted from module patterns: IAM principal creation, network boundary
|
||||||
|
creation, key/secret creation, external data transfer.
|
||||||
- Auto-promote after 3 observed usages.
|
- Auto-promote after 3 observed usages.
|
||||||
|
- A module's pattern tree wires field is defined against the stack's
|
||||||
|
relationship type, not against any substrate's module block. The stack →
|
||||||
|
substrate translation is the substrate adapter's job (§12). The pattern
|
||||||
|
pipeline itself is substrate-agnostic.
|
||||||
|
|
||||||
🟡 OPEN (W1.B): Multi-stack edge case rule. The multiStack: true exception needs a falsifiable rule. Recommendation: permitted only for (a) DR-region mirror of the primary stack, (b) time-boxed experimental stack with TTL ≤ 30 days, (c) explicit Infra & Ops approval for a documented reason captured in multiStack.justification. Pending sign-off.
|
## 4. Developer Surface
|
||||||
|
|
||||||
🟡 OPEN (sub-decision surfaced this revision): The L2 thin-composition tree's wires field is defined against the IR's relationship type, not against a Terraform module block. The IR → Terraform translation is the Terraform adapter's job (Section 12). The thin-composition pipeline itself is substrate-agnostic.
|
|
||||||
|
|
||||||
## 4. Layer 3A — Developer Consumer Surface
|
|
||||||
|
|
||||||
Locked commitments (unchanged from v0.1):
|
|
||||||
|
|
||||||
- Tag-based reference to the central pipeline template.
|
- Tag-based reference to the central pipeline template.
|
||||||
|
|
||||||
- Developer-owned workflow file, no platform auto-sync.
|
- Developer-owned workflow file, no platform auto-sync.
|
||||||
|
- Tag mutability for production-bound references: tag for dev/qa, SHA for
|
||||||
|
prod. The platform provides a CLI command that resolves the current tag
|
||||||
|
to its SHA for prod-bound workflows.
|
||||||
|
|
||||||
- L3A and L3B are parallel paths, not a progression.
|
## 5. Agentic Surface
|
||||||
|
|
||||||
🟡 OPEN (W2.A): Tag mutability for production-bound references. Path A (tag throughout with protection) vs. Path B (tag for dev/qa, SHA for prod). Recommendation: Path B, justified by the vision's "Audit truth lives outside the repository" bet [1] and the "Not a mutable audit log" anti-goal [1]; SHA-pinning is the only guarantee that the exact bytes reviewed in dev/qa are the bytes deployed to prod. The platform provides a CLI command that resolves the current tag to its SHA for prod-bound workflows. Pending sign-off.
|
|
||||||
|
|
||||||
## 5. Layer 3B — Agentic Consumer Surface
|
|
||||||
|
|
||||||
Locked commitments (unchanged from v0.1):
|
|
||||||
|
|
||||||
- Hybrid runtime, skill as markdown, agent as executor.
|
|
||||||
|
|
||||||
|
- Hybrid runtime: skill as markdown, agent as executor.
|
||||||
- Trust model: trust and always verify on the platform side.
|
- Trust model: trust and always verify on the platform side.
|
||||||
|
|
||||||
- Skill envelope (4 dimensions).
|
- Skill envelope (4 dimensions).
|
||||||
|
- Stateless agents; all state in the platform.
|
||||||
- Stateless agents, all state in the platform.
|
- Initial skill catalog: web API, worker, scheduled job, static asset,
|
||||||
|
basic observability bootstrap. Addition criteria: (a) reviewable for
|
||||||
Environment progression — locked (this revision):
|
sensitive data, (b) expressible as a single contract submission,
|
||||||
|
(c) documented use case.
|
||||||
| Environment | Autonomy | Attester | Gate |
|
- `profile: agentic` unlocks agentic-specific fields
|
||||||
|---|---|---|---|
|
(`naturalLanguageIntent`, `confidenceAtSubmission`, `agentTrace`).
|
||||||
| dev | Full autonomy (no HITL) | — | Confidence signal ≥ 0.50, all six inputs present |
|
|
||||||
| qa | Held for attestation | QA | GitHub Deployment approval + full QA matrix (see §10) |
|
|
||||||
| prod | Held for attestation | SRE | GitHub Deployment approval + full SRE matrix (see §10) |
|
|
||||||
| dr | Held for attestation | SRE | GitHub Deployment approval + dr-drill evidence (see §10) |
|
|
||||||
|
|
||||||
Staging is removed. Dev is the only autonomous environment and absorbs integration, contract, security smoke, and performance smoke validation. The CDLC reference document's environment model is a doc-sync item flagged at the top of this document.
|
|
||||||
|
|
||||||
Profile marker: profile: agentic unlocks L3B-specific fields naturalLanguageIntent, confidenceAtSubmission, agentTrace).
|
|
||||||
|
|
||||||
🟡 OPEN (BA.A): Skill catalog. Which skills exist in the initial L3B capability set, who decides what gets added, how are skills deprecated. Pending resolution.
|
|
||||||
|
|
||||||
## 6. Cross-Cutting — Central Pipeline Template
|
## 6. Cross-Cutting — Central Pipeline Template
|
||||||
|
|
||||||
Locked commitments (unchanged from v0.1):
|
- JSON Schema (draft 2020-12) with a thin domain-specific wrapper.
|
||||||
|
|
||||||
- JSON Schema (draft 2020-12) with thin domain-specific wrapper.
|
|
||||||
|
|
||||||
- Central repo + generated client libraries.
|
- Central repo + generated client libraries.
|
||||||
|
- Multi-stage validation pipeline: schema → policy → NFR → confidence.
|
||||||
- Multi-stage validation pipeline (schema → policy → NFR → confidence).
|
|
||||||
|
|
||||||
- Distributed enrichment.
|
- Distributed enrichment.
|
||||||
|
- GitOps reconciler + substrate execution layer.
|
||||||
- GitOps reconciler + Terraform execution layer.
|
- The pipeline emits a `PolicyCheckResult` record per policy rule evaluated;
|
||||||
|
the confidence signal consumes these as one normalized input (§8).
|
||||||
Locked additions this revision:
|
|
||||||
|
|
||||||
- The GitOps reconciler is the platform's K8s API. The cdlc-gitops repository's state materializes into K8s CRDs (ArgoCD Applications or Flux Kustomizations) that the reconciler watches. This is the platform's internal state surface.
|
|
||||||
|
|
||||||
- The pipeline emits a PolicyCheckResult record per policy rule evaluated. The confidence signal consumes these as one normalized input (Section 8).
|
|
||||||
|
|
||||||
🟡 OPEN (W3.D): L1/L2 standard versioning details — semver scheme, pin model, evolution compatibility contract.
|
|
||||||
|
|
||||||
🟡 OPEN (W3.E): Schema mandatory vs. optional inputs — which are required for all consumers, which are required only for higher environments, which are always optional.
|
|
||||||
|
|
||||||
## 7. Cross-Cutting — Contract Schema
|
## 7. Cross-Cutting — Contract Schema
|
||||||
|
|
||||||
Locked commitments (unchanged from v0.1):
|
|
||||||
|
|
||||||
- Central repo + generated client libraries.
|
- Central repo + generated client libraries.
|
||||||
|
- Strict fail-fast at the schema stage with reason codes from a published
|
||||||
- Strict fail-fast at schema stage, multi-stage validation pipeline with reason codes from a published vocabulary.
|
vocabulary.
|
||||||
|
- Per-environment mandatory fields: dev requires stack + environment; qa
|
||||||
🟡 OPEN (W3.E): Schema mandatory vs. optional inputs. The CDLC reference contract example [1] is illustrative; the v1 contract schema needs explicit per-field mandatory/optional declarations per environment.
|
adds `validation.e2eSuite` + `validation.loadTest`; prod adds runbook +
|
||||||
|
dashboard + oncall; dr adds `drDrillRef`. `inputs` is always optional.
|
||||||
|
`profile: agentic` fields are optional everywhere (`naturalLanguageIntent`
|
||||||
|
required when profile is agentic).
|
||||||
|
|
||||||
## 8. Cross-Cutting — Confidence Signal
|
## 8. Cross-Cutting — Confidence Signal
|
||||||
|
|
||||||
Locked commitments (unchanged from v0.1):
|
- Six canonical inputs: policy, validation, freshness, source, history, NFRs.
|
||||||
|
|
||||||
- Six canonical inputs.
|
|
||||||
|
|
||||||
- Weighted sum with per-input breakdown.
|
- Weighted sum with per-input breakdown.
|
||||||
|
|
||||||
- Per-environment thresholds: dev ≥ 0.50, qa ≥ 0.75, prod ≥ 0.90, dr ≥ 0.95.
|
- Per-environment thresholds: dev ≥ 0.50, qa ≥ 0.75, prod ≥ 0.90, dr ≥ 0.95.
|
||||||
|
- Structured output: `{ score, band, perInput, reasonCodes }`.
|
||||||
- Structured output { score, band, perInput, reasonCodes }.
|
- 1-year storage; no algorithm retraining in v1.
|
||||||
|
|
||||||
- 1-year storage, no algorithm retraining in v1.
|
|
||||||
|
|
||||||
- Halt with explicit reason on missing input.
|
- Halt with explicit reason on missing input.
|
||||||
|
- Severity → score penalty: critical → hard override to mandatory block,
|
||||||
Locked additions this revision:
|
high → -0.2, medium → -0.05, low → -0.01, info → 0.0. One critical finding
|
||||||
|
hard-overrides the score regardless of all other inputs.
|
||||||
- The policy check results input is a list of PolicyCheckResult records from the normalized schema (Section 9, 12). The signal does not know which engine produced which result.
|
- Thresholds frozen for v1; tuning begins post-v1 with quarterly FP/FN
|
||||||
|
tracking per environment. Override authority = Infra & Ops + SRE joint
|
||||||
- Severity → score penalty mapping: critical → hard override to mandatory block, high → -0.2, medium → -0.05, low → -0.01, info → 0.0. One critical finding hard-overrides the score regardless of all other inputs.
|
sign-off; any override is itself a confidence-event in the audit stream.
|
||||||
|
|
||||||
🟡 OPEN (BA.B): Threshold tuning policy. The initial thresholds (dev 0.50, qa 0.75, prod 0.90, dr 0.95) are starting values. The tuning process, false-positive/false-negative tracking, and override authority are pending.
|
|
||||||
|
|
||||||
## 9. Cross-Cutting — Audit and Evidence Stream
|
## 9. Cross-Cutting — Audit and Evidence Stream
|
||||||
|
|
||||||
Locked commitments (unchanged from v0.1):
|
- Every delivery action produces an immutable, hash-chained evidence event.
|
||||||
|
- The audit stream is the platform's certified record of what happened, when,
|
||||||
- Tiered audit ledger: S3 with Object Lock in compliance mode (cold, source of truth, 7-year retention) + GitHub audit repo (hot, query index, not part of the chain).
|
and why.
|
||||||
|
- Events are written to a DynamoDB outbox and rendered on an evidence
|
||||||
- Daily checkpoints.
|
timeline.
|
||||||
|
|
||||||
- Event schema: JWS detached signature, prev_event_hash chain, controlled-vocabulary event_type.
|
## 10. Cross-Cutting — HITL Matrix
|
||||||
|
|
||||||
- Outbox pattern with local durable outbox + async worker.
|
Human-in-the-loop gates for higher environments:
|
||||||
|
|
||||||
- Linkage via workflow run ID or agent invocation ID.
|
| Environment | Autonomy | Attester | Gate |
|
||||||
|
|---|---|---|---|
|
||||||
Locked additions this revision:
|
| dev | Full autonomy (no HITL) | — | Confidence ≥ 0.50, all six inputs present |
|
||||||
|
| qa | Held for attestation | QA | Platform-runner deployment approval + full QA matrix |
|
||||||
- The outbox database is DynamoDB. RPO is zero (synchronous write to local outbox before contract submission ack); RTO is the async worker's recovery from the dead-letter queue. Single-region in v1; multi-region is a v2 concern.
|
| prod | Held for attestation | SRE | Platform-runner deployment approval + full SRE matrix |
|
||||||
|
| dr | Held for attestation | SRE | Platform-runner deployment approval + dr-drill evidence |
|
||||||
- The outbox also stores the per-contract QA and prod approver identities (Section 10). The platform-internal identity-distinctness check reads from this outbox. This is the only durable record of the approver identities outside GitHub's audit log.
|
|
||||||
|
Staging does not exist. Dev is the only autonomous environment and absorbs
|
||||||
🟡 OPEN (BA.C): On-call and operational ownership. The platform's on-call rotation, escalation paths when L3A or L3B halts unexpectedly, and the relationship to consumer on-call.
|
integration, contract, security smoke, and performance smoke validation.
|
||||||
|
|
||||||
## 10. Cross-Cutting — Human-in-the-Loop Mechanics
|
- Pre-execution gate model. 1 business day = warn + escalate; 2 business
|
||||||
|
days = auto-freeze + re-submit. Rejection extends the audit chain; no
|
||||||
Purpose. The human gates at higher environments. The vision's "Lower Environments are Autonomous; Higher Environments are Attested" tenet [1] and the "deliberate human attestation — not as a rubber stamp" requirement [1] are the binding constraints.
|
partial deploy to roll back.
|
||||||
|
- Separation of duties: the platform-internal identity record in the
|
||||||
### 10.1 Gate model
|
DynamoDB outbox enforces `qaApprover ≠ prodApprover` for the same contract.
|
||||||
|
|
||||||
Pre-execution gates. The contract is held in a "validated but not applied" state until the human attests. qa, prod, and dr are PR-based attestation gates backed by GitHub Environments with required reviewers.
|
## 11. Cross-Cutting — Separation of Duties
|
||||||
|
|
||||||
For qa and prod, there is no partial deployment to roll back on rejection. For dr, the same model — promotion to the DR environment is a separate GitHub Deployment, gated by SRE, against a separate cluster/region. The canary/deployment-rollback model is explicitly not in scope for v1.
|
- CODEOWNERS routes the right reviewer to the right environment.
|
||||||
|
- The DynamoDB outbox enforces identity distinctness across environment
|
||||||
### 10.2 Reviewer routing
|
approvers.
|
||||||
|
|
||||||
GitHub CODEOWNERS + GitHub Environment required reviewers. qa → QA team; prod → SRE team; dr → SRE team. CODEOWNERS is the routing layer; it does not enforce identity distinctness.
|
## 12. Cross-Cutting — Substrate Execution
|
||||||
|
|
||||||
### 10.3 Separation of duties — identity distinctness
|
The technical execution layer. Primitives and modules are substrate-agnostic
|
||||||
|
in shape; substrate adapters are the only substrate-specific component.
|
||||||
Mechanism is platform-internal, not GitHub-native, not Kyverno (in v1).
|
|
||||||
|
The architecture defines a **Target Stack** — a substrate-neutral
|
||||||
Sequence:
|
description of:
|
||||||
|
|
||||||
1. On promotion dev → qa, the platform reads the QA approver's GitHub identity from the GitHub Deployment approval event and writes it to the DynamoDB outbox keyed by contractId.
|
- The resources to create (typed against the stack schema).
|
||||||
|
- Their relationships (the module's pattern tree).
|
||||||
2. On promotion qa → prod, the platform reads the stored QA approver identity from the outbox and the new SRE approver identity from the GitHub Deployment approval event.
|
- Their inputs (wired from the contract).
|
||||||
|
- Policy hooks (the points in the pattern where policy checks attach).
|
||||||
3. If qaApprover == prodApprover, the platform blocks the prod promotion, writes a SEPARATION_OF_DUTIES_VIOLATION event to the evidence stream, and routes a halt artifact to the SRE on-call.
|
|
||||||
|
The registry, the module pattern tree, the contract schema, and the
|
||||||
4. The check is implemented in the central pipeline repo, not as an external policy. The platform is the only writer to the outbox; the check is in the same process that has authority to block the promotion.
|
`PolicyCheckResult` schema are all defined against the stack schema. None is
|
||||||
|
defined against any specific substrate.
|
||||||
### 10.4 Full HITL attestation matrix
|
|
||||||
|
**v1 implementation reality:** the stack is shaped to round-trip cleanly to
|
||||||
| Env | Concern | Evidence artifact | Freshness | Source | Attester |
|
Terraform because there is no other adapter to differentiate from. As
|
||||||
|---|---|---|---|---|---|
|
additional adapters appear, the stack gets more expressive and the adapters
|
||||||
| qa | Functional correctness | Last successful run of contract-declared validation.e2eSuite with pass rate ≥ 99% | Last 24h | Test runner declared in contract | QA |
|
gain translation logic, but the primitive content, the module pattern tree,
|
||||||
| qa | Performance baseline | Load test report (k6 / Gatling / Locust) showing p99 latency < declared NFR and throughput > declared minimum | Last 7d | Load test runner declared in contract | QA |
|
and the contract schema do not change. This is the design that prevents a
|
||||||
| qa | Security posture | Vulnerability scan (Trivy, Snyk, or contract-declared equivalent) with no criticals/highs, signed by Security on-call | Last 24h | Security scanner + Security team signature | QA |
|
polyglot mess.
|
||||||
| qa | Contract NFRs | Platform-generated report: schema valid, NFR assertions (latency, throughput, error rate) within declared bounds | At submission | Platform contract validator | QA |
|
|
||||||
| prod | Operational readiness | Runbook published, dashboard exists, on-call rotation assigned, alerts configured | At submission, validated against last 30d history | Platform + SRE | SRE |
|
The substrate adapter:
|
||||||
| prod | Incident response | Sev-1 runbook tabletop or live drill completed | Last 90d | SRE drill record | SRE |
|
|
||||||
| prod | Capacity / cost | FinOps forecast for next 30d within budget envelope, cost anomaly baseline stored, budget alert configured | Forecast valid for next 30d | FinOps + SRE | SRE |
|
- Translates the stack-typed module pattern tree to a substrate root module
|
||||||
| prod | Resilience | DR drill, chaos engineering report, backup verified | DR: 180d; chaos: 90d; backup: 30d | SRE + Platform | SRE |
|
that calls the primitive modules.
|
||||||
| dr | dr-region deploy with the most recent prod-bound dr drill as canary evidence | dr drill report | Last 180d | SRE | SRE |
|
- Is a thin layer. It does not own primitive/module content; it only
|
||||||
|
translates.
|
||||||
### 10.5 Timeout behavior
|
- Is the only substrate-specific code in the platform.
|
||||||
|
|
||||||
| Time | State | Action |
|
Policy checks run on the substrate plan output. Results are normalized to
|
||||||
|---|---|---|
|
`PolicyCheckResult` records by a policy adapter. The confidence signal
|
||||||
| Submission | PENDING_ATTESTATION | Notify responsible team |
|
consumes the union of all `PolicyCheckResult` records, regardless of engine
|
||||||
| 1 business day | PENDING_ATTESTATION_WARNING | Notify team + platform on-call (elevated path); emit PENDING_ATTESTATION_TIMEOUT_WARNING event |
|
— substrate-agnostic over its inputs, matching the module model's
|
||||||
| 2 business days | PENDING_ATTESTATION_AUTO_FREEZE | Auto-freeze; require re-submission; emit PENDING_ATTESTATION_AUTO_FREEZE event; new submission linked via supersedes |
|
substrate-agnosticism over its outputs.
|
||||||
|
|
||||||
### 10.6 Rejection and rollback
|
## 13. Cross-Cutting — Platform Runners
|
||||||
|
|
||||||
Rejection returns the contract to a HELD state with the rejection reason captured as a PROMOTION_REJECTED event. The consumer fixes the cause and re-submits; the new submission is linked to the rejected one via supersedes. The audit chain is extended, not torn up — matching the resolution session's answer.
|
The platform runs on platform-managed runners (GitHub Actions in
|
||||||
|
production). Runner-specific code = workflow YAML, OIDC trust, CODEOWNERS,
|
||||||
There is no partial deployment to roll back at any v1 gate.
|
environments. The contract schema, stack, `PolicyCheckResult`, confidence
|
||||||
|
signal, and audit stream are portable (runner-agnostic); a second runner
|
||||||
## 11. Cross-Cutting — Agentic Stack
|
platform needs a runner adapter + workflow-template translator, with no
|
||||||
|
change to the modules/stack/confidence/audit.
|
||||||
Locked commitments (unchanged from v0.1):
|
|
||||||
|
## 14. Versioning
|
||||||
- Hybrid runtime, platform-managed control plane + consumer-owned agent.
|
|
||||||
|
- Primitives and modules use semver: interface → MAJOR, behavior → MINOR,
|
||||||
- Versioned, signed skill catalog over MCP.
|
lifecycle → PATCH.
|
||||||
|
- A module pins primitives by `name@semver`; the resolver picks the highest
|
||||||
- Skill envelope enforced on invocation and result submission.
|
compatible.
|
||||||
|
- A MAJOR bump requires a new registry entry (immutable publication); the
|
||||||
- Consumer-owned skill execution environment. Platform does not run the skill.
|
old entry enters a 12-month deprecation window.
|
||||||
|
- The central deploy pipeline is referenced by a floating MAJOR + MINOR tag
|
||||||
- Stateless agents, all state in the platform.
|
(e.g. `@v1.4`); patch fixes flow within the tag, breaking changes land
|
||||||
|
under the next MINOR tag.
|
||||||
Locked additions this revision:
|
|
||||||
|
See [Versioning](pipeline/versioning) for the consumer-facing details.
|
||||||
- Skills are reviewed for sensitive data before release. Secrets, customer data, internal IPs, and other sensitive payloads are forbidden in skill markdown. The review is owned by Infra & Ops and is the mandatory release gate for any new skill. This is the trade-off for accepting the L3B runtime threat model (skill content is consumer-readable, so the platform must not put anything sensitive in it).
|
|
||||||
|
## 15. OpenTofu
|
||||||
🟡 OPEN (BA.A): Skill catalog. Initial skill set, addition process, deprecation process.
|
|
||||||
|
Not in v1. The substrate abstraction (§12) makes OpenTofu a future adapter,
|
||||||
## 12. Cross-Cutting — L1/L2 Substrate Execution
|
not an architecture change. Revisit when an OpenTofu adapter is requested.
|
||||||
|
|
||||||
Purpose. The technical execution layer for the L1/L2 substrate, including the substrate abstraction that protects v1 from polyglot mess while leaving v2+ room to grow.
|
|
||||||
|
|
||||||
### 12.1 Substrate abstraction (locked this revision)
|
|
||||||
|
|
||||||
L1/L2 are substrate-agnostic in shape. The architecture defines a Target Stack Intermediate Representation (IR) — a substrate-neutral description of:
|
|
||||||
|
|
||||||
- Resources with typed input contracts, typed output contracts, and declared NFRs.
|
|
||||||
|
|
||||||
- Relationships (single parent per child, with a shared keyword for multi-relationship dependencies).
|
|
||||||
|
|
||||||
- Composition (a tree of resources with max depth 5).
|
|
||||||
|
|
||||||
- Policy hooks (the points in the composition where policy checks attach).
|
|
||||||
|
|
||||||
The L1 registry, the L2 thin-composition tree, the YML standard, and the policy check result schema are all defined against the IR. None of them is defined against any specific substrate.
|
|
||||||
|
|
||||||
Substrate adapters are the only substrate-specific code. An adapter compiles the IR into a substrate execution plan. v1 ships exactly one adapter: the Terraform adapter. v2+ may add additional adapters (OpenTofu, Pulumi, K8s CRDs) without architectural change.
|
|
||||||
|
|
||||||
v1 implementation reality: the IR is shaped to round-trip cleanly to Terraform because there is no other adapter to differentiate from. The IR and the Terraform output are nearly isomorphic in v1. As additional adapters appear in v2+, the IR gets more expressive (e.g., substrate-specific output types) and the adapters gain translation logic, but the L1 module content, the YML standard, and the thin-composition tree do not change. This is the design that prevents the polyglot mess.
|
|
||||||
|
|
||||||
Why not build the abstraction earlier? Building a substrate-agnostic IR before there is a second adapter to test against is speculative generality. The v1 commitment is: (1) the L1 module interface is defined against the IR even though the only adapter is Terraform, and (2) the central pipeline, registry, and policy schema consume the IR-typed contracts. The adapter is the only place where substrate terminology appears in v1.
|
|
||||||
|
|
||||||
### 12.2 Terraform adapter (v1)
|
|
||||||
|
|
||||||
The Terraform adapter:
|
|
||||||
|
|
||||||
- Translates the IR-typed L1 module interface to a Terraform variable block and a Terraform output block.
|
|
||||||
|
|
||||||
- Translates the IR-typed L2 thin-composition tree to a Terraform root module that calls the L1 modules.
|
|
||||||
|
|
||||||
- Translates the IR-typed relationships to Terraform module references.
|
|
||||||
|
|
||||||
- Emits a Terraform plan from the IR.
|
|
||||||
|
|
||||||
The adapter is a thin layer. It does not own L1/L2 content; it only translates.
|
|
||||||
|
|
||||||
### 12.3 State storage
|
|
||||||
|
|
||||||
Locked: S3 (state files) + DynamoDB (state locking), cloud-managed. Single-region in v1.
|
|
||||||
|
|
||||||
### 12.4 Policy toolchain
|
|
||||||
|
|
||||||
Locked:
|
|
||||||
|
|
||||||
- Checkov for Terraform plan policy (the four L2 thin-composition checks: secrets-in-plaintext, public ingress, IAM wildcard, KMS key reference, plus tag and naming convention). Checkov is open-source, has a broad rule catalog, and is GitOps-friendly.
|
|
||||||
|
|
||||||
- Kyverno for K8s-native policy (platform-internal state in the GitOps reconciler, separation-of-dues-adjacent checks if any are added in v2, future CRD validation).
|
|
||||||
|
|
||||||
- OPA/Rego is reserved for cross-resource policy and is explicitly last resort due to Rego complexity.
|
|
||||||
|
|
||||||
### 12.5 Execution layer
|
|
||||||
|
|
||||||
Locked: GitHub Actions. terraform plan and terraform apply run in the central pipeline repo's GitHub Actions workflow. State locking via DynamoDB. AWS credentials via OIDC federation (long-lived credentials are forbidden). The platform does not run terraform apply against a developer's workstation; all execution is in the central pipeline.
|
|
||||||
|
|
||||||
### 12.6 Policy result normalization (locked this revision)
|
|
||||||
|
|
||||||
The confidence signal does not consume raw Checkov or Kyverno output. It consumes a normalized PolicyCheckResult schema produced by substrate-specific adapters.
|
|
||||||
|
|
||||||
Schema (canonical form, lives in the central pipeline repo):
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"contractId": "uuid",
|
|
||||||
"evaluatedAt": "ISO-8601",
|
|
||||||
"engine": "checkov | kyverno | opa",
|
|
||||||
"ruleId": "CKV_AWS_24 | KYVERNO_NO_PRIVILEGED | ...",
|
|
||||||
"severity": "critical | high | medium | low | info",
|
|
||||||
"result": "pass | fail | skipped | error",
|
|
||||||
"message": "human-readable",
|
|
||||||
"evidence": { "...engine-specific payload, opaque to the signal..." },
|
|
||||||
"resourceRef": "IR-typed resource identifier"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The Checkov adapter runs in the same GitHub Actions step as Checkov itself and translates Checkov JSON to PolicyCheckResult records. The Kyverno adapter runs as a controller in the platform's K8s cluster and translates Kyverno PolicyReport CRDs to PolicyCheckResult records. The confidence signal's policy input component is the union of all PolicyCheckResult records, regardless of engine. The signal does not know which engine produced which result — substrate-agnostic over its inputs, matching the L1/L2 model's substrate-agnostic over its outputs.
|
|
||||||
|
|
||||||
### 12.7 Registry maintenance
|
|
||||||
|
|
||||||
Locked: L1 module publication updates the L1 registry in the same PR as the module. Registry and module land together. The registry is the IR-typed contract, not a Terraform-specific variable schema. The L1 registry, the central pipeline, and the policy schema all consume the same IR-typed contract — there is one source of truth for the L1 interface, not multiple substrate-specific copies.
|
|
||||||
|
|
||||||
### 12.8 Contract-schema-to-IR resolution
|
|
||||||
|
|
||||||
The contract schema declares the consumer's intent in IR-typed terms. The central pipeline resolves the contract to a target stack (a list of L1 module instances with their inputs and the relationships between them). The Terraform adapter compiles the target stack to a Terraform execution plan. This resolution is substrate-agnostic — the target stack is in the IR.
|
|
||||||
|
|
||||||
🟡 OPEN (W3.D): L1/L2 standard versioning details, including pin model and evolution compatibility contract.
|
|
||||||
|
|
||||||
## 13. Consolidated Open Design Decisions
|
|
||||||
|
|
||||||
The following 11 decisions remain open. They are the gating items for v1.0.
|
|
||||||
|
|
||||||
### From Wave 1 (L1/L2 Substrate)
|
|
||||||
|
|
||||||
- (W1.A) AI-refinement trigger. Recommendation: joint condition — N ≥ 50 consecutive changes with zero rollbacks AND no L1/L2 incident in last 6 months AND Infra & Ops unilateral override. Pending sign-off.
|
|
||||||
|
|
||||||
- (W1.B) Multi-stack edge case rule. Recommendation: permitted only for (a) DR-region mirror, (b) time-boxed experimental stack with TTL ≤ 30d, (c) explicit Infra & Ops approval with documented justification in multiStack.justification. Pending sign-off.
|
|
||||||
|
|
||||||
### From Wave 2 (L3A/L3B)
|
|
||||||
|
|
||||||
- (W2.A) Tag mutability for production-bound references. Recommendation: Path B (tag for dev/qa, SHA for prod) with platform-provided CLI to resolve tag → SHA. Pending sign-off.
|
|
||||||
|
|
||||||
### From Wave 3 (Technical Execution)
|
|
||||||
|
|
||||||
- (W3.D) L1/L2 standard versioning details. Semver scheme, pin model, evolution compatibility contract.
|
|
||||||
|
|
||||||
- (W3.E) Schema mandatory vs. optional inputs. Per-field mandatory/optional declarations per environment.
|
|
||||||
|
|
||||||
### From Beyond Architecture
|
|
||||||
|
|
||||||
- (BA.A) Skill catalog. Initial L3B skill set, addition process, deprecation process.
|
|
||||||
|
|
||||||
- (BA.B) Confidence signal threshold tuning. Initial thresholds are starting values; tuning process, FP/FN tracking, override authority.
|
|
||||||
|
|
||||||
- (BA.C) On-call and operational ownership. Platform on-call rotation, escalation paths, relationship to consumer on-call.
|
|
||||||
|
|
||||||
- (BA.D) Cost and capacity governance. Cloud cost ownership, consumption reporting, runaway spend detection and halting.
|
|
||||||
|
|
||||||
- (BA.E) Consumer onboarding. Developer and citizen developer onboarding flow, "getting started" path through the contract schema.
|
|
||||||
|
|
||||||
- (BA.F) Cross-platform evolution. What changes if a second source-control system (e.g., GitLab) is added; which architectural decisions are portable.
|
|
||||||
|
|
||||||
## 14. Document Status and Next Steps
|
|
||||||
|
|
||||||
Status: v0.2. Eight of the original 15 open items are locked. Eleven remain open. The architecture is internally consistent for the locked items; resolution of the open items is the path to v1.0.
|
|
||||||
|
|
||||||
Doc-sync items (out of scope of this document but flagged for the same change set):
|
|
||||||
|
|
||||||
- The CDLC reference document's environment model assumes staging exists. Path A invalidates that. The CDLC contract example's targetEnvironments: [staging, production] must be revised to [dev, qa, prod, dr].
|
|
||||||
|
|
||||||
To finalize to v1.0:
|
|
||||||
|
|
||||||
1. Resolve the 11 open items in Section 13.
|
|
||||||
|
|
||||||
2. Validate the locked substrate abstraction against a real v1 implementation spike (one L1 module, one L2 thin-composition, one Terraform adapter, one contract submission end-to-end). The spike validates that the IR-shaped commitments do not require a polyglot mess.
|
|
||||||
|
|
||||||
3. Validate the locked HITL matrix against a tabletop exercise with QA and SRE.
|
|
||||||
|
|
||||||
4. Sign-off pass.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Final Asks — three remaining open questions, then sign-off
|
|
||||||
|
|
||||||
I have three open questions that gate v1.0. Resolve them and I will revise the architecture document to v1.0 and mark it ready for implementation.
|
|
||||||
|
|
||||||
Q1. W1.A + W1.B — AI-refinement trigger and multi-stack edge case rule. The recommendations are in the document. Do you accept them as committed, or do you want to amend?
|
|
||||||
|
|
||||||
Q2. W2.A — Tag mutability for production-bound references. Path A (tag throughout with protection) vs. Path B (tag for dev/qa, SHA for prod). My recommendation is Path B with a platform CLI to resolve tag → SHA. Accept or amend?
|
|
||||||
|
|
||||||
Q3. BA.A — Initial L3B skill catalog. The demo plan uses 3 stub skills deploy-web-api, add-observability, add-basic-auth). For v1.0, the real platform needs a defensible initial skill set. My recommendation: start with the 5 most common infrastructure intents (web API, worker, scheduled job, static asset, basic observability bootstrap) and grow from there. The criteria for addition: a skill must (a) be reviewable for sensitive data per the locked skill-review gate, (b) be expressible as a single contract submission, and (c) have a documented use case. Accept or amend?
|
|
||||||
|
|
||||||
Once these three are resolved, plus the 8 remaining items (W3.D, W3.E, BA.B, BA.C, BA.D, BA.E, BA.F, and the OpenTofu timing sub-decision), the architecture moves to v1.0.
|
|
||||||
|
|
||||||
Sign-off request. Are you ready for me to draft v1.0 once these are resolved, or do you want to amend the v0.2 above first?
|
|
||||||
@@ -0,0 +1,313 @@
|
|||||||
|
# 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-asset` is the worked example, but every
|
||||||
|
step applies to `microservice` and any future module.
|
||||||
|
|
||||||
|
## The model
|
||||||
|
|
||||||
|
Consumers have their own repos and consume ACDL by referencing `uses:` the
|
||||||
|
central pipeline definitions. The consumer declares a **contract** (which
|
||||||
|
module, which environment, which inputs); the ACDL platform owns the
|
||||||
|
pipelines, modules, substrate adapter, and evidence stream.
|
||||||
|
|
||||||
|
You do not write infrastructure modules, workflow YAML, or adapter code.
|
||||||
|
You write a contract YAML file and the platform does the rest. Your
|
||||||
|
repository contains only your application code, your contracts, and your CI
|
||||||
|
definitions.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
A["your repo<br/>(app code + contracts + CI definitions)"] -->|uses: acdl/.github/workflows/deploy.yml@v1.4| 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.4`). 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.4`.
|
||||||
|
Contact the platform team if you have not been onboarded.
|
||||||
|
|
||||||
|
## Step 1 — Create a consumer repo
|
||||||
|
|
||||||
|
Create a repository for your application. The top level holds your app
|
||||||
|
code; your contract lives at `.acdl/contract.yaml`. Example for a static
|
||||||
|
site:
|
||||||
|
|
||||||
|
```
|
||||||
|
my-static-site/
|
||||||
|
index.html
|
||||||
|
assets/
|
||||||
|
style.css
|
||||||
|
logo.png
|
||||||
|
.acdl/
|
||||||
|
contract.yaml
|
||||||
|
.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.4
|
||||||
|
```
|
||||||
|
|
||||||
|
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-asset` example:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
uses: acdl/pipelines/deploy.yaml@v1.4
|
||||||
|
module: static-asset
|
||||||
|
environment: dev
|
||||||
|
inputs:
|
||||||
|
bucket_name: my-static-site-assets
|
||||||
|
region: us-east-1
|
||||||
|
```
|
||||||
|
|
||||||
|
A `microservice` example:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
uses: acdl/pipelines/deploy.yaml@v1.4
|
||||||
|
module: microservice
|
||||||
|
environment: dev
|
||||||
|
inputs:
|
||||||
|
image: my-registry/my-microservice:latest
|
||||||
|
port: 8080
|
||||||
|
env:
|
||||||
|
LOG_LEVEL: info
|
||||||
|
```
|
||||||
|
|
||||||
|
### Contract fields
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|-------|------|----------|-------------|
|
||||||
|
| `uses` | string | yes | Reference to the central deployment pipeline, **versioned** with a floating MAJOR+MINOR tag (e.g. `acdl/pipelines/deploy.yaml@v1.4`). Bare or `@main` references are discouraged. See [Versioning](pipeline/versioning). |
|
||||||
|
| `module` | string | yes | Module name from the registry — any primitive or module (e.g. `static-asset`, `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>/`.
|
||||||
|
|
||||||
|
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.4
|
||||||
|
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.4`
|
||||||
|
to the reusable workflow **at the pinned tag**.
|
||||||
|
2. A **platform-provided runner** checks out **your** repo.
|
||||||
|
3. The runner checks out the **ACDL platform repo** into the workspace —
|
||||||
|
this is how the pipeline fetches the platform code at run time. You
|
||||||
|
never clone the platform repo yourself.
|
||||||
|
4. The runner installs the runtime dependencies the platform requires.
|
||||||
|
5. The runner invokes `scripts/run_platform.sh` against your
|
||||||
|
`.acdl/contract.yaml`.
|
||||||
|
|
||||||
|
You see the streamed output (infrastructure plan, policy-check results,
|
||||||
|
confidence signal) in your run logs. The `--check-only` and `--plan-only`
|
||||||
|
flags are platform-side modes visible in the pipeline logs; you do not pass
|
||||||
|
them yourself — the reusable workflow selects the mode based on the
|
||||||
|
`environment` in your contract (`dev` = full apply; higher environments
|
||||||
|
hold for attestation).
|
||||||
|
|
||||||
|
### Local validation (optional)
|
||||||
|
|
||||||
|
A consumer *may* clone the ACDL platform repo to run `--check-only` against
|
||||||
|
their contract before pushing — this is optional and not required for the
|
||||||
|
happy path. If you do this, the runtime dependencies must be installed
|
||||||
|
locally, and any AWS credentials follow the
|
||||||
|
[Credentials](../README.md#credentials--zero-trust) override model: a
|
||||||
|
static key in `.env.secrets` (gitignored) is rotated **out of band by you**
|
||||||
|
— the platform guarantees daily rotation for platform-runner runs, not for
|
||||||
|
locally-held copies.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scripts/run_platform.sh --check-only path/to/your/.acdl/contract.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 5 — What the pipeline does
|
||||||
|
|
||||||
|
Each stage of the central deployment pipeline:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
S1["validate-contract<br/>schema check"] --> S2
|
||||||
|
S2["resolve-stack<br/>contract -> Target Stack"] --> S3
|
||||||
|
S3["security checks<br/>(adapter)"] --> S4
|
||||||
|
S4["infrastructure plan<br/>(adapter compiles the stack)"] --> S5
|
||||||
|
S5["policy checks<br/>(adapter -> PolicyCheckResult)"] --> S6
|
||||||
|
S6["confidence<br/>score + band (dev >= 0.50)"] --> S7
|
||||||
|
S7["evidence event<br/>to the audit outbox"] --> S8
|
||||||
|
S8["infrastructure apply<br/>(dev only)"]
|
||||||
|
```
|
||||||
|
|
||||||
|
1. **validate-contract** — validates your contract YAML against the contract
|
||||||
|
schema. Fails fast on missing fields, unknown modules, or wrong types.
|
||||||
|
2. **resolve-stack** — the contract resolver resolves your contract to a
|
||||||
|
Target Stack instance. It loads the module's pattern, expands its
|
||||||
|
children, wires your contract inputs to the children's inputs, and emits
|
||||||
|
a stack JSON instance.
|
||||||
|
3. **security checks** (adapter) — security checks run on the resolved
|
||||||
|
stack before any infrastructure is planned.
|
||||||
|
4. **infrastructure plan** (adapter) — the substrate adapter compiles the
|
||||||
|
stack to an infrastructure plan. You see the plan in your run logs.
|
||||||
|
5. **policy checks** (adapter) — policy checks run on the plan. The results
|
||||||
|
are normalized to `PolicyCheckResult` records. Each result has a
|
||||||
|
severity, rule ID, and pass/fail status.
|
||||||
|
6. **confidence** — the confidence signal computes a score from 6 inputs
|
||||||
|
(policy, validation, freshness, source, history, NFRs). For `dev`, the
|
||||||
|
threshold is ≥ 0.50. If the band is `pass`, the pipeline proceeds.
|
||||||
|
7. **evidence event** — a hash-chained evidence event is written to the
|
||||||
|
audit outbox.
|
||||||
|
8. **infrastructure apply** (dev only) — the infrastructure plan is applied,
|
||||||
|
creating the resources in your AWS account. An evidence event for the
|
||||||
|
apply is recorded.
|
||||||
|
|
||||||
|
## Step 6 — What gets created
|
||||||
|
|
||||||
|
After a successful `dev` run, the resources declared by your module's
|
||||||
|
pattern exist in your AWS account, and an evidence event is recorded.
|
||||||
|
|
||||||
|
For the `static-asset` example:
|
||||||
|
|
||||||
|
- **An S3 bucket** named `my-static-site-assets` in `us-east-1` with
|
||||||
|
versioning enabled.
|
||||||
|
- **An evidence event** in the audit outbox with the contract ID, stack
|
||||||
|
name (`static-asset`), confidence score, and band.
|
||||||
|
- **A confidence band** of `pass` (score ≥ 0.50 for dev).
|
||||||
|
|
||||||
|
For other modules, consult the module's README
|
||||||
|
(`modules/l1/<name>/README.md` or `modules/l2/<name>/README.md`) for the
|
||||||
|
exact resources created.
|
||||||
|
|
||||||
|
## Step 7 — Upload your content (static-asset example)
|
||||||
|
|
||||||
|
The platform provisions the infrastructure; you upload your content. For
|
||||||
|
the `static-asset` module:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
aws s3 sync ./assets s3://my-static-site-assets/ --acl public-read
|
||||||
|
```
|
||||||
|
|
||||||
|
For a `microservice`, the platform provisions the ECS service and ALB; you
|
||||||
|
push your container image to the ECR repo the platform created.
|
||||||
|
|
||||||
|
## Step 8 — Promote to qa / prod
|
||||||
|
|
||||||
|
Change `environment` in your contract (keeping the same versioned `uses:`):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
uses: acdl/pipelines/deploy.yaml@v1.4
|
||||||
|
environment: qa # QA attestation + confidence >= 0.75
|
||||||
|
```
|
||||||
|
|
||||||
|
Higher environments require human attestation (a platform-runner deployment
|
||||||
|
approval) and higher confidence thresholds. See [Environments](environments/)
|
||||||
|
for the full table.
|
||||||
|
|
||||||
|
## Step 9 — Compliance extensions
|
||||||
|
|
||||||
|
Each module lists compliance extension points for the future compliance
|
||||||
|
milestone (GDPR, SOX, SOC2, HIPAA, DORA). See each module's README under
|
||||||
|
`modules/l1/<name>/README.md` or `modules/l2/<name>/README.md` for the
|
||||||
|
per-module extension points. Common examples:
|
||||||
|
|
||||||
|
- **KMS key** — shared encryption key for SSE.
|
||||||
|
- **S3 access logs** — access logging to a separate audit bucket.
|
||||||
|
- **Object Lock** — 7-year immutable retention for evidence.
|
||||||
|
- **Public access block** — prevent data exfiltration.
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
| Resource | Path | Description |
|
||||||
|
|----------|------|-------------|
|
||||||
|
| Central deployment pipeline contract | `pipelines/deploy.yaml` | The pipeline stages your contract references. |
|
||||||
|
| Reusable deploy workflow | `.github/workflows/deploy.yml` | The workflow your repo invokes via `uses:`. |
|
||||||
|
| Contract schema | `schemas/contract.schema.json` | JSON Schema for consumer contracts. |
|
||||||
|
| Stack schema | `schemas/stack.schema.json` | JSON Schema for the resolved stack instance. |
|
||||||
|
| Module catalog | [modules/](modules/) | All primitives and modules. |
|
||||||
|
| Sample contract | `contracts/static-asset.yaml` | The reference example contract (uses `@v1.4`). |
|
||||||
|
| Contract resolver | `core/contract_resolver.py` | Resolves contracts to stack instances. |
|
||||||
|
| Substrate adapter | `adapters/terraform/adapter.py` | Compiles stack instances to infrastructure. |
|
||||||
|
| Platform pipeline runner | `scripts/run_platform.sh` | The pipeline runner (platform-side; consumers do not invoke it directly). |
|
||||||
|
| Environments | [environments/](environments/) | Platform-managed environments + onboarding. |
|
||||||
|
| Versioning | [pipeline/versioning](pipeline/versioning) | The `uses:` tag + module versioning. |
|
||||||
|
| Platform README | `README.md` | How the platform works + how to run the platform repo locally. |
|
||||||
|
| Credentials & zero-trust | `README.md#credentials--zero-trust` | The OIDC/ABAC default + static-key override model. |
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# 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-asset` module):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
uses: acdl/pipelines/deploy.yaml@v1.4
|
||||||
|
module: static-asset
|
||||||
|
environment: dev
|
||||||
|
inputs:
|
||||||
|
bucket_name: my-static-site-assets
|
||||||
|
region: us-east-1
|
||||||
|
```
|
||||||
|
|
||||||
|
A `microservice` example:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
uses: acdl/pipelines/deploy.yaml@v1.4
|
||||||
|
module: microservice
|
||||||
|
environment: dev
|
||||||
|
inputs:
|
||||||
|
image: my-registry/my-microservice:latest
|
||||||
|
port: 8080
|
||||||
|
env:
|
||||||
|
LOG_LEVEL: info
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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.4`). 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-asset`, `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 contract
|
||||||
|
|
||||||
|
The reference example is
|
||||||
|
[`contracts/static-asset.yaml`](https://github.com/acdl/acdl/blob/main/contracts/static-asset.yaml),
|
||||||
|
which uses `@v1.4` as the canonical versioned `uses:` reference.
|
||||||
|
|
||||||
|
## 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,69 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
## Onboarding scaffold (current state)
|
||||||
|
|
||||||
|
The platform repo ships a minimal onboarding scaffold:
|
||||||
|
|
||||||
|
- [`core/environments/`](https://github.com/acdl/acdl/blob/main/core/environments/)
|
||||||
|
— environment definitions (a sample `dev.json`).
|
||||||
|
- `core/environment_check.py` — checks whether an environment is defined for
|
||||||
|
a given contract's repo + environment name; prints the friendly onboarding
|
||||||
|
prompt when none is defined.
|
||||||
|
- `scripts/run_platform.sh` calls the check before contract validation.
|
||||||
|
|
||||||
|
The scaffold is minimal: the actual provisioning of a new environment is a
|
||||||
|
platform-team action today. Self-service environment provisioning is on the
|
||||||
|
[roadmap](../).
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
# ACDL — Agentic Cloud Delivery Platform
|
||||||
|
|
||||||
|
Consumers declare intent; the platform delivers safe production deployment
|
||||||
|
through an agentic stack — automatically, safely, and with a complete audit
|
||||||
|
trail. A merged change progresses through lower environments end-to-end
|
||||||
|
without a platform engineer joining a thread; a non-technical consumer ships
|
||||||
|
a production deployment by declaring intent, without authoring a workflow,
|
||||||
|
a configuration file, or an infrastructure module.
|
||||||
|
|
||||||
|
## Two repositories
|
||||||
|
|
||||||
|
There are two kinds of repository in the ACDL model:
|
||||||
|
|
||||||
|
- **Platform repo (this one).** The source code of the platform. It owns
|
||||||
|
`modules/`, `adapters/`, `core/`, `schemas/`, `pipelines/`, `scripts/`,
|
||||||
|
and the reusable workflow files. Platform engineers work here. A consumer
|
||||||
|
never clones it.
|
||||||
|
- **Consumer repo (yours).** A consumer repo contains only its application
|
||||||
|
code, one or more contracts (`.acdl/contract.yaml`), and one or more CI
|
||||||
|
definitions (a thin `.github/workflows/deploy.yml` that `uses:` the central
|
||||||
|
reusable workflow, pointing at the appropriate environment + contract).
|
||||||
|
The consumer does not write infrastructure modules, workflow YAML, or
|
||||||
|
adapter code.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
| Section | Audience | What it covers |
|
||||||
|
|---------|----------|----------------|
|
||||||
|
| [Consumer Guide](consumer-guide) | Consumers | Step-by-step: create a repo, write a contract, reference the central pipeline, ship a deployment. |
|
||||||
|
| [Modules](modules/) | Consumers + platform engineers | The module catalog — primitives and modules, their inputs/outputs, and usage. |
|
||||||
|
| [Contracts](contracts/) | Consumers | The contract schema, fields, and a worked sample. |
|
||||||
|
| [Pipeline](pipeline/) | Consumers + platform engineers | The central CI + deployment pipeline and its stages. |
|
||||||
|
| [Versioning](pipeline/versioning) | Consumers + platform engineers | Module versioning + deploy-pipeline versioning (the `uses:` tag). |
|
||||||
|
| [Environments](environments/) | Consumers | Platform-managed environments and the first-run onboarding flow. |
|
||||||
|
| [Architecture](architecture) | Platform engineers | The current architecture — layers, cross-cutting concerns, the substrate abstraction. |
|
||||||
|
| [Vision](vision) | All | The why — the friction the platform absorbs and the north star. |
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Contract-driven deploys** — a consumer writes a YAML contract; the
|
||||||
|
platform resolves it to a stack, compiles it, and deploys it.
|
||||||
|
- **Reusable versioned deploy workflow** — consumer repos `uses:` a
|
||||||
|
versioned central workflow; no platform code is cloned by the consumer.
|
||||||
|
- **Module catalog** — primitives (single resources) and modules (patterns
|
||||||
|
of primitives) with self-documented inputs/outputs.
|
||||||
|
- **Zero-trust credentials** — OIDC federation + attribute-based
|
||||||
|
authorization (ABAC) by default; no long-lived keys in consumer repos.
|
||||||
|
- **Security + policy checks** — a security-check stage and a policy-check
|
||||||
|
stage run before any infrastructure is created.
|
||||||
|
- **Confidence signal** — a computed, explainable score gates promotion.
|
||||||
|
- **Evidence outbox** — every deployment writes a hash-chained evidence
|
||||||
|
event to an audit outbox.
|
||||||
|
- **Shell reproducibility** — `scripts/run_ci.sh` mirrors the CI pipeline
|
||||||
|
locally; `scripts/run_platform.sh --check-only` runs offline.
|
||||||
|
- **Platform-managed environments** — consumers provide no AWS account,
|
||||||
|
VPC, subnet, or state bucket; the platform manages environments.
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
|
||||||
|
Planned future features (no dates; tracked in the internal roadmap):
|
||||||
|
|
||||||
|
- **Dynamic module creation from a contract** — an agentic flow where a
|
||||||
|
consumer creates a module directly from the contract file (the "composition"
|
||||||
|
mechanism, redesigned).
|
||||||
|
- **Compliance milestone** — per-module compliance extension points (GDPR,
|
||||||
|
SOX, SOC2, HIPAA, DORA) wired into the pipeline.
|
||||||
|
- **Additional substrate adapters** — beyond the Terraform adapter.
|
||||||
|
- **Environment self-service** — a consumer-facing flow to request and
|
||||||
|
provision a new platform-managed environment.
|
||||||
|
- **HITL gates for qa / prod / dr** — human attestation + higher confidence
|
||||||
|
thresholds for higher environments.
|
||||||
|
- **OIDC for all platform runners** — zero-trust credentials everywhere.
|
||||||
|
|
||||||
|
## Quick links
|
||||||
|
|
||||||
|
- [Consumer Guide](consumer-guide) — start here if you are a consumer.
|
||||||
|
- [Architecture](architecture) — start here if you are a platform engineer.
|
||||||
|
- The [README](https://github.com/acdl/acdl) describes the platform repo.
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# Modules
|
||||||
|
|
||||||
|
Reusable building blocks for cloud infrastructure. There are two kinds:
|
||||||
|
|
||||||
|
- **Primitives** — a single cloud resource or a small group of related
|
||||||
|
resources (e.g. a VPC with subnets and routing). Each primitive has an
|
||||||
|
`interface.json` declaring its inputs and outputs.
|
||||||
|
- **Modules** — a pattern that references multiple primitives to deploy a
|
||||||
|
complete stack (e.g. an ECS Fargate microservice). Each module has a
|
||||||
|
`composition.json` declaring its children and wires.
|
||||||
|
|
||||||
|
The substrate adapter compiles a module instance to infrastructure. Each
|
||||||
|
module's README documents which resources it creates.
|
||||||
|
|
||||||
|
## Primitives
|
||||||
|
|
||||||
|
| Module | What it creates | Source |
|
||||||
|
|--------|----------------|--------|
|
||||||
|
| `s3` | `aws_s3_bucket` — a single S3 bucket | [modules/l1/s3/README.md](https://github.com/acdl/acdl/blob/main/modules/l1/s3/README.md) |
|
||||||
|
| `vpc` | `aws_vpc` + `aws_subnet` + `aws_route_table` + `aws_internet_gateway` — VPC with subnets and routing | [modules/l1/vpc/README.md](https://github.com/acdl/acdl/blob/main/modules/l1/vpc/README.md) |
|
||||||
|
| `ecs-cluster` | `aws_ecs_cluster` — ECS Fargate cluster | [modules/l1/ecs-cluster/README.md](https://github.com/acdl/acdl/blob/main/modules/l1/ecs-cluster/README.md) |
|
||||||
|
| `ecs-service` | `aws_ecs_task_definition` + `aws_ecs_service` — Fargate service with task definition | [modules/l1/ecs-service/README.md](https://github.com/acdl/acdl/blob/main/modules/l1/ecs-service/README.md) |
|
||||||
|
| `iam-role` | `aws_iam_role` — IAM role with assume-role policy | [modules/l1/iam-role/README.md](https://github.com/acdl/acdl/blob/main/modules/l1/iam-role/README.md) |
|
||||||
|
| `alb` | `aws_lb` + `aws_lb_target_group` + `aws_lb_listener` — Application Load Balancer | [modules/l1/alb/README.md](https://github.com/acdl/acdl/blob/main/modules/l1/alb/README.md) |
|
||||||
|
| `ecr` | `aws_ecr_repository` — ECR container image repository | [modules/l1/ecr/README.md](https://github.com/acdl/acdl/blob/main/modules/l1/ecr/README.md) |
|
||||||
|
|
||||||
|
## Modules
|
||||||
|
|
||||||
|
| Module | What it references | Source |
|
||||||
|
|--------|--------------------|--------|
|
||||||
|
| `static-asset` | 1 primitive (s3) — a static-asset S3 bucket | [modules/l2/static-asset/README.md](https://github.com/acdl/acdl/blob/main/modules/l2/static-asset/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.
|
||||||
|
|
||||||
|
## 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.4`).
|
||||||
|
The workflow checks out the consumer repo, then checks out the ACDL platform
|
||||||
|
repo into the runner workspace, and runs `scripts/run_platform.sh` against
|
||||||
|
the consumer's contract. The consumer never clones the platform repo or
|
||||||
|
invokes its scripts locally. See the [Consumer Guide](../consumer-guide/)
|
||||||
|
for the end-to-end happy path.
|
||||||
|
|
||||||
|
## Deployment stages
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
S1["validate-contract<br/>schema check"] --> S2
|
||||||
|
S2["resolve-stack<br/>contract -> Target Stack"] --> S3
|
||||||
|
S3["security checks<br/>(adapter)"] --> S4
|
||||||
|
S4["infrastructure plan<br/>(adapter compiles the stack)"] --> S5
|
||||||
|
S5["policy checks<br/>(adapter -> PolicyCheckResult)"] --> S6
|
||||||
|
S6["confidence<br/>score + band"] --> S7
|
||||||
|
S7["evidence event<br/>to the audit outbox"] --> S8
|
||||||
|
S8["infrastructure apply<br/>(dev only)"]
|
||||||
|
```
|
||||||
|
|
||||||
|
1. **validate-contract** — validates the contract YAML against the contract
|
||||||
|
schema. Fails fast on missing fields, unknown modules, or wrong types.
|
||||||
|
2. **resolve-stack** — the contract resolver resolves the contract to a
|
||||||
|
Target Stack instance (loads the module's pattern, expands its children,
|
||||||
|
wires the contract inputs, emits a stack JSON instance).
|
||||||
|
3. **security checks** (adapter) — security checks run on the resolved
|
||||||
|
stack before any infrastructure is planned.
|
||||||
|
4. **infrastructure plan** (adapter) — the substrate adapter compiles the
|
||||||
|
stack to an infrastructure plan.
|
||||||
|
5. **policy checks** (adapter) — policy checks run on the plan. Results are
|
||||||
|
normalized to `PolicyCheckResult` records (severity, rule ID, pass/fail).
|
||||||
|
6. **confidence** — the confidence signal computes a score from 6 inputs
|
||||||
|
(policy, validation, freshness, source, history, NFRs). For `dev`, the
|
||||||
|
threshold is ≥ 0.50. If the band is `pass`, the pipeline proceeds.
|
||||||
|
7. **evidence event** — a hash-chained evidence event is written to the
|
||||||
|
audit outbox.
|
||||||
|
8. **infrastructure apply** (dev only) — the infrastructure plan is applied,
|
||||||
|
creating the resources. An evidence event for the apply is recorded.
|
||||||
|
|
||||||
|
Higher environments hold for human attestation (see
|
||||||
|
[Environments](../environments/)).
|
||||||
|
|
||||||
|
## Output streaming
|
||||||
|
|
||||||
|
`scripts/run_platform.sh` streams output by default so the user can see what
|
||||||
|
the platform is doing:
|
||||||
|
|
||||||
|
- **`--check-only`**: streams the emitted infrastructure file content.
|
||||||
|
- **`--plan-only`** and **full mode**: streams the infrastructure plan output.
|
||||||
|
- **Full mode**: prints policy-check results with severity, rule ID, and
|
||||||
|
pass/fail status.
|
||||||
|
|
||||||
|
A `--quiet` flag suppresses streaming (output to log files only).
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# Versioning
|
||||||
|
|
||||||
|
ACDL uses two versioning schemes: one for modules, one for the deploy
|
||||||
|
pipeline. Both matter to a consumer.
|
||||||
|
|
||||||
|
## Module versioning
|
||||||
|
|
||||||
|
Primitives and modules use **semver** with three triggers:
|
||||||
|
|
||||||
|
- **interface → MAJOR** — a breaking change to the module's inputs/outputs.
|
||||||
|
- **behavior → MINOR** — a backward-compatible behavior change.
|
||||||
|
- **lifecycle → PATCH** — a fix or internal change.
|
||||||
|
|
||||||
|
A MAJOR bump requires a **new registry entry** (immutable publication); the
|
||||||
|
old entry enters a **12-month deprecation window**. A module pins its
|
||||||
|
primitives by `name@semver`; the resolver picks the highest compatible.
|
||||||
|
|
||||||
|
Module versions are tracked in
|
||||||
|
[`registry.json`](https://github.com/acdl/acdl/blob/main/modules/registry.json).
|
||||||
|
|
||||||
|
## Deploy-pipeline versioning (the `uses:` tag)
|
||||||
|
|
||||||
|
The central deploy pipeline is referenced by a **floating MAJOR + MINOR
|
||||||
|
tag** in a consumer's contract and CI definition:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
uses: acdl/pipelines/deploy.yaml@v1.4
|
||||||
|
```
|
||||||
|
|
||||||
|
Version constraints cannot be expressed inside the contract, so the tag in
|
||||||
|
`uses:` is the only immutability lever a consumer has.
|
||||||
|
|
||||||
|
**Unversioned references are discouraged.** Do not use `@main` or a bare
|
||||||
|
`acdl/pipelines/deploy.yaml` — `main` is constantly updated and can cause
|
||||||
|
unexpected failures. Pinning to a MAJOR+MINOR tag means:
|
||||||
|
|
||||||
|
- **Immutability** — the pipeline behavior you tested is the behavior you
|
||||||
|
get. Patch fixes flow within the tag; breaking changes land under the
|
||||||
|
next MINOR tag (`@v1.5`), which you opt into explicitly.
|
||||||
|
- **Resilience** — your deployment does not break because an unrelated
|
||||||
|
change landed on `main`.
|
||||||
|
- **Reproducibility** — your setup is stable. You upgrade on your schedule
|
||||||
|
by bumping the tag.
|
||||||
|
|
||||||
|
## When a new tag is released
|
||||||
|
|
||||||
|
When a new MINOR tag is released (e.g. `@v1.5`), review its changelog and
|
||||||
|
bump your `uses:` reference when ready. The old tag continues to receive
|
||||||
|
patch fixes until the next MINOR tag.
|
||||||
|
|
||||||
|
## Production-bound references
|
||||||
|
|
||||||
|
For production-bound workflows, the platform resolves the current tag to its
|
||||||
|
SHA (tag for dev/qa, SHA for prod). This prevents a silent patch from
|
||||||
|
changing a production deployment. The platform provides a CLI command for
|
||||||
|
the tag → SHA resolution.
|
||||||
+1
-1
@@ -68,4 +68,4 @@ The vision is realized when:
|
|||||||
## What this vision is, and what it isn't
|
## What this vision is, and what it isn't
|
||||||
|
|
||||||
* **It is:** A principles document. The North Star, the tenets, the strategic bets, the anti-goals. It's intended to be the page that orients a new team, a new stakeholder, or a new architectural decision. It should not need to be rewritten when a tool changes.
|
* **It is:** A principles document. The North Star, the tenets, the strategic bets, the anti-goals. It's intended to be the page that orients a new team, a new stakeholder, or a new architectural decision. It should not need to be rewritten when a tool changes.
|
||||||
* **It isn't:** An architecture. The four-layer model (L1 Terraform primitives, L2 composed stacks, L3A developer surface, L3B agentic surface), the central pipeline template model, the schema location, the dual HITL mechanics, the enterprise evidence stream integration — all of that belongs in the architecture document, where it can be specific and evolve independently.
|
* **It isn't:** An architecture. The four-layer model (primitives, modules, developer surface, agentic surface), the central pipeline template model, the schema location, the dual HITL mechanics, the enterprise evidence stream integration — all of that belongs in the architecture document, where it can be specific and evolve independently.
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
# 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:
|
|
||||||
|
|
||||||
- **L1 primitives** — a single cloud resource or a small group of
|
|
||||||
related resources (e.g. a VPC with subnets and routing). Each L1 has
|
|
||||||
an `interface.json` declaring its inputs and outputs, and a `README.md`
|
|
||||||
in plain language.
|
|
||||||
- **L2 compositions** — a composition that references multiple L1s to
|
|
||||||
deploy a complete stack (e.g. an ECS Fargate microservice). **The L2
|
|
||||||
composition layer is being redesigned.** The previous implementation
|
|
||||||
has been removed; a new mechanism will be designed in a later phase.
|
|
||||||
|
|
||||||
The Terraform adapter (`adapters/terraform/adapter.py`) compiles a
|
|
||||||
module instance to Terraform. Each module's README documents which
|
|
||||||
Terraform resources it creates.
|
|
||||||
|
|
||||||
## L1 primitives
|
|
||||||
|
|
||||||
| Module | What it creates | README |
|
|
||||||
|--------|----------------|--------|
|
|
||||||
| `l1-s3` | `aws_s3_bucket` — a single S3 bucket | [README](l1/l1-s3/README.md) |
|
|
||||||
| `l1-vpc` | `aws_vpc` + `aws_subnet` + `aws_route_table` + `aws_internet_gateway` — VPC with subnets and routing | [README](l1/l1-vpc/README.md) |
|
|
||||||
| `l1-ecs-cluster` | `aws_ecs_cluster` — ECS Fargate cluster | [README](l1/l1-ecs-cluster/README.md) |
|
|
||||||
| `l1-ecs-service` | `aws_ecs_task_definition` + `aws_ecs_service` — Fargate service with task definition | [README](l1/l1-ecs-service/README.md) |
|
|
||||||
| `l1-iam-role` | `aws_iam_role` — IAM role with assume-role policy | [README](l1/l1-iam-role/README.md) |
|
|
||||||
| `l1-alb` | `aws_lb` + `aws_lb_target_group` + `aws_lb_listener` — Application Load Balancer | [README](l1/l1-alb/README.md) |
|
|
||||||
| `l1-ecr` | `aws_ecr_repository` — ECR container image repository | [README](l1/l1-ecr/README.md) |
|
|
||||||
|
|
||||||
## L2 compositions
|
|
||||||
|
|
||||||
| Module | What it references | README |
|
|
||||||
|--------|--------------------|--------|
|
|
||||||
| `l2-microservice` | 6 L1s (vpc, cluster, ecr, iam-role, alb, ecs-service) — **under redesign** | [README](l2/l2-microservice/README.md) |
|
|
||||||
| `l2-static-asset` | 1 L1 (s3) — **under redesign** | [README](l2/l2-static-asset/README.md) |
|
|
||||||
|
|
||||||
## Registry
|
|
||||||
|
|
||||||
Module versions are tracked in `registry.json`. Only L1 entries are
|
|
||||||
active; L2 entries have been pruned pending the composition redesign.
|
|
||||||
|
|
||||||
## Template
|
|
||||||
|
|
||||||
New modules should use [README-TEMPLATE.md](README-TEMPLATE.md) as
|
|
||||||
their starting point.
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
# l2-microservice — ECS Fargate microservice (composition being redesigned)
|
|
||||||
|
|
||||||
> **Module kind:** L2 composition | **Version:** TBD | **Status:** Under redesign
|
|
||||||
|
|
||||||
A composition that references multiple L1 primitives to deploy an ECS
|
|
||||||
Fargate microservice end-to-end (VPC, cluster, ECR, IAM role, ALB,
|
|
||||||
ECS service).
|
|
||||||
|
|
||||||
**The composition layer is being redesigned.** The previous
|
|
||||||
thin-composition implementation (a `composition.json` with children +
|
|
||||||
wires) has been removed. A new composition mechanism will be designed
|
|
||||||
in a later phase.
|
|
||||||
|
|
||||||
## Resources
|
|
||||||
|
|
||||||
TBD — the composition will reference these L1 primitives:
|
|
||||||
|
|
||||||
| L1 module | Purpose | README |
|
|
||||||
|-----------|---------|--------|
|
|
||||||
| `l1-vpc` | VPC, subnets, routing | [README](../l1/l1-vpc/README.md) |
|
|
||||||
| `l1-ecs-cluster` | ECS Fargate cluster | [README](../l1/l1-ecs-cluster/README.md) |
|
|
||||||
| `l1-ecr` | ECR image repository | [README](../l1/l1-ecr/README.md) |
|
|
||||||
| `l1-iam-role` | IAM task execution role | [README](../l1/l1-iam-role/README.md) |
|
|
||||||
| `l1-alb` | Application Load Balancer | [README](../l1/l1-alb/README.md) |
|
|
||||||
| `l1-ecs-service` | ECS task definition + service | [README](../l1/l1-ecs-service/README.md) |
|
|
||||||
|
|
||||||
## Inputs
|
|
||||||
|
|
||||||
TBD — will be defined when the composition mechanism is redesigned.
|
|
||||||
|
|
||||||
## Outputs
|
|
||||||
|
|
||||||
TBD — will be defined when the composition mechanism is redesigned.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
TBD — the composition mechanism is being redesigned. Until then, use
|
|
||||||
the L1 primitives directly. See each L1 module's README for usage
|
|
||||||
examples.
|
|
||||||
|
|
||||||
## Compliance extension points
|
|
||||||
|
|
||||||
The composition will need to wire compliance resources across L1s
|
|
||||||
when the compliance milestone (GDPR, SOX, SOC2, HIPAA, DORA) lands:
|
|
||||||
|
|
||||||
- **KMS key** — shared encryption key referenced by S3, ECR, CloudWatch Logs, and Secrets Manager.
|
|
||||||
- **CloudTrail** — management-plane audit trail for the entire stack.
|
|
||||||
- **VPC Flow Logs** — network audit trail.
|
|
||||||
- **Security groups** — proper network segmentation between ALB, service, and data tiers.
|
|
||||||
- **Private subnets** — ECS tasks in private subnets with NAT egress.
|
|
||||||
|
|
||||||
See each L1 module's README for per-module compliance extension points.
|
|
||||||
|
|
||||||
## Versioning
|
|
||||||
|
|
||||||
Versioning will be defined when the composition mechanism is
|
|
||||||
redesigned.
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
# l2-static-asset — S3 static asset (composition being redesigned)
|
|
||||||
|
|
||||||
> **Module kind:** L2 composition | **Version:** TBD | **Status:** Under redesign
|
|
||||||
|
|
||||||
A composition that references the `l1-s3` primitive to deploy a single
|
|
||||||
S3 bucket for static asset hosting.
|
|
||||||
|
|
||||||
**The composition layer is being redesigned.** The previous
|
|
||||||
thin-composition implementation (a `composition.json` with children +
|
|
||||||
wires) has been removed. A new composition mechanism will be designed
|
|
||||||
in a later phase.
|
|
||||||
|
|
||||||
## Resources
|
|
||||||
|
|
||||||
TBD — the composition will reference this L1 primitive:
|
|
||||||
|
|
||||||
| L1 module | Purpose | README |
|
|
||||||
|-----------|---------|--------|
|
|
||||||
| `l1-s3` | S3 bucket | [README](../l1/l1-s3/README.md) |
|
|
||||||
|
|
||||||
## Inputs
|
|
||||||
|
|
||||||
TBD — will be defined when the composition mechanism is redesigned.
|
|
||||||
|
|
||||||
## Outputs
|
|
||||||
|
|
||||||
TBD — will be defined when the composition mechanism is redesigned.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
TBD — the composition mechanism is being redesigned. Until then, use
|
|
||||||
`l1-s3` directly. See the [l1-s3 README](../l1/l1-s3/README.md) for a
|
|
||||||
usage example.
|
|
||||||
|
|
||||||
## Compliance extension points
|
|
||||||
|
|
||||||
The composition will need to wire compliance resources when the
|
|
||||||
compliance milestone (GDPR, SOX, SOC2, HIPAA, DORA) lands:
|
|
||||||
|
|
||||||
- **KMS key** — shared encryption key for S3 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.
|
|
||||||
|
|
||||||
See the [l1-s3 README](../l1/l1-s3/README.md) for per-module compliance
|
|
||||||
extension points.
|
|
||||||
|
|
||||||
## Versioning
|
|
||||||
|
|
||||||
Versioning will be defined when the composition mechanism is
|
|
||||||
redesigned.
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
{
|
|
||||||
"l1-s3": {
|
|
||||||
"1.0.0": {
|
|
||||||
"interface": "modules-ir/l1/l1-s3/interface.json",
|
|
||||||
"published_at": "2026-07-21T19:00:00Z",
|
|
||||||
"deprecated": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"l1-vpc": {
|
|
||||||
"1.0.0": {
|
|
||||||
"interface": "modules-ir/l1/l1-vpc/interface.json",
|
|
||||||
"published_at": "2026-07-21T21:30:00Z",
|
|
||||||
"deprecated": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"l1-ecs-cluster": {
|
|
||||||
"1.0.0": {
|
|
||||||
"interface": "modules-ir/l1/l1-ecs-cluster/interface.json",
|
|
||||||
"published_at": "2026-07-21T21:30:00Z",
|
|
||||||
"deprecated": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"l1-ecs-service": {
|
|
||||||
"1.0.0": {
|
|
||||||
"interface": "modules-ir/l1/l1-ecs-service/interface.json",
|
|
||||||
"published_at": "2026-07-21T21:30:00Z",
|
|
||||||
"deprecated": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"l1-iam-role": {
|
|
||||||
"1.0.0": {
|
|
||||||
"interface": "modules-ir/l1/l1-iam-role/interface.json",
|
|
||||||
"published_at": "2026-07-21T21:30:00Z",
|
|
||||||
"deprecated": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"l1-alb": {
|
|
||||||
"1.0.0": {
|
|
||||||
"interface": "modules-ir/l1/l1-alb/interface.json",
|
|
||||||
"published_at": "2026-07-21T21:30:00Z",
|
|
||||||
"deprecated": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"l1-ecr": {
|
|
||||||
"1.0.0": {
|
|
||||||
"interface": "modules-ir/l1/l1-ecr/interface.json",
|
|
||||||
"published_at": "2026-07-21T21:30:00Z",
|
|
||||||
"deprecated": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# <module-name> — <plain-language description>
|
# <module-name> — <plain-language description>
|
||||||
|
|
||||||
> **Module kind:** L1 primitive | **Version:** 1.0.0
|
> **Module kind:** primitive | **Version:** 1.0.0
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# 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 substrate 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) |
|
||||||
|
|
||||||
|
## Modules
|
||||||
|
|
||||||
|
| Module | What it references | README |
|
||||||
|
|--------|--------------------|--------|
|
||||||
|
| `microservice` | 6 primitives (vpc, cluster, ecr, iam-role, alb, ecs-service) | [README](l2/microservice/README.md) |
|
||||||
|
| `static-asset` | 1 primitive (s3) | [README](l2/static-asset/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.
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
# l1-alb — Application Load Balancer (load balancer + target group + listener)
|
# alb — Application Load Balancer (load balancer + target group + listener)
|
||||||
|
|
||||||
> **Module kind:** L1 primitive | **Version:** 1.0.0
|
> **Module kind:** primitive | **Version:** 1.0.0
|
||||||
|
|
||||||
An Application Load Balancer with a target group and a listener. This is
|
An Application Load Balancer with a target group and a listener. This is
|
||||||
a multi-resource module: it creates a load balancer, a target group, and
|
a multi-resource module: it creates a load balancer, a target group, and
|
||||||
a listener that forwards traffic to the target group. The target group
|
a listener that forwards traffic to the target group. The target group
|
||||||
is what `l1-ecs-service` registers its tasks with.
|
is what `ecs-service` registers its tasks with.
|
||||||
|
|
||||||
## Resources
|
## Resources
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ is what `l1-ecs-service` registers its tasks with.
|
|||||||
| Name | Type | Required | Default | Description |
|
| Name | Type | Required | Default | Description |
|
||||||
|------|------|----------|---------|-------------|
|
|------|------|----------|---------|-------------|
|
||||||
| `name` | string | yes | — | Name tag for the load balancer and child resources |
|
| `name` | string | yes | — | Name tag for the load balancer and child resources |
|
||||||
| `subnets` | string | yes | — | Comma-separated subnet ids (from `l1-vpc`) |
|
| `subnets` | string | yes | — | Comma-separated subnet ids (from `vpc`) |
|
||||||
| `security_group` | string | yes | — | Security group id for the load balancer |
|
| `security_group` | string | yes | — | Security group id for the load balancer |
|
||||||
| `port` | number | no | 80 | Listener port |
|
| `port` | number | no | 80 | Listener port |
|
||||||
| `protocol` | string | no | `HTTP` | Listener protocol |
|
| `protocol` | string | no | `HTTP` | Listener protocol |
|
||||||
@@ -40,7 +40,7 @@ is what `l1-ecs-service` registers its tasks with.
|
|||||||
{
|
{
|
||||||
"id": "alb",
|
"id": "alb",
|
||||||
"type": "aws:elbv2:loadbalancer",
|
"type": "aws:elbv2:loadbalancer",
|
||||||
"module": "l1-alb@1.0.0",
|
"module": "alb@1.0.0",
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"name": "acdl-microservice",
|
"name": "acdl-microservice",
|
||||||
"subnets": "ref:vpc.subnet_ids",
|
"subnets": "ref:vpc.subnet_ids",
|
||||||
@@ -52,7 +52,7 @@ is what `l1-ecs-service` registers its tasks with.
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The `target_group_arn` output is referenced by `l1-ecs-service` as its
|
The `target_group_arn` output is referenced by `ecs-service` as its
|
||||||
`lb_target_group_arn` input to wire the service to the ALB.
|
`lb_target_group_arn` input to wire the service to the ALB.
|
||||||
|
|
||||||
## Compliance extension points
|
## Compliance extension points
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "l1-alb",
|
"name": "alb",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"kind": "l1",
|
"kind": "l1",
|
||||||
"type": "aws:elbv2:loadbalancer",
|
"type": "aws:elbv2:loadbalancer",
|
||||||
"description": "Application Load Balancer primitive (substrate-agnostic IR types aws:elbv2:loadbalancer + aws:elbv2:listener + aws:elbv2:targetgroup; the Terraform adapter translates to aws_lb/aws_lb_listener/aws_lb_target_group).",
|
"description": "Application Load Balancer primitive (substrate-agnostic stack types aws:elbv2:loadbalancer + aws:elbv2:listener + aws:elbv2:targetgroup; the Terraform adapter translates to aws_lb/aws_lb_listener/aws_lb_target_group).",
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
},
|
},
|
||||||
"subnets": {
|
"subnets": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Comma-separated subnet ids (ref to l1-vpc).",
|
"description": "Comma-separated subnet ids (ref to vpc).",
|
||||||
"required": true
|
"required": true
|
||||||
},
|
},
|
||||||
"security_group": {
|
"security_group": {
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# l1-ecr — ECR repository
|
# ecr — ECR repository
|
||||||
|
|
||||||
> **Module kind:** L1 primitive | **Version:** 1.0.0
|
> **Module kind:** primitive | **Version:** 1.0.0
|
||||||
|
|
||||||
A single ECR repository that hosts the container image for the ECS
|
A single ECR repository that hosts the container image for the ECS
|
||||||
task. The simplest container-registry module — one resource, two
|
task. The simplest container-registry module — one resource, two
|
||||||
@@ -32,7 +32,7 @@ inputs, two outputs.
|
|||||||
{
|
{
|
||||||
"id": "ecr",
|
"id": "ecr",
|
||||||
"type": "aws:ecr:repository",
|
"type": "aws:ecr:repository",
|
||||||
"module": "l1-ecr@1.0.0",
|
"module": "ecr@1.0.0",
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"name": "acdl-microservice",
|
"name": "acdl-microservice",
|
||||||
"region": "us-east-1"
|
"region": "us-east-1"
|
||||||
@@ -41,7 +41,7 @@ inputs, two outputs.
|
|||||||
```
|
```
|
||||||
|
|
||||||
The `repository_url` output is used to build the `image` input for
|
The `repository_url` output is used to build the `image` input for
|
||||||
`l1-ecs-service` (e.g. `<repository_url>:latest`).
|
`ecs-service` (e.g. `<repository_url>:latest`).
|
||||||
|
|
||||||
## Compliance extension points
|
## Compliance extension points
|
||||||
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "l1-ecr",
|
"name": "ecr",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"kind": "l1",
|
"kind": "l1",
|
||||||
"type": "aws:ecr:repository",
|
"type": "aws:ecr:repository",
|
||||||
"description": "ECR repository primitive (substrate-agnostic IR type aws:ecr:repository; the Terraform adapter translates to aws_ecr_repository).",
|
"description": "ECR repository primitive (substrate-agnostic stack type aws:ecr:repository; the Terraform adapter translates to aws_ecr_repository).",
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
# l1-ecs-cluster — ECS Fargate cluster
|
# ecs-cluster — ECS Fargate cluster
|
||||||
|
|
||||||
> **Module kind:** L1 primitive | **Version:** 1.0.0
|
> **Module kind:** primitive | **Version:** 1.0.0
|
||||||
|
|
||||||
An ECS Fargate cluster. The simplest ECS module — one resource, two
|
An ECS Fargate cluster. The simplest ECS module — one resource, two
|
||||||
inputs, two outputs. The cluster is the container orchestration
|
inputs, two outputs. The cluster is the container orchestration
|
||||||
boundary that `l1-ecs-service` references for task placement.
|
boundary that `ecs-service` references for task placement.
|
||||||
|
|
||||||
## Resources
|
## Resources
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ boundary that `l1-ecs-service` references for task placement.
|
|||||||
{
|
{
|
||||||
"id": "cluster",
|
"id": "cluster",
|
||||||
"type": "aws:ecs:cluster",
|
"type": "aws:ecs:cluster",
|
||||||
"module": "l1-ecs-cluster@1.0.0",
|
"module": "ecs-cluster@1.0.0",
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"name": "acdl-microservice",
|
"name": "acdl-microservice",
|
||||||
"region": "us-east-1"
|
"region": "us-east-1"
|
||||||
@@ -40,7 +40,7 @@ boundary that `l1-ecs-service` references for task placement.
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The `cluster_arn` output is referenced by `l1-ecs-service` as its
|
The `cluster_arn` output is referenced by `ecs-service` as its
|
||||||
`cluster_arn` input.
|
`cluster_arn` input.
|
||||||
|
|
||||||
## Compliance extension points
|
## Compliance extension points
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "l1-ecs-cluster",
|
"name": "ecs-cluster",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"kind": "l1",
|
"kind": "l1",
|
||||||
"type": "aws:ecs:cluster",
|
"type": "aws:ecs:cluster",
|
||||||
"description": "ECS Fargate cluster primitive (substrate-agnostic IR type aws:ecs:cluster; the Terraform adapter translates to aws_ecs_cluster).",
|
"description": "ECS Fargate cluster primitive (substrate-agnostic stack type aws:ecs:cluster; the Terraform adapter translates to aws_ecs_cluster).",
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# l1-ecs-service — ECS Fargate service (task definition + service)
|
# ecs-service — ECS Fargate service (task definition + service)
|
||||||
|
|
||||||
> **Module kind:** L1 primitive | **Version:** 1.0.0
|
> **Module kind:** primitive | **Version:** 1.0.0
|
||||||
|
|
||||||
An ECS Fargate service with its task definition. Runs a container image
|
An ECS Fargate service with its task definition. Runs a container image
|
||||||
on Fargate, optionally behind an ALB target group. This is a
|
on Fargate, optionally behind an ALB target group. This is a
|
||||||
@@ -23,10 +23,10 @@ runs it.
|
|||||||
| `cpu` | number | no | 256 | Task CPU units (Fargate) |
|
| `cpu` | number | no | 256 | Task CPU units (Fargate) |
|
||||||
| `memory` | number | no | 512 | Task memory in MiB (Fargate) |
|
| `memory` | number | no | 512 | Task memory in MiB (Fargate) |
|
||||||
| `env` | string | no | — | Environment variables as a JSON map string |
|
| `env` | string | no | — | Environment variables as a JSON map string |
|
||||||
| `cluster_arn` | arn | yes | — | ECS cluster ARN (from `l1-ecs-cluster`) |
|
| `cluster_arn` | arn | yes | — | ECS cluster ARN (from `ecs-cluster`) |
|
||||||
| `subnets` | string | yes | — | Comma-separated subnet ids (from `l1-vpc`) |
|
| `subnets` | string | yes | — | Comma-separated subnet ids (from `vpc`) |
|
||||||
| `security_group` | string | yes | — | Security group id for the service ENIs |
|
| `security_group` | string | yes | — | Security group id for the service ENIs |
|
||||||
| `lb_target_group_arn` | arn | no | — | Optional ALB target group ARN (from `l1-alb`) |
|
| `lb_target_group_arn` | arn | no | — | Optional ALB target group ARN (from `alb`) |
|
||||||
| `region` | string | yes | — | AWS region the service is created in |
|
| `region` | string | yes | — | AWS region the service is created in |
|
||||||
|
|
||||||
## Outputs
|
## Outputs
|
||||||
@@ -42,7 +42,7 @@ runs it.
|
|||||||
{
|
{
|
||||||
"id": "service",
|
"id": "service",
|
||||||
"type": "aws:ecs:task_definition",
|
"type": "aws:ecs:task_definition",
|
||||||
"module": "l1-ecs-service@1.0.0",
|
"module": "ecs-service@1.0.0",
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"image": "581513795199.dkr.ecr.us-east-1.amazonaws.com/acdl-microservice:latest",
|
"image": "581513795199.dkr.ecr.us-east-1.amazonaws.com/acdl-microservice:latest",
|
||||||
"port": 8080,
|
"port": 8080,
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "l1-ecs-service",
|
"name": "ecs-service",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"kind": "l1",
|
"kind": "l1",
|
||||||
"type": "aws:ecs:task_definition",
|
"type": "aws:ecs:task_definition",
|
||||||
"description": "ECS Fargate service primitive (substrate-agnostic IR types aws:ecs:task_definition + aws:ecs:service; the Terraform adapter translates to aws_ecs_task_definition/aws_ecs_service).",
|
"description": "ECS Fargate service primitive (substrate-agnostic stack types aws:ecs:task_definition + aws:ecs:service; the Terraform adapter translates to aws_ecs_task_definition/aws_ecs_service).",
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"image": {
|
"image": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -34,12 +34,12 @@
|
|||||||
},
|
},
|
||||||
"cluster_arn": {
|
"cluster_arn": {
|
||||||
"type": "arn",
|
"type": "arn",
|
||||||
"description": "ECS cluster ARN (ref to l1-ecs-cluster).",
|
"description": "ECS cluster ARN (ref to ecs-cluster).",
|
||||||
"required": true
|
"required": true
|
||||||
},
|
},
|
||||||
"subnets": {
|
"subnets": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Comma-separated subnet ids (ref to l1-vpc).",
|
"description": "Comma-separated subnet ids (ref to vpc).",
|
||||||
"required": true
|
"required": true
|
||||||
},
|
},
|
||||||
"security_group": {
|
"security_group": {
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
},
|
},
|
||||||
"lb_target_group_arn": {
|
"lb_target_group_arn": {
|
||||||
"type": "arn",
|
"type": "arn",
|
||||||
"description": "Optional ALB target group ARN (ref to l1-alb).",
|
"description": "Optional ALB target group ARN (ref to alb).",
|
||||||
"required": false
|
"required": false
|
||||||
},
|
},
|
||||||
"region": {
|
"region": {
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# l1-iam-role — IAM role
|
# iam-role — IAM role
|
||||||
|
|
||||||
> **Module kind:** L1 primitive | **Version:** 1.0.0
|
> **Module kind:** primitive | **Version:** 1.0.0
|
||||||
|
|
||||||
A single IAM role with an assume-role policy and optional managed
|
A single IAM role with an assume-role policy and optional managed
|
||||||
policy attachments. Used as the ECS task execution role.
|
policy attachments. Used as the ECS task execution role.
|
||||||
@@ -33,7 +33,7 @@ policy attachments. Used as the ECS task execution role.
|
|||||||
{
|
{
|
||||||
"id": "roles",
|
"id": "roles",
|
||||||
"type": "aws:iam:role",
|
"type": "aws:iam:role",
|
||||||
"module": "l1-iam-role@1.0.0",
|
"module": "iam-role@1.0.0",
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"role_name": "acdl-microservice-exec",
|
"role_name": "acdl-microservice-exec",
|
||||||
"assume_role_policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"ecs-tasks.amazonaws.com\"},\"Action\":\"sts:AssumeRole\"}]}",
|
"assume_role_policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"ecs-tasks.amazonaws.com\"},\"Action\":\"sts:AssumeRole\"}]}",
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "l1-iam-role",
|
"name": "iam-role",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"kind": "l1",
|
"kind": "l1",
|
||||||
"type": "aws:iam:role",
|
"type": "aws:iam:role",
|
||||||
"description": "IAM role primitive (substrate-agnostic IR type aws:iam:role; the Terraform adapter translates to aws_iam_role).",
|
"description": "IAM role primitive (substrate-agnostic stack type aws:iam:role; the Terraform adapter translates to aws_iam_role).",
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"role_name": {
|
"role_name": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# l1-s3 — S3 bucket
|
# s3 — S3 bucket
|
||||||
|
|
||||||
> **Module kind:** L1 primitive | **Version:** 1.0.0
|
> **Module kind:** primitive | **Version:** 1.0.0
|
||||||
|
|
||||||
A single S3 bucket for object storage. The simplest module — one
|
A single S3 bucket for object storage. The simplest module — one
|
||||||
resource, two inputs, two outputs. Versioning is enabled by default.
|
resource, two inputs, two outputs. Versioning is enabled by default.
|
||||||
@@ -37,7 +37,7 @@ resource, two inputs, two outputs. Versioning is enabled by default.
|
|||||||
{
|
{
|
||||||
"id": "s3",
|
"id": "s3",
|
||||||
"type": "aws:s3:bucket",
|
"type": "aws:s3:bucket",
|
||||||
"module": "l1-s3@1.0.0",
|
"module": "s3@1.0.0",
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"bucket_name": "acdl-spike-bucket",
|
"bucket_name": "acdl-spike-bucket",
|
||||||
"region": "us-east-1"
|
"region": "us-east-1"
|
||||||
@@ -45,7 +45,7 @@ resource, two inputs, two outputs. Versioning is enabled by default.
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
A concrete instance is at `spike_instance.json` (used by the platform
|
A concrete instance is at `instance.json` (used by the platform
|
||||||
pipeline as the regression baseline).
|
pipeline as the regression baseline).
|
||||||
|
|
||||||
## Compliance extension points
|
## Compliance extension points
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"stack": {
|
"stack": {
|
||||||
"name": "l1-s3",
|
"name": "s3",
|
||||||
"kind": "l1",
|
"kind": "l1",
|
||||||
"depth": 1
|
"depth": 1
|
||||||
},
|
},
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
{
|
{
|
||||||
"id": "s3",
|
"id": "s3",
|
||||||
"type": "aws:s3:bucket",
|
"type": "aws:s3:bucket",
|
||||||
"module": "l1-s3@1.0.0",
|
"module": "s3@1.0.0",
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"bucket_name": "acdl-spike-bucket",
|
"bucket_name": "acdl-spike-bucket",
|
||||||
"region": "us-east-1"
|
"region": "us-east-1"
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "l1-s3",
|
"name": "s3",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"kind": "l1",
|
"kind": "l1",
|
||||||
"type": "aws:s3:bucket",
|
"type": "aws:s3:bucket",
|
||||||
"description": "S3 bucket primitive (substrate-agnostic IR type aws:s3:bucket; the Terraform adapter translates to aws_s3_bucket).",
|
"description": "S3 bucket primitive (substrate-agnostic stack type aws:s3:bucket; the Terraform adapter translates to aws_s3_bucket).",
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"bucket_name": {
|
"bucket_name": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# l1-vpc — VPC with subnets and routing
|
# vpc — VPC with subnets and routing
|
||||||
|
|
||||||
> **Module kind:** L1 primitive | **Version:** 1.0.0
|
> **Module kind:** primitive | **Version:** 1.0.0
|
||||||
|
|
||||||
A VPC with one subnet per availability zone and a route table with a
|
A VPC with one subnet per availability zone and a route table with a
|
||||||
default route through an internet gateway. The networking foundation
|
default route through an internet gateway. The networking foundation
|
||||||
@@ -38,7 +38,7 @@ that other modules (ALB, ECS service) reference for subnet ids.
|
|||||||
{
|
{
|
||||||
"id": "vpc",
|
"id": "vpc",
|
||||||
"type": "aws:ec2:vpc",
|
"type": "aws:ec2:vpc",
|
||||||
"module": "l1-vpc@1.0.0",
|
"module": "vpc@1.0.0",
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"cidr": "10.0.0.0/16",
|
"cidr": "10.0.0.0/16",
|
||||||
"azs": "us-east-1a,us-east-1b",
|
"azs": "us-east-1a,us-east-1b",
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "l1-vpc",
|
"name": "vpc",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"kind": "l1",
|
"kind": "l1",
|
||||||
"type": "aws:ec2:vpc",
|
"type": "aws:ec2:vpc",
|
||||||
"description": "VPC primitive (substrate-agnostic IR types aws:ec2:vpc + aws:ec2:subnet + aws:ec2:routetable; the Terraform adapter translates to aws_vpc/aws_subnet/aws_route_table).",
|
"description": "VPC primitive (substrate-agnostic stack types aws:ec2:vpc + aws:ec2:subnet + aws:ec2:routetable; the Terraform adapter translates to aws_vpc/aws_subnet/aws_route_table).",
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"cidr": {
|
"cidr": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user