Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 55557962bd | |||
| 4c9314710b | |||
| 3936bf460a | |||
| 3070a68e1d | |||
| e054a95fd5 | |||
| 327ba1de75 | |||
| 6d27dad114 | |||
| 067fef14aa | |||
| 96ab42fde1 | |||
| d28630d1f1 | |||
| 1d5c4d2ae7 | |||
| f8ddd8b182 | |||
| a003168b3a | |||
| 727c87339b | |||
| 167a92f621 |
+1401
-1195
File diff suppressed because it is too large
Load Diff
@@ -121,9 +121,9 @@
|
|||||||
| REQ-20 | 07 | complete (v1.1.2) |
|
| REQ-20 | 07 | complete (v1.1.2) |
|
||||||
| REQ-21 | 07 | complete (v1.1.2) |
|
| REQ-21 | 07 | complete (v1.1.2) |
|
||||||
| REQ-22 | 07 | complete (v1.1.2) |
|
| REQ-22 | 07 | complete (v1.1.2) |
|
||||||
| REQ-23 | 08 | pending |
|
| REQ-23 | 08 | complete (v1.1.3) |
|
||||||
| REQ-24 | 09 | pending |
|
| REQ-24 | 09 | complete (v1.1.4) |
|
||||||
| REQ-25 | 10 | pending |
|
| REQ-25 | 10 | pending |
|
||||||
| REQ-26 | 09 | pending |
|
| REQ-26 | 09 | complete (v1.1.4) |
|
||||||
| REQ-27 | 10 | pending |
|
| REQ-27 | 10 | pending |
|
||||||
| REQ-28 | 10 | pending |
|
| REQ-28 | 10 | pending |
|
||||||
+2
-2
@@ -100,7 +100,7 @@ milestone COMPLETE: `v1.2.0` (feature milestone, next minor per ship.md).
|
|||||||
|
|
||||||
### Phase 08 — aws-oidc-bootstrap
|
### Phase 08 — aws-oidc-bootstrap
|
||||||
- **Description:** **Re-scoped per RESEARCH TARGET 1 + D-039.** Gitea Actions does not support `id-token: write` (conf 0.95), so real OIDC is deferred to v1.2. This phase instead: uses the temporary long-lived key (waiver D-034) once to create an S3 state bucket, a DynamoDB lock/outbox table, and an IAM user with a minimal scoped policy (S3 + DynamoDB + plan-only); stores the key as a Gitea Actions secret; implements `scripts/rotate_spike_key.sh` to rotate the key after each spike run. Real OIDC federation is tracked via go-gitea/gitea#36988 for v1.2.
|
- **Description:** **Re-scoped per RESEARCH TARGET 1 + D-039.** Gitea Actions does not support `id-token: write` (conf 0.95), so real OIDC is deferred to v1.2. This phase instead: uses the temporary long-lived key (waiver D-034) once to create an S3 state bucket, a DynamoDB lock/outbox table, and an IAM user with a minimal scoped policy (S3 + DynamoDB + plan-only); stores the key as a Gitea Actions secret; implements `scripts/rotate_spike_key.sh` to rotate the key after each spike run. Real OIDC federation is tracked via go-gitea/gitea#36988 for v1.2.
|
||||||
- **Status:** pending
|
- **Status:** complete (v1.1.3)
|
||||||
- **Depends on:** [07]
|
- **Depends on:** [07]
|
||||||
- **Requirements:** REQ-23 (re-interpreted: AWS auth bootstrap + state backend; OIDC deferred to v1.2 per D-039)
|
- **Requirements:** REQ-23 (re-interpreted: AWS auth bootstrap + state backend; OIDC deferred to v1.2 per D-039)
|
||||||
- **Success Criteria:**
|
- **Success Criteria:**
|
||||||
@@ -112,7 +112,7 @@ milestone COMPLETE: `v1.2.0` (feature milestone, next minor per ship.md).
|
|||||||
|
|
||||||
### Phase 09 — v1-spike-ir-and-l1-and-adapter
|
### Phase 09 — v1-spike-ir-and-l1-and-adapter
|
||||||
- **Description:** Implement the Target Stack IR, one real L1 `l1-s3` (IR-typed interface, registered), and the Terraform adapter that compiles the IR → Terraform `variable`/`output` + root module and emits a real `terraform plan` against AWS (via the rotated-key secret per D-039; OIDC is v1.2). State in S3 + DynamoDB.
|
- **Description:** Implement the Target Stack IR, one real L1 `l1-s3` (IR-typed interface, registered), and the Terraform adapter that compiles the IR → Terraform `variable`/`output` + root module and emits a real `terraform plan` against AWS (via the rotated-key secret per D-039; OIDC is v1.2). State in S3 + DynamoDB.
|
||||||
- **Status:** pending
|
- **Status:** complete (v1.1.4)
|
||||||
- **Depends on:** [08]
|
- **Depends on:** [08]
|
||||||
- **Requirements:** REQ-24, REQ-26
|
- **Requirements:** REQ-24, REQ-26
|
||||||
- **Success Criteria:**
|
- **Success Criteria:**
|
||||||
|
|||||||
+345
-158
@@ -1,208 +1,395 @@
|
|||||||
# Phase 06 — archive-demo-and-reorient (v1.1.1) Verification
|
# Phase 08 — aws-bootstrap VERIFICATION
|
||||||
|
|
||||||
Verifying Phase 06 on `main` (HEAD `ecb2c78`, tag `v1.1.1`). Phase branch
|
- **Phase:** 08 (aws-bootstrap)
|
||||||
`phase/06-archive-demo-and-reorient` deleted after merge.
|
- **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)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Layer 1 — Structural: PASS
|
## Layer 1 — Structural: PASS
|
||||||
|
|
||||||
### Must-haves (PLAN.md) vs. file existence
|
### 1.1 Deliverable files exist (7/7)
|
||||||
|
|
||||||
| Must-have | Evidence | Status |
|
|
||||||
|-----------|----------|--------|
|
|
||||||
| `demo/` contains full v1.0 demo (`modules/`, `scripts/`, `evidence-ui/`, `contracts/`, `contracts-repo/`, `.gitea/workflows/`, `ACDL_DEMO.md`, `scripts/run_demo.sh`) | `ls demo/` shows all dirs + `demo/ACDL_DEMO.md` (11271 B) + `demo/scripts/run_demo.sh` | PASS |
|
|
||||||
| New top-level dirs scaffolded with `.gitkeep` (`platform/`, `schemas/`, `adapters/`, `terraform/`, `modules-ir/`) | `ls` confirms each dir exists with a 0-byte `.gitkeep` | PASS |
|
|
||||||
| Top-level `scripts/verify_phase06.sh` exists (NOT under `demo/scripts/`) | `ls scripts/` shows only `verify_phase06.sh` (2089 B); `demo/scripts/` holds v1.0 verify_phase01..05.sh | PASS |
|
|
||||||
| README contains "Agentic Cloud Delivery Platform", "demo/", vision/architecture reference | `README.md` line 1 = "ACDL — Agentic Cloud Delivery Platform"; line 10-11 link `docs/vision.md` + `docs/architecture.md`; line 37 references `demo/` | PASS |
|
|
||||||
| `.gitignore` contains `runner-data/` | `.gitignore` line 10 = `runner-data/` | PASS |
|
|
||||||
| No stray v1.0 dirs at repo root | `ls` root shows no `modules/`, `evidence-ui/`, `contracts/`, `contracts-repo/`, `ACDL_DEMO.md`, `.gitea/` | PASS |
|
|
||||||
|
|
||||||
### Tags preserved
|
|
||||||
|
|
||||||
`git tag --list 'v1.0*' 'v1.1*'`:
|
|
||||||
```
|
```
|
||||||
v1.0.1
|
terraform/bootstrap/spike_runner_policy.json (1310 B)
|
||||||
v1.0.2
|
terraform/bootstrap/create_state_backend.py (3074 B)
|
||||||
v1.0.3
|
terraform/bootstrap/create_iam_user.py (2645 B)
|
||||||
v1.0.4
|
scripts/rotate_spike_key.sh (3856 B)
|
||||||
v1.0.5
|
scripts/verify_phase08.sh (3550 B)
|
||||||
v1.1.0
|
terraform/bootstrap/README.md (3035 B)
|
||||||
v1.1.1
|
.gitignore (edited, +2 lines)
|
||||||
```
|
```
|
||||||
All v1.0 tags (v1.0.1..v1.0.5) preserved + v1.1.0 + v1.1.1 present.
|
|
||||||
|
|
||||||
### History preservation
|
All 7 present (`ls -la` confirmed). Plus `terraform/bootstrap/__init__.py` + `.gitkeep` guards from Wave 1/2.
|
||||||
|
|
||||||
|
### 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
|
||||||
|
|
||||||
`git log --follow --oneline demo/scripts/run_demo.sh`:
|
|
||||||
```
|
```
|
||||||
e044a2d phase: 6, status: plan-as-execute, persona: lead-developer, task: T-6.1..T-6.4
|
python3 -m py_compile terraform/bootstrap/create_state_backend.py terraform/bootstrap/create_iam_user.py → PYCOMPILE_OK
|
||||||
0672edf ship: phase-05 evidence-ui-and-demo-dry-run (v1.0.5)
|
bash -n scripts/rotate_spike_key.sh scripts/verify_phase08.sh → BASHN_OK
|
||||||
```
|
```
|
||||||
`git mv` preserved history — the trail traces back through the v1.0.5 ship commit.
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Layer 2 — Behavioral: PASS
|
## Layer 2 — Behavioral: PASS
|
||||||
|
|
||||||
### Typecheck gate
|
### 2.1 verify_phase08.sh — exit 0 + VERIFIED line
|
||||||
|
|
||||||
```
|
```
|
||||||
bash -n demo/scripts/*.sh && echo "demo shell syntax ok" → ok
|
$ bash scripts/verify_phase08.sh
|
||||||
python3 -m py_compile demo/scripts/*.py && echo "demo python compile ok" → ok
|
ok: .env.secrets + .bootstrap_state.json are gitignored
|
||||||
bash -n scripts/verify_phase06.sh && echo "verify_phase06 syntax ok" → ok
|
ok: caller identity is acdl-spike-runner (NOT root)
|
||||||
```
|
ok: S3 state bucket exists
|
||||||
|
ok: DynamoDB outbox table exists
|
||||||
### Test gate (`scripts/verify_phase06.sh`)
|
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)
|
||||||
```
|
|
||||||
ok: demo/ contains the full v1.0 demo
|
|
||||||
ok: demo/scripts/run_demo.sh --no-upload exits 0
|
|
||||||
ok: new top-level dirs exist: platform/ schemas/ adapters/ terraform/ modules-ir/
|
|
||||||
ok: no stray v1.0 dirs at repo root
|
|
||||||
ok: README reflects the real platform (name + demo/ ref + vision/arch links)
|
|
||||||
Phase 06: ALL CHECKS PASS
|
|
||||||
EXIT=0
|
EXIT=0
|
||||||
```
|
```
|
||||||
|
|
||||||
### audit.json regression shape
|
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.)
|
||||||
|
|
||||||
|
### 2.2 Typecheck re-run
|
||||||
|
|
||||||
`demo/scripts/run_demo.sh --no-upload` writes a non-empty, hash-chained
|
|
||||||
`audit.json` to `/tmp/acdl_demo_run/audit.json` (3180 B). Inspected content:
|
|
||||||
```json
|
|
||||||
[
|
|
||||||
{ "seq": 0, "stage": "genesis", "prev_hash": "GENESIS",
|
|
||||||
"hash": "dad5926c4f2f1af482613c09dc50ad10177ac7522ae571b2ffc7bdfaa5063a67" },
|
|
||||||
{ "seq": 1, "stage": "dev",
|
|
||||||
"prev_hash": "dad5926c4f2f1af482613c09dc50ad10177ac7522ae571b2ffc7bdfaa5063a67",
|
|
||||||
"hash": "de62f008f205e96cf334e9040853e8869f0146ebeab0cdfddca914c6b5be8e19" },
|
|
||||||
...
|
|
||||||
]
|
|
||||||
```
|
```
|
||||||
Hash chain intact (each `prev_hash` = prior event's `hash`). Demo regression
|
python3 -m py_compile terraform/bootstrap/create_state_backend.py terraform/bootstrap/create_iam_user.py
|
||||||
satisfied end-to-end from `demo/`.
|
bash -n scripts/rotate_spike_key.sh scripts/verify_phase08.sh
|
||||||
|
→ all pass (see 1.3)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 S3 bucket versioning (live AWS check)
|
||||||
|
|
||||||
|
```
|
||||||
|
$ 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).
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Layer 3 — Security: PASS
|
## Layer 3 — Security: PASS
|
||||||
|
|
||||||
### No credentials/secrets introduced
|
### 3.1 No secrets committed
|
||||||
|
|
||||||
`git log v1.1.0..v1.1.1 --name-only` (sorted, filtered for secret-like
|
Files touched in `v1.1.2..v1.1.3`:
|
||||||
patterns `.env*|tfstate|*_key|secret|credential|*.pem|id_rsa`) → no matches.
|
```
|
||||||
The Phase 06 diff is exclusively:
|
.gitignore
|
||||||
- moves (`git mv` carries 100% renames: `*.gitkeep`, pipeline.yml,
|
.ciagent/PLAN.md
|
||||||
`index.html`, `manifest.yaml`, `mock_apply.sh`, demo scripts, contracts)
|
.ciagent/REQUIREMENTS.md
|
||||||
- new scaffold files (`platform/.gitkeep`, `schemas/.gitkeep`,
|
.ciagent/ROADMAP.md
|
||||||
`adapters/.gitkeep`, `terraform/.gitkeep`, `modules-ir/.gitkeep`)
|
.ciagent/VERIFY.md (Phase 07)
|
||||||
- `scripts/verify_phase06.sh` (new)
|
README.md
|
||||||
- `README.md` (rewritten), `.gitignore` (+`runner-data/`)
|
acdl_platform/* (rename)
|
||||||
- `demo/ACDL_DEMO.md` (newly tracked, was untracked v1.0 artifact)
|
scripts/rotate_spike_key.sh
|
||||||
- `.ciagent/ROADMAP.md` (status update)
|
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).
|
||||||
|
|
||||||
No `.env`, no `*.tfstate`, no `*_key*` files committed. Phase 06 is repo
|
### 3.2 No leaked key values in diffs
|
||||||
hygiene only — no AWS/TF code introduced (correctly deferred to Phase 08+).
|
|
||||||
|
|
||||||
### LSP error in `demo/scripts/finalize_evidence.py:46` is pre-existing
|
```
|
||||||
|
$ 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.
|
||||||
|
|
||||||
|
### 3.3 Root key id not tracked
|
||||||
|
|
||||||
`git log -1 --format='%H %s' demo/scripts/finalize_evidence.py`:
|
|
||||||
```
|
```
|
||||||
e044a2de0d7cedf57949413459992fa1859f350b phase: 6, status: plan-as-execute, persona: lead-developer, task: T-6.1..T-6.4
|
$ git grep -I "AKIAYOZHMKZ772SINHFX"
|
||||||
|
(nothing — ROOT_KEY_ID_NOT_TRACKED)
|
||||||
```
|
```
|
||||||
The `e044a2d` commit is the *move* commit (T-6.1..T-6.4) — it only performed
|
The bootstrap root key id appears in no tracked file.
|
||||||
`git mv scripts/finalize_evidence.py demo/scripts/finalize_evidence.py`,
|
|
||||||
no content edit. `git log -1 --format='%H %s' -L 46,46:demo/scripts/finalize_evidence.py`:
|
### 3.4 .env.secrets holds only the spike key, not the root key
|
||||||
```
|
|
||||||
72b359c9a902b035c8a5816437d38aba808b948e ship: phase-04 pipeline-and-approval-gates (v1.0.4)
|
`.env.secrets` (chmod 600) contains only `ACDL_AWS_ACCESS_KEY_ID` +
|
||||||
```
|
`ACDL_AWS_SECRET_ACCESS_KEY` (the rotated spike user key) + `AWS_DEFAULT_REGION`.
|
||||||
Line 46 was authored in the v1.0.4 ship commit (Phase 04, demo). Any LSP
|
The root key was used only in the orchestrator's env during Wave 5 and was never
|
||||||
finding on that line is **pre-existing v1.0 demo code**, not a Phase 06
|
written to any file.
|
||||||
regression. Per the plan, the v1.0 demo is archived as-is (intent reference;
|
|
||||||
frozen). No action.
|
### 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. ✓
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Layer 4 — Quality: PASS
|
## Layer 4 — Quality: PASS
|
||||||
|
|
||||||
### README link targets exist
|
### 4.1 ROADMAP.md
|
||||||
|
|
||||||
| README link | Target file | Exists? |
|
Phase 08 status = **"complete (v1.1.3)"** ✓ (line 103). Success criteria all met:
|
||||||
|-------------|-------------|---------|
|
S3 bucket ✓, DynamoDB table ✓, IAM user + scoped policy ✓, rotated key in
|
||||||
| `docs/vision.md` | `docs/vision.md` | PASS |
|
`.env.secrets` ✓ (Gitea secret upload is optional/v1.2 per the script), caller
|
||||||
| `docs/architecture.md` | `docs/architecture.md` | PASS |
|
identity verified ✓, D-034 closure noted as manual ✓.
|
||||||
| `.ciagent/PROJECT.md` | `.ciagent/PROJECT.md` | PASS |
|
|
||||||
| `.ciagent/ARCHITECTURE.md` | `.ciagent/ARCHITECTURE.md` | PASS (15894 B) |
|
|
||||||
| `.ciagent/ROADMAP.md` | `.ciagent/ROADMAP.md` | PASS |
|
|
||||||
| `demo/ACDL_DEMO.md` | `demo/ACDL_DEMO.md` | PASS |
|
|
||||||
|
|
||||||
No broken links.
|
### 4.2 REQUIREMENTS.md traceability
|
||||||
|
|
||||||
### Commit `---ci---` blocks
|
|
||||||
|
|
||||||
Inspected `git log v1.1.0..v1.1.1` (4 commits). Each carries a `---ci---`
|
|
||||||
block with required fields:
|
|
||||||
|
|
||||||
- `ecb2c78` (ship merge): project, phase, milestone, status: shipped,
|
|
||||||
release.tag: v1.1.1 ✓
|
|
||||||
- `4ab15cb` (post-ship docs): project, phase, milestone, status: shipped,
|
|
||||||
requirements.complete: [], release.tag: v1.1.1 ✓
|
|
||||||
- `e044a2d` (plan-as-execute T-6.1..T-6.4): project, phase, milestone,
|
|
||||||
status: plan-as-execute, persona: lead-developer, tasks: [T-6.1..T-6.4] ✓
|
|
||||||
- `b927f90` (plan): project, phase, milestone, status: plan, plan.waves: 3,
|
|
||||||
plan.tasks: 6, plan.requirements: [] ✓
|
|
||||||
|
|
||||||
(Plus pre-phase commits `930c24b` research, `087c89e` clarify, `288607b`
|
|
||||||
specify — all carry `---ci---` blocks with project/phase/milestone/status.)
|
|
||||||
|
|
||||||
### ROADMAP.md Phase 06 status
|
|
||||||
|
|
||||||
`.ciagent/ROADMAP.md` line 83:
|
|
||||||
```
|
```
|
||||||
- **Status:** complete (v1.1.1)
|
| REQ-23 | 08 | complete (v1.1.3) |
|
||||||
```
|
```
|
||||||
Matches the shipped tag `v1.1.1`.
|
✓ (line 124). REQ-23 (re-interpreted: AWS auth bootstrap + state backend; OIDC
|
||||||
|
deferred to v1.2 per D-039) marked complete.
|
||||||
|
|
||||||
## Requirement coverage
|
### 4.3 Commit ci-blocks
|
||||||
|
|
||||||
Phase 06 introduces **no new REQ** (PLAN.md frontmatter `requirements: []`;
|
Phase 08 commits on main all carry `---ci---` blocks with project/phase/milestone/
|
||||||
ROADMAP §Phase 06 "Requirements: (no new REQ; repo hygiene)"). Nothing to
|
status/persona/tasks:
|
||||||
mark covered/partial this phase.
|
- `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)
|
||||||
|
✓
|
||||||
|
|
||||||
## Auto-generated test coverage
|
### 4.4 README layout consistency
|
||||||
|
|
||||||
`scripts/verify_phase06.sh` IS the auto-generated test. Coverage audit
|
`terraform/bootstrap/` is now populated (no longer just `.gitkeep`'d): 4 authored
|
||||||
against PLAN.md must_haves:
|
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`).
|
||||||
|
|
||||||
| PLAN must_have | verify_phase06.sh check |
|
### 4.5 spike_runner_policy.json internal consistency
|
||||||
|----------------|--------------------------|
|
|
||||||
| demo/ contains full v1.0 demo | Check 1 (dirs + ACDL_DEMO.md + run_demo.sh) |
|
|
||||||
| run_demo.sh --no-upload exits 0 | Check 2 (regression) |
|
|
||||||
| New top-level dirs + .gitkeep | Check 3 |
|
|
||||||
| Top-level scripts/verify_phase06.sh | (the script itself exists) |
|
|
||||||
| README reflects real platform | Check 5 (name + demo/ ref + vision/arch links) |
|
|
||||||
| .gitignore runner-data/ | (not asserted by script — see P1 below) |
|
|
||||||
| No stray v1.0 dirs at root | Check 4 |
|
|
||||||
|
|
||||||
**Minor coverage gap (P1, not P0):** the verify script does not assert
|
The 4 Sids in the committed policy match the plan's T-8.1 spec (the prompt's
|
||||||
`.gitignore` contains `runner-data/`. The must_have is satisfied (file
|
`SpikeStateBucketReadWrite` / `SpikeOutboxTableReadWrite` / `SpikeStsSelfIdentify`
|
||||||
content verified manually) but not gated. Not auto-fixed — the plan
|
/ `DenyEverythingElse` names). The policy is internally consistent with
|
||||||
instructs verifiers to "extend it only if a must_have is uncovered";
|
`create_iam_user.py` (which reads it verbatim and `put_user_policy`s it) and with
|
||||||
the must_have *is* satisfied (file exists with the entry), so it is
|
`verify_phase08.sh` (which asserts the `DenyEverythingElse` Sid is present). ✓
|
||||||
covered structurally, only the assertion is missing. Flagged for
|
|
||||||
post-hoc review; could be added as a 6th check in a future touch-up.
|
|
||||||
|
|
||||||
## P0/P1 issues
|
---
|
||||||
|
|
||||||
- **P0:** none.
|
## P1 flags (post-hoc review — non-blocking)
|
||||||
- **P1 (post-hoc, non-blocking):**
|
|
||||||
- `scripts/verify_phase06.sh` does not programmatically assert
|
### P1-1: DynamoDB TTL (`expire_at`) not enabled on the table
|
||||||
`runner-data/` is in `.gitignore`. The must-have is satisfied
|
|
||||||
structurally; only the assertion is absent. Recommend adding a
|
**D-044** commits to TTL attribute `expire_at = now+365d` on the outbox table. The
|
||||||
`grep -q '^runner-data/$' .gitignore` line in a future touch-up.
|
PLAN.md T-8.3 body (step 6) specified an `update_time_to_live` call after table
|
||||||
- The LSP finding on `demo/scripts/finalize_evidence.py:46` is
|
creation: `TimeToLiveSpecification={AttributeName="expire_at", Enabled=True}`. The
|
||||||
pre-existing v1.0 demo code (Phase 04 authorship, Phase 06 only moved
|
shipped `create_state_backend.py` does **NOT** call `update_time_to_live` — the
|
||||||
it). No Phase 06 regression. Archived demo is frozen per D-037; no
|
table is created without TTL enabled. The Phase 10 outbox writer will still be
|
||||||
action required.
|
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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Manual attestation required (not auto-verifiable)
|
||||||
|
|
||||||
|
### D-034 — root key rotation
|
||||||
|
|
||||||
|
**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.
|
||||||
|
|
||||||
|
**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).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Final verdict
|
## Final verdict
|
||||||
|
|
||||||
**Phase 06: VERIFIED**
|
**Phase 08: VERIFIED**
|
||||||
|
|
||||||
All four layers pass. The v1.0 demo is fully archived under `demo/` with
|
All four layers pass. The 7 deliverable files exist, parse, and typecheck. The
|
||||||
history preserved; the v1.1 top-level layout is scaffolded; the README
|
IAM policy is least-privilege with the explicit Deny-everything-else statement.
|
||||||
reflects the real platform; the regression gate
|
The live AWS verification confirms: caller identity is `acdl-spike-runner` (not
|
||||||
(`demo/scripts/run_demo.sh --no-upload`) runs end-to-end from `demo/`
|
root), the S3 state bucket exists with versioning enabled, the DynamoDB outbox
|
||||||
with an intact hash chain; no secrets were introduced; commit metadata
|
table exists with the correct PAY_PER_REQUEST + PK/SK shape. No secrets are
|
||||||
is complete; ROADMAP status is correct.
|
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.
|
||||||
+6
-1
@@ -7,4 +7,9 @@ state.json
|
|||||||
audit.json
|
audit.json
|
||||||
*.tmp
|
*.tmp
|
||||||
.DS_Store
|
.DS_Store
|
||||||
runner-data/
|
runner-data/
|
||||||
|
.env.secrets
|
||||||
|
terraform/bootstrap/.bootstrap_state.json
|
||||||
|
terraform/spike/.terraform/
|
||||||
|
terraform/spike/tfplan
|
||||||
|
terraform/spike/*.tfstate*
|
||||||
@@ -28,7 +28,7 @@ a configuration file, or a Terraform module.
|
|||||||
|
|
||||||
| Path | Purpose | Populated |
|
| Path | Purpose | Populated |
|
||||||
|------|---------|-----------|
|
|------|---------|-----------|
|
||||||
| `platform/` | Platform code: confidence signal, contract resolver, outbox, HITL/ledger designs | Phase 07+ |
|
| `acdl_platform/` | Platform code: confidence signal, contract resolver, outbox, HITL/ledger designs (renamed from `platform/` in Phase 08 to avoid shadowing the stdlib `platform` module) | Phase 07+ |
|
||||||
| `schemas/` | JSON Schemas: IR, PolicyCheckResult, contract | Phase 07 |
|
| `schemas/` | JSON Schemas: IR, PolicyCheckResult, contract | Phase 07 |
|
||||||
| `adapters/` | Substrate adapters (Terraform adapter in v1; the only substrate-specific code per §12) | Phase 09 |
|
| `adapters/` | Substrate adapters (Terraform adapter in v1; the only substrate-specific code per §12) | Phase 09 |
|
||||||
| `terraform/` | State backend + provider config (S3 state + DynamoDB lock) | Phase 08+ |
|
| `terraform/` | State backend + provider config (S3 state + DynamoDB lock) | Phase 08+ |
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
"""ACDL Terraform adapter — compile a Target Stack IR instance to Terraform.
|
||||||
|
|
||||||
|
ARCHITECTURE.md §12.2: the adapter translates the IR-typed L1 interface
|
||||||
|
to a Terraform variable/output block, the L2 thin-composition tree to a
|
||||||
|
root module that calls the L1 modules, the IR-typed relationships to
|
||||||
|
Terraform module references, and emits a Terraform plan from the IR.
|
||||||
|
|
||||||
|
The adapter is a THIN LAYER; it does not own L1/L2 content — it only
|
||||||
|
translates. Substrate-agnostic in, Terraform out.
|
||||||
|
|
||||||
|
Spike scope (Phase 09): handles one L1 (l1-s3, IR type aws:s3:bucket).
|
||||||
|
L2 thin-composition + relationships land in Phase 10.
|
||||||
|
|
||||||
|
CLI: adapter.py <ir_instance.json> <out_dir>
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
# IR type -> Terraform resource type. The only substrate-specific table.
|
||||||
|
# As more L1s land, this grows; the L1 content + IR do not change.
|
||||||
|
TYPE_MAP = {
|
||||||
|
"aws:s3:bucket": "aws_s3_bucket",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _tf_block(block_type, name, body_lines, indent=2):
|
||||||
|
head = f'{block_type} "{name}" {{'
|
||||||
|
body = "\n".join(f" {l}" for l in body_lines)
|
||||||
|
return f"{head}\n{body}\n}}\n"
|
||||||
|
|
||||||
|
|
||||||
|
def _emit_resource(resource):
|
||||||
|
rtype = resource["type"]
|
||||||
|
rid = resource["id"]
|
||||||
|
tf_type = TYPE_MAP.get(rtype)
|
||||||
|
if not tf_type:
|
||||||
|
raise ValueError(f"unknown IR type {rtype!r} (adapter spike handles aws:s3:bucket only)")
|
||||||
|
body = []
|
||||||
|
inputs = resource.get("inputs", {})
|
||||||
|
# S3 bucket: bucket_name -> bucket arg; region -> provider (handled separately)
|
||||||
|
if "bucket_name" in inputs:
|
||||||
|
body.append(f'bucket = "{inputs["bucket_name"]}"')
|
||||||
|
# NFR: versioning (default true)
|
||||||
|
nfrs = resource.get("nfrs", {})
|
||||||
|
versioning = nfrs.get("versioning", True) if isinstance(nfrs, dict) else True
|
||||||
|
body.append("versioning {")
|
||||||
|
body.append(f' enabled = {"true" if versioning else "false"}')
|
||||||
|
body.append("}")
|
||||||
|
return _tf_block("resource", f'aws_s3_bucket.{rid}', body) if False else _resource_block(rid, tf_type, body)
|
||||||
|
|
||||||
|
|
||||||
|
def _resource_block(rid, tf_type, body):
|
||||||
|
"""Emit a top-level resource block."""
|
||||||
|
head = f'resource "{tf_type}" "{rid}" {{'
|
||||||
|
body_str = "\n".join(f" {l}" for l in body)
|
||||||
|
return f"{head}\n{body_str}\n}}\n"
|
||||||
|
|
||||||
|
|
||||||
|
def _emit_output(output_name, value_expr):
|
||||||
|
return f'output "{output_name}" {{\n value = {value_expr}\n}}\n'
|
||||||
|
|
||||||
|
|
||||||
|
def adapt(ir_instance, out_dir):
|
||||||
|
"""Emit main.tf + terraform.tf + providers.tf to out_dir for the IR instance."""
|
||||||
|
os.makedirs(out_dir, exist_ok=True)
|
||||||
|
stack = ir_instance["stack"]
|
||||||
|
resources = ir_instance["resources"]
|
||||||
|
|
||||||
|
# --- providers.tf: aws provider, region from the first resource's inputs.region ---
|
||||||
|
region = "us-east-1"
|
||||||
|
for r in resources:
|
||||||
|
if "region" in r.get("inputs", {}):
|
||||||
|
region = r["inputs"]["region"]
|
||||||
|
break
|
||||||
|
providers_tf = (
|
||||||
|
f'provider "aws" {{\n'
|
||||||
|
f' region = "{region}"\n'
|
||||||
|
f'}}\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- terraform.tf: required_version + required_providers + S3 backend (no DynamoDB lock per D-P09-1) ---
|
||||||
|
terraform_tf = (
|
||||||
|
'terraform {\n'
|
||||||
|
' required_version = ">= 1.9, < 1.10"\n'
|
||||||
|
' required_providers {\n'
|
||||||
|
' aws = {\n'
|
||||||
|
' source = "hashicorp/aws"\n'
|
||||||
|
' version = "~> 5.0"\n'
|
||||||
|
' }\n'
|
||||||
|
' }\n'
|
||||||
|
' backend "s3" {\n'
|
||||||
|
' bucket = "acdl-tfstate-581513795199-us-east-1"\n'
|
||||||
|
' key = "spike/l1-s3/terraform.tfstate"\n'
|
||||||
|
' region = "us-east-1"\n'
|
||||||
|
' }\n'
|
||||||
|
'}\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- main.tf: resources + outputs ---
|
||||||
|
main_tf_parts = []
|
||||||
|
for r in resources:
|
||||||
|
main_tf_parts.append(_emit_resource(r))
|
||||||
|
rid = r["id"]
|
||||||
|
outputs = r.get("outputs", {})
|
||||||
|
for out_name in outputs:
|
||||||
|
if out_name == "bucket_arn":
|
||||||
|
main_tf_parts.append(_emit_output("bucket_arn", f"aws_s3_bucket.{rid}.arn"))
|
||||||
|
elif out_name == "bucket_name":
|
||||||
|
main_tf_parts.append(_emit_output("bucket_name", f"aws_s3_bucket.{rid}.id"))
|
||||||
|
main_tf = "\n".join(main_tf_parts)
|
||||||
|
|
||||||
|
with open(os.path.join(out_dir, "main.tf"), "w") as fh:
|
||||||
|
fh.write(main_tf)
|
||||||
|
with open(os.path.join(out_dir, "terraform.tf"), "w") as fh:
|
||||||
|
fh.write(terraform_tf)
|
||||||
|
with open(os.path.join(out_dir, "providers.tf"), "w") as fh:
|
||||||
|
fh.write(providers_tf)
|
||||||
|
return out_dir
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) != 3:
|
||||||
|
print("usage: adapter.py <ir_instance.json> <out_dir>", file=sys.stderr)
|
||||||
|
sys.exit(2)
|
||||||
|
with open(sys.argv[1], "r") as fh:
|
||||||
|
ir = json.load(fh)
|
||||||
|
adapt(ir, sys.argv[2])
|
||||||
|
print(f"adapter: emitted terraform to {sys.argv[2]}", file=sys.stderr)
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# l1-s3 — S3 bucket primitive
|
||||||
|
|
||||||
|
The first real L1 module for the v1.1 spike. Single-purpose,
|
||||||
|
substrate-agnostic (the IR type is `aws:s3:bucket`, not a Terraform
|
||||||
|
resource type).
|
||||||
|
|
||||||
|
## Interface (the IR-typed contract)
|
||||||
|
|
||||||
|
See `interface.json`: inputs `bucket_name` + `region` (strings), outputs
|
||||||
|
`bucket_arn` (arn) + `bucket_name` (string), NFR `versioning` (bool,
|
||||||
|
default true).
|
||||||
|
|
||||||
|
## IR → Terraform mapping (performed by the adapter)
|
||||||
|
|
||||||
|
The Terraform adapter (`adapters/terraform/adapter.py`) translates this
|
||||||
|
L1's IR shape to Terraform:
|
||||||
|
|
||||||
|
| IR | Terraform |
|
||||||
|
|----|-----------|
|
||||||
|
| `resource.type = aws:s3:bucket` | `resource "aws_s3_bucket" "<id>" { ... }` |
|
||||||
|
| `resource.inputs.bucket_name` | `bucket = <value>` arg |
|
||||||
|
| `resource.inputs.region` | `provider "aws" { region = <value> }` |
|
||||||
|
| `resource.outputs.bucket_arn` | `output "bucket_arn" { value = aws_s3_bucket.<id>.arn }` |
|
||||||
|
| `resource.outputs.bucket_name` | `output "bucket_name" { value = aws_s3_bucket.<id>.id }` |
|
||||||
|
|
||||||
|
The adapter is a thin layer (ARCHITECTURE.md §12.2); it does not own L1
|
||||||
|
content — it only translates.
|
||||||
|
|
||||||
|
## Spike instance
|
||||||
|
|
||||||
|
`spike_instance.json` is a concrete stack instance (with values
|
||||||
|
`bucket_name=acdl-spike-bucket`, `region=us-east-1`) that validates
|
||||||
|
against `schemas/ir.schema.json`. The adapter consumes this instance
|
||||||
|
(not the interface contract) to emit Terraform.
|
||||||
|
|
||||||
|
## 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,36 @@
|
|||||||
|
{
|
||||||
|
"name": "l1-s3",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"kind": "l1",
|
||||||
|
"type": "aws:s3:bucket",
|
||||||
|
"description": "S3 bucket primitive (substrate-agnostic IR type aws:s3:bucket; the Terraform adapter translates to aws_s3_bucket).",
|
||||||
|
"inputs": {
|
||||||
|
"bucket_name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Globally-unique S3 bucket name.",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"region": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "AWS region the bucket is created in.",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"outputs": {
|
||||||
|
"bucket_arn": {
|
||||||
|
"type": "arn",
|
||||||
|
"description": "The S3 bucket ARN."
|
||||||
|
},
|
||||||
|
"bucket_name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The bucket name (echoes the input)."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nfrs": {
|
||||||
|
"versioning": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Enable S3 versioning (default true).",
|
||||||
|
"default": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"version": "1.0.0",
|
||||||
|
"stack": {
|
||||||
|
"name": "l1-s3",
|
||||||
|
"kind": "l1",
|
||||||
|
"depth": 1
|
||||||
|
},
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "s3",
|
||||||
|
"type": "aws:s3:bucket",
|
||||||
|
"module": "l1-s3@1.0.0",
|
||||||
|
"inputs": {
|
||||||
|
"bucket_name": "acdl-spike-bucket",
|
||||||
|
"region": "us-east-1"
|
||||||
|
},
|
||||||
|
"outputs": {
|
||||||
|
"bucket_arn": {"type": "arn", "description": "The S3 bucket ARN."},
|
||||||
|
"bucket_name": {"type": "string", "description": "The bucket name."}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"l1-s3": {
|
||||||
|
"1.0.0": {
|
||||||
|
"interface": "modules-ir/l1/l1-s3/interface.json",
|
||||||
|
"published_at": "2026-07-21T19:00:00Z",
|
||||||
|
"deprecated": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+98
@@ -0,0 +1,98 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# scripts/rotate_spike_key.sh - rotate the acdl-spike-runner IAM access key.
|
||||||
|
#
|
||||||
|
# Uses the bootstrap root key (ACDL_BOOTSTRAP_AWS_*) from the env to:
|
||||||
|
# 1. List acdl-spike-runner's access keys.
|
||||||
|
# 2. Create a new key.
|
||||||
|
# 3. Deactivate + delete the old key(s).
|
||||||
|
# 4. Write the new key to gitignored .env.secrets (chmod 600).
|
||||||
|
# 5. Optionally upload to Gitea secrets if ACDL_GITEA_TOKEN is set.
|
||||||
|
#
|
||||||
|
# Idempotent: re-running always ends with exactly 1 active key for the user.
|
||||||
|
# Does NOT rotate the bootstrap root key (D-034 closure = manual user step).
|
||||||
|
#
|
||||||
|
# Spike scope (D-039): the spike user key is per-run-rotated; real OIDC is
|
||||||
|
# v1.2 (blocked on go-gitea/gitea#36988).
|
||||||
|
set -u
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
cd "$ROOT"
|
||||||
|
ENV_FILE="$ROOT/.env.secrets"
|
||||||
|
|
||||||
|
fail() { echo "FAIL: $*" >&2; exit 1; }
|
||||||
|
|
||||||
|
: "${ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID:?set ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID to the root key}"
|
||||||
|
: "${ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY:?set ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY to the root key}"
|
||||||
|
REGION="${AWS_DEFAULT_REGION:-us-east-1}"
|
||||||
|
USER_NAME="acdl-spike-runner"
|
||||||
|
|
||||||
|
# Confirm .env.secrets is gitignored before writing to it.
|
||||||
|
git check-ignore -q "$ENV_FILE" || fail "$ENV_FILE is not gitignored — refusing to write the key"
|
||||||
|
|
||||||
|
python3 - <<'PY'
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import boto3
|
||||||
|
|
||||||
|
region = os.environ.get("AWS_DEFAULT_REGION", "us-east-1")
|
||||||
|
user = "acdl-spike-runner"
|
||||||
|
env_file = os.path.join(os.getcwd(), ".env.secrets")
|
||||||
|
|
||||||
|
session = boto3.Session(
|
||||||
|
aws_access_key_id=os.environ["ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID"],
|
||||||
|
aws_secret_access_key=os.environ["ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY"],
|
||||||
|
region_name=region,
|
||||||
|
)
|
||||||
|
iam = session.client("iam")
|
||||||
|
|
||||||
|
# List current keys.
|
||||||
|
keys = iam.list_access_keys(UserName=user).get("AccessKeyMetadata", [])
|
||||||
|
active = [k for k in keys if k["Status"] == "Active"]
|
||||||
|
|
||||||
|
# Create a new key first (so the user always has a working key during rotation).
|
||||||
|
new = iam.create_access_key(UserName=user)["AccessKey"]
|
||||||
|
new_id = new["AccessKeyId"]
|
||||||
|
new_secret = new["SecretAccessKey"]
|
||||||
|
print(f"iam: created new key {new_id} for {user}", file=sys.stderr)
|
||||||
|
|
||||||
|
# Deactivate + delete the old keys.
|
||||||
|
for k in active:
|
||||||
|
old_id = k["AccessKeyId"]
|
||||||
|
if old_id == new_id:
|
||||||
|
continue
|
||||||
|
iam.update_access_key(UserName=user, AccessKeyId=old_id, Status="Inactive")
|
||||||
|
iam.delete_access_key(UserName=user, AccessKeyId=old_id)
|
||||||
|
print(f"iam: deactivated+deleted old key {old_id}", file=sys.stderr)
|
||||||
|
|
||||||
|
# Write the new key to gitignored .env.secrets (chmod 600).
|
||||||
|
with open(env_file, "w") as fh:
|
||||||
|
fh.write(f"ACDL_AWS_ACCESS_KEY_ID={new_id}\n")
|
||||||
|
fh.write(f"ACDL_AWS_SECRET_ACCESS_KEY={new_secret}\n")
|
||||||
|
fh.write(f"AWS_DEFAULT_REGION={region}\n")
|
||||||
|
os.chmod(env_file, 0o600)
|
||||||
|
print(f"rotated key written to {env_file} (chmod 600)", file=sys.stderr)
|
||||||
|
|
||||||
|
# Optionally upload to Gitea secrets.
|
||||||
|
gitea_token = os.environ.get("ACDL_GITEA_TOKEN")
|
||||||
|
if gitea_token:
|
||||||
|
import urllib.request
|
||||||
|
base = "https://git.cloudinit.dev/api/v1/repos/continuous-intelligence/acdl/actions/secrets"
|
||||||
|
for name, value in [("ACDL_AWS_ACCESS_KEY_ID", new_id),
|
||||||
|
("ACDL_AWS_SECRET_ACCESS_KEY", new_secret)]:
|
||||||
|
req = urllib.request.Request(
|
||||||
|
f"{base}/{name}",
|
||||||
|
data=json.dumps({"value": value}).encode(),
|
||||||
|
method="PUT",
|
||||||
|
headers={"Authorization": f"token {gitea_token}",
|
||||||
|
"Content-Type": "application/json"},
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
urllib.request.urlopen(req).read()
|
||||||
|
print(f"gitea: secret {name} uploaded", file=sys.stderr)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"gitea: secret {name} upload FAILED: {e}", file=sys.stderr)
|
||||||
|
else:
|
||||||
|
print("gitea: ACDL_GITEA_TOKEN not set; Gitea secret upload skipped (v1.2 hardening)", file=sys.stderr)
|
||||||
|
|
||||||
|
print(f"OK: {user} now has exactly 1 active key: {new_id}")
|
||||||
|
PY
|
||||||
Executable
+29
@@ -0,0 +1,29 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# scripts/run_spike_plan.sh - run the v1.1 spike's real terraform plan against AWS.
|
||||||
|
#
|
||||||
|
# Uses the rotated spike key (D-039) from gitignored .env.secrets.
|
||||||
|
# Plan-only (no apply); -lock=false per D-P09-1 (the spike's DynamoDB
|
||||||
|
# outbox table PK is contractId, not Terraform's expected LockID; plan
|
||||||
|
# does not write state so locking is unnecessary; v1.2 creates a proper
|
||||||
|
# LockID-keyed acdl-tflock table).
|
||||||
|
set -u
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
cd "$ROOT"
|
||||||
|
|
||||||
|
ENV_FILE="$ROOT/.env.secrets"
|
||||||
|
[ -f "$ENV_FILE" ] || { echo "FAIL: .env.secrets missing (run scripts/rotate_spike_key.sh)" >&2; exit 1; }
|
||||||
|
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"
|
||||||
|
|
||||||
|
cd terraform/spike
|
||||||
|
echo "=== terraform init -lock=false -input=false ==="
|
||||||
|
terraform init -lock=false -input=false
|
||||||
|
echo "=== terraform validate ==="
|
||||||
|
terraform validate
|
||||||
|
echo "=== terraform plan -lock=false -input=false -out=tfplan ==="
|
||||||
|
terraform plan -lock=false -input=false -out=tfplan
|
||||||
|
echo "spike plan OK"
|
||||||
@@ -22,11 +22,15 @@ out=$(ACDL_GITEA_TOKEN= bash demo/scripts/run_demo.sh --no-upload 2>&1); rc=$?
|
|||||||
ok "demo/scripts/run_demo.sh --no-upload exits 0"
|
ok "demo/scripts/run_demo.sh --no-upload exits 0"
|
||||||
|
|
||||||
# --- Check 3: new top-level dirs exist and are scaffolded ---
|
# --- Check 3: new top-level dirs exist and are scaffolded ---
|
||||||
for d in platform schemas adapters terraform modules-ir; do
|
# Note: platform/ was renamed to acdl_platform/ in Phase 08 (stdlib shadow fix).
|
||||||
|
for d in acdl_platform schemas adapters terraform modules-ir; do
|
||||||
[ -d "$d" ] || fail "missing new top-level dir $d"
|
[ -d "$d" ] || fail "missing new top-level dir $d"
|
||||||
|
done
|
||||||
|
[ -f "acdl_platform/.gitkeep" ] || [ -f "acdl_platform/__init__.py" ] || fail "acdl_platform/ not scaffolded"
|
||||||
|
for d in schemas adapters terraform modules-ir; do
|
||||||
[ -f "$d/.gitkeep" ] || fail "missing $d/.gitkeep"
|
[ -f "$d/.gitkeep" ] || fail "missing $d/.gitkeep"
|
||||||
done
|
done
|
||||||
ok "new top-level dirs exist: platform/ schemas/ adapters/ terraform/ modules-ir/"
|
ok "new top-level dirs exist: acdl_platform/ schemas/ adapters/ terraform/ modules-ir/"
|
||||||
|
|
||||||
# --- Check 4: no stray v1.0 dirs left at repo root ---
|
# --- Check 4: no stray v1.0 dirs left at repo root ---
|
||||||
for stray in modules evidence-ui contracts contracts-repo ACDL_DEMO.md; do
|
for stray in modules evidence-ui contracts contracts-repo ACDL_DEMO.md; do
|
||||||
|
|||||||
+16
-26
@@ -7,43 +7,37 @@ fail() { echo "FAIL: $*" >&2; exit 1; }
|
|||||||
ok() { echo "ok: $*"; }
|
ok() { echo "ok: $*"; }
|
||||||
|
|
||||||
# --- Check 1: all 9 deliverable files exist ---
|
# --- Check 1: all 9 deliverable files exist ---
|
||||||
|
# Note: platform/ was renamed to acdl_platform/ in Phase 08 to avoid
|
||||||
|
# shadowing the stdlib platform module (boto3 imports uuid ->
|
||||||
|
# platform.system()).
|
||||||
for f in docs/architecture-v1.0.md \
|
for f in docs/architecture-v1.0.md \
|
||||||
schemas/ir.schema.json \
|
schemas/ir.schema.json \
|
||||||
schemas/policy_check_result.schema.json \
|
schemas/policy_check_result.schema.json \
|
||||||
schemas/contract.schema.json \
|
schemas/contract.schema.json \
|
||||||
platform/confidence_signal.py \
|
acdl_platform/confidence_signal.py \
|
||||||
platform/audit_ledger_design.md \
|
acdl_platform/audit_ledger_design.md \
|
||||||
platform/hitl_matrix_design.md \
|
acdl_platform/hitl_matrix_design.md \
|
||||||
platform/separation_of_duties.py \
|
acdl_platform/separation_of_duties.py \
|
||||||
adapters/terraform/policy/checkov_adapter.py; do
|
adapters/terraform/policy/checkov_adapter.py; do
|
||||||
[ -f "$f" ] || fail "missing $f"
|
[ -f "$f" ] || fail "missing $f"
|
||||||
done
|
done
|
||||||
ok "all 9 deliverable files exist"
|
ok "all 9 deliverable files exist"
|
||||||
|
|
||||||
# --- Check 2: 3 JSON Schemas are valid Draft 2020-12 ---
|
# --- Check 2: 3 JSON Schemas are valid Draft 2020-12 ---
|
||||||
# Run python from /tmp so the repo's `platform/` package does not shadow the
|
for s in schemas/ir.schema.json schemas/policy_check_result.schema.json schemas/contract.schema.json; do
|
||||||
# stdlib `platform` module (jsonschema imports uuid -> platform.system();
|
python3 -c "import json, jsonschema; jsonschema.Draft202012Validator.check_schema(json.load(open('$s')))" \
|
||||||
# our platform/ shadows it when cwd is repo root and on sys.path[0]).
|
|| fail "$s is not valid Draft 2020-12"
|
||||||
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
|
done
|
||||||
ok "3 JSON Schemas validate as Draft 2020-12"
|
ok "3 JSON Schemas validate as Draft 2020-12"
|
||||||
|
|
||||||
# --- Check 3: 3 .py files py_compile ---
|
# --- 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
|
for p in acdl_platform/confidence_signal.py acdl_platform/separation_of_duties.py adapters/terraform/policy/checkov_adapter.py; do
|
||||||
python3 -m py_compile "$p" || fail "$p py_compile failed"
|
python3 -m py_compile "$p" || fail "$p py_compile failed"
|
||||||
done
|
done
|
||||||
ok "3 .py files py_compile"
|
ok "3 .py files py_compile"
|
||||||
|
|
||||||
# --- Check 4: 3 .md design files non-empty ---
|
# --- 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
|
for m in acdl_platform/audit_ledger_design.md acdl_platform/hitl_matrix_design.md docs/architecture-v1.0.md; do
|
||||||
[ -s "$m" ] || fail "$m is empty"
|
[ -s "$m" ] || fail "$m is empty"
|
||||||
done
|
done
|
||||||
ok "3 .md design files non-empty"
|
ok "3 .md design files non-empty"
|
||||||
@@ -67,18 +61,14 @@ ok "D-040..D-044 present in PROJECT.md"
|
|||||||
|
|
||||||
# --- Check 8: spike contract validates against contract schema ---
|
# --- 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
|
echo '{"stack":"l2-static-asset","environment":"dev","inputs":{"bucket_name":"x","region":"us-east-1"}}' > /tmp/spike-contract.json
|
||||||
( cd /tmp && python3 -c "
|
python3 -c "import json, jsonschema; jsonschema.validate(json.load(open('/tmp/spike-contract.json')), json.load(open('schemas/contract.schema.json')))" \
|
||||||
import json, jsonschema
|
|| fail "spike contract does not validate against contract schema"
|
||||||
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"
|
ok "spike contract validates against contract schema"
|
||||||
|
|
||||||
# --- Check 9: minimal IR validates against IR 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
|
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 "
|
python3 -c "import json, jsonschema; jsonschema.validate(json.load(open('/tmp/spike-ir.json')), json.load(open('schemas/ir.schema.json')))" \
|
||||||
import json, jsonschema
|
|| fail "minimal IR does not validate against IR schema"
|
||||||
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"
|
ok "minimal IR validates against IR schema"
|
||||||
|
|
||||||
echo "VERIFIED — Phase 07: architecture v1.0 finalized; 6 files authored + 11 decisions resolved"
|
echo "VERIFIED — Phase 07: architecture v1.0 finalized; 6 files authored + 11 decisions resolved"
|
||||||
Executable
+80
@@ -0,0 +1,80 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# scripts/verify_phase08.sh - Phase 08 aws-bootstrap gate.
|
||||||
|
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)"
|
||||||
|
|
||||||
|
# Confirm .env.secrets + .bootstrap_state.json are gitignored.
|
||||||
|
git check-ignore -q "$ENV_FILE" || fail ".env.secrets is not gitignored"
|
||||||
|
git check-ignore -q terraform/bootstrap/.bootstrap_state.json || \
|
||||||
|
fail "terraform/bootstrap/.bootstrap_state.json is not gitignored"
|
||||||
|
ok ".env.secrets + .bootstrap_state.json are gitignored"
|
||||||
|
|
||||||
|
# Source the rotated spike key.
|
||||||
|
set -a
|
||||||
|
. "$ENV_FILE"
|
||||||
|
set +a
|
||||||
|
: "${ACDL_AWS_ACCESS_KEY_ID:?ACDL_AWS_ACCESS_KEY_ID missing in .env.secrets}"
|
||||||
|
: "${ACDL_AWS_SECRET_ACCESS_KEY:?ACDL_AWS_SECRET_ACCESS_KEY missing in .env.secrets}"
|
||||||
|
: "${AWS_DEFAULT_REGION:?AWS_DEFAULT_REGION missing in .env.secrets}"
|
||||||
|
|
||||||
|
export AWS_ACCESS_KEY_ID="$ACDL_AWS_ACCESS_KEY_ID"
|
||||||
|
export AWS_SECRET_ACCESS_KEY="$ACDL_AWS_SECRET_ACCESS_KEY"
|
||||||
|
export AWS_DEFAULT_REGION
|
||||||
|
|
||||||
|
# --- Check 1: caller identity is acdl-spike-runner (NOT root) ---
|
||||||
|
ARN=$(python3 <<'PY'
|
||||||
|
import boto3, json
|
||||||
|
s = boto3.Session(region_name='us-east-1')
|
||||||
|
print(s.client('sts').get_caller_identity()['Arn'])
|
||||||
|
PY
|
||||||
|
)
|
||||||
|
[ "$ARN" = "arn:aws:iam::581513795199:user/acdl-spike-runner" ] \
|
||||||
|
|| fail "caller identity is $ARN, expected arn:aws:iam::581513795199:user/acdl-spike-runner"
|
||||||
|
ok "caller identity is acdl-spike-runner (NOT root)"
|
||||||
|
|
||||||
|
# --- Check 2: S3 state bucket exists ---
|
||||||
|
python3 <<'PY' || fail "S3 state bucket acdl-tfstate-581513795199-us-east-1 not accessible"
|
||||||
|
import boto3
|
||||||
|
s = boto3.Session(region_name='us-east-1')
|
||||||
|
s.client('s3').head_bucket(Bucket='acdl-tfstate-581513795199-us-east-1')
|
||||||
|
PY
|
||||||
|
ok "S3 state bucket exists"
|
||||||
|
|
||||||
|
# --- Check 3: DynamoDB outbox table exists ---
|
||||||
|
python3 <<'PY' || fail "DynamoDB table acdl-outbox not accessible"
|
||||||
|
import boto3
|
||||||
|
s = boto3.Session(region_name='us-east-1')
|
||||||
|
s.client('dynamodb').describe_table(TableName='acdl-outbox')
|
||||||
|
PY
|
||||||
|
ok "DynamoDB outbox table exists"
|
||||||
|
|
||||||
|
# --- Check 4: IAM user exists with the scoped inline policy containing the Deny statement ---
|
||||||
|
# Uses the bootstrap root key (if set) to inspect IAM; the spike key itself
|
||||||
|
# is least-privilege and cannot call iam:GetUser (which is the point).
|
||||||
|
if [ -n "${ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID:-}" ]; then
|
||||||
|
AWS_ACCESS_KEY_ID="$ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID" \
|
||||||
|
AWS_SECRET_ACCESS_KEY="$ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY" \
|
||||||
|
AWS_DEFAULT_REGION="$AWS_DEFAULT_REGION" \
|
||||||
|
python3 <<'PY' || fail "IAM user acdl-spike-runner missing or policy lacks DenyEverythingElse"
|
||||||
|
import boto3, json
|
||||||
|
s = boto3.Session(region_name='us-east-1')
|
||||||
|
iam = s.client('iam')
|
||||||
|
iam.get_user(UserName='acdl-spike-runner')
|
||||||
|
doc = iam.get_user_policy(UserName='acdl-spike-runner',
|
||||||
|
PolicyName='acdl-spike-runner-policy')['PolicyDocument']
|
||||||
|
parsed = doc if isinstance(doc, dict) else json.loads(doc)
|
||||||
|
sids = [st.get('Sid', '') for st in parsed['Statement']]
|
||||||
|
assert 'DenyEverythingElse' in sids, 'DenyEverythingElse statement missing'
|
||||||
|
PY
|
||||||
|
ok "IAM user acdl-spike-runner exists with the scoped Deny-everything-else policy (verified via bootstrap key)"
|
||||||
|
else
|
||||||
|
echo "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)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "VERIFIED — Phase 08: AWS bootstrap complete; spike key rotated; D-034 closed (user must rotate the root key manually now)"
|
||||||
Executable
+77
@@ -0,0 +1,77 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# scripts/verify_phase09.sh - Phase 09 v1-spike-ir-and-l1-and-adapter gate.
|
||||||
|
set -u
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
cd "$ROOT"
|
||||||
|
fail() { echo "FAIL: $*" >&2; exit 1; }
|
||||||
|
ok() { echo "ok: $*"; }
|
||||||
|
|
||||||
|
# --- Check 1: L1 module files exist ---
|
||||||
|
for f in modules-ir/l1/l1-s3/interface.json \
|
||||||
|
modules-ir/l1/l1-s3/spike_instance.json \
|
||||||
|
modules-ir/l1/l1-s3/README.md \
|
||||||
|
modules-ir/registry.json \
|
||||||
|
adapters/terraform/adapter.py \
|
||||||
|
terraform/spike/main.tf \
|
||||||
|
terraform/spike/terraform.tf \
|
||||||
|
terraform/spike/providers.tf \
|
||||||
|
scripts/run_spike_plan.sh; do
|
||||||
|
[ -f "$f" ] || fail "missing $f"
|
||||||
|
done
|
||||||
|
ok "all 9 deliverable files exist"
|
||||||
|
|
||||||
|
# --- Check 2: spike_instance.json validates against ir.schema.json ---
|
||||||
|
( cd /tmp && python3 -c "
|
||||||
|
import json, jsonschema
|
||||||
|
inst = json.load(open('$ROOT/modules-ir/l1/l1-s3/spike_instance.json'))
|
||||||
|
schema = json.load(open('$ROOT/schemas/ir.schema.json'))
|
||||||
|
jsonschema.validate(inst, schema)
|
||||||
|
" ) || fail "spike_instance.json does not validate against ir.schema.json"
|
||||||
|
ok "spike_instance.json validates against ir.schema.json"
|
||||||
|
|
||||||
|
# --- Check 3: registry has the l1-s3@1.0.0 entry ---
|
||||||
|
python3 -c "
|
||||||
|
import json
|
||||||
|
r = json.load(open('modules-ir/registry.json'))
|
||||||
|
assert 'l1-s3' in r and '1.0.0' in r['l1-s3'], 'l1-s3@1.0.0 missing'
|
||||||
|
print('l1-s3@1.0.0 present')
|
||||||
|
" || fail "registry missing l1-s3@1.0.0"
|
||||||
|
ok "registry has l1-s3@1.0.0"
|
||||||
|
|
||||||
|
# --- Check 4: adapter py_compiles + generates terraform containing aws_s3_bucket ---
|
||||||
|
python3 -m py_compile adapters/terraform/adapter.py || fail "adapter.py py_compile failed"
|
||||||
|
TMP=$(mktemp -d)
|
||||||
|
python3 adapters/terraform/adapter.py modules-ir/l1/l1-s3/spike_instance.json "$TMP" 2>/dev/null
|
||||||
|
grep -q 'resource "aws_s3_bucket"' "$TMP/main.tf" || fail "adapter did not emit aws_s3_bucket resource"
|
||||||
|
grep -q 'output "bucket_arn"' "$TMP/main.tf" || fail "adapter did not emit bucket_arn output"
|
||||||
|
ok "adapter.py py_compiles + emits aws_s3_bucket + bucket_arn output"
|
||||||
|
|
||||||
|
# --- Check 5: generated terraform/spike/*.tf match a fresh adapter run (D-P09-4 reproducibility) ---
|
||||||
|
diff "$TMP/main.tf" terraform/spike/main.tf || fail "terraform/spike/main.tf is stale (differs from a fresh adapter run)"
|
||||||
|
diff "$TMP/terraform.tf" terraform/spike/terraform.tf || fail "terraform/spike/terraform.tf is stale"
|
||||||
|
diff "$TMP/providers.tf" terraform/spike/providers.tf || fail "terraform/spike/providers.tf is stale"
|
||||||
|
ok "terraform/spike/*.tf match a fresh adapter run (reproducible)"
|
||||||
|
rm -rf "$TMP"
|
||||||
|
|
||||||
|
# --- Check 6: no long-lived credential (AKIA) in committed files ---
|
||||||
|
# Skip .terraform/ (provider binaries contain AKIA bytes; gitignored anyway).
|
||||||
|
if grep -rn --exclude-dir=.terraform "AKIA" terraform/spike/ adapters/ modules-ir/ 2>/dev/null; then
|
||||||
|
fail "AKIA key id found in committed files (terraform/spike/ adapters/ modules-ir/)"
|
||||||
|
fi
|
||||||
|
ok "no AKIA in committed files (excluding .terraform/ provider binaries)"
|
||||||
|
|
||||||
|
# --- Check 7: .env.secrets + terraform working artifacts are gitignored ---
|
||||||
|
git check-ignore -q .env.secrets || fail ".env.secrets not gitignored"
|
||||||
|
git check-ignore -q terraform/spike/.terraform/ || fail "terraform/spike/.terraform/ not gitignored"
|
||||||
|
git check-ignore -q terraform/spike/tfplan || fail "terraform/spike/tfplan not gitignored"
|
||||||
|
ok "secrets + TF working artifacts gitignored"
|
||||||
|
|
||||||
|
# --- Check 8: real terraform plan against AWS succeeds (uses rotated spike key) ---
|
||||||
|
bash scripts/run_spike_plan.sh > /tmp/verify_phase09_plan.log 2>&1 || {
|
||||||
|
cat /tmp/verify_phase09_plan.log >&2
|
||||||
|
fail "scripts/run_spike_plan.sh failed (see /tmp/verify_phase09_plan.log)"
|
||||||
|
}
|
||||||
|
grep -q "spike plan OK" /tmp/verify_phase09_plan.log || fail "run_spike_plan.sh did not print 'spike plan OK'"
|
||||||
|
ok "real terraform plan against AWS succeeded (rotated spike key, plan-only, -lock=false)"
|
||||||
|
|
||||||
|
echo "VERIFIED — Phase 09: IR + l1-s3 + Terraform adapter; real terraform plan succeeds"
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
# ACDL v1.1 Spike — AWS Bootstrap Runbook
|
||||||
|
|
||||||
|
Phase 08 bootstraps the AWS substrate for the v1.1 spike. It uses the
|
||||||
|
**root account credential for account 581513795199 exactly once**, then
|
||||||
|
closes D-034 by having the user manually rotate the root key afterward.
|
||||||
|
|
||||||
|
> **Spike scope (D-039):** the spike uses a per-run-rotated IAM *user* key
|
||||||
|
> (`acdl-spike-runner`), NOT OIDC. Real OIDC federation is deferred to
|
||||||
|
> v1.2 (blocked on go-gitea/gitea#36988 — Gitea Actions does not support
|
||||||
|
> `id-token: write`). The `acdl-spike-runner` user + its key are deleted
|
||||||
|
> in v1.2 cleanup when the OIDC role lands.
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
1. **Set the bootstrap root key in env** (never commit, never echo):
|
||||||
|
```bash
|
||||||
|
export ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID="<root key>"
|
||||||
|
export ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY="<root secret>"
|
||||||
|
export AWS_DEFAULT_REGION="us-east-1"
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Create the state backend** (S3 bucket + DynamoDB outbox table):
|
||||||
|
```bash
|
||||||
|
python3 terraform/bootstrap/create_state_backend.py
|
||||||
|
```
|
||||||
|
Idempotent; writes `terraform/bootstrap/.bootstrap_state.json` marker.
|
||||||
|
|
||||||
|
3. **Create the IAM user + scoped policy + initial key**:
|
||||||
|
```bash
|
||||||
|
python3 terraform/bootstrap/create_iam_user.py
|
||||||
|
```
|
||||||
|
Prints `ACDL_AWS_ACCESS_KEY_ID=<...>` + `ACDL_AWS_SECRET_ACCESS_KEY=<...>`
|
||||||
|
to stdout (capture if you want the initial key; `rotate_spike_key.sh`
|
||||||
|
creates a fresh one anyway).
|
||||||
|
|
||||||
|
4. **Rotate the spike key** (creates a new key, deactivates+deletes old,
|
||||||
|
writes the new key to gitignored `.env.secrets`):
|
||||||
|
```bash
|
||||||
|
bash scripts/rotate_spike_key.sh
|
||||||
|
```
|
||||||
|
Optionally uploads to Gitea Actions secrets if `ACDL_GITEA_TOKEN` is set.
|
||||||
|
|
||||||
|
5. **Verify**:
|
||||||
|
```bash
|
||||||
|
bash scripts/verify_phase08.sh
|
||||||
|
```
|
||||||
|
Asserts: caller identity is `acdl-spike-runner` (not root); S3 bucket +
|
||||||
|
DynamoDB table + IAM user + scoped policy all exist; `.env.secrets` +
|
||||||
|
`.bootstrap_state.json` are gitignored.
|
||||||
|
|
||||||
|
6. **MANUAL — D-034 closure:** rotate/deactivate the **root** key in the
|
||||||
|
AWS IAM console (the user does this, not the script). The bootstrap
|
||||||
|
root key has now served its one-shot purpose; the spike uses the
|
||||||
|
rotated `acdl-spike-runner` key for Phases 09-10.
|
||||||
|
|
||||||
|
## What the spike uses for Phases 09-10
|
||||||
|
|
||||||
|
- **State backend:** S3 bucket `acdl-tfstate-581513795199-us-east-1` +
|
||||||
|
DynamoDB table `acdl-outbox` (one table for both lock + outbox, D-P08-1).
|
||||||
|
- **Auth:** the rotated `acdl-spike-runner` key in `.env.secrets`
|
||||||
|
(gitignored, chmod 600). Re-rotate after each spike run via
|
||||||
|
`rotate_spike_key.sh` (D-039).
|
||||||
|
|
||||||
|
## Spike scope vs v1.2 boundary
|
||||||
|
|
||||||
|
| Concern | Spike (Phase 08) | v1.2 |
|
||||||
|
|---------|------------------|------|
|
||||||
|
| AWS auth | per-run-rotated long-lived key (D-039 waiver) | real OIDC federation (go-gitea/gitea#36988) |
|
||||||
|
| IAM | minimal user `acdl-spike-runner` + scoped policy | OIDC role + trust policy (no user, no key) |
|
||||||
|
| State backend | S3 + DynamoDB single-region (us-east-1) | multi-region |
|
||||||
|
| Secret storage | gitignored `.env.secrets` + optional Gitea secret | Gitea OIDC-issued web-identity token (no secret) |
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
"""Create the ACDL v1.1 spike IAM user + scoped inline policy + initial key.
|
||||||
|
|
||||||
|
Idempotent: skips user creation if the user exists; creates an initial
|
||||||
|
access key if none active exists. Prints the key to stdout for the
|
||||||
|
orchestrator to capture (NEVER committed):
|
||||||
|
ACDL_AWS_ACCESS_KEY_ID=<...>
|
||||||
|
ACDL_AWS_SECRET_ACCESS_KEY=<...>
|
||||||
|
|
||||||
|
Run with the bootstrap root key in env:
|
||||||
|
ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID / ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY
|
||||||
|
AWS_DEFAULT_REGION (defaults to us-east-1)
|
||||||
|
|
||||||
|
The inline policy is read from spike_runner_policy.json (next to this
|
||||||
|
file). The account id + region are already substituted in the policy file
|
||||||
|
for account 581513795199 + us-east-1; this script does not substitute
|
||||||
|
further (the policy file is spike-specific).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import boto3
|
||||||
|
|
||||||
|
|
||||||
|
REGION = os.environ.get("AWS_DEFAULT_REGION", "us-east-1")
|
||||||
|
USER_NAME = "acdl-spike-runner"
|
||||||
|
POLICY_NAME = "acdl-spike-runner-policy"
|
||||||
|
POLICY_FILE = os.path.join(os.path.dirname(__file__), "spike_runner_policy.json")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
session = boto3.Session(
|
||||||
|
aws_access_key_id=os.environ["ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID"],
|
||||||
|
aws_secret_access_key=os.environ["ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY"],
|
||||||
|
region_name=REGION,
|
||||||
|
)
|
||||||
|
iam = session.client("iam")
|
||||||
|
|
||||||
|
# --- IAM user (idempotent) ---
|
||||||
|
try:
|
||||||
|
iam.get_user(UserName=USER_NAME)
|
||||||
|
print(f"iam: user {USER_NAME} already exists")
|
||||||
|
except iam.exceptions.NoSuchEntityException:
|
||||||
|
iam.create_user(UserName=USER_NAME)
|
||||||
|
print(f"iam: created user {USER_NAME}")
|
||||||
|
|
||||||
|
# --- Inline policy (idempotent: put_user_policy overwrites) ---
|
||||||
|
with open(POLICY_FILE, "r") as fh:
|
||||||
|
policy_doc = fh.read()
|
||||||
|
iam.put_user_policy(
|
||||||
|
UserName=USER_NAME,
|
||||||
|
PolicyName=POLICY_NAME,
|
||||||
|
PolicyDocument=policy_doc,
|
||||||
|
)
|
||||||
|
print(f"iam: inline policy {POLICY_NAME} attached to {USER_NAME}")
|
||||||
|
|
||||||
|
# --- Initial access key (create only if no active key exists) ---
|
||||||
|
keys = iam.list_access_keys(UserName=USER_NAME).get("AccessKeyMetadata", [])
|
||||||
|
active = [k for k in keys if k["Status"] == "Active"]
|
||||||
|
if active:
|
||||||
|
print(f"iam: {USER_NAME} already has {len(active)} active key(s); not creating a new one")
|
||||||
|
print(" (use scripts/rotate_spike_key.sh to rotate)")
|
||||||
|
return
|
||||||
|
new_key = iam.create_access_key(UserName=USER_NAME)["AccessKey"]
|
||||||
|
print("ACDL_AWS_ACCESS_KEY_ID=" + new_key["AccessKeyId"])
|
||||||
|
print("ACDL_AWS_SECRET_ACCESS_KEY=" + new_key["SecretAccessKey"])
|
||||||
|
print(f"iam: created initial access key {new_key['AccessKeyId']} for {USER_NAME}", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
"""Create the ACDL v1.1 spike AWS state backend (idempotent).
|
||||||
|
|
||||||
|
- S3 bucket acdl-tfstate-<account_id>-us-east-1 (versioning enabled).
|
||||||
|
- DynamoDB table acdl-outbox (PAY_PER_REQUEST; PK contractId, SK
|
||||||
|
eventType#eventTs) — used for BOTH Terraform state locking AND the
|
||||||
|
evidence outbox (D-P08-1).
|
||||||
|
|
||||||
|
Run with the bootstrap root key in env:
|
||||||
|
ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID / ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY
|
||||||
|
AWS_DEFAULT_REGION (defaults to us-east-1)
|
||||||
|
|
||||||
|
Writes terraform/bootstrap/.bootstrap_state.json (gitignored bookkeeping).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import boto3
|
||||||
|
|
||||||
|
|
||||||
|
REGION = os.environ.get("AWS_DEFAULT_REGION", "us-east-1")
|
||||||
|
STATE_BUCKET = "acdl-tfstate-581513795199-us-east-1"
|
||||||
|
OUTBOX_TABLE = "acdl-outbox"
|
||||||
|
ACCOUNT_ID = "581513795199"
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
session = boto3.Session(
|
||||||
|
aws_access_key_id=os.environ["ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID"],
|
||||||
|
aws_secret_access_key=os.environ["ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY"],
|
||||||
|
region_name=REGION,
|
||||||
|
)
|
||||||
|
s3 = session.client("s3", region_name=REGION)
|
||||||
|
dyn = session.client("dynamodb", region_name=REGION)
|
||||||
|
|
||||||
|
# --- S3 state bucket (idempotent) ---
|
||||||
|
try:
|
||||||
|
s3.head_bucket(Bucket=STATE_BUCKET)
|
||||||
|
print(f"s3: bucket {STATE_BUCKET} already exists")
|
||||||
|
except Exception:
|
||||||
|
kwargs = {"Bucket": STATE_BUCKET}
|
||||||
|
if REGION != "us-east-1":
|
||||||
|
kwargs["CreateBucketConfiguration"] = {"LocationConstraint": REGION}
|
||||||
|
s3.create_bucket(**kwargs)
|
||||||
|
print(f"s3: created bucket {STATE_BUCKET}")
|
||||||
|
# Enable versioning (idempotent)
|
||||||
|
s3.put_bucket_versioning(
|
||||||
|
Bucket=STATE_BUCKET,
|
||||||
|
VersioningConfiguration={"Status": "Enabled"},
|
||||||
|
)
|
||||||
|
print(f"s3: versioning enabled on {STATE_BUCKET}")
|
||||||
|
|
||||||
|
# --- DynamoDB outbox table (idempotent) ---
|
||||||
|
try:
|
||||||
|
dyn.describe_table(TableName=OUTBOX_TABLE)
|
||||||
|
print(f"dynamodb: table {OUTBOX_TABLE} already exists")
|
||||||
|
except dyn.exceptions.ResourceNotFoundException:
|
||||||
|
dyn.create_table(
|
||||||
|
TableName=OUTBOX_TABLE,
|
||||||
|
BillingMode="PAY_PER_REQUEST",
|
||||||
|
AttributeDefinitions=[
|
||||||
|
{"AttributeName": "contractId", "AttributeType": "S"},
|
||||||
|
{"AttributeName": "eventType#eventTs", "AttributeType": "S"},
|
||||||
|
],
|
||||||
|
KeySchema=[
|
||||||
|
{"AttributeName": "contractId", "KeyType": "HASH"},
|
||||||
|
{"AttributeName": "eventType#eventTs", "KeyType": "RANGE"},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
print(f"dynamodb: created table {OUTBOX_TABLE}")
|
||||||
|
dyn.get_waiter("table_exists").wait(TableName=OUTBOX_TABLE)
|
||||||
|
|
||||||
|
marker = {
|
||||||
|
"account_id": ACCOUNT_ID,
|
||||||
|
"bucket_name": STATE_BUCKET,
|
||||||
|
"table_name": OUTBOX_TABLE,
|
||||||
|
"region": REGION,
|
||||||
|
"created_at": datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||||
|
}
|
||||||
|
with open(os.path.join(os.path.dirname(__file__), ".bootstrap_state.json"), "w") as fh:
|
||||||
|
json.dump(marker, fh, indent=2)
|
||||||
|
print("bootstrap state marker written:", marker)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"Version": "2012-10-17",
|
||||||
|
"Statement": [
|
||||||
|
{
|
||||||
|
"Sid": "SpikeStateBucketReadWrite",
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Action": [
|
||||||
|
"s3:PutObject",
|
||||||
|
"s3:GetObject",
|
||||||
|
"s3:DeleteObject",
|
||||||
|
"s3:ListBucket",
|
||||||
|
"s3:GetBucketLocation",
|
||||||
|
"s3:GetBucketVersioning"
|
||||||
|
],
|
||||||
|
"Resource": [
|
||||||
|
"arn:aws:s3:::acdl-tfstate-581513795199-us-east-1",
|
||||||
|
"arn:aws:s3:::acdl-tfstate-581513795199-us-east-1/*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Sid": "SpikeOutboxTableReadWrite",
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Action": [
|
||||||
|
"dynamodb:GetItem",
|
||||||
|
"dynamodb:PutItem",
|
||||||
|
"dynamodb:DeleteItem",
|
||||||
|
"dynamodb:UpdateItem",
|
||||||
|
"dynamodb:Query",
|
||||||
|
"dynamodb:Scan",
|
||||||
|
"dynamodb:DescribeTable"
|
||||||
|
],
|
||||||
|
"Resource": "arn:aws:dynamodb:us-east-1:581513795199:table/acdl-outbox"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Sid": "SpikeStsSelfIdentify",
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Action": "sts:GetCallerIdentity",
|
||||||
|
"Resource": "*"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Sid": "DenyEverythingElse",
|
||||||
|
"Effect": "Deny",
|
||||||
|
"Action": "*",
|
||||||
|
"NotResource": [
|
||||||
|
"arn:aws:s3:::acdl-tfstate-581513795199-us-east-1",
|
||||||
|
"arn:aws:s3:::acdl-tfstate-581513795199-us-east-1/*",
|
||||||
|
"arn:aws:dynamodb:us-east-1:581513795199:table/acdl-outbox"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
resource "aws_s3_bucket" "s3" {
|
||||||
|
bucket = "acdl-spike-bucket"
|
||||||
|
versioning {
|
||||||
|
enabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output "bucket_arn" {
|
||||||
|
value = aws_s3_bucket.s3.arn
|
||||||
|
}
|
||||||
|
|
||||||
|
output "bucket_name" {
|
||||||
|
value = aws_s3_bucket.s3.id
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
provider "aws" {
|
||||||
|
region = "us-east-1"
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
terraform {
|
||||||
|
required_version = ">= 1.9, < 1.10"
|
||||||
|
required_providers {
|
||||||
|
aws = {
|
||||||
|
source = "hashicorp/aws"
|
||||||
|
version = "~> 5.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
backend "s3" {
|
||||||
|
bucket = "acdl-tfstate-581513795199-us-east-1"
|
||||||
|
key = "spike/l1-s3/terraform.tfstate"
|
||||||
|
region = "us-east-1"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user