Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/witnex-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
7 changes: 4 additions & 3 deletions crates/witnex-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions crates/witnex-prover/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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/

Expand Down
10 changes: 7 additions & 3 deletions packages/examples/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
110 changes: 110 additions & 0 deletions packages/examples/src/round-trip.ts
Original file line number Diff line number Diff line change
@@ -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 "<text>"`. 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();
11 changes: 11 additions & 0 deletions packages/examples/tsconfig.build.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": false,
"outDir": "./dist",
"rootDir": "./src",
"paths": {
"@witnex/sdk": ["../sdk/dist/index.d.ts"]
}
}
}
17 changes: 17 additions & 0 deletions packages/examples/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"noUncheckedIndexedAccess": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"paths": {
"@witnex/sdk": ["../sdk/src/index.ts"]
}
},
"include": ["src"]
}
2 changes: 1 addition & 1 deletion packages/sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
77 changes: 59 additions & 18 deletions packages/sdk/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,62 +1,103 @@
/**
* @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<u8>` 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;

/** 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;
}

/** 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;
}
4 changes: 4 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading