Squash merge of phase/03-l2-modules-and-core-scripts; 4 L2s + 5 core scripts; verify_phase03.sh green.
14 KiB
ACDL — Architecture (initial)
Initial architecture for the ACDL demo. May be incomplete; refined at phase boundaries.
Overview
The demo is a three-repo, stub-driven system that simulates an autonomous cloud delivery platform. No real cloud or AI is used; every "infrastructure" action is a bash/Python stub that emits structured evidence. The platform is driven by either a developer-supplied contract.yaml (L3A) or a natural-language GitHub Issue parsed by a keyword script (L3B), then flows through an autonomous Dev stage, manual QA and Prod approval gates, and finally publishes a hash-chained audit trail to a Pages site.
┌──────────────── acdl-contracts ─────────────────┐
Developer ───▶ │ commit contract.yaml Issue (NL intent) │
└────────────┬───────────────────┬────────────────┘
│ (push) │ (issue opened)
▼ ▼
┌─────────────────┐ ┌──────────────────────┐
│ reusable │ │ issue workflow → │
│ pipeline │ │ l3b_agent_stub.py → │
│ (acdl repo) │ │ contract.yaml → push │
└────────┬────────┘ └──────────────────────┘
│
┌────────────────────┼────────────────────┐
▼ ▼ ▼
Dev (autonomous) QA (approval) Prod (approval)
mock_executor.sh environment gate environment gate
policy_checker.py
confidence_signal.py
│
▼
evidence_writer.py ──▶ audit.json (hash-chained) ──▶ acdl-evidence
│
▼
index.html (Pages)
timeline UI
Components
| Name | Description | Boundaries | Depends On |
|---|---|---|---|
acdl repo |
Platform meta repo: reusable workflows, L1/L2 stub modules, core scripts | Owns workflows + stubs; does not hold contracts or evidence | — |
| L1 modules | Single-purpose infra primitives (EKS Fargate, IAM, Lambda, API Gateway, EventBridge, SQS, S3, CloudWatch) | One folder per L1; manifest.yaml + mock_apply.sh; do not compose with other L1s |
acdl repo |
| L2 modules | Composed stacks (invoice, commodity-price-feed, energy-analytics-api, regulatory-reporting) | Reference L1s by name; max depth 5; expressed as a composition manifest | L1 modules |
mock_executor.sh |
Reads an L2 composition, invokes each L1 mock_apply.sh, writes state.json |
Bash; reads L2 manifest + L1 manifests | L1/L2 modules |
policy_checker.py |
Reads contract.yaml; fails on forbidden keys (e.g. public-ingress: true) |
Python; emits POLICY_VIOLATION:<REASON> or pass |
contract.yaml |
confidence_signal.py |
Base 0.90; on policy failure drops to 0.40 and echoes reason | Python; calls policy_checker | policy_checker.py |
evidence_writer.py |
Appends an event to audit.json, links to previous event via SHA-256 chain |
Python; canonical-JSON hashing | audit.json |
l3b_agent_stub.py |
Parses Issue text by keywords, emits contract.yaml |
Python keyword map; no external APIs | contract.yaml schema |
acdl-contracts repo |
Developer + agentic entry surface; holds contracts + issue workflow | Triggers main pipeline on push | acdl reusable workflow |
acdl-evidence repo |
Pages host for audit.json + index.html timeline |
Read-only for the pipeline; written at finalize stage | evidence_writer.py output |
| Reusable pipeline workflow | Dev → QA → Prod → Finalize stages with environment gates | Gitea Actions; calls core scripts | All core scripts |
Data Flow
- A
contract.yamlarrives either by direct push (L3A) or by the issue workflow runningl3b_agent_stub.py(L3B). - Push to
acdl-contractstriggers the reusable pipeline in theacdlrepo. - Dev stage:
policy_checker.pyvalidates the contract;mock_executor.shapplies the L2 composition's L1s;confidence_signal.pycomputes the score;evidence_writer.pyrecords each step. If score < 0.50, the stage fails and evidence records the rejection. - QA stage: the workflow pauses on the
qaenvironment; a human approves. - Prod stage: same gate on the
prodenvironment. - Finalize: the workflow commits the updated
audit.jsontoacdl-evidence; Pages republishesindex.html, which fetches and renders the timeline.
Build Order
- Repo scaffolding: create
acdl-contractsandacdl-evidencein the org; seedacdldirectory layout. - L1 modules (8 stubs).
- L2 modules (4 compositions).
- Core scripts (
mock_executor.sh,policy_checker.py,confidence_signal.py,evidence_writer.py,l3b_agent_stub.py). - Reusable pipeline workflow (Dev → QA → Prod → Finalize) + environment gates.
- Issue-triggered L3B workflow in
acdl-contracts. - Evidence UI (
index.html+ Pages config). - Demo dry-run + the four scripted acts.
Gitea API Surface (Phase 01 research)
Authoritative findings from the Gitea docs (added in RESEARCH; supersedes any GitHub-Pages / GitHub-Environments assumptions carried over from the spec):
| Capability | Gitea support | ACDL approach |
|---|---|---|
| Org-scoped repo create | POST /api/v1/orgs/{org}/repos (CreateRepoOption) |
Used to create acdl-contracts + acdl-evidence |
| Native Pages | None (no [pages] config section) |
Serve acdl-evidence via raw file URLs: https://git.cloudinit.dev/continuous-intelligence/acdl-evidence/raw/branch/main/index.html; index.html fetches audit.json from the same raw path. Requires [cors] ENABLED=true on the server if the UI is loaded cross-origin. |
| Environments API | None; jobs.<id>.environment is ignored by act_runner |
Model QA/Prod gates as workflow_dispatch approval inputs (D-004 / D-013); optionally create qa and prod branches as a visible stand-in |
repository_dispatch trigger |
Not supported | Cross-repo trigger via workflow_dispatch API: POST /api/v1/repos/{owner}/{repo}/actions/workflows/{workflow_id}/dispatches called from a step using $GITEA_TOKEN |
Reusable workflows (workflow_call) |
Supported | acdl/.gitea/workflows/pipeline.yml called via uses: continuous-intelligence/acdl/.gitea/workflows/pipeline.yml@milestone/v1.0-initial |
workflow_dispatch |
Supported (trigger + API) | Used for the manual-approval fallback and the issue workflow's cross-repo trigger |
issues.opened trigger |
Supported | Drives the L3B issue-trigger workflow in acdl-contracts |
act_runner labels |
Single label only (runs-on: ubuntu-latest) |
All workflows use runs-on: ubuntu-latest |
| Context | ${{ gitea.* }} and ${{ github.* }} both work |
Workflows use gitea.* for clarity |
Branch pinning rule
The reusable workflow in the acdl repo lives on milestone/v1.0-initial
(that is the repo's default branch). uses: references from acdl-contracts
must pin to @milestone/v1.0-initial, not @main (the acdl repo has no
main branch). The new repos acdl-contracts and acdl-evidence use
default_branch: "main" (D-015) so their default branch exists immediately
for pushes.
Default verification toolchain
There is no package.json; ACDL is bash + python stubs. The verification gate
substitutes bash -n and python -m py_compile for npm run typecheck, and
per-phase scripts/verify_phaseNN.sh for npm test. npm run build is a
no-op (no build step). See PERSONAS.md / VERIFICATION note.
L1 module schema (Phase 02 research)
Each L1 module lives at modules/l1/<name>/ with exactly two files:
manifest.yaml— declares the L1's identity + a flatinputs:map. Schema (D-017):name: l1-eks-fargate # matches the folder name kind: l1 # literal "l1"; substrate-agnostic description: <one-line> inputs: <key>: description: <one-line> type: string # only "string" allowed (flat, max-depth-1)mock_apply.sh— uniform stub per D-007 + D-018:#!/usr/bin/env bash set -euo pipefail echo "[L1: <name>] applying..." sleep 1 echo "[L1: <name>] OK" exit 0mock_apply.shdoes NOT read input values; the manifest is for traceability and for Phase 03'smock_executor.shto enumerate the L1s in an L2.
L1 list (fixed per REQ-02 / D-019)
| Folder | Description |
|---|---|
l1-eks-fargate |
Serverless container compute substrate |
l1-iam-role |
Identity and access role primitive |
l1-lambda |
Event-driven function primitive |
l1-api-gateway |
HTTP routing primitive |
l1-eventbridge |
Event bus primitive |
l1-sqs |
Queue primitive |
l1-s3 |
Object store primitive |
l1-cloudwatch |
Observability primitive |
L1 modules are single-purpose, substrate-agnostic, max-depth-1 (per PROJECT.md Constraints). They do not compose with other L1s.
L2 module schema + core scripts (Phase 03 research)
L2 manifest.yaml schema (D-020)
name: l2-commodity-price-feed # matches the folder name
kind: l2 # literal "l2"
description: <one-line>
l1s: # ordered list of L1 references
- name: l1-eks-fargate # MUST match an existing L1 folder name
inputs:
cluster_name: price-feed-cluster
region: us-east-1
cpu_arch: arm64
- name: l1-lambda
inputs:
function_name: price-ingest
runtime: python3.11
handler: index.handler
# ... up to 5 L1 references per L2 (max-depth-5 per REQ-05; L2->L1 is depth 1)
L2s reference L1s by name only (no path); mock_executor.sh resolves
the name to modules/l1/<name>/.
L2 list (fixed per REQ-04)
| Folder | Description | L1s (per S&P Global Energy / Platts use cases) |
|---|---|---|
l2-invoice-service |
Billing + invoicing microservice | l1-eks-fargate, l1-iam-role, l1-lambda, l1-sqs, l1-s3 |
l2-commodity-price-feed |
Real-time price ingestion | l1-eks-fargate, l1-lambda, l1-api-gateway, l1-eventbridge, l1-s3 |
l2-energy-analytics-api |
Historical query API | l1-eks-fargate, l1-api-gateway, l1-lambda, l1-s3, l1-cloudwatch |
l2-regulatory-reporting |
Compliance + reporting | l1-eks-fargate, l1-iam-role, l1-lambda, l1-sqs, l1-s3 |
Each L2 references exactly 5 L1s (within the max-depth-5 constraint; L2→L1 is depth 1, so depth-5 is generous but the spec caps composition depth at 5 — the count is 5 to demonstrate a realistic composed stack).
contract.yaml schema (D-021)
stack: l2-commodity-price-feed # MUST match an existing L2 folder name
inputs: # top-level params for the L2 (optional)
environment: dev
owner: platform-team
public-ingress: false # bool; true triggers POLICY_VIOLATION:PUBLIC_INGRESS
The public-ingress key is the only policy-enforced field in Phase 03.
Phase 04's pipeline reads contract.yaml, runs policy_checker.py, then
mock_executor.sh to apply the L2.
state.json shape (D-022)
mock_executor.sh writes state.json to its working directory:
{
"l2": "l2-commodity-price-feed",
"l1s": [
{"name": "l1-eks-fargate", "applied": true, "exit_code": 0},
{"name": "l1-lambda", "applied": true, "exit_code": 0},
...
],
"contract": {
"stack": "l2-commodity-price-feed",
"inputs": {...},
"public-ingress": false
}
}
audit.json event + hash chain (D-023)
audit.json is a JSON array of event objects. evidence_writer.py
appends one event per call. Hash chain:
- Construct the event dict with
hashset to empty string. - Serialize via
json.dumps(event, sort_keys=True, separators=(",", ":"))— canonical JSON (deterministic key order, no whitespace). - Compute
hash = sha256(canonical_json.encode("utf-8")).hexdigest(). - Set
event["hash"] = hash. - Append to
audit.json.
Genesis event (when audit.json is empty or missing):
{
"seq": 0,
"ts": "2026-07-21T13:00:00Z",
"stage": "genesis",
"event": "audit log initialized",
"prev_hash": "GENESIS",
"hash": "<sha256 of the canonical json of this event with hash empty>"
}
Subsequent events: seq = prev.seq + 1, prev_hash = prev.hash.
Core script I/O contracts
| Script | Input | Output | Exit |
|---|---|---|---|
mock_executor.sh |
<contract.yaml path> (argv[1]); reads L2 manifest from modules/l2/<contract.stack>/manifest.yaml |
writes state.json to cwd; prints per-L1 progress |
0 on all-L1s-pass; non-zero on any L1 failure |
policy_checker.py |
<contract.yaml path> (argv[1]) |
stdout: POLICY_PASS or POLICY_VIOLATION:PUBLIC_INGRESS |
0 on pass; 1 on violation |
confidence_signal.py |
<contract.yaml path> (argv[1]); calls policy_checker |
stdout: `{"score": 0.90 | 0.40, "reason": "..."}` |
evidence_writer.py |
argv: `--stage <dev | qa | prod |
l3b_agent_stub.py |
argv[1] = issue body text (or stdin if no argv); optional -o <path> (default stdout) |
writes a contract.yaml (D-021 schema) with stack set by the D-008 keyword map |
0 on success; 1 on empty input |