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
4 changes: 2 additions & 2 deletions examples/langchain-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,6 @@ const cl = new CommandLayer({

const input = { prompt: "Summarize how receipts prove agent actions." };

const result = await cl.wrap("chain.invoke", async () => chain.invoke(input));
const result = await cl.wrap("attest", async () => chain.invoke(input));

console.log(JSON.stringify(result.receipt, null, 2));
process.stdout.write(JSON.stringify(result.receipt, null, 2) + "\n");
21 changes: 20 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,28 @@ const TRUST_VERBS = [

export type TrustVerb = (typeof TRUST_VERBS)[number];

const TRUST_VERB_SET: ReadonlySet<string> = new Set(TRUST_VERBS);

export function normalizeTrustVerb(verb: string): string {
const prefix = `clas.${TRUST_FAMILY}.`;
return verb.startsWith(prefix) ? verb.slice(prefix.length) : verb;
}

/**
* Normalize and validate a trust verb. Throws if the resulting short verb is
* not a member of the schema-defined TRUST_VERBS enum.
*/
function resolveAndValidateTrustVerb(verb: string): string {
const normalized = normalizeTrustVerb(verb);
if (!TRUST_VERB_SET.has(normalized)) {
throw new Error(
`Invalid trust verb "${verb}". Must be one of: ${TRUST_VERBS.join(", ")}. ` +
`Fully-qualified names like "clas.trust-verification.verify" are also accepted.`,
);
}
return normalized;
}

export class CommandLayer {
private readonly config: {
signer: string;
Expand Down Expand Up @@ -124,7 +141,9 @@ export class CommandLayer {
typeof fnOrOptions === "function" ? {} : fnOrOptions.input ?? {};

const startedMs = Date.now();
const normalizedVerb = normalizeTrustVerb(verb);
// Validate verb before running the wrapped function so callers get a
// synchronous, descriptive error rather than a non-schema-valid receipt.
const normalizedVerb = resolveAndValidateTrustVerb(verb);
const startedAt = new Date().toISOString();

try {
Expand Down
54 changes: 51 additions & 3 deletions test/receipt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { webcrypto } from "node:crypto";
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";

import { CommandLayer } from "../src/index.js";
import { validateTrustReceipt } from "../src/index.js";
import { canonicalize } from "../src/canonicalize.js";
import { canonicalPayloadFromReceiptInput } from "../src/receipt.js";

Expand Down Expand Up @@ -47,6 +48,35 @@ test("wrapping an action creates a receipt with required fields", async () => {
assert.ok(result.receipt.execution.completed_at);
});

test("wrap produces a schema-valid receipt (round-trip)", async () => {
const cl = new CommandLayer({
signer: "verifyagent.eth",
keyId: "vC4WbcNoq2znSCiQ",
privateKeyPem: await generatePrivateKeyPem(),
});

const result = await cl.wrap("verify", {
input: { content: "hello world" },
run: async () => ({ approved: true }),
});

const validation = validateTrustReceipt(result.receipt);
assert.equal(validation.ok, true, `Receipt failed schema validation: ${validation.errors.join("; ")}`);
});

test("wrap rejects an unrecognized verb before running the wrapped function", async () => {
const cl = new CommandLayer({
signer: "verifyagent.eth",
keyId: "vC4WbcNoq2znSCiQ",
privateKeyPem: await generatePrivateKeyPem(),
});

await assert.rejects(
() => cl.wrap("summarize", async () => "should not run"),
/Invalid trust verb/,
);
});

test("signature is verifiable over raw canonical payload bytes", async () => {
const { pem, publicKey } = await generateKeyPair();

Expand All @@ -56,7 +86,7 @@ test("signature is verifiable over raw canonical payload bytes", async () => {
privateKeyPem: pem,
});

const { receipt } = await cl.wrap("summarize", {
const { receipt } = await cl.wrap("authenticate", {
input: { x: 1 },
run: async () => ({ y: 2 }),
});
Expand Down Expand Up @@ -92,7 +122,7 @@ test("wrap returns signed error receipt when wrapped agent throws", async () =>
privateKeyPem: await generatePrivateKeyPem(),
});

const result = await cl.wrap("summarize", {
const result = await cl.wrap("authenticate", {
input: { content: "hello" },
run: async () => {
throw new Error("simulated failure");
Expand All @@ -105,6 +135,24 @@ test("wrap returns signed error receipt when wrapped agent throws", async () =>
assert.equal(result.receipt.proof.alg, "ed25519");
});

test("error receipt is also schema-valid", async () => {
const cl = new CommandLayer({
signer: "verifyagent.eth",
keyId: "vC4WbcNoq2znSCiQ",
privateKeyPem: await generatePrivateKeyPem(),
});

const result = await cl.wrap("authenticate", {
input: { content: "hello" },
run: async () => {
throw new Error("simulated failure");
},
});

const validation = validateTrustReceipt(result.receipt);
assert.equal(validation.ok, true, `Error receipt failed schema validation: ${validation.errors.join("; ")}`);
});

test("fully-qualified trust capability verb normalizes to short verb", async () => {
const cl = new CommandLayer({
signer: "verifyagent.eth",
Expand Down Expand Up @@ -144,7 +192,7 @@ test("verification helper posts to verifierUrl", async () => {
verifierUrl,
});

const { receipt } = await cl.wrap("summarize", {
const { receipt } = await cl.wrap("verify", {
input: { content: "hello" },
run: async () => "hello",
});
Expand Down
Loading