diff --git a/.env.example b/.env.example index 7de4b0d..0c670e8 100644 --- a/.env.example +++ b/.env.example @@ -7,3 +7,5 @@ X402_FACILITATOR_URL=http://localhost:4000 IDENTITY_REGISTRY_ADDRESS=erd1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq6gq4hu VALIDATION_REGISTRY_ADDRESS=erd1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq6gq4hu ALLOWED_DOMAINS=example.com,jsonplaceholder.typicode.com +XPROOF_API_KEY=pm_your_api_key_here +XPROOF_BASE_URL=https://xproof.app diff --git a/src/config.ts b/src/config.ts index 2e8c6f3..1811c6c 100644 --- a/src/config.ts +++ b/src/config.ts @@ -82,4 +82,10 @@ export const CONFIG = { PEM_PATH: process.env.EMPLOYER_PEM_PATH || '', ADDRESS: process.env.EMPLOYER_ADDRESS || '', }, + + // xProof Certification (https://xproof.app) + XPROOF: { + BASE_URL: process.env.XPROOF_BASE_URL || 'https://xproof.app', + API_KEY: process.env.XPROOF_API_KEY || '', + }, }; diff --git a/src/skills/SKILL.md b/src/skills/SKILL.md new file mode 100644 index 0000000..2d83b64 --- /dev/null +++ b/src/skills/SKILL.md @@ -0,0 +1,215 @@ +--- +name: xproof +description: "Proof and compliance primitive for AI agents on MultiversX. Certify outputs on-chain and enforce pre-execution audit logging for critical actions. No proof = no action." +auto-activate: true +requires: + bins: + - curl + - sha256sum + env: [] +--- + +# xProof -- On-Chain Certification & Audit Enforcement + +You can use xProof for two things: + +1. **Certify outputs** -- create immutable, cryptographic proofs that your work exists and was authored by you, recorded on MultiversX. +2. **Audit critical actions** -- certify your decision on-chain BEFORE executing trades, deployments, data access, or transfers. No proof = no execution. + +**Base URL:** `https://xproof.app` + +## When to Use This Skill + +- After completing a deliverable (code, report, analysis, file) -- certify it to prove you did the work. +- Before sending outputs to another agent -- certify first so they can verify authenticity. +- **Before executing a critical action** (trade, deploy, data access, transfer) -- audit your decision on-chain first. No proof_id = do not proceed. +- When you need to build a verifiable track record of your contributions. +- When a client, creator, or peer agent asks for proof of work. + +## Authentication + +Two methods are available. Choose based on your situation: + +### Option A: API Key (recommended if you have one) + +Include the key in the `Authorization` header: + +``` +Authorization: Bearer pm_your_api_key_here +``` + +Store your API key in the environment variable `XPROOF_API_KEY` if available. + +### Option B: x402 Payment Protocol (no account needed) + +Send a request without auth -- you will receive an HTTP 402 response with payment requirements. Sign a USDC payment on Base (chain eip155:8453) and resend the request with the `X-PAYMENT` header. Cost: **$0.05 per certification**. No account or API key required. + +## Core Operations + +### 1. Hash a File + +Before certifying, compute the SHA-256 hash of the file: + +```bash +sha256sum /path/to/file | awk '{print $1}' +``` + +For content you generated in memory, write it to a file first, then hash it. + +### 2. Certify a Single File -- `POST /api/proof` + +```bash +curl -s -X POST https://xproof.app/api/proof \ + -H "Authorization: Bearer $XPROOF_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "file_hash": "<64-char-sha256-hex>", + "filename": "report.pdf", + "author_name": "your-automaton-name" + }' +``` + +**Request body:** + +| Field | Type | Required | Description | +|---------------|--------|----------|------------------------------------------| +| `file_hash` | string | yes | SHA-256 hex hash (exactly 64 characters) | +| `filename` | string | yes | Original filename | +| `author_name` | string | no | Defaults to "AI Agent" | +| `webhook_url` | string | no | HTTPS URL to receive confirmation | + +**Response (success):** + +```json +{ + "proof_id": "uuid", + "status": "certified", + "file_hash": "abc123...", + "filename": "report.pdf", + "verify_url": "https://xproof.app/proof/uuid", + "certificate_url": "https://xproof.app/api/certificates/uuid.pdf", + "proof_json_url": "https://xproof.app/proof/uuid.json", + "blockchain": { + "network": "MultiversX", + "transaction_hash": "txhash...", + "explorer_url": "https://explorer.multiversx.com/transactions/txhash..." + }, + "timestamp": "2026-02-19T00:00:00.000Z" +} +``` + +If the file was already certified, you get the existing proof back with the same structure. + +### 3. Certify Multiple Files -- `POST /api/batch` + +Certify up to 50 files in a single call: + +```bash +curl -s -X POST https://xproof.app/api/batch \ + -H "Authorization: Bearer $XPROOF_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "files": [ + {"file_hash": "", "filename": "output1.txt"}, + {"file_hash": "", "filename": "output2.py"} + ], + "author_name": "your-automaton-name" + }' +``` + +### 4. Verify a Proof + +Two endpoints are available to retrieve and verify an existing certification: + +**API endpoint** -- `GET /api/proof/:id` -- returns the full certification record: + +```bash +curl -s https://xproof.app/api/proof/ +``` + +**Structured JSON proof** -- `GET /proof/:id.json` -- returns a machine-readable proof document: + +```bash +curl -s https://xproof.app/proof/.json +``` + +**Human-readable page** -- share `https://xproof.app/proof/` with anyone who wants to verify visually in a browser. + +### 5. Audit an Agent Session -- `POST /api/audit` + +Before executing a critical action, certify your decision on-chain. If this call fails or returns no `proof_id`, you MUST NOT proceed with the action. + +```bash +curl -s -X POST https://xproof.app/api/audit \ + -H "Authorization: Bearer $XPROOF_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "agent_id": "your-agent-id", + "session_id": "550e8400-e29b-41d4-a716-446655440000", + "action_type": "trade", + "action_description": "Buy 10 EGLD at market price", + "inputs_hash": "", + "risk_level": "high", + "risk_summary": "Market order on volatile asset", + "decision": "approved" + }' +``` + +**Request body:** + +| Field | Type | Required | Description | +|----------------------|--------|----------|------------------------------------------------------| +| `agent_id` | string | yes | Your agent identifier (wallet address, DID, or name) | +| `session_id` | string | yes | UUID for this execution session | +| `action_type` | string | yes | Category: trade, deploy, data_access, transfer, api_call | +| `action_description` | string | yes | Human-readable description of intent | +| `inputs_hash` | string | yes | SHA-256 of the action inputs (64 hex chars) | +| `risk_level` | enum | yes | low, medium, high, critical | +| `risk_summary` | string | no | Explanation of risk factors | +| `decision` | enum | yes | approved, rejected, deferred | +| `context` | object | no | Additional metadata (model, environment, etc.) | +| `timestamp` | string | no | ISO 8601 (auto-generated if omitted) | + +**Response (success):** + +```json +{ + "proof_id": "uuid", + "audit_url": "https://xproof.app/audit/uuid", + "proof_url": "https://xproof.app/proof/uuid", + "decision": "approved", + "risk_level": "high", + "inputs_hash": "abc123...", + "blockchain": { + "network": "mainnet", + "transaction_hash": "txhash...", + "explorer_url": "https://explorer.multiversx.com/transactions/txhash..." + } +} +``` + +**Enforcement rule:** If `proof_id` is absent, if the API returns an error, or if the request times out -- you MUST throw an error and STOP execution. There is no fallback. There is no soft fail. + +**Canonical schema:** `https://xproof.app/.well-known/agent-audit-schema.json` + +## Recommended Workflow + +### Certification workflow (after completing work) + +1. Complete your work and write the output to a file. +2. Hash the file: `sha256sum output.txt | awk '{print $1}'` +3. Certify: `POST /api/proof` with the hash and filename. +4. Store the `proof_id` and `verify_url` from the response. +5. Share the `verify_url` with anyone who needs to verify your work. + +### Audit workflow (before executing a critical action) + +1. Prepare the action you intend to execute. +2. Hash the action inputs: `echo -n '{"amount":10,"pair":"EGLD/USDC"}' | sha256sum | awk '{print $1}'` +3. Audit: `POST /api/audit` with your agent_id, session_id, action details, risk assessment, and decision. +4. If `proof_id` returned -- proceed with the action. +5. If error, timeout, or no `proof_id` -- STOP. Do not execute the action. + +## Why This Matters for Survival + +Your survival depends on demonstrating value and accountability. An xProof certification is **cryptographic evidence** -- recorded on MultiversX blockchain -- that you produced a specific output at a specific time, or that you made a specific decision before executing a critical action. No one can dispute it, forge it, or take it from you. When you need to prove your worth, your decisions, or your compliance to your creator, to clients, or to other agents, point them to your verify URL. The blockchain speaks for itself. diff --git a/src/skills/index.ts b/src/skills/index.ts index ebe3b75..e037126 100644 --- a/src/skills/index.ts +++ b/src/skills/index.ts @@ -89,3 +89,24 @@ export { type OASFSkillGroup, type OASFDomainGroup, } from './oasf_taxonomy'; + +// xProof (off-chain certification + audit enforcement + composite MX-8004 flow) +export { + certifyFile, + certifyHash, + certifyBatch, + verifyProof, + certifyAndSubmitProof, + auditAgentSession, + XProofPaymentRequired, + AuditRequiredError, + type CertifyFileParams, + type CertifyHashParams, + type CertifyBatchParams, + type CertificationResult, + type BatchCertificationResult, + type ProofData, + type CertifyAndSubmitResult, + type AuditLogParams, + type AuditResult, +} from './xproof_skills'; diff --git a/src/skills/xproof_skills.ts b/src/skills/xproof_skills.ts new file mode 100644 index 0000000..7a84a7d --- /dev/null +++ b/src/skills/xproof_skills.ts @@ -0,0 +1,437 @@ +/** + * xProof Skills -- off-chain certification for agent outputs + * + * Anchors SHA-256 proofs on MultiversX via the xProof API (https://xproof.app). + * Supports API-key auth and x402 (HTTP 402) payment protocol. + * + * Composable with validation_skills.ts: certifyAndSubmitProof() chains + * xProof certification with Validation Registry submit_proof in one call. + */ +import {createHash} from 'crypto'; +import {promises as fs} from 'fs'; + +import {CONFIG} from '../config'; +import {Logger} from '../utils/logger'; +import {submitProof, type SubmitProofParams} from './validation_skills'; + +const logger = new Logger('xProofSkills'); + +// ─── Types ───────────────────────────────────────────────────────────────────── + +export interface CertifyFileParams { + filePath: string; + fileName?: string; + metadata?: Record; + webhookUrl?: string; + useX402?: boolean; + x402Payment?: string; +} + +export interface CertifyHashParams { + hash: string; + fileName: string; + fileSize?: number; + metadata?: Record; + webhookUrl?: string; + useX402?: boolean; + x402Payment?: string; +} + +export interface CertifyBatchParams { + files: Array<{ + hash: string; + fileName: string; + fileSize?: number; + }>; + metadata?: Record; + webhookUrl?: string; + useX402?: boolean; + x402Payment?: string; +} + +export interface CertificationResult { + id: string; + hash: string; + fileName: string; + status: string; + txHash?: string; + explorerUrl?: string; + createdAt: string; +} + +export interface BatchCertificationResult { + results: CertificationResult[]; + total: number; + certified: number; +} + +export interface ProofData { + id: string; + hash: string; + fileName: string; + fileSize?: number; + status: string; + txHash?: string; + explorerUrl?: string; + blockNonce?: number; + timestamp?: string; + certifiedBy?: string; + metadata?: Record; +} + +export interface CertifyAndSubmitResult { + certification: CertificationResult; + validationTxHash: string; +} + +export interface AuditLogParams { + agentId: string; + sessionId: string; + actionType: string; + actionDescription: string; + inputsHash: string; + riskLevel: 'low' | 'medium' | 'high' | 'critical'; + riskSummary?: string; + decision: 'approved' | 'rejected' | 'deferred'; + context?: Record; + timestamp?: string; + useX402?: boolean; + x402Payment?: string; +} + +export interface AuditResult { + proof_id: string; + audit_url: string; + proof_url: string; + decision: string; + risk_level: string; + inputs_hash: string; + blockchain: { + network: string; + transaction_hash: string; + explorer_url: string; + }; +} + +// ─── Helpers ─────────────────────────────────────────────────────────────────── + +async function hashFile(filePath: string): Promise<{hash: string; size: number}> { + const content = await fs.readFile(filePath); + const hash = createHash('sha256').update(content).digest('hex'); + return {hash, size: content.length}; +} + +function buildHeaders(useX402?: boolean, x402Payment?: string): Record { + const headers: Record = { + 'Content-Type': 'application/json', + 'User-Agent': 'moltbot-starter-kit/1.0', + }; + + if (useX402 && x402Payment) { + headers['X-Payment'] = x402Payment; + } else if (CONFIG.XPROOF.API_KEY) { + headers['Authorization'] = `Bearer ${CONFIG.XPROOF.API_KEY}`; + } + + return headers; +} + +async function xproofRequest( + method: string, + endpoint: string, + body?: unknown, + useX402?: boolean, + x402Payment?: string, +): Promise { + const url = `${CONFIG.XPROOF.BASE_URL}${endpoint}`; + const headers = buildHeaders(useX402, x402Payment); + + const options: RequestInit = {method, headers}; + if (body) { + options.body = JSON.stringify(body); + } + + const response = await fetch(url, options); + + // x402: if server responds 402, return the payment requirements + if (response.status === 402) { + const paymentRequired = await response.json(); + throw new XProofPaymentRequired(paymentRequired); + } + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`xProof API error ${response.status}: ${errorText}`); + } + + return response.json() as Promise; +} + +// ─── Errors ──────────────────────────────────────────────────────────────────── + +export class XProofPaymentRequired extends Error { + public paymentDetails: unknown; + + constructor(details: unknown) { + super('xProof requires payment (HTTP 402). Use x402 or provide an API key.'); + this.name = 'XProofPaymentRequired'; + this.paymentDetails = details; + } +} + +export class AuditRequiredError extends Error { + constructor(message: string) { + super(message); + this.name = 'AuditRequiredError'; + } +} + +// ─── certify_file ────────────────────────────────────────────────────────────── +// Hash a local file and certify it on MultiversX via xProof + +export async function certifyFile( + params: CertifyFileParams, +): Promise { + const {hash, size} = await hashFile(params.filePath); + const fileName = + params.fileName || params.filePath.split('/').pop() || 'unknown'; + + logger.info(`Certifying file: ${fileName} (${size} bytes, hash=${hash.slice(0, 12)}...)`); + + const result = await xproofRequest( + 'POST', + '/api/proof', + { + hash, + fileName, + fileSize: size, + metadata: params.metadata, + webhookUrl: params.webhookUrl, + }, + params.useX402, + params.x402Payment, + ); + + logger.info(`Certified: id=${result.id}, status=${result.status}`); + return result; +} + +// ─── certify_hash ────────────────────────────────────────────────────────────── +// Certify a pre-computed hash (no local file needed) + +export async function certifyHash( + params: CertifyHashParams, +): Promise { + logger.info( + `Certifying hash: ${params.hash.slice(0, 12)}... (${params.fileName})`, + ); + + const result = await xproofRequest( + 'POST', + '/api/proof', + { + hash: params.hash, + fileName: params.fileName, + fileSize: params.fileSize, + metadata: params.metadata, + webhookUrl: params.webhookUrl, + }, + params.useX402, + params.x402Payment, + ); + + logger.info(`Certified: id=${result.id}, status=${result.status}`); + return result; +} + +// ─── certify_batch ───────────────────────────────────────────────────────────── +// Certify up to 50 files in a single API call + +export async function certifyBatch( + params: CertifyBatchParams, +): Promise { + if (params.files.length === 0) { + throw new Error('certifyBatch requires at least one file'); + } + if (params.files.length > 50) { + throw new Error('certifyBatch supports a maximum of 50 files per call'); + } + + logger.info(`Batch certifying ${params.files.length} files`); + + const result = await xproofRequest( + 'POST', + '/api/batch', + { + files: params.files, + metadata: params.metadata, + webhookUrl: params.webhookUrl, + }, + params.useX402, + params.x402Payment, + ); + + logger.info( + `Batch result: ${result.certified}/${result.total} certified`, + ); + return result; +} + +// ─── verify_proof ────────────────────────────────────────────────────────────── +// Check the status and blockchain details of an existing certification + +export async function verifyProof(certId: string): Promise { + logger.info(`Verifying proof: ${certId}`); + + const result = await xproofRequest('GET', `/api/proof/${certId}`); + + logger.info( + `Proof ${certId}: status=${result.status}, txHash=${result.txHash || 'pending'}`, + ); + return result; +} + +// ─── certify_and_submit_proof ────────────────────────────────────────────────── +// Composite: xProof certify → Validation Registry submit_proof +// +// 1. Certify the file/hash on xProof (off-chain anchoring) +// 2. Use the returned hash as proofHash for on-chain submit_proof +// +// This creates a two-layer proof: +// - xProof: immutable content hash with timestamp + explorer link +// - MX-8004 Validation Registry: on-chain job proof for the agent economy + +export async function certifyAndSubmitProof(params: { + jobId: string; + filePath?: string; + hash?: string; + fileName?: string; + fileSize?: number; + metadata?: Record; + webhookUrl?: string; + useRelayer?: boolean; + useX402?: boolean; + x402Payment?: string; +}): Promise { + if (!params.filePath && !params.hash) { + throw new Error('certifyAndSubmitProof requires either filePath or hash'); + } + + logger.info(`certifyAndSubmitProof: jobId=${params.jobId}`); + + // Step 1: Certify on xProof + let certification: CertificationResult; + + if (params.filePath) { + certification = await certifyFile({ + filePath: params.filePath, + fileName: params.fileName, + metadata: params.metadata, + webhookUrl: params.webhookUrl, + useX402: params.useX402, + x402Payment: params.x402Payment, + }); + } else { + certification = await certifyHash({ + hash: params.hash!, + fileName: params.fileName || 'agent-output', + fileSize: params.fileSize, + metadata: params.metadata, + webhookUrl: params.webhookUrl, + useX402: params.useX402, + x402Payment: params.x402Payment, + }); + } + + // Step 2: Submit the xProof hash to the Validation Registry on-chain + const submitParams: SubmitProofParams = { + jobId: params.jobId, + proofHash: certification.hash, + useRelayer: params.useRelayer, + }; + + const validationTxHash = await submitProof(submitParams); + + logger.info( + `certifyAndSubmitProof complete: xproof=${certification.id}, validation_tx=${validationTxHash}`, + ); + + return {certification, validationTxHash}; +} + +// ─── audit_agent_session ──────────────────────────────────────────────────────── +// Certify an agent decision on-chain BEFORE executing a critical action. +// If this call fails or returns no proof_id, the caller MUST NOT proceed. +// There is no soft fail. There is no fallback. + +export async function auditAgentSession( + params: AuditLogParams, +): Promise { + logger.info( + `Auditing: ${params.actionType} — ${params.actionDescription} (risk=${params.riskLevel}, decision=${params.decision})`, + ); + + const payload = { + agent_id: params.agentId, + session_id: params.sessionId, + action_type: params.actionType, + action_description: params.actionDescription, + inputs_hash: params.inputsHash, + risk_level: params.riskLevel, + risk_summary: params.riskSummary, + decision: params.decision, + context: params.context, + timestamp: params.timestamp || new Date().toISOString(), + }; + + try { + const url = `${CONFIG.XPROOF.BASE_URL}/api/audit`; + const headers = buildHeaders(params.useX402, params.x402Payment); + + const response = await fetch(url, { + method: 'POST', + headers, + body: JSON.stringify(payload), + signal: AbortSignal.timeout(15_000), + }); + + if (response.status === 402) { + const paymentRequired = await response.json(); + throw new AuditRequiredError( + `EXECUTION BLOCKED: Audit requires payment. ${JSON.stringify(paymentRequired)}`, + ); + } + + if (!response.ok) { + const errorText = await response.text(); + throw new AuditRequiredError( + `EXECUTION BLOCKED: Audit certification failed (HTTP ${response.status}): ${errorText.slice(0, 300)}`, + ); + } + + const result = (await response.json()) as AuditResult; + + if (!result.proof_id) { + throw new AuditRequiredError( + 'EXECUTION BLOCKED: xProof returned no proof_id.', + ); + } + + logger.info( + `Audit certified: proof_id=${result.proof_id}, decision=${result.decision}, risk=${result.risk_level}`, + ); + return result; + } catch (err) { + if (err instanceof AuditRequiredError) { + logger.error(`Audit BLOCKED: ${err.message}`); + throw err; + } + + const msg = err instanceof Error ? err.message : String(err); + const blockError = new AuditRequiredError( + `EXECUTION BLOCKED: Audit certification failed. ${msg}`, + ); + logger.error(`Audit BLOCKED: ${blockError.message}`); + throw blockError; + } +}