From 5f9627cd9257ad9593390bdc40458953ba7ab8de Mon Sep 17 00:00:00 2001 From: eonedgeproject-code Date: Tue, 23 Jun 2026 22:02:46 +0700 Subject: [PATCH 1/2] fix(sdk): make ProofBundle types round-trip with the Rust runtime The @witnex/sdk types did not match the serde-derived JSON the Rust runtime actually emits, so a bundle produced by `witnex demo summarize` could not round-trip through the SDK: - struct fields are snake_case (input_hash, model_id, tool_calls, ...), not camelCase; - Digest, Nonce, and Proof.bytes serialize as byte arrays (number[]), not hex/base64 strings; - ProofBundle was missing the top-level `commitment` field that the Rust ProofBundle and every emitted bundle include. Align the SDK interfaces with witnex-core / witnex-prover, document the byte-array encoding, and note the planned hex/base64 follow-up. Also flesh out the previously empty examples package with a runnable round-trip example that parses and structurally validates a bundle, wire real build/typecheck/test scripts (replacing the echo stubs), and make the SDK `test` script run a typecheck instead of a no-op. Refresh stale module docs: witnex-core no longer "types only" (it owns the hashing/commitment logic), witnex-prover no longer references the completed "Prompt 2", and the CLI `verify` help reflects the Phase 1 structural recompute. --- crates/witnex-cli/src/main.rs | 2 +- crates/witnex-core/src/lib.rs | 7 +- crates/witnex-prover/src/lib.rs | 5 +- packages/examples/package.json | 10 ++- packages/examples/src/round-trip.ts | 110 ++++++++++++++++++++++++++ packages/examples/tsconfig.build.json | 11 +++ packages/examples/tsconfig.json | 18 +++++ packages/sdk/package.json | 2 +- packages/sdk/src/index.ts | 77 +++++++++++++----- 9 files changed, 214 insertions(+), 28 deletions(-) create mode 100644 packages/examples/src/round-trip.ts create mode 100644 packages/examples/tsconfig.build.json create mode 100644 packages/examples/tsconfig.json diff --git a/crates/witnex-cli/src/main.rs b/crates/witnex-cli/src/main.rs index 7b2de1e..a1e9134 100644 --- a/crates/witnex-cli/src/main.rs +++ b/crates/witnex-cli/src/main.rs @@ -49,7 +49,7 @@ enum Command { #[command(subcommand)] demo: DemoCommand, }, - /// Verify a proof bundle (implemented in the Risc0 slice). + /// Verify a proof bundle (Phase 1: structural commitment recompute). Verify { /// Path to a proof bundle JSON file. path: PathBuf, diff --git a/crates/witnex-core/src/lib.rs b/crates/witnex-core/src/lib.rs index b3a7eb2..822c352 100644 --- a/crates/witnex-core/src/lib.rs +++ b/crates/witnex-core/src/lib.rs @@ -21,9 +21,10 @@ //! commits to *what the model was asked and what it returned*, not to *whether //! the returned answer is the "right" one*. //! -//! This crate defines **only types and traits** — no proving, hashing, or I/O -//! logic lives here yet. See [`witnex-prover`] and [`witnex-verifier`] for the -//! Risc0 proof machinery. +//! Beyond the core types and traits, this crate provides the canonical hashing +//! and trace-commitment logic (see [`hash`] and [`trace`]). It does **not** do +//! proving or I/O — see [`witnex-prover`] and [`witnex-verifier`] for the Risc0 +//! proof machinery. //! //! [`witnex-prover`]: https://github.com/witnex/witnex //! [`witnex-verifier`]: https://github.com/witnex/witnex diff --git a/crates/witnex-prover/src/lib.rs b/crates/witnex-prover/src/lib.rs index c14c87a..22b0b49 100644 --- a/crates/witnex-prover/src/lib.rs +++ b/crates/witnex-prover/src/lib.rs @@ -6,8 +6,9 @@ //! **well-formed** — that its commitments form a consistent hash chain — *not* //! that the LLM inference was correct. //! -//! This module currently defines **only types** (no proving logic). The Risc0 -//! host/guest integration lands in Prompt 2. +//! This module currently defines **only types** (no proving logic). Wiring the +//! Risc0 host/guest so a real receipt fills `Proof::bytes` is the remaining +//! zkVM work; see the `zkvm/` workspace. //! //! [Risc0]: https://dev.risczero.com/ diff --git a/packages/examples/package.json b/packages/examples/package.json index 5106bfe..c604c72 100644 --- a/packages/examples/package.json +++ b/packages/examples/package.json @@ -6,11 +6,15 @@ "license": "MIT OR Apache-2.0", "type": "module", "scripts": { - "build": "echo \"(no examples yet)\"", - "lint": "echo \"(no examples yet)\"", - "test": "echo \"(no examples yet)\"" + "build": "tsc -p tsconfig.build.json", + "typecheck": "tsc -p tsconfig.json", + "lint": "tsc -p tsconfig.json", + "test": "tsc -p tsconfig.build.json && node dist/round-trip.js" }, "dependencies": { "@witnex/sdk": "workspace:*" + }, + "devDependencies": { + "typescript": "^6.0.3" } } diff --git a/packages/examples/src/round-trip.ts b/packages/examples/src/round-trip.ts new file mode 100644 index 0000000..0837319 --- /dev/null +++ b/packages/examples/src/round-trip.ts @@ -0,0 +1,110 @@ +/** + * Example: round-trip a Witnex `ProofBundle` through the SDK types. + * + * The Rust demo CLI emits a bundle with `witnex demo summarize ""`. This + * example parses that exact JSON shape into the SDK's {@link ProofBundle} type + * and performs a **structural** sanity check: every digest is a 32-byte array, + * tool calls are well-formed, and the commitment is present. + * + * It does NOT recompute the commitment or verify the ZK proof — recomputation + * lives in the Rust verifier today, and proof verification is a later phase. The + * point here is to show the SDK types faithfully model what the runtime emits. + * + * Run: `pnpm --filter @witnex/examples build && node dist/round-trip.js` + */ + +import type { Digest, ProofBundle, ToolCall } from "@witnex/sdk"; + +/** + * A sample bundle in the exact serde-derived JSON shape the Rust CLI writes: + * `snake_case` fields, `Digest`/`Nonce`/`proof.bytes` as byte arrays, a + * top-level `commitment`, and (in Phase 1) an empty `proof.bytes`. + * + * The digest/nonce/commitment bytes below are illustrative placeholders — a + * real bundle's digests are SHA-256 outputs over the canonical encodings. + */ +const SAMPLE_BUNDLE_JSON = `{ + "trace": { + "input_hash": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31], + "prompt_template_hash": [31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0], + "model_id": "claude-opus-4-8", + "output_hash": [9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9], + "tool_calls": [], + "timestamp": 1750700000000, + "nonce": [255, 254, 253, 252, 251, 250, 249, 248, 247, 246, 245, 244, 243, 242, 241, 240, 239, 238, 237, 236, 235, 234, 233, 232, 231, 230, 229, 228, 227, 226, 225, 224] + }, + "commitment": [42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42], + "proof": { "bytes": [] } +}`; + +/** SHA-256 produces 32-byte digests; {@link Digest} mirrors that fixed width. */ +const DIGEST_LEN = 32; + +/** Narrow an arbitrary value to a 32-byte digest array. */ +function isDigest(value: unknown): value is Digest { + return ( + Array.isArray(value) && + value.length === DIGEST_LEN && + value.every((b) => Number.isInteger(b) && b >= 0 && b <= 255) + ); +} + +function assert(condition: boolean, message: string): asserts condition { + if (!condition) { + throw new Error(`bundle is malformed: ${message}`); + } +} + +/** + * Parse and structurally validate a Witnex proof bundle. + * + * Throws if the JSON does not match the {@link ProofBundle} shape. Returns the + * typed bundle on success. + */ +export function parseBundle(json: string): ProofBundle { + const bundle = JSON.parse(json) as ProofBundle; + + assert(isDigest(bundle.commitment), "commitment must be a 32-byte digest"); + + const { trace } = bundle; + assert(isDigest(trace.input_hash), "trace.input_hash must be a 32-byte digest"); + assert( + isDigest(trace.prompt_template_hash), + "trace.prompt_template_hash must be a 32-byte digest", + ); + assert(isDigest(trace.output_hash), "trace.output_hash must be a 32-byte digest"); + assert(isDigest(trace.nonce), "trace.nonce must be a 32-byte array"); + assert(typeof trace.model_id === "string", "trace.model_id must be a string"); + assert(typeof trace.timestamp === "number", "trace.timestamp must be a number"); + assert(Array.isArray(trace.tool_calls), "trace.tool_calls must be an array"); + + trace.tool_calls.forEach((call: ToolCall, i) => { + assert(typeof call.name === "string", `tool_calls[${i}].name must be a string`); + assert(isDigest(call.input_hash), `tool_calls[${i}].input_hash must be a 32-byte digest`); + assert(isDigest(call.output_hash), `tool_calls[${i}].output_hash must be a 32-byte digest`); + }); + + assert(Array.isArray(bundle.proof.bytes), "proof.bytes must be a byte array"); + + return bundle; +} + +/** Render a digest as a lowercase hex string for display. */ +function toHex(digest: Digest): string { + return digest.map((b) => b.toString(16).padStart(2, "0")).join(""); +} + +function main(): void { + const bundle = parseBundle(SAMPLE_BUNDLE_JSON); + + console.log("Parsed a structurally valid Witnex proof bundle:"); + console.log(` model: ${bundle.trace.model_id}`); + console.log(` tool calls: ${bundle.trace.tool_calls.length}`); + console.log(` commitment: ${toHex(bundle.commitment)}`); + console.log( + ` proof: ${bundle.proof.bytes.length} bytes` + + (bundle.proof.bytes.length === 0 ? " (placeholder — Risc0 guest not yet wired)" : ""), + ); +} + +main(); diff --git a/packages/examples/tsconfig.build.json b/packages/examples/tsconfig.build.json new file mode 100644 index 0000000..6546174 --- /dev/null +++ b/packages/examples/tsconfig.build.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "outDir": "./dist", + "rootDir": "./src", + "paths": { + "@witnex/sdk": ["../sdk/dist/index.d.ts"] + } + } +} diff --git a/packages/examples/tsconfig.json b/packages/examples/tsconfig.json new file mode 100644 index 0000000..593231b --- /dev/null +++ b/packages/examples/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "noUncheckedIndexedAccess": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "baseUrl": ".", + "paths": { + "@witnex/sdk": ["../sdk/src/index.ts"] + } + }, + "include": ["src"] +} diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 71d24ea..43f132d 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -18,7 +18,7 @@ "scripts": { "build": "tsc -p tsconfig.json", "typecheck": "tsc -p tsconfig.json --noEmit", - "test": "echo \"(no tests yet)\"" + "test": "tsc -p tsconfig.json --noEmit" }, "devDependencies": { "typescript": "^6.0.3" diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 6f100ee..b284dd6 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -1,14 +1,32 @@ /** * @witnex/sdk — TypeScript types for the Witnex verifiable AI agent framework. * - * These interfaces mirror the canonical Rust types in `witnex-core` so that a - * proof bundle produced by the Rust runtime round-trips through the SDK. + * These interfaces mirror the canonical Rust types in `witnex-core` and + * `witnex-prover` so that a proof bundle produced by the Rust runtime + * round-trips through the SDK without re-shaping. + * + * The shapes here match the **serde-derived JSON** the Rust runtime emits: + * - struct fields are `snake_case` (serde's default), e.g. `input_hash`, + * `model_id`, `tool_calls`; + * - newtype wrappers (`Digest`, `Nonce`, `ModelId`, `Timestamp`) serialize + * transparently to their inner value; + * - `Digest`, `Nonce`, and `Proof.bytes` are byte arrays (`number[]`, each + * element 0–255), because `[u8; 32]` / `Vec` serialize as JSON arrays. * * Phase 1: **types only**. Proving and verification are exposed in later phases. */ -/** A 32-byte SHA-256 digest, encoded as a lowercase hex string. */ -export type Digest = string; +/** + * A 32-byte SHA-256 digest. + * + * Serialized by the Rust runtime as an array of 32 byte values (`number[]`, + * each 0–255), mirroring `Digest([u8; 32])` in `witnex-core`. + * + * > Note: a human-friendly hex/base64 on-disk encoding is a planned follow-up + * > in `witnex-core` (see its `Digest` `TODO`). When that lands, this type + * > becomes `string`; until then it is the raw byte array. + */ +export type Digest = number[]; /** Identifier of the LLM that produced an output, e.g. `"claude-opus-4-8"`. */ export type ModelId = string; @@ -16,17 +34,22 @@ export type ModelId = string; /** A Unix timestamp in milliseconds. */ export type Timestamp = number; -/** A per-trace random nonce, encoded as a lowercase hex string. */ -export type Nonce = string; +/** + * A per-trace random nonce. + * + * Serialized as an array of 32 byte values (`number[]`), mirroring + * `Nonce([u8; 32])` in `witnex-core`. + */ +export type Nonce = number[]; /** A single tool invocation recorded within an {@link ExecutionTrace}. */ export interface ToolCall { /** The tool's name, e.g. `"web_search"`. */ name: string; /** SHA-256 digest of the canonical encoding of the tool's input arguments. */ - inputHash: Digest; + input_hash: Digest; /** SHA-256 digest of the canonical encoding of the tool's output. */ - outputHash: Digest; + output_hash: Digest; /** When the tool call completed. */ timestamp: Timestamp; } @@ -34,29 +57,47 @@ export interface ToolCall { /** A complete, tamper-evident record of one agent execution. */ export interface ExecutionTrace { /** SHA-256 digest of the agent's input. */ - inputHash: Digest; + input_hash: Digest; /** SHA-256 digest of the prompt template the agent applied to the input. */ - promptTemplateHash: Digest; - /** Identifier of the LLM that produced {@link outputHash}. */ - modelId: ModelId; + prompt_template_hash: Digest; + /** Identifier of the LLM that produced {@link ExecutionTrace.output_hash}. */ + model_id: ModelId; /** SHA-256 digest of the agent's final output. */ - outputHash: Digest; + output_hash: Digest; /** Tool calls made during execution, in the order they occurred. */ - toolCalls: ToolCall[]; + tool_calls: ToolCall[]; /** When the execution completed. */ timestamp: Timestamp; /** Per-trace random nonce for uniqueness and replay resistance. */ nonce: Nonce; } -/** An opaque zero-knowledge proof that an {@link ExecutionTrace} is well-formed. */ +/** + * An opaque zero-knowledge proof that an {@link ExecutionTrace} is well-formed. + * + * In the Risc0 implementation this wraps a serialized receipt. In Phase 1 it is + * a placeholder byte container that is **empty** until the guest lands. + */ export interface Proof { - /** Serialized Risc0 receipt, base64-encoded. */ - bytes: string; + /** Serialized Risc0 receipt as a byte array. Empty (`[]`) until implemented. */ + bytes: number[]; } -/** A self-contained bundle pairing a trace with its proof. */ +/** + * A self-contained bundle pairing a trace with its commitment and proof. + * + * This is the artifact a Witnex agent emits and a verifier consumes — the + * single JSON file produced by the demo CLI. + */ export interface ProofBundle { + /** The execution trace being attested to. */ trace: ExecutionTrace; + /** + * The trace's canonical commitment (the public *journal*): the value the + * proof attests to, equal to the commitment of `trace` for an untampered + * bundle. A verifier checks `trace` against this. + */ + commitment: Digest; + /** The proof that `trace` is well-formed. Empty until the Risc0 guest lands. */ proof: Proof; } From 6ed220f14fd1c4cc1ad66ff939c8cd41c615c90e Mon Sep 17 00:00:00 2001 From: eonedgeproject-code Date: Tue, 23 Jun 2026 22:17:01 +0700 Subject: [PATCH 2/2] fix(ci): update lockfile for examples typescript dep, drop deprecated baseUrl Two CI fixes for the examples package: - pnpm-lock.yaml: add the typescript devDependency to the packages/examples importer so `pnpm install --frozen-lockfile` passes (it was rejecting the stale lockfile). - packages/examples/tsconfig.json: remove `baseUrl`, which TypeScript 6.0 reports as a hard error (TS5101). `paths` resolves relative to the tsconfig without it. Verified with the repo toolchain (pnpm 9, typescript 6.0.3): frozen-lockfile install, `pnpm lint`, `pnpm typecheck`, and the examples build + run all pass. --- packages/examples/tsconfig.json | 1 - pnpm-lock.yaml | 4 ++++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/examples/tsconfig.json b/packages/examples/tsconfig.json index 593231b..6057fbb 100644 --- a/packages/examples/tsconfig.json +++ b/packages/examples/tsconfig.json @@ -9,7 +9,6 @@ "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "noEmit": true, - "baseUrl": ".", "paths": { "@witnex/sdk": ["../sdk/src/index.ts"] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b9eed16..a291105 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,6 +26,10 @@ importers: '@witnex/sdk': specifier: workspace:* version: link:../sdk + devDependencies: + typescript: + specifier: ^6.0.3 + version: 6.0.3 packages/sdk: devDependencies: