Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d6b192307a | |||
| 2ed2ca6bac | |||
| 4b8758404c | |||
| 35a336aba2 | |||
| d3aa960eb8 | |||
| e29319a720 | |||
| 7afaa34b60 | |||
| 622abe015b | |||
| 8437a51c6c | |||
| cc4c27c8ab | |||
| 798f430218 | |||
| e71539d681 |
+88
-1465
File diff suppressed because it is too large
Load Diff
@@ -123,7 +123,7 @@
|
||||
| REQ-22 | 07 | complete (v1.1.2) |
|
||||
| REQ-23 | 08 | complete (v1.1.3) |
|
||||
| REQ-24 | 09 | complete (v1.1.4) |
|
||||
| REQ-25 | 10 | pending |
|
||||
| REQ-25 | 10 | complete (v1.1.5) |
|
||||
| REQ-26 | 09 | complete (v1.1.4) |
|
||||
| REQ-27 | 10 | pending |
|
||||
| REQ-28 | 10 | pending |
|
||||
| REQ-27 | 10 | complete (v1.1.5) |
|
||||
| REQ-28 | 10 | complete (v1.1.5) |
|
||||
@@ -0,0 +1,230 @@
|
||||
# ACDL v1.1 Milestone — Multi-Persona Code Review
|
||||
|
||||
**Reviewer:** ci-code-reviewer (model: glm-5.2)
|
||||
**Scope:** v1.1 milestone — Phases 06–10 (tags v1.1.1..v1.1.5), diff `v1.1.0..HEAD`
|
||||
**Date:** 2026-07-21
|
||||
**Verdict:** **READY TO SHIP** — 0 P0, 1 P1 (carried-forward), 0 P2 new
|
||||
|
||||
---
|
||||
|
||||
## Lens 1 — Correctness
|
||||
|
||||
The schemas + Python modules + Terraform implement what the decisions +
|
||||
`ARCHITECTURE.md` committed. Spot-checks all pass.
|
||||
|
||||
### Findings
|
||||
|
||||
- **`schemas/ir.schema.json`** (REQ-17): resources / relationships / composition
|
||||
(max-depth-5) / policy hooks (via PolicyCheckResult consumer) all present per
|
||||
§12.1. Substrate-agnostic: `aws_s3_bucket` appears ONLY in `$comment` and
|
||||
`description` strings (which explain the IR→Terraform mapping); it does NOT
|
||||
appear in any constraining keyword (`enum`/`const`/`pattern`/`required`). The
|
||||
schema body uses IR types (`aws:s3:bucket`). **Correct.**
|
||||
- **`schemas/contract.schema.json`** (REQ-22, W3.E): per-env mandatory via `allOf`
|
||||
if/then — qa requires `validation.e2eSuite`+`validation.loadTest`; prod requires
|
||||
`runbook`+`dashboard`+`oncall`; dr requires `drDrillRef`. The `profile:agentic`
|
||||
conditional is `if: {required:[profile], profile:{const:agentic}}` →
|
||||
`then: {required:[naturalLanguageIntent]}` — this is the **fixed** form
|
||||
(requires `profile` to be present before checking `const`), not the Phase 07
|
||||
initial bug. Verified: prod-missing-runbook rejected; agentic-without-NLI
|
||||
rejected; qa-without-validation rejected; dr-without-drDrillRef rejected;
|
||||
dev + agentic-with-NLI accepted. **Correct.**
|
||||
- **`acdl_platform/confidence_signal.py`** (REQ-19, D-040): `WEIGHTS` sum to
|
||||
1.0 (verified: 0.30+0.25+0.10+0.15+0.10+0.10 = 1.0). `PENALTY["critical"] = None`
|
||||
(hard-override sentinel). The critical-override short-circuit
|
||||
(`if p is None: return Signal(0.0, "block", ...)`) returns BEFORE the
|
||||
`score = max(0.0, min(1.0, base - penalty))` clamp. Dev-warn→block flip present
|
||||
(`if environment == "dev" and band == "warn": band = "block"`). The `policy`
|
||||
input key is read as `inputs.get("policy")` (not `policy_results`) — matches the
|
||||
Phase 10 e2e `run_spike_e2e.sh` which passes `inputs = {"policy": pcr, ...}`.
|
||||
Adversarial test: a critical-fail PCR → `score=0.0 band=block reasons=['CRITICAL_OVERRIDE:...']`.
|
||||
**Correct.**
|
||||
- **`acdl_platform/contract_resolver.py`** (REQ-27): `resolve()` loads YAML →
|
||||
validates against `contract.schema.json` → looks up L2 in registry → loads
|
||||
`composition.json` → maps wires → emits IR → validates against `ir.schema.json`.
|
||||
Wire mapping verified: `contract.inputs.bucket_name` →
|
||||
`child.inputs.bucket_name` via `wires.bucket_name.{target:s3, input:bucket_name}`.
|
||||
Resolved spike IR has `resources[0].inputs = {bucket_name: acdl-spike-bucket,
|
||||
region: us-east-1}`. Prod-missing-runbook raises `jsonschema.ValidationError`
|
||||
(not a generic ValueError). **Correct.**
|
||||
- **`acdl_platform/outbox_writer.py`** (D-044, D-P10-3): SHA-256 over canonical
|
||||
JSON (`sort_keys=True, separators=(",", ":")`). `prev_event_hash` defaults to
|
||||
`"GENESIS"`. DynamoDB item shape: PK `contractId` (S), SK
|
||||
`eventType#eventTs` (S), TTL `expire_at` (N, now+365d). Append-only
|
||||
(`put_item` only; 0 `delete_item`/`update_item`). **Correct.**
|
||||
- **`adapters/terraform/adapter.py`** (REQ-26, D-P10-1): `TYPE_MAP =
|
||||
{aws:s3:bucket -> aws_s3_bucket}`. Backend key derived from stack name:
|
||||
`spike/l2-static-asset/terraform.tfstate` (verified). Unknown IR type raises
|
||||
`ValueError`. Resources array handling is shape-driven (iterates
|
||||
`ir_instance["resources"]`; works for both l1 and l2 IR). **Correct.**
|
||||
- **`adapters/terraform/policy/checkov_adapter.py`** (REQ-18, D-043): `RULE_MAP`
|
||||
has exactly 11 Checkov rule IDs (CKV_AWS_41/45/46/20/57/24/25/1/40/7/33). The
|
||||
`ACDL_TAG_NAMING` SKIPPED record is appended (severity: info, result:
|
||||
skipped). Tolerates both Checkov JSON shapes — nested
|
||||
`{framework: {results: {...}}}` and legacy `{framework: {passed_checks:...}}`
|
||||
(the `results = body.get("results", body)` fallback). **Correct.**
|
||||
|
||||
### Verdict: PASS — no issues.
|
||||
|
||||
---
|
||||
|
||||
## Lens 2 — Testing
|
||||
|
||||
The verify scripts are real gates that fail on regression, not presence checks.
|
||||
|
||||
### Findings
|
||||
|
||||
- **`scripts/verify_phase07.sh`**: Check 2 uses
|
||||
`jsonschema.Draft202012Validator.check_schema(...) || fail` — actually
|
||||
validates the 3 schemas as Draft 2020-12 (fails if a schema is broken).
|
||||
Check 8 cross-checks the spike contract against `contract.schema.json` via
|
||||
`jsonschema.validate(...) || fail`. Check 9 cross-checks a minimal IR against
|
||||
`ir.schema.json`. Every check has `|| fail`. **Real gate.**
|
||||
- **`scripts/verify_phase10.sh`**: 8 checks, each with `|| fail`. Check (h) is the
|
||||
REQ-28 substrate-agnostic scan. **Synthetic leak test performed:** appended
|
||||
`LEAK = "aws_s3_bucket"` to `acdl_platform/separation_of_duties.py` and ran the
|
||||
Check (h) grep — it caught the leak (`acdl_platform/separation_of_duties.py:44:
|
||||
LEAK = "aws_s3_bucket"`), then reverted. The check also scans `modules-ir/`
|
||||
JSON for `aws_*` resource-type VALUES (excluding `description`/`$comment`
|
||||
strings). **Real gate.**
|
||||
- **`scripts/run_spike_e2e.sh`** + **`scripts/run_spike_plan.sh`**: touch real AWS
|
||||
— `terraform init/validate/plan -lock=false` + `checkov` + DynamoDB
|
||||
`put_item`/`query`. NOT stubbed (the spike key is loaded from gitignored
|
||||
`.env.secrets`). The e2e runner uses `|| fail` on every step, so a DynamoDB
|
||||
outage or terraform failure exits 1 (verified: outbox write failure propagates
|
||||
via `|| fail "outbox write failed"`). **Real e2e.**
|
||||
|
||||
### Verdict: PASS — no issues.
|
||||
|
||||
---
|
||||
|
||||
## Lens 3 — Security
|
||||
|
||||
AWS key handling (D-034/D-039), IAM least-privilege, gitignore discipline, no
|
||||
secrets in commits. All clean.
|
||||
|
||||
### Findings
|
||||
|
||||
- **No leaked key IDs in executable code:**
|
||||
`git log v1.1.0..HEAD -p | grep -iE "AKIA[A-Z0-9]{16}" | grep -v "^#"` returns
|
||||
matches ONLY inside `.ciagent/VERIFY.md` (the Phase 09 narrative — the
|
||||
carried-forward P1-1). No `.py`, `.tf`, `.json`, `.yaml`, or `.sh` file
|
||||
contains an `AKIA…` key ID. **Clean.**
|
||||
- **No leaked secret keys:**
|
||||
`git log v1.1.0..HEAD -p | grep -iE "aws_secret_access_key.*=.*[A-Za-z0-9/+=]{40}" | grep -v "^#"`
|
||||
returns nothing. **Clean.**
|
||||
- **`terraform/bootstrap/spike_runner_policy.json`** (REQ-23): least-privilege.
|
||||
Allow actions: `s3:{PutObject,GetObject,DeleteObject,ListBucket,GetBucketLocation,GetBucketVersioning}`
|
||||
+ `dynamodb:{GetItem,PutItem,DeleteItem,UpdateItem,Query,Scan,DescribeTable}`
|
||||
+ `sts:GetCallerIdentity`. **No** `iam:*`, **no** `ec2:*`, **no**
|
||||
`s3:CreateBucket`, **no** `s3:DeleteBucket`, **no** `terraform apply`
|
||||
(apply is out of spike scope). `DenyEverythingElse` `NotResource` lists exactly
|
||||
3 ARNs (state bucket + bucket objects + outbox table); everything else is
|
||||
denied. **Correct.**
|
||||
- **Gitignore discipline:** `.env.secrets`, `terraform/bootstrap/.bootstrap_state.json`,
|
||||
`terraform/spike/.terraform/`, `terraform/spike/.terraform.lock.hcl`,
|
||||
`terraform/spike/tfplan`, `terraform/spike/*.tfstate*` all gitignored
|
||||
(`git check-ignore` confirms each). **Correct.**
|
||||
- **Outbox write is append-only:** `grep -c "delete_item|update_item"
|
||||
outbox_writer.py` = 0 (only `put_item`). **Correct.**
|
||||
- **E2E runner is plan-only:** `grep -c "terraform apply" run_spike_e2e.sh` = 0
|
||||
(only `init + validate + plan`). **Correct.**
|
||||
|
||||
### P1 (carried-forward, NOT auto-fixed)
|
||||
|
||||
- **P1-1:** The `.ciagent/VERIFY.md` Phase 09 narrative contains two AWS access
|
||||
key IDs — `AKIAYOZHMKZ7RK26N66W` (the rotated spike key id) and
|
||||
`AKIAYOZHMKZ772SINHFX` (the deactivated root key id). Confirmed still present
|
||||
(`grep -c` returns 2). These are **public identifiers, not secret pairs**;
|
||||
they live in the `.ciagent/` audit narrative, not in any executable code
|
||||
path. Recommended for a future hygiene redaction pass (replace with
|
||||
`AKIA…SPIKE` / `AKIA…ROOT-DEACTIVATED` placeholders). **Non-blocking for v1.2
|
||||
ship; flagged for post-hoc review.**
|
||||
|
||||
### Verdict: PASS — 1 carried-forward P1 (non-blocking).
|
||||
|
||||
---
|
||||
|
||||
## Lens 4 — Performance
|
||||
|
||||
Not a concern for the spike (plan-only, single resource, no load). **Skipped.**
|
||||
|
||||
---
|
||||
|
||||
## Lens 5 — Maintainability
|
||||
|
||||
The `acdl_platform/` rename, substrate-agnostic boundary, and decision trail
|
||||
are all consistent.
|
||||
|
||||
### Findings
|
||||
|
||||
- **`acdl_platform/` rename (Phase 08 prep, fixing the stdlib `platform`
|
||||
shadow):** consistently applied across `scripts/verify_phase06.sh`,
|
||||
`scripts/verify_phase07.sh`, `README.md`, and the Python imports
|
||||
(`import acdl_platform.confidence_signal as c` in `run_spike_e2e.sh`).
|
||||
`grep -l acdl_platform` confirms all three files reference the renamed dir.
|
||||
**Consistent.**
|
||||
- **Decision trail:** every schema/module cites its source. Sampled 3 files:
|
||||
- `acdl_platform/confidence_signal.py` cites `REQ-19`, `D-040`,
|
||||
`ARCHITECTURE.md §8`.
|
||||
- `acdl_platform/contract_resolver.py` cites `ARCHITECTURE.md §12.8`.
|
||||
- `schemas/ir.schema.json` cites `ARCHITECTURE.md §12.1`, `§3`, `W3.D`.
|
||||
**Citations present.**
|
||||
- **Spike-vs-v1.2 boundary** documented in each design doc:
|
||||
`acdl_platform/audit_ledger_design.md`, `acdl_platform/hitl_matrix_design.md`,
|
||||
and `.ciagent/PLAN.md` all reference `v1.2`. **Boundary documented.**
|
||||
- **Substrate-agnostic boundary (REQ-28):** the adapter is the only
|
||||
substrate-specific code. `acdl_platform/` Python is clean (verified by the
|
||||
Check (h) grep + the synthetic leak test). `modules-ir/` JSON data files
|
||||
contain only IR types (`aws:s3:bucket`); `aws_s3_bucket` appears only in
|
||||
`description`/`$comment` strings that explain the mapping. **Boundary holds.**
|
||||
|
||||
### Verdict: PASS — no issues.
|
||||
|
||||
---
|
||||
|
||||
## Lens 6 — Adversarial
|
||||
|
||||
Tried to break the spike. All failure modes handled correctly.
|
||||
|
||||
### Findings
|
||||
|
||||
- **`contracts/spike.yaml` with `environment: prod` (missing runbook):** the
|
||||
contract schema rejects it via the `allOf` if/then (`runbook` is a required
|
||||
property when `environment == "prod"`). `contract_resolver.py` raises
|
||||
`jsonschema.ValidationError` (not a generic ValueError). **Handled.**
|
||||
- **IR instance with a resource type not in `TYPE_MAP` (e.g.
|
||||
`aws:ec2:instance`):** the adapter raises
|
||||
`ValueError("unknown IR type 'aws:ec2:instance' (adapter spike handles
|
||||
aws:s3:bucket only)")`. **Handled.**
|
||||
- **Confidence signal gets a critical-fail `PolicyCheckResult`:** hard-overrides
|
||||
to `score=0.0`, `band=block`, `reasonCodes=['CRITICAL_OVERRIDE:...']`. The
|
||||
short-circuit returns BEFORE the score clamp. **Handled.**
|
||||
- **Outbox write fails (DynamoDB unreachable):** `outbox_writer.py` raises
|
||||
(boto3 `put_item` propagates the exception); `run_spike_e2e.sh` line 93 uses
|
||||
`|| fail "outbox write failed"` → exit 1. **Handled (no silent success).**
|
||||
- **Missing confidence input (e.g. `nfrs` absent):** `compute()` returns
|
||||
`Signal(0.0, "block", {}, ["INPUT_MISSING:nfrs"])`. **Handled.**
|
||||
|
||||
### Verdict: PASS — no issues.
|
||||
|
||||
---
|
||||
|
||||
## P0 / P1 / P2 Summary
|
||||
|
||||
| Severity | Count | Action |
|
||||
|-----------|-------|--------|
|
||||
| **P0** | 0 | none (no auto-fix needed) |
|
||||
| **P1** | 1 | P1-1 (carried-forward): two AWS access key IDs in `.ciagent/VERIFY.md` Phase 09 narrative — flagged for post-hoc hygiene redaction; non-blocking |
|
||||
| **P2** | 0 | none |
|
||||
|
||||
---
|
||||
|
||||
## Milestone verdict
|
||||
|
||||
**v1.1 milestone: READY TO SHIP**
|
||||
|
||||
- 0 P0 issues (no blocking fixes).
|
||||
- 1 P1 carried-forward (non-blocking; flagged for post-hoc review).
|
||||
- All 5 lenses pass. REQ-16..28 satisfied. The IR commitments hold (REQ-28).
|
||||
- Ready for the COMPLETE gate → ship `v1.2.0` → audit.
|
||||
+6
-4
@@ -71,12 +71,14 @@ phase produced a runnable increment and ended with a phase-completion commit
|
||||
|
||||
---
|
||||
|
||||
## v1.1 (Active — architecture finalization + v1 spike)
|
||||
## v1.1 (Complete — architecture finalization + v1 spike, 2026-07-21)
|
||||
|
||||
Five-phase breakdown to finalize the architecture to v1.0 and prove the
|
||||
locked commitments with one end-to-end implementation spike. Milestone
|
||||
`v1.1-spike` covers the real platform's first materialization. Ship tag at
|
||||
milestone COMPLETE: `v1.2.0` (feature milestone, next minor per ship.md).
|
||||
`v1.1-spike` covers the real platform's first materialization. Ship tag
|
||||
at milestone COMPLETE: **`v1.2.0`** (feature milestone, next minor per
|
||||
ship.md). **Status: COMPLETE — all 5 phases shipped (v1.1.1..v1.1.5) +
|
||||
verified; review READY TO SHIP (0 P0); audit pending.**
|
||||
|
||||
### Phase 06 — archive-demo-and-reorient
|
||||
- **Description:** Move the v1.0 demo (`modules/`, `scripts/`, `evidence-ui/`, `contracts/`, demo `.gitea/workflows/`) to `demo/`. Establish the new repo layout (`platform/`, `schemas/`, `adapters/`, `terraform/`, `modules-ir/`). Rewrite README to reflect the real platform. Verify the demo still runs from `demo/` (regression check).
|
||||
@@ -122,7 +124,7 @@ milestone COMPLETE: `v1.2.0` (feature milestone, next minor per ship.md).
|
||||
|
||||
### Phase 10 — v1-spike-l2-and-contract-e2e
|
||||
- **Description:** Implement `l2-static-asset` (thin-composition referencing `l1-s3`), the contract schema + contract→IR resolution, and one end-to-end contract submission (`contracts/spike.yaml` for `l2-static-asset`) flowing through schema validation → IR resolution → `terraform plan` → Checkov `PolicyCheckResult` → confidence signal → evidence event to the DynamoDB outbox. Verify the IR commitments hold (no polyglot mess).
|
||||
- **Status:** pending
|
||||
- **Status:** complete (v1.1.5)
|
||||
- **Depends on:** [09]
|
||||
- **Requirements:** REQ-25, REQ-27, REQ-28
|
||||
- **Success Criteria:**
|
||||
|
||||
+118
-329
@@ -1,395 +1,184 @@
|
||||
# Phase 08 — aws-bootstrap VERIFICATION
|
||||
# Phase 10 — v1-spike-l2-and-contract-e2e (v1.1) VERIFY
|
||||
|
||||
- **Phase:** 08 (aws-bootstrap)
|
||||
- **Milestone:** v1.1 (feature)
|
||||
- **Tag:** v1.1.3
|
||||
- **Verifier:** ci-verifier (glm-5.2)
|
||||
- **Date:** 2026-07-21
|
||||
- **Verdict:** **VERIFIED** (2 P1 flags for post-hoc review; D-034 manual attestation required)
|
||||
**Verdict: Phase 10: VERIFIED**
|
||||
**Tag: v1.1.5** — milestone capstone shipped.
|
||||
|
||||
The phase goal is genuinely achieved. The end-to-end spike pipeline runs
|
||||
green against real AWS, and REQ-28 (the binding spike claim — the IR
|
||||
commitments hold, the adapter is the only substrate-specific code) is
|
||||
proven by `scripts/verify_phase10.sh` Check (h).
|
||||
|
||||
---
|
||||
|
||||
## Layer 1 — Structural: PASS
|
||||
|
||||
### 1.1 Deliverable files exist (7/7)
|
||||
### Deliverable files (9 + regenerated TF)
|
||||
|
||||
```
|
||||
terraform/bootstrap/spike_runner_policy.json (1310 B)
|
||||
terraform/bootstrap/create_state_backend.py (3074 B)
|
||||
terraform/bootstrap/create_iam_user.py (2645 B)
|
||||
scripts/rotate_spike_key.sh (3856 B)
|
||||
scripts/verify_phase08.sh (3550 B)
|
||||
terraform/bootstrap/README.md (3035 B)
|
||||
.gitignore (edited, +2 lines)
|
||||
```
|
||||
All 9 Phase 10 deliverable files exist; the regenerated TF is present:
|
||||
|
||||
All 7 present (`ls -la` confirmed). Plus `terraform/bootstrap/__init__.py` + `.gitkeep` guards from Wave 1/2.
|
||||
| File | Exists | Notes |
|
||||
|------|--------|-------|
|
||||
| `modules-ir/l2/l2-static-asset/composition.json` | ✅ | kind=l2, depth=1, one child `l1-s3@1.0.0`, wires passthrough (`bucket_name`→s3.bucket_name, `region`→s3.region) |
|
||||
| `modules-ir/l2/l2-static-asset/README.md` | ✅ | D-P10-1 doc; references l1-s3 only; internally consistent |
|
||||
| `modules-ir/registry.json` | ✅ | both `l1-s3@1.0.0` + `l2-static-asset@1.0.0` entries present |
|
||||
| `contracts/spike.yaml` | ✅ | valid YAML; stack=l2-static-asset, environment=dev, inputs bucket_name=acdl-spike-bucket, region=us-east-1 |
|
||||
| `acdl_platform/contract_resolver.py` | ✅ | `resolve()` + `__main__` CLI; loads YAML → validates contract schema → looks up L2 → loads composition → maps wires → emits IR → validates IR schema |
|
||||
| `adapters/terraform/adapter.py` | ✅ | D-P10-1: backend key derived from stack name (`spike/<stack_name>/terraform.tfstate`); handles both l1 + l2 IR (resources array is the same shape) |
|
||||
| `acdl_platform/outbox_writer.py` | ✅ | `write_event()` + `__main__` CLI; SHA-256 canonical JSON hash; GENESIS chain; TTL expire_at; single `put_item` (append-only) |
|
||||
| `scripts/run_spike_e2e.sh` | ✅ | 8-step orchestrator; bash -n passes |
|
||||
| `scripts/verify_phase10.sh` | ✅ | 8-check gate; bash -n passes |
|
||||
| `terraform/spike/main.tf` | ✅ | `resource "aws_s3_bucket" "s3"` + versioning + bucket_arn/bucket_name outputs (regenerated by adapter) |
|
||||
| `terraform/spike/terraform.tf` | ✅ | `key = "spike/l2-static-asset/terraform.tfstate"` — derived from stack name per D-P10-1 |
|
||||
| `terraform/spike/providers.tf` | ✅ | aws provider, region=us-east-1 |
|
||||
|
||||
### 1.2 spike_runner_policy.json — valid IAM policy
|
||||
|
||||
`python3 -c "import json; json.load(open(...))"` parses. Structure:
|
||||
|
||||
- `Version: "2012-10-17"` ✓
|
||||
- 4 statements with Sids: `SpikeStateBucketReadWrite`, `SpikeOutboxTableReadWrite`,
|
||||
`SpikeStsSelfIdentify`, `DenyEverythingElse` ✓ (matches the spec)
|
||||
- `DenyEverythingElse`: `Effect: "Deny"`, `Action: "*"`, `NotResource` = the 3 ARNs
|
||||
(state bucket, state bucket objects, outbox table) ✓
|
||||
- S3 Allow grants only object ops + `ListBucket` + `GetBucketLocation` + `GetBucketVersioning`
|
||||
— no `CreateBucket`/`DeleteBucket` ✓
|
||||
- DynamoDB Allow grants only item ops + `Query`/`Scan`/`DescribeTable`
|
||||
— no `dynamodb:CreateTable`/`DeleteTable` ✓
|
||||
- STS Allow grants only `GetCallerIdentity` (Resource `*`, required by AWS) ✓
|
||||
- No `terraform`, `iam:`, or `ec2:` actions in any Allow statement ✓
|
||||
- Account id `581513795199` concrete in all ARNs ✓
|
||||
- Bucket name `acdl-tfstate-581513795199-us-east-1` matches the operational template ✓
|
||||
- DynamoDB table ARN ends with `table/acdl-outbox` (D-P08-1 consolidated) ✓
|
||||
|
||||
Least-privilege confirmed: the Deny's `NotResource` lists exactly the 3 granted ARNs,
|
||||
so everything else (every other S3 bucket, every other DynamoDB table, every other
|
||||
service) is denied.
|
||||
|
||||
### 1.3 Typecheck gate
|
||||
|
||||
```
|
||||
python3 -m py_compile terraform/bootstrap/create_state_backend.py terraform/bootstrap/create_iam_user.py → PYCOMPILE_OK
|
||||
bash -n scripts/rotate_spike_key.sh scripts/verify_phase08.sh → BASHN_OK
|
||||
```
|
||||
|
||||
### 1.4 .gitignore
|
||||
|
||||
```
|
||||
11:.env.secrets
|
||||
12:terraform/bootstrap/.bootstrap_state.json
|
||||
```
|
||||
Both present. `git check-ignore` exits 0 for both.
|
||||
|
||||
### 1.5 terraform/bootstrap/README.md
|
||||
|
||||
- 6 numbered steps (set env → create_state_backend → create_iam_user → rotate → verify → MANUAL D-034) ✓
|
||||
- Step 6 marked **MANUAL — D-034 closure** (root key rotation in AWS console, user does it) ✓
|
||||
- "Spike scope vs v1.2 boundary" table present (4 rows: AWS auth, IAM, state backend, secret storage) ✓
|
||||
- Table matches PROJECT.md D-039 (per-run-rotated long-lived key; OIDC deferred to v1.2,
|
||||
blocked on go-gitea/gitea#36988) + ARCHITECTURE.md §12.5 (long-lived creds forbidden;
|
||||
D-039 waiver for the spike) ✓
|
||||
|
||||
### 1.6 Tags
|
||||
|
||||
```
|
||||
v1.1.0 v1.1.1 v1.1.2 v1.1.3
|
||||
```
|
||||
All four present; v1.1.3 is the Phase 08 ship tag.
|
||||
|
||||
### 1.7 Runtime artifacts (gitignored)
|
||||
|
||||
```
|
||||
.env.secrets -rw------- (600) 141 B ← rotated spike key
|
||||
terraform/bootstrap/.bootstrap_state.json -rw-r--r-- (644) 186 B ← bootstrap marker
|
||||
```
|
||||
|
||||
`.bootstrap_state.json` contents:
|
||||
```json
|
||||
{
|
||||
"account_id": "581513795199",
|
||||
"bucket_name": "acdl-tfstate-581513795199-us-east-1",
|
||||
"table_name": "acdl-outbox",
|
||||
"region": "us-east-1",
|
||||
"created_at": "2026-07-21T19:00:35Z"
|
||||
}
|
||||
```
|
||||
All 5 must-have keys present (account_id, bucket_name, table_name, region, created_at).
|
||||
No secrets in the marker (it is bookkeeping only).
|
||||
|
||||
### 1.8 History preservation
|
||||
|
||||
```
|
||||
git log --follow terraform/bootstrap/create_state_backend.py
|
||||
f8ddd8b phase: 8, status: plan-as-execute, persona: security-engineer+platform-engineer, task: T-8.1..T-8.4
|
||||
```
|
||||
Creation point is the T-8.2/8.3 Phase 08 commit; history intact.
|
||||
### Tags + .gitignore
|
||||
- Tags `v1.1.0`..`v1.1.5` all present.
|
||||
- `.gitignore` line 14: `terraform/spike/.terraform.lock.hcl` (P1-2 fix from P10 prep 798f430).
|
||||
|
||||
---
|
||||
|
||||
## Layer 2 — Behavioral: PASS
|
||||
|
||||
### 2.1 verify_phase08.sh — exit 0 + VERIFIED line
|
||||
### Gate re-run (real AWS)
|
||||
|
||||
```
|
||||
$ bash scripts/verify_phase08.sh
|
||||
ok: .env.secrets + .bootstrap_state.json are gitignored
|
||||
ok: caller identity is acdl-spike-runner (NOT root)
|
||||
ok: S3 state bucket exists
|
||||
ok: DynamoDB outbox table exists
|
||||
ok: IAM check skipped (ACDL_BOOTSTRAP_AWS_* not set; the spike key is least-privilege and cannot iam:GetUser — that itself confirms the policy denies non-granted actions)
|
||||
VERIFIED — Phase 08: AWS bootstrap complete; spike key rotated; D-034 closed (user must rotate the root key manually now)
|
||||
$ bash scripts/verify_phase10.sh
|
||||
ok: composition.json: l2-static-asset references l1-s3 only (depth 1)
|
||||
ok: contracts/spike.yaml validates against the contract schema
|
||||
ok: contract_resolver.py resolves spike.yaml to an IR-schema-valid instance
|
||||
ok: adapter.py compiles L2 IR to terraform with aws_s3_bucket
|
||||
ok: run_spike_e2e.sh completes the full pipeline end-to-end
|
||||
ok: confidence band is pass for dev
|
||||
ok: evidence event is written to the DynamoDB outbox
|
||||
ok: REQ-28: adapter is the only substrate-specific code; modules-ir/ + acdl_platform/ are substrate-agnostic (docs/comments excluded)
|
||||
VERIFIED — Phase 10: L2 + contract-e2e; IR commitments hold (REQ-28)
|
||||
EXIT=0
|
||||
```
|
||||
|
||||
The spike key (loaded from `.env.secrets`) successfully authenticated to STS
|
||||
(caller = `arn:aws:iam::581513795199:user/acdl-spike-runner`, NOT root), called
|
||||
`s3:head_bucket` on the state bucket, and `dynamodb:describe_table` on the outbox
|
||||
table. The IAM `get_user`/`get_user_policy` check was gracefully skipped because
|
||||
the bootstrap root key was not present in the verifier's env — and that skip is
|
||||
itself evidence the least-privilege policy works: the spike key *cannot* call
|
||||
`iam:GetUser`, exactly as the scoped policy intends. (The orchestrator's Wave 5
|
||||
run already verified the IAM user + Deny statement via the root key; that
|
||||
assertion is recorded in the phase execution log.)
|
||||
All 8 checks green against live AWS.
|
||||
|
||||
### 2.2 Typecheck re-run
|
||||
### Typecheck
|
||||
`python3 -m py_compile acdl_platform/contract_resolver.py acdl_platform/outbox_writer.py adapters/terraform/adapter.py && bash -n scripts/run_spike_e2e.sh scripts/verify_phase10.sh` → **TYPECHECK OK**.
|
||||
|
||||
```
|
||||
python3 -m py_compile terraform/bootstrap/create_state_backend.py terraform/bootstrap/create_iam_user.py
|
||||
bash -n scripts/rotate_spike_key.sh scripts/verify_phase08.sh
|
||||
→ all pass (see 1.3)
|
||||
```
|
||||
### Resolver cross-check
|
||||
`python3 acdl_platform/contract_resolver.py contracts/spike.yaml /tmp/p10_ir.json` → emits an IR instance that **validates against `schemas/ir.schema.json`**. Stack `{name: l2-static-asset, kind: l2, depth: 1}`, 1 resource `s3` (type `aws:s3:bucket`, module `l1-s3@1.0.0`), 1 relationship (root→s3, parent).
|
||||
|
||||
### 2.3 S3 bucket versioning (live AWS check)
|
||||
### Adapter cross-check
|
||||
Running the adapter against the resolved L2 IR emits `main.tf` with `resource "aws_s3_bucket" "s3"` + the backend key `spike/l2-static-asset/terraform.tfstate` (derived from the stack name per D-P10-1). ✅
|
||||
|
||||
```
|
||||
$ python3 -c "import boto3; s=boto3.Session(region_name='us-east-1').client('s3'); print(s.get_bucket_versioning(Bucket='acdl-tfstate-581513795199-us-east-1'))"
|
||||
{..., 'Status': 'Enabled'}
|
||||
```
|
||||
Versioning confirmed enabled on the state bucket (state-file safety, ARCHITECTURE.md §12.3).
|
||||
### E2E pipeline (Wave 5, real AWS)
|
||||
`run_spike_e2e.sh` exits 0 and prints:
|
||||
- `terraform plan OK (1 to add, 0 to change, 0 to destroy expected)` — real AWS plan succeeds.
|
||||
- `checkov: 6 failed, 5 passed` → `PolicyCheckResult: 12 record(s)` (incl. ACDL_TAG_NAMING SKIPPED per D-043).
|
||||
- `confidence: score=0.8 band=pass` (dev threshold ≥ 0.50).
|
||||
- `outbox: contractId= 11111111-... hash= 6e4711b9...` — DynamoDB `put_item` to `acdl-outbox`.
|
||||
- Final line: `=== SPIKE E2E OK ===`.
|
||||
|
||||
### 2.4 DynamoDB table shape (live AWS check)
|
||||
|
||||
```
|
||||
BillingMode: PAY_PER_REQUEST
|
||||
KeySchema: [{'AttributeName': 'contractId', 'KeyType': 'HASH'},
|
||||
{'AttributeName': 'eventType#eventTs', 'KeyType': 'RANGE'}]
|
||||
```
|
||||
Matches D-044 (PAY_PER_REQUEST, PK `contractId`, SK `eventType#eventTs`).
|
||||
|
||||
Note: `dynamodb:DescribeTimeToLive` returned `AccessDenied` for the spike key —
|
||||
this is **correct least-privilege behavior** (the policy grants only item ops +
|
||||
Query/Scan/DescribeTable, not `DescribeTimeToLive`). See P1 flag #1 below re: TTL
|
||||
enablement.
|
||||
|
||||
### 2.5 Rotation idempotency (second run)
|
||||
|
||||
The verifier's env did not carry the bootstrap root key
|
||||
(`ACDL_BOOTSTRAP_AWS_*`), so a second `bash scripts/rotate_spike_key.sh` could
|
||||
not be executed live by the verifier. **However**: the orchestrator's Wave 5
|
||||
already ran the rotation once (deactivating the initial key + creating the
|
||||
current `AKIAYOZHMKZ7RK26N66W`); the script's logic is sound (create-new →
|
||||
deactivate-old → delete-old → exactly 1 active key), and the live
|
||||
`verify_phase08.sh` PASS confirms the currently-rotated key authenticates as
|
||||
`acdl-spike-runner`. The idempotency invariant (exactly 1 active key) is
|
||||
enforced by the script's create-then-delete ordering. Re-rotation is a Phase
|
||||
09/10 pre-run step, not a Phase 08 verify gate.
|
||||
Outbox query (`verify_phase10.sh` Check g): `Count=4` (spike has been run multiple times; new events accumulate — append-only outbox, RPO=0, GENESIS chain for each).
|
||||
|
||||
---
|
||||
|
||||
## Layer 3 — Security: PASS
|
||||
|
||||
### 3.1 No secrets committed
|
||||
### No credentials committed in v1.1.4..v1.1.5
|
||||
|
||||
Files touched in `v1.1.2..v1.1.3`:
|
||||
```
|
||||
.gitignore
|
||||
.ciagent/PLAN.md
|
||||
.ciagent/REQUIREMENTS.md
|
||||
.ciagent/ROADMAP.md
|
||||
.ciagent/VERIFY.md (Phase 07)
|
||||
README.md
|
||||
acdl_platform/* (rename)
|
||||
scripts/rotate_spike_key.sh
|
||||
scripts/verify_phase06.sh scripts/verify_phase07.sh
|
||||
scripts/verify_phase08.sh
|
||||
terraform/bootstrap/README.md
|
||||
terraform/bootstrap/create_iam_user.py
|
||||
terraform/bootstrap/create_state_backend.py
|
||||
terraform/bootstrap/spike_runner_policy.json
|
||||
```
|
||||
No `.env*`, no `*.tfstate`, no `*_key*`, no `credentials`, no `.bootstrap_state.json`
|
||||
(it is gitignored, not committed).
|
||||
`git log v1.1.4..v1.1.5 --name-only` shows only:
|
||||
- `.ciagent/PLAN.md`, `.ciagent/REQUIREMENTS.md`, `.ciagent/ROADMAP.md`, `.ciagent/VERIFY.md` (P09 narrative)
|
||||
- `.gitignore` (P1-2 fix)
|
||||
- the 9 Phase 10 deliverable files
|
||||
- `terraform/spike/terraform.tf` (regenerated backend config — bucket name is the state bucket ARN, not a credential)
|
||||
|
||||
### 3.2 No leaked key values in diffs
|
||||
No `.env*`, no `*.tfstate`, no `*_key*`, no `tfplan`, no `.terraform.lock.hcl` (the latter is gitignored via line 14).
|
||||
|
||||
```
|
||||
$ git log v1.1.2..v1.1.3 -p | grep -oE "AKIA[A-Z0-9]{16}"
|
||||
(nothing)
|
||||
$ git log v1.1.2..v1.1.3 -p | grep -oE "(SecretAccessKey|secret_access_key)['\"]?\s*[:=]\s*['\"]?[A-Za-z0-9/+=]{40}"
|
||||
(nothing)
|
||||
```
|
||||
The broader grep for `AKIA|aws_secret_access_key|access_key_id` returns lines, but
|
||||
**all are env-var-name references or placeholder text** (`ACDL_AWS_ACCESS_KEY_ID`,
|
||||
`<root secret>`, `<...>`, `os.environ["..."]`) — **zero actual secret values**.
|
||||
Confirmed: no AKIA key id, no 40-char secret string appears in any commit diff or
|
||||
message.
|
||||
### AKIA scan
|
||||
`git log v1.1.4..v1.1.5 -p | grep -iE "AKIA[A-Z0-9]{16}" | grep -v "^#"` returns matches only inside `.ciagent/VERIFY.md` (the **Phase 09** verify narrative — `AKIAYOZHMKZ7RK26N66W` the rotated spike key id and `AKIAYOZHMKZ772SINHFX` the deactivated root key id, both already flagged as P1-1 in the P09 verify). These are **access key IDs (public identifiers), not secret access key pairs** — and they live in the `.ciagent/` audit narrative, not in any executable code path. None of the Phase 10 deliverable files (9 files + regenerated TF) contain any `AKIA…` or `aws_secret…` string.
|
||||
|
||||
### 3.3 Root key id not tracked
|
||||
**P10-specific AKIA check:** scanned the 9 deliverable files + regenerated TF for `AKIA[A-Z0-9]{16}` and `aws_secret_access_key`/`secret_key` — **no matches**. Clean.
|
||||
|
||||
```
|
||||
$ git grep -I "AKIAYOZHMKZ772SINHFX"
|
||||
(nothing — ROOT_KEY_ID_NOT_TRACKED)
|
||||
```
|
||||
The bootstrap root key id appears in no tracked file.
|
||||
### .env.secrets
|
||||
- `git check-ignore .env.secrets` → `.env.secrets` (gitignored). ✅
|
||||
- File holds only the **spike user** key (`ACDL_AWS_ACCESS_KEY_ID`, `ACDL_AWS_SECRET_ACCESS_KEY`, `AWS_DEFAULT_REGION`); the root key id is **absent** (deactivated per D-034 in Phase 08).
|
||||
|
||||
### 3.4 .env.secrets holds only the spike key, not the root key
|
||||
### No long-lived credential in generated Terraform
|
||||
`grep -rn --exclude-dir=.terraform -E "AKIA|aws_secret" terraform/spike/main.tf terraform/spike/terraform.tf terraform/spike/providers.tf acdl_platform/ contracts/ modules-ir/` → **no matches**. The generated TF references only the state bucket name (`acdl-tfstate-581513795199-us-east-1`) — a bucket name, not a credential.
|
||||
|
||||
`.env.secrets` (chmod 600) contains only `ACDL_AWS_ACCESS_KEY_ID` +
|
||||
`ACDL_AWS_SECRET_ACCESS_KEY` (the rotated spike user key) + `AWS_DEFAULT_REGION`.
|
||||
The root key was used only in the orchestrator's env during Wave 5 and was never
|
||||
written to any file.
|
||||
### Outbox write is append-only
|
||||
`grep -c "delete_item\|update_item" acdl_platform/outbox_writer.py` → **0**. Only `put_item` is called (D-P10-3 single event; GENESIS → one event; append-only).
|
||||
|
||||
### 3.5 rotate_spike_key.sh reads root key from env, never a file
|
||||
|
||||
- Validates `ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID` + `ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY`
|
||||
via `: "${VAR:?...}"` (raises if missing) ✓
|
||||
- Does NOT echo their values ✓
|
||||
- Passes them into the inline `python3 - <<'PYEOF'` block via `os.environ[...]` ✓
|
||||
- **Refuses to write `.env.secrets` if not gitignored**: `git check-ignore -q "$ENV_FILE"
|
||||
|| fail "$ENV_FILE is not gitignored — refusing to write the key"` ✓ (line 29)
|
||||
- Writes only the new spike key (AccessKeyId is printed to stderr for the log; the
|
||||
SecretAccessKey goes only to `.env.secrets`) ✓
|
||||
- chmod 600 on `.env.secrets` ✓
|
||||
- Prints the D-034 manual-step note in the header comment ✓
|
||||
|
||||
### 3.6 Spike caller is the user, not root
|
||||
|
||||
`verify_phase08.sh` asserts `Arn == "arn:aws:iam::581513795199:user/acdl-spike-runner"`
|
||||
and explicitly fails if it is `:root` (line 37-38). The live run returned the user ARN. ✓
|
||||
|
||||
### 3.7 Least-privilege policy enforced
|
||||
|
||||
The Deny statement's `NotResource` lists exactly the 3 ARNs (state bucket + state
|
||||
bucket objects + outbox table), so every other AWS action is denied. Confirmed live:
|
||||
the spike key can `s3:head_bucket` + `dynamodb:describe_table` but is denied
|
||||
`dynamodb:DescribeTimeToLive` (the policy does not grant it) and `iam:GetUser`
|
||||
(the verify script's IAM check was skipped because the spike key cannot call it —
|
||||
which is the policy working as intended). No `terraform apply`, no `iam:*`, no
|
||||
`ec2:*`, no `s3:CreateBucket`/`DeleteBucket` granted. ✓
|
||||
### E2E runner is plan-only
|
||||
`grep -c "terraform apply" scripts/run_spike_e2e.sh` → **0**. The runner calls `terraform init + validate + plan` only (spike scope; apply gated by HITL in v1.2 per the Out-of-Scope table).
|
||||
|
||||
---
|
||||
|
||||
## Layer 4 — Quality: PASS
|
||||
|
||||
### 4.1 ROADMAP.md
|
||||
### README layout
|
||||
README's layout table still matches reality: `acdl_platform/`, `schemas/`, `adapters/`, `terraform/`, `modules-ir/` all populated and described accurately. `modules-ir/` row notes `l1-s3` + `l2-static-asset` (Phase 09–10). ✅
|
||||
|
||||
Phase 08 status = **"complete (v1.1.3)"** ✓ (line 103). Success criteria all met:
|
||||
S3 bucket ✓, DynamoDB table ✓, IAM user + scoped policy ✓, rotated key in
|
||||
`.env.secrets` ✓ (Gitea secret upload is optional/v1.2 per the script), caller
|
||||
identity verified ✓, D-034 closure noted as manual ✓.
|
||||
### Commit ci-blocks
|
||||
All 8 Phase 10 commits (798f430 prep, cc4c27c plan, 8437a51 Wave 1, 622abe0 Wave 2, 7afaa34 Wave 3, e29319a Wave 4, d3aa960 traceability, 35a336a ship) carry `---ci---` blocks with `project/phase/milestone/status/persona/tasks` (or `release.tag` for the ship commit). ✅
|
||||
|
||||
### 4.2 REQUIREMENTS.md traceability
|
||||
### Roadmap + Requirements
|
||||
- `ROADMAP.md` Phase 10 → `Status: complete (v1.1.5)`. ✅
|
||||
- `REQUIREMENTS.md` traceability: REQ-25/27/28 → `complete (v1.1.5)`. ✅
|
||||
|
||||
```
|
||||
| REQ-23 | 08 | complete (v1.1.3) |
|
||||
```
|
||||
✓ (line 124). REQ-23 (re-interpreted: AWS auth bootstrap + state backend; OIDC
|
||||
deferred to v1.2 per D-039) marked complete.
|
||||
### L2 README internal consistency
|
||||
`modules-ir/l2/l2-static-asset/README.md` accurately explains D-P10-1 (the adapter consumes the resolved IR; for depth-1, the L2 root module IS the L1's resource — no separate module block; relationships ignored at TF level for the spike). References `l1-s3` only. ✅
|
||||
|
||||
### 4.3 Commit ci-blocks
|
||||
### Spike scope vs v1.2 boundary (D-P10-1/2/3)
|
||||
- **D-P10-1:** L2 composition is depth-1, one child, wires passthrough. The adapter's backend key now derives from the stack name (spike/l2-static-asset/terraform.tfstate). ✅
|
||||
- **D-P10-2:** The contract is YAML (`contracts/spike.yaml`); the resolver parses YAML → dict → validates against `schemas/contract.schema.json` (JSON Schema draft 2020-12). ✅
|
||||
- **D-P10-3:** The evidence event is a **single** `CONFIDENCE_COMPUTED` event with `prev_event_hash=GENESIS`. The chain is GENESIS → this event (one link). ✅
|
||||
|
||||
Phase 08 commits on main all carry `---ci---` blocks with project/phase/milestone/
|
||||
status/persona/tasks:
|
||||
- `a003168` — plan (status: plan)
|
||||
- `f8ddd8b` — T-8.1..T-8.4 (persona: security-engineer+platform-engineer)
|
||||
- `1d5c4d2` — T-8.5..T-8.7 (persona: platform-engineer+lead-developer)
|
||||
- `d28630d` — T-8.8 (persona: lead-developer)
|
||||
- `96ab42f` — traceability (status: shipped)
|
||||
- `067fef1` — ship: phase-08 aws-bootstrap (v1.1.3)
|
||||
✓
|
||||
### REQ-28 (the binding spike claim)
|
||||
Re-confirmed via the in-repo substrate-agnostic scan:
|
||||
- `grep -rn --include='*.py' -E 'aws_s3_bucket|aws_[a-z]+_[a-z]+' acdl_platform/` → **no matches** (platform Python is substrate-agnostic).
|
||||
- Python scan of `modules-ir/` JSON for `aws_*` resource-type **values** (excluding `description`/`$comment` strings, which may legitimately reference the mapping to explain it) → **CLEAN**.
|
||||
- `adapters/terraform/adapter.py` DOES contain `aws_s3_bucket` (in `TYPE_MAP` + resource emission) — as it must; it is the only substrate-specific code. ✅
|
||||
|
||||
### 4.4 README layout consistency
|
||||
`verify_phase10.sh` Check (h) prints:
|
||||
> ok: REQ-28: adapter is the only substrate-specific code; modules-ir/ + acdl_platform/ are substrate-agnostic (docs/comments excluded)
|
||||
|
||||
`terraform/bootstrap/` is now populated (no longer just `.gitkeep`'d): 4 authored
|
||||
files + the gitignored `.bootstrap_state.json` marker. The repo-root README's
|
||||
layout table still matches reality (the `acdl_platform/` rename from the Phase 08
|
||||
prep commit `727c873` is reflected; both `scripts/verify_phase06.sh` and
|
||||
`scripts/verify_phase07.sh` were updated and still pass: `EXIT06=0`, `EXIT07=0`).
|
||||
|
||||
### 4.5 spike_runner_policy.json internal consistency
|
||||
|
||||
The 4 Sids in the committed policy match the plan's T-8.1 spec (the prompt's
|
||||
`SpikeStateBucketReadWrite` / `SpikeOutboxTableReadWrite` / `SpikeStsSelfIdentify`
|
||||
/ `DenyEverythingElse` names). The policy is internally consistent with
|
||||
`create_iam_user.py` (which reads it verbatim and `put_user_policy`s it) and with
|
||||
`verify_phase08.sh` (which asserts the `DenyEverythingElse` Sid is present). ✓
|
||||
**The spike's central proof holds.** The IR commitments are intact: no polyglot mess.
|
||||
|
||||
---
|
||||
|
||||
## P1 flags (post-hoc review — non-blocking)
|
||||
## Requirements coverage
|
||||
|
||||
### P1-1: DynamoDB TTL (`expire_at`) not enabled on the table
|
||||
|
||||
**D-044** commits to TTL attribute `expire_at = now+365d` on the outbox table. The
|
||||
PLAN.md T-8.3 body (step 6) specified an `update_time_to_live` call after table
|
||||
creation: `TimeToLiveSpecification={AttributeName="expire_at", Enabled=True}`. The
|
||||
shipped `create_state_backend.py` does **NOT** call `update_time_to_live` — the
|
||||
table is created without TTL enabled. The Phase 10 outbox writer will still be
|
||||
able to write `expire_at` as an integer epoch, but DynamoDB will not auto-expire
|
||||
rows until TTL is enabled.
|
||||
|
||||
**Impact:** non-blocking for the spike (the spike writes one event + reads it back;
|
||||
TTL is a long-term cleanup optimization, not a correctness requirement). But D-044
|
||||
is a locked decision and the plan body explicitly required it.
|
||||
|
||||
**Recommended fix (Phase 09 or 10):** add an idempotent
|
||||
`dyn.update_time_to_live(TableName=OUTBOX_TABLE,
|
||||
TimeToLiveSpecification={"AttributeName": "expire_at", "Enabled": True})` call
|
||||
after the table is ACTIVE. This requires the bootstrap root key (or a one-shot
|
||||
escalation) since the spike key's policy does not grant `dynamodb:UpdateTimeToLive`
|
||||
— correctly, since that is an admin op.
|
||||
|
||||
### P1-2: `.bootstrap_state.json` marker has 5 keys, not the 7 the T-8.3 spec listed
|
||||
|
||||
The T-8.3 plan body specified the marker should include `versioning: true` and
|
||||
`ttl_attribute: "expire_at"` (7 keys). The shipped marker has only 5 keys
|
||||
(`account_id`, `bucket_name`, `table_name`, `region`, `created_at`). The PLAN.md
|
||||
**must_have** line (the binding requirement) lists only those 5 keys, so this is
|
||||
not a must_have violation — but it is a deviation from the fuller T-8.3 spec.
|
||||
|
||||
**Impact:** cosmetic. The marker is bookkeeping; the verify script does not assert
|
||||
the extra two keys. Non-blocking.
|
||||
|
||||
**Recommended fix:** add `"versioning": true` + `"ttl_attribute": "expire_at"` to
|
||||
the marker dict in `create_state_backend.py` (2-line addition; can be done with
|
||||
the P1-1 fix).
|
||||
|
||||
Neither P1 is auto-fixed by the verifier (the verifier is instructed not to edit
|
||||
code, only VERIFY.md). Both are flagged for the Phase 09/10 author or a post-hoc
|
||||
hardening commit.
|
||||
| REQ | Phase | Plan claim | Verified | Status |
|
||||
|-----|-------|------------|----------|--------|
|
||||
| REQ-25 | 10 | T-10.1/2/3 | composition.json + registry + README present + shape correct | **covered** |
|
||||
| REQ-27 | 10 | T-10.4/5/6/7/8 | contract + resolver + adapter + outbox + e2e runner; full pipeline runs end-to-end against real AWS | **covered** |
|
||||
| REQ-28 | 10 | T-10.9 | verify_phase10.sh Check (h) passes; adapter is the only substrate-specific code | **covered** |
|
||||
|
||||
---
|
||||
|
||||
## Manual attestation required (not auto-verifiable)
|
||||
## Integration links
|
||||
|
||||
### D-034 — root key rotation
|
||||
- `contract_resolver.py` imports `yaml` + `jsonschema` (both available); loads `schemas/contract.schema.json`, `modules-ir/registry.json`, `modules-ir/l2/l2-static-asset/composition.json`, `modules-ir/l1/l1-s3/interface.json`, `schemas/ir.schema.json` — all resolve.
|
||||
- `adapter.py` imports stdlib only; consumes the IR instance shape emitted by the resolver (`stack`, `resources[].{id,type,inputs,outputs,nfrs}`). ✅
|
||||
- `outbox_writer.py` imports `boto3` (available); writes to `acdl-outbox` (Phase 08 table). ✅
|
||||
- `run_spike_e2e.sh` calls `contract_resolver.py` → `adapter.py` → `terraform` → `checkov` → `checkov_adapter.py` → `acdl_platform.confidence_signal` → `outbox_writer.py`. All imports + paths resolve. ✅
|
||||
- `verify_phase10.sh` calls `run_spike_e2e.sh` (Check e), queries DynamoDB (Check g), greps the repo (Check h). All paths resolve. ✅
|
||||
|
||||
**Decision D-034** (one-shot bootstrap waiver) requires the user to manually
|
||||
rotate/deactivate the bootstrap **root** account key in the AWS IAM console after
|
||||
Phase 08, because the root key was the one-shot bootstrap credential and must not
|
||||
remain active.
|
||||
---
|
||||
|
||||
**Why the verifier cannot check this:** the root key is never committed, never
|
||||
written to a tracked file, and (per the security model) should already be
|
||||
deactivated by the user. The verifier has no AWS API path to inspect the root
|
||||
account's own access keys without the root key itself (which would defeat the
|
||||
purpose). The `rotate_spike_key.sh` script explicitly does NOT rotate the root key
|
||||
and prints the D-034 reminder; `verify_phase08.sh` notes "D-034 closed (user must
|
||||
rotate the root key manually now)" in its VERIFIED line.
|
||||
## P0 / P1 issues
|
||||
|
||||
**Action required from the user:** confirm in the AWS IAM console
|
||||
(https://console.aws.amazon.com/iam/ → Users → root → Security credentials) that
|
||||
the bootstrap root access key used for Wave 5 is either **deactivated** or
|
||||
**deleted**. Record the closure in `PROJECT.md` D-034 (the traceability commit
|
||||
`96ab42f` should already note this; if not, the user should add it).
|
||||
**P0: none.**
|
||||
|
||||
**P1: none new to Phase 10.** The P1-1 from Phase 09 (two AWS access key IDs — `AKIAYOZHMKZ7RK26N66W` + `AKIAYOZHMKZ772SINHFX` — appearing in the `.ciagent/VERIFY.md` Phase 09 narrative) is **carried forward, not introduced here**. These are public key identifiers (not secret pairs) and live in the `.ciagent/` audit narrative, not in executable code. Recommended for a future hygiene redaction pass; non-blocking for v1.2 ship.
|
||||
|
||||
---
|
||||
|
||||
## Final verdict
|
||||
|
||||
**Phase 08: VERIFIED**
|
||||
**Phase 10: VERIFIED**
|
||||
|
||||
All four layers pass. The 7 deliverable files exist, parse, and typecheck. The
|
||||
IAM policy is least-privilege with the explicit Deny-everything-else statement.
|
||||
The live AWS verification confirms: caller identity is `acdl-spike-runner` (not
|
||||
root), the S3 state bucket exists with versioning enabled, the DynamoDB outbox
|
||||
table exists with the correct PAY_PER_REQUEST + PK/SK shape. No secrets are
|
||||
committed (no AKIA values, no secret strings, no root key id in any tracked file).
|
||||
`.env.secrets` + `.bootstrap_state.json` are gitignored; `.env.secrets` is chmod
|
||||
600 and holds only the rotated spike key (not the root key). The two P1 flags
|
||||
(TTL not enabled; marker missing 2 cosmetic keys) are non-blocking and flagged
|
||||
for post-hoc review. D-034 (manual root-key rotation) is a manual attestation
|
||||
item the verifier cannot auto-check.
|
||||
The milestone capstone is genuinely achieved:
|
||||
- (a) `l2-static-asset` references `l1-s3` only (depth 1). ✅
|
||||
- (b) One contract submission (`contracts/spike.yaml`) completes the full pipeline end-to-end (resolve → IR → terraform plan against real AWS → Checkov → confidence `pass` → DynamoDB outbox write). ✅
|
||||
- (c) `verify_phase10.sh` proves the adapter is the only substrate-specific code (REQ-28). ✅
|
||||
- (d) Evidence event written to the DynamoDB outbox (RPO=0, GENESIS chain). ✅
|
||||
|
||||
The IR commitments hold. Ready for the COMPLETE gate → review → ship `v1.2.0` → audit.
|
||||
@@ -11,5 +11,6 @@ runner-data/
|
||||
.env.secrets
|
||||
terraform/bootstrap/.bootstrap_state.json
|
||||
terraform/spike/.terraform/
|
||||
terraform/spike/.terraform.lock.hcl
|
||||
terraform/spike/tfplan
|
||||
terraform/spike/*.tfstate*
|
||||
@@ -0,0 +1,119 @@
|
||||
"""ACDL Contract Resolver — resolve a contract to a Target Stack IR instance.
|
||||
|
||||
ARCHITECTURE.md §12.8: the contract declares intent in IR-typed terms;
|
||||
the resolver resolves the contract to a target stack (list of L1
|
||||
instances + inputs + relationships); the adapter compiles the target
|
||||
stack to a plan.
|
||||
|
||||
Steps:
|
||||
1. Load the contract (YAML -> dict).
|
||||
2. Validate the contract against schemas/contract.schema.json.
|
||||
3. Look up the L2 in modules-ir/registry.json.
|
||||
4. Load the L2's composition.json (the thin-composition tree).
|
||||
5. Map the contract's inputs through the composition's wires to the
|
||||
child L1's inputs.
|
||||
6. Emit an IR instance {version, stack:{name, kind:l2, depth},
|
||||
resources:[<L1 instances with concrete inputs>], relationships:[...]}.
|
||||
7. Validate the IR instance against schemas/ir.schema.json.
|
||||
|
||||
CLI: contract_resolver.py <contract.yaml> <out_ir.json>
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
import yaml
|
||||
import jsonschema
|
||||
|
||||
|
||||
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
||||
def _load_json(path):
|
||||
with open(path, "r") as fh:
|
||||
return json.load(fh)
|
||||
|
||||
|
||||
def resolve(contract_path, repo_root=None):
|
||||
"""Resolve a contract YAML to an IR instance dict."""
|
||||
rr = repo_root or REPO_ROOT
|
||||
|
||||
# 1. Load the contract YAML.
|
||||
with open(contract_path, "r") as fh:
|
||||
contract = yaml.safe_load(fh)
|
||||
|
||||
# 2. Validate the contract against the contract schema.
|
||||
contract_schema = _load_json(os.path.join(rr, "schemas/contract.schema.json"))
|
||||
jsonschema.validate(contract, contract_schema)
|
||||
|
||||
# 3. Look up the L2 in the registry.
|
||||
stack_name = contract["stack"]
|
||||
registry = _load_json(os.path.join(rr, "modules-ir/registry.json"))
|
||||
if stack_name not in registry:
|
||||
raise ValueError(f"stack {stack_name!r} not in registry")
|
||||
versions = registry[stack_name]
|
||||
# Pick the highest 1.x.x (spike: just take the first non-deprecated).
|
||||
entry = next(v for v in versions.values() if not v.get("deprecated", False))
|
||||
|
||||
# 4. Load the L2's composition.json.
|
||||
composition_key = entry.get("composition") or entry.get("interface")
|
||||
composition = _load_json(os.path.join(rr, composition_key))
|
||||
|
||||
# 5. Map the contract's inputs through the wires to the child L1's inputs.
|
||||
wires = composition.get("wires", {})
|
||||
contract_inputs = contract.get("inputs", {})
|
||||
children = composition.get("children", [])
|
||||
|
||||
resources = []
|
||||
relationships = []
|
||||
for child in children:
|
||||
child_id = child["id"]
|
||||
child_module = child["module"] # e.g. l1-s3@1.0.0
|
||||
# Map inputs via wires whose target is this child.
|
||||
child_inputs = {}
|
||||
for wire_name, wire in wires.items():
|
||||
if wire.get("target") == child_id and wire_name in contract_inputs:
|
||||
child_inputs[wire["input"]] = contract_inputs[wire_name]
|
||||
# Load the L1 interface to get the IR type + outputs.
|
||||
l1_name, l1_version = child_module.split("@", 1)
|
||||
l1_entry = registry.get(l1_name, {}).get(l1_version)
|
||||
if not l1_entry:
|
||||
raise ValueError(f"L1 {child_module!r} not in registry")
|
||||
l1_iface = _load_json(os.path.join(rr, l1_entry["interface"]))
|
||||
resources.append({
|
||||
"id": child_id,
|
||||
"type": l1_iface["type"],
|
||||
"module": child_module,
|
||||
"inputs": child_inputs,
|
||||
"outputs": l1_iface.get("outputs", {}),
|
||||
})
|
||||
relationships.append({"from": "root", "to": child_id, "kind": "parent"})
|
||||
|
||||
# 6. Emit the IR instance.
|
||||
ir_instance = {
|
||||
"version": "1.0.0",
|
||||
"stack": {
|
||||
"name": composition["name"],
|
||||
"kind": composition["kind"],
|
||||
"depth": composition["depth"],
|
||||
},
|
||||
"resources": resources,
|
||||
"relationships": relationships,
|
||||
}
|
||||
|
||||
# 7. Validate the IR instance against the IR schema.
|
||||
ir_schema = _load_json(os.path.join(rr, "schemas/ir.schema.json"))
|
||||
jsonschema.validate(ir_instance, ir_schema)
|
||||
|
||||
return ir_instance
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 3:
|
||||
print("usage: contract_resolver.py <contract.yaml> <out_ir.json>", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
ir = resolve(sys.argv[1])
|
||||
with open(sys.argv[2], "w") as fh:
|
||||
json.dump(ir, fh, indent=2)
|
||||
print(f"resolver: emitted IR to {sys.argv[2]}", file=sys.stderr)
|
||||
@@ -0,0 +1,71 @@
|
||||
"""ACDL Outbox Writer — write an evidence event to the DynamoDB outbox.
|
||||
|
||||
ARCHITECTURE.md §9: DynamoDB outbox, RPO=0 (synchronous write before
|
||||
ack). The event is hash-chained (SHA-256 over canonical JSON); the first
|
||||
event has prev_event_hash="GENESIS". D-P10-3: the spike writes ONE
|
||||
CONFIDENCE_COMPUTED event.
|
||||
|
||||
The outbox table (Phase 08): acdl-outbox, PAY_PER_REQUEST, PK contractId,
|
||||
SK eventType#eventTs, TTL expire_at = now + 365d (D-044).
|
||||
|
||||
CLI: outbox_writer.py <event.json> (uses AWS creds from env)
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
import boto3
|
||||
|
||||
|
||||
OUTBOX_TABLE = "acdl-outbox"
|
||||
REGION = os.environ.get("AWS_DEFAULT_REGION", "us-east-1")
|
||||
|
||||
|
||||
def _canonical_hash(event):
|
||||
"""SHA-256 over canonical JSON (sort_keys, compact separators)."""
|
||||
canonical = json.dumps(event, sort_keys=True, separators=(",", ":"))
|
||||
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def write_event(event, outbox_table=OUTBOX_TABLE, region=REGION):
|
||||
"""Write an evidence event to the DynamoDB outbox. Returns the item dict."""
|
||||
contract_id = event["contractId"]
|
||||
event_type = event.get("eventType", "CONFIDENCE_COMPUTED")
|
||||
event_ts = event.get("ts") or datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
sk = f"{event_type}#{event_ts}"
|
||||
|
||||
# Chain: first event = GENESIS (D-P10-3 spike writes one event).
|
||||
prev_hash = event.get("prev_event_hash", "GENESIS")
|
||||
event_hash = _canonical_hash(event)
|
||||
|
||||
item = {
|
||||
"contractId": {"S": contract_id},
|
||||
"eventType#eventTs": {"S": sk},
|
||||
"payload": {"S": json.dumps(event, sort_keys=True)},
|
||||
"prev_event_hash": {"S": prev_hash},
|
||||
"hash": {"S": event_hash},
|
||||
"environment": {"S": str(event.get("environment", ""))},
|
||||
"stack": {"S": str(event.get("stack", ""))},
|
||||
"score": {"N": str(event.get("score", 0))},
|
||||
"band": {"S": str(event.get("band", ""))},
|
||||
"expire_at": {"N": str(int((datetime.datetime.now(datetime.timezone.utc) +
|
||||
datetime.timedelta(days=365)).timestamp()))},
|
||||
}
|
||||
|
||||
session = boto3.Session(region_name=region)
|
||||
dyn = session.client("dynamodb")
|
||||
dyn.put_item(TableName=outbox_table, Item=item)
|
||||
return item
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 2:
|
||||
print("usage: outbox_writer.py <event.json>", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
with open(sys.argv[1], "r") as fh:
|
||||
event = json.load(fh)
|
||||
item = write_event(event)
|
||||
print(json.dumps({k: list(v.values())[0] for k, v in item.items()}, indent=2))
|
||||
@@ -82,6 +82,8 @@ def adapt(ir_instance, out_dir):
|
||||
)
|
||||
|
||||
# --- terraform.tf: required_version + required_providers + S3 backend (no DynamoDB lock per D-P09-1) ---
|
||||
# The backend key is derived from the stack name so l1 vs l2 spikes use separate state keys (D-P10-1).
|
||||
stack_name = stack.get("name", "spike")
|
||||
terraform_tf = (
|
||||
'terraform {\n'
|
||||
' required_version = ">= 1.9, < 1.10"\n'
|
||||
@@ -93,7 +95,7 @@ def adapt(ir_instance, out_dir):
|
||||
' }\n'
|
||||
' backend "s3" {\n'
|
||||
' bucket = "acdl-tfstate-581513795199-us-east-1"\n'
|
||||
' key = "spike/l1-s3/terraform.tfstate"\n'
|
||||
f' key = "spike/{stack_name}/terraform.tfstate"\n'
|
||||
' region = "us-east-1"\n'
|
||||
' }\n'
|
||||
'}\n'
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
stack: l2-static-asset
|
||||
environment: dev
|
||||
inputs:
|
||||
bucket_name: acdl-spike-bucket
|
||||
region: us-east-1
|
||||
@@ -0,0 +1,31 @@
|
||||
# l2-static-asset — thin-composition (S3 static asset)
|
||||
|
||||
The v1.1 spike's L2. A thin-composition that references `l1-s3` only
|
||||
(depth 1). The contract's inputs (`bucket_name`, `region`) map 1:1
|
||||
through the wires to the L1's inputs.
|
||||
|
||||
## Composition (the IR-typed thin-composition tree)
|
||||
|
||||
See `composition.json`: `kind=l2`, `depth=1`, one child `l1-s3@1.0.0`,
|
||||
wires `{bucket_name → s3.inputs.bucket_name, region → s3.inputs.region}`
|
||||
(passthrough).
|
||||
|
||||
## IR → Terraform mapping (D-P10-1)
|
||||
|
||||
The Terraform adapter consumes the *resolved IR instance* (which has
|
||||
`kind=l2` + the L1 resource `s3` in its `resources` array). For a
|
||||
depth-1 thin-composition, the L2 root module **IS** the L1's resource —
|
||||
no separate `module "l1_s3" { source = "..." }` block. The existing
|
||||
adapter `TYPE_MAP` + resource emission handle both l1 and l2 instances
|
||||
(the resources array is the same shape). The `relationships` array is
|
||||
ignored at the Terraform level for the spike (composition ordering is
|
||||
implicit in the single resource).
|
||||
|
||||
v1.2 may emit a real `module "l1_s3" { source = "..." }` block when L1s
|
||||
become published Terraform modules rather than inline resources.
|
||||
|
||||
## Versioning (W3.D)
|
||||
|
||||
`1.0.0` — interface MAJOR, behavior MINOR, lifecycle PATCH. MAJOR bumps
|
||||
require a new registry entry (immutable publication); old entries enter
|
||||
a 12-month deprecation window.
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "l2-static-asset",
|
||||
"version": "1.0.0",
|
||||
"kind": "l2",
|
||||
"depth": 1,
|
||||
"description": "Thin-composition: a single S3 bucket for static asset hosting. References l1-s3 only (depth 1).",
|
||||
"children": [
|
||||
{
|
||||
"id": "s3",
|
||||
"module": "l1-s3@1.0.0"
|
||||
}
|
||||
],
|
||||
"wires": {
|
||||
"bucket_name": {"target": "s3", "input": "bucket_name"},
|
||||
"region": {"target": "s3", "input": "region"}
|
||||
}
|
||||
}
|
||||
@@ -5,5 +5,12 @@
|
||||
"published_at": "2026-07-21T19:00:00Z",
|
||||
"deprecated": false
|
||||
}
|
||||
},
|
||||
"l2-static-asset": {
|
||||
"1.0.0": {
|
||||
"composition": "modules-ir/l2/l2-static-asset/composition.json",
|
||||
"published_at": "2026-07-21T19:30:00Z",
|
||||
"deprecated": false
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+99
@@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env bash
|
||||
# scripts/run_spike_e2e.sh - the v1.1 spike end-to-end pipeline (Phase 10 capstone).
|
||||
#
|
||||
# Orchestrates: contract validation -> IR resolution -> terraform plan
|
||||
# (real AWS) -> Checkov -> PolicyCheckResult -> confidence signal ->
|
||||
# evidence event to DynamoDB outbox.
|
||||
#
|
||||
# Uses the rotated spike key (D-039) from gitignored .env.secrets.
|
||||
# Plan-only (no apply); -lock=false per D-P09-1.
|
||||
set -u
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
fail() { echo "FAIL: $*" >&2; exit 1; }
|
||||
|
||||
ENV_FILE="$ROOT/.env.secrets"
|
||||
[ -f "$ENV_FILE" ] || fail ".env.secrets missing (run scripts/rotate_spike_key.sh)"
|
||||
set -a
|
||||
. "$ENV_FILE"
|
||||
set +a
|
||||
export AWS_ACCESS_KEY_ID="$ACDL_AWS_ACCESS_KEY_ID"
|
||||
export AWS_SECRET_ACCESS_KEY="$ACDL_AWS_SECRET_ACCESS_KEY"
|
||||
export AWS_DEFAULT_REGION="$AWS_DEFAULT_REGION"
|
||||
|
||||
CONTRACT="contracts/spike.yaml"
|
||||
CONTRACT_ID="11111111-1111-1111-1111-111111111111" # spike fixed UUID
|
||||
WORK="/tmp/spike_e2e"
|
||||
rm -rf "$WORK"; mkdir -p "$WORK"
|
||||
|
||||
echo "=== Step 1+2: resolve contract -> IR (validates contract schema + IR schema) ==="
|
||||
python3 acdl_platform/contract_resolver.py "$CONTRACT" "$WORK/spike_ir.json" || fail "contract resolution failed"
|
||||
python3 -c "import json; d=json.load(open('$WORK/spike_ir.json')); print(f\"IR: {d['stack']['name']} {d['stack']['kind']} {len(d['resources'])} resource(s)\")"
|
||||
|
||||
echo "=== Step 3: adapter compiles IR -> terraform/spike/*.tf (regenerate) ==="
|
||||
python3 adapters/terraform/adapter.py "$WORK/spike_ir.json" terraform/spike || fail "adapter failed"
|
||||
echo "adapter: emitted terraform/spike/{main.tf,terraform.tf,providers.tf}"
|
||||
|
||||
echo "=== Step 4: terraform init + validate + plan -lock=false (real AWS) ==="
|
||||
cd terraform/spike
|
||||
terraform init -reconfigure -lock=false -input=false >> "$WORK/tf.log" 2>&1 || fail "terraform init failed"
|
||||
terraform validate >> "$WORK/tf.log" 2>&1 || fail "terraform validate failed"
|
||||
terraform plan -lock=false -input=false -out=tfplan >> "$WORK/tf.log" 2>&1 || fail "terraform plan failed"
|
||||
echo "terraform plan OK (1 to add, 0 to change, 0 to destroy expected)"
|
||||
cd "$ROOT"
|
||||
|
||||
echo "=== Step 5: run Checkov on terraform/spike/main.tf ==="
|
||||
checkov -f terraform/spike/main.tf --framework terraform -o json --soft-fail > "$WORK/checkov.json" 2> "$WORK/checkov.err"
|
||||
[ -s "$WORK/checkov.json" ] || fail "checkov produced no output"
|
||||
echo "checkov: $(python3 -c "import json; d=json.load(open('$WORK/checkov.json')); print(len(d.get('results',{}).get('failed_checks',[])), 'failed,', len(d.get('results',{}).get('passed_checks',[])), 'passed')")"
|
||||
|
||||
echo "=== Step 6: Checkov adapter -> PolicyCheckResult list ==="
|
||||
python3 adapters/terraform/policy/checkov_adapter.py "$WORK/checkov.json" "$CONTRACT_ID" > "$WORK/pcr.json" || fail "checkov adapter failed"
|
||||
PCR_COUNT=$(python3 -c "import json; print(len(json.load(open('$WORK/pcr.json'))))")
|
||||
echo "PolicyCheckResult: $PCR_COUNT record(s)"
|
||||
|
||||
echo "=== Step 7: confidence signal compute ==="
|
||||
python3 <<PY > "$WORK/signal.json" || fail "confidence signal failed"
|
||||
import json
|
||||
import acdl_platform.confidence_signal as c
|
||||
pcr = json.load(open("$WORK/pcr.json"))
|
||||
inputs = {
|
||||
"policy": pcr,
|
||||
"validation": {"schema": True, "ir_resolved": True, "tf_validated": True, "tf_planned": True},
|
||||
"freshness": {"age_days": 0, "max_age_days": 7},
|
||||
"source": {"submitter": "spike", "commit_sha": "spike-sha", "signed": False},
|
||||
"history": {"prior_rollbacks": 0, "prior_policy_fails": 0},
|
||||
"nfrs": {"conformance": None},
|
||||
}
|
||||
sig = c.compute("$CONTRACT_ID", "dev", inputs)
|
||||
print(json.dumps({"score": sig.score, "band": sig.band, "perInput": sig.perInput, "reasonCodes": sig.reasonCodes}, indent=2))
|
||||
PY
|
||||
BAND=$(python3 -c "import json; print(json.load(open('$WORK/signal.json'))['band'])")
|
||||
SCORE=$(python3 -c "import json; print(round(json.load(open('$WORK/signal.json'))['score'],3))")
|
||||
echo "confidence: score=$SCORE band=$BAND"
|
||||
[ "$BAND" = "pass" ] || fail "confidence band is $BAND, expected pass for dev"
|
||||
|
||||
echo "=== Step 8: write evidence event to DynamoDB outbox ==="
|
||||
python3 <<PY > "$WORK/event.json" || fail "event build failed"
|
||||
import json, datetime
|
||||
sig = json.load(open("$WORK/signal.json"))
|
||||
event = {
|
||||
"contractId": "$CONTRACT_ID",
|
||||
"eventType": "CONFIDENCE_COMPUTED",
|
||||
"ts": datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||
"environment": "dev",
|
||||
"stack": "l2-static-asset",
|
||||
"score": sig["score"],
|
||||
"band": sig["band"],
|
||||
"prev_event_hash": "GENESIS",
|
||||
}
|
||||
print(json.dumps(event, indent=2))
|
||||
PY
|
||||
python3 acdl_platform/outbox_writer.py "$WORK/event.json" > "$WORK/outbox_item.json" || fail "outbox write failed"
|
||||
echo "outbox: $(python3 -c "import json; d=json.load(open('$WORK/outbox_item.json')); print('contractId=', d['contractId'], 'hash=', d['hash'][:16]+'...')")"
|
||||
|
||||
echo ""
|
||||
echo "=== SPIKE E2E OK ==="
|
||||
echo "contract=$CONTRACT -> IR -> terraform plan -> Checkov -> confidence ($BAND) -> outbox"
|
||||
exit 0
|
||||
Executable
+140
@@ -0,0 +1,140 @@
|
||||
#!/usr/bin/env bash
|
||||
# scripts/verify_phase10.sh - Phase 10 v1-spike-l2-and-contract-e2e gate (capstone).
|
||||
set -u
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
fail() { echo "FAIL: $*" >&2; exit 1; }
|
||||
ok() { echo "ok: $*"; }
|
||||
|
||||
ENV_FILE="$ROOT/.env.secrets"
|
||||
[ -f "$ENV_FILE" ] || fail ".env.secrets missing (run scripts/rotate_spike_key.sh first)"
|
||||
git check-ignore -q "$ENV_FILE" || fail ".env.secrets is not gitignored"
|
||||
set -a
|
||||
. "$ENV_FILE"
|
||||
set +a
|
||||
export AWS_ACCESS_KEY_ID="$ACDL_AWS_ACCESS_KEY_ID"
|
||||
export AWS_SECRET_ACCESS_KEY="$ACDL_AWS_SECRET_ACCESS_KEY"
|
||||
export AWS_DEFAULT_REGION="$AWS_DEFAULT_REGION"
|
||||
|
||||
# --- Check (a): composition.json exists + shape ---
|
||||
python3 <<'PY' || fail "composition.json shape wrong"
|
||||
import json
|
||||
c = json.load(open('modules-ir/l2/l2-static-asset/composition.json'))
|
||||
assert c['kind'] == 'l2' and c['depth'] == 1
|
||||
assert len(c['children']) == 1 and c['children'][0]['module'] == 'l1-s3@1.0.0'
|
||||
assert c['wires']['bucket_name']['target'] == 's3'
|
||||
assert c['wires']['region']['target'] == 's3'
|
||||
print('composition.json: kind=l2 depth=1 one child l1-s3@1.0.0 wires passthrough')
|
||||
PY
|
||||
ok "composition.json: l2-static-asset references l1-s3 only (depth 1)"
|
||||
|
||||
# --- Check (b): spike.yaml validates against contract schema ---
|
||||
python3 <<'PY' || fail "spike.yaml does not validate against contract schema"
|
||||
import yaml, json, jsonschema
|
||||
contract = yaml.safe_load(open('contracts/spike.yaml'))
|
||||
schema = json.load(open('schemas/contract.schema.json'))
|
||||
jsonschema.validate(contract, schema)
|
||||
print('spike.yaml validates against contract.schema.json')
|
||||
PY
|
||||
ok "contracts/spike.yaml validates against the contract schema"
|
||||
|
||||
# --- Check (c): resolver py_compiles + emits IR validating against ir.schema.json ---
|
||||
python3 -m py_compile acdl_platform/contract_resolver.py || fail "contract_resolver.py py_compile failed"
|
||||
TMP=$(mktemp -d)
|
||||
python3 acdl_platform/contract_resolver.py contracts/spike.yaml "$TMP/spike_ir.json" 2>/dev/null
|
||||
( cd /tmp && python3 -c "
|
||||
import json, jsonschema
|
||||
inst = json.load(open('$TMP/spike_ir.json'))
|
||||
schema = json.load(open('$ROOT/schemas/ir.schema.json'))
|
||||
jsonschema.validate(inst, schema)
|
||||
print('IR validates against ir.schema.json')
|
||||
" ) || fail "resolver IR does not validate against ir.schema.json"
|
||||
ok "contract_resolver.py resolves spike.yaml to an IR-schema-valid instance"
|
||||
|
||||
# --- Check (d): adapter py_compiles + emits main.tf with aws_s3_bucket ---
|
||||
python3 -m py_compile adapters/terraform/adapter.py || fail "adapter.py py_compile failed"
|
||||
python3 adapters/terraform/adapter.py "$TMP/spike_ir.json" "$TMP/tf" 2>/dev/null
|
||||
grep -q 'resource "aws_s3_bucket"' "$TMP/tf/main.tf" || fail "adapter did not emit aws_s3_bucket"
|
||||
ok "adapter.py compiles L2 IR to terraform with aws_s3_bucket"
|
||||
rm -rf "$TMP"
|
||||
|
||||
# --- Check (e): run_spike_e2e.sh exits 0 ---
|
||||
bash scripts/run_spike_e2e.sh > /tmp/verify_phase10_e2e.log 2>&1 || {
|
||||
cat /tmp/verify_phase10_e2e.log >&2
|
||||
fail "run_spike_e2e.sh failed"
|
||||
}
|
||||
grep -q "SPIKE E2E OK" /tmp/verify_phase10_e2e.log || fail "run_spike_e2e.sh did not print SPIKE E2E OK"
|
||||
ok "run_spike_e2e.sh completes the full pipeline end-to-end"
|
||||
|
||||
# --- Check (f): confidence band is pass for dev ---
|
||||
grep -q "band=pass" /tmp/verify_phase10_e2e.log || fail "confidence band is not pass for dev"
|
||||
ok "confidence band is pass for dev"
|
||||
|
||||
# --- Check (g): outbox item exists ---
|
||||
python3 <<'PY' || fail "outbox item not found in DynamoDB"
|
||||
import boto3
|
||||
s = boto3.Session(region_name='us-east-1')
|
||||
dyn = s.client('dynamodb')
|
||||
r = dyn.query(TableName='acdl-outbox',
|
||||
KeyConditionExpression='contractId = :cid',
|
||||
ExpressionAttributeValues={':cid': {'S': '11111111-1111-1111-1111-111111111111'}})
|
||||
assert r.get('Count', 0) >= 1, f'no outbox item for the spike contractId (Count={r.get("Count", 0)})'
|
||||
print(f'outbox item present (Count={r["Count"]})')
|
||||
PY
|
||||
ok "evidence event is written to the DynamoDB outbox"
|
||||
|
||||
# --- Check (h): REQ-28 - the adapter is the only substrate-specific code ---
|
||||
# The IR commitments hold: the adapter is the only place that knows Terraform
|
||||
# resource types (aws_s3_bucket). The L1/L2 interfaces, the IR schema, the
|
||||
# contract, the resolver, the confidence signal, and the outbox writer are
|
||||
# substrate-agnostic. Documentation (.md) + schema $comment/description strings
|
||||
# may mention aws_s3_bucket *to explain the mapping* — that's not a violation;
|
||||
# the check scans actual executable code (.py) + data files (.json/.yaml)
|
||||
# for resource-type declarations, excluding .md files + description/comment
|
||||
# string values.
|
||||
LEAK=$(grep -rn --include='*.py' -E 'aws_s3_bucket|aws_[a-z]+_[a-z]+' \
|
||||
acdl_platform/ 2>/dev/null)
|
||||
if [ -n "$LEAK" ]; then
|
||||
echo "$LEAK" >&2
|
||||
fail "REQ-28 violated: substrate-specific terms found in acdl_platform/ Python code (the platform must be substrate-agnostic)"
|
||||
fi
|
||||
# modules-ir/ data files: exclude .md (docs may reference the mapping); check
|
||||
# only .json for actual resource-type field declarations (not description strings).
|
||||
LEAK2=$(python3 <<'PY' 2>&1 || true
|
||||
import json, os, sys
|
||||
leaks = []
|
||||
for root, dirs, files in os.walk('modules-ir'):
|
||||
for f in files:
|
||||
if not f.endswith('.json'):
|
||||
continue
|
||||
path = os.path.join(root, f)
|
||||
with open(path) as fh:
|
||||
try:
|
||||
data = json.load(fh)
|
||||
except Exception:
|
||||
continue
|
||||
# Walk the JSON; flag 'aws_s3_bucket' (Terraform type) appearing as a
|
||||
# VALUE (not a key), excluding description/comment strings.
|
||||
def walk(obj, path_str=''):
|
||||
if isinstance(obj, dict):
|
||||
for k, v in obj.items():
|
||||
if k in ('description', '$comment') and isinstance(v, str):
|
||||
continue # docs/comment strings are allowed to mention it
|
||||
walk(v, path_str + '/' + k)
|
||||
elif isinstance(obj, str):
|
||||
if obj.startswith('aws_') and obj != 'aws:s3:bucket':
|
||||
leaks.append(f'{path}: {path_str} = {obj!r}')
|
||||
walk(data)
|
||||
if leaks:
|
||||
print('\n'.join(leaks))
|
||||
PY
|
||||
)
|
||||
if [ -n "$LEAK2" ]; then
|
||||
echo "$LEAK2" >&2
|
||||
fail "REQ-28 violated: substrate-specific resource-type values found in modules-ir/ JSON"
|
||||
fi
|
||||
ADAPT_HAS=$(grep -rn --include='*.py' -E 'aws_s3_bucket' adapters/terraform/ 2>/dev/null)
|
||||
[ -n "$ADAPT_HAS" ] || fail "REQ-28: adapter does not contain aws_s3_bucket (it should — it's the substrate-specific code)"
|
||||
ok "REQ-28: adapter is the only substrate-specific code; modules-ir/ + acdl_platform/ are substrate-agnostic (docs/comments excluded)"
|
||||
|
||||
echo "VERIFIED — Phase 10: L2 + contract-e2e; IR commitments hold (REQ-28)"
|
||||
@@ -8,7 +8,7 @@ terraform {
|
||||
}
|
||||
backend "s3" {
|
||||
bucket = "acdl-tfstate-581513795199-us-east-1"
|
||||
key = "spike/l1-s3/terraform.tfstate"
|
||||
key = "spike/l2-static-asset/terraform.tfstate"
|
||||
region = "us-east-1"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user