From 0fd6df9c9db9e51a4559f0f466ae169a532c3def Mon Sep 17 00:00:00 2001 From: hudsonaikins-crown Date: Fri, 3 Jul 2026 14:22:55 -0700 Subject: [PATCH] Harden live queue runner smoke path --- README.md | 2 +- dashboard/.env.example | 4 + dashboard/package.json | 2 + .../proof-pack-viewer/FindingQueueTable.tsx | 11 +- .../proof-pack-viewer/QueueStatusPanels.tsx | 35 ++++-- dashboard/tests/e2e/proof-pack-viewer.e2e.ts | 3 + .../docs/cloudflare-neon-deployment.mdx | 2 +- docs/post-deploy-live-smoke.md | 101 ++++++++++++++++++ src/agent_permit/cli.py | 32 ++++++ tests/unit/test_cli.py | 38 +++++++ 10 files changed, 218 insertions(+), 12 deletions(-) create mode 100644 docs/post-deploy-live-smoke.md diff --git a/README.md b/README.md index 1d1c420..4b8d98b 100644 --- a/README.md +++ b/README.md @@ -185,7 +185,7 @@ and runs the scanner: ```bash set -a; source .env; set +a -uv run --extra db --extra deep-agent agent-permit runner --once --deep-agent auto +uv run --extra db --extra deep-agent agent-permit runner --once --deep-agent auto --agent-recursion-limit 20 ``` Run the documentation site: diff --git a/dashboard/.env.example b/dashboard/.env.example index a1a6e38..96fcc8f 100644 --- a/dashboard/.env.example +++ b/dashboard/.env.example @@ -1 +1,5 @@ +# Local Worker dev: VITE_AGENT_PERMIT_API_URL=http://127.0.0.1:8787/api + +# Production deploy uses: +# VITE_AGENT_PERMIT_API_URL=https://agent-permit-worker.hudson-228.workers.dev/api diff --git a/dashboard/package.json b/dashboard/package.json index ab4cb56..f3a765c 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -6,6 +6,8 @@ "scripts": { "dev": "vite", "build": "tsc -b && vite build", + "build:prod": "VITE_AGENT_PERMIT_API_URL=https://agent-permit-worker.hudson-228.workers.dev/api bun run build", + "deploy:prod": "bun run build:prod && bun x wrangler pages deploy dist --project-name agent-permit-dashboard --branch main", "lint": "eslint .", "preview": "vite preview", "test:e2e": "playwright test", diff --git a/dashboard/src/components/proof-pack-viewer/FindingQueueTable.tsx b/dashboard/src/components/proof-pack-viewer/FindingQueueTable.tsx index 8b8cb72..e40701e 100644 --- a/dashboard/src/components/proof-pack-viewer/FindingQueueTable.tsx +++ b/dashboard/src/components/proof-pack-viewer/FindingQueueTable.tsx @@ -90,6 +90,7 @@ export function FindingQueueTable({ /> void @@ -141,8 +144,12 @@ function QueueOptionalPanels({ /> ) : null} - {recentJob || jobEvents.length > 0 ? ( - + {recentJob || activeJobs.length > 0 || jobEvents.length > 0 ? ( + ) : null} ) diff --git a/dashboard/src/components/proof-pack-viewer/QueueStatusPanels.tsx b/dashboard/src/components/proof-pack-viewer/QueueStatusPanels.tsx index 5298566..a3c09cf 100644 --- a/dashboard/src/components/proof-pack-viewer/QueueStatusPanels.tsx +++ b/dashboard/src/components/proof-pack-viewer/QueueStatusPanels.tsx @@ -9,7 +9,7 @@ import { Input } from "@/components/ui/input" import type { ApiStatus, QueueScanInput, RunEvent, ScanJob } from "@/data/liveApi" const RUNNER_COMMAND = - "set -a; source .env; set +a; uv run --extra db --extra deep-agent agent-permit runner --once --deep-agent auto" + "set -a; source .env; set +a; uv run --extra db --extra deep-agent agent-permit runner --once --deep-agent auto --agent-recursion-limit 20" export function LiveStatusStrip({ apiStatus, @@ -194,33 +194,36 @@ function RecentJobNotice({ recentJob }: { recentJob: ScanJob | null }) { return (
- Job queued for {recentJob.repositoryLabel}. Run the local runner command above - to start the scan. + {jobStatusMessage(recentJob)}
) } export function QueueProgressPanel({ + activeJobs = [], events, job, }: { + activeJobs?: ScanJob[] events: RunEvent[] job: ScanJob | null }) { + const visibleJob = job ?? activeJobs[0] ?? null + return (
-

Latest queued scan

+

Scan handoff

- {job - ? `${job.repositoryLabel} is ${job.status}.` + {visibleJob + ? jobStatusMessage(visibleJob) : "Waiting for runner events."}

- {job ? ( + {visibleJob ? ( - {job.id} + {visibleJob.id} ) : null}
@@ -250,6 +253,22 @@ export function QueueProgressPanel({ ) } +function jobStatusMessage(job: ScanJob) { + if (job.status === "queued") { + return `${job.repositoryLabel} is queued. Start the local runner to clone and scan it.` + } + if (job.status === "running") { + return `${job.repositoryLabel} is running. The local runner is scanning and writing artifacts.` + } + if (job.status === "completed") { + return `${job.repositoryLabel} completed. Refreshing findings from the Worker API.` + } + if (job.status === "failed") { + return `${job.repositoryLabel} failed. ${job.error ?? "Review runner logs."}` + } + return `${job.repositoryLabel} status: ${job.status}.` +} + function liveStatusLabel(apiStatus: ApiStatus) { const labels: Record = { error: "Worker API unavailable", diff --git a/dashboard/tests/e2e/proof-pack-viewer.e2e.ts b/dashboard/tests/e2e/proof-pack-viewer.e2e.ts index 1e3bd56..02f231d 100644 --- a/dashboard/tests/e2e/proof-pack-viewer.e2e.ts +++ b/dashboard/tests/e2e/proof-pack-viewer.e2e.ts @@ -13,6 +13,9 @@ test("queue screen supports scan form, search, drilldown, back, and theme cycle" await expect(page.getByTestId("runner-command")).toContainText( "agent-permit runner --once", ) + await expect(page.getByTestId("runner-command")).toContainText( + "--agent-recursion-limit 20", + ) await expect(page.getByTestId("queue-scan-submit")).toBeDisabled() await page .getByTestId("queue-scan-path") diff --git a/docs-site/content/docs/cloudflare-neon-deployment.mdx b/docs-site/content/docs/cloudflare-neon-deployment.mdx index 3b54646..5b44aad 100644 --- a/docs-site/content/docs/cloudflare-neon-deployment.mdx +++ b/docs-site/content/docs/cloudflare-neon-deployment.mdx @@ -41,7 +41,7 @@ Process a dashboard-queued repository scan from the local repo root: ```bash set -a; source .env; set +a -uv run --extra db --extra deep-agent agent-permit runner --once --deep-agent auto +uv run --extra db --extra deep-agent agent-permit runner --once --deep-agent auto --agent-recursion-limit 20 ``` The Worker only creates the queue record. The dashboard should queue GitHub repository URLs first. The local runner claims the job from Neon, clones GitHub URLs into `.agent-permit/runner-worktrees`, runs the scanner, writes artifacts, and updates the shared database. Absolute local paths still work for advanced local scans. diff --git a/docs/post-deploy-live-smoke.md b/docs/post-deploy-live-smoke.md new file mode 100644 index 0000000..2a30a52 --- /dev/null +++ b/docs/post-deploy-live-smoke.md @@ -0,0 +1,101 @@ +# Post-Deploy Live Smoke + +Date: 2026-07-03 + +Purpose: prove the deployed dashboard, Worker API, Neon database, local CLI runner, deterministic scanner, and Deep Agent handoff are wired together. + +## Deployed surfaces + +| Surface | URL | +| --- | --- | +| Worker API | `https://agent-permit-worker.hudson-228.workers.dev` | +| Dashboard | `https://agent-permit-dashboard.pages.dev` | +| Docs | `https://agent-permit-docs.hudson-228.workers.dev/docs` | + +## Smoke path + +1. Open the dashboard. +2. Click `Queue scan`. +3. Paste a GitHub repository URL. +4. Submit the job. +5. Run the local runner from the repo root: + +```bash +set -a; source .env; set +a +uv run --extra db --extra deep-agent agent-permit runner --once --deep-agent auto --agent-recursion-limit 20 +``` + +6. Refresh the dashboard and verify the finding count, queue status, and drilldown content changed. + +## Expected proof + +The Worker should show: + +- one new job record +- one matching scan run +- repository source equal to the queued GitHub URL +- findings written by deterministic scanners +- Deep Agent usage attached when `OPENROUTER_API_KEY` is configured + +The runner should show: + +- `Status: runner_job_complete` +- `Deep Agent: completed (...)` when live model review succeeds +- local artifacts under `.agent-permit/runner-worktrees//.agent-permit/runs//` + +## Release gate + +Run before a release tag: + +```bash +uv run pytest -q +python3 tools/release_check.py +cd dashboard && bun run lint && bun run build && bun run test:e2e +cd ../docs-site && bun run build +cd ../worker && bun test +``` + +Do not tag a release if the live smoke job is failed, the dashboard still points to localhost, or proof pack generation reports missing required artifacts. + +## Verified smoke on 2026-07-03 + +Target: + +```text +https://github.com/github/github-mcp-server +``` + +Queued job: + +```text +job_46a7dfa7-875b-4c64-bc05-554bc30a3ccd +``` + +Runner result: + +```text +Status: runner_job_complete +Deep Agent: completed (openrouter:anthropic/claude-sonnet-4.6) +``` + +Worker snapshot after completion: + +| Metric | Value | +| --- | ---: | +| repositories | 9 | +| runs | 9 | +| findings | 46 | +| queued jobs | 0 | +| latest run permit status | `needs_review` | +| latest run findings | 20 | +| latest run graph paths | 9 | +| latest run controls | 29 | +| files indexed | 456 | +| model calls | 4 | +| input tokens | 55,073 | +| output tokens | 2,554 | +| total tokens | 57,627 | +| cached tokens | 34,330 | +| cache hit ratio | 0.6234 | + +The deployed path is proven for GitHub URL queueing, local clone execution, deterministic scanner output, Deep Agent report generation, model usage ingestion, and dashboard-readable Worker state. diff --git a/src/agent_permit/cli.py b/src/agent_permit/cli.py index 55983c0..453b80e 100644 --- a/src/agent_permit/cli.py +++ b/src/agent_permit/cli.py @@ -9,8 +9,10 @@ import os from pathlib import Path import re +import shutil import subprocess import sys +import time from typing import Any, TextIO from urllib.parse import urlparse @@ -510,6 +512,15 @@ def build_parser() -> argparse.ArgumentParser: f"{DEFAULT_DEEP_AGENT_RECURSION_LIMIT}" ), ) + runner_parser.add_argument( + "--clone-retention-days", + type=int, + default=int(os.getenv("AGENT_PERMIT_RUNNER_CLONE_RETENTION_DAYS", "7")), + help=( + "remove runner GitHub clone worktrees older than this many days; " + "set 0 to disable cleanup; default 7" + ), + ) runner_parser.add_argument( "--phoenix", action="store_true", @@ -785,6 +796,7 @@ def main( deep_agent=args.deep_agent, model=args.model, agent_recursion_limit=args.agent_recursion_limit, + clone_retention_days=args.clone_retention_days, enable_phoenix=args.phoenix, enable_langsmith=args.langsmith, stdout=stdout, @@ -1679,6 +1691,20 @@ def _runner_clone_root() -> Path: return root +def _cleanup_stale_runner_clones(retention_days: int) -> None: + if retention_days <= 0: + return + clone_root = _runner_clone_root() + if not clone_root.exists(): + return + cutoff = time.time() - (retention_days * 24 * 60 * 60) + for child in clone_root.iterdir(): + if not child.is_dir(): + continue + if child.stat().st_mtime < cutoff: + shutil.rmtree(child) + + def _github_clone_slug(source: str, *, job_id: str) -> str: parsed = urlparse(source) parts = parsed.path.strip("/").removesuffix(".git").split("/") @@ -1698,6 +1724,7 @@ def run_runner( deep_agent: str = "auto", model: str | None = None, agent_recursion_limit: int = DEFAULT_DEEP_AGENT_RECURSION_LIMIT, + clone_retention_days: int = 7, enable_phoenix: bool = False, enable_langsmith: bool = False, stdout: TextIO, @@ -1709,6 +1736,11 @@ def run_runner( if deep_agent not in {"auto", "required", "off"}: print("error: --deep-agent must be auto, required, or off", file=stderr) return 2 + try: + _cleanup_stale_runner_clones(clone_retention_days) + except OSError as exc: + print(f"error: failed to clean runner clone worktrees: {exc}", file=stderr) + return 1 try: store = store_from_env() claimed = store.claim_next_scan_job() diff --git a/tests/unit/test_cli.py b/tests/unit/test_cli.py index f30d6bd..869e1fb 100644 --- a/tests/unit/test_cli.py +++ b/tests/unit/test_cli.py @@ -1,7 +1,9 @@ from datetime import datetime, timezone from io import StringIO import json +import os from pathlib import Path +import time import agent_permit.cli as cli from agent_permit import __version__ @@ -159,6 +161,40 @@ def fake_clone(command, *, capture_output, check, text): assert "Target: https://github.com/github/github-mcp-server" in stdout.getvalue() +def test_runner_cleans_stale_github_clone_worktrees( + tmp_path, + monkeypatch, +) -> None: + stdout = StringIO() + stderr = StringIO() + monkeypatch.chdir(tmp_path) + monkeypatch.delenv("OPENROUTER_API_KEY", raising=False) + clone_root = tmp_path / ".agent-permit" / "runner-worktrees" + stale_clone = clone_root / "old__repo__job_old" + fresh_clone = clone_root / "fresh__repo__job_fresh" + stale_clone.mkdir(parents=True) + fresh_clone.mkdir(parents=True) + stale_time = time.time() - (8 * 24 * 60 * 60) + os.utime(stale_clone, (stale_time, stale_time)) + + class FakeStore: + def claim_next_scan_job(self): + return None + + monkeypatch.setattr(cli, "store_from_env", lambda: FakeStore()) + + exit_code = main( + ["runner", "--once", "--clone-retention-days", "7"], + stdout=stdout, + stderr=stderr, + ) + + assert exit_code == 0 + assert stderr.getvalue() == "" + assert not stale_clone.exists() + assert fresh_clone.is_dir() + + def test_runner_required_deep_agent_marks_job_and_run_failed( tmp_path, monkeypatch, @@ -380,6 +416,7 @@ def write_ingest_records(self, records): def fake_run_investigate(artifact_dir, **kwargs) -> int: captured["artifact_dir"] = artifact_dir captured["model"] = kwargs["model"] + captured["agent_recursion_limit"] = kwargs["agent_recursion_limit"] context = EvidenceContext.load(artifact_dir) (artifact_dir / "agent-investigation.md").write_text( build_investigation_markdown(context), @@ -423,6 +460,7 @@ def fake_run_investigate(artifact_dir, **kwargs) -> int: assert completed == ["job_test_runner_deep"] assert failed == [] assert captured["model"] == "openrouter:test/model" + assert captured["agent_recursion_limit"] == 12 assert len(ingested) == 1 assert ingested[0].model_usage is not None assert ingested[0].model_usage.model == "openrouter:test/model"