Skip to content

Commit c902eb8

Browse files
cmangunclaude
andcommitted
Initial scaffold: SvelteKit viewer with parser, verifier, and metrics
- SvelteKit app scaffold with static adapter - Bundle parser: JSONL trace/receipts + JSON manifests - Client-side verifier: hash chain continuity, schema validation, event-receipt linkage, bundle completeness - Metrics computation: event types, policy decisions, duration - Example bundle with 4 trace events and receipts - TypeScript verifier package stub - UX documentation with layout and color system Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent e254cba commit c902eb8

15 files changed

Lines changed: 506 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Changelog
2+
3+
## [0.1.0] - 2025-02-21
4+
5+
### Added
6+
- SvelteKit viewer app scaffold
7+
- Bundle parser (JSONL + JSON)
8+
- Client-side verifier (hash chain, schema, linkage)
9+
- Metrics computation
10+
- Example bundle
11+
- UX documentation

CONTRIBUTING.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Contributing
2+
3+
1. Open an issue
4+
2. Fork and branch
5+
3. Submit PR with screenshots/GIFs for UI changes
6+
4. Ensure all verification tests pass

README.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# agentic-evidence-viewer
2+
3+
A drag-drop viewer for **agent traces, receipts, policy decisions, and artifacts**.
4+
5+
This repo lets a reviewer open a bundle and see:
6+
- Integrity status (PASS/FAIL) with reasons
7+
- Trace timeline and event details
8+
- Policy decisions and required gates
9+
- Artifact manifests and previews (safe types)
10+
- Summary metrics (cost/latency/errors)
11+
12+
Bundle and receipt standards:
13+
- [agentic-receipts](https://github.com/cmangun/agentic-receipts)
14+
15+
## Quick Start
16+
17+
```bash
18+
cd apps/viewer
19+
npm install
20+
npm run dev
21+
```
22+
23+
Then drag-drop a bundle from `examples/bundles/`.
24+
25+
## Architecture
26+
27+
```
28+
Bundle (dropped) → Parser → Verifier → Renderer
29+
30+
[schema + chain + signature]
31+
32+
Integrity Badge (PASS/FAIL)
33+
```
34+
35+
### Views
36+
37+
| View | Description |
38+
|------|-------------|
39+
| **Timeline** | Chronological trace events with expandable details |
40+
| **Policy** | Allow/deny decisions with rationale and policy hash |
41+
| **Artifacts** | File explorer with safe previews (text/markdown/JSON) |
42+
| **Metrics** | Cost, latency, error rates, tool call distribution |
43+
| **Integrity** | Hash chain verification, signature status |
44+
45+
## Tech Stack
46+
47+
- **SvelteKit** — UI framework
48+
- **TypeScript** — Type safety
49+
- **WASM verifier** — Client-side hash chain and signature verification
50+
51+
## Suite
52+
53+
This repo is part of the **Agentic Evidence Suite**:
54+
- [agentic-receipts](https://github.com/cmangun/agentic-receipts) (standard)
55+
- [agentic-trace-cli](https://github.com/cmangun/agentic-trace-cli) (tooling)
56+
- [agentic-artifacts](https://github.com/cmangun/agentic-artifacts) (outputs)
57+
- [agentic-policy-engine](https://github.com/cmangun/agentic-policy-engine) (governance)
58+
- [agentic-eval-harness](https://github.com/cmangun/agentic-eval-harness) (scenarios)
59+
- [agentic-evidence-viewer](https://github.com/cmangun/agentic-evidence-viewer) (review UI)
60+
61+
## License
62+
63+
MIT

SECURITY.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Security Policy
2+
3+
Report vulnerabilities in bundle verification or artifact preview rendering responsibly.
4+
5+
## Scope
6+
7+
- Client-side verification bypass
8+
- XSS via artifact preview
9+
- Bundle parsing exploits

apps/viewer/package.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"name": "agentic-evidence-viewer",
3+
"version": "0.1.0",
4+
"private": true,
5+
"scripts": {
6+
"dev": "vite dev",
7+
"build": "vite build",
8+
"preview": "vite preview"
9+
},
10+
"devDependencies": {
11+
"@sveltejs/adapter-static": "^3.0.0",
12+
"@sveltejs/kit": "^2.0.0",
13+
"svelte": "^5.0.0",
14+
"typescript": "^5.0.0",
15+
"vite": "^5.0.0"
16+
}
17+
}

apps/viewer/src/lib/metrics.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/**
2+
* Metrics computation from parsed bundles.
3+
*
4+
* Computes summary statistics for display in the metrics panel.
5+
*/
6+
7+
import type { ParsedBundle, TraceEvent } from './parser';
8+
9+
export interface BundleMetrics {
10+
totalEvents: number;
11+
eventTypeCounts: Record<string, number>;
12+
totalReceipts: number;
13+
totalArtifacts: number;
14+
policyDecisions: {
15+
allow: number;
16+
deny: number;
17+
require_approval: number;
18+
};
19+
durationMs: number;
20+
hasSignatures: boolean;
21+
}
22+
23+
/**
24+
* Compute metrics from a parsed bundle.
25+
*/
26+
export function computeMetrics(bundle: ParsedBundle): BundleMetrics {
27+
const eventTypeCounts: Record<string, number> = {};
28+
for (const event of bundle.events) {
29+
eventTypeCounts[event.event_type] = (eventTypeCounts[event.event_type] || 0) + 1;
30+
}
31+
32+
const policyEvents = bundle.events.filter((e) => e.event_type === 'policy_decision');
33+
const policyDecisions = { allow: 0, deny: 0, require_approval: 0 };
34+
for (const event of policyEvents) {
35+
const decision = (event.payload as Record<string, string>).decision;
36+
if (decision in policyDecisions) {
37+
policyDecisions[decision as keyof typeof policyDecisions]++;
38+
}
39+
}
40+
41+
const timestamps = bundle.events.map((e) => new Date(e.timestamp).getTime());
42+
const durationMs =
43+
timestamps.length >= 2 ? Math.max(...timestamps) - Math.min(...timestamps) : 0;
44+
45+
const hasSignatures = bundle.receipts.some((r) => r.signature != null);
46+
47+
return {
48+
totalEvents: bundle.events.length,
49+
eventTypeCounts,
50+
totalReceipts: bundle.receipts.length,
51+
totalArtifacts: bundle.artifacts.length,
52+
policyDecisions,
53+
durationMs,
54+
hasSignatures,
55+
};
56+
}

apps/viewer/src/lib/parser.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/**
2+
* Bundle parser: reads trace JSONL, receipts JSONL, and manifests.
3+
*
4+
* Parses the contents of an agent evidence bundle into typed structures
5+
* for rendering in the viewer.
6+
*/
7+
8+
export interface TraceEvent {
9+
event_id: string;
10+
event_type: string;
11+
timestamp: string;
12+
sequence: number;
13+
parent_event_id?: string;
14+
payload: Record<string, unknown>;
15+
metadata?: Record<string, unknown>;
16+
}
17+
18+
export interface Receipt {
19+
receipt_id: string;
20+
event_id: string;
21+
hash: string;
22+
prev_hash: string;
23+
timestamp: string;
24+
event_type: string;
25+
signature?: {
26+
algorithm: string;
27+
public_key: string;
28+
value: string;
29+
};
30+
metadata?: Record<string, unknown>;
31+
}
32+
33+
export interface Artifact {
34+
artifact_id: string;
35+
event_id: string;
36+
hash: string;
37+
path: string;
38+
artifact_type: string;
39+
content_type?: string;
40+
size_bytes?: number;
41+
provenance?: {
42+
trace_event_ids: string[];
43+
model?: string;
44+
tool?: string;
45+
};
46+
}
47+
48+
export interface BundleManifest {
49+
bundle_id: string;
50+
version: string;
51+
created_at: string;
52+
trace_file: string;
53+
receipts_file: string;
54+
artifacts_manifest?: string;
55+
event_count: number;
56+
metadata?: Record<string, unknown>;
57+
}
58+
59+
export interface ParsedBundle {
60+
manifest: BundleManifest;
61+
events: TraceEvent[];
62+
receipts: Receipt[];
63+
artifacts: Artifact[];
64+
}
65+
66+
/**
67+
* Parse JSONL content into an array of objects.
68+
*/
69+
export function parseJsonl<T>(content: string): T[] {
70+
return content
71+
.trim()
72+
.split('\n')
73+
.filter((line) => line.trim())
74+
.map((line) => JSON.parse(line) as T);
75+
}
76+
77+
/**
78+
* Parse a complete bundle from file contents.
79+
*/
80+
export function parseBundle(files: Map<string, string>): ParsedBundle {
81+
const manifestContent = files.get('bundle.json') || files.get('manifest.json');
82+
if (!manifestContent) {
83+
throw new Error('Bundle manifest (bundle.json or manifest.json) not found');
84+
}
85+
86+
const manifest: BundleManifest = JSON.parse(manifestContent);
87+
88+
const traceContent = files.get(manifest.trace_file) || '';
89+
const receiptsContent = files.get(manifest.receipts_file) || '';
90+
const artifactsContent = manifest.artifacts_manifest
91+
? files.get(manifest.artifacts_manifest) || ''
92+
: '';
93+
94+
const events = traceContent ? parseJsonl<TraceEvent>(traceContent) : [];
95+
const receipts = receiptsContent ? parseJsonl<Receipt>(receiptsContent) : [];
96+
97+
let artifacts: Artifact[] = [];
98+
if (artifactsContent) {
99+
const artifactManifest = JSON.parse(artifactsContent);
100+
artifacts = artifactManifest.artifacts || [];
101+
}
102+
103+
return { manifest, events, receipts, artifacts };
104+
}

apps/viewer/src/lib/verifier.ts

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
/**
2+
* Client-side verification of hash chains and signatures.
3+
*
4+
* Verifies bundle integrity without requiring a server:
5+
* - Hash chain continuity (each receipt chains to the previous)
6+
* - Schema validation (required fields present)
7+
* - Signature verification (when available, via WebCrypto or WASM)
8+
*/
9+
10+
import type { Receipt, ParsedBundle } from './parser';
11+
12+
export interface VerificationResult {
13+
passed: boolean;
14+
checks: VerificationCheck[];
15+
summary: string;
16+
}
17+
18+
export interface VerificationCheck {
19+
name: string;
20+
passed: boolean;
21+
details: string;
22+
}
23+
24+
/**
25+
* Verify the integrity of a parsed bundle.
26+
*/
27+
export function verifyBundle(bundle: ParsedBundle): VerificationResult {
28+
const checks: VerificationCheck[] = [];
29+
30+
// Check 1: Bundle has events and receipts
31+
checks.push({
32+
name: 'Bundle completeness',
33+
passed: bundle.events.length > 0 && bundle.receipts.length > 0,
34+
details: `${bundle.events.length} events, ${bundle.receipts.length} receipts`,
35+
});
36+
37+
// Check 2: Event count matches manifest
38+
checks.push({
39+
name: 'Event count match',
40+
passed: bundle.events.length === bundle.manifest.event_count,
41+
details: `Manifest declares ${bundle.manifest.event_count}, found ${bundle.events.length}`,
42+
});
43+
44+
// Check 3: Receipt count matches event count
45+
checks.push({
46+
name: 'Receipt coverage',
47+
passed: bundle.receipts.length === bundle.events.length,
48+
details: `${bundle.receipts.length} receipts for ${bundle.events.length} events`,
49+
});
50+
51+
// Check 4: Hash chain continuity
52+
const chainResult = verifyHashChain(bundle.receipts);
53+
checks.push(chainResult);
54+
55+
// Check 5: Event-receipt linkage
56+
const linkageResult = verifyEventReceiptLinkage(bundle);
57+
checks.push(linkageResult);
58+
59+
const passed = checks.every((c) => c.passed);
60+
const failedCount = checks.filter((c) => !c.passed).length;
61+
62+
return {
63+
passed,
64+
checks,
65+
summary: passed
66+
? `All ${checks.length} checks passed`
67+
: `${failedCount}/${checks.length} checks failed`,
68+
};
69+
}
70+
71+
/**
72+
* Verify hash chain continuity: each receipt's prev_hash matches the
73+
* previous receipt's hash.
74+
*/
75+
function verifyHashChain(receipts: Receipt[]): VerificationCheck {
76+
if (receipts.length === 0) {
77+
return { name: 'Hash chain', passed: false, details: 'No receipts to verify' };
78+
}
79+
80+
// First receipt should have prev_hash = "genesis"
81+
if (receipts[0].prev_hash !== 'genesis') {
82+
return {
83+
name: 'Hash chain',
84+
passed: false,
85+
details: `First receipt prev_hash is "${receipts[0].prev_hash}", expected "genesis"`,
86+
};
87+
}
88+
89+
// Each subsequent receipt should chain to the previous
90+
for (let i = 1; i < receipts.length; i++) {
91+
if (receipts[i].prev_hash !== receipts[i - 1].hash) {
92+
return {
93+
name: 'Hash chain',
94+
passed: false,
95+
details: `Chain broken at receipt ${i}: prev_hash doesn't match previous hash`,
96+
};
97+
}
98+
}
99+
100+
return {
101+
name: 'Hash chain',
102+
passed: true,
103+
details: `${receipts.length} receipts form a continuous chain`,
104+
};
105+
}
106+
107+
/**
108+
* Verify that every event has a corresponding receipt.
109+
*/
110+
function verifyEventReceiptLinkage(bundle: ParsedBundle): VerificationCheck {
111+
const receiptEventIds = new Set(bundle.receipts.map((r) => r.event_id));
112+
const unlinked = bundle.events.filter((e) => !receiptEventIds.has(e.event_id));
113+
114+
return {
115+
name: 'Event-receipt linkage',
116+
passed: unlinked.length === 0,
117+
details:
118+
unlinked.length === 0
119+
? 'All events have corresponding receipts'
120+
: `${unlinked.length} events missing receipts`,
121+
};
122+
}

0 commit comments

Comments
 (0)