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
10 changes: 10 additions & 0 deletions changelog/entries/2026-07-08-order-dock-kerf-quotes.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"id": "2026-07-08-order-dock-kerf-quotes",
"version": "0.9.4",
"date": "2026-07-08",
"category": "feat",
"title": "Order dock + live SendCutSend quotes via kerf",
"summary": "The viewer grows an order dock rendering the fused vcad+kerf order lifecycle with approval deep-links, and sheet-metal quotes can carry the fab's own displayed price (pricing_basis \"quoted\").",
"features": ["mcp", "fabricate", "viewer", "kerf", "sheet-metal"],
"mcpTools": ["quote_manufacturing", "get_order_feed"]
}
10 changes: 10 additions & 0 deletions changelog/entries/2026-07-08-physics-replay-inline-viewer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"id": "2026-07-08-physics-replay-inline-viewer",
"version": "0.9.4",
"date": "2026-07-08",
"category": "feat",
"title": "Physics replay in the inline viewer",
"summary": "create_robot_env mounts the chat viewer with a transport bar: play/pause/scrub/speed a gym rollout with kernel-FK-accurate poses, plus live-follow while the agent is still stepping.",
"features": ["mcp", "physics", "viewer", "mcp-apps"],
"mcpTools": ["create_robot_env", "get_sim_replay", "get_sim_version", "get_preview_glb"]
}
10 changes: 10 additions & 0 deletions changelog/entries/2026-07-08-spend-approval-receipt-gates.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"id": "2026-07-08-spend-approval-receipt-gates",
"version": "0.9.4",
"date": "2026-07-08",
"category": "feat",
"title": "Protocol-native spend approval + receipt-gated ordering",
"summary": "authorize_spend carries the approval page to the human via MCP URL elicitation, and place_order fail-closed gates money on doc-hash match and re-verified clearance receipt claims.",
"features": ["mcp", "fabricate", "elicitation", "receipts"],
"mcpTools": ["authorize_spend", "place_order"]
}
391 changes: 391 additions & 0 deletions docs/agent-native-factory.md

Large diffs are not rendered by default.

99 changes: 99 additions & 0 deletions packages/mcp/src/__tests__/deprecated-surface.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/**
* Tripwire for MCP surfaces deprecated by the 2026-07-28 spec RC.
*
* Roots, Sampling, and Logging are deprecated (12-month removal window;
* replacements: tool params / direct LLM APIs / stderr + OpenTelemetry).
* vcad's server has never used any of them — these tests keep it that way
* so an SDK bump or a well-meaning contributor can't quietly adopt a
* surface that dies mid-2027. Elicitation is NOT deprecated — it is the
* blessed replacement pattern and is exercised by the ordering tools.
*/
import { describe, it, expect, beforeAll } from "vitest";
import { readdirSync, readFileSync, statSync } from "node:fs";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { Engine } from "@vcad/engine";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import { createServer } from "../server.js";

let engine: Engine;

beforeAll(async () => {
engine = await Engine.init();
});

async function connect() {
const server = await createServer(engine, { user: null });
const [clientT, serverT] = InMemoryTransport.createLinkedPair();
const client = new Client(
{ name: "test", version: "0.0.0" },
{ capabilities: {} },
);
await Promise.all([client.connect(clientT), server.connect(serverT)]);
return { client, server };
}

describe("deprecated MCP surface tripwire (roots / sampling / logging)", () => {
it("advertises no deprecated capabilities", async () => {
const { client, server } = await connect();
const caps = client.getServerCapabilities() as Record<string, unknown>;
expect(caps).toBeDefined();
for (const key of ["sampling", "roots", "logging"]) {
expect(caps[key], `server must not advertise \`${key}\``).toBeUndefined();
}
await client.close();
await server.close();
});

it("registers no handlers for deprecated methods", async () => {
const { client, server } = await connect();
const handlers = (
server as unknown as { _requestHandlers?: Map<string, unknown> }
)._requestHandlers;
expect(handlers, "SDK internal _requestHandlers map").toBeDefined();
for (const method of [
"logging/setLevel",
"sampling/createMessage",
"roots/list",
]) {
expect(
handlers?.has(method),
`no handler may be registered for \`${method}\``,
).toBe(false);
}
await client.close();
await server.close();
});

it("first-party source never calls the deprecated SDK surface", () => {
// Static sweep of src/ (excluding generated viewer bundles, which
// legitimately contain GLSL "sampling" strings, and this test dir).
const srcRoot = fileURLToPath(new URL("..", import.meta.url));
const offenders: string[] = [];
const banned = [
"sendLoggingMessage",
"logging/setLevel",
"sampling/createMessage",
"listRoots",
"roots/list",
];
const walk = (dir: string): void => {
for (const entry of readdirSync(dir)) {
const p = join(dir, entry);
if (statSync(p).isDirectory()) {
if (entry === "__tests__" || entry === "node_modules") continue;
walk(p);
continue;
}
if (!p.endsWith(".ts") || p.endsWith(".generated.ts")) continue;
const text = readFileSync(p, "utf8");
for (const needle of banned) {
if (text.includes(needle)) offenders.push(`${p}: ${needle}`);
}
}
};
walk(srcRoot);
expect(offenders, offenders.join("\n")).toEqual([]);
});
});
184 changes: 184 additions & 0 deletions packages/mcp/src/__tests__/elicitation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import { describe, it, expect, beforeAll, beforeEach, afterEach } from "vitest";
import { Engine } from "@vcad/engine";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import { ElicitRequestSchema } from "@modelcontextprotocol/sdk/types.js";
import { createServer } from "../server.js";
import { documents } from "../tools/session.js";
import { InMemoryFabricateStore } from "../fabricate/store.js";
import type { Order } from "../fabricate/types.js";

/**
* M3 URL-mode elicitation through the REAL server pipeline: the elicit bridge
* injected by createServer detects the client's `elicitation.url` capability
* at call time and carries the spend-approval page to the human in-band.
* decline ⇒ the authorization is revoked; cancel ⇒ the proposal stands;
* no capability ⇒ the bridge reports unsupported and the out-of-band note is
* returned (the dock is the floor, elicitation is the accelerator).
*
* Anonymous connections use the in-memory fabricate store, whose state is
* module-global — seeding an order under owner "local" from the test makes it
* visible to the server's own store instance.
*/

let engine: Engine;

beforeAll(async () => {
engine = await Engine.init();
});

const OWNER = "local"; // ownerId(null) — anonymous connection scope

function makeOrder(orderId: string): Order {
const now = new Date().toISOString();
return {
order_id: orderId,
document_id: "doc_elicit",
quote_id: `q_${orderId}`,
state: "QUOTED",
fab: "digitalmetal",
fab_order_ref: null,
amount_total_minor: 5000,
currency: "USD",
ship_to: null,
events: [{ state: "QUOTED", at: now, note: "quote" }],
created_at: now,
updated_at: now,
};
}

interface ElicitSeen {
mode?: string;
message?: string;
url?: string;
elicitationId?: string;
}

async function connectWith(
action: "decline" | "cancel" | null,
seen: ElicitSeen[],
) {
const server = await createServer(engine, { user: null });
const [clientT, serverT] = InMemoryTransport.createLinkedPair();
const client = new Client(
{ name: "test", version: "0.0.0" },
// null = a client that never declared the elicitation capability.
{ capabilities: action ? { elicitation: { url: {} } } : {} },
);
if (action) {
client.setRequestHandler(ElicitRequestSchema, async (req) => {
const p = req.params as ElicitSeen;
seen.push({
mode: p.mode,
message: p.message,
url: p.url,
elicitationId: p.elicitationId,
});
return { action };
});
}
await Promise.all([client.connect(clientT), server.connect(serverT)]);
return { client, server };
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const body = (r: unknown): any =>
JSON.parse((r as { content: Array<{ text: string }> }).content[0].text);

describe("authorize_spend URL elicitation (capability-detected, fail-open to out-of-band)", () => {
let prev: string | undefined;
beforeEach(() => {
prev = process.env.VCAD_FABRICATE_ORDERING;
process.env.VCAD_FABRICATE_ORDERING = "1";
documents.clear();
});
afterEach(() => {
if (prev === undefined) delete process.env.VCAD_FABRICATE_ORDERING;
else process.env.VCAD_FABRICATE_ORDERING = prev;
});

it("decline revokes the authorization (nothing can ever be charged against it)", async () => {
const store = new InMemoryFabricateStore();
await store.saveOrder(makeOrder("ord_elicit_decline"), 4000, OWNER);
const seen: ElicitSeen[] = [];
const { client, server } = await connectWith("decline", seen);

const res = await client.callTool({
name: "authorize_spend",
arguments: { order_id: "ord_elicit_decline" },
});
expect(res.isError ?? false).toBe(false);
const out = body(res);
expect(out.status).toBe("revoked");
expect(out.note).toContain("Declined");

// The elicitation was URL-mode with the approval deep link, keyed by the
// authorization id (the completion-notification handle).
expect(seen).toHaveLength(1);
expect(seen[0].mode).toBe("url");
expect(seen[0].url).toBe(`https://vcad.io/authorize/${out.authorization_id}`);
expect(seen[0].elicitationId).toBe(out.authorization_id);
expect(seen[0].message).toContain("$50.00");

// The DB row is the truth: revoked, and place_order refuses it.
expect((await store.getAuthorization(out.authorization_id, OWNER))?.status).toBe(
"revoked",
);
const placed = await client.callTool({
name: "place_order",
arguments: {
order_id: "ord_elicit_decline",
authorization_id: out.authorization_id,
},
});
expect(placed.isError).toBe(true);
expect(body(placed).error).toContain("revoked");

await client.close();
await server.close();
});

it("cancel leaves the proposal pending (a dismissed prompt is not a decision)", async () => {
const store = new InMemoryFabricateStore();
await store.saveOrder(makeOrder("ord_elicit_cancel"), 4000, OWNER);
const seen: ElicitSeen[] = [];
const { client, server } = await connectWith("cancel", seen);

const res = await client.callTool({
name: "authorize_spend",
arguments: { order_id: "ord_elicit_cancel" },
});
const out = body(res);
expect(seen).toHaveLength(1); // the elicitation WAS attempted
expect(out.status).toBe("pending_human");
expect(out.note).toContain("HUMAN must approve");
expect(
(await store.getAuthorization(out.authorization_id, OWNER))?.status,
).toBe("pending_human");

await client.close();
await server.close();
});

it("without the elicitation.url capability the bridge stays quiet (out-of-band note)", async () => {
const store = new InMemoryFabricateStore();
await store.saveOrder(makeOrder("ord_elicit_nocap"), 4000, OWNER);
const seen: ElicitSeen[] = [];
const { client, server } = await connectWith(null, seen);

const res = await client.callTool({
name: "authorize_spend",
arguments: { order_id: "ord_elicit_nocap" },
});
const out = body(res);
expect(seen).toHaveLength(0); // urlSupported() was false — never attempted
expect(out.status).toBe("pending_human");
expect(out.note).toContain("HUMAN must approve");
expect(
(await store.getAuthorization(out.authorization_id, OWNER))?.status,
).toBe("pending_human");

await client.close();
await server.close();
});
});
71 changes: 71 additions & 0 deletions packages/mcp/src/__tests__/kerf-contract.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { describe, it, expect } from "vitest";
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { KERF_JOB_STATES } from "../fabricate/kerf/contract.js";

/**
* Cross-repo contract drift check: the kerf types mirrored into
* `src/fabricate/kerf/contract.ts` must match the source of truth in the kerf
* repo (github.com/ecto/kerf, packages/core/src). Runs only where a kerf
* clone sits at the conventional sibling-ish path (a dev machine); CI has no
* clone and skips — the mirror header documents the same discipline.
*/

const KERF_CORE_SRC = "/Users/cam/Developer/kerf/packages/core/src";
const hasKerfClone = existsSync(KERF_CORE_SRC);

const read = (file: string): string => readFileSync(join(KERF_CORE_SRC, file), "utf8");

/** The mirrored literal vocabularies (from contract.ts — keep in sync there). */
const PRICING_BASIS = ["estimate", "quoted", "binding"] as const;
const ORACLE_IDS = [
"kerf/upload-hash",
"kerf/quote-extraction",
"kerf/intent-audit",
"kerf/confirmation-page",
"kerf/confirmation-email",
"kerf/card-settlement",
"kerf/tracking",
"kerf/canary",
] as const;
const VERDICTS = ["pass", "fail", "unverifiable"] as const;

describe.skipIf(!hasKerfClone)("kerf contract mirror matches the kerf sources", () => {
it("all 17 JobState strings appear verbatim in kerf core/job.ts", () => {
const src = read("job.ts");
expect(KERF_JOB_STATES).toHaveLength(17);
for (const state of KERF_JOB_STATES) {
expect(src, `JobState "${state}" in kerf job.ts`).toContain(`"${state}"`);
}
// And nothing extra on kerf's side: every quoted ALL-CAPS literal in the
// JobState union block is one we mirror.
const union = src.slice(src.indexOf("export type JobState"), src.indexOf("TRANSITIONS"));
const literals = [...union.matchAll(/"([A-Z_]+)"/g)].map((m) => m[1]);
for (const lit of literals) {
expect(
(KERF_JOB_STATES as readonly string[]).includes(lit),
`kerf JobState "${lit}" is mirrored in contract.ts`,
).toBe(true);
}
});

it("PricingBasis members appear verbatim in kerf core/quote.ts", () => {
const src = read("quote.ts");
for (const basis of PRICING_BASIS) {
expect(src, `PricingBasis "${basis}"`).toContain(`"${basis}"`);
}
expect(src).toContain("export type PricingBasis");
});

it("OracleId and Verdict members appear verbatim in kerf core/evidence.ts", () => {
const src = read("evidence.ts");
for (const oracle of ORACLE_IDS) {
expect(src, `OracleId "${oracle}"`).toContain(`"${oracle}"`);
}
for (const verdict of VERDICTS) {
expect(src, `Verdict "${verdict}"`).toContain(`"${verdict}"`);
}
expect(src).toContain("export type OracleId");
expect(src).toContain("export type Verdict");
});
});
Loading
Loading