From de89abe04e6d0de462e9b34d84b55e7e765a1c9c Mon Sep 17 00:00:00 2001 From: Cybertech <131432994+cybertechsoft@users.noreply.github.com> Date: Sat, 19 Sep 2026 16:58:44 +0530 Subject: [PATCH 1/6] Add files via upload Add LoopGrid onboarding quickstart --- CHANGELOG.md | 19 ++++++ README.md | 33 ++++++++++ scripts/quickstart.py | 138 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 190 insertions(+) create mode 100644 scripts/quickstart.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 6adc116..58fb08a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 7cd4fa5..dd5c58f 100644 --- a/README.md +++ b/README.md @@ -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+. diff --git a/scripts/quickstart.py b/scripts/quickstart.py new file mode 100644 index 0000000..2ea9c78 --- /dev/null +++ b/scripts/quickstart.py @@ -0,0 +1,138 @@ +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 + + summary = { + "decision_id": decision_id, + "workspace_id": payload["summary"].get("workspace_id"), + "decision_type": payload["summary"].get("decision_type"), + "lifecycle": payload["summary"].get("lifecycle"), + "coverage": payload.get("coverage"), + "verification": result, + "artifacts": { + "evidence": str(bundle_path), + "trusted_public_key": str(trusted_key_path), + }, + } + summary_path.write_text(json.dumps(summary, indent=2), encoding="utf-8") + + 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: {payload['coverage']['score']}%") + if result.get("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(json.dumps(result, indent=2)) + 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() From 6bf0eaec0a60060d00fbd34cbc1afacfefb7f5c6 Mon Sep 17 00:00:00 2001 From: Cybertech <131432994+cybertechsoft@users.noreply.github.com> Date: Sat, 19 Sep 2026 17:03:06 +0530 Subject: [PATCH 2/6] Create .dockerignore Add Docker build exclusions --- .dockerignore | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..2f5398f --- /dev/null +++ b/.dockerignore @@ -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 From 276ab1c67450d9074009edb0978e90c937a5cdba Mon Sep 17 00:00:00 2001 From: Cybertech <131432994+cybertechsoft@users.noreply.github.com> Date: Sat, 19 Sep 2026 17:04:27 +0530 Subject: [PATCH 3/6] Update tests.yml Extend CI with quickstart and container health test --- .github/workflows/tests.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d3db0e8..95cc2d6 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -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 @@ -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 From 0a1f558083046ad3ed59057413cf8193bfa4e1b5 Mon Sep 17 00:00:00 2001 From: Cybertech <131432994+cybertechsoft@users.noreply.github.com> Date: Sat, 19 Sep 2026 17:05:25 +0530 Subject: [PATCH 4/6] Create publish-container.yml Add GHCR container publishing workflow --- .github/workflows/publish-container.yml | 77 +++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 .github/workflows/publish-container.yml diff --git a/.github/workflows/publish-container.yml b/.github/workflows/publish-container.yml new file mode 100644 index 0000000..b442b0d --- /dev/null +++ b/.github/workflows/publish-container.yml @@ -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 From 0549510ee554c8e43d9acfcae34be3f40293f77d Mon Sep 17 00:00:00 2001 From: Cybertech <131432994+cybertechsoft@users.noreply.github.com> Date: Sat, 19 Sep 2026 17:14:27 +0530 Subject: [PATCH 5/6] Update .gitignore Update gitignore for onboarding artifacts --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 2a2e517..20fe8e3 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,5 @@ build/ *.egg-info/ node_modules/ .DS_Store +demo-output/ +.quickstart-ci/ From 714352364254983b179d6aa38ce85d7b83d423a3 Mon Sep 17 00:00:00 2001 From: Cybertech <131432994+cybertechsoft@users.noreply.github.com> Date: Sat, 19 Sep 2026 17:29:10 +0530 Subject: [PATCH 6/6] Update quickstart.py Harden quickstart result handling --- scripts/quickstart.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/scripts/quickstart.py b/scripts/quickstart.py index 2ea9c78..bffafe5 100644 --- a/scripts/quickstart.py +++ b/scripts/quickstart.py @@ -92,32 +92,38 @@ def run(output_dir: Path) -> int: 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"), - "lifecycle": payload["summary"].get("lifecycle"), - "coverage": payload.get("coverage"), - "verification": result, - "artifacts": { - "evidence": str(bundle_path), - "trusted_public_key": str(trusted_key_path), - }, + "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") 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: {payload['coverage']['score']}%") - if result.get("valid"): + 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(json.dumps(result, indent=2)) + print(f" Verifier reported {failure_count} failure(s) and {warning_count} warning(s).") + print(f" Details: {summary_path}") return 2