From 461b950c5e2e01c283c1003a3a9325f5c4379554 Mon Sep 17 00:00:00 2001 From: BubBeatz Production Date: Sun, 21 Jun 2026 18:20:13 +0000 Subject: [PATCH 1/3] feat: wire cAPI evidence to gnomledger, audit all rejections, add queryable audit endpoint Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .env.example | 16 +++++ README.md | 18 ++++++ src/app/api/audit/route.ts | 58 ++++++++++++++++++ src/lib/covenant/pgl-ledger.ts | 109 +++++++++++++++++++++++++++++++++ src/lib/covenant/runtime.ts | 109 ++++++++++++++++++++++++++++----- src/lib/covenant/types.ts | 25 ++++++++ 6 files changed, 320 insertions(+), 15 deletions(-) create mode 100644 .env.example create mode 100644 src/app/api/audit/route.ts create mode 100644 src/lib/covenant/pgl-ledger.ts diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..1e9f4fa --- /dev/null +++ b/.env.example @@ -0,0 +1,16 @@ +# Covenant (cAPI) configuration. Copy to .env.local and fill in as needed. +# Every value is optional — unset means the related integration stays disabled +# and the runtime falls back to its in-process behavior. + +# --- PGL ledger (gnomledger) forwarding --- +# When PGL_LEDGER_URL is set, every sealed evidence record (Phase 7) is mirrored +# into gnomledger's append-only, hash-chained ledger. Leave empty to keep the +# local seal only. +PGL_LEDGER_URL= +PGL_LEDGER_API_KEY= +PGL_LEDGER_TIMEOUT_MS=8000 + +# --- Phase 6 execution bridge (Veklom BYOS MCP gateway) --- +BYOS_MCP_GATEWAY_URL= +BYOS_INTERNAL_API_KEY= +COVENANT_EXEC_TIMEOUT_MS=10000 diff --git a/README.md b/README.md index ac71572..2a9949c 100644 --- a/README.md +++ b/README.md @@ -69,12 +69,30 @@ The runtime seeds a realistic fleet (agents, capabilities, three-tier policies, | `GET` | `/api/discover/{agentId}` | Capability discovery (effective permissions) | | `GET` | `/api/compose?agent_id&capability_id` | Policy composition + effective permissions | | `GET` | `/api/pgl/{hash}` | Retrieve an evidence record | +| `GET` | `/api/audit` | Query the audit trail (`agent_id`, `capability_id`, `status`, `forwarded`, `since`, `limit`) | | `GET` | `/api/replay/{hash}` | Walk the hash chain backwards | | `POST` | `/api/quarantine/{id}` | Approve / deny a quarantined request | | `POST` | `/api/policy/{id}` | Enable / disable a policy | | `POST` | `/api/agent/{id}` | Toggle agent suspension | | `POST` | `/api/budget` | Set an agent's budget for a capability | +## PGL ledger forwarding + +Phase 7 always seals a local SHA-256 hash-chained record. When `PGL_LEDGER_URL` +is set, every sealed record is also mirrored into the external **gnomledger** +(Project Genome Ledger) via `POST /api/v1/ledger/events` — an append-only, +per-agent hash chain that survives restarts and is independently verifiable at +`GET /api/v1/ledger/agents/{agent_id}/verify`. Forwarding is best-effort and +never blocks the pipeline: each evidence record carries an `external_ledger` +status (`disabled` · `pending` · `sealed` · `failed`) visible via `/api/audit` +and the Phase 8 trace. + +| Env var | Purpose | Default | +|---------|---------|---------| +| `PGL_LEDGER_URL` | gnomledger base URL; empty = forwarding disabled | _(unset)_ | +| `PGL_LEDGER_API_KEY` | gnomledger `x-api-key` (operator role or higher) | _(unset)_ | +| `PGL_LEDGER_TIMEOUT_MS` | forward request timeout | `8000` | + ## Stack Next.js 14 (App Router) · TypeScript · Tailwind CSS · Framer Motion · Node `crypto`. Part of the **Veklom** ecosystem. diff --git a/src/app/api/audit/route.ts b/src/app/api/audit/route.ts new file mode 100644 index 0000000..9877933 --- /dev/null +++ b/src/app/api/audit/route.ts @@ -0,0 +1,58 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getEngine } from "@/lib/covenant/engine"; +import { isLedgerConfigured } from "@/lib/covenant/pgl-ledger"; +import type { AuditQuery, Decision, LedgerForwardStatus } from "@/lib/covenant/types"; + +export const dynamic = "force-dynamic"; + +const DECISIONS: Decision[] = ["authorized", "denied", "error", "quarantined"]; +const FORWARD_STATES: LedgerForwardStatus[] = ["pending", "sealed", "failed", "disabled"]; + +/** + * Queryable audit trail. Returns sealed evidence newest-first with the external + * PGL (gnomledger) mirror status per record. + * + * Query params: agent_id, capability_id, status, forwarded, since (ISO-8601), + * limit (1-500). + */ +export function GET(req: NextRequest) { + const sp = req.nextUrl.searchParams; + + const statusParam = sp.get("status"); + if (statusParam && !DECISIONS.includes(statusParam as Decision)) { + return NextResponse.json( + { error: `status must be one of: ${DECISIONS.join(", ")}` }, + { status: 400 }, + ); + } + + const forwardedParam = sp.get("forwarded"); + if (forwardedParam && !FORWARD_STATES.includes(forwardedParam as LedgerForwardStatus)) { + return NextResponse.json( + { error: `forwarded must be one of: ${FORWARD_STATES.join(", ")}` }, + { status: 400 }, + ); + } + + const limitParam = sp.get("limit"); + const limit = limitParam ? Number(limitParam) : undefined; + if (limitParam && (!Number.isFinite(limit) || (limit as number) < 1)) { + return NextResponse.json({ error: "limit must be a positive integer" }, { status: 400 }); + } + + const query: AuditQuery = { + agent_id: sp.get("agent_id") ?? undefined, + capability_id: sp.get("capability_id") ?? undefined, + status: (statusParam as Decision | null) ?? undefined, + forwarded: (forwardedParam as LedgerForwardStatus | null) ?? undefined, + since: sp.get("since") ?? undefined, + limit, + }; + + const result = getEngine().runtime.queryAudit(query); + return NextResponse.json({ + pgl_ledger: { configured: isLedgerConfigured() }, + query, + ...result, + }); +} diff --git a/src/lib/covenant/pgl-ledger.ts b/src/lib/covenant/pgl-ledger.ts new file mode 100644 index 0000000..5128c21 --- /dev/null +++ b/src/lib/covenant/pgl-ledger.ts @@ -0,0 +1,109 @@ +/** + * PGL Ledger client — forwards sealed evidence to gnomledger. + * + * cAPI seals a SHA-256 hash-chained evidence record in-process (Phase 7). That + * proves integrity *within* this runtime, but the canonical, tamper-evident + * proof lives in the Project Genome Ledger (gnomledger) — an append-only, + * per-agent hash chain persisted out-of-process. This client mirrors every + * sealed evidence record into gnomledger's `POST /api/v1/ledger/events`, so the + * audit trail survives process restarts and is independently verifiable via + * `GET /api/v1/ledger/agents/{agent_id}/verify`. + * + * Config is env-driven (same convention as `mcp-bridge.ts`); when the ledger + * URL is not configured the client is a no-op that reports `disabled` — it never + * throws and never blocks the pipeline. The local seal always stands. + */ + +import type { Evidence, LedgerForward } from "./types"; + +const LEDGER_URL = process.env.PGL_LEDGER_URL ?? ""; +const LEDGER_API_KEY = process.env.PGL_LEDGER_API_KEY ?? ""; +const LEDGER_TIMEOUT = Number(process.env.PGL_LEDGER_TIMEOUT_MS ?? 8_000); + +/** Shape returned by gnomledger `POST /api/v1/ledger/events`. */ +interface LedgerEventResponse { + event_id: string; + event_hash: string; + prev_event_hash: string | null; +} + +export function isLedgerConfigured(): boolean { + return LEDGER_URL.length > 0; +} + +function summarize(evidence: Evidence): string { + const summary = `covenant ${evidence.result.status}: ${evidence.what.capability_name} · ${evidence.what.action}`; + return summary.slice(0, 255); +} + +/** + * Mirror one sealed evidence record into gnomledger. Idempotent on the cAPI + * `pgl_hash`, so retries (or duplicate forwards) collapse to a single chained + * event. Resolves with a `LedgerForward` describing the outcome — never rejects. + */ +export async function forwardEvidence(evidence: Evidence): Promise { + if (!isLedgerConfigured()) { + return { status: "disabled" }; + } + + const body = { + agent_id: evidence.who.agent_id, + event_type: "custom" as const, + actor: evidence.who.owner_id || evidence.who.agent_id, + summary: summarize(evidence), + details: { + source: "capi", + evidence_id: evidence.evidence_id, + connection_id: evidence.connection_id, + capi_pgl_hash: evidence.pgl_hash, + capi_previous_hash: evidence.previous_hash ?? null, + decision: evidence.result.status, + what: evidence.what, + when: evidence.when, + why: evidence.why, + how: evidence.how, + result: evidence.result, + compliance: evidence.compliance, + }, + idempotency_key: evidence.pgl_hash, + }; + + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), LEDGER_TIMEOUT); + try { + const res = await fetch(`${LEDGER_URL.replace(/\/$/, "")}/api/v1/ledger/events`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-api-key": LEDGER_API_KEY, + }, + body: JSON.stringify(body), + signal: controller.signal, + }); + if (!res.ok) { + const detail = await res.text().catch(() => ""); + return { + status: "failed", + forwarded_at: new Date().toISOString(), + error: `gnomledger ${res.status}: ${detail.slice(0, 200) || res.statusText}`, + }; + } + const json = (await res.json()) as LedgerEventResponse; + return { + status: "sealed", + event_id: json.event_id, + event_hash: json.event_hash, + prev_event_hash: json.prev_event_hash ?? undefined, + forwarded_at: new Date().toISOString(), + }; + } catch (err: unknown) { + const message = err instanceof Error ? err.message : String(err); + return { + status: "failed", + forwarded_at: new Date().toISOString(), + error: message, + }; + } finally { + clearTimeout(timer); + } +} diff --git a/src/lib/covenant/runtime.ts b/src/lib/covenant/runtime.ts index 261d2eb..478b5c9 100644 --- a/src/lib/covenant/runtime.ts +++ b/src/lib/covenant/runtime.ts @@ -17,8 +17,10 @@ import { import { SafetyLayer } from "./safety"; import { IntelligenceLayer } from "./intelligence"; import { GovernanceLayer } from "./governance"; +import { forwardEvidence, isLedgerConfigured } from "./pgl-ledger"; import type { AgentIdentity, + AuditQuery, CapabilityIdentity, CovenantRequest, CovenantResponse, @@ -156,7 +158,7 @@ export class CovenantRuntime { private generateEvidence( request: CovenantRequest, - agent: AgentIdentity, + agent: AgentIdentity | undefined, capability: CapabilityIdentity | undefined, status: Decision, policy_id: string, @@ -170,9 +172,9 @@ export class CovenantRuntime { pgl_hash: "", timestamp: now, who: { - agent_id: agent.agent_id, - agent_public_key: agent.public_key, - owner_id: agent.owner_id, + agent_id: agent?.agent_id ?? request.agent_id, + agent_public_key: agent?.public_key ?? "unverified", + owner_id: agent?.owner_id ?? "unknown", }, what: { capability_id: request.capability_id, @@ -212,9 +214,36 @@ export class CovenantRuntime { this.lastEvidenceHash = evidence.pgl_hash; this.evidenceLedger.set(evidence.pgl_hash, evidence); this.auditLog.unshift(evidence); + this.forwardToPgl(evidence); return evidence; } + /** + * Mirror a sealed evidence record into the external PGL (gnomledger). The + * pipeline stays synchronous: the local seal is authoritative and immediate, + * while the external chain is reconciled best-effort. The stored record is + * mutated in place when the forward resolves, so `/api/audit` and + * `/api/pgl/{hash}` reflect the live status. + */ + private forwardToPgl(evidence: Evidence): void { + if (!isLedgerConfigured()) { + evidence.external_ledger = { status: "disabled" }; + return; + } + evidence.external_ledger = { status: "pending" }; + forwardEvidence(evidence) + .then((forward) => { + evidence.external_ledger = forward; + }) + .catch((err: unknown) => { + evidence.external_ledger = { + status: "failed", + forwarded_at: new Date().toISOString(), + error: err instanceof Error ? err.message : String(err), + }; + }); + } + getEvidence(hash: string): Evidence | undefined { return this.evidenceLedger.get(hash); } @@ -223,6 +252,29 @@ export class CovenantRuntime { return this.auditLog.slice(0, limit); } + /** + * Query the audit log with filters — the backing query for `GET /api/audit`. + * Records are returned newest-first. `since` filters on the seal timestamp + * (ISO-8601). `forwarded` filters on the external PGL mirror status. + */ + queryAudit(filter: AuditQuery = {}): { total: number; matched: number; records: Evidence[] } { + const { agent_id, capability_id, status, since, forwarded, limit = 100 } = filter; + const sinceMs = since ? Date.parse(since) : NaN; + const matches = this.auditLog.filter((e) => { + if (agent_id && e.who.agent_id !== agent_id) return false; + if (capability_id && e.what.capability_id !== capability_id) return false; + if (status && e.result.status !== status) return false; + if (forwarded && (e.external_ledger?.status ?? "disabled") !== forwarded) return false; + if (!Number.isNaN(sinceMs) && Date.parse(e.timestamp) < sinceMs) return false; + return true; + }); + return { + total: this.auditLog.length, + matched: matches.length, + records: matches.slice(0, Math.max(1, Math.min(limit, 500))), + }; + } + /** Walk the hash chain backwards from a given evidence hash. */ replay(hash: string): { evidence?: Evidence; chain: Evidence[] } { const evidence = this.evidenceLedger.get(hash); @@ -261,16 +313,38 @@ export class CovenantRuntime { }); }; - const fail = (code: string, message: string): CovenantResponse => { - const delta = this.applyTrust(request.agent_id, "error"); + /** + * Reject a call before it clears the gates — and still seal an evidence + * record. Pre-authorization rejections (unknown/suspended/spoofed agents, + * replays, missing capabilities) are agent actions too, so they belong in + * the audit log and the hash chain, not just the trace. + */ + const reject = ( + code: string, + message: string, + reason: string, + agentMaybe: AgentIdentity | undefined, + outcome: Decision = "error", + ): CovenantResponse => { + const ev = this.generateEvidence( + request, + agentMaybe, + undefined, + outcome, + "security-reject", + { reason }, + 0, + ); + const delta = this.applyTrust(request.agent_id, outcome); return { connection_id: request.connection_id, - status: "error", + status: outcome, + evidence_hash: ev.pgl_hash, error: { code, message }, metadata: { trust_delta: delta, new_trust_score: this.trust.get(request.agent_id)?.score ?? 0, - audit_logged: false, + audit_logged: true, }, trace, }; @@ -281,11 +355,11 @@ export class CovenantRuntime { const agent = this.agents.get(request.agent_id); if (!agent) { mark(1, "Identity & Security", "fail", "Agent not found", { agent_id: request.agent_id }, p); - return fail("401", "Agent not found"); + return reject("401", "Agent not found", "agent_not_found", undefined); } if (this.suspended.has(agent.agent_id)) { mark(1, "Identity & Security", "fail", "Agent suspended", { agent_id: agent.agent_id }, p); - return fail("403", "Agent is suspended"); + return reject("403", "Agent is suspended", "agent_suspended", agent); } const message = canonicalRequestMessage(request); const signatureValid = verifyMessage(message, request.agent_signature, agent.public_key); @@ -293,14 +367,13 @@ export class CovenantRuntime { mark(1, "Identity & Security", "fail", "Invalid Ed25519 signature", { agent: agent.agent_name, }, p); - return fail("401", "Invalid signature"); + return reject("401", "Invalid signature", "invalid_signature", agent); } if (this.seenConnections.has(request.connection_id)) { mark(1, "Identity & Security", "fail", "Replay detected (duplicate connection_id)", { connection_id: request.connection_id, }, p); - this.applyTrust(request.agent_id, "denied"); - return fail("403", "Duplicate request detected (replay attack)"); + return reject("403", "Duplicate request detected (replay attack)", "replay_detected", agent, "denied"); } this.seenConnections.add(request.connection_id); mark(1, "Identity & Security", "pass", `Verified ${agent.agent_name} · Ed25519 ok · no replay`, { @@ -317,7 +390,7 @@ export class CovenantRuntime { mark(2, "Capability & Policy", "fail", "Capability not found", { capability_id: request.capability_id, }, p); - return fail("404", "Capability not found"); + return reject("404", "Capability not found", "capability_not_found", agent); } const trust = this.trust.get(request.agent_id); const delegation = this.governance.getDelegation(request.agent_id, request.capability_id); @@ -510,17 +583,23 @@ export class CovenantRuntime { // ===== PHASE 7 — EVIDENCE & PROOF ===== p = performance.now(); const evidence = this.generateEvidence(request, agent, capability, "authorized", policyId, output, executionMs); + const ledgerState = evidence.external_ledger?.status ?? "disabled"; mark(7, "Evidence & Proof", "pass", `Sealed · ${evidence.pgl_hash.slice(0, 16)}…`, { pgl_hash: evidence.pgl_hash, previous_hash: evidence.previous_hash, output_hash: evidence.result.output_hash, + pgl_ledger: evidence.external_ledger, }, p); // ===== PHASE 8 — AUDIT & COMPLIANCE ===== p = performance.now(); this.safety.observe(request.agent_id, request.capability_id, false); - mark(8, "Audit & Compliance", "pass", `Logged · retained ${evidence.compliance.retention_policy} · ${evidence.compliance.data_classification}`, { + const ledgerNote = ledgerState === "disabled" + ? "local seal only" + : `gnomledger ${ledgerState}`; + mark(8, "Audit & Compliance", "pass", `Logged · retained ${evidence.compliance.retention_policy} · ${evidence.compliance.data_classification} · ${ledgerNote}`, { compliance: evidence.compliance, + pgl_ledger: evidence.external_ledger, }, p); // ===== PHASE 9 — RESPONSE ===== diff --git a/src/lib/covenant/types.ts b/src/lib/covenant/types.ts index 90e39f5..7a1e3f6 100644 --- a/src/lib/covenant/types.ts +++ b/src/lib/covenant/types.ts @@ -128,6 +128,18 @@ export type DataClassification = | "confidential" | "restricted"; +export type LedgerForwardStatus = "pending" | "sealed" | "failed" | "disabled"; + +/** Outcome of mirroring an evidence record into the external PGL (gnomledger). */ +export interface LedgerForward { + status: LedgerForwardStatus; + event_id?: string; + event_hash?: string; + prev_event_hash?: string; + forwarded_at?: string; + error?: string; +} + export interface Evidence { evidence_id: string; connection_id: string; @@ -156,6 +168,19 @@ export interface Evidence { retention_policy: string; }; previous_hash?: string; + /** External PGL (gnomledger) mirror status; set after Phase 7 sealing. */ + external_ledger?: LedgerForward; +} + +/** Filters accepted by the queryable audit-trail endpoint. */ +export interface AuditQuery { + agent_id?: string; + capability_id?: string; + status?: Decision; + /** ISO-8601 lower bound on the evidence seal timestamp. */ + since?: string; + forwarded?: LedgerForwardStatus; + limit?: number; } export interface TrustScore { From 9891619444adaa30391220c36f30b35a255e6150 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Sun, 21 Jun 2026 18:28:21 +0000 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=93=9D=20Add=20docstrings=20to=20`dev?= =?UTF-8?q?in/1782066008-capi-gnomledger-audit`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Docstrings generation was requested by @reprewindai-dev. The following files were modified: * `src/app/api/audit/route.ts` * `src/lib/covenant/pgl-ledger.ts` These file types are not supported: * `.env.example` * `README.md` --- src/app/api/audit/route.ts | 9 +++++---- src/lib/covenant/pgl-ledger.ts | 22 +++++++++++++++++++--- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/app/api/audit/route.ts b/src/app/api/audit/route.ts index 9877933..4785404 100644 --- a/src/app/api/audit/route.ts +++ b/src/app/api/audit/route.ts @@ -9,11 +9,12 @@ const DECISIONS: Decision[] = ["authorized", "denied", "error", "quarantined"]; const FORWARD_STATES: LedgerForwardStatus[] = ["pending", "sealed", "failed", "disabled"]; /** - * Queryable audit trail. Returns sealed evidence newest-first with the external - * PGL (gnomledger) mirror status per record. + * Handles GET requests to query and return the audit trail. * - * Query params: agent_id, capability_id, status, forwarded, since (ISO-8601), - * limit (1-500). + * Accepts optional query parameters to filter results: `agent_id`, `capability_id`, `status`, + * `forwarded`, `since` (ISO-8601), and `limit`. + * + * @returns JSON response containing `pgl_ledger.configured`, the audit query, and results */ export function GET(req: NextRequest) { const sp = req.nextUrl.searchParams; diff --git a/src/lib/covenant/pgl-ledger.ts b/src/lib/covenant/pgl-ledger.ts index 5128c21..2322ddc 100644 --- a/src/lib/covenant/pgl-ledger.ts +++ b/src/lib/covenant/pgl-ledger.ts @@ -27,19 +27,35 @@ interface LedgerEventResponse { prev_event_hash: string | null; } +/** + * Determines if the ledger is configured. + * + * @returns `true` if the ledger URL is configured, `false` otherwise. + */ export function isLedgerConfigured(): boolean { return LEDGER_URL.length > 0; } +/** + * Builds a text summary of evidence for ledger recording. + * + * @returns A string (max 255 characters) in the format `covenant {status}: {capability_name} · {action}` + */ function summarize(evidence: Evidence): string { const summary = `covenant ${evidence.result.status}: ${evidence.what.capability_name} · ${evidence.what.action}`; return summary.slice(0, 255); } /** - * Mirror one sealed evidence record into gnomledger. Idempotent on the cAPI - * `pgl_hash`, so retries (or duplicate forwards) collapse to a single chained - * event. Resolves with a `LedgerForward` describing the outcome — never rejects. + * Forwards a sealed evidence record to gnomledger's ledger API. + * + * The forwarding is idempotent on the evidence's `pgl_hash`; retries or duplicate + * forwards collapse to a single ledger event. If the ledger is unconfigured, returns + * `{ status: "disabled" }` without making a request. Never rejects. + * + * @returns A `LedgerForward` result: `{ status: "disabled" }` if unconfigured; + * `{ status: "sealed", event_id, event_hash, prev_event_hash?, forwarded_at }` on + * successful mirroring; or `{ status: "failed", error, forwarded_at }` on error. */ export async function forwardEvidence(evidence: Evidence): Promise { if (!isLedgerConfigured()) { From bf8fd4c180e52aa271e2f3496b38540047d9cdd7 Mon Sep 17 00:00:00 2001 From: reprewindai-dev Date: Sun, 21 Jun 2026 14:38:31 -0400 Subject: [PATCH 3/3] fix: CodeRabbit limit check and timeout parsing, fix hash integrity verification --- src/app/api/audit/route.ts | 10 ++++++++-- src/lib/covenant/pgl-ledger.ts | 3 ++- src/lib/covenant/runtime.ts | 1 + 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/app/api/audit/route.ts b/src/app/api/audit/route.ts index 4785404..f9d56f9 100644 --- a/src/app/api/audit/route.ts +++ b/src/app/api/audit/route.ts @@ -37,8 +37,14 @@ export function GET(req: NextRequest) { const limitParam = sp.get("limit"); const limit = limitParam ? Number(limitParam) : undefined; - if (limitParam && (!Number.isFinite(limit) || (limit as number) < 1)) { - return NextResponse.json({ error: "limit must be a positive integer" }, { status: 400 }); + if ( + limitParam && + (!Number.isInteger(limit) || (limit as number) < 1 || (limit as number) > 500) + ) { + return NextResponse.json( + { error: "limit must be an integer between 1 and 500" }, + { status: 400 }, + ); } const query: AuditQuery = { diff --git a/src/lib/covenant/pgl-ledger.ts b/src/lib/covenant/pgl-ledger.ts index 2322ddc..aebef6d 100644 --- a/src/lib/covenant/pgl-ledger.ts +++ b/src/lib/covenant/pgl-ledger.ts @@ -18,7 +18,8 @@ import type { Evidence, LedgerForward } from "./types"; const LEDGER_URL = process.env.PGL_LEDGER_URL ?? ""; const LEDGER_API_KEY = process.env.PGL_LEDGER_API_KEY ?? ""; -const LEDGER_TIMEOUT = Number(process.env.PGL_LEDGER_TIMEOUT_MS ?? 8_000); +const parsedTimeout = Number(process.env.PGL_LEDGER_TIMEOUT_MS ?? 8_000); +const LEDGER_TIMEOUT = Number.isFinite(parsedTimeout) && parsedTimeout > 0 ? parsedTimeout : 8_000; /** Shape returned by gnomledger `POST /api/v1/ledger/events`. */ interface LedgerEventResponse { diff --git a/src/lib/covenant/runtime.ts b/src/lib/covenant/runtime.ts index 478b5c9..b70654e 100644 --- a/src/lib/covenant/runtime.ts +++ b/src/lib/covenant/runtime.ts @@ -210,6 +210,7 @@ export class CovenantRuntime { evidence.pgl_hash = hashObject({ ...evidence, pgl_hash: undefined, + external_ledger: undefined, }); this.lastEvidenceHash = evidence.pgl_hash; this.evidenceLedger.set(evidence.pgl_hash, evidence);