ship: phase-07 architecture-v1-finalization (v1.1.2)
---ci--- project: acdl phase: 7 milestone: v1.1 status: shipped release: tag: v1.1.2 ---/ci--- Squash merge of phase/07-architecture-v1-finalization. Architecture finalized to v1.0: all 11 open decisions resolved (PROJECT.md); 9 deliverable files authored across 5 waves: - docs/architecture-v1.0.md (REQ-16, snapshot) - schemas/ir.schema.json (REQ-17, Target Stack IR) - schemas/policy_check_result.schema.json + adapters/terraform/policy/checkov_adapter.py (REQ-18) - platform/confidence_signal.py (REQ-19, 6-input signal, co-authored backend+security) - platform/audit_ledger_design.md (REQ-20, tiered ledger, spike scope D-041) - platform/hitl_matrix_design.md + platform/separation_of_duties.py (REQ-21) - schemas/contract.schema.json (REQ-22, per-env mandatory W3.E) scripts/verify_phase07.sh green: all 9 files present + validate.
This commit is contained in:
@@ -114,13 +114,13 @@
|
||||
|
||||
| Requirement | Phase | Status |
|
||||
|-------------|-------|--------|
|
||||
| REQ-16 | 07 | pending |
|
||||
| REQ-17 | 07 | pending |
|
||||
| REQ-18 | 07 | pending |
|
||||
| REQ-19 | 07 | pending |
|
||||
| REQ-20 | 07 | pending |
|
||||
| REQ-21 | 07 | pending |
|
||||
| REQ-22 | 07 | pending |
|
||||
| REQ-16 | 07 | complete (v1.1.2) |
|
||||
| REQ-17 | 07 | complete (v1.1.2) |
|
||||
| REQ-18 | 07 | complete (v1.1.2) |
|
||||
| REQ-19 | 07 | complete (v1.1.2) |
|
||||
| REQ-20 | 07 | complete (v1.1.2) |
|
||||
| REQ-21 | 07 | complete (v1.1.2) |
|
||||
| REQ-22 | 07 | complete (v1.1.2) |
|
||||
| REQ-23 | 08 | pending |
|
||||
| REQ-24 | 09 | pending |
|
||||
| REQ-25 | 10 | pending |
|
||||
|
||||
+1
-1
@@ -90,7 +90,7 @@ milestone COMPLETE: `v1.2.0` (feature milestone, next minor per ship.md).
|
||||
|
||||
### Phase 07 — architecture-v1-finalization
|
||||
- **Description:** Resolve the 11 open decisions in `docs/architecture.md` §13 (already recorded in `PROJECT.md`). Author the locked schemas + designs: `schemas/ir.schema.json` (REQ-17), `schemas/policy_check_result.schema.json` (REQ-18), `schemas/contract.schema.json` (REQ-22), `platform/confidence_signal.py` spec (REQ-19), `platform/audit_ledger_design.md` (REQ-20), `platform/hitl_matrix_design.md` (REQ-21). Mark architecture v1.0.
|
||||
- **Status:** pending
|
||||
- **Status:** complete (v1.1.2)
|
||||
- **Depends on:** [06]
|
||||
- **Requirements:** REQ-16, REQ-17, REQ-18, REQ-19, REQ-20, REQ-21, REQ-22
|
||||
- **Success Criteria:**
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
"""Translate Checkov JSON output to ACDL PolicyCheckResult records.
|
||||
|
||||
Reads Checkov's JSON output (one framework key, e.g. terraform_plan),
|
||||
emits a list of PolicyCheckResult dicts conforming to
|
||||
schemas/policy_check_result.schema.json. Run Checkov with --soft-fail so
|
||||
Checkov never exits non-zero; the confidence signal decides the gate, not
|
||||
Checkov's exit code.
|
||||
|
||||
Spike scope (D-043): tag/naming is a single SKIPPED record. A custom
|
||||
Checkov YAML rule for tag presence lands in v1.2.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import sys
|
||||
|
||||
|
||||
RULE_MAP = {
|
||||
"CKV_AWS_41": ("secrets-in-plaintext", "high"),
|
||||
"CKV_AWS_45": ("secrets-in-plaintext", "high"),
|
||||
"CKV_AWS_46": ("secrets-in-plaintext", "high"),
|
||||
"CKV_AWS_20": ("public-ingress", "high"),
|
||||
"CKV_AWS_57": ("public-ingress", "high"),
|
||||
"CKV_AWS_24": ("public-ingress", "medium"),
|
||||
"CKV_AWS_25": ("public-ingress", "medium"),
|
||||
"CKV_AWS_1": ("iam-wildcard", "high"),
|
||||
"CKV_AWS_40": ("iam-wildcard", "medium"),
|
||||
"CKV_AWS_7": ("kms-key-reference", "medium"),
|
||||
"CKV_AWS_33": ("kms-key-reference", "medium"),
|
||||
}
|
||||
|
||||
_RESULT_MAP = {"PASSED": "pass", "FAILED": "fail", "SKIPPED": "skipped"}
|
||||
|
||||
|
||||
def _iso8601_now():
|
||||
return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
|
||||
def _to_pcr(checkov_record, contract_id, result_str):
|
||||
check_id = checkov_record.get("check_id", "")
|
||||
default_sev = RULE_MAP.get(check_id, (check_id, "info"))[1]
|
||||
severity = checkov_record.get("severity", default_sev)
|
||||
if isinstance(severity, str):
|
||||
severity = severity.lower()
|
||||
return {
|
||||
"contractId": contract_id,
|
||||
"evaluatedAt": _iso8601_now(),
|
||||
"engine": "checkov",
|
||||
"ruleId": check_id,
|
||||
"severity": severity,
|
||||
"result": _RESULT_MAP.get(result_str, "error"),
|
||||
"message": checkov_record.get("check_name", ""),
|
||||
"evidence": {
|
||||
"file_path": checkov_record.get("file_path"),
|
||||
"resource": checkov_record.get("resource"),
|
||||
"resource_address": checkov_record.get("resource_address"),
|
||||
"code_block": checkov_record.get("code_block"),
|
||||
},
|
||||
"resourceRef": checkov_record.get("resource_address") or checkov_record.get("resource", ""),
|
||||
}
|
||||
|
||||
|
||||
def _emit_tag_naming_skipped(contract_id):
|
||||
return {
|
||||
"contractId": contract_id,
|
||||
"evaluatedAt": _iso8601_now(),
|
||||
"engine": "checkov",
|
||||
"ruleId": "ACDL_TAG_NAMING",
|
||||
"severity": "info",
|
||||
"result": "skipped",
|
||||
"message": "tag/naming check deferred to v1.2 (D-043)",
|
||||
"evidence": {},
|
||||
"resourceRef": "",
|
||||
}
|
||||
|
||||
|
||||
def adapt(checkov_json_path, contract_id):
|
||||
with open(checkov_json_path, "r", encoding="utf-8") as fh:
|
||||
data = json.load(fh)
|
||||
out = []
|
||||
for framework, body in data.items():
|
||||
results = body.get("results", body) if isinstance(body, dict) else {}
|
||||
if not isinstance(results, dict):
|
||||
continue
|
||||
for rec in results.get("passed_checks", []):
|
||||
out.append(_to_pcr(rec, contract_id, "PASSED"))
|
||||
for rec in results.get("failed_checks", []):
|
||||
out.append(_to_pcr(rec, contract_id, "FAILED"))
|
||||
for rec in results.get("skipped_checks", []):
|
||||
out.append(_to_pcr(rec, contract_id, "SKIPPED"))
|
||||
out.append(_emit_tag_naming_skipped(contract_id))
|
||||
return out
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 3:
|
||||
print("usage: checkov_adapter.py <checkov.json> <contract-id>", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
print(json.dumps(adapt(sys.argv[1], sys.argv[2]), indent=2))
|
||||
@@ -0,0 +1,458 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,103 @@
|
||||
# ACDL Tiered Audit Ledger Design (REQ-20)
|
||||
|
||||
> **Status:** design authored in Phase 07 (milestone v1.1); the spike
|
||||
> (Phases 08-10) implements the **v1.0 hash chain + DynamoDB outbox write**
|
||||
> (D-041); the v1.2 build-out implements S3 Object Lock + JWS + async
|
||||
> worker + DLQ + daily checkpoints.
|
||||
|
||||
The audit stream is the platform's tamper-evident record of every delivery
|
||||
action. The vision's "Audit truth lives outside the repository" bet [1]
|
||||
and "Not a mutable audit log" anti-goal [1] are the binding constraints.
|
||||
Version-control history does not satisfy regulatory evidence; the ledger
|
||||
is the source of truth.
|
||||
|
||||
## Three tiers
|
||||
|
||||
- **Cold tier (source of truth):** S3 with **Object Lock in compliance
|
||||
mode**, **7-year retention** (ARCHITECTURE.md §9). No one — including
|
||||
root — can delete or overwrite until retention expires. The regulatory
|
||||
record.
|
||||
- **Hot tier (query index):** the `acdl-evidence` audit repo (unchanged
|
||||
from the v1.0 demo). Not part of the chain; a queryable mirror the
|
||||
evidence UI (`evidence-ui/index.html`) reads. Lightweight attestation
|
||||
linkage lives in the repo; the regulatory event body lives in S3.
|
||||
- **Outbox (write path):** DynamoDB, **RPO = 0** (synchronous write before
|
||||
contract submission ack). Single-region in v1 (`us-east-1`).
|
||||
|
||||
## Spike scope (D-041) — what Phases 08-10 implement
|
||||
|
||||
- **DynamoDB outbox:** table `acdl-outbox`, `PAY_PER_REQUEST` (D-044),
|
||||
PK `contractId`, SK `eventType#eventTs`, TTL `expire_at` = now + 365d
|
||||
(1-year storage per ARCHITECTURE.md §8).
|
||||
- **`prev_event_hash` chain:** SHA-256 over canonical JSON
|
||||
(`json.dumps(event, sort_keys=True, separators=(",", ":"))`), lifted
|
||||
from the v1.0 demo's `evidence_writer.py`. Auto-genesis: first event
|
||||
has `prev_hash="GENESIS"`.
|
||||
- **Synchronous write** via boto3 `put_item` (strong-consistent by
|
||||
default). No separate async worker / DLQ in the spike (RTO = workflow
|
||||
re-run).
|
||||
- **Mirror to `acdl-evidence`:** unchanged from v1.0 — the finalize step
|
||||
commits `audit.json` to the evidence repo (the hot tier).
|
||||
- **Spike evidence event shape:**
|
||||
`{seq, ts, stage, event, prev_hash, hash, contractId, environment, stack, score, band}`.
|
||||
|
||||
## v1.2 build-out — what Phase 07 designs but the spike defers
|
||||
|
||||
- **S3 Object Lock:** bucket `acdl-evidence-lock-<account-id>`, Object
|
||||
Lock enabled at creation, compliance mode, 7-yr retention
|
||||
(`RetainUntilDate` = now + 7y). The outbox→S3 path is an async worker
|
||||
that reads from the outbox and writes to Object Lock.
|
||||
- **JWS detached signature (RFC 7515):** the event payload is
|
||||
canonical-JSON-serialized, SHA-256 hashed, signed with a private key;
|
||||
the signature is stored *detached* alongside the payload. Signing key =
|
||||
**platform-level KMS key** (not per-contract — a per-contract key would
|
||||
explode the key-management surface), rotated **quarterly**. The `jws`
|
||||
field is added to the event shape in v1.2.
|
||||
- **Async worker + DLQ:** a Lambda (or a Gitea Actions scheduled workflow)
|
||||
reads the outbox, writes to S3 Object Lock, signs with KMS. DLQ = an
|
||||
SQS dead-letter queue for failed writes. RTO = DLQ replay.
|
||||
- **Daily checkpoints (§9):** a daily job reads the last event hash and
|
||||
writes a "checkpoint" event to the ledger (+ optionally to a public
|
||||
notarization service). The spike runs in minutes, not days — no
|
||||
checkpoint in spike.
|
||||
|
||||
## JWS vs chain — orthogonality note
|
||||
|
||||
The `prev_event_hash` chain gives ordering/tamper-evidence *within* the
|
||||
log (a deleted event breaks the chain visibly); JWS gives authenticity
|
||||
*per event* (a forged event is detectable without re-reading the whole
|
||||
chain). The chain is spike-scope; JWS is v1.2. Together they cover both
|
||||
integrity properties the vision's "Not a mutable audit log" anti-goal
|
||||
requires.
|
||||
|
||||
## Outbox item shape (full, spike + v1.2)
|
||||
|
||||
- PK `contractId` (UUID).
|
||||
- SK `eventType#eventTs` (e.g. `POLICY_CHECKED#2026-07-21T12:00:00Z`).
|
||||
- `payload` (the event body — hash-chained in spike, JWS-signed in v1.2).
|
||||
- `prev_event_hash` (chain link; `GENESIS` for the first event).
|
||||
- `hash` (this event's SHA-256 over canonical JSON).
|
||||
- `approver_qa` (Gitea username of the QA approver; empty in dev-only
|
||||
spike; populated on qa-promotion — D-042).
|
||||
- `approver_prod` (SRE username; empty in spike).
|
||||
- `environment`, `stack`, `score`, `band`.
|
||||
- `expire_at` (TTL = now + 365d).
|
||||
- **v1.2 only:** `jws` (detached signature), `checkpoint_ref`.
|
||||
|
||||
## RPO / RTO table
|
||||
|
||||
| Phase | RPO | RTO |
|
||||
|-------|-----|-----|
|
||||
| Spike (D-041) | 0 (sync outbox write) | workflow re-run |
|
||||
| v1.2 | 0 (sync outbox) | async worker DLQ replay |
|
||||
|
||||
## Decision trail
|
||||
|
||||
- **D-041** — spike scope = hash chain + outbox write; Object Lock + JWS
|
||||
+ worker + DLQ are v1.2.
|
||||
- **D-044** — outbox mode `PAY_PER_REQUEST`; PK/SK; TTL `expire_at` =
|
||||
now + 365d; no separate async worker in spike.
|
||||
- **D-042** — approver identities (`approver_qa`, `approver_prod`) live
|
||||
in the outbox; the separation-of-duties check
|
||||
(`platform/separation_of_duties.py`) reads `approver_qa` and compares
|
||||
to the prod-dispatch `gitea.actor`.
|
||||
@@ -0,0 +1,175 @@
|
||||
"""ACDL Confidence Signal (REQ-19).
|
||||
|
||||
The platform's certified answer to "is this safe to proceed?" (vision
|
||||
tenet: "Safety is Computed, Not Assumed"). Every delivery action produces
|
||||
a measurable, explainable confidence signal; reliance on operator
|
||||
instinct is not a substitute.
|
||||
|
||||
Inputs (weights sum to 1.0, D-040):
|
||||
1. policy_results (0.30) — list[PolicyCheckResult] (schemas/policy_check_result.schema.json)
|
||||
2. validation (0.25) — {schema: bool, ir_resolved: bool, tf_validated: bool, tf_planned: bool}
|
||||
3. freshness (0.10) — {age_days: float, max_age_days: float}
|
||||
4. source (0.15) — {submitter: str, commit_sha: str, signed: bool}
|
||||
5. history (0.10) — {prior_rollbacks: int, prior_policy_fails: int}
|
||||
6. nfrs (0.10) — {declared: list[str], conformance: float|None}
|
||||
|
||||
Severity -> penalty (locked, ARCHITECTURE.md §8):
|
||||
critical -> hard override (score = 0, block)
|
||||
high -> -0.20
|
||||
medium -> -0.05
|
||||
low -> -0.01
|
||||
info -> 0.00
|
||||
|
||||
Per-env thresholds (locked, ARCHITECTURE.md §8): dev 0.50, qa 0.75, prod 0.90, dr 0.95.
|
||||
Output: {score, band, perInput, reasonCodes}.
|
||||
Halt with explicit reason on missing input (§8).
|
||||
|
||||
Spike cold-start (A-6.2): inputs 3 (freshness), 5 (history), 6 (nfrs) are
|
||||
'present + neutral 0.5' because the spike is the first submission with no
|
||||
history and no declared NFRs. The gate is *presence*, not *conformance* —
|
||||
the 'all six inputs present' dev gate (§5) is satisfied by non-null
|
||||
per-input scores.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, asdict
|
||||
from typing import List, Literal, Optional, Dict, Any
|
||||
import json
|
||||
import sys
|
||||
|
||||
|
||||
WEIGHTS = {
|
||||
"policy": 0.30,
|
||||
"validation": 0.25,
|
||||
"freshness": 0.10,
|
||||
"source": 0.15,
|
||||
"history": 0.10,
|
||||
"nfrs": 0.10,
|
||||
}
|
||||
|
||||
PENALTY = {
|
||||
"critical": None,
|
||||
"high": 0.20,
|
||||
"medium": 0.05,
|
||||
"low": 0.01,
|
||||
"info": 0.0,
|
||||
}
|
||||
|
||||
THRESHOLDS = {"dev": 0.50, "qa": 0.75, "prod": 0.90, "dr": 0.95}
|
||||
|
||||
|
||||
@dataclass
|
||||
class Signal:
|
||||
score: float
|
||||
band: Literal["pass", "warn", "block"]
|
||||
perInput: Dict[str, float]
|
||||
reasonCodes: List[str]
|
||||
|
||||
|
||||
def _per_input_score(name: str, raw: Any) -> tuple:
|
||||
"""Return (score in [0,1], reasons list). Unknown/missing -> 0.5 + INPUT_MISSING."""
|
||||
reasons: List[str] = []
|
||||
if raw is None:
|
||||
return 0.5, [f"INPUT_MISSING:{name}"]
|
||||
if name == "policy":
|
||||
pcrs = raw if isinstance(raw, list) else []
|
||||
if not pcrs:
|
||||
return 0.5, []
|
||||
scores = []
|
||||
for pcr in pcrs:
|
||||
r = pcr.get("result", "skipped")
|
||||
if r == "pass" or r == "skipped":
|
||||
scores.append(1.0)
|
||||
else:
|
||||
scores.append(0.0)
|
||||
return sum(scores) / len(scores), []
|
||||
if name == "validation":
|
||||
keys = ("schema", "ir_resolved", "tf_validated", "tf_planned")
|
||||
if not isinstance(raw, dict):
|
||||
return 0.5, []
|
||||
trues = sum(1 for k in keys if raw.get(k))
|
||||
return trues / 4.0, []
|
||||
if name == "freshness":
|
||||
if not isinstance(raw, dict):
|
||||
return 0.5, []
|
||||
age = float(raw.get("age_days", 0))
|
||||
mx = float(raw.get("max_age_days", 1)) or 1
|
||||
s = 1.0 - (age / mx)
|
||||
return max(0.0, min(1.0, s)), []
|
||||
if name == "source":
|
||||
if not isinstance(raw, dict):
|
||||
return 0.5, []
|
||||
if raw.get("submitter") and raw.get("commit_sha"):
|
||||
return 1.0, []
|
||||
return 0.5, []
|
||||
if name == "history":
|
||||
if not isinstance(raw, dict):
|
||||
return 0.5, []
|
||||
rollbacks = int(raw.get("prior_rollbacks", 0))
|
||||
fails = int(raw.get("prior_policy_fails", 0))
|
||||
s = 1.0 - (rollbacks * 0.2 + fails * 0.1)
|
||||
return max(0.0, min(1.0, s)), []
|
||||
if name == "nfrs":
|
||||
if not isinstance(raw, dict):
|
||||
return 0.5, []
|
||||
conf = raw.get("conformance")
|
||||
if conf is None:
|
||||
return 0.5, []
|
||||
return float(conf), []
|
||||
return 0.5, []
|
||||
|
||||
|
||||
def compute(contract_id: str, environment: str,
|
||||
inputs: Dict[str, Any]) -> Signal:
|
||||
"""Orchestrate the 6-input weighted sum + severity penalty + band."""
|
||||
missing = sorted(set(WEIGHTS.keys()) - set(inputs.keys()))
|
||||
if missing:
|
||||
return Signal(0.0, "block", {},
|
||||
[f"INPUT_MISSING:{m}" for m in missing])
|
||||
|
||||
per_input: Dict[str, float] = {}
|
||||
reasons: List[str] = []
|
||||
base = 0.0
|
||||
for name, weight in WEIGHTS.items():
|
||||
raw = inputs.get(name)
|
||||
s, r = _per_input_score(name, raw)
|
||||
per_input[name] = s
|
||||
reasons.extend(r)
|
||||
base += s * weight
|
||||
|
||||
penalty = 0.0
|
||||
policy_input = inputs.get("policy")
|
||||
pcrs = policy_input if isinstance(policy_input, list) else []
|
||||
for pcr in pcrs:
|
||||
if not isinstance(pcr, dict):
|
||||
continue
|
||||
if pcr.get("result") != "fail":
|
||||
continue
|
||||
sev = pcr.get("severity")
|
||||
p = PENALTY.get(sev, 0.0)
|
||||
if p is None:
|
||||
return Signal(0.0, "block", per_input,
|
||||
reasons + [f"CRITICAL_OVERRIDE:{pcr.get('ruleId','?')}"])
|
||||
penalty += p
|
||||
|
||||
score = max(0.0, min(1.0, base - penalty))
|
||||
threshold = THRESHOLDS[environment]
|
||||
if score >= threshold:
|
||||
band = "pass"
|
||||
elif score < threshold - 0.10:
|
||||
band = "block"
|
||||
else:
|
||||
band = "warn"
|
||||
if environment == "dev" and band == "warn":
|
||||
band = "block"
|
||||
return Signal(score, band, per_input, reasons)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 3:
|
||||
print("usage: confidence_signal.py <inputs.json> <environment>", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
env = sys.argv[2]
|
||||
with open(sys.argv[1], "r", encoding="utf-8") as fh:
|
||||
inputs = json.load(fh)
|
||||
sig = compute("cli", env, inputs)
|
||||
print(json.dumps(asdict(sig), indent=2))
|
||||
@@ -0,0 +1,115 @@
|
||||
# ACDL Human-in-the-Loop Matrix + Separation-of-Duties Design (REQ-21)
|
||||
|
||||
> **Status:** design authored in Phase 07 (milestone v1.1); v1.2 wires the
|
||||
> gates. The spike (Phases 08-10) is **dev-only**; HITL is not exercised
|
||||
> (the spike contract has `environment: dev`).
|
||||
|
||||
The vision's "Lower Environments are Autonomous; Higher Environments are
|
||||
Attested" tenet [1] and the "deliberate human attestation — not as a
|
||||
rubber stamp" requirement [1] are the binding constraints.
|
||||
|
||||
## Gate model (ARCHITECTURE.md §10.1)
|
||||
|
||||
**Pre-execution gates.** The contract is held in a "validated but not
|
||||
applied" state until the human attests. qa, prod, dr are attestation
|
||||
gates. No partial deployment to roll back on rejection (qa, prod); dr is
|
||||
a separate deployment against a separate cluster/region. The
|
||||
canary/deployment-rollback model is explicitly not in scope for v1.
|
||||
|
||||
## Gitea-specific gate mechanics (D-042)
|
||||
|
||||
Gitea has **no Environments API** and ignores `environment:` blocks
|
||||
(v1.0 D-013; re-confirmed in RESEARCH TARGET 1). The pre-execution gate
|
||||
is modeled as a `workflow_dispatch` with approval inputs:
|
||||
|
||||
- **qa gate:** `workflow_dispatch` with `approve_qa: true`; the dispatch
|
||||
run's `gitea.actor` is the QA approver.
|
||||
- **prod gate:** `workflow_dispatch` with `approve_prod: true`;
|
||||
`gitea.actor` is the SRE approver.
|
||||
- **dr gate:** `workflow_dispatch` with `approve_dr: true`; same.
|
||||
|
||||
The approver identity of record = `gitea.actor` of the dispatch run
|
||||
(D-042). There is no other approval-identity signal in Gitea. The v1.2
|
||||
real-OIDC path (blocked on go-gitea/gitea#36988) does not change this —
|
||||
OIDC authorizes the *runner* to AWS, it does not change how the platform
|
||||
records the *human* approver.
|
||||
|
||||
## Reviewer routing (ARCHITECTURE.md §10.2)
|
||||
|
||||
Gitea CODEOWNERS routes the right reviewer to the right gate:
|
||||
|
||||
- qa → QA team
|
||||
- prod → SRE team
|
||||
- dr → SRE team
|
||||
|
||||
CODEOWNERS **routes**; it does **not** enforce identity distinctness (that
|
||||
is the platform-internal outbox check in
|
||||
`platform/separation_of_duties.py`).
|
||||
|
||||
## Full 8-concern attestation matrix (§10.4, lifted verbatim)
|
||||
|
||||
| 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 |
|
||||
|
||||
## Timeout behavior (§10.5)
|
||||
|
||||
| Time | State | Action |
|
||||
|---|---|---|
|
||||
| Submission | PENDING_ATTESTATION | Notify responsible team |
|
||||
| 1 business day | PENDING_ATTESTATION_WARNING | Notify team + platform on-call (elevated path); emit `PENDING_ATTESTATION_TIMEOUT_WARNING` event |
|
||||
| 2 business days | PENDING_ATTESTATION_AUTO_FREEZE | Auto-freeze; require re-submission; emit `PENDING_ATTESTATION_AUTO_FREEZE` event; new submission linked via `supersedes` |
|
||||
|
||||
**Implementation:** a Gitea `on: schedule` workflow (runs hourly) that
|
||||
scans the DynamoDB outbox for `PENDING_ATTESTATION` events with `ts`
|
||||
older than 1/2 business days and emits the warn/freeze events. Not
|
||||
implemented in the spike (dev-only).
|
||||
|
||||
## Rejection and rollback (§10.6)
|
||||
|
||||
Rejection returns the contract to a `HELD` state with the rejection
|
||||
reason captured as a `PROMOTION_REJECTED` event. The consumer fixes the
|
||||
cause and re-submits; the new submission is linked to the rejected one
|
||||
via `supersedes` (a contract-schema field — `schemas/contract.schema.json`).
|
||||
The audit chain is **extended, not torn up** (the "Not a mutable audit
|
||||
log" anti-goal). No partial deployment to roll back at any v1 gate.
|
||||
|
||||
## Separation of duties (§10.3) — pointer to the .py
|
||||
|
||||
The identity-distinctness check is platform-internal, not GitHub-native,
|
||||
not Kyverno (in v1). Sequence:
|
||||
|
||||
1. On promotion dev → qa, the platform reads the QA approver's identity
|
||||
from the `workflow_dispatch` run's `gitea.actor` and writes it to the
|
||||
DynamoDB outbox keyed by `contractId` (attribute `approver_qa`).
|
||||
2. On promotion qa → prod, the platform reads the stored `approver_qa`
|
||||
from the outbox and the new SRE approver's `gitea.actor` from the
|
||||
prod-dispatch run.
|
||||
3. If `approver_qa == approver_prod`, the platform blocks the prod
|
||||
promotion, writes a `SEPARATION_OF_DUTIES_VIOLATION` event to the
|
||||
evidence stream, and routes a halt artifact to the SRE on-call.
|
||||
4. The check is implemented in `platform/separation_of_duties.py`
|
||||
(T-7.8). The platform is the only writer to the outbox; the check is
|
||||
in the same process that has authority to block the promotion.
|
||||
|
||||
## Spike scope note
|
||||
|
||||
The spike is dev-only (REQ-27 contract has `environment: dev`), so HITL
|
||||
is not exercised. Phase 07 authors the design; Phase 10's
|
||||
`verify_phase10.sh` does not assert HITL behavior. v1.2 wires the gates
|
||||
against this design.
|
||||
|
||||
## Decision trail
|
||||
|
||||
- **D-042** — approver identity = `gitea.actor` of the `workflow_dispatch`
|
||||
run; no Environments API in Gitea.
|
||||
- **D-013** (v1.0) — the `workflow_dispatch` approval-input fallback,
|
||||
re-used for the real platform's pre-execution gate model.
|
||||
@@ -0,0 +1,42 @@
|
||||
"""Check that qaApprover != prodApprover for a contract (ARCHITECTURE.md
|
||||
§10.3, D-042). Reads `approver_qa` from the DynamoDB outbox for the
|
||||
contractId, compares to the prod-dispatch `gitea.actor`. Blocks on
|
||||
equality, emits `SEPARATION_OF_DUTIES_VIOLATION`, routes a halt artifact
|
||||
to SRE on-call.
|
||||
|
||||
Spike scope (A-8.1): the spike is dev-only (REQ-27 contract has
|
||||
environment: dev); HITL is not exercised. This module is authored to its
|
||||
full v1.2 shape but the spike calls it with current_prod_approver=None
|
||||
and a None outbox_client — the check returns (True, 'no QA approver
|
||||
recorded (dev-only spike)').
|
||||
"""
|
||||
|
||||
from typing import Optional, Tuple
|
||||
|
||||
|
||||
def check(outbox_client, contract_id: str,
|
||||
current_prod_approver: Optional[str]) -> Tuple[bool, str]:
|
||||
"""Return (ok, reason). ok=False means block the prod promotion."""
|
||||
if outbox_client is None:
|
||||
return (True, "no outbox client (dev-only spike)")
|
||||
item = outbox_client.get(contract_id)
|
||||
if item is None:
|
||||
return (True, "no prior approver (first promotion)")
|
||||
qa_approver = item.get("approver_qa")
|
||||
if not qa_approver:
|
||||
return (True, "no QA approver recorded (dev-only spike)")
|
||||
if current_prod_approver is None:
|
||||
return (True, "no prod approver supplied (dev-only spike)")
|
||||
if qa_approver == current_prod_approver:
|
||||
return (False,
|
||||
f"SEPARATION_OF_DUTIES_VIOLATION: "
|
||||
f"qaApprover==prodApprover=={qa_approver}")
|
||||
return (True, "distinct")
|
||||
|
||||
|
||||
def route_halt_artifact(contract_id: str, violation_reason: str,
|
||||
oncall_client) -> None:
|
||||
"""Route a halt artifact to SRE on-call. Spike: stub that logs. v1.2
|
||||
wires a real pager."""
|
||||
print(f"[halt-artifact] contract={contract_id} reason={violation_reason} "
|
||||
f"oncall={oncall_client}", flush=True)
|
||||
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://acdl.cloudinit.dev/schemas/contract.schema.json",
|
||||
"title": "ACDL Contract",
|
||||
"description": "Consumer-declared intent. The central pipeline resolves a contract to a Target Stack IR (schemas/ir.schema.json), the Terraform adapter compiles the IR to a plan. Strict fail-fast at schema stage with reason codes from a published vocabulary.",
|
||||
"$comment": "Per-env mandatory inputs per W3.E (PROJECT.md). 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). W2.A (tag for dev/qa, SHA for prod) is a workflow-reference concern, not a schema field; the platform CLI resolves tag->SHA for prod-bound workflows.",
|
||||
"type": "object",
|
||||
"required": ["stack", "environment"],
|
||||
"properties": {
|
||||
"stack": {
|
||||
"type": "string",
|
||||
"pattern": "^l2-[a-z][a-z0-9-]*$",
|
||||
"description": "L2 thin-composition reference (resolved by the pipeline to a Target Stack IR)."
|
||||
},
|
||||
"environment": {
|
||||
"type": "string",
|
||||
"enum": ["dev", "qa", "prod", "dr"],
|
||||
"description": "Target environment. Staging does not exist (Path A locked, ARCHITECTURE.md §5)."
|
||||
},
|
||||
"inputs": {
|
||||
"type": "object",
|
||||
"description": "L2-level parameter map. Free-form in v1, typed per-L1 in v1.2 (W3.E).",
|
||||
"additionalProperties": {"type": ["string", "number", "boolean"]}
|
||||
},
|
||||
"validation": {
|
||||
"type": "object",
|
||||
"description": "Validation evidence required in qa (W3.E).",
|
||||
"properties": {
|
||||
"e2eSuite": {"type": "string", "description": "Reference to the contract-declared e2e suite (last 24h, pass rate >= 99%)."},
|
||||
"loadTest": {"type": "string", "description": "Reference to the load test report (last 7d, p99 < declared NFR)."}
|
||||
}
|
||||
},
|
||||
"runbook": {"type": "string", "description": "Runbook reference, mandatory in prod (W3.E)."},
|
||||
"dashboard": {"type": "string", "description": "Dashboard reference, mandatory in prod (W3.E)."},
|
||||
"oncall": {"type": "string", "description": "On-call rotation reference, mandatory in prod (W3.E)."},
|
||||
"drDrillRef": {"type": "string", "description": "DR drill report reference (last 180d), mandatory in dr (W3.E)."},
|
||||
"profile": {
|
||||
"type": "string",
|
||||
"enum": ["developer", "agentic"],
|
||||
"default": "developer",
|
||||
"description": "Consumer surface. 'agentic' unlocks L3B fields (ARCHITECTURE.md §5)."
|
||||
},
|
||||
"naturalLanguageIntent": {
|
||||
"type": "string",
|
||||
"description": "L3B: the original natural-language prompt. Required when profile is agentic (W3.E)."
|
||||
},
|
||||
"confidenceAtSubmission": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1,
|
||||
"description": "L3B: the agent's self-reported confidence at submission time."
|
||||
},
|
||||
"agentTrace": {
|
||||
"type": "string",
|
||||
"description": "L3B: reference to the agent's execution trace."
|
||||
},
|
||||
"supersedes": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Prior contractId this re-submission replaces (after rejection — ARCHITECTURE.md §10.6)."
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"if": {"properties": {"environment": {"const": "qa"}}},
|
||||
"then": {"required": ["validation"],
|
||||
"properties": {"validation": {"required": ["e2eSuite", "loadTest"]}}}
|
||||
},
|
||||
{
|
||||
"if": {"properties": {"environment": {"const": "prod"}}},
|
||||
"then": {"required": ["runbook", "dashboard", "oncall"]}
|
||||
},
|
||||
{
|
||||
"if": {"properties": {"environment": {"const": "dr"}}},
|
||||
"then": {"required": ["drDrillRef"]}
|
||||
},
|
||||
{
|
||||
"if": {"required": ["profile"], "properties": {"profile": {"const": "agentic"}}},
|
||||
"then": {"required": ["naturalLanguageIntent"]}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://acdl.cloudinit.dev/schemas/ir.schema.json",
|
||||
"title": "ACDL Target Stack IR",
|
||||
"description": "Substrate-neutral description of a target stack: resources with typed inputs/outputs/NFRs, relationships (single parent per child), composition tree (max depth 5), and policy hooks. The L1 registry, L2 thin-composition tree, contract YML, and PolicyCheckResult schema are all defined against this IR. Substrate adapters (the Terraform adapter in v1) are the only substrate-specific code.",
|
||||
"$comment": "v1 ships one adapter (Terraform). The IR is nearly isomorphic to Terraform in v1 (ARCHITECTURE.md §12.1); the adapter compiles resource.module -> module block, resource.inputs -> variable + arg, resource.outputs -> output, relationship.kind=uses_output -> interpolation, relationship.kind=parent -> composition ordering hint. As more adapters appear (v2+), the IR gains expressiveness; the L1 content + contract YML + thin-composition tree do not change. The schema body is substrate-agnostic: no Terraform block keywords (variable/output/resource as blocks) and no aws_ provider prefixes in the schema keywords; type values are IR types (aws:s3:bucket), not Terraform resource types (aws_s3_bucket).",
|
||||
"type": "object",
|
||||
"required": ["version", "stack", "resources"],
|
||||
"properties": {
|
||||
"version": {
|
||||
"type": "string",
|
||||
"description": "IR schema version (semver).",
|
||||
"pattern": "^\\d+\\.\\d+\\.\\d+$"
|
||||
},
|
||||
"stack": {
|
||||
"type": "object",
|
||||
"description": "The L1/L2 stack identity this IR represents.",
|
||||
"required": ["name", "kind", "depth"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"pattern": "^l[12]-[a-z][a-z0-9-]*$",
|
||||
"description": "Stack name matching the L1/L2 folder name."
|
||||
},
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"enum": ["l1", "l2"],
|
||||
"description": "l1 = primitive; l2 = thin-composition."
|
||||
},
|
||||
"depth": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 5,
|
||||
"description": "Composition depth (ARCHITECTURE.md §3: max depth 5). L2->L1 is depth 1."
|
||||
}
|
||||
}
|
||||
},
|
||||
"resources": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {"$ref": "#/$defs/resource"}
|
||||
},
|
||||
"relationships": {
|
||||
"type": "array",
|
||||
"description": "Optional in v1; present when the adapter needs explicit ordering/output wiring hints beyond parent composition.",
|
||||
"items": {"$ref": "#/$defs/relationship"}
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"resource": {
|
||||
"type": "object",
|
||||
"required": ["id", "type", "module", "inputs"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z][a-z0-9-]*$",
|
||||
"description": "Local IR resource id (unique within the stack)."
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "IR-typed resource identifier (substrate-agnostic), e.g. 'aws:s3:bucket'. NOT a Terraform resource type ('aws_s3_bucket'); the adapter translates IR type -> substrate type."
|
||||
},
|
||||
"module": {
|
||||
"type": "string",
|
||||
"pattern": "^l1-[a-z][a-z0-9-]*@\\d+\\.\\d+\\.\\d+$",
|
||||
"description": "L1 registry reference: name@semver (W3.D). MAJOR bumps require a new registry entry (immutable publication); old entry enters a 12-month deprecation window."
|
||||
},
|
||||
"parent": {
|
||||
"type": "string",
|
||||
"description": "Parent resource id. Absent for the root. Single parent per child (ARCHITECTURE.md §12.1)."
|
||||
},
|
||||
"inputs": {
|
||||
"type": "object",
|
||||
"description": "Input values keyed by the L1 module's declared inputs. Free-form in v1 (validated at contract->IR resolution against the L1 registry); typed per-L1 in v1.2.",
|
||||
"additionalProperties": {"type": ["string", "number", "boolean"]}
|
||||
},
|
||||
"outputs": {
|
||||
"type": "object",
|
||||
"description": "Typed output contract. The adapter translates this to a substrate output block (e.g. Terraform output).",
|
||||
"additionalProperties": {"$ref": "#/$defs/outputSpec"}
|
||||
},
|
||||
"nfrs": {
|
||||
"type": "object",
|
||||
"description": "Declared non-functional requirements (latency, throughput, error rate). Opaque to the adapter; consumed by the confidence signal's NFR input.",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"outputSpec": {
|
||||
"type": "object",
|
||||
"required": ["type"],
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "IR-typed output type: a primitive ('string', 'arn') or a reference ('ref:<resourceId>.<outputName>')."
|
||||
},
|
||||
"description": {"type": "string"}
|
||||
}
|
||||
},
|
||||
"relationship": {
|
||||
"type": "object",
|
||||
"required": ["from", "to", "kind"],
|
||||
"properties": {
|
||||
"from": {"type": "string", "description": "Source resource id."},
|
||||
"to": {"type": "string", "description": "Target resource id."},
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"enum": ["parent", "depends_on", "uses_output"],
|
||||
"description": "v1 uses 'parent' (composition ordering) + 'uses_output' (interpolation). 'depends_on' is reserved for v2 explicit-dependency cases."
|
||||
},
|
||||
"shared_keyword": {
|
||||
"type": "string",
|
||||
"description": "Reserved for v2 multi-relationship dependencies. Unused in v1."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://acdl.cloudinit.dev/schemas/policy_check_result.schema.json",
|
||||
"title": "ACDL PolicyCheckResult",
|
||||
"description": "Normalized policy check result — the contract between policy engines and the confidence signal. Engine-specific adapters (checkov_adapter.py, future kyverno_adapter) translate native engine output to this shape. The confidence signal consumes a list of these as its policy input; it is engine-agnostic. The severity enum drives the severity->penalty mapping (critical hard-override, high -0.2, medium -0.05, low -0.01, info 0.0).",
|
||||
"$comment": "Canonical PolicyCheckResult (ARCHITECTURE.md §12.6). The confidence signal (platform/confidence_signal.py) consumes a list of these as its policy input; it is engine-agnostic. Adapters translate native output to this shape; the signal never reads engine-specific evidence.",
|
||||
"type": "object",
|
||||
"required": ["contractId", "evaluatedAt", "engine", "ruleId", "severity", "result", "message", "resourceRef"],
|
||||
"properties": {
|
||||
"contractId": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "The contract this check was evaluated against."
|
||||
},
|
||||
"evaluatedAt": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "ISO-8601 timestamp of evaluation."
|
||||
},
|
||||
"engine": {
|
||||
"type": "string",
|
||||
"enum": ["checkov", "kyverno", "opa"],
|
||||
"description": "Policy engine that produced this result."
|
||||
},
|
||||
"ruleId": {
|
||||
"type": "string",
|
||||
"description": "Rule identifier (e.g. CKV_AWS_24, KYVERNO_NO_PRIVILEGED, ACDL_TAG_NAMING)."
|
||||
},
|
||||
"severity": {
|
||||
"type": "string",
|
||||
"enum": ["critical", "high", "medium", "low", "info"],
|
||||
"description": "Severity drives the confidence signal's penalty mapping (ARCHITECTURE.md §8)."
|
||||
},
|
||||
"result": {
|
||||
"type": "string",
|
||||
"enum": ["pass", "fail", "skipped", "error"],
|
||||
"description": "Check outcome."
|
||||
},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"description": "Human-readable result message."
|
||||
},
|
||||
"evidence": {
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"description": "Engine-specific payload (file_path, resource, code_block, etc.). Opaque to the confidence signal; present for audit/debug."
|
||||
},
|
||||
"resourceRef": {
|
||||
"type": "string",
|
||||
"description": "IR-typed resource identifier (the resource this check evaluated)."
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+84
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env bash
|
||||
# scripts/verify_phase07.sh - Phase 07 architecture-v1-finalization gate.
|
||||
set -u
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
fail() { echo "FAIL: $*" >&2; exit 1; }
|
||||
ok() { echo "ok: $*"; }
|
||||
|
||||
# --- Check 1: all 9 deliverable files exist ---
|
||||
for f in docs/architecture-v1.0.md \
|
||||
schemas/ir.schema.json \
|
||||
schemas/policy_check_result.schema.json \
|
||||
schemas/contract.schema.json \
|
||||
platform/confidence_signal.py \
|
||||
platform/audit_ledger_design.md \
|
||||
platform/hitl_matrix_design.md \
|
||||
platform/separation_of_duties.py \
|
||||
adapters/terraform/policy/checkov_adapter.py; do
|
||||
[ -f "$f" ] || fail "missing $f"
|
||||
done
|
||||
ok "all 9 deliverable files exist"
|
||||
|
||||
# --- Check 2: 3 JSON Schemas are valid Draft 2020-12 ---
|
||||
# Run python from /tmp so the repo's `platform/` package does not shadow the
|
||||
# stdlib `platform` module (jsonschema imports uuid -> platform.system();
|
||||
# our platform/ shadows it when cwd is repo root and on sys.path[0]).
|
||||
check_schema() {
|
||||
( cd /tmp && python3 -c "
|
||||
import json, jsonschema
|
||||
s = json.load(open('$1'))
|
||||
jsonschema.Draft202012Validator.check_schema(s)
|
||||
" >/dev/null 2>&1 )
|
||||
}
|
||||
for s in "$ROOT/schemas/ir.schema.json" "$ROOT/schemas/policy_check_result.schema.json" "$ROOT/schemas/contract.schema.json"; do
|
||||
check_schema "$s" || fail "$(basename "$s") is not valid Draft 2020-12"
|
||||
done
|
||||
ok "3 JSON Schemas validate as Draft 2020-12"
|
||||
|
||||
# --- Check 3: 3 .py files py_compile ---
|
||||
for p in platform/confidence_signal.py platform/separation_of_duties.py adapters/terraform/policy/checkov_adapter.py; do
|
||||
python3 -m py_compile "$p" || fail "$p py_compile failed"
|
||||
done
|
||||
ok "3 .py files py_compile"
|
||||
|
||||
# --- Check 4: 3 .md design files non-empty ---
|
||||
for m in platform/audit_ledger_design.md platform/hitl_matrix_design.md docs/architecture-v1.0.md; do
|
||||
[ -s "$m" ] || fail "$m is empty"
|
||||
done
|
||||
ok "3 .md design files non-empty"
|
||||
|
||||
# --- Check 5: all 11 decision IDs + OpenTofu in PROJECT.md ---
|
||||
for id in W1.A W1.B W2.A W3.D W3.E BA.A BA.B BA.C BA.D BA.E BA.F; do
|
||||
grep -q "$id" .ciagent/PROJECT.md || fail "missing $id in PROJECT.md"
|
||||
done
|
||||
grep -qi "opentofu" .ciagent/PROJECT.md || fail "missing OpenTofu in PROJECT.md"
|
||||
ok "all 11 decision IDs + OpenTofu present in PROJECT.md"
|
||||
|
||||
# --- Check 6: docs/architecture-v1.0.md status is v1.0 ---
|
||||
grep -q "v1.0" docs/architecture-v1.0.md || fail "architecture-v1.0.md missing v1.0"
|
||||
ok "docs/architecture-v1.0.md status is v1.0"
|
||||
|
||||
# --- Check 7: D-040..D-044 present in PROJECT.md ---
|
||||
for d in D-040 D-041 D-042 D-043 D-044; do
|
||||
grep -q "$d" .ciagent/PROJECT.md || fail "missing $d in PROJECT.md"
|
||||
done
|
||||
ok "D-040..D-044 present in PROJECT.md"
|
||||
|
||||
# --- Check 8: spike contract validates against contract schema ---
|
||||
echo '{"stack":"l2-static-asset","environment":"dev","inputs":{"bucket_name":"x","region":"us-east-1"}}' > /tmp/spike-contract.json
|
||||
( cd /tmp && python3 -c "
|
||||
import json, jsonschema
|
||||
jsonschema.validate(json.load(open('/tmp/spike-contract.json')), json.load(open('$ROOT/schemas/contract.schema.json')))
|
||||
" ) || fail "spike contract does not validate against contract schema"
|
||||
ok "spike contract validates against contract schema"
|
||||
|
||||
# --- Check 9: minimal IR validates against IR schema ---
|
||||
echo '{"version":"1.0.0","stack":{"name":"l2-static-asset","kind":"l2","depth":1},"resources":[{"id":"s3","type":"aws:s3:bucket","module":"l1-s3@1.0.0","inputs":{"bucket_name":"x","region":"us-east-1"}}]}' > /tmp/spike-ir.json
|
||||
( cd /tmp && python3 -c "
|
||||
import json, jsonschema
|
||||
jsonschema.validate(json.load(open('/tmp/spike-ir.json')), json.load(open('$ROOT/schemas/ir.schema.json')))
|
||||
" ) || fail "minimal IR does not validate against IR schema"
|
||||
ok "minimal IR validates against IR schema"
|
||||
|
||||
echo "VERIFIED — Phase 07: architecture v1.0 finalized; 6 files authored + 11 decisions resolved"
|
||||
Reference in New Issue
Block a user