diff --git a/.github/workflows/product-validation.yml b/.github/workflows/product-validation.yml new file mode 100644 index 0000000..5d06fb2 --- /dev/null +++ b/.github/workflows/product-validation.yml @@ -0,0 +1,61 @@ +name: product-validation + +on: + pull_request: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +jobs: + validate: + runs-on: ubuntu-latest + timeout-minutes: 30 + env: + TABELLIO_ARTIFACT_BASE_URI: artifact+github://IntelIP/agent-permit-office/actions/runs/${{ github.run_id }}/tabellio-validation-${{ github.event.pull_request.head.sha || github.sha }}/.artifacts/tabellio/visual-captures/ + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + persist-credentials: false + ref: ${{ github.event.pull_request.head.sha || github.sha }} + - uses: actions/setup-node@v6 + with: {node-version: 22} + - uses: oven-sh/setup-bun@v2 + with: {bun-version: "1.3.3"} + - name: Checkout pinned Tabellio runner + uses: actions/checkout@v6 + with: + repository: IntelIP/Tabellio + ref: c4789837ee5ad779198141d6ffd2fea77792fbfb + path: .tabellio-runner + persist-credentials: false + - name: Isolate validator from candidate scan + run: mv .tabellio-runner "$RUNNER_TEMP/tabellio-runner" + - uses: actions/setup-python@v6 + with: {python-version: "3.12"} + - uses: astral-sh/setup-uv@v6 + with: {enable-cache: true} + - name: Install Chromium runtime + run: bun x playwright@1.61.1 install --with-deps chromium + - name: Validate exact candidate + run: | + set -o pipefail + install -m 755 "$RUNNER_TEMP/tabellio-runner/scripts/tabellio-validator.mjs" "$RUNNER_TEMP/tabellio-validator" + ln -s "$RUNNER_TEMP/tabellio-runner/scripts/finalize-tabellio-visual-captures.mjs" "$RUNNER_TEMP/tabellio-design-captures" + PATH="$RUNNER_TEMP:$PATH" node "$RUNNER_TEMP/tabellio-runner/scripts/tabellio-validate.mjs" gate \ + --repo . --repo-id IntelIP/agent-permit-office \ + --base origin/main --commit HEAD --manifest tabellio.validation.json | tee tabellio-validation-result.json + - name: Upload validation result + if: always() + uses: actions/upload-artifact@v4 + with: + name: tabellio-validation-${{ github.event.pull_request.head.sha || github.sha }} + path: | + tabellio-validation-result.json + refs/tabellio/validations/ + .artifacts/tabellio/ + include-hidden-files: true + if-no-files-found: warn diff --git a/.gitignore b/.gitignore index 939d9a0..cda684b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .DS_Store .agent-permit/ +.artifacts/ .codex/ .coverage .entire/ diff --git a/.tabellio/validators.json b/.tabellio/validators.json new file mode 100644 index 0000000..982a097 --- /dev/null +++ b/.tabellio/validators.json @@ -0,0 +1,55 @@ +{ + "schemaVersion": "tabellio-adapter/v0.1", + "profiles": { + "static": { + "commands": [ + ["uv", "sync", "--frozen", "--all-extras", "--dev"], + ["bun", "install", "--frozen-lockfile", "--cwd", "dashboard"], + ["bun", "run", "--cwd", "dashboard", "playwright", "install", "chromium"], + ["bun", "install", "--frozen-lockfile", "--cwd", "docs-site"], + ["python3", "tools/release_check.py"] + ], + "metrics": [], + "cost": {"telemetry": "not_applicable", "usd": null, "modelCalls": null, "toolCalls": null}, + "summary": "Locked environment and release packaging checks" + }, + "schema": { + "commands": [["uv", "run", "--frozen", "pytest", "-q", "tests/unit/test_artifacts.py", "tests/unit/test_models.py", "tests/unit/test_sarif.py", "tests/unit/test_rule_registry.py"]], + "metrics": [{"name": "scanner_artifact_contracts_pass", "unit": "boolean", "passValue": 1, "failValue": 0}], + "cost": {"telemetry": "not_applicable", "usd": null, "modelCalls": null, "toolCalls": null}, + "summary": "Deterministic scanner artifact and SARIF schemas" + }, + "semantic": { + "commands": [["uv", "run", "--frozen", "pytest", "-q", "tests/unit/test_baseline.py", "tests/unit/test_evals.py", "tests/unit/test_permit_engine.py", "tests/unit/test_policy.py"]], + "metrics": [{"name": "permit_policy_oracles_pass", "unit": "boolean", "passValue": 1, "failValue": 0}], + "cost": {"telemetry": "not_applicable", "usd": null, "modelCalls": null, "toolCalls": null}, + "summary": "Permit decisions, policy boundaries, and evaluation oracles" + }, + "workflow": { + "commands": [["uv", "run", "--frozen", "pytest", "-q", "tests/unit/test_cli.py", "tests/unit/test_cli_ci_mode.py", "tests/unit/test_demo.py", "tests/unit/test_fixtures.py", "tests/unit/test_github_action_packaging.py"]], + "metrics": [{"name": "cli_ci_proofpack_workflows_pass", "unit": "boolean", "passValue": 1, "failValue": 0}], + "cost": {"telemetry": "not_applicable", "usd": null, "modelCalls": null, "toolCalls": null}, + "summary": "CLI, CI mode, fixtures, and packaged action workflows" + }, + "visual": { + "commands": [["bun", "run", "--cwd", "dashboard", "build"], ["bun", "run", "--cwd", "dashboard", "design:capture"], ["node", "scripts/design/finalize-captures.mjs"]], + "metrics": [{"name": "visual_capture_count", "unit": "count", "pattern": "Visual capture count: ([0-9]+)"}], + "cost": {"telemetry": "not_applicable", "usd": null, "modelCalls": null, "toolCalls": null}, + "summary": "Deterministic PermitGraph dashboard matrix with font and overflow checks" + }, + "operational": { + "commands": [["uv", "run", "--frozen", "pytest", "-q", "tests/unit/test_model_provider.py", "tests/unit/test_db.py"]], + "metrics": [ + {"name": "local_provider_and_storage_boundaries_pass", "unit": "boolean", "passValue": 1, "failValue": 0} + ], + "cost": {"telemetry": "available", "usd": 0, "modelCalls": 0, "toolCalls": 0}, + "summary": "Local provider and storage paths with no live model calls" + }, + "security": { + "commands": [["uv", "run", "--frozen", "pytest", "-q", "tests/unit/test_capability_graph.py", "tests/unit/test_ci_workflows.py", "tests/unit/test_credential_refs.py", "tests/unit/test_mcp_config.py", "tests/unit/test_path_finder.py", "tests/unit/test_prompt_instructions.py"]], + "metrics": [{"name": "agent_attack_path_controls_pass", "unit": "boolean", "passValue": 1, "failValue": 0}], + "cost": {"telemetry": "not_applicable", "usd": null, "modelCalls": null, "toolCalls": null}, + "summary": "Credential, MCP, workflow, prompt, and attack-path controls" + } + } +} diff --git a/AGENTS.md b/AGENTS.md index 5eb3071..056083f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -60,3 +60,11 @@ For docs-only work: cd docs-site bun run build ``` + +## Product validation gate + +- Unit tests are structural evidence only. Before review or merge readiness, run the committed `tabellio.validation.json` against the exact candidate commit with `tabellio-validate gate`. +- Schema, semantic, workflow, operational, and security evidence must pass. `blocked` is not `passed`; a new commit invalidates earlier evidence. +- Preserve PermitGraph's boundary: validators may scan committed fixtures and this repository, but never execute target repository code, MCP servers, target workflows, or paid model providers. +- Keep validation external cost at zero. Upload generated evidence from CI; do not commit it. +- Track rollout and failures in Plane item `APO-120`. diff --git a/dashboard/package.json b/dashboard/package.json index f3a765c..6f787e8 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -11,6 +11,7 @@ "lint": "eslint .", "preview": "vite preview", "test:e2e": "playwright test", + "design:capture": "playwright test --config playwright.design.config.ts", "test:unit": "vitest run tests/unit" }, "dependencies": { diff --git a/dashboard/playwright.design.config.ts b/dashboard/playwright.design.config.ts new file mode 100644 index 0000000..6cbcf49 --- /dev/null +++ b/dashboard/playwright.design.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from "@playwright/test"; + +import baseConfig from "./playwright.config"; + +export default defineConfig({ + ...baseConfig, + reporter: [["list"]], + testMatch: /design-memory\.capture\.ts/, + use: { + ...baseConfig.use, + screenshot: "off", + trace: "off", + }, + webServer: { + command: "VITE_AGENT_PERMIT_API_URL=/api bun run build && bun run preview -- --host 127.0.0.1 --port 5173", + reuseExistingServer: false, + url: "http://127.0.0.1:5173", + }, +}); diff --git a/dashboard/tests/e2e/design-memory.capture.ts b/dashboard/tests/e2e/design-memory.capture.ts new file mode 100644 index 0000000..1e11889 --- /dev/null +++ b/dashboard/tests/e2e/design-memory.capture.ts @@ -0,0 +1,122 @@ +import { mkdirSync, readFileSync } from "node:fs"; +import { resolve } from "node:path"; + +import { expect, test } from "@playwright/test"; + +type Profile = { + policy: { + surfaces: Array<{ id: string; kind: "route" | "component"; target: string; viewports: string[]; themes: string[]; states: string[] }>; + viewports: Array<{ id: string; width: number; height: number }>; + }; +}; + +const dashboardRoot = process.cwd(); +const repoRoot = resolve(dashboardRoot, ".."); +const profile = JSON.parse(readFileSync(resolve(repoRoot, "design/product.design.json"), "utf8")) as Profile; +type DashboardFixtureSource = { + generatedAt: string; + findings: Array>; + repos: Array>; + runs: Array>; +}; + +const dashboardSnapshot = JSON.parse( + readFileSync(resolve(dashboardRoot, "src/data/generated/dashboardSnapshot.json"), "utf8"), +) as DashboardFixtureSource; +const apiSnapshot = JSON.stringify({ + generatedAt: dashboardSnapshot.generatedAt, + repositories: dashboardSnapshot.repos.map((repo) => ({ + id: repo.id, + label: repo.label, + local_path: repo.source, + })), + runs: dashboardSnapshot.runs.map((run) => { + const repo = dashboardSnapshot.repos.find((candidate) => candidate.id === run.repoId); + const metrics = (run.metrics ?? {}) as Record; + const artifacts = Array.isArray(run.artifacts) ? run.artifacts : []; + return { + artifact_dir: artifacts[0] ?? "", + branch: "fixture", + cache_hit_ratio: metrics.cacheHitRatio ?? 0, + cached_tokens: metrics.cachedTokens ?? 0, + completed_at: run.completedAt, + controls_count: metrics.controls ?? 0, + findings_count: metrics.findings ?? 0, + graph_paths_count: metrics.graphPaths ?? 0, + local_path: repo?.source ?? "fixture", + model: null, + model_calls: metrics.modelCalls ?? 0, + permit_status: run.status, + repository_id: run.repoId, + repository_label: repo?.label ?? run.repoId, + run_id: run.id, + status: "completed", + total_tokens: metrics.totalTokens ?? 0, + }; + }), + findings: dashboardSnapshot.findings.map((finding) => { + const metrics = (finding.metrics ?? {}) as Record; + return { + cache_hit_ratio: metrics.cacheHitRatio ?? 0, + cached_tokens: metrics.cachedTokens ?? 0, + controls_count: metrics.controls ?? 0, + finding_id: finding.id, + findings_count: metrics.findings ?? 1, + graph_paths_count: metrics.graphPaths ?? 0, + local_path: finding.source, + model_calls: metrics.modelCalls ?? 0, + permit_status: finding.status, + recommendation: finding.remediation, + repository_label: finding.repo, + risk: finding.summary, + rule_id: finding.rule, + run_id: finding.runId, + severity: finding.severity, + title: finding.title, + total_tokens: metrics.totalTokens ?? 0, + }; + }), +}); +const output = resolve(repoRoot, ".artifacts/tabellio/visual-captures"); +mkdirSync(output, { recursive: true }); + +test.describe("Tabellio visual capture matrix", () => { + test.describe.configure({ mode: "serial" }); + + for (const surface of profile.policy.surfaces) { + if (surface.kind !== "route") throw new Error(`Capture adapter missing for component surface: ${surface.id}`); + for (const viewportId of surface.viewports) for (const theme of surface.themes) for (const state of surface.states) { + if (state !== "default") throw new Error(`Capture adapter missing for state: ${surface.id}/${state}`); + const viewport = profile.policy.viewports.find((entry) => entry.id === viewportId); + if (!viewport) throw new Error(`Unknown viewport: ${viewportId}`); + const id = `${surface.id}--${viewportId}--${theme}--${state}`; + + test(id, async ({ page }) => { + let fixtureRequests = 0; + await page.setViewportSize({ width: viewport.width, height: viewport.height }); + await page.emulateMedia({ colorScheme: theme as "light" | "dark", reducedMotion: "reduce" }); + await page.route("**/api/snapshot", async (route) => { + fixtureRequests += 1; + await route.fulfill({ body: apiSnapshot, contentType: "application/json", status: 200 }); + }); + await page.goto(surface.target, { waitUntil: "domcontentloaded" }); + await page.evaluate((selectedTheme) => { + document.documentElement.classList.remove("light", "dark"); + document.documentElement.classList.add(selectedTheme); + document.documentElement.style.colorScheme = selectedTheme; + }, theme); + await page.addStyleTag({ content: "*,*::before,*::after{animation:none!important;transition:none!important;caret-color:transparent!important}" }); + await page.evaluate(() => document.fonts.ready); + + await expect(page.getByRole("heading", { name: "Repository findings" })).toBeVisible(); + await expect(page.getByText("Live Worker data", { exact: true })).toBeVisible(); + expect(fixtureRequests).toBeGreaterThan(0); + await expect(page.getByText("unknown policy", { exact: true })).toHaveCount(0); + await expect(page.locator("html")).toHaveClass(new RegExp(`(?:^|\\s)${theme}(?:\\s|$)`)); + expect(await page.evaluate(() => getComputedStyle(document.body).fontFamily)).toContain("DM Sans"); + expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth + 1)).toBe(true); + await page.screenshot({ path: resolve(output, `${id}.png`), animations: "disabled" }); + }); + } + } +}); diff --git a/design/baselines/manifest.json b/design/baselines/manifest.json new file mode 100644 index 0000000..0f53ed5 --- /dev/null +++ b/design/baselines/manifest.json @@ -0,0 +1,11 @@ +{ + "schemaVersion": "tabellio-visual-baselines/v0.1", + "status": "proposed", + "productId": "permitgraph/dashboard", + "profilePath": "design/product.design.json", + "profileDigest": "03ef2f28268a93862651ebd0a8ae8ac2fd24b350619e968cc9a43c56e5189e7a", + "sourceCommit": null, + "approvedAt": null, + "approvedBy": null, + "captures": [] +} diff --git a/design/decisions/0001-design-memory-bootstrap.md b/design/decisions/0001-design-memory-bootstrap.md new file mode 100644 index 0000000..15a8dbe --- /dev/null +++ b/design/decisions/0001-design-memory-bootstrap.md @@ -0,0 +1,5 @@ +# Design memory bootstrap + +PermitGraph keeps its current visual authority in `dashboard/src/index.css` and shadcn configuration in `dashboard/components.json`. The portable product profile references those sources and preserves the separation between deterministic permit evidence and model explanation. + +Initial screenshot baselines remain proposed until dashboard routes and permit states are captured from an exact committed head and approved by a human. diff --git a/design/product.design.json b/design/product.design.json new file mode 100644 index 0000000..aa24bbd --- /dev/null +++ b/design/product.design.json @@ -0,0 +1,63 @@ +{ + "schemaVersion": "tabellio-product-design/v0.1", + "productId": "permitgraph/dashboard", + "title": "PermitGraph Dashboard", + "audience": ["Security engineers", "Platform operators", "AI governance teams"], + "platforms": ["web", "docs"], + "visualThesis": "Calm security-control software that makes permit state, evidence, risk paths, and remediation decisions immediately legible without alarmist decoration.", + "sources": { + "tokens": [ + {"path": "dashboard/src/index.css", "digest": "25661b152f617fdc477e91eba08c0eb6b969e70c5a168b47db697ec74ee25581"} + ], + "components": [ + {"path": "dashboard/components.json", "digest": "49101704a7f660f485a362e1d31d0f123877c34b58bc0cdc7796e99a7fe4438a"} + ], + "decisions": ["AGENTS.md", "design/decisions/0001-design-memory-bootstrap.md"] + }, + "referenceLock": { + "primary": "Existing PermitGraph dashboard tokens and deterministic evidence-review workflow", + "preserve": [ + "DM Sans typography", + "Muted teal primary actions", + "Distinct approved, review, blocked, critical, agent, and artifact semantics", + "Evidence-first information hierarchy", + "True black high-contrast dark surfaces" + ], + "borrow": ["Security operations patterns from real control-plane interfaces"], + "roleRules": [ + "Permit colors communicate deterministic status only", + "Risk emphasis must remain subordinate to evidence and remediation", + "Use Phosphor icons for product actions" + ], + "reject": [ + "Cyberpunk decoration", + "Red-first fear signaling without evidence", + "Generic compliance dashboard card grids", + "Model prose presented as deterministic finding state" + ], + "mediaStrategy": "Prefer graph paths, proof-pack artifacts, and real scanner output over decorative illustration." + }, + "policy": { + "themes": ["light", "dark"], + "viewports": [ + {"id": "desktop", "width": 1440, "height": 1000, "deviceScaleFactor": 1}, + {"id": "mobile", "width": 390, "height": 844, "deviceScaleFactor": 1} + ], + "states": ["default", "hover", "focus", "disabled", "loading", "empty", "error", "approved", "review", "blocked", "critical"], + "surfaces": [ + {"id": "dashboard", "kind": "route", "target": "/", "viewports": ["desktop", "mobile"], "themes": ["light", "dark"], "states": ["default"]} + ], + "forbiddenPatterns": [ + "Status conveyed by color alone", + "Raw colors outside canonical token definitions", + "Target repository code execution during validation", + "Motion without reduced-motion behavior" + ], + "accessibility": { + "standard": "WCAG 2.2 AA", + "keyboardRequired": true, + "reducedMotionRequired": true + } + }, + "baselines": {"manifest": "design/baselines/manifest.json"} +} diff --git a/scripts/design/finalize-captures.mjs b/scripts/design/finalize-captures.mjs new file mode 100644 index 0000000..d466c8a --- /dev/null +++ b/scripts/design/finalize-captures.mjs @@ -0,0 +1,27 @@ +import { execFileSync, spawnSync } from "node:child_process"; + +const sourceCommit = execFileSync("git", ["rev-parse", "HEAD"], { encoding: "utf8" }).trim(); +const artifactBaseUri = process.env.TABELLIO_ARTIFACT_BASE_URI?.trim(); +if (!artifactBaseUri) { + throw new Error("TABELLIO_ARTIFACT_BASE_URI is required; visual evidence without a durable URI is blocked."); +} + +const args = [ + "--repo", ".", + "--profile", "design/product.design.json", + "--captures-dir", ".artifacts/tabellio/visual-captures", + "--artifact-base-uri", artifactBaseUri, + "--source-commit", sourceCommit, + "--out", ".artifacts/tabellio/visual-baseline-candidate.json", +]; +const script = process.env.TABELLIO_DESIGN_CAPTURES_SCRIPT?.trim(); +const command = script ? process.execPath : "tabellio-design-captures"; +const result = spawnSync(command, script ? [script, ...args] : args, { + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], +}); +if (result.stdout) process.stdout.write(result.stdout); +if (result.stderr) process.stderr.write(result.stderr); +if (result.status !== 0) process.exit(result.status ?? 1); +const payload = JSON.parse(result.stdout); +console.log(`Visual capture count: ${payload.captureCount}`); diff --git a/tabellio.validation.json b/tabellio.validation.json new file mode 100644 index 0000000..562cd8e --- /dev/null +++ b/tabellio.validation.json @@ -0,0 +1,37 @@ +{ + "schemaVersion": "tabellio-validation/v0.2", + "id": "apo-product-validity", + "failFast": false, + "requireEntireCheckpoint": false, + "acceptance": { + "id": "APO-120", + "source": "plane", + "risk": "critical", + "outcomes": [ + "PermitGraph produces valid, deterministic scanner and proof-pack artifacts.", + "Permit decisions follow policy and evaluation oracles.", + "CLI and GitHub Action workflows preserve CI fail-closed behavior.", + "PermitGraph dashboard captures cover declared viewport and theme states.", + "Credential, MCP, workflow, prompt, and attack-path controls detect unsafe expansion." + ], + "invariants": [ + "Deterministic scanner artifacts remain the source of truth.", + "Validation never executes target code or calls a live model provider." + ], + "forbiddenOutcomes": [ + "Model prose creates a finding without scanner evidence.", + "Validation executes a target repository tool, workflow, MCP server, or package script.", + "Unknown provider cost is treated as zero." + ], + "requiredValidatorTypes": ["schema", "semantic", "workflow", "visual", "operational", "security"] + }, + "validators": [ + {"id": "static-release", "type": "static", "argv": ["tabellio-validator", "--profile", "static"], "cwd": ".", "timeoutMs": 900000, "required": true, "evidence": null, "policy": {"metricThresholds": [], "maxCostUsd": null, "requireCostTelemetry": false}}, + {"id": "schema-scanner-artifacts", "type": "schema", "argv": ["tabellio-validator", "--profile", "schema", "--validator-id", "schema-scanner-artifacts", "--out", ".artifacts/tabellio/schema.json"], "cwd": ".", "timeoutMs": 300000, "required": true, "evidence": {"path": ".artifacts/tabellio/schema.json"}, "policy": {"metricThresholds": [{"metric": "scanner_artifact_contracts_pass", "operator": "eq", "value": 1}], "maxCostUsd": null, "requireCostTelemetry": false}}, + {"id": "semantic-permit-policy", "type": "semantic", "argv": ["tabellio-validator", "--profile", "semantic", "--validator-id", "semantic-permit-policy", "--out", ".artifacts/tabellio/semantic.json"], "cwd": ".", "timeoutMs": 300000, "required": true, "evidence": {"path": ".artifacts/tabellio/semantic.json"}, "policy": {"metricThresholds": [{"metric": "permit_policy_oracles_pass", "operator": "eq", "value": 1}], "maxCostUsd": null, "requireCostTelemetry": false}}, + {"id": "workflow-cli-ci", "type": "workflow", "argv": ["tabellio-validator", "--profile", "workflow", "--validator-id", "workflow-cli-ci", "--out", ".artifacts/tabellio/workflow.json"], "cwd": ".", "timeoutMs": 300000, "required": true, "evidence": {"path": ".artifacts/tabellio/workflow.json"}, "policy": {"metricThresholds": [{"metric": "cli_ci_proofpack_workflows_pass", "operator": "eq", "value": 1}], "maxCostUsd": null, "requireCostTelemetry": false}}, + {"id": "visual-dashboard", "type": "visual", "argv": ["tabellio-validator", "--profile", "visual", "--validator-id", "visual-dashboard", "--out", ".artifacts/tabellio/visual.json"], "cwd": ".", "timeoutMs": 600000, "required": true, "evidence": {"path": ".artifacts/tabellio/visual.json"}, "policy": {"metricThresholds": [{"metric": "visual_capture_count", "operator": "eq", "value": 4}], "maxCostUsd": null, "requireCostTelemetry": false}}, + {"id": "operational-provider-boundary", "type": "operational", "argv": ["tabellio-validator", "--profile", "operational", "--validator-id", "operational-provider-boundary", "--out", ".artifacts/tabellio/operational.json"], "cwd": ".", "timeoutMs": 300000, "required": true, "evidence": {"path": ".artifacts/tabellio/operational.json"}, "policy": {"metricThresholds": [{"metric": "local_provider_and_storage_boundaries_pass", "operator": "eq", "value": 1}], "maxCostUsd": 0, "requireCostTelemetry": true}}, + {"id": "security-agent-boundaries", "type": "security", "argv": ["tabellio-validator", "--profile", "security", "--validator-id", "security-agent-boundaries", "--out", ".artifacts/tabellio/security.json"], "cwd": ".", "timeoutMs": 300000, "required": true, "evidence": {"path": ".artifacts/tabellio/security.json"}, "policy": {"metricThresholds": [{"metric": "agent_attack_path_controls_pass", "operator": "eq", "value": 1}], "maxCostUsd": null, "requireCostTelemetry": false}} + ] +}