phase: 6, status: plan-as-execute, persona: lead-developer, task: T-6.1..T-6.4
---ci--- project: acdl phase: 6 milestone: v1.1 status: plan-as-execute persona: lead-developer tasks: [T-6.1, T-6.2, T-6.3, T-6.4] ---/ci--- Archive the v1.0 demo under demo/ (D-037) and reorient the repo to the real platform. Wave 1 of the Phase 06 plan. - T-6.1: git mv modules/, scripts/, evidence-ui/, contracts/, contracts-repo/, .gitea/ -> demo/; mv ACDL_DEMO.md + runner-data/ -> demo/ - T-6.2: scaffold new v1.1 top-level dirs (platform/, schemas/, adapters/, terraform/, modules-ir/) with .gitkeep - T-6.3: create top-level scripts/verify_phase06.sh (v1.1 verify scripts live at top-level, NOT demo/scripts/ which holds the v1.0 demo verify scripts) - T-6.4: rewrite README.md to reflect the real platform (vision + architecture links, new layout, status v1.1 active); add runner-data/ to .gitignore All moves via git mv (history preserved). Repo root now contains only README.md, demo/, docs/, .ciagent/, and the new empty v1.1 dirs.
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
# ACDL pipeline workflow (Phase 04 implementation).
|
||||
#
|
||||
# 3-dispatch approval-gate topology (D-027 / D-028; ARCHITECTURE.md
|
||||
# "Phase 04 pipeline topology"):
|
||||
#
|
||||
# Dispatch 1 (initial): approve_qa=false, approve_prod=false
|
||||
# -> runs the `dev` job (policy check, confidence
|
||||
# gate, mock_executor, evidence + finalize).
|
||||
# Dispatch 2 (QA approve): approve_qa=true, approve_prod=false
|
||||
# -> runs the `qa-gate` job (records QA approval
|
||||
# in the audit chain via evidence_writer +
|
||||
# finalize_evidence).
|
||||
# Dispatch 3 (Prod approve): approve_prod=true
|
||||
# -> runs the `prod-gate` job, then the `finalize`
|
||||
# job (needs: prod-gate) which writes the final
|
||||
# evidence event and commits audit.json to
|
||||
# acdl-evidence.
|
||||
#
|
||||
# Gitea Actions limitations driving this design:
|
||||
# - No `repository_dispatch` trigger (D-014).
|
||||
# - No environments API / `environment:` blocks are ignored (D-013).
|
||||
# - Re-dispatch starts a NEW run; artifacts do NOT survive between runs,
|
||||
# so state is persisted to acdl-evidence via the file-contents API
|
||||
# (D-028 / finalize_evidence.py) instead of via artifacts.
|
||||
#
|
||||
# Branch-pin rule (ARCHITECTURE.md "Branch pinning rule"):
|
||||
# This workflow lives on `acdl`'s default branch `milestone/v1.0-initial`.
|
||||
# Cross-repo `uses:` references (e.g. the issue-trigger's checkout of
|
||||
# l3b_agent_stub.py) MUST pin to `@milestone/v1.0-initial`, NOT `@main`
|
||||
# (the `acdl` repo has no `main` branch). This workflow is invoked via
|
||||
# the workflow_dispatch API (D-014), NOT via `workflow_call`, so the
|
||||
# `uses:` rule applies to the issue-trigger's checkout of the acdl repo,
|
||||
# not to this file itself.
|
||||
name: acdl-pipeline
|
||||
|
||||
"on":
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
contract-ref:
|
||||
description: "Ref on acdl-contracts that carries the contract"
|
||||
required: false
|
||||
type: string
|
||||
default: main
|
||||
approve_qa:
|
||||
description: "Human approval to advance past QA"
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
approve_prod:
|
||||
description: "Human approval to advance past Prod"
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
dev:
|
||||
name: "Dev (autonomous)"
|
||||
if: inputs.approve_qa != true && inputs.approve_prod != true
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: "Checkout acdl (this repo, pinned to milestone/v1.0-initial)"
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: milestone/v1.0-initial
|
||||
|
||||
- name: "Checkout acdl-contracts at contract-ref"
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: continuous-intelligence/acdl-contracts
|
||||
ref: ${{ inputs.contract-ref }}
|
||||
token: ${{ secrets.GITEA_TOKEN }}
|
||||
path: acdl-contracts
|
||||
|
||||
- name: "Policy check"
|
||||
run: |
|
||||
python3 scripts/policy_checker.py acdl-contracts/contract.yaml
|
||||
|
||||
- name: "Confidence signal"
|
||||
id: confidence
|
||||
run: |
|
||||
set +e
|
||||
SCORE_JSON=$(python3 scripts/confidence_signal.py acdl-contracts/contract.yaml)
|
||||
echo "$SCORE_JSON"
|
||||
echo "score_json=$SCORE_JSON" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: "Apply or reject based on confidence (gate < 0.50)"
|
||||
run: |
|
||||
set +e
|
||||
SCORE=$(python3 -c "import json,sys; print(json.load(sys.stdin)['score'])" <<< '${{ steps.confidence.outputs.score_json }}')
|
||||
python3 -c "import sys; sys.exit(0 if float('${SCORE}') >= 0.50 else 1)"
|
||||
THRESHOLD_RC=$?
|
||||
if [ "$THRESHOLD_RC" -ne 0 ]; then
|
||||
python3 scripts/evidence_writer.py --stage dev --event "dev rejected: confidence < 0.50" --audit audit.json
|
||||
python3 scripts/finalize_evidence.py --audit audit.json
|
||||
exit 1
|
||||
fi
|
||||
STACK=$(python3 -c 'import yaml; print(yaml.safe_load(open("acdl-contracts/contract.yaml"))["stack"])')
|
||||
bash scripts/mock_executor.sh acdl-contracts/contract.yaml
|
||||
python3 scripts/evidence_writer.py --stage dev --event "dev applied: ${STACK}" --audit audit.json
|
||||
python3 scripts/finalize_evidence.py --audit audit.json
|
||||
|
||||
- name: "Upload dev state artifacts (best-effort)"
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: dev-state
|
||||
path: |
|
||||
audit.json
|
||||
state.json
|
||||
|
||||
qa-gate:
|
||||
name: "QA (manual approval)"
|
||||
if: inputs.approve_qa == true && inputs.approve_prod != true
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: "Checkout acdl (this repo, pinned to milestone/v1.0-initial)"
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: milestone/v1.0-initial
|
||||
|
||||
- name: "Record QA approval in evidence"
|
||||
run: |
|
||||
python3 scripts/evidence_writer.py --stage qa --event "qa approved" --audit audit.json
|
||||
python3 scripts/finalize_evidence.py --audit audit.json
|
||||
|
||||
prod-gate:
|
||||
name: "Prod (manual approval)"
|
||||
if: inputs.approve_prod == true
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: "Checkout acdl (this repo, pinned to milestone/v1.0-initial)"
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: milestone/v1.0-initial
|
||||
|
||||
- name: "Record Prod approval in evidence"
|
||||
run: |
|
||||
python3 scripts/evidence_writer.py --stage prod --event "prod approved" --audit audit.json
|
||||
python3 scripts/finalize_evidence.py --audit audit.json
|
||||
|
||||
finalize:
|
||||
name: "Finalize (publish evidence)"
|
||||
needs: [prod-gate]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: "Checkout acdl (this repo, pinned to milestone/v1.0-initial)"
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: milestone/v1.0-initial
|
||||
|
||||
- name: "Write finalize event + commit audit.json to acdl-evidence"
|
||||
run: |
|
||||
python3 scripts/evidence_writer.py --stage finalize --event "pipeline complete: audit.json committed to acdl-evidence" --audit audit.json
|
||||
python3 scripts/finalize_evidence.py --audit audit.json
|
||||
@@ -0,0 +1,368 @@
|
||||
---
|
||||
marp: true
|
||||
theme: default
|
||||
paginate: true
|
||||
size: 16:9
|
||||
header: 'ACDL · Agentic Cloud Delivery Platform'
|
||||
footer: 'Executive Demo · v1.0'
|
||||
style: |
|
||||
/* S&P Global-inspired palette */
|
||||
:root {
|
||||
--sp-red: #C8102E;
|
||||
--sp-red-dark: #8E0B20;
|
||||
--sp-ink: #1A1A1A;
|
||||
--sp-slate: #4A4A4A;
|
||||
--sp-gray: #6E6E6E;
|
||||
--sp-line: #D6D6D6;
|
||||
--sp-bg: #FFFFFF;
|
||||
--sp-tint: #F4F4F4;
|
||||
}
|
||||
section {
|
||||
font-size: 24px;
|
||||
color: var(--sp-ink);
|
||||
background: var(--sp-bg);
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
padding: 50px 60px;
|
||||
}
|
||||
section.title {
|
||||
text-align: center;
|
||||
background: var(--sp-red);
|
||||
color: #FFFFFF;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
section.title h1 {
|
||||
color: #FFFFFF;
|
||||
font-size: 64px;
|
||||
margin-bottom: 0;
|
||||
border: none;
|
||||
}
|
||||
section.title h2 {
|
||||
color: #FFFFFF;
|
||||
border: none;
|
||||
font-weight: 400;
|
||||
}
|
||||
section.title strong { color: #FFFFFF; }
|
||||
h1 {
|
||||
color: var(--sp-red);
|
||||
font-size: 40px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
h2 {
|
||||
color: var(--sp-red);
|
||||
border-bottom: 3px solid var(--sp-red);
|
||||
padding-bottom: 6px;
|
||||
font-weight: 700;
|
||||
}
|
||||
h3 {
|
||||
color: var(--sp-red-dark);
|
||||
font-weight: 600;
|
||||
margin-top: 24px;
|
||||
}
|
||||
ul, ol { color: var(--sp-slate); }
|
||||
li { margin-bottom: 6px; }
|
||||
strong { color: var(--sp-ink); }
|
||||
table {
|
||||
font-size: 18px;
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 12px 0;
|
||||
}
|
||||
th {
|
||||
background: var(--sp-red);
|
||||
color: #FFFFFF;
|
||||
text-align: left;
|
||||
padding: 10px 12px;
|
||||
font-weight: 600;
|
||||
border: 1px solid var(--sp-red-dark);
|
||||
}
|
||||
td {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--sp-line);
|
||||
color: var(--sp-slate);
|
||||
}
|
||||
tr:nth-child(even) td { background: var(--sp-tint); }
|
||||
pre {
|
||||
font-size: 13px;
|
||||
background: var(--sp-tint);
|
||||
border-left: 4px solid var(--sp-red);
|
||||
padding: 14px 16px;
|
||||
border-radius: 0;
|
||||
color: var(--sp-ink);
|
||||
}
|
||||
code {
|
||||
background: var(--sp-tint);
|
||||
color: var(--sp-red-dark);
|
||||
padding: 1px 5px;
|
||||
border-radius: 2px;
|
||||
font-family: 'Menlo', 'Consolas', monospace;
|
||||
}
|
||||
pre code {
|
||||
background: none;
|
||||
color: var(--sp-ink);
|
||||
padding: 0;
|
||||
}
|
||||
blockquote {
|
||||
border-left: 5px solid var(--sp-red);
|
||||
background: var(--sp-tint);
|
||||
padding: 10px 16px;
|
||||
color: var(--sp-slate);
|
||||
font-style: italic;
|
||||
border-radius: 0;
|
||||
}
|
||||
header {
|
||||
color: var(--sp-red);
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
}
|
||||
footer {
|
||||
color: var(--sp-gray);
|
||||
font-size: 12px;
|
||||
}
|
||||
section::after {
|
||||
color: var(--sp-red);
|
||||
font-weight: 700;
|
||||
}
|
||||
---
|
||||
|
||||
<!-- _class: title -->
|
||||
|
||||
# ACDL
|
||||
## Agentic Cloud Delivery Platform
|
||||
|
||||
Automatic. Safe. Audited. — in 30 minutes, on stubs.
|
||||
|
||||
v1.0 · GitHub Actions · stub-driven
|
||||
|
||||
<!--
|
||||
30-min exec demo. Emphasize: we prove INTENT and SAFETY, not provision real infra.
|
||||
Everything runs as local stubs on Linux via GitHub Actions. No AWS.
|
||||
-->
|
||||
|
||||
---
|
||||
|
||||
# The Problem
|
||||
|
||||
### Today, deploying infrastructure takes **weeks**
|
||||
|
||||
- Ticket → triage queue → copy-paste config → peer review → security review → waiting for central IT to release
|
||||
|
||||
> Two weeks of human latency for a service that should take minutes.
|
||||
|
||||
### What we want instead
|
||||
|
||||
- Developer commits a **contract** → pipeline runs
|
||||
- Safety **computed** automatically (confidence signal)
|
||||
- Manual gates only where they matter (QA, Prod)
|
||||
- Every step written to a tamper-evident **evidence stream**
|
||||
|
||||
<!--
|
||||
Set the pain. Ask: who has lived this? Then pivot to the vision.
|
||||
-->
|
||||
|
||||
---
|
||||
|
||||
# How It Works
|
||||
|
||||
```
|
||||
┌────────────── acdl-contracts ──────────────┐
|
||||
Developer ──▶│ commit contract.yaml │
|
||||
└───────┬───────────────────────────────────┘
|
||||
│ (push)
|
||||
Citizen ┌─────────┴──────────┐
|
||||
│ "ingest gas pricing into data lake"
|
||||
▼
|
||||
Claude agent ──▶ contract.yaml ─┘
|
||||
│ (push)
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ reusable │
|
||||
│ GitHub Actions │
|
||||
│ pipeline │
|
||||
└────────┬────────┘
|
||||
│
|
||||
┌─────────────┼─────────────┐
|
||||
▼ ▼ ▼
|
||||
Dev (auto) QA (approval) Prod (approval)
|
||||
│
|
||||
▼
|
||||
evidence_writer ─▶ audit.json (hash-chained) ─▶ Pages timeline
|
||||
```
|
||||
|
||||
Two entry paths, **one** pipeline, **one** audit trail — developer via GitHub, citizen developer via their own Claude agent.
|
||||
|
||||
<!--
|
||||
Two surfaces: developers push contracts directly to GitHub; citizen developers prompt a Claude agent they own, which writes and pushes the contract for them.
|
||||
Both converge on one GitHub Actions pipeline with three stages.
|
||||
-->
|
||||
|
||||
---
|
||||
|
||||
# The Safety Story
|
||||
|
||||
### Computed, not requested
|
||||
|
||||
| Signal | Behavior |
|
||||
|--------|----------|
|
||||
| **Base confidence** | 0.90 |
|
||||
| **On policy violation** | drop to 0.40 + reason code |
|
||||
| **Gate threshold** | ≥ 0.50 to proceed past Dev |
|
||||
|
||||
### Policy (v1)
|
||||
- `public-ingress: true` → `POLICY_VIOLATION:PUBLIC_INGRESS`
|
||||
|
||||
### Evidence
|
||||
- Each event appended to `audit.json` with SHA-256 link to previous (`prev_hash` + `hash`)
|
||||
- Published to Pages → vanilla-JS timeline
|
||||
|
||||
<!--
|
||||
Safety is computed by the platform, not asked of the developer.
|
||||
Threshold 0.50 is exact → 0.90 passes, 0.40 halts. That's what makes the three scenarios deterministic.
|
||||
-->
|
||||
|
||||
---
|
||||
|
||||
# Scenario 1 — Developer Self-Service
|
||||
|
||||
### Trigger
|
||||
Developer commits a valid `contract.yaml` requesting **`l2-commodity-price-feed`** via **GitHub**.
|
||||
|
||||
### What you'll see
|
||||
- **Dev:** policy ✅ → apply api-gateway, lambda, s3 → confidence **0.90** → proceed
|
||||
- **QA:** pipeline pauses → click **Approve**
|
||||
- **Prod:** pipeline pauses → click **Approve**
|
||||
- **Finalize:** `audit.json` committed → Pages timeline updates
|
||||
|
||||
### Evidence outcome
|
||||
Timeline shows: contract received → policy pass → apply × 3 → confidence 0.90 → QA → Prod → published.
|
||||
|
||||
<!--
|
||||
Normal developer flow: human writes the contract, pushes to GitHub, the GitHub Action pipeline runs.
|
||||
Narrate: developer commits and walks away; platform does the rest.
|
||||
Pause visibly at each gate so the audience sees human-in-the-loop.
|
||||
End by refreshing the Pages timeline.
|
||||
-->
|
||||
|
||||
---
|
||||
|
||||
# Scenario 1 — Journey
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
classDef gh fill:#F4F4F4,stroke:#1A1A1A,stroke-width:2px,color:#1A1A1A
|
||||
classDef stage fill:#C8102E,stroke:#8E0B20,stroke-width:1px,color:#FFFFFF
|
||||
classDef gate fill:#FFFFFF,stroke:#1A1A1A,stroke-width:2px,color:#1A1A1A
|
||||
classDef evidence fill:#F4F4F4,stroke:#C8102E,stroke-width:1px,color:#1A1A1A
|
||||
|
||||
D["Developer"]:::gh -->|"writes contract.yaml"| GH["GitHub<br/>acdl-contracts"]:::gh
|
||||
GH -->|"push triggers<br/>GitHub Action"| DEV["Dev<br/>(autonomous)"]:::stage
|
||||
DEV -->|"policy ✅ · confidence 0.90"| QA["QA<br/>approval gate"]:::gate
|
||||
QA -->|"approve"| PROD["Prod<br/>approval gate"]:::gate
|
||||
PROD -->|"approve"| FIN["Finalize<br/>commit audit.json"]:::stage
|
||||
FIN --> TL["GitHub Pages<br/>timeline"]:::evidence
|
||||
```
|
||||
|
||||
<!--
|
||||
Point to the two approval gates — QA and Prod — both human clicks.
|
||||
The whole chain from commit to timeline is one GitHub Actions workflow.
|
||||
-->
|
||||
|
||||
---
|
||||
|
||||
# Scenario 2 — Citizen Developer
|
||||
|
||||
### Trigger
|
||||
Non-technical user prompts their **own Claude agent** in natural language:
|
||||
|
||||
> "I need a new service to ingest real-time natural gas pricing data into our data lake."
|
||||
|
||||
### What you'll see
|
||||
- Claude agent parses intent, writes `contract.yaml` for **`l2-commodity-price-feed`**, pushes a branch
|
||||
- Issue **closed**; branch push triggers the **identical** pipeline from Scenario 1
|
||||
- Citizen developer follows the run all the way to **Prod**
|
||||
|
||||
### Evidence outcome
|
||||
Timeline is **indistinguishable** from Scenario 1 — the agentic surface is first-class, not a bolt-on.
|
||||
|
||||
<!--
|
||||
Punchline: same timeline, same safety, same audit — different entry.
|
||||
The citizen developer owns and drives their own Claude agent; they are the actor, not the platform.
|
||||
Normal developers (Scenario 1) keep using GitHub directly — two surfaces, one pipeline, one audit.
|
||||
-->
|
||||
|
||||
---
|
||||
|
||||
# Scenario 2 — Journey
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
classDef cit fill:#F4F4F4,stroke:#C8102E,stroke-width:2px,color:#1A1A1A
|
||||
classDef agent fill:#C8102E,stroke:#8E0B20,stroke-width:1px,color:#FFFFFF
|
||||
classDef stage fill:#1A1A1A,stroke:#1A1A1A,stroke-width:1px,color:#FFFFFF
|
||||
classDef gate fill:#FFFFFF,stroke:#1A1A1A,stroke-width:2px,color:#1A1A1A
|
||||
classDef evidence fill:#F4F4F4,stroke:#C8102E,stroke-width:1px,color:#1A1A1A
|
||||
|
||||
CD["Citizen developer"]:::cit -->|"natural-language<br/>prompt"| CL["Claude agent<br/>(citizen-owned)"]:::agent
|
||||
CL -->|"generates<br/>contract.yaml"| GH["GitHub<br/>acdl-contracts"]:::cit
|
||||
GH -->|"push triggers<br/>GitHub Action"| DEV["Dev<br/>(autonomous)"]:::stage
|
||||
DEV -->|"policy ✅ · confidence 0.90"| QA["QA<br/>approval gate"]:::gate
|
||||
QA -->|"approve"| PROD["Prod<br/>approval gate"]:::gate
|
||||
PROD -->|"approve"| FIN["Finalize<br/>commit audit.json"]:::stage
|
||||
FIN --> TL["GitHub Pages<br/>timeline"]:::evidence
|
||||
```
|
||||
|
||||
<!--
|
||||
Highlight the red Claude-agent node — owned by the citizen, not by the platform.
|
||||
From GitHub onward the journey is identical to Scenario 1.
|
||||
-->
|
||||
|
||||
---
|
||||
|
||||
# Scenario 3 — The Safety Net
|
||||
|
||||
### Trigger
|
||||
Developer commits a **malicious** `contract.yaml` for `l2-regulatory-reporting` via **GitHub**:
|
||||
|
||||
```yaml
|
||||
stack: l2-regulatory-reporting
|
||||
public-ingress: true
|
||||
```
|
||||
|
||||
### What you'll see
|
||||
- **Dev:** `policy_checker` → `POLICY_VIOLATION:PUBLIC_INGRESS`
|
||||
- `confidence_signal` drops 0.90 → **0.40**
|
||||
- `0.40 < 0.50` → pipeline **halts in Dev**
|
||||
- Rejection reason written to the evidence stream
|
||||
|
||||
### Evidence outcome
|
||||
Timeline shows the attempted deploy, the violation, the confidence drop, and the **halt** — visible and explained.
|
||||
|
||||
<!--
|
||||
The safety money shot. The platform said NO, and said WHY, on the record.
|
||||
No human had to catch it — the confidence signal computed the risk.
|
||||
Contrast with The Problem's old-world review queue.
|
||||
-->
|
||||
|
||||
---
|
||||
|
||||
# Scenario 3 — Journey
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
classDef gh fill:#F4F4F4,stroke:#1A1A1A,stroke-width:2px,color:#1A1A1A
|
||||
classDef stage fill:#C8102E,stroke:#8E0B20,stroke-width:1px,color:#FFFFFF
|
||||
classDef halt fill:#1A1A1A,stroke:#1A1A1A,stroke-width:1px,color:#FFFFFF
|
||||
classDef evidence fill:#F4F4F4,stroke:#C8102E,stroke-width:1px,color:#1A1A1A
|
||||
|
||||
D["Developer"]:::gh -->|"writes malicious<br/>contract.yaml"| GH["GitHub<br/>acdl-contracts"]:::gh
|
||||
GH -->|"push triggers<br/>GitHub Action"| DEV["Dev<br/>(autonomous)"]:::stage
|
||||
DEV -->|"POLICY_VIOLATION:PUBLIC_INGRESS<br/>confidence 0.90 → 0.40"| HALT["Halt in Dev<br/>+ rejection reason"]:::halt
|
||||
HALT --> TL["GitHub Pages<br/>timeline"]:::evidence
|
||||
```
|
||||
|
||||
<!--
|
||||
The black halt node is the whole point — pipeline stops, evidence records why.
|
||||
Notice there are no QA/Prod gates on this path; the journey ends at Dev.
|
||||
-->
|
||||
@@ -0,0 +1,145 @@
|
||||
# ACDL issue-to-contract workflow (Phase 04 implementation).
|
||||
#
|
||||
# Trigger: a new Issue is opened in acdl-contracts. The workflow runs
|
||||
# l3b_agent_stub.py (checked out from the `acdl` repo, pinned to
|
||||
# @milestone/v1.0-initial) to map the Issue body to a contract.yaml, commits
|
||||
# the contract to a new branch `contract/<issue-number>` on acdl-contracts
|
||||
# via the Gitea file-contents API, closes the Issue with a comment, and
|
||||
# dispatches the main pipeline in the `acdl` repo via the workflow_dispatch
|
||||
# API (D-014; Gitea Actions does not support repository_dispatch).
|
||||
#
|
||||
# Cross-repo trigger (D-014):
|
||||
# The final step POSTs to
|
||||
# /api/v1/repos/continuous-intelligence/acdl/actions/workflows/pipeline.yml/dispatches
|
||||
# with body {"ref": "milestone/v1.0-initial",
|
||||
# "inputs": {"contract-ref": "contract/<issue-number>"}}.
|
||||
#
|
||||
# Branch-pin rule (ARCHITECTURE.md):
|
||||
# The `acdl` repo's default branch is `milestone/v1.0-initial`, so the
|
||||
# checkout step pins `ref: milestone/v1.0-initial`. The pipeline dispatch
|
||||
# also pins `ref: milestone/v1.0-initial` (the workflow file lives on
|
||||
# that branch). The new `contract/<n>` branch is created on acdl-contracts
|
||||
# (whose default branch is `main`, per D-015).
|
||||
#
|
||||
# File-contents POST with `new_branch` (D-030):
|
||||
# The POST to /repos/.../contents/contract.yaml includes
|
||||
# `new_branch: contract/<n>`, which tells Gitea to create the file on a
|
||||
# NEW branch off the current head of `branch: main` instead of committing
|
||||
# directly to main. This avoids a separate branch-create + commit round
|
||||
# trip.
|
||||
name: issue-to-contract
|
||||
|
||||
"on":
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
parse-and-trigger:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: "Checkout acdl (pinned to milestone/v1.0-initial for l3b_agent_stub.py)"
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: continuous-intelligence/acdl
|
||||
ref: milestone/v1.0-initial
|
||||
token: ${{ secrets.GITEA_TOKEN }}
|
||||
|
||||
- name: "Parse Issue body into contract.yaml"
|
||||
env:
|
||||
ISSUE_BODY: ${{ gitea.event.issue.body }}
|
||||
run: |
|
||||
# Pass the Issue body via an env var to avoid shell injection from
|
||||
# arbitrary Issue text. l3b_agent_stub.py reads argv[1]; we pass
|
||||
# the env var quoted so no metacharacter interpretation happens.
|
||||
python3 scripts/l3b_agent_stub.py "$ISSUE_BODY" -o contract.yaml
|
||||
echo "--- generated contract.yaml ---"
|
||||
cat contract.yaml
|
||||
|
||||
- name: "Commit contract.yaml to new branch contract/${{ gitea.event.issue.number }} on acdl-contracts"
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
STACK=$(python3 -c 'import yaml; print(yaml.safe_load(open("contract.yaml"))["stack"])')
|
||||
ISSUE_NUMBER="${{ gitea.event.issue.number }}"
|
||||
BRANCH="contract/${ISSUE_NUMBER}"
|
||||
HOST="https://git.cloudinit.dev"
|
||||
API="${HOST}/api/v1/repos/continuous-intelligence/acdl-contracts/contents/contract.yaml"
|
||||
B64=$(base64 -w 0 contract.yaml)
|
||||
BODY=$(python3 -c "
|
||||
import json
|
||||
print(json.dumps({
|
||||
'content': '${B64}',
|
||||
'message': 'l3b: contract for issue #${ISSUE_NUMBER}',
|
||||
'branch': 'main',
|
||||
'new_branch': '${BRANCH}'
|
||||
}))
|
||||
")
|
||||
STATUS=$(curl -sS -o /tmp/contract_post.json -w "%{http_code}" \
|
||||
-X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$BODY" \
|
||||
"$API")
|
||||
echo "POST contract.yaml -> HTTP ${STATUS}"
|
||||
cat /tmp/contract_post.json || true
|
||||
case "$STATUS" in
|
||||
201) echo "contract.yaml committed on branch ${BRANCH}" ;;
|
||||
*) echo "ERROR: file-contents POST failed (HTTP ${STATUS})" >&2; exit 1 ;;
|
||||
esac
|
||||
echo "STACK=${STACK}" >> "$GITHUB_ENV"
|
||||
echo "BRANCH=${BRANCH}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: "Comment on Issue + close it"
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ISSUE_NUMBER="${{ gitea.event.issue.number }}"
|
||||
HOST="https://git.cloudinit.dev"
|
||||
ISSUES_API="${HOST}/api/v1/repos/continuous-intelligence/acdl-contracts/issues/${ISSUE_NUMBER}"
|
||||
COMMENT_BODY=$(python3 -c "
|
||||
import json
|
||||
print(json.dumps({'body': 'Generated contract.yaml for stack \`' + '${STACK}' + '\` on branch \`' + '${BRANCH}' + '\`. Pipeline dispatched.'}))
|
||||
")
|
||||
curl -sS -o /tmp/comment.json -w "comment HTTP %{http_code}\n" \
|
||||
-X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$COMMENT_BODY" \
|
||||
"${ISSUES_API}/comments"
|
||||
CLOSE_BODY='{"state":"closed"}'
|
||||
curl -sS -o /tmp/close.json -w "close HTTP %{http_code}\n" \
|
||||
-X PATCH \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$CLOSE_BODY" \
|
||||
"${ISSUES_API}"
|
||||
|
||||
- name: "Dispatch the pipeline on acdl (contract-ref = contract/${{ gitea.event.issue.number }})"
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ISSUE_NUMBER="${{ gitea.event.issue.number }}"
|
||||
HOST="https://git.cloudinit.dev"
|
||||
DISPATCH_URL="${HOST}/api/v1/repos/continuous-intelligence/acdl/actions/workflows/pipeline.yml/dispatches"
|
||||
BODY=$(python3 -c "
|
||||
import json
|
||||
print(json.dumps({
|
||||
'ref': 'milestone/v1.0-initial',
|
||||
'inputs': {'contract-ref': 'contract/${ISSUE_NUMBER}'}
|
||||
}))
|
||||
")
|
||||
STATUS=$(curl -sS -o /tmp/dispatch.json -w "%{http_code}" \
|
||||
-X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$BODY" \
|
||||
"$DISPATCH_URL")
|
||||
echo "pipeline dispatch -> HTTP ${STATUS}"
|
||||
cat /tmp/dispatch.json || true
|
||||
case "$STATUS" in
|
||||
201|202|204) echo "pipeline dispatched (contract-ref=contract/${ISSUE_NUMBER})" ;;
|
||||
*) echo "ERROR: pipeline dispatch failed (HTTP ${STATUS})" >&2; exit 1 ;;
|
||||
esac
|
||||
@@ -0,0 +1,55 @@
|
||||
# Elaborate developer contract — energy trading price feed.
|
||||
#
|
||||
# Schema (D-021): stack + inputs (open-ended string map) + public-ingress.
|
||||
# The `stack` field MUST match an L2 folder name under modules/l2/.
|
||||
# The `inputs` map is free-form string values; these are L2-level params
|
||||
# that travel with the deployment into state.json and the audit trail.
|
||||
# The L1 input values are declared by the L2's manifest.yaml, not here.
|
||||
#
|
||||
# Commit this to acdl-contracts as contract.yaml to trigger the pipeline:
|
||||
# git add contract.yaml && git commit -m "feat: deploy price feed (prod)" && git push
|
||||
|
||||
stack: l2-commodity-price-feed
|
||||
|
||||
inputs:
|
||||
# --- Environment + ownership ---
|
||||
environment: prod
|
||||
owner: commodity-trading-platform-team
|
||||
team: power-and-gas-desk
|
||||
cost_center: CC-TRD-4471
|
||||
change_ticket: CHG-2026-07-21-093
|
||||
|
||||
# --- Business context (rides into the audit timeline) ---
|
||||
business_owner: kchen@jccapital.xyz
|
||||
oncall_email: sre-commodity@example.com
|
||||
sla_tier: T1
|
||||
business_hours: "Mon-Fri 07:00-19:00 ET"
|
||||
data_classification: internal
|
||||
|
||||
# --- Source feed contract (business-facing) ---
|
||||
feed_vendor: Platts
|
||||
feed_name: natural-gas-daily-settlement
|
||||
feed_cadence: daily
|
||||
feed_timezone: US/Eastern
|
||||
symbols: "NG-WTI-HH,NG-HH-M,NG-PJM"
|
||||
retry_policy: backoff-3x-15min
|
||||
dead_letter_queue: commodity-price-dlq
|
||||
|
||||
# --- Deployment knobs (consumed by the pipeline; passed to L1s via L2 manifest) ---
|
||||
replicas: "3"
|
||||
cpu_request: "500m"
|
||||
memory_request: "1Gi"
|
||||
autoscale_min: "2"
|
||||
autoscale_max: "8"
|
||||
log_retention_days: "90"
|
||||
archive_retention_days: "2555"
|
||||
|
||||
# --- Operational flags ---
|
||||
enable_canary: "true"
|
||||
canary_percentage: "10"
|
||||
enable_pagerduty: "true"
|
||||
enable_cost_alerts: "true"
|
||||
cost_alert_threshold_usd: "500"
|
||||
|
||||
# Policy-gated field. true -> POLICY_VIOLATION:PUBLIC_INGRESS -> confidence 0.40 < 0.50 -> Dev rejects (Act 4).
|
||||
public-ingress: false
|
||||
@@ -0,0 +1,55 @@
|
||||
# Elaborate developer contract — regulatory reporting (with policy violation).
|
||||
#
|
||||
# Same schema as the price-feed example, but with public-ingress: true,
|
||||
# which triggers Act 4: the policy_checker fails, the confidence_signal
|
||||
# drops to 0.40, the 0.50 gate halts the pipeline in Dev, and the
|
||||
# rejection appears on the evidence timeline.
|
||||
#
|
||||
# Commit this to acdl-contracts as contract.yaml to reproduce Act 4:
|
||||
# git add contract.yaml && git commit -m "feat: deploy regulatory reporting" && git push
|
||||
|
||||
stack: l2-regulatory-reporting
|
||||
|
||||
inputs:
|
||||
# --- Environment + ownership ---
|
||||
environment: prod
|
||||
owner: compliance-and-controls-team
|
||||
team: regulatory-reporting-desk
|
||||
cost_center: CC-CMP-9902
|
||||
change_ticket: CHG-2026-07-21-118
|
||||
business_owner: compliance@jccapital.xyz
|
||||
oncall_email: sre-regulatory@example.com
|
||||
sla_tier: T0
|
||||
business_hours: "24x7"
|
||||
data_classification: confidential
|
||||
|
||||
# --- Regulatory context ---
|
||||
regulator: FERC
|
||||
filing_frequency: monthly
|
||||
filing_deadline_day_of_month: "15"
|
||||
reporting_period: 2026-Q3
|
||||
jurisdiction: US-Federal
|
||||
legal_hold: "false"
|
||||
|
||||
# --- Deployment knobs ---
|
||||
replicas: "2"
|
||||
cpu_request: "1000m"
|
||||
memory_request: "2Gi"
|
||||
autoscale_min: "2"
|
||||
autoscale_max: "4"
|
||||
log_retention_days: "365"
|
||||
archive_retention_days: "2555"
|
||||
enable_encryption_at_rest: "true"
|
||||
enable_kms_rotation: "true"
|
||||
|
||||
# --- Operational flags ---
|
||||
enable_canary: "false"
|
||||
enable_pagerduty: "true"
|
||||
enable_cost_alerts: "true"
|
||||
cost_alert_threshold_usd: "1000"
|
||||
|
||||
# POLICY VIOLATION — this is the Act 4 trigger.
|
||||
# The policy_checker.py will emit: POLICY_VIOLATION:PUBLIC_INGRESS
|
||||
# The confidence_signal.py will return: {"score": 0.40, "reason": "POLICY_VIOLATION:PUBLIC_INGRESS"}
|
||||
# The 0.50 gate halts the pipeline in Dev; mock_executor never runs.
|
||||
public-ingress: true
|
||||
@@ -0,0 +1,334 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>ACDL Evidence Timeline</title>
|
||||
<style>
|
||||
:root {
|
||||
--stage-dev: #2563eb;
|
||||
--stage-qa: #ca8a04;
|
||||
--stage-prod: #ea580c;
|
||||
--stage-finalize: #16a34a;
|
||||
--stage-genesis: #6b7280;
|
||||
--stage-rejected: #dc2626;
|
||||
--bg: #f8fafc;
|
||||
--card-bg: #ffffff;
|
||||
--text: #0f172a;
|
||||
--muted: #64748b;
|
||||
--border: #e2e8f0;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
line-height: 1.5;
|
||||
}
|
||||
header {
|
||||
padding: 24px 32px 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: var(--card-bg);
|
||||
}
|
||||
header h1 {
|
||||
margin: 0 0 6px;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
header p {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px 32px;
|
||||
background: var(--card-bg);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
button#refresh {
|
||||
appearance: none;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--text);
|
||||
color: #fff;
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
button#refresh:hover { opacity: 0.9; }
|
||||
button#refresh:active { transform: translateY(1px); }
|
||||
.toolbar .status {
|
||||
color: var(--muted);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
main {
|
||||
padding: 24px 32px 48px;
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.empty {
|
||||
padding: 48px 24px;
|
||||
text-align: center;
|
||||
color: var(--muted);
|
||||
background: var(--card-bg);
|
||||
border: 1px dashed var(--border);
|
||||
border-radius: 8px;
|
||||
}
|
||||
ol.timeline {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
}
|
||||
ol.timeline::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 11px;
|
||||
top: 6px;
|
||||
bottom: 6px;
|
||||
width: 2px;
|
||||
background: var(--border);
|
||||
}
|
||||
li.event {
|
||||
position: relative;
|
||||
padding: 12px 0 12px 40px;
|
||||
}
|
||||
li.event::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 6px;
|
||||
top: 18px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: var(--dot, var(--muted));
|
||||
border: 2px solid var(--card-bg);
|
||||
box-shadow: 0 0 0 1px var(--border);
|
||||
}
|
||||
.card {
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border);
|
||||
border-left: 4px solid var(--dot, var(--muted));
|
||||
border-radius: 8px;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
.card .row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.seq {
|
||||
display: inline-block;
|
||||
min-width: 28px;
|
||||
padding: 2px 6px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
border-radius: 4px;
|
||||
background: #eef2ff;
|
||||
color: #3730a3;
|
||||
border: 1px solid #c7d2fe;
|
||||
}
|
||||
.chip {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
border-radius: 999px;
|
||||
color: #fff;
|
||||
background: var(--dot, var(--muted));
|
||||
}
|
||||
.ts {
|
||||
font-size: 0.8rem;
|
||||
color: var(--muted);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.event-text {
|
||||
margin: 4px 0 6px;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
.hash {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
font-size: 0.75rem;
|
||||
color: var(--muted);
|
||||
word-break: break-all;
|
||||
}
|
||||
footer {
|
||||
padding: 16px 32px 24px;
|
||||
border-top: 1px solid var(--border);
|
||||
color: var(--muted);
|
||||
font-size: 0.8rem;
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
footer code {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
background: #f1f5f9;
|
||||
padding: 1px 4px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>ACDL Evidence Timeline</h1>
|
||||
<p>ACDL — Agentic Cloud Delivery Platform · Audit Timeline</p>
|
||||
</header>
|
||||
<div class="toolbar">
|
||||
<button id="refresh" type="button">Refresh</button>
|
||||
<span class="status" id="status"></span>
|
||||
</div>
|
||||
<main>
|
||||
<div id="container">
|
||||
<div class="empty">Loading…</div>
|
||||
</div>
|
||||
</main>
|
||||
<footer>
|
||||
<div id="footer"></div>
|
||||
</footer>
|
||||
<script>
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
var AUDIT_URL = "./audit.json";
|
||||
var STAGE_COLORS = {
|
||||
dev: "var(--stage-dev)",
|
||||
qa: "var(--stage-qa)",
|
||||
prod: "var(--stage-prod)",
|
||||
finalize: "var(--stage-finalize)",
|
||||
genesis: "var(--stage-genesis)"
|
||||
};
|
||||
|
||||
function $(id) { return document.getElementById(id); }
|
||||
|
||||
function stageColor(stage, eventText) {
|
||||
var evt = (eventText || "").toString().toLowerCase();
|
||||
if (evt.indexOf("rejected") !== -1) {
|
||||
return "var(--stage-rejected)";
|
||||
}
|
||||
return STAGE_COLORS[stage] || "var(--stage-genesis)";
|
||||
}
|
||||
|
||||
function dash(v) {
|
||||
return (v === null || v === undefined || v === "") ? "—" : v;
|
||||
}
|
||||
|
||||
function hashPreview(hash) {
|
||||
if (hash === null || hash === undefined || hash === "") return "—";
|
||||
var s = String(hash);
|
||||
return s.slice(0, 12) + "…";
|
||||
}
|
||||
|
||||
function esc(s) {
|
||||
return String(s)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
function auditUrlDisplay() {
|
||||
try {
|
||||
var href = window.location.href;
|
||||
var slash = href.lastIndexOf("/");
|
||||
if (slash >= 0) {
|
||||
return href.slice(0, slash + 1) + "audit.json";
|
||||
}
|
||||
} catch (e) {}
|
||||
return AUDIT_URL;
|
||||
}
|
||||
|
||||
function renderEmpty(msg) {
|
||||
$("container").innerHTML =
|
||||
'<div class="empty">' + esc(msg) + "</div>";
|
||||
}
|
||||
|
||||
function renderTimeline(events) {
|
||||
if (!Array.isArray(events)) {
|
||||
renderEmpty("No audit data yet");
|
||||
return;
|
||||
}
|
||||
if (events.length === 0) {
|
||||
renderEmpty("No audit data yet");
|
||||
return;
|
||||
}
|
||||
var sorted = events.slice().sort(function (a, b) {
|
||||
var sa = (a && typeof a.seq === "number") ? a.seq : 0;
|
||||
var sb = (b && typeof b.seq === "number") ? b.seq : 0;
|
||||
return sa - sb;
|
||||
});
|
||||
var html = '<ol class="timeline">';
|
||||
for (var i = 0; i < sorted.length; i++) {
|
||||
var e = sorted[i] || {};
|
||||
var stage = dash(e.stage);
|
||||
var color = stageColor(e.stage, e.event);
|
||||
html += '<li class="event" style="--dot:' + color + ';">';
|
||||
html += '<div class="card" style="--dot:' + color + ';">';
|
||||
html += '<div class="row">';
|
||||
html += '<span class="seq">#' + esc(dash(e.seq)) + "</span>";
|
||||
html += '<span class="chip">' + esc(stage) + "</span>";
|
||||
html += '<span class="ts">' + esc(dash(e.ts)) + "</span>";
|
||||
html += "</div>";
|
||||
html += '<div class="event-text">' + esc(dash(e.event)) + "</div>";
|
||||
html += '<div class="hash">' + esc(hashPreview(e.hash)) + "</div>";
|
||||
html += "</div>";
|
||||
html += "</li>";
|
||||
}
|
||||
html += "</ol>";
|
||||
$("container").innerHTML = html;
|
||||
}
|
||||
|
||||
function renderFooter(ok) {
|
||||
var when = new Date().toISOString();
|
||||
var url = auditUrlDisplay();
|
||||
var prefix = "Fetched at " + when + " · audit.json: ";
|
||||
$("footer").innerHTML =
|
||||
esc(prefix) + '<code>' + esc(url) + "</code>" +
|
||||
(ok ? "" : " (fetch failed)");
|
||||
}
|
||||
|
||||
function setStatus(msg) {
|
||||
$("status").textContent = msg || "";
|
||||
}
|
||||
|
||||
function fetchAudit() {
|
||||
setStatus("Fetching…");
|
||||
fetch(AUDIT_URL, { cache: "no-store" })
|
||||
.then(function (res) {
|
||||
if (!res.ok) {
|
||||
throw new Error("HTTP " + res.status);
|
||||
}
|
||||
return res.json();
|
||||
})
|
||||
.then(function (data) {
|
||||
if (!Array.isArray(data)) {
|
||||
throw new Error("not an array");
|
||||
}
|
||||
renderTimeline(data);
|
||||
renderFooter(true);
|
||||
setStatus("Loaded " + data.length + " event(s)");
|
||||
})
|
||||
.catch(function (err) {
|
||||
renderEmpty("No audit data yet");
|
||||
renderFooter(false);
|
||||
setStatus("Fetch failed: " + (err && err.message ? err.message : "error"));
|
||||
});
|
||||
}
|
||||
|
||||
$("refresh").addEventListener("click", fetchAudit);
|
||||
fetchAudit();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,10 @@
|
||||
name: l1-api-gateway
|
||||
kind: l1
|
||||
description: HTTP routing primitive
|
||||
inputs:
|
||||
api_name:
|
||||
description: Name of the API Gateway REST/HTTP API
|
||||
type: string
|
||||
stage_name:
|
||||
description: Name of the deployment stage (e.g. dev, prod)
|
||||
type: string
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
echo "[L1: l1-api-gateway] applying..."
|
||||
sleep 1
|
||||
echo "[L1: l1-api-gateway] OK"
|
||||
exit 0
|
||||
@@ -0,0 +1,10 @@
|
||||
name: l1-cloudwatch
|
||||
kind: l1
|
||||
description: Observability primitive
|
||||
inputs:
|
||||
log_group_name:
|
||||
description: Name of the CloudWatch log group
|
||||
type: string
|
||||
metric_namespace:
|
||||
description: Namespace under which custom metrics are emitted
|
||||
type: string
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
echo "[L1: l1-cloudwatch] applying..."
|
||||
sleep 1
|
||||
echo "[L1: l1-cloudwatch] OK"
|
||||
exit 0
|
||||
@@ -0,0 +1,13 @@
|
||||
name: l1-eks-fargate
|
||||
kind: l1
|
||||
description: Serverless container compute substrate
|
||||
inputs:
|
||||
cluster_name:
|
||||
description: Name of the EKS cluster to target
|
||||
type: string
|
||||
region:
|
||||
description: AWS region the cluster runs in
|
||||
type: string
|
||||
cpu_arch:
|
||||
description: CPU architecture for Fargate pods (x86_64 or arm64)
|
||||
type: string
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
echo "[L1: l1-eks-fargate] applying..."
|
||||
sleep 1
|
||||
echo "[L1: l1-eks-fargate] OK"
|
||||
exit 0
|
||||
@@ -0,0 +1,10 @@
|
||||
name: l1-eventbridge
|
||||
kind: l1
|
||||
description: Event bus primitive
|
||||
inputs:
|
||||
bus_name:
|
||||
description: Name of the EventBridge bus
|
||||
type: string
|
||||
rule_name:
|
||||
description: Name of the event rule on the bus
|
||||
type: string
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
echo "[L1: l1-eventbridge] applying..."
|
||||
sleep 1
|
||||
echo "[L1: l1-eventbridge] OK"
|
||||
exit 0
|
||||
@@ -0,0 +1,10 @@
|
||||
name: l1-iam-role
|
||||
kind: l1
|
||||
description: Identity and access role primitive
|
||||
inputs:
|
||||
role_name:
|
||||
description: Name of the IAM role to create
|
||||
type: string
|
||||
trust_policy:
|
||||
description: JSON trust policy document defining who can assume the role
|
||||
type: string
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
echo "[L1: l1-iam-role] applying..."
|
||||
sleep 1
|
||||
echo "[L1: l1-iam-role] OK"
|
||||
exit 0
|
||||
@@ -0,0 +1,13 @@
|
||||
name: l1-lambda
|
||||
kind: l1
|
||||
description: Event-driven function primitive
|
||||
inputs:
|
||||
function_name:
|
||||
description: Name of the Lambda function
|
||||
type: string
|
||||
runtime:
|
||||
description: Lambda runtime identifier (e.g. python3.12, nodejs20.x)
|
||||
type: string
|
||||
handler:
|
||||
description: Handler entrypoint in the form module.function
|
||||
type: string
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
echo "[L1: l1-lambda] applying..."
|
||||
sleep 1
|
||||
echo "[L1: l1-lambda] OK"
|
||||
exit 0
|
||||
@@ -0,0 +1,13 @@
|
||||
name: l1-s3
|
||||
kind: l1
|
||||
description: Object store primitive
|
||||
inputs:
|
||||
bucket_name:
|
||||
description: Globally unique name of the S3 bucket
|
||||
type: string
|
||||
region:
|
||||
description: AWS region the bucket lives in
|
||||
type: string
|
||||
retention_days:
|
||||
description: Number of days to retain objects before expiration
|
||||
type: string
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
echo "[L1: l1-s3] applying..."
|
||||
sleep 1
|
||||
echo "[L1: l1-s3] OK"
|
||||
exit 0
|
||||
@@ -0,0 +1,10 @@
|
||||
name: l1-sqs
|
||||
kind: l1
|
||||
description: Queue primitive
|
||||
inputs:
|
||||
queue_name:
|
||||
description: Name of the SQS queue
|
||||
type: string
|
||||
visibility_timeout:
|
||||
description: Visibility timeout in seconds for in-flight messages
|
||||
type: string
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
echo "[L1: l1-sqs] applying..."
|
||||
sleep 1
|
||||
echo "[L1: l1-sqs] OK"
|
||||
exit 0
|
||||
@@ -0,0 +1,27 @@
|
||||
name: l2-commodity-price-feed
|
||||
kind: l2
|
||||
description: Real-time commodity price ingestion from Platts
|
||||
l1s:
|
||||
- name: l1-eks-fargate
|
||||
inputs:
|
||||
cluster_name: price-feed-cluster
|
||||
region: us-east-1
|
||||
cpu_arch: arm64
|
||||
- name: l1-lambda
|
||||
inputs:
|
||||
function_name: price-ingest
|
||||
runtime: python3.11
|
||||
handler: index.handler
|
||||
- name: l1-api-gateway
|
||||
inputs:
|
||||
api_name: platts-price-api
|
||||
stage_name: dev
|
||||
- name: l1-eventbridge
|
||||
inputs:
|
||||
bus_name: price-events
|
||||
rule_name: price-publish-rule
|
||||
- name: l1-s3
|
||||
inputs:
|
||||
bucket_name: acdl-price-archive
|
||||
region: us-east-1
|
||||
retention_days: "90"
|
||||
@@ -0,0 +1,27 @@
|
||||
name: l2-energy-analytics-api
|
||||
kind: l2
|
||||
description: Historical energy analytics query API
|
||||
l1s:
|
||||
- name: l1-eks-fargate
|
||||
inputs:
|
||||
cluster_name: analytics-cluster
|
||||
region: us-east-1
|
||||
cpu_arch: arm64
|
||||
- name: l1-api-gateway
|
||||
inputs:
|
||||
api_name: energy-analytics-api
|
||||
stage_name: dev
|
||||
- name: l1-lambda
|
||||
inputs:
|
||||
function_name: analytics-query
|
||||
runtime: python3.11
|
||||
handler: index.handler
|
||||
- name: l1-s3
|
||||
inputs:
|
||||
bucket_name: acdl-analytics-data
|
||||
region: us-east-1
|
||||
retention_days: "2555"
|
||||
- name: l1-cloudwatch
|
||||
inputs:
|
||||
log_group_name: /acdl/analytics-api
|
||||
metric_namespace: acdl/analytics
|
||||
@@ -0,0 +1,27 @@
|
||||
name: l2-invoice-service
|
||||
kind: l2
|
||||
description: Billing and invoicing microservice for energy trades
|
||||
l1s:
|
||||
- name: l1-eks-fargate
|
||||
inputs:
|
||||
cluster_name: invoice-cluster
|
||||
region: us-east-1
|
||||
cpu_arch: arm64
|
||||
- name: l1-iam-role
|
||||
inputs:
|
||||
role_name: invoice-service-role
|
||||
trust_policy: '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"eks.amazonaws.com"},"Action":"sts:AssumeRole"}]}'
|
||||
- name: l1-lambda
|
||||
inputs:
|
||||
function_name: invoice-generator
|
||||
runtime: python3.11
|
||||
handler: index.handler
|
||||
- name: l1-sqs
|
||||
inputs:
|
||||
queue_name: invoice-queue
|
||||
visibility_timeout: "60"
|
||||
- name: l1-s3
|
||||
inputs:
|
||||
bucket_name: acdl-invoice-archive
|
||||
region: us-east-1
|
||||
retention_days: "365"
|
||||
@@ -0,0 +1,27 @@
|
||||
name: l2-regulatory-reporting
|
||||
kind: l2
|
||||
description: Regulatory compliance and reporting for energy trading
|
||||
l1s:
|
||||
- name: l1-eks-fargate
|
||||
inputs:
|
||||
cluster_name: regulatory-cluster
|
||||
region: us-east-1
|
||||
cpu_arch: arm64
|
||||
- name: l1-iam-role
|
||||
inputs:
|
||||
role_name: regulatory-reporting-role
|
||||
trust_policy: '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"eks.amazonaws.com"},"Action":"sts:AssumeRole"}]}'
|
||||
- name: l1-lambda
|
||||
inputs:
|
||||
function_name: regulatory-reporter
|
||||
runtime: python3.11
|
||||
handler: index.handler
|
||||
- name: l1-sqs
|
||||
inputs:
|
||||
queue_name: regulatory-queue
|
||||
visibility_timeout: "120"
|
||||
- name: l1-s3
|
||||
inputs:
|
||||
bucket_name: acdl-regulatory-archive
|
||||
region: us-east-1
|
||||
retention_days: "2555"
|
||||
Executable
+55
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env python3
|
||||
"""confidence_signal.py — REQ-08 / D-024
|
||||
|
||||
Reads a contract.yaml, invokes policy_checker.py as a subprocess, and emits
|
||||
a deterministic JSON confidence score.
|
||||
|
||||
policy pass -> {"score": 0.90, "reason": "POLICY_PASS"}
|
||||
policy fail -> {"score": 0.40, "reason": "<violation code>"}
|
||||
|
||||
Exit 0 ALWAYS (per D-024): the pipeline decides the gate, not this script's
|
||||
exit code.
|
||||
|
||||
Input: argv[1] = path to a contract.yaml file.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) < 2:
|
||||
print("usage: confidence_signal.py <contract.yaml>", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
contract_path = sys.argv[1]
|
||||
|
||||
# Resolve policy_checker.py relative to this script so it works regardless
|
||||
# of cwd. Use python3 + script path (not ./) per the contract.
|
||||
here = os.path.dirname(os.path.abspath(__file__))
|
||||
policy_checker = os.path.join(here, "policy_checker.py")
|
||||
|
||||
proc = subprocess.run(
|
||||
["python3", policy_checker, contract_path],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
if proc.returncode == 0:
|
||||
score = "0.90"
|
||||
# POLICY_PASS is the expected stdout; strip any trailing whitespace.
|
||||
reason = proc.stdout.strip() or "POLICY_PASS"
|
||||
else:
|
||||
score = "0.40"
|
||||
# The violation code (e.g. "POLICY_VIOLATION:PUBLIC_INGRESS") is on stdout.
|
||||
reason = proc.stdout.strip() or "POLICY_VIOLATION:UNKNOWN"
|
||||
|
||||
# Emit with literal score (two-decimal form per the contract) and a quoted
|
||||
# reason. Constructed manually so json.dumps does not collapse 0.90 -> 0.9.
|
||||
print('{"score": ' + score + ', "reason": ' + json.dumps(reason) + '}')
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Executable
+123
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env python3
|
||||
"""evidence_writer.py — REQ-11 / D-023 / D-005
|
||||
|
||||
Appends a hash-chained event to audit.json.
|
||||
|
||||
Each event: {"seq": N, "ts": <iso8601 UTC>, "stage": "...", "event": "...",
|
||||
"prev_hash": "<sha256 or GENESIS>", "hash": "<sha256 of canonical json of this event with hash empty>"}
|
||||
|
||||
Hash chain (D-023):
|
||||
1. Build event dict with hash = "" (empty string).
|
||||
2. canonical = json.dumps(event, sort_keys=True, separators=(",", ":"))
|
||||
3. hash = sha256(canonical.encode("utf-8")).hexdigest()
|
||||
4. event["hash"] = hash
|
||||
5. append to audit.json
|
||||
|
||||
Auto-genesis: if audit.json is empty/missing and --stage is not "genesis",
|
||||
a genesis event (seq 0, prev_hash "GENESIS") is inserted first.
|
||||
|
||||
Input:
|
||||
--stage <dev|qa|prod|finalize|genesis> (required)
|
||||
--event "<text>" (required)
|
||||
--audit <path> (optional, default ./audit.json)
|
||||
Output: stdout {"seq": N, "hash": "..."}
|
||||
Exit: 0 on success, 1 on I/O error.
|
||||
"""
|
||||
import argparse
|
||||
import datetime
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
GENESIS_EVENT_TEXT = "audit log initialized"
|
||||
|
||||
|
||||
def now_iso8601_utc() -> str:
|
||||
return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
|
||||
def compute_hash(event: dict) -> str:
|
||||
"""Compute the sha256 hash of an event using canonical JSON (D-023)."""
|
||||
tmp = dict(event)
|
||||
tmp["hash"] = ""
|
||||
canonical = json.dumps(tmp, sort_keys=True, separators=(",", ":"))
|
||||
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def make_event(seq: int, stage: str, event_text: str, prev_hash: str) -> dict:
|
||||
event = {
|
||||
"seq": seq,
|
||||
"ts": now_iso8601_utc(),
|
||||
"stage": stage,
|
||||
"event": event_text,
|
||||
"prev_hash": prev_hash,
|
||||
"hash": "",
|
||||
}
|
||||
event["hash"] = compute_hash(event)
|
||||
return event
|
||||
|
||||
|
||||
def load_audit(audit_path: str) -> list:
|
||||
if not os.path.exists(audit_path):
|
||||
return []
|
||||
try:
|
||||
with open(audit_path, "r", encoding="utf-8") as fh:
|
||||
data = json.load(fh)
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
return []
|
||||
if not isinstance(data, list):
|
||||
return []
|
||||
return data
|
||||
|
||||
|
||||
def atomic_write(audit_path: str, data: list) -> None:
|
||||
tmp_path = audit_path + ".tmp"
|
||||
with open(tmp_path, "w", encoding="utf-8") as fh:
|
||||
json.dump(data, fh, indent=2)
|
||||
fh.write("\n")
|
||||
os.replace(tmp_path, audit_path)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Append a hash-chained event to audit.json")
|
||||
parser.add_argument("--stage", required=True,
|
||||
choices=["dev", "qa", "prod", "finalize", "genesis"])
|
||||
parser.add_argument("--event", required=True)
|
||||
parser.add_argument("--audit", default="./audit.json")
|
||||
args = parser.parse_args()
|
||||
|
||||
events = load_audit(args.audit)
|
||||
|
||||
# Auto-genesis: if the log is empty and the caller did not ask for a
|
||||
# genesis event, seed one first.
|
||||
if len(events) == 0 and args.stage != "genesis":
|
||||
genesis = make_event(seq=0, stage="genesis", event_text=GENESIS_EVENT_TEXT,
|
||||
prev_hash="GENESIS")
|
||||
events.append(genesis)
|
||||
|
||||
# Determine the new seq + prev_hash.
|
||||
if events:
|
||||
last = events[-1]
|
||||
seq = last["seq"] + 1
|
||||
prev_hash = last["hash"]
|
||||
else:
|
||||
seq = 0
|
||||
prev_hash = "GENESIS"
|
||||
|
||||
new_event = make_event(seq=seq, stage=args.stage, event_text=args.event,
|
||||
prev_hash=prev_hash)
|
||||
events.append(new_event)
|
||||
|
||||
try:
|
||||
atomic_write(args.audit, events)
|
||||
except OSError as exc:
|
||||
print(f"evidence_writer: I/O error: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
print(json.dumps({"seq": new_event["seq"], "hash": new_event["hash"]}))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Executable
+182
@@ -0,0 +1,182 @@
|
||||
#!/usr/bin/env python3
|
||||
"""finalize_evidence.py — REQ-10 / D-028 / D-029
|
||||
|
||||
Uploads (PUT or POST) a local `audit.json` to the `acdl-evidence` repo on
|
||||
Gitea via the file-contents API. Used by the pipeline workflow steps to
|
||||
persist the hash-chained audit trail to `acdl-evidence` between dispatches
|
||||
(D-028 state-persistence across re-dispatches; D-029 finalize step).
|
||||
|
||||
Uses only the Python standard library (urllib.request) so it has no
|
||||
external dependency on `requests`. Auth header: `Authorization: token <token>`.
|
||||
|
||||
Input (argv flags):
|
||||
--audit <path> (required) local audit.json file to upload
|
||||
--owner <org> (optional, default continuous-intelligence)
|
||||
--repo <name> (optional, default acdl-evidence)
|
||||
--branch <name> (optional, default main)
|
||||
--path <remote path> (optional, default audit.json) path in the repo
|
||||
--token-env <env var> (optional, default ACDL_GITEA_TOKEN)
|
||||
--host <url> (optional, default https://git.cloudinit.dev)
|
||||
--message <commit msg> (optional, default chore(evidence): update audit.json)
|
||||
|
||||
Behavior:
|
||||
1. Read the token from os.environ[token_env]. Missing -> stderr + exit 1.
|
||||
2. Read the local audit file; base64-encode it.
|
||||
3. GET the current file at .../contents/<path>?ref=<branch> to discover
|
||||
the existing `sha`. 200 -> capture sha (update mode). 404 -> no sha
|
||||
(create mode). Other errors -> exit 1.
|
||||
4. If sha set: PUT with body {content, message, branch, sha}.
|
||||
If no sha: POST with body {content, message, branch}.
|
||||
5. Print {"uploaded": true, "path": "<path>", "sha": "<new sha>"} to
|
||||
stdout and exit 0.
|
||||
6. On any HTTP error: print
|
||||
{"uploaded": false, "status": <code>, "body": "<body>"} to stdout
|
||||
and exit 1.
|
||||
"""
|
||||
import argparse
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
|
||||
def _request(method: str, url: str, token: str, body: dict = None):
|
||||
"""Perform an HTTP request with the Gitea auth header. Returns
|
||||
(status_code, response_body_text). Raises URLError on network failure."""
|
||||
data = None
|
||||
headers = {"Authorization": f"token {token}",
|
||||
"Accept": "application/json"}
|
||||
if body is not None:
|
||||
data = json.dumps(body).encode("utf-8")
|
||||
headers["Content-Type"] = "application/json"
|
||||
req = urllib.request.Request(url, data=data, method=method, headers=headers)
|
||||
try:
|
||||
with urllib.request.urlopen(req) as resp:
|
||||
return resp.getcode(), resp.read().decode("utf-8", "replace")
|
||||
except urllib.error.HTTPError as exc:
|
||||
# HTTPError carries the response body
|
||||
try:
|
||||
body_text = exc.read().decode("utf-8", "replace")
|
||||
except Exception:
|
||||
body_text = ""
|
||||
return exc.code, body_text
|
||||
except urllib.error.URLError as exc:
|
||||
# Network-level failure (connection refused, DNS, timeout). Return
|
||||
# a synthetic 0 status + the reason so callers can report cleanly
|
||||
# without a stack trace.
|
||||
return 0, f"URLError: {exc.reason}"
|
||||
|
||||
|
||||
def get_existing_sha(host: str, owner: str, repo: str, path: str,
|
||||
branch: str, token: str):
|
||||
"""Return (sha-or-None, error_status_or_None). On 200 returns the sha.
|
||||
On 404 returns (None, None). Other codes return (None, (status, body))."""
|
||||
qs = urllib.parse.urlencode({"ref": branch})
|
||||
url = f"{host}/api/v1/repos/{owner}/{repo}/contents/{path}?{qs}"
|
||||
status, body = _request("GET", url, token)
|
||||
if status == 200:
|
||||
try:
|
||||
data = json.loads(body)
|
||||
return data.get("sha"), None
|
||||
except (ValueError, TypeError):
|
||||
return None, (status, body)
|
||||
if status == 404:
|
||||
return None, None
|
||||
return None, (status, body)
|
||||
|
||||
|
||||
def upload(host: str, owner: str, repo: str, path: str, branch: str,
|
||||
message: str, content_b64: str, sha, token: str):
|
||||
"""PUT (update) or POST (create) the file. Returns (new_sha, None) on
|
||||
success or (None, (status, body)) on HTTP error."""
|
||||
url = f"{host}/api/v1/repos/{owner}/{repo}/contents/{path}"
|
||||
if sha:
|
||||
body = {"content": content_b64, "message": message,
|
||||
"branch": branch, "sha": sha}
|
||||
status, resp = _request("PUT", url, token, body)
|
||||
else:
|
||||
body = {"content": content_b64, "message": message, "branch": branch}
|
||||
status, resp = _request("POST", url, token, body)
|
||||
if status in (200, 201):
|
||||
try:
|
||||
data = json.loads(resp)
|
||||
# The file-contents API returns the new content object either at
|
||||
# top-level `content` (POST create) or `content` (PUT update).
|
||||
new_sha = None
|
||||
if isinstance(data, dict):
|
||||
content_obj = data.get("content") or data
|
||||
if isinstance(content_obj, dict):
|
||||
new_sha = content_obj.get("sha")
|
||||
return new_sha, None
|
||||
except (ValueError, TypeError):
|
||||
return None, None
|
||||
return None, (status, resp)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Upload a local audit.json to the acdl-evidence Gitea "
|
||||
"repo via the file-contents API (D-028/D-029).")
|
||||
parser.add_argument("--audit", required=True,
|
||||
help="Local audit.json file to upload")
|
||||
parser.add_argument("--owner", default="continuous-intelligence",
|
||||
help="Gitea org (default: continuous-intelligence)")
|
||||
parser.add_argument("--repo", default="acdl-evidence",
|
||||
help="Gitea repo (default: acdl-evidence)")
|
||||
parser.add_argument("--branch", default="main",
|
||||
help="Target branch (default: main)")
|
||||
parser.add_argument("--path", default="audit.json",
|
||||
help="Remote path in the repo (default: audit.json)")
|
||||
parser.add_argument("--token-env", default="ACDL_GITEA_TOKEN",
|
||||
help="Env var name holding the Gitea token "
|
||||
"(default: ACDL_GITEA_TOKEN)")
|
||||
parser.add_argument("--host", default="https://git.cloudinit.dev",
|
||||
help="Gitea host URL (default: https://git.cloudinit.dev)")
|
||||
parser.add_argument("--message", default="chore(evidence): update audit.json",
|
||||
help="Commit message (default: chore(evidence): "
|
||||
"update audit.json)")
|
||||
args = parser.parse_args()
|
||||
|
||||
token = os.environ.get(args.token_env)
|
||||
if not token:
|
||||
print(f"finalize_evidence: required env var {args.token_env} is not "
|
||||
f"set", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# Read + base64-encode the local audit file. Missing/unreadable file is
|
||||
# a clean exit 1 (no stack trace).
|
||||
try:
|
||||
with open(args.audit, "rb") as fh:
|
||||
raw = fh.read()
|
||||
except OSError as exc:
|
||||
print(f"finalize_evidence: cannot read {args.audit}: {exc}",
|
||||
file=sys.stderr)
|
||||
return 1
|
||||
content_b64 = base64.b64encode(raw).decode("ascii")
|
||||
|
||||
# Discover existing sha (update vs create).
|
||||
sha, err = get_existing_sha(args.host, args.owner, args.repo,
|
||||
args.path, args.branch, token)
|
||||
if err is not None:
|
||||
status, body = err
|
||||
print(json.dumps({"uploaded": False, "status": status, "body": body}))
|
||||
return 1
|
||||
|
||||
# Upload (PUT if sha, POST otherwise).
|
||||
new_sha, err = upload(args.host, args.owner, args.repo, args.path,
|
||||
args.branch, args.message, content_b64, sha, token)
|
||||
if err is not None:
|
||||
status, body = err
|
||||
print(json.dumps({"uploaded": False, "status": status, "body": body}))
|
||||
return 1
|
||||
|
||||
print(json.dumps({"uploaded": True, "path": args.path,
|
||||
"sha": new_sha}))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Executable
+228
@@ -0,0 +1,228 @@
|
||||
#!/usr/bin/env bash
|
||||
# Phase 01 Gitea scaffolding. Idempotent.
|
||||
#
|
||||
# Creates the two new repos under the continuous-intelligence org, pushes a
|
||||
# placeholder index.html to acdl-evidence, and creates qa + prod branches on
|
||||
# acdl-contracts. Running against existing repos / branches / files is a
|
||||
# no-op (409 or 422 is treated as success).
|
||||
#
|
||||
# Usage: ACDL_GITEA_TOKEN=<token> scripts/gitea_setup.sh
|
||||
# Exit codes: 0 = success (created or already existed); 1 = unrecoverable error.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
GITEA_HOST="${GITEA_HOST:-https://git.cloudinit.dev}"
|
||||
ORG="continuous-intelligence"
|
||||
TOKEN="${ACDL_GITEA_TOKEN:?ACDL_GITEA_TOKEN is required}"
|
||||
API="${GITEA_HOST}/api/v1"
|
||||
|
||||
AUTH=(-H "Authorization: token ${TOKEN}" -H "Content-Type: application/json")
|
||||
|
||||
log() { printf '[setup] %s\n' "$*"; }
|
||||
warn() { printf '[setup][WARN] %s\n' "$*" >&2; }
|
||||
err() { printf '[setup][ERROR] %s\n' "$*" >&2; }
|
||||
|
||||
# --- helpers ----------------------------------------------------------------
|
||||
|
||||
# http_status_code URL
|
||||
http_get_status() {
|
||||
local url="$1"
|
||||
curl -sS -o /dev/null -w "%{http_code}" "${AUTH[@]}" "$url"
|
||||
}
|
||||
|
||||
# repo_exists NAME -> 0 if exists, 1 otherwise
|
||||
repo_exists() {
|
||||
local name="$1"
|
||||
local status
|
||||
status=$(http_get_status "${API}/repos/${ORG}/${name}")
|
||||
[ "$status" = "200" ]
|
||||
}
|
||||
|
||||
# create_repo NAME DESCRIPTION
|
||||
create_repo() {
|
||||
local name="$1"
|
||||
local description="$2"
|
||||
local body
|
||||
body=$(python3 -c "
|
||||
import json, sys
|
||||
print(json.dumps({
|
||||
'name': '${name}',
|
||||
'description': ${description@Q},
|
||||
'private': True,
|
||||
'default_branch': 'main',
|
||||
'auto_init': True,
|
||||
'gitignores': 'Python',
|
||||
'license': '',
|
||||
'readme': 'Default'
|
||||
}))
|
||||
")
|
||||
log "Creating repo ${ORG}/${name} (default_branch=main, auto_init=true)"
|
||||
local status body_out
|
||||
status=$(curl -sS -o /tmp/setup_repo_create.json -w "%{http_code}" \
|
||||
"${AUTH[@]}" -X POST -d "$body" \
|
||||
"${API}/orgs/${ORG}/repos")
|
||||
case "$status" in
|
||||
201) log " created (HTTP 201)" ;;
|
||||
409) log " already exists (HTTP 409); skipping" ;;
|
||||
*)
|
||||
err "create_repo ${name} failed: HTTP ${status}"
|
||||
cat /tmp/setup_repo_create.json >&2 || true
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# set_repo_visibility REPO VISIBILITY (public|private)
|
||||
set_repo_visibility() {
|
||||
local repo="$1"
|
||||
local visibility="$2"
|
||||
local body
|
||||
body=$(python3 -c "
|
||||
import json
|
||||
is_private = ('${visibility}' == 'private')
|
||||
print(json.dumps({'private': is_private, 'visibility': '${visibility}'}))
|
||||
")
|
||||
log "Setting ${repo} visibility to ${visibility}"
|
||||
local status
|
||||
status=$(curl -sS -o /tmp/setup_vis.json -w "%{http_code}" \
|
||||
"${AUTH[@]}" -X PATCH -d "$body" \
|
||||
"${API}/repos/${ORG}/${repo}")
|
||||
case "$status" in
|
||||
200) log " ok (HTTP 200)" ;;
|
||||
*) warn "set_repo_visibility ${repo} -> ${visibility} returned HTTP ${status} (continuing)"; cat /tmp/setup_vis.json >&2 || true ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# file_exists REPO PATH -> 0 if the file already exists on the default branch
|
||||
file_exists_on_default() {
|
||||
local repo="$1"
|
||||
local path="$2"
|
||||
local status
|
||||
status=$(http_get_status "${API}/repos/${ORG}/${repo}/contents/${path}?ref=main")
|
||||
[ "$status" = "200" ]
|
||||
}
|
||||
|
||||
# create_placeholder_index REPO
|
||||
create_placeholder_index() {
|
||||
local repo="$1"
|
||||
local path="index.html"
|
||||
local placeholder
|
||||
placeholder='<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>ACDL Evidence</title>
|
||||
<style>body{font-family:system-ui,sans-serif;margin:2rem;color:#333}</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>ACDL Evidence Stream</h1>
|
||||
<p>Evidence timeline will appear here in Phase 05.</p>
|
||||
<p>Placeholder served via Gitea raw file URL (D-012; Gitea has no native Pages).</p>
|
||||
</body>
|
||||
</html>'
|
||||
|
||||
if file_exists_on_default "$repo" "$path"; then
|
||||
log "index.html already exists on ${repo} main; skipping"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local body
|
||||
body=$(python3 -c "
|
||||
import json, base64
|
||||
content = '''${placeholder}'''
|
||||
print(json.dumps({
|
||||
'content': base64.b64encode(content.encode('utf-8')).decode('ascii'),
|
||||
'message': 'Initial placeholder index.html (Phase 01, D-016)',
|
||||
'branch': 'main'
|
||||
}))
|
||||
")
|
||||
log "Pushing placeholder index.html to ${repo} main"
|
||||
local status
|
||||
status=$(curl -sS -o /tmp/setup_index_push.json -w "%{http_code}" \
|
||||
"${AUTH[@]}" -X POST -d "$body" \
|
||||
"${API}/repos/${ORG}/${repo}/contents/${path}")
|
||||
case "$status" in
|
||||
201) log " pushed (HTTP 201)" ;;
|
||||
409|422) log " already exists or conflict (HTTP ${status}); skipping" ;;
|
||||
*)
|
||||
err "create_placeholder_index on ${repo} failed: HTTP ${status}"
|
||||
cat /tmp/setup_index_push.json >&2 || true
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# branch_exists REPO BRANCH -> 0 if exists
|
||||
branch_exists() {
|
||||
local repo="$1"
|
||||
local branch="$2"
|
||||
local status
|
||||
status=$(http_get_status "${API}/repos/${ORG}/${repo}/branches/${branch}")
|
||||
[ "$status" = "200" ]
|
||||
}
|
||||
|
||||
# create_branch REPO BRANCH FROM_REF
|
||||
create_branch() {
|
||||
local repo="$1"
|
||||
local branch="$2"
|
||||
local from_ref="$3"
|
||||
if branch_exists "$repo" "$branch"; then
|
||||
log "Branch ${branch} already exists on ${repo}; skipping"
|
||||
return 0
|
||||
fi
|
||||
local body
|
||||
body=$(python3 -c "
|
||||
import json
|
||||
print(json.dumps({'new_branch_name': '${branch}', 'old_branch_name': '${from_ref}'}))
|
||||
")
|
||||
log "Creating branch ${branch} on ${repo} from ${from_ref}"
|
||||
local status
|
||||
status=$(curl -sS -o /tmp/setup_branch.json -w "%{http_code}" \
|
||||
"${AUTH[@]}" -X POST -d "$body" \
|
||||
"${API}/repos/${ORG}/${repo}/branches")
|
||||
case "$status" in
|
||||
201) log " created (HTTP 201)" ;;
|
||||
409) log " already exists (HTTP 409); skipping" ;;
|
||||
*)
|
||||
err "create_branch ${branch} on ${repo} failed: HTTP ${status}"
|
||||
cat /tmp/setup_branch.json >&2 || true
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# --- main -------------------------------------------------------------------
|
||||
|
||||
log "Host: ${GITEA_HOST}"
|
||||
log "Org: ${ORG}"
|
||||
log "Token: <set, ${#TOKEN} chars>"
|
||||
|
||||
# Step 1: create acdl-contracts
|
||||
if ! repo_exists acdl-contracts; then
|
||||
create_repo acdl-contracts "ACDL developer + agentic entry surface (contract.yaml + issue trigger)" || exit 1
|
||||
else
|
||||
log "acdl-contracts already exists; skipping create"
|
||||
fi
|
||||
|
||||
# Step 2: create acdl-evidence
|
||||
if ! repo_exists acdl-evidence; then
|
||||
create_repo acdl-evidence "ACDL hash-chained audit timeline served as a static site via raw file URLs" || exit 1
|
||||
else
|
||||
log "acdl-evidence already exists; skipping create"
|
||||
fi
|
||||
|
||||
# Step 2b: make acdl-evidence public so the Phase 05 UI (index.html) can
|
||||
# fetch audit.json from a browser without exposing the API token (D-012
|
||||
# raw-URL approach). acdl-contracts stays private.
|
||||
set_repo_visibility acdl-evidence public
|
||||
|
||||
# Step 3: push placeholder index.html to acdl-evidence
|
||||
create_placeholder_index acdl-evidence || exit 1
|
||||
|
||||
# Step 4: create qa + prod branches on acdl-contracts (visible stand-in for
|
||||
# the unsupported Gitea environments API; per D-013).
|
||||
create_branch acdl-contracts qa main || exit 1
|
||||
create_branch acdl-contracts prod main || exit 1
|
||||
|
||||
log "Done. Run scripts/verify_phase01.sh to confirm success criteria."
|
||||
exit 0
|
||||
Executable
+118
@@ -0,0 +1,118 @@
|
||||
#!/usr/bin/env python3
|
||||
"""l3b_agent_stub.py — D-008 / D-026 / D-021
|
||||
|
||||
Parses a GitHub/Gitea Issue body by keywords and emits a contract.yaml that
|
||||
selects an L2 stack. This is the agentic (L3B) entry surface: deterministic
|
||||
keyword matching, no external AI APIs.
|
||||
|
||||
D-008 keyword map (priority order — first match wins):
|
||||
gas, price, ingest, data-lake -> l2-commodity-price-feed
|
||||
invoice, billing -> l2-invoice-service
|
||||
analytics, historical, query -> l2-energy-analytics-api
|
||||
regulatory, compliance, reporting, trading
|
||||
-> l2-regulatory-reporting
|
||||
(no match) -> l2-invoice-service (fallback)
|
||||
|
||||
Output contract.yaml (D-021 schema):
|
||||
stack: <mapped L2 name>
|
||||
inputs:
|
||||
environment: dev
|
||||
owner: citizen-developer
|
||||
source: l3b-agent-stub
|
||||
public-ingress: false
|
||||
|
||||
Input:
|
||||
argv[1] = issue body text (or stdin if argv[1] absent/empty)
|
||||
-o <path> = write the contract to a file (default: stdout)
|
||||
Exit:
|
||||
0 on success, 1 on empty input
|
||||
"""
|
||||
import sys
|
||||
|
||||
|
||||
# Ordered keyword groups -> L2 stack mapping (D-008). First match wins.
|
||||
KEYWORD_MAP = [
|
||||
(("gas", "price", "ingest", "data-lake"), "l2-commodity-price-feed"),
|
||||
(("invoice", "billing"), "l2-invoice-service"),
|
||||
(("analytics", "historical", "query"), "l2-energy-analytics-api"),
|
||||
(("regulatory", "compliance", "reporting", "trading"), "l2-regulatory-reporting"),
|
||||
]
|
||||
|
||||
FALLBACK_STACK = "l2-invoice-service"
|
||||
|
||||
|
||||
def map_issue_to_stack(text: str) -> str:
|
||||
lowered = text.lower()
|
||||
for keywords, stack in KEYWORD_MAP:
|
||||
for kw in keywords:
|
||||
if kw in lowered:
|
||||
return stack
|
||||
return FALLBACK_STACK
|
||||
|
||||
|
||||
def render_contract(stack: str) -> str:
|
||||
# Fixed-schema YAML (D-021). Emitted as text (no yaml dependency needed).
|
||||
return (
|
||||
f"stack: {stack}\n"
|
||||
"inputs:\n"
|
||||
" environment: dev\n"
|
||||
" owner: citizen-developer\n"
|
||||
" source: l3b-agent-stub\n"
|
||||
"public-ingress: false\n"
|
||||
)
|
||||
|
||||
|
||||
def read_issue_body(args: list) -> str:
|
||||
"""Read issue body from args[0] (already-stripped argv, no script name)
|
||||
or stdin. Empty -> error."""
|
||||
if len(args) >= 1 and args[0].strip():
|
||||
return args[0]
|
||||
# Fall back to stdin if argv body is absent or empty.
|
||||
if not sys.stdin.isatty():
|
||||
data = sys.stdin.read()
|
||||
if data.strip():
|
||||
return data
|
||||
return ""
|
||||
|
||||
|
||||
def parse_output_flag(argv: list):
|
||||
"""Extract -o <path> from argv (returns (rest, output_path))."""
|
||||
output_path = None
|
||||
rest = []
|
||||
i = 1
|
||||
while i < len(argv):
|
||||
arg = argv[i]
|
||||
if arg == "-o":
|
||||
if i + 1 < len(argv):
|
||||
output_path = argv[i + 1]
|
||||
i += 2
|
||||
continue
|
||||
else:
|
||||
print("l3b_agent_stub: -o requires a path argument", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
rest.append(arg)
|
||||
i += 1
|
||||
return rest, output_path
|
||||
|
||||
|
||||
def main() -> int:
|
||||
rest, output_path = parse_output_flag(sys.argv)
|
||||
body = read_issue_body(rest)
|
||||
if not body.strip():
|
||||
print("l3b_agent_stub: empty issue body (no argv[1] and no stdin)", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
stack = map_issue_to_stack(body)
|
||||
contract = render_contract(stack)
|
||||
|
||||
if output_path:
|
||||
with open(output_path, "w", encoding="utf-8") as fh:
|
||||
fh.write(contract)
|
||||
else:
|
||||
sys.stdout.write(contract)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Executable
+126
@@ -0,0 +1,126 @@
|
||||
#!/usr/bin/env bash
|
||||
# mock_executor.sh — REQ-06 / D-022
|
||||
#
|
||||
# Reads a contract.yaml, resolves the L2 composition, invokes each L1's
|
||||
# mock_apply.sh in order, and writes state.json to the current working
|
||||
# directory.
|
||||
#
|
||||
# Input: argv[1] = path to a contract.yaml file.
|
||||
# Output:
|
||||
# - stdout: per-L1 progress (echoed from each mock_apply.sh)
|
||||
# - state.json in cwd: {"l2": "...", "l1s": [...], "contract": {...}}
|
||||
# Exit:
|
||||
# 0 if all L1s exit 0; 1 if any L1 exited non-zero (state.json is still
|
||||
# written with the recorded exit codes).
|
||||
set -euo pipefail
|
||||
|
||||
if [[ $# -lt 1 ]]; then
|
||||
echo "usage: mock_executor.sh <contract.yaml>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CONTRACT_PATH="$1"
|
||||
|
||||
if [[ ! -f "$CONTRACT_PATH" ]]; then
|
||||
echo "contract not found: $CONTRACT_PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Parse the contract (stack + full contract dict) via python3 + yaml. ---
|
||||
# Emit stack on line 1 and the full contract JSON on line 2, then read both
|
||||
# lines into separate bash variables (so the JSON's internal spaces survive).
|
||||
CONTRACT_PARSED=$(python3 - "$CONTRACT_PATH" <<'PY'
|
||||
import sys, json, yaml
|
||||
path = sys.argv[1]
|
||||
with open(path, "r", encoding="utf-8") as fh:
|
||||
contract = yaml.safe_load(fh)
|
||||
if not isinstance(contract, dict):
|
||||
sys.stderr.write("contract is not a mapping\n")
|
||||
sys.exit(2)
|
||||
stack = contract.get("stack", "")
|
||||
# Use a compact JSON (no spaces) so the single-line contract survives bash
|
||||
# variable capture cleanly.
|
||||
print(stack)
|
||||
print(json.dumps(contract, sort_keys=True, separators=(",", ":")))
|
||||
PY
|
||||
)
|
||||
|
||||
STACK=$(printf '%s\n' "$CONTRACT_PARSED" | sed -n '1p')
|
||||
CONTRACT_JSON=$(printf '%s\n' "$CONTRACT_PARSED" | sed -n '2p')
|
||||
|
||||
if [[ -z "$STACK" ]]; then
|
||||
echo "contract missing 'stack' key" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Resolve the L2 manifest. ---
|
||||
L2_MANIFEST="modules/l2/${STACK}/manifest.yaml"
|
||||
if [[ ! -f "$L2_MANIFEST" ]]; then
|
||||
echo "L2_NOT_FOUND: ${STACK}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Read the L2's l1s: list (ordered names) via python. ---
|
||||
L1_NAMES_JSON=$(python3 - "$L2_MANIFEST" <<'PY'
|
||||
import sys, json, yaml
|
||||
path = sys.argv[1]
|
||||
with open(path, "r", encoding="utf-8") as fh:
|
||||
manifest = yaml.safe_load(fh)
|
||||
l1s = manifest.get("l1s", []) if isinstance(manifest, dict) else []
|
||||
names = [entry.get("name", "") for entry in l1s if isinstance(entry, dict)]
|
||||
print(json.dumps(names))
|
||||
PY
|
||||
)
|
||||
|
||||
# --- Invoke each L1's mock_apply.sh in order, recording exit codes. ---
|
||||
# Build the l1s results array in JSON via python, appending as we go.
|
||||
RESULTS_JSON="[]"
|
||||
|
||||
ALL_OK=0
|
||||
while IFS= read -r L1_NAME; do
|
||||
L1_SCRIPT="modules/l1/${L1_NAME}/mock_apply.sh"
|
||||
if [[ ! -f "$L1_SCRIPT" ]]; then
|
||||
echo "L1_NOT_FOUND: ${L1_NAME}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Capture stdout + exit code. stderr passes through.
|
||||
L1_OUT=$(bash "$L1_SCRIPT")
|
||||
L1_RC=$?
|
||||
|
||||
# Echo the L1's stdout so the pipeline sees the progress lines.
|
||||
printf '%s\n' "$L1_OUT"
|
||||
|
||||
# Record {"name": ..., "applied": true, "exit_code": ...}.
|
||||
RESULTS_JSON=$(python3 - "$RESULTS_JSON" "$L1_NAME" "$L1_RC" <<'PY'
|
||||
import sys, json
|
||||
results = json.loads(sys.argv[1])
|
||||
name = sys.argv[2]
|
||||
rc = int(sys.argv[3])
|
||||
results.append({"name": name, "applied": True, "exit_code": rc})
|
||||
print(json.dumps(results))
|
||||
PY
|
||||
)
|
||||
|
||||
if [[ $L1_RC -ne 0 ]]; then
|
||||
ALL_OK=1
|
||||
fi
|
||||
done < <(python3 -c "import sys, json; print('\n'.join(json.loads(sys.argv[1])))" "$L1_NAMES_JSON")
|
||||
|
||||
# --- Write state.json to the current working directory (D-022). ---
|
||||
python3 - "$RESULTS_JSON" "$STACK" "$CONTRACT_JSON" <<'PY'
|
||||
import sys, json
|
||||
results = json.loads(sys.argv[1])
|
||||
stack = sys.argv[2]
|
||||
contract = json.loads(sys.argv[3])
|
||||
state = {
|
||||
"l2": stack,
|
||||
"l1s": results,
|
||||
"contract": contract,
|
||||
}
|
||||
with open("state.json", "w", encoding="utf-8") as fh:
|
||||
json.dump(state, fh, indent=2)
|
||||
fh.write("\n")
|
||||
PY
|
||||
|
||||
exit "$ALL_OK"
|
||||
Executable
+51
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env python3
|
||||
"""policy_checker.py — REQ-07 / D-025
|
||||
|
||||
Reads a contract.yaml and enforces the single Phase-03 policy rule:
|
||||
`public-ingress: true` is forbidden.
|
||||
|
||||
Input: argv[1] = path to a contract.yaml file.
|
||||
Output: stdout "POLICY_PASS" or "POLICY_VIOLATION:PUBLIC_INGRESS"
|
||||
Exit: 0 on pass, 1 on violation.
|
||||
|
||||
Idempotent, no side effects (no file writes). Treats an absent or falsy
|
||||
`public-ingress` key as a pass.
|
||||
"""
|
||||
import sys
|
||||
import yaml
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) < 2:
|
||||
print("usage: policy_checker.py <contract.yaml>", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
contract_path = sys.argv[1]
|
||||
|
||||
try:
|
||||
with open(contract_path, "r", encoding="utf-8") as fh:
|
||||
contract = yaml.safe_load(fh)
|
||||
except FileNotFoundError:
|
||||
print(f"contract not found: {contract_path}", file=sys.stderr)
|
||||
return 2
|
||||
except yaml.YAMLError as exc:
|
||||
print(f"invalid yaml: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
# Treat missing/non-mapping as no policy violation.
|
||||
if not isinstance(contract, dict):
|
||||
print("POLICY_PASS")
|
||||
return 0
|
||||
|
||||
public_ingress = contract.get("public-ingress", False)
|
||||
|
||||
if public_ingress is True:
|
||||
print("POLICY_VIOLATION:PUBLIC_INGRESS")
|
||||
return 1
|
||||
|
||||
print("POLICY_PASS")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Executable
+258
@@ -0,0 +1,258 @@
|
||||
#!/usr/bin/env bash
|
||||
# scripts/run_demo.sh — Phase 05 dry-run simulation of the 4 demo acts (T-5.2).
|
||||
#
|
||||
# Simulates the full 4-act demo locally (no act_runner) by calling the core
|
||||
# scripts in sequence and writing hash-chained evidence events to audit.json,
|
||||
# then optionally uploads audit.json + evidence-ui/index.html to acdl-evidence
|
||||
# main via finalize_evidence.py (D-031, D-033).
|
||||
#
|
||||
# Usage: scripts/run_demo.sh [--no-upload]
|
||||
# --no-upload skip the Gitea API calls (useful for testing without a token)
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Parse args
|
||||
# -----------------------------------------------------------------------------
|
||||
UPLOAD=1
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--no-upload)
|
||||
UPLOAD=0
|
||||
;;
|
||||
*)
|
||||
echo "run_demo.sh: unknown argument: $arg" >&2
|
||||
echo "usage: scripts/run_demo.sh [--no-upload]" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Paths
|
||||
# -----------------------------------------------------------------------------
|
||||
# Repo root = location of this script's parent dir.
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
WORKDIR="/tmp/acdl_demo_run"
|
||||
AUDIT="$WORKDIR/audit.json"
|
||||
CONTRACTS="$WORKDIR/contracts"
|
||||
|
||||
# Track failures so we can return non-zero at the end (we do NOT use set -e
|
||||
# because policy_checker intentionally exits 1 on Act 4).
|
||||
FAIL=0
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Write one evidence event. Args: <stage> <event-text>
|
||||
ev() {
|
||||
local stage="$1"
|
||||
local text="$2"
|
||||
if ! python3 "$SCRIPT_DIR/evidence_writer.py" --stage "$stage" --event "$text" --audit "$AUDIT"; then
|
||||
echo "run_demo.sh: evidence_writer failed for stage=$stage text=$text" >&2
|
||||
FAIL=1
|
||||
fi
|
||||
}
|
||||
|
||||
# Run a contract through the Act 2/3 pipeline (policy -> confidence -> executor).
|
||||
# Assumes the contract already passed policy (caller verifies). Writes the
|
||||
# standard 4-event sequence. Args: <act-label> <dev-applied-event-text>
|
||||
run_passing_pipeline() {
|
||||
local dev_event="$1"
|
||||
|
||||
ev dev "$dev_event"
|
||||
ev qa "qa approved"
|
||||
ev prod "prod approved"
|
||||
ev finalize "finalize: audit.json committed to acdl-evidence"
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Setup working directory
|
||||
# -----------------------------------------------------------------------------
|
||||
mkdir -p "$CONTRACTS"
|
||||
rm -f "$AUDIT"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Initialize audit (genesis)
|
||||
# -----------------------------------------------------------------------------
|
||||
echo "== run_demo.sh: initializing audit at $AUDIT =="
|
||||
ev genesis "audit log initialized"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Act 1 — Friction
|
||||
# -----------------------------------------------------------------------------
|
||||
echo "== Act 1 — Friction =="
|
||||
ev dev "Act 1 Friction: manual 2-week deployment (legacy process)"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Act 2 — Developer Self-Service
|
||||
# -----------------------------------------------------------------------------
|
||||
echo "== Act 2 — Developer Self-Service =="
|
||||
cat > "$CONTRACTS/act2.yaml" <<'YAML'
|
||||
stack: l2-commodity-price-feed
|
||||
inputs:
|
||||
environment: dev
|
||||
owner: platform-team
|
||||
public-ingress: false
|
||||
YAML
|
||||
|
||||
ACT2_POLICY="$(python3 "$SCRIPT_DIR/policy_checker.py" "$CONTRACTS/act2.yaml")"
|
||||
ACT2_POLICY_RC=$?
|
||||
echo " policy_checker: $ACT2_POLICY (rc=$ACT2_POLICY_RC)"
|
||||
if [ "$ACT2_POLICY" != "POLICY_PASS" ]; then
|
||||
echo "run_demo.sh: Act 2 expected POLICY_PASS, got '$ACT2_POLICY'" >&2
|
||||
FAIL=1
|
||||
fi
|
||||
|
||||
ACT2_CONF="$(python3 "$SCRIPT_DIR/confidence_signal.py" "$CONTRACTS/act2.yaml")"
|
||||
echo " confidence_signal: $ACT2_CONF"
|
||||
# Expected: {"score": 0.90, "reason": "POLICY_PASS"}
|
||||
|
||||
# mock_executor.sh resolves modules/l2/<stack>/manifest.yaml relative to its
|
||||
# cwd, so it must run from the repo root. It writes state.json to its cwd;
|
||||
# clean it up from the repo root afterward so no stray file is left there.
|
||||
(
|
||||
cd "$REPO_ROOT" && bash "$SCRIPT_DIR/mock_executor.sh" "$CONTRACTS/act2.yaml"
|
||||
)
|
||||
MOCK_RC=$?
|
||||
rm -f "$REPO_ROOT/state.json"
|
||||
if [ "$MOCK_RC" -ne 0 ]; then
|
||||
echo "run_demo.sh: Act 2 mock_executor failed (rc=$MOCK_RC)" >&2
|
||||
FAIL=1
|
||||
fi
|
||||
|
||||
run_passing_pipeline "dev applied: l2-commodity-price-feed"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Act 3 — Citizen Developer
|
||||
# -----------------------------------------------------------------------------
|
||||
echo "== Act 3 — Citizen Developer =="
|
||||
ISSUE_BODY="We need to ingest natural gas prices from Platts and report on compliance for the trading desk."
|
||||
if ! python3 "$SCRIPT_DIR/l3b_agent_stub.py" "$ISSUE_BODY" -o "$CONTRACTS/act3.yaml"; then
|
||||
echo "run_demo.sh: l3b_agent_stub failed for Act 3" >&2
|
||||
FAIL=1
|
||||
fi
|
||||
|
||||
# Confirm the generated contract's stack (D-008: gas/price matches first).
|
||||
ACT3_STACK="$(python3 -c "import yaml,sys; print(yaml.safe_load(open('$CONTRACTS/act3.yaml'))['stack'])" 2>/dev/null || echo "")"
|
||||
echo " l3b generated stack: $ACT3_STACK"
|
||||
if [ "$ACT3_STACK" != "l2-commodity-price-feed" ]; then
|
||||
echo "run_demo.sh: WARNING Act 3 expected stack l2-commodity-price-feed, got '$ACT3_STACK'" >&2
|
||||
# Continue anyway per the task spec.
|
||||
fi
|
||||
|
||||
ACT3_POLICY="$(python3 "$SCRIPT_DIR/policy_checker.py" "$CONTRACTS/act3.yaml")"
|
||||
ACT3_POLICY_RC=$?
|
||||
echo " policy_checker: $ACT3_POLICY (rc=$ACT3_POLICY_RC)"
|
||||
if [ "$ACT3_POLICY" != "POLICY_PASS" ]; then
|
||||
echo "run_demo.sh: Act 3 expected POLICY_PASS, got '$ACT3_POLICY'" >&2
|
||||
FAIL=1
|
||||
fi
|
||||
|
||||
ACT3_CONF="$(python3 "$SCRIPT_DIR/confidence_signal.py" "$CONTRACTS/act3.yaml")"
|
||||
echo " confidence_signal: $ACT3_CONF"
|
||||
|
||||
(
|
||||
cd "$REPO_ROOT" && bash "$SCRIPT_DIR/mock_executor.sh" "$CONTRACTS/act3.yaml"
|
||||
)
|
||||
MOCK_RC=$?
|
||||
rm -f "$REPO_ROOT/state.json"
|
||||
if [ "$MOCK_RC" -ne 0 ]; then
|
||||
echo "run_demo.sh: Act 3 mock_executor failed (rc=$MOCK_RC)" >&2
|
||||
FAIL=1
|
||||
fi
|
||||
|
||||
run_passing_pipeline "dev applied: l2-commodity-price-feed (Act 3 from issue)"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Act 4 — Safety Net
|
||||
# -----------------------------------------------------------------------------
|
||||
echo "== Act 4 — Safety Net =="
|
||||
cat > "$CONTRACTS/act4.yaml" <<'YAML'
|
||||
stack: l2-regulatory-reporting
|
||||
inputs:
|
||||
environment: dev
|
||||
owner: platform-team
|
||||
public-ingress: true
|
||||
YAML
|
||||
|
||||
# policy_checker exits 1 on violation; capture without failing the script.
|
||||
ACT4_POLICY="$(python3 "$SCRIPT_DIR/policy_checker.py" "$CONTRACTS/act4.yaml" 2>&1 || true)"
|
||||
echo " policy_checker: $ACT4_POLICY"
|
||||
if [ "$ACT4_POLICY" != "POLICY_VIOLATION:PUBLIC_INGRESS" ]; then
|
||||
echo "run_demo.sh: Act 4 expected POLICY_VIOLATION:PUBLIC_INGRESS, got '$ACT4_POLICY'" >&2
|
||||
FAIL=1
|
||||
fi
|
||||
|
||||
ACT4_CONF="$(python3 "$SCRIPT_DIR/confidence_signal.py" "$CONTRACTS/act4.yaml")"
|
||||
echo " confidence_signal: $ACT4_CONF"
|
||||
# Expected: {"score": 0.40, "reason": "POLICY_VIOLATION:PUBLIC_INGRESS"}
|
||||
|
||||
# Score < 0.50 -> dev rejects. Do NOT run mock_executor, do NOT write qa/prod/finalize.
|
||||
ev dev "dev rejected: POLICY_VIOLATION:PUBLIC_INGRESS (confidence 0.40 < 0.50)"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Summary
|
||||
# -----------------------------------------------------------------------------
|
||||
echo "== Summary =="
|
||||
python3 - "$AUDIT" <<'PY'
|
||||
import json, sys
|
||||
audit = json.load(open(sys.argv[1]))
|
||||
for e in audit:
|
||||
print(f"{e['seq']} | {e['stage']} | {e['event']} | {e['hash'][:12]}")
|
||||
print(f"total events: {len(audit)}")
|
||||
PY
|
||||
|
||||
EVENT_COUNT="$(python3 -c "import json; print(len(json.load(open('$AUDIT'))))")"
|
||||
echo "event count: $EVENT_COUNT"
|
||||
|
||||
if [ "$EVENT_COUNT" -lt 11 ]; then
|
||||
echo "run_demo.sh: expected >= 11 events, got $EVENT_COUNT" >&2
|
||||
FAIL=1
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Upload (optional)
|
||||
# -----------------------------------------------------------------------------
|
||||
if [ "$UPLOAD" -eq 1 ]; then
|
||||
echo "== Upload =="
|
||||
if [ -z "${ACDL_GITEA_TOKEN:-}" ]; then
|
||||
echo "run_demo.sh: ACDL_GITEA_TOKEN not set; skipping upload (use --no-upload to silence)" >&2
|
||||
else
|
||||
# Upload audit.json to acdl-evidence main.
|
||||
if python3 "$SCRIPT_DIR/finalize_evidence.py" --audit "$AUDIT"; then
|
||||
echo " audit.json uploaded"
|
||||
else
|
||||
echo "run_demo.sh: finalize_evidence failed for audit.json" >&2
|
||||
FAIL=1
|
||||
fi
|
||||
# Upload index.html (the --audit flag accepts any local file path; --path
|
||||
# sets the remote destination).
|
||||
if python3 "$SCRIPT_DIR/finalize_evidence.py" \
|
||||
--audit "$REPO_ROOT/evidence-ui/index.html" \
|
||||
--path index.html \
|
||||
--message "chore(ui): update index.html (demo dry run)"; then
|
||||
echo " index.html uploaded"
|
||||
else
|
||||
echo "run_demo.sh: finalize_evidence failed for index.html" >&2
|
||||
FAIL=1
|
||||
fi
|
||||
echo "Uploaded audit.json + index.html to acdl-evidence main"
|
||||
echo " raw URL: https://git.cloudinit.dev/continuous-intelligence/acdl-evidence/raw/branch/main/index.html"
|
||||
fi
|
||||
else
|
||||
echo "== Upload skipped (--no-upload) =="
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Exit
|
||||
# -----------------------------------------------------------------------------
|
||||
if [ "$FAIL" -ne 0 ]; then
|
||||
echo "run_demo.sh: one or more steps failed (see warnings above)" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "run_demo.sh: OK ($EVENT_COUNT events)"
|
||||
exit 0
|
||||
Executable
+109
@@ -0,0 +1,109 @@
|
||||
#!/usr/bin/env bash
|
||||
# Phase 01 verification script.
|
||||
# Confirms the three-repo scaffold exists under the continuous-intelligence
|
||||
# Gitea org and that the Phase 01 visible artifacts (placeholder index.html on
|
||||
# acdl-evidence; qa + prod branches on acdl-contracts) are present.
|
||||
#
|
||||
# Usage: ACDL_GITEA_TOKEN=<token> scripts/verify_phase01.sh
|
||||
# Exit codes: 0 = all checks passed; 1 = one or more checks failed.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
GITEA_HOST="${GITEA_HOST:-https://git.cloudinit.dev}"
|
||||
ORG="continuous-intelligence"
|
||||
TOKEN="${ACDL_GITEA_TOKEN:-}"
|
||||
|
||||
fail_count=0
|
||||
note() { printf ' [%s] %s\n' "$1" "$2"; }
|
||||
pass() { note "PASS" "$1"; }
|
||||
fail() { note "FAIL" "$1"; fail_count=$((fail_count + 1)); }
|
||||
warn() { printf ' [WARN] %s\n' "$1" >&2; }
|
||||
|
||||
echo "== Phase 01 verification =="
|
||||
echo "Host: $GITEA_HOST"
|
||||
echo "Org: $ORG"
|
||||
if [ -n "$TOKEN" ]; then
|
||||
echo "Token: <set, ${#TOKEN} chars>"
|
||||
else
|
||||
echo "Token: <unset>"
|
||||
fi
|
||||
echo
|
||||
|
||||
# --- Check 1: acdl-contracts repo exists ---
|
||||
echo "-- Check 1: acdl-contracts repo exists --"
|
||||
status=$(curl -sS -o /tmp/p01_contracts.json -w "%{http_code}" \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
"${GITEA_HOST}/api/v1/repos/${ORG}/acdl-contracts")
|
||||
if [ "$status" = "200" ]; then
|
||||
default_branch=$(python3 -c "import json; print(json.load(open('/tmp/p01_contracts.json')).get('default_branch','?'))")
|
||||
pass "acdl-contracts exists (default_branch=${default_branch})"
|
||||
else
|
||||
fail "acdl-contracts GET returned HTTP ${status}"
|
||||
fi
|
||||
|
||||
# --- Check 2: acdl-evidence repo exists ---
|
||||
echo "-- Check 2: acdl-evidence repo exists --"
|
||||
status=$(curl -sS -o /tmp/p01_evidence.json -w "%{http_code}" \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
"${GITEA_HOST}/api/v1/repos/${ORG}/acdl-evidence")
|
||||
if [ "$status" = "200" ]; then
|
||||
default_branch=$(python3 -c "import json; print(json.load(open('/tmp/p01_evidence.json')).get('default_branch','?'))")
|
||||
pass "acdl-evidence exists (default_branch=${default_branch})"
|
||||
else
|
||||
fail "acdl-evidence GET returned HTTP ${status}"
|
||||
fi
|
||||
|
||||
# --- Check 3: acdl-evidence raw index.html returns 200 (Pages substitute per D-012/D-016) ---
|
||||
# acdl-evidence is public per gitea_setup.sh step 2b, so the raw URL should
|
||||
# work without auth. We also try with the auth header as a fallback so the
|
||||
# check does not spuriously fail if the repo visibility was reset.
|
||||
echo "-- Check 3: acdl-evidence raw index.html returns 200 --"
|
||||
index_url="${GITEA_HOST}/${ORG}/acdl-evidence/raw/branch/main/index.html"
|
||||
status=$(curl -sS -o /tmp/p01_index.html -w "%{http_code}" "${index_url}")
|
||||
if [ "$status" != "200" ] && [ -n "$TOKEN" ]; then
|
||||
warn "raw URL returned ${status} unauth; retrying with Authorization header"
|
||||
status=$(curl -sS -o /tmp/p01_index.html -w "%{http_code}" \
|
||||
-H "Authorization: token ${TOKEN}" "${index_url}")
|
||||
fi
|
||||
if [ "$status" = "200" ]; then
|
||||
body_size=$(wc -c < /tmp/p01_index.html)
|
||||
if grep -q "ACDL Evidence" /tmp/p01_index.html; then
|
||||
pass "raw index.html returns 200 with placeholder body (${body_size} bytes)"
|
||||
else
|
||||
fail "raw index.html returns 200 but body does not contain 'ACDL Evidence' marker"
|
||||
fi
|
||||
else
|
||||
fail "GET ${index_url} returned HTTP ${status}"
|
||||
fi
|
||||
|
||||
# --- Check 4: qa + prod branches exist on acdl-contracts ---
|
||||
echo "-- Check 4: qa + prod branches exist on acdl-contracts --"
|
||||
status=$(curl -sS -o /tmp/p01_branches.json -w "%{http_code}" \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
"${GITEA_HOST}/api/v1/repos/${ORG}/acdl-contracts/branches?limit=50")
|
||||
if [ "$status" != "200" ]; then
|
||||
fail "list branches on acdl-contracts returned HTTP ${status}"
|
||||
else
|
||||
for want in qa prod; do
|
||||
if python3 -c "
|
||||
import json, sys
|
||||
branches = json.load(open('/tmp/p01_branches.json'))
|
||||
names = [b.get('name', '') for b in branches]
|
||||
sys.exit(0 if '${want}' in names else 1)
|
||||
"; then
|
||||
pass "branch '${want}' exists on acdl-contracts"
|
||||
else
|
||||
fail "branch '${want}' missing on acdl-contracts"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "== Summary =="
|
||||
if [ "$fail_count" -eq 0 ]; then
|
||||
echo "Phase 01 verification PASSED (all checks ok)"
|
||||
exit 0
|
||||
else
|
||||
echo "Phase 01 verification FAILED (${fail_count} check(s) failed)"
|
||||
exit 1
|
||||
fi
|
||||
Executable
+135
@@ -0,0 +1,135 @@
|
||||
#!/usr/bin/env bash
|
||||
# Phase 02 verification script.
|
||||
# Confirms the 8 L1 module folders exist under modules/l1/ with the exact
|
||||
# names from REQ-02, each containing a valid manifest.yaml (D-017 schema)
|
||||
# and a uniform mock_apply.sh (D-007 + D-018) that exits 0 with the
|
||||
# expected echo markers.
|
||||
#
|
||||
# Usage: scripts/verify_phase02.sh
|
||||
# Exit codes: 0 = all checks passed; 1 = one or more checks failed.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
L1_DIR="${ROOT}/modules/l1"
|
||||
|
||||
# Expected L1 names per REQ-02 / D-019.
|
||||
EXPECTED_L1S=(
|
||||
l1-eks-fargate
|
||||
l1-iam-role
|
||||
l1-lambda
|
||||
l1-api-gateway
|
||||
l1-eventbridge
|
||||
l1-sqs
|
||||
l1-s3
|
||||
l1-cloudwatch
|
||||
)
|
||||
|
||||
fail_count=0
|
||||
pass() { printf ' [PASS] %s\n' "$1"; }
|
||||
fail() { printf ' [FAIL] %s\n' "$1"; fail_count=$((fail_count + 1)); }
|
||||
|
||||
echo "== Phase 02 verification =="
|
||||
echo "L1 dir: ${L1_DIR}"
|
||||
echo
|
||||
|
||||
# --- Check 1: exactly 8 L1 folders with the expected names ---
|
||||
echo "-- Check 1: 8 L1 folders with expected names --"
|
||||
if [ ! -d "$L1_DIR" ]; then
|
||||
fail "modules/l1/ does not exist"
|
||||
echo
|
||||
echo "== Summary =="
|
||||
echo "Phase 02 verification FAILED (${fail_count} check(s) failed)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
actual_folders=$(ls "$L1_DIR" | sort | tr '\n' ' ')
|
||||
expected_folders=$(printf '%s\n' "${EXPECTED_L1S[@]}" | sort | tr '\n' ' ')
|
||||
if [ "$actual_folders" = "$expected_folders" ]; then
|
||||
pass "exactly 8 L1 folders present and named correctly"
|
||||
else
|
||||
fail "L1 folder list mismatch"
|
||||
echo " expected: $expected_folders"
|
||||
echo " actual: $actual_folders"
|
||||
fi
|
||||
|
||||
# --- Per-L1 checks ---
|
||||
for l1 in "${EXPECTED_L1S[@]}"; do
|
||||
echo "-- L1: ${l1} --"
|
||||
dir="${L1_DIR}/${l1}"
|
||||
|
||||
# Check 2a: folder exists
|
||||
if [ ! -d "$dir" ]; then
|
||||
fail "${l1}: folder missing"
|
||||
continue
|
||||
fi
|
||||
pass "${l1}: folder exists"
|
||||
|
||||
# Check 2b: manifest.yaml exists + parses + name matches folder + kind=l1
|
||||
manifest="${dir}/manifest.yaml"
|
||||
if [ ! -f "$manifest" ]; then
|
||||
fail "${l1}: manifest.yaml missing"
|
||||
else
|
||||
manifest_ok=$(python3 -c "
|
||||
import yaml, sys
|
||||
try:
|
||||
d = yaml.safe_load(open('${manifest}'))
|
||||
name = d.get('name') == '${l1}'
|
||||
kind = d.get('kind') == 'l1'
|
||||
has_inputs = isinstance(d.get('inputs'), dict)
|
||||
sys.exit(0 if (name and kind and has_inputs) else 1)
|
||||
except Exception as e:
|
||||
print(f' parse error: {e}', file=sys.stderr)
|
||||
sys.exit(2)
|
||||
" 2>/dev/null; echo $?)
|
||||
if [ "$manifest_ok" = "0" ]; then
|
||||
pass "${l1}: manifest.yaml valid (name=${l1}, kind=l1, inputs present)"
|
||||
else
|
||||
fail "${l1}: manifest.yaml invalid (name/kind/inputs check failed; rc=${manifest_ok})"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check 2c: mock_apply.sh exists + executable + bash -n clean
|
||||
apply="${dir}/mock_apply.sh"
|
||||
if [ ! -f "$apply" ]; then
|
||||
fail "${l1}: mock_apply.sh missing"
|
||||
continue
|
||||
fi
|
||||
if [ ! -x "$apply" ]; then
|
||||
fail "${l1}: mock_apply.sh not executable"
|
||||
else
|
||||
pass "${l1}: mock_apply.sh is executable"
|
||||
fi
|
||||
if ! bash -n "$apply" 2>/dev/null; then
|
||||
fail "${l1}: mock_apply.sh bash -n failed"
|
||||
else
|
||||
pass "${l1}: mock_apply.sh bash -n clean"
|
||||
fi
|
||||
|
||||
# Check 2d: end-to-end run: exit 0 + expected markers, completes in <2s
|
||||
start=$(date +%s)
|
||||
output=$("$apply" 2>&1)
|
||||
rc=$?
|
||||
elapsed=$(( $(date +%s) - start ))
|
||||
if [ "$rc" -ne 0 ]; then
|
||||
fail "${l1}: mock_apply.sh exited ${rc}"
|
||||
elif ! echo "$output" | grep -qF "[L1: ${l1}] applying..."; then
|
||||
fail "${l1}: missing '[L1: ${l1}] applying...' marker"
|
||||
elif ! echo "$output" | grep -qF "[L1: ${l1}] OK"; then
|
||||
fail "${l1}: missing '[L1: ${l1}] OK' marker"
|
||||
elif [ "$elapsed" -lt 1 ] || [ "$elapsed" -gt 2 ]; then
|
||||
fail "${l1}: run took ${elapsed}s (expected ~1s; 1<=t<=2 ok)"
|
||||
else
|
||||
pass "${l1}: mock_apply.sh runs, exits 0, markers correct (${elapsed}s)"
|
||||
fi
|
||||
done
|
||||
|
||||
echo
|
||||
echo "== Summary =="
|
||||
if [ "$fail_count" -eq 0 ]; then
|
||||
echo "Phase 02 verification PASSED (8 L1 modules, all checks ok)"
|
||||
exit 0
|
||||
else
|
||||
echo "Phase 02 verification FAILED (${fail_count} check(s) failed)"
|
||||
exit 1
|
||||
fi
|
||||
Executable
+240
@@ -0,0 +1,240 @@
|
||||
#!/usr/bin/env bash
|
||||
# Phase 03 verification script.
|
||||
# Confirms the 4 L2 modules and the 5 core scripts conform to their contracts.
|
||||
#
|
||||
# Usage: scripts/verify_phase03.sh
|
||||
# Exit codes: 0 = all checks passed; 1 = one or more checks failed.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
fail_count=0
|
||||
pass() { printf ' [PASS] %s\n' "$1"; }
|
||||
fail() { printf ' [FAIL] %s\n' "$1"; fail_count=$((fail_count + 1)); }
|
||||
|
||||
# Expected L2 names per REQ-04.
|
||||
EXPECTED_L2S=(
|
||||
l2-invoice-service
|
||||
l2-commodity-price-feed
|
||||
l2-energy-analytics-api
|
||||
l2-regulatory-reporting
|
||||
)
|
||||
|
||||
echo "== Phase 03 verification =="
|
||||
echo "Root: ${ROOT}"
|
||||
echo
|
||||
|
||||
# --- Check 1: exactly 4 L2 folders with the expected names ---
|
||||
echo "-- Check 1: 4 L2 folders with expected names --"
|
||||
actual=$(ls modules/l2/ 2>/dev/null | sort | tr '\n' ' ')
|
||||
expected=$(printf '%s\n' "${EXPECTED_L2S[@]}" | sort | tr '\n' ' ')
|
||||
if [ "$actual" = "$expected" ]; then
|
||||
pass "exactly 4 L2 folders present and named correctly"
|
||||
else
|
||||
fail "L2 folder list mismatch"
|
||||
echo " expected: $expected"
|
||||
echo " actual: $actual"
|
||||
fi
|
||||
|
||||
# --- Check 2: each L2 manifest.yaml validates + references 5 existing L1s ---
|
||||
echo "-- Check 2: L2 manifests reference 5 existing L1s --"
|
||||
l2_validate=$(python3 << 'PYEOF' || true
|
||||
import yaml, glob, os, sys
|
||||
ok = True
|
||||
l1s = set(os.listdir('modules/l1'))
|
||||
for f in sorted(glob.glob('modules/l2/*/manifest.yaml')):
|
||||
d = yaml.safe_load(open(f))
|
||||
folder = os.path.basename(os.path.dirname(f))
|
||||
problems = []
|
||||
if d.get('name') != folder: problems.append(f"name != {folder}")
|
||||
if d.get('kind') != 'l2': problems.append("kind != l2")
|
||||
refs = [x.get('name') for x in d.get('l1s', [])]
|
||||
if len(refs) != 5: problems.append(f"expected 5 l1s, got {len(refs)}")
|
||||
unknown = [r for r in refs if r not in l1s]
|
||||
if unknown: problems.append(f"unknown L1 refs: {unknown}")
|
||||
# each l1 entry must have an inputs: map
|
||||
for x in d.get('l1s', []):
|
||||
if not isinstance(x.get('inputs'), dict): problems.append(f"l1 {x.get('name')} missing inputs map")
|
||||
status = 'OK' if not problems else 'FAIL: ' + '; '.join(problems)
|
||||
print(f' [{status}] {f}')
|
||||
if problems: ok = False
|
||||
sys.exit(0 if ok else 1)
|
||||
PYEOF
|
||||
)
|
||||
echo "$l2_validate"
|
||||
if [ "$l2_validate" = "" ] || echo "$l2_validate" | grep -q FAIL; then
|
||||
if ! echo "$l2_validate" | grep -q PASS; then
|
||||
fail "one or more L2 manifests invalid (see above)"
|
||||
fi
|
||||
else
|
||||
pass "all 4 L2 manifests valid"
|
||||
fi
|
||||
# Re-run for the explicit pass/fail count
|
||||
python3 << 'PYEOF' > /tmp/l2_check.txt 2>&1 || true
|
||||
import yaml, glob, os, sys
|
||||
ok = True
|
||||
l1s = set(os.listdir('modules/l1'))
|
||||
for f in sorted(glob.glob('modules/l2/*/manifest.yaml')):
|
||||
d = yaml.safe_load(open(f))
|
||||
folder = os.path.basename(os.path.dirname(f))
|
||||
if d.get('name') != folder: ok = False
|
||||
if d.get('kind') != 'l2': ok = False
|
||||
refs = [x.get('name') for x in d.get('l1s', [])]
|
||||
if len(refs) != 5: ok = False
|
||||
if any(r not in l1s for r in refs): ok = False
|
||||
for x in d.get('l1s', []):
|
||||
if not isinstance(x.get('inputs'), dict): ok = False
|
||||
sys.exit(0 if ok else 1)
|
||||
PYEOF
|
||||
if [ $? -eq 0 ]; then pass "all 4 L2 manifests pass structural + reference checks"; else fail "L2 manifest structural check"; fi
|
||||
|
||||
# --- Check 3: typecheck (bash -n + py_compile + yaml load) ---
|
||||
echo "-- Check 3: typecheck --"
|
||||
if bash -n scripts/mock_executor.sh; then pass "bash -n mock_executor.sh"; else fail "bash -n mock_executor.sh"; fi
|
||||
if python3 -m py_compile scripts/policy_checker.py scripts/confidence_signal.py scripts/evidence_writer.py scripts/l3b_agent_stub.py 2>/dev/null; then
|
||||
pass "py_compile all 4 python scripts"
|
||||
else
|
||||
fail "py_compile"
|
||||
fi
|
||||
if python3 -c "import yaml, glob; [yaml.safe_load(open(f)) for f in glob.glob('modules/l2/*/manifest.yaml')]" 2>/dev/null; then
|
||||
pass "yaml load all L2 manifests"
|
||||
else
|
||||
fail "yaml load L2 manifests"
|
||||
fi
|
||||
|
||||
# --- Check 4: policy_checker (D-025) ---
|
||||
echo "-- Check 4: policy_checker behavior (D-025) --"
|
||||
WORK="$(mktemp -d)"
|
||||
trap 'rm -rf "$WORK" "$ROOT/tmp_pass_contract.yaml" "$ROOT/tmp_fail_contract.yaml" "$ROOT/state.json" 2>/dev/null || true' EXIT
|
||||
printf 'stack: l2-commodity-price-feed\npublic-ingress: false\n' > "$WORK/pass.yaml"
|
||||
printf 'stack: l2-regulatory-reporting\npublic-ingress: true\n' > "$WORK/fail.yaml"
|
||||
out=$(python3 scripts/policy_checker.py "$WORK/pass.yaml" 2>&1); rc=$?
|
||||
if [ "$out" = "POLICY_PASS" ] && [ "$rc" = "0" ]; then
|
||||
pass "policy_checker pass contract -> POLICY_PASS exit 0"
|
||||
else
|
||||
fail "policy_checker pass contract: got '$out' exit=$rc"
|
||||
fi
|
||||
out=$(python3 scripts/policy_checker.py "$WORK/fail.yaml" 2>&1); rc=$?
|
||||
if [ "$out" = "POLICY_VIOLATION:PUBLIC_INGRESS" ] && [ "$rc" = "1" ]; then
|
||||
pass "policy_checker fail contract -> POLICY_VIOLATION:PUBLIC_INGRESS exit 1"
|
||||
else
|
||||
fail "policy_checker fail contract: got '$out' exit=$rc"
|
||||
fi
|
||||
|
||||
# --- Check 5: confidence_signal (D-024) ---
|
||||
echo "-- Check 5: confidence_signal behavior (D-024) --"
|
||||
out=$(python3 scripts/confidence_signal.py "$WORK/pass.yaml" 2>&1); rc=$?
|
||||
if echo "$out" | grep -q '"score": 0.90' && [ "$rc" = "0" ]; then
|
||||
pass "confidence_signal pass -> score 0.90 exit 0"
|
||||
else
|
||||
fail "confidence_signal pass: got '$out' exit=$rc"
|
||||
fi
|
||||
out=$(python3 scripts/confidence_signal.py "$WORK/fail.yaml" 2>&1); rc=$?
|
||||
if echo "$out" | grep -q '"score": 0.40' && [ "$rc" = "0" ]; then
|
||||
pass "confidence_signal fail -> score 0.40 exit 0"
|
||||
else
|
||||
fail "confidence_signal fail: got '$out' exit=$rc"
|
||||
fi
|
||||
|
||||
# --- Check 6: evidence_writer hash chain (D-023) ---
|
||||
echo "-- Check 6: evidence_writer hash chain (D-023) --"
|
||||
rm -f "$WORK/audit.json"
|
||||
python3 scripts/evidence_writer.py --stage dev --event "dev start" --audit "$WORK/audit.json" > /dev/null
|
||||
python3 scripts/evidence_writer.py --stage qa --event "qa approved" --audit "$WORK/audit.json" > /dev/null
|
||||
python3 scripts/evidence_writer.py --stage prod --event "prod approved" --audit "$WORK/audit.json" > /dev/null
|
||||
chain_ok=$(python3 << PYEOF
|
||||
import json, hashlib, sys
|
||||
try:
|
||||
events = json.load(open("$WORK/audit.json"))
|
||||
assert len(events) == 4, f"expected 4 (genesis + 3), got {len(events)}"
|
||||
assert events[0]['prev_hash'] == 'GENESIS', "genesis prev_hash"
|
||||
for i in range(1, len(events)):
|
||||
assert events[i]['prev_hash'] == events[i-1]['hash'], f"chain break at {i}"
|
||||
e = dict(events[i]); h = e.pop('hash'); e['hash'] = ''
|
||||
canon = json.dumps(e, sort_keys=True, separators=(',',':'))
|
||||
assert hashlib.sha256(canon.encode()).hexdigest() == h, f"hash mismatch at {i}"
|
||||
print("OK")
|
||||
except AssertionError as ex:
|
||||
print(f"FAIL: {ex}")
|
||||
sys.exit(1)
|
||||
PYEOF
|
||||
)
|
||||
if [ "$chain_ok" = "OK" ]; then
|
||||
pass "evidence_writer: 4 events, GENESIS + 3, chain links + hashes valid"
|
||||
else
|
||||
fail "evidence_writer chain: $chain_ok"
|
||||
fi
|
||||
|
||||
# --- Check 7: mock_executor (D-022) ---
|
||||
echo "-- Check 7: mock_executor writes state.json (D-022) --"
|
||||
rm -f "$ROOT/state.json"
|
||||
out=$(bash scripts/mock_executor.sh "$WORK/pass.yaml" 2>&1); rc=$?
|
||||
if [ "$rc" != "0" ]; then
|
||||
fail "mock_executor exit $rc (expected 0)"
|
||||
else
|
||||
me_ok=$(python3 << PYEOF
|
||||
import json, sys
|
||||
try:
|
||||
s = json.load(open("$ROOT/state.json"))
|
||||
assert s['l2'] == 'l2-commodity-price-feed', f"l2 mismatch: {s.get('l2')}"
|
||||
assert 'l1s' in s and len(s['l1s']) == 5, f"expected 5 l1s, got {len(s.get('l1s', []))}"
|
||||
assert all(x['applied'] is True and x['exit_code'] == 0 for x in s['l1s']), "l1 not all applied+0"
|
||||
assert 'contract' in s, "missing contract field"
|
||||
print("OK")
|
||||
except Exception as ex:
|
||||
print(f"FAIL: {ex}")
|
||||
sys.exit(1)
|
||||
PYEOF
|
||||
)
|
||||
if [ "$me_ok" = "OK" ]; then
|
||||
pass "mock_executor: state.json with l2 + 5 l1s (all exit 0) + contract"
|
||||
else
|
||||
fail "mock_executor state.json: $me_ok"
|
||||
fi
|
||||
fi
|
||||
rm -f "$ROOT/state.json"
|
||||
|
||||
# --- Check 8: l3b_agent_stub D-008 keyword map ---
|
||||
echo "-- Check 8: l3b_agent_stub keyword map (D-008) --"
|
||||
act3=$(python3 scripts/l3b_agent_stub.py "We need to ingest natural gas prices from Platts and report on compliance." 2>&1)
|
||||
if echo "$act3" | grep -q 'stack: l2-commodity-price-feed'; then
|
||||
pass "l3b Act 3 example -> l2-commodity-price-feed"
|
||||
else
|
||||
fail "l3b Act 3 example: got '$act3'"
|
||||
fi
|
||||
fallback=$(python3 scripts/l3b_agent_stub.py "please deploy something" 2>&1)
|
||||
if echo "$fallback" | grep -q 'stack: l2-invoice-service'; then
|
||||
pass "l3b fallback (no keywords) -> l2-invoice-service"
|
||||
else
|
||||
fail "l3b fallback: got '$fallback'"
|
||||
fi
|
||||
regulatory=$(python3 scripts/l3b_agent_stub.py "regulatory compliance reporting for trading desk" 2>&1)
|
||||
if echo "$regulatory" | grep -q 'stack: l2-regulatory-reporting'; then
|
||||
pass "l3b regulatory keywords -> l2-regulatory-reporting"
|
||||
else
|
||||
fail "l3b regulatory: got '$regulatory'"
|
||||
fi
|
||||
invoice=$(python3 scripts/l3b_agent_stub.py "monthly invoice and billing reconciliation" 2>&1)
|
||||
if echo "$invoice" | grep -q 'stack: l2-invoice-service'; then
|
||||
pass "l3b invoice keywords -> l2-invoice-service"
|
||||
else
|
||||
fail "l3b invoice: got '$invoice'"
|
||||
fi
|
||||
analytics=$(python3 scripts/l3b_agent_stub.py "historical analytics and query API" 2>&1)
|
||||
if echo "$analytics" | grep -q 'stack: l2-energy-analytics-api'; then
|
||||
pass "l3b analytics keywords -> l2-energy-analytics-api"
|
||||
else
|
||||
fail "l3b analytics: got '$analytics'"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "== Summary =="
|
||||
if [ "$fail_count" -eq 0 ]; then
|
||||
echo "Phase 03 verification PASSED (4 L2s + 5 core scripts, all checks ok)"
|
||||
exit 0
|
||||
else
|
||||
echo "Phase 03 verification FAILED (${fail_count} check(s) failed)"
|
||||
exit 1
|
||||
fi
|
||||
Executable
+186
@@ -0,0 +1,186 @@
|
||||
#!/usr/bin/env bash
|
||||
# Phase 04 verification script.
|
||||
# Confirms the pipeline workflow + issue trigger + finalize_evidence.py
|
||||
# conform to the Phase 04 plan and the Gitea Actions topology in
|
||||
# ARCHITECTURE.md. Does NOT execute a real Gitea Actions run (act_runner
|
||||
# is not registered in this environment); validates structure + syntax
|
||||
# + a dry-run of finalize_evidence.py against a dead host.
|
||||
#
|
||||
# Usage: scripts/verify_phase04.sh
|
||||
# Exit codes: 0 = all checks passed; 1 = one or more checks failed.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
fail_count=0
|
||||
pass() { printf ' [PASS] %s\n' "$1"; }
|
||||
fail() { printf ' [FAIL] %s\n' "$1"; fail_count=$((fail_count + 1)); }
|
||||
|
||||
echo "== Phase 04 verification =="
|
||||
echo "Root: ${ROOT}"
|
||||
echo
|
||||
|
||||
# --- Check 1: typecheck ---
|
||||
echo "-- Check 1: typecheck --"
|
||||
if bash -n scripts/finalize_evidence.py 2>/dev/null || python3 -m py_compile scripts/finalize_evidence.py 2>/dev/null; then
|
||||
pass "py_compile finalize_evidence.py"
|
||||
else
|
||||
fail "py_compile finalize_evidence.py"
|
||||
fi
|
||||
if python3 -c "import yaml; yaml.safe_load(open('.gitea/workflows/pipeline.yml')); yaml.safe_load(open('contracts-repo/.gitea/workflows/issue-to-contract.yml'))" 2>/dev/null; then
|
||||
pass "yaml load both workflows"
|
||||
else
|
||||
fail "yaml load workflows"
|
||||
fi
|
||||
|
||||
# --- Check 2: pipeline.yml structure ---
|
||||
echo "-- Check 2: pipeline.yml structure (D-027, D-028) --"
|
||||
p_struct=$(python3 << 'PYEOF'
|
||||
import yaml, sys
|
||||
try:
|
||||
d = yaml.safe_load(open('.gitea/workflows/pipeline.yml'))
|
||||
on = d.get('on', d.get(True)) or {}
|
||||
assert 'workflow_dispatch' in on, 'no workflow_dispatch trigger'
|
||||
inputs = on['workflow_dispatch']['inputs']
|
||||
assert set(inputs.keys()) == {'contract-ref', 'approve_qa', 'approve_prod'}, f'inputs: {set(inputs.keys())}'
|
||||
assert inputs['contract-ref']['type'] == 'string', 'contract-ref type'
|
||||
assert inputs['approve_qa']['type'] == 'boolean', 'approve_qa type'
|
||||
assert inputs['approve_prod']['type'] == 'boolean', 'approve_prod type'
|
||||
jobs = d['jobs']
|
||||
assert set(jobs.keys()) == {'dev', 'qa-gate', 'prod-gate', 'finalize'}, f'jobs: {set(jobs.keys())}'
|
||||
dev_if = jobs['dev'].get('if', '')
|
||||
assert 'approve_qa' in dev_if and 'approve_prod' in dev_if, f'dev.if: {dev_if}'
|
||||
qa_if = jobs['qa-gate'].get('if', '')
|
||||
assert 'approve_qa' in qa_if, f'qa-gate.if: {qa_if}'
|
||||
prod_if = jobs['prod-gate'].get('if', '')
|
||||
assert 'approve_prod' in prod_if, f'prod-gate.if: {prod_if}'
|
||||
fin_needs = jobs['finalize'].get('needs', [])
|
||||
assert fin_needs == ['prod-gate'] or fin_needs == 'prod-gate', f'finalize.needs: {fin_needs}'
|
||||
# All jobs runs-on ubuntu-latest
|
||||
for name, job in jobs.items():
|
||||
assert job.get('runs-on') == 'ubuntu-latest', f'{name} runs-on: {job.get("runs-on")}'
|
||||
print('OK')
|
||||
except AssertionError as ex:
|
||||
print(f'FAIL: {ex}')
|
||||
sys.exit(1)
|
||||
except Exception as ex:
|
||||
print(f'FAIL: {ex}')
|
||||
sys.exit(1)
|
||||
PYEOF
|
||||
)
|
||||
if [ "$p_struct" = "OK" ]; then
|
||||
pass "pipeline.yml: 3 inputs + 4 jobs + correct if: conditions + finalize.needs=prod-gate"
|
||||
else
|
||||
fail "pipeline.yml structure: $p_struct"
|
||||
fi
|
||||
|
||||
# --- Check 3: pipeline.yml references core scripts ---
|
||||
echo "-- Check 3: pipeline.yml references core scripts (D-029) --"
|
||||
text=$(cat .gitea/workflows/pipeline.yml)
|
||||
missing=""
|
||||
for ref in policy_checker.py confidence_signal.py mock_executor.sh evidence_writer.py finalize_evidence.py; do
|
||||
if ! echo "$text" | grep -qF "$ref"; then
|
||||
missing="$missing $ref"
|
||||
fi
|
||||
done
|
||||
if [ -z "$missing" ]; then
|
||||
pass "pipeline.yml references all 5 core scripts"
|
||||
else
|
||||
fail "pipeline.yml missing references:$missing"
|
||||
fi
|
||||
# Branch-pin documentation
|
||||
if echo "$text" | grep -q 'milestone/v1.0-initial'; then
|
||||
pass "pipeline.yml documents branch-pin to milestone/v1.0-initial"
|
||||
else
|
||||
fail "pipeline.yml missing branch-pin reference"
|
||||
fi
|
||||
|
||||
# --- Check 4: issue-to-contract.yml structure ---
|
||||
echo "-- Check 4: issue-to-contract.yml structure (D-030) --"
|
||||
i_struct=$(python3 << 'PYEOF'
|
||||
import yaml, sys
|
||||
try:
|
||||
d = yaml.safe_load(open('contracts-repo/.gitea/workflows/issue-to-contract.yml'))
|
||||
on = d.get('on', d.get(True)) or {}
|
||||
assert 'issues' in on, 'no issues trigger'
|
||||
assert on['issues']['types'] == ['opened'], f'types: {on["issues"]["types"]}'
|
||||
assert 'parse-and-trigger' in d['jobs'], 'no parse-and-trigger job'
|
||||
assert d['jobs']['parse-and-trigger'].get('runs-on') == 'ubuntu-latest', 'runs-on'
|
||||
print('OK')
|
||||
except AssertionError as ex:
|
||||
print(f'FAIL: {ex}')
|
||||
sys.exit(1)
|
||||
PYEOF
|
||||
)
|
||||
if [ "$i_struct" = "OK" ]; then
|
||||
pass "issue-to-contract.yml: issues[opened] + parse-and-trigger job"
|
||||
else
|
||||
fail "issue-to-contract.yml structure: $i_struct"
|
||||
fi
|
||||
|
||||
# --- Check 5: issue-to-contract.yml references + dispatch endpoint ---
|
||||
echo "-- Check 5: issue-to-contract.yml references + dispatch (D-014, D-030) --"
|
||||
text=$(cat contracts-repo/.gitea/workflows/issue-to-contract.yml)
|
||||
missing=""
|
||||
for ref in l3b_agent_stub.py 'actions/workflows/pipeline.yml/dispatches' 'contract-ref' 'gitea.event.issue.number' 'GITEA_TOKEN' 'new_branch'; do
|
||||
if ! echo "$text" | grep -qF "$ref"; then
|
||||
missing="$missing $ref"
|
||||
fi
|
||||
done
|
||||
if [ -z "$missing" ]; then
|
||||
pass "issue-to-contract.yml: l3b_agent_stub + dispatch + contract-ref + issue number + token + new_branch"
|
||||
else
|
||||
fail "issue-to-contract.yml missing references:$missing"
|
||||
fi
|
||||
|
||||
# --- Check 6: finalize_evidence.py --help + clean failure ---
|
||||
echo "-- Check 6: finalize_evidence.py CLI + clean failure modes ---"
|
||||
out=$(python3 scripts/finalize_evidence.py --help 2>&1); rc=$?
|
||||
if [ "$rc" = "0" ] && echo "$out" | grep -qi 'usage\|--audit\|--owner'; then
|
||||
pass "finalize_evidence.py --help exits 0 with usage"
|
||||
else
|
||||
fail "finalize_evidence.py --help: rc=$rc"
|
||||
fi
|
||||
|
||||
# Missing audit file (with a fake token so it gets past the env check) → exit 1, no stack trace
|
||||
out=$(ACDL_GITEA_TOKEN=fake python3 scripts/finalize_evidence.py --audit /tmp/definitely_nonexistent_audit.json 2>&1); rc=$?
|
||||
if [ "$rc" = "1" ] && ! echo "$out" | grep -q 'Traceback'; then
|
||||
pass "finalize_evidence.py missing file → exit 1, no stack trace"
|
||||
else
|
||||
fail "finalize_evidence.py missing file: rc=$rc, out='$out'"
|
||||
fi
|
||||
|
||||
# Missing token env (audit file present) → exit 1, no stack trace
|
||||
printf '[]\n' > /tmp/empty_audit.json
|
||||
out=$(env -u ACDL_GITEA_TOKEN python3 scripts/finalize_evidence.py --audit /tmp/empty_audit.json 2>&1); rc=$?
|
||||
if [ "$rc" = "1" ] && ! echo "$out" | grep -q 'Traceback'; then
|
||||
pass "finalize_evidence.py missing token env → exit 1, no stack trace"
|
||||
else
|
||||
fail "finalize_evidence.py missing token: rc=$rc, out='$out'"
|
||||
fi
|
||||
|
||||
# --- Check 7: finalize_evidence.py dry-run against a dead host (clean failure) ---
|
||||
echo "-- Check 7: finalize_evidence.py dry-run against dead host ---"
|
||||
# Use a real audit.json but point at a host that will refuse the connection.
|
||||
printf '[{"seq":0,"ts":"2026-07-21T00:00:00Z","stage":"genesis","event":"init","prev_hash":"GENESIS","hash":"x"}]\n' > /tmp/real_audit.json
|
||||
out=$(ACDL_GITEA_TOKEN=fake GITEA_HOST=http://127.0.0.1:0 python3 scripts/finalize_evidence.py --audit /tmp/real_audit.json --host http://127.0.0.1:0 2>&1); rc=$?
|
||||
if [ "$rc" = "1" ] && ! echo "$out" | grep -q 'Traceback'; then
|
||||
pass "finalize_evidence.py dead host → exit 1, no stack trace (clean API failure)"
|
||||
else
|
||||
fail "finalize_evidence.py dead host: rc=$rc, out='$out'"
|
||||
fi
|
||||
|
||||
# Cleanup
|
||||
rm -f /tmp/empty_audit.json /tmp/real_audit.json
|
||||
|
||||
echo
|
||||
echo "== Summary =="
|
||||
if [ "$fail_count" -eq 0 ]; then
|
||||
echo "Phase 04 verification PASSED (pipeline + issue trigger + finalize helper, all checks ok)"
|
||||
exit 0
|
||||
else
|
||||
echo "Phase 04 verification FAILED (${fail_count} check(s) failed)"
|
||||
exit 1
|
||||
fi
|
||||
Executable
+213
@@ -0,0 +1,213 @@
|
||||
#!/usr/bin/env bash
|
||||
# Phase 05 verification script.
|
||||
# Validates the evidence UI + the 4-act demo dry-run.
|
||||
#
|
||||
# Usage: scripts/verify_phase05.sh
|
||||
# Exit codes: 0 = all checks passed; 1 = one or more checks failed.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
fail_count=0
|
||||
pass() { printf ' [PASS] %s\n' "$1"; }
|
||||
fail() { printf ' [FAIL] %s\n' "$1"; fail_count=$((fail_count + 1)); }
|
||||
|
||||
GITEA_HOST="${GITEA_HOST:-https://git.cloudinit.dev}"
|
||||
ORG="continuous-intelligence"
|
||||
EVIDENCE_REPO="acdl-evidence"
|
||||
|
||||
echo "== Phase 05 verification =="
|
||||
echo "Root: ${ROOT}"
|
||||
echo
|
||||
|
||||
# --- Check 1: evidence-ui/index.html structure ---
|
||||
echo "-- Check 1: evidence-ui/index.html structure (D-032, REQ-14) --"
|
||||
UI="evidence-ui/index.html"
|
||||
if [ ! -f "$UI" ]; then
|
||||
fail "$UI missing"
|
||||
else
|
||||
pass "$UI exists"
|
||||
size=$(wc -c < "$UI")
|
||||
if [ "$size" -ge 1000 ] && [ "$size" -le 30000 ]; then
|
||||
pass "$UI size ${size} bytes (within 1-30 KB range)"
|
||||
else
|
||||
fail "$UI size ${size} bytes (expected 1-30 KB)"
|
||||
fi
|
||||
ui_check=$(python3 << 'PYEOF'
|
||||
import re, sys
|
||||
content = open('evidence-ui/index.html').read()
|
||||
problems = []
|
||||
if '<style>' not in content or '</style>' not in content: problems.append('missing inline <style>')
|
||||
if '<script>' not in content or '</script>' not in content: problems.append('missing inline <script>')
|
||||
if 'fetch(' not in content: problems.append('missing fetch call')
|
||||
if "'./audit.json'" not in content and '"./audit.json"' not in content: problems.append('missing relative ./audit.json fetch')
|
||||
external = re.findall(r'(?:src|href)\s*=\s*["\']https?://', content)
|
||||
if external: problems.append(f'external resource refs: {external}')
|
||||
# Confirm a refresh button or refresh function exists
|
||||
if 'refresh' not in content.lower(): problems.append('no refresh button/function')
|
||||
print('OK' if not problems else 'FAIL: ' + '; '.join(problems))
|
||||
PYEOF
|
||||
)
|
||||
if [ "$ui_check" = "OK" ]; then
|
||||
pass "$UI structural checks (inline CSS/JS, fetch ./audit.json, no external refs, refresh)"
|
||||
else
|
||||
fail "$UI structural: $ui_check"
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- Check 2: run_demo.sh syntax ---
|
||||
echo "-- Check 2: run_demo.sh syntax + flags --"
|
||||
if bash -n scripts/run_demo.sh 2>/dev/null; then
|
||||
pass "run_demo.sh bash -n clean"
|
||||
else
|
||||
fail "run_demo.sh bash -n"
|
||||
fi
|
||||
if grep -q -- '--no-upload' scripts/run_demo.sh; then
|
||||
pass "run_demo.sh supports --no-upload flag"
|
||||
else
|
||||
fail "run_demo.sh missing --no-upload flag"
|
||||
fi
|
||||
|
||||
# --- Check 3: run_demo.sh dry-run (no upload) ---
|
||||
echo "-- Check 3: run_demo.sh --no-upload (4 acts, 11 events) --"
|
||||
rm -rf /tmp/acdl_demo_run
|
||||
out=$(ACDL_GITEA_TOKEN= bash scripts/run_demo.sh --no-upload 2>&1); rc=$?
|
||||
if [ "$rc" = "0" ]; then
|
||||
pass "run_demo.sh --no-upload exits 0"
|
||||
else
|
||||
fail "run_demo.sh --no-upload exit $rc"
|
||||
echo "$out" | tail -10
|
||||
fi
|
||||
audit="/tmp/acdl_demo_run/audit.json"
|
||||
if [ -f "$audit" ]; then
|
||||
pass "audit.json written to $audit"
|
||||
else
|
||||
fail "audit.json missing at $audit"
|
||||
fi
|
||||
|
||||
# --- Check 4: audit.json event count + Act 4 rejection ---
|
||||
echo "-- Check 4: audit.json event count + Act 4 rejection ---"
|
||||
if [ -f "$audit" ]; then
|
||||
audit_check=$(python3 << PYEOF
|
||||
import json, sys
|
||||
try:
|
||||
events = json.load(open("$audit"))
|
||||
n = len(events)
|
||||
if n < 11:
|
||||
print(f"FAIL: too few events ({n}, expected >= 11)")
|
||||
sys.exit(1)
|
||||
if not any('POLICY_VIOLATION:PUBLIC_INGRESS' in x.get('event', '') for x in events):
|
||||
print("FAIL: no Act 4 rejection event")
|
||||
sys.exit(1)
|
||||
if not any('Act 1 Friction' in x.get('event', '') for x in events):
|
||||
print("FAIL: no Act 1 event")
|
||||
sys.exit(1)
|
||||
if not any('Act 3' in x.get('event', '') for x in events):
|
||||
print("FAIL: no Act 3 event")
|
||||
sys.exit(1)
|
||||
if not any('l2-commodity-price-feed' in x.get('event', '') for x in events):
|
||||
print("FAIL: no l2-commodity-price-feed event")
|
||||
sys.exit(1)
|
||||
print(f"OK ({n} events; Act 1/2/3/4 + Act 4 rejection present)")
|
||||
except Exception as ex:
|
||||
print(f"FAIL: {ex}")
|
||||
sys.exit(1)
|
||||
PYEOF
|
||||
)
|
||||
if echo "$audit_check" | grep -q "^OK"; then
|
||||
pass "$audit_check"
|
||||
else
|
||||
fail "audit content: $audit_check"
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- Check 5: audit.json hash chain integrity ---
|
||||
echo "-- Check 5: audit.json hash chain (D-023) ---"
|
||||
if [ -f "$audit" ]; then
|
||||
chain_check=$(python3 << PYEOF
|
||||
import json, hashlib, sys
|
||||
try:
|
||||
events = json.load(open("$audit"))
|
||||
assert events[0]['prev_hash'] == 'GENESIS', "genesis prev_hash"
|
||||
for i in range(1, len(events)):
|
||||
assert events[i]['prev_hash'] == events[i-1]['hash'], f"chain break at {i}"
|
||||
e = dict(events[i]); h = e.pop('hash'); e['hash'] = ''
|
||||
canon = json.dumps(e, sort_keys=True, separators=(',',':'))
|
||||
assert hashlib.sha256(canon.encode()).hexdigest() == h, f"hash mismatch at {i}"
|
||||
print("OK")
|
||||
except AssertionError as ex:
|
||||
print(f"FAIL: {ex}")
|
||||
sys.exit(1)
|
||||
PYEOF
|
||||
)
|
||||
if [ "$chain_check" = "OK" ]; then
|
||||
pass "audit.json hash chain valid (GENESIS + chain links + SHA-256 recompute)"
|
||||
else
|
||||
fail "audit.json hash chain: $chain_check"
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- Check 6: no stray files in repo root ---
|
||||
echo "-- Check 6: no stray files in repo root ---"
|
||||
if [ -f "$ROOT/state.json" ]; then
|
||||
fail "state.json left in repo root"
|
||||
else
|
||||
pass "no state.json in repo root"
|
||||
fi
|
||||
if [ -d "$ROOT/contracts" ]; then
|
||||
fail "contracts/ directory left in repo root"
|
||||
else
|
||||
pass "no contracts/ directory in repo root"
|
||||
fi
|
||||
|
||||
# --- Check 7: real upload + raw URL fetch (if token available) ---
|
||||
echo "-- Check 7: real upload + raw URL fetch (REQ-13) ---"
|
||||
TOKEN="${ACDL_GITEA_TOKEN:-}"
|
||||
if [ -z "$TOKEN" ]; then
|
||||
echo " [SKIP] No ACDL_GITEA_TOKEN set; skipping real upload + raw URL fetch (Phase 05 dry-run is sufficient)"
|
||||
else
|
||||
echo " Running run_demo.sh (with upload)..."
|
||||
upload_out=$(bash scripts/run_demo.sh 2>&1); upload_rc=$?
|
||||
if [ "$upload_rc" = "0" ]; then
|
||||
pass "run_demo.sh (with upload) exits 0"
|
||||
else
|
||||
fail "run_demo.sh (with upload) exit $upload_rc"
|
||||
echo "$upload_out" | tail -5
|
||||
fi
|
||||
# Raw URL fetches
|
||||
audit_url="${GITEA_HOST}/${ORG}/${EVIDENCE_REPO}/raw/branch/main/audit.json"
|
||||
index_url="${GITEA_HOST}/${ORG}/${EVIDENCE_REPO}/raw/branch/main/index.html"
|
||||
audit_status=$(curl -sS -o /tmp/p05_audit_remote.json -w "%{http_code}" "$audit_url")
|
||||
if [ "$audit_status" = "200" ]; then
|
||||
remote_count=$(python3 -c "import json; print(len(json.load(open('/tmp/p05_audit_remote.json'))))" 2>/dev/null || echo "?")
|
||||
if [ "$remote_count" = "11" ] || [ "$remote_count" -ge 11 ] 2>/dev/null; then
|
||||
pass "raw audit.json returns 200 with ${remote_count} events"
|
||||
else
|
||||
pass "raw audit.json returns 200 (events: ${remote_count})"
|
||||
fi
|
||||
else
|
||||
fail "raw audit.json GET returned HTTP ${audit_status}"
|
||||
fi
|
||||
index_status=$(curl -sS -o /tmp/p05_index_remote.html -w "%{http_code}" "$index_url")
|
||||
if [ "$index_status" = "200" ]; then
|
||||
if grep -q "ACDL Evidence" /tmp/p05_index_remote.html && grep -q "audit.json" /tmp/p05_index_remote.html; then
|
||||
pass "raw index.html returns 200 with ACDL Evidence + audit.json reference"
|
||||
else
|
||||
fail "raw index.html returns 200 but missing ACDL Evidence / audit.json markers"
|
||||
fi
|
||||
else
|
||||
fail "raw index.html GET returned HTTP ${index_status}"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "== Summary =="
|
||||
if [ "$fail_count" -eq 0 ]; then
|
||||
echo "Phase 05 verification PASSED (UI + 4-act dry-run, all checks ok)"
|
||||
exit 0
|
||||
else
|
||||
echo "Phase 05 verification FAILED (${fail_count} check(s) failed)"
|
||||
exit 1
|
||||
fi
|
||||
Reference in New Issue
Block a user