/** * GridStamp Proof Agent — Claude Agent SDK Wrapper * * Wraps the GridStamp verification pipeline in a Claude-driven agentic loop. * The agent orchestrates four tools: * read_sensor → capture a camera frame from the injected driver * verify_spatial_proof → run SSIM/LPIPS/depth verification * check_antispoofing → run replay/patch/depth-injection detection * write_attestation → append a proof-chain entry and audit log record * * Every attestation is appended to ./proof-audit.ndjson — the append-only * audit trail is the insurance wedge: immutable, timestamped, machine-readable. * * Usage: * import { runProofAgent } from './agent/proof-agent.js'; * const result = await runProofAgent({ robotId, hmacSecret, sensorReader }); */ import fs from 'node:fs'; import path from 'node:path'; import Anthropic from '@anthropic-ai/sdk'; import type { ContentBlock, MessageParam, Tool, ToolUseBlock, ToolResultBlockParam } from '@anthropic-ai/sdk/resources/messages.js'; import type { CameraFrame, Pose, SpatialProof } from '../types/index.js'; import { generateSpatialProof, } from '../verification/spatial-proof.js'; import { ProofChain, hashProof, } from '../verification/proof-chain.js'; import { FrameIntegrityChecker, } from '../antispoofing/detector.js'; import type { ThreatDetection } from '../types/index.js'; import { generateNonce } from '../utils/crypto.js'; // ============================================================ // PUBLIC TYPES // ============================================================ /** Inject a function that reads a sensor frame given a sensor ID. */ export type SensorReader = (sensorId: string) => Promise; export interface ProofAgentOptions { /** Robot/device identifier — must match your GridStamp config. */ robotId: string; /** HMAC signing secret (min 32 chars). */ hmacSecret: string; /** * Caller-provided sensor reader. The agent calls this whenever it invokes * the read_sensor tool. In production, wire this to your camera driver. * In tests, inject a mock that returns synthetic frames. */ sensorReader: SensorReader; /** * Expected GPS/world location for the spatial proof verification. * Defaults to { position: {x:0,y:0,z:0}, orientation: {w:1,x:0,y:0,z:0} }. */ expectedPose?: Pose; /** * Path for the append-only NDJSON audit log. * Defaults to ./proof-audit.ndjson (relative to process.cwd()). */ auditLogPath?: string; /** * Anthropic API key. Falls back to ANTHROPIC_API_KEY env var. */ apiKey?: string; /** * Max agentic loop iterations (safety guard). * Defaults to 12. */ maxIterations?: number; } export interface AuditLogEntry { proofId: string; robotId: string; status: 'pass' | 'fail' | 'spoofing_detected'; confidence: number; spoofingSignals: readonly string[]; timestamp: number; chainLength: number; } export interface AttestationResult { /** 'pass' | 'fail' | 'spoofing_detected' */ status: 'pass' | 'fail' | 'spoofing_detected'; /** Stable proof ID from the proof chain. */ proofId: string; /** Composite spatial verification score [0,1]. */ confidence: number; /** Human-readable spoofing signal descriptions (empty if clean). */ spoofingSignals: readonly string[]; /** Absolute path to the NDJSON audit log file. */ auditTrailPath: string; /** Number of proofs in the chain after this attestation. */ chainLength: number; } // ============================================================ // SYSTEM PROMPT // ============================================================ /** * Verification policy injected as the cached system prompt. * cache_control with ttl "1h" — stable across many fleet runs per hour. */ const VERIFICATION_POLICY = `You are the GridStamp proof-of-presence verification agent. ## Role You verify that an autonomous robot, drone, or delivery agent is physically present at the claimed location by running a cryptographic pipeline: 1. Read sensor data from the hardware 2. Verify the spatial proof (SSIM + LPIPS + depth comparison) 3. Check for anti-spoofing signals (replay, adversarial patches, depth injection) 4. Write a tamper-evident attestation to the proof chain ## What counts as valid - Spatial proof PASSES when the composite score >= 0.75 AND no critical spoofing threats exist. - Any critical-severity spoofing threat overrides a passing spatial score → status = "spoofing_detected". - A failed spatial proof with no spoofing signals → status = "fail". - A passing spatial proof with no critical threats → status = "pass". ## Procedure Always run all four tools in sequence: read_sensor → verify_spatial_proof → check_antispoofing → write_attestation Do not skip steps. Do not call write_attestation unless you have results from both verify_spatial_proof and check_antispoofing. ## Output format After write_attestation succeeds, output a single JSON object: { "status": "pass" | "fail" | "spoofing_detected", "proof_id": "", "confidence": , "spoofing_signals": ["signal1", ...], "chain_length": } No additional commentary.`; // ============================================================ // TOOL DEFINITIONS // ============================================================ const PROOF_TOOLS: Tool[] = [ { name: 'read_sensor', description: 'Read a camera frame from the specified sensor. Returns the frame metadata including HMAC signature, sequence number, and pose.', input_schema: { type: 'object' as const, properties: { sensor_id: { type: 'string', description: 'Sensor/camera identifier (e.g. "primary-cam", "oak-d-001")', }, }, required: ['sensor_id'], }, }, { name: 'verify_spatial_proof', description: 'Run spatial verification (SSIM + LPIPS + depth MAE) comparing the captured frame against the expected render at the claimed location. Returns composite score and pass/fail.', input_schema: { type: 'object' as const, properties: { frame_id: { type: 'string', description: 'Frame ID returned by read_sensor', }, expected_location: { type: 'string', description: 'Human-readable description of expected location (e.g. "warehouse dock B-7")', }, }, required: ['frame_id', 'expected_location'], }, }, { name: 'check_antispoofing', description: 'Run anti-spoofing analysis on the captured frame. Detects replay attacks, adversarial patches, depth injection, and camera tampering. Returns list of threats.', input_schema: { type: 'object' as const, properties: { frame_id: { type: 'string', description: 'Frame ID returned by read_sensor', }, }, required: ['frame_id'], }, }, { name: 'write_attestation', description: 'Write a tamper-evident attestation to the proof chain and append an audit log entry. Call this only after both verify_spatial_proof and check_antispoofing have completed.', input_schema: { type: 'object' as const, properties: { proof_id: { type: 'string', description: 'Proof ID to record', }, status: { type: 'string', enum: ['pass', 'fail', 'spoofing_detected'], description: 'Final attestation status', }, metadata: { type: 'object', description: 'Additional metadata to record (e.g. sensor_id, location, scores)', }, }, required: ['proof_id', 'status', 'metadata'], }, }, ]; // ============================================================ // PROOF AGENT // ============================================================ export async function runProofAgent(options: ProofAgentOptions): Promise { const { robotId, hmacSecret, sensorReader, auditLogPath = path.resolve(process.cwd(), 'proof-audit.ndjson'), apiKey, maxIterations = 12, } = options; const expectedPose: Pose = options.expectedPose ?? { position: { x: 0, y: 0, z: 0 }, orientation: { w: 1, x: 0, y: 0, z: 0 }, timestamp: Date.now(), }; const client = new Anthropic({ apiKey: apiKey ?? process.env['ANTHROPIC_API_KEY'] }); // In-memory state shared across tool calls within one agent run const frameStore = new Map(); const proofStore = new Map(); const integrityChecker = new FrameIntegrityChecker(hmacSecret); const chain = new ProofChain(); let finalResult: AttestationResult | null = null; // ── Tool execution (PostToolUse logic inlined here) ───────────────────────── async function executeReadSensor(args: { sensor_id: string }): Promise { const frame = await sensorReader(args.sensor_id); frameStore.set(frame.id, frame); return JSON.stringify({ frame_id: frame.id, sensor_id: args.sensor_id, width: frame.width, height: frame.height, has_depth: frame.depth != null, has_pose: frame.pose != null, has_hmac: frame.hmac != null, sequence_number: frame.sequenceNumber, timestamp: frame.timestamp, }); } async function executeVerifySpatialProof(args: { frame_id: string; expected_location: string; }): Promise { const frame = frameStore.get(args.frame_id); if (!frame) { return JSON.stringify({ error: `Frame ${args.frame_id} not found. Call read_sensor first.` }); } // Build an "expected render" from the frame itself — in production this // would come from long-term memory 3DGS. This is the bridge point callers // can override by providing a richer sensorReader. const expectedRender = { rgb: frame.rgb, depth: frame.depth ?? new Float32Array(0), width: frame.width, height: frame.height, pose: expectedPose, renderTimeMs: 0, }; const prevHash = chain.tipHash; const proof = generateSpatialProof( robotId, expectedPose, frame, expectedRender, 'agent-merkle-root', [], hmacSecret, undefined, undefined, prevHash, ); proofStore.set(proof.id, proof); return JSON.stringify({ proof_id: proof.id, passed: proof.passed, composite: proof.metrics.composite, ssim: proof.metrics.ssim, lpips: proof.metrics.lpips, depth_mae: proof.metrics.depthMAE, location: args.expected_location, }); } function executeCheckAntispoofing(args: { frame_id: string }): string { const frame = frameStore.get(args.frame_id); if (!frame) { return JSON.stringify({ error: `Frame ${args.frame_id} not found. Call read_sensor first.` }); } const integrity = integrityChecker.check(frame); const isSafe = integrityChecker.isSafe(integrity); const threats: readonly ThreatDetection[] = integrity.threats; const criticalThreats = threats.filter(t => t.severity === 'critical'); const highThreats = threats.filter(t => t.severity === 'high'); return JSON.stringify({ is_safe: isSafe, hmac_valid: integrity.hmacValid, sequence_valid: integrity.sequenceValid, timing_valid: integrity.timingValid, threat_count: threats.length, critical_count: criticalThreats.length, high_count: highThreats.length, threats: threats.map(t => ({ type: t.type, severity: t.severity, confidence: t.confidence, details: t.details, })), }); } function executeWriteAttestation(args: { proof_id: string; status: 'pass' | 'fail' | 'spoofing_detected'; metadata: Record; }): string { const proof = proofStore.get(args.proof_id); let chainLength = chain.length; if (proof) { try { chain.append(proof); chainLength = chain.length; } catch { // Chain link may already exist or genesis mismatch — log but continue } } const spoofingSignals: string[] = []; if (args.metadata['threats'] && Array.isArray(args.metadata['threats'])) { for (const t of args.metadata['threats'] as Array<{ severity: string; details: string }>) { if (t.severity === 'critical' || t.severity === 'high') { spoofingSignals.push(t.details); } } } const confidence = typeof args.metadata['composite'] === 'number' ? args.metadata['composite'] : 0; // PostToolUse hook — append to audit log synchronously const auditEntry: AuditLogEntry = { proofId: args.proof_id, robotId, status: args.status, confidence, spoofingSignals, timestamp: Date.now(), chainLength, }; const line = JSON.stringify(auditEntry) + '\n'; fs.appendFileSync(auditLogPath, line, 'utf8'); // Store result for the caller finalResult = { status: args.status, proofId: args.proof_id, confidence, spoofingSignals, auditTrailPath: auditLogPath, chainLength, }; return JSON.stringify({ success: true, proof_id: args.proof_id, status: args.status, chain_length: chainLength, audit_log: auditLogPath, }); } // ── Agentic loop ───────────────────────────────────────────────────────────── const messages: MessageParam[] = [ { role: 'user', content: `Verify proof-of-presence for robot "${robotId}". Sensor: "primary-cam". Expected location: "claimed operational zone".`, }, ]; let iterations = 0; while (iterations < maxIterations) { iterations++; const response = await client.messages.create({ model: 'claude-sonnet-4-6', max_tokens: 2048, system: [ { type: 'text', text: VERIFICATION_POLICY, cache_control: { type: 'ephemeral', ttl: '1h' } as unknown as Anthropic.CacheControlEphemeral, }, ], tools: PROOF_TOOLS, messages, }); // Append assistant response messages.push({ role: 'assistant', content: response.content }); if (response.stop_reason === 'end_turn') { // Try to parse the final JSON result from the text block if (!finalResult) { for (const block of response.content) { if (block.type === 'text') { try { const parsed = JSON.parse(block.text.trim()) as { status?: 'pass' | 'fail' | 'spoofing_detected'; proof_id?: string; confidence?: number; spoofing_signals?: string[]; chain_length?: number; }; if (parsed.status && parsed.proof_id) { finalResult = { status: parsed.status, proofId: parsed.proof_id, confidence: parsed.confidence ?? 0, spoofingSignals: parsed.spoofing_signals ?? [], auditTrailPath: auditLogPath, chainLength: parsed.chain_length ?? chain.length, }; } } catch { // Not JSON — continue } } } } break; } if (response.stop_reason !== 'tool_use') { break; } // Execute tool calls const toolUseBlocks = response.content.filter( (b: ContentBlock): b is ToolUseBlock => b.type === 'tool_use', ); const toolResults: ToolResultBlockParam[] = []; for (const toolUse of toolUseBlocks) { let result: string; try { switch (toolUse.name) { case 'read_sensor': result = await executeReadSensor(toolUse.input as { sensor_id: string }); break; case 'verify_spatial_proof': result = await executeVerifySpatialProof( toolUse.input as { frame_id: string; expected_location: string }, ); break; case 'check_antispoofing': result = executeCheckAntispoofing(toolUse.input as { frame_id: string }); break; case 'write_attestation': result = executeWriteAttestation( toolUse.input as { proof_id: string; status: 'pass' | 'fail' | 'spoofing_detected'; metadata: Record; }, ); break; default: result = JSON.stringify({ error: `Unknown tool: ${toolUse.name}` }); } } catch (err) { result = JSON.stringify({ error: String(err) }); } toolResults.push({ type: 'tool_result', tool_use_id: toolUse.id, content: result, }); } messages.push({ role: 'user', content: toolResults }); } // Fallback if agent didn't call write_attestation if (!finalResult) { const fallbackId = generateNonce(16); const auditEntry: AuditLogEntry = { proofId: fallbackId, robotId, status: 'fail', confidence: 0, spoofingSignals: ['Agent loop did not complete attestation'], timestamp: Date.now(), chainLength: chain.length, }; fs.appendFileSync(auditLogPath, JSON.stringify(auditEntry) + '\n', 'utf8'); finalResult = { status: 'fail', proofId: fallbackId, confidence: 0, spoofingSignals: ['Agent loop did not complete attestation'], auditTrailPath: auditLogPath, chainLength: chain.length, }; } return finalResult; }