Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
.git
.gitignore
.github
.env
.venv
venv
__pycache__
*.py[cod]
.pytest_cache
.mypy_cache
.ruff_cache
node_modules
demo-output
.quickstart-ci
data
*.db
*.sqlite
*.sqlite3
*.pem
*.key
*.bin
*.zip
dist
build
77 changes: 77 additions & 0 deletions .github/workflows/publish-container.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
name: Publish container

on:
push:
branches: [main]
paths:
- 'app/**'
- 'verifier/**'
- 'requirements.txt'
- 'run.py'
- 'Dockerfile'
- '.dockerignore'
- '.github/workflows/publish-container.yml'
release:
types: [published]
workflow_dispatch:

permissions:
contents: read
packages: write

concurrency:
group: publish-container-${{ github.ref }}
cancel-in-progress: false

env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}

jobs:
publish:
runs-on: ubuntu-latest

steps:
- name: Check out source
uses: actions/checkout@v7
with:
ref: ${{ github.event.release.tag_name || github.ref }}

- name: Set up QEMU
uses: docker/setup-qemu-action@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4

- name: Log in to GitHub Container Registry
uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract image metadata
id: meta
uses: docker/metadata-action@v6
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
flavor: |
latest=false
tags: |
type=raw,value=edge,enable=${{ github.event_name == 'push' }}
type=sha,prefix=sha-,format=short,enable=${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }}
type=raw,value=manual,enable=${{ github.event_name == 'workflow_dispatch' }}
type=semver,pattern={{version}},value=${{ github.event.release.tag_name }},enable=${{ github.event_name == 'release' }}
type=semver,pattern={{major}}.{{minor}},value=${{ github.event.release.tag_name }},enable=${{ github.event_name == 'release' }}
type=raw,value=latest,enable=${{ github.event_name == 'release' && !github.event.release.prerelease }}

- name: Build and push multi-platform image
uses: docker/build-push-action@v7
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
19 changes: 19 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ jobs:
- run: python -m pip install -r requirements.txt
- run: python -m pytest -q
- run: python -m py_compile app/*.py verifier/*.py
- name: Run quickstart smoke test
if: matrix.python-version == '3.12'
run: python scripts/quickstart.py --output-dir .quickstart-ci

javascript-sdk:
runs-on: ubuntu-latest
Expand All @@ -42,3 +45,19 @@ jobs:
- uses: actions/checkout@v7
- name: Build LoopGrid image
run: docker build -t loopgrid-ci .
- name: Smoke test container startup
shell: bash
run: |
set -euo pipefail
docker run -d --name loopgrid-ci-run -p 8000:8000 loopgrid-ci
trap 'docker rm -f loopgrid-ci-run >/dev/null 2>&1 || true' EXIT
for attempt in {1..30}; do
if curl --fail --silent --show-error http://127.0.0.1:8000/health > /tmp/loopgrid-health.json; then
cat /tmp/loopgrid-health.json
exit 0
fi
sleep 1
done
echo 'LoopGrid container did not become healthy in time.' >&2
docker logs loopgrid-ci-run >&2 || true
exit 1
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ build/
*.egg-info/
node_modules/
.DS_Store
demo-output/
.quickstart-ci/
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,22 @@
# Unreleased — Developer onboarding and container distribution

## Added

- Cross-platform `scripts/quickstart.py` local evaluation path using isolated SQLite state and a temporary local Ed25519 signer.
- Quickstart exports `demo-output/evidence.zip` and independently verifies it against an out-of-band trusted public key.
- GitHub Container Registry publishing workflow for `ghcr.io/cybertechsoft/loopgrid`.
- Multi-platform container builds for `linux/amd64` and `linux/arm64`.
- Docker build context hardening via `.dockerignore` so local secrets, keys, databases and generated artifacts are not copied into images.
- CI now smoke-tests container startup and the `/health` endpoint after building the image.
- `edge` container tag for tested `main`; future releases publish semantic-version and `latest` tags.

## Compatibility

- No evidence schema, signing, verifier, API, Python SDK or JavaScript SDK contract changes.
- GitHub Marketplace verifier `v1.0.0` remains compatible.

---

# LoopGrid v0.8.1 — OTLP Interoperability

## Added / changed
Expand Down
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,39 @@ The append-only decision lifecycle includes `decision_created`, `model_completed
- Optional live OpenAI/Anthropic replay/provider validation helpers.
- Optional RFC3161 timestamp and AWS KMS adapter paths.

## Fast local evaluation

For a no-Docker, no-account evaluation path, install the repository dependencies once and run the cross-platform quickstart:

```bash
python -m pip install -r requirements.txt
python scripts/quickstart.py
```

The quickstart uses an isolated temporary SQLite database and temporary local Ed25519 signer. It creates a synthetic refund decision with policy evaluation, human approval, tool execution and observed outcome, exports a portable evidence bundle, and verifies that bundle with an out-of-band trusted public key. It does not modify an existing LoopGrid database or deployment.

Expected final output:

```text
[OK] Decision captured: dec_...
[OK] Evidence exported: .../demo-output/evidence.zip
[OK] Evidence coverage: 100%
[OK] VERIFIED
```

Artifacts are written to `demo-output/` and are synthetic evaluation data only.

## Container image

The repository publishes a multi-platform development image from `main` to GitHub Container Registry:

```bash
docker pull ghcr.io/cybertechsoft/loopgrid:edge
docker run --rm -p 8000:8000 -v loopgrid_demo_data:/app/data ghcr.io/cybertechsoft/loopgrid:edge
```

Then open `http://127.0.0.1:8000/`. The `edge` tag tracks tested `main`; versioned and `latest` container tags are published from future GitHub releases. The default container invocation above is for local evaluation, not the production design-partner posture described below.

## Quickest design-partner deployment

Requires Docker Desktop/Engine and Python 3.10+.
Expand Down
144 changes: 144 additions & 0 deletions scripts/quickstart.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
from __future__ import annotations

import argparse
import json
import os
import sys
import tempfile
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))


def _sqlite_url(path: Path) -> str:
return "sqlite:///" + path.resolve().as_posix()


def _prepare_local_environment(state_dir: Path) -> None:
"""Configure an isolated, local-only LoopGrid evaluation environment.

Environment variables are set before importing the LoopGrid application because
runtime settings and the signing provider are initialized at import time.
"""
state_dir.mkdir(parents=True, exist_ok=True)
os.environ["LOOPGRID_ENV"] = "development"
os.environ["LOOPGRID_REQUIRE_USER_AUTH"] = "false"
os.environ["LOOPGRID_DATABASE_URL"] = _sqlite_url(state_dir / "loopgrid.db")
os.environ["LOOPGRID_SIGNER_PROVIDER"] = "local_ed25519"
os.environ["LOOPGRID_SIGNING_KEY_PATH"] = str(state_dir / "signing-key.pem")
os.environ["LOOPGRID_PUBLIC_KEY_PATH"] = str(state_dir / "signing-public.pem")
os.environ["LOOPGRID_PAYLOAD_KEY_PATH"] = str(state_dir / "payload-key.bin")
os.environ["LOOPGRID_STRICT_PRODUCTION_SAFETY"] = "true"


def run(output_dir: Path) -> int:
output_dir = output_dir.resolve()
output_dir.mkdir(parents=True, exist_ok=True)

# Never reuse an existing evaluation database or signing key. The quickstart is
# intended to be deterministic from the user's point of view and isolated from
# any real/local LoopGrid deployment they may already have.
with tempfile.TemporaryDirectory(prefix="loopgrid-quickstart-") as tmp:
state_dir = Path(tmp)
_prepare_local_environment(state_dir)

try:
from fastapi.testclient import TestClient
from app.main import app
from verifier.loopgrid_verify import verify_bundle
except Exception as exc: # pragma: no cover - friendly CLI path
print("[FAIL] LoopGrid dependencies are not installed.")
print("Run: python -m pip install -r requirements.txt")
print(f"Detail: {type(exc).__name__}: {exc}")
return 1

bundle_path = output_dir / "evidence.zip"
trusted_key_path = output_dir / "trusted-public-key.pem"
summary_path = output_dir / "quickstart-result.json"

for path in (bundle_path, trusted_key_path, summary_path):
if path.exists():
path.unlink()

print("LoopGrid quickstart")
print("-------------------")
print("Creating an isolated local evaluation using SQLite and a temporary Ed25519 signer...")

try:
with TestClient(app) as client:
demo = client.post("/api/v1/demo/refund")
demo.raise_for_status()
payload = demo.json()
decision_id = payload["summary"]["decision_id"]

evidence = client.get(
f"/api/v1/decisions/{decision_id}/evidence",
params={"include_payloads": "false"},
)
evidence.raise_for_status()
bundle_path.write_bytes(evidence.content)

public_key = client.get("/api/v1/public-key.pem")
public_key.raise_for_status()
trusted_key_path.write_bytes(public_key.content)

result = verify_bundle(
str(bundle_path),
trusted_public_key=str(trusted_key_path),
)
except Exception as exc:
print(f"[FAIL] Quickstart failed: {type(exc).__name__}: {exc}")
return 1

verification_valid = bool(result.get("valid"))
failure_count = len(result.get("failures") or [])
warning_count = len(result.get("warnings") or [])
coverage_score = int((payload.get("coverage") or {}).get("score") or 0)

# Keep the machine-readable quickstart summary intentionally minimal.
# Verifier internals and key material/paths are not persisted or echoed here.
summary = {
"decision_id": decision_id,
"workspace_id": payload["summary"].get("workspace_id"),
"decision_type": payload["summary"].get("decision_type"),
"coverage_score": coverage_score,
"verification_status": "VERIFIED" if verification_valid else "INVALID",
"failure_count": failure_count,
"warning_count": warning_count,
"evidence_file": bundle_path.name,
}
summary_path.write_text(json.dumps(summary, indent=2), encoding="utf-8")
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed

print(f"[OK] Decision captured: {decision_id}")
print(f"[OK] Evidence exported: {bundle_path}")
print(f"[OK] Trusted public key exported: {trusted_key_path}")
print(f"[OK] Evidence coverage: {coverage_score}%")
if verification_valid:
print("[OK] VERIFIED")
print(" The exported bundle verified with an out-of-band trusted public key.")
print(f" Details: {summary_path}")
return 0

print("[FAIL] INVALID")
print(f" Verifier reported {failure_count} failure(s) and {warning_count} warning(s).")
print(f" Details: {summary_path}")
return 2


def main() -> None:
parser = argparse.ArgumentParser(
description="Create and independently verify a synthetic LoopGrid evidence bundle locally."
)
parser.add_argument(
"--output-dir",
default="demo-output",
help="Directory for evidence.zip, trusted-public-key.pem, and result JSON (default: demo-output)",
)
args = parser.parse_args()
raise SystemExit(run(Path(args.output_dir)))


if __name__ == "__main__":
main()
Loading