From 68bd396beb07c49ea4f04d577f023e4cabaa0fd0 Mon Sep 17 00:00:00 2001 From: Fabiano Cruz Date: Thu, 3 Sep 2026 13:32:08 -0300 Subject: [PATCH 1/3] feat(types): publish all 15 meta-tool definitions; conformance sees enums and embedded shapes (ent#933) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Core-side sync of the published contract with the managed runtime, after the ent#932 runtime reconciliation landed: - SHARED_META_TOOL_DEFINITIONS now publishes the full 15-tool surface (was 3: the invoice/notify/pay demo tools). codespar_wallet and codespar_kyc — which had no published definition at all — are included, with property sets, types, and required arrays mirroring the runtime. - codespar_pay aligned with the ent#932 decision: method vocabulary is pix/card/boleto/wire as a structured enum (no sepa/usdc/ted), and the recipient property publishes the runtime's dual string-or-bank-account- object capability. Managed-only extras (boleto_quote, DICT lifecycle, expected_amount_minor) stay out of the shared baseline by design. - Closed vocabularies now live in structured `enum` arrays (and embedded shapes in nested `properties`), mirrored into contract.enums, so a conformance test compares them structurally instead of trusting prose. - New meta-tool-definition-conformance.ts: definitionViolations() checks contract/schema drift, enum well-formedness, enum-vs-prose agreement, and ghost rails (sepa/ted banned everywhere; usdc only enum-owned or in a codespar_crypto_pay redirect sentence). sharedDefinitionConformanceReasons() is the cross-runtime comparator: the historical structural check plus enum honoring (extend, never shrink), embedded-shape matching, and a retired-rail sweep of runtime prose. Tripwire tests prove each check fails on the exact drift the ent#933 audit found, with positive controls for the allowed cases. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01BtvPxidmzsA22yoVJSWdba --- packages/types/src/index.ts | 1 + .../src/meta-tool-definition-conformance.ts | 365 +++++++++++++ .../types/src/meta-tool-definitions.test.ts | 338 +++++++++++- packages/types/src/meta-tool-definitions.ts | 514 ++++++++++++++++-- 4 files changed, 1149 insertions(+), 69 deletions(-) create mode 100644 packages/types/src/meta-tool-definition-conformance.ts diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 38c0cbe..cf10148 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -2,3 +2,4 @@ export * from "./types.js"; export * from "./guards.js"; export * from "./meta-tool-contract.js"; export * from "./meta-tool-definitions.js"; +export * from "./meta-tool-definition-conformance.js"; diff --git a/packages/types/src/meta-tool-definition-conformance.ts b/packages/types/src/meta-tool-definition-conformance.ts new file mode 100644 index 0000000..33cda34 --- /dev/null +++ b/packages/types/src/meta-tool-definition-conformance.ts @@ -0,0 +1,365 @@ +/* ── Definition conformance: schema, enums, and embedded shapes ── + * + * The checks that keep a published SharedMetaToolDefinition honest, and the + * comparator a runtime's conformance test uses to hold its agent-facing + * tools to the shared contract. + * + * Why prose is in scope now (ent#933): the previous conformance check + * compared only property names, per-property type, and the required set — + * "structural, not prose" — while the actual vocabularies (which rails + * codespar_pay takes, which actions a tool accepts) lived ONLY in + * description prose. The check stayed green across a triple drift: the + * published contract advertised rails with no route (sepa, usdc), missed + * capabilities the runtime had (recipient-as-object), and covered 3 of 15 + * tools. The fix is two-sided: vocabularies move into structured `enum` + * arrays where a test can see them (meta-tool-definitions.ts), and the + * checks below assert BOTH the structured schema AND the enums/embedded + * shapes — including that prose and schema agree, so a vocabulary can no + * longer drift in a description string alone. + * ─────────────────────────────────────────────────────────────── */ + +import type { + MetaToolInputProperty, + SharedMetaToolDefinition, +} from "./meta-tool-definitions.js"; + +/** A single definition-level violation: which check failed and why. */ +export interface DefinitionViolation { + code: "contract-drift" | "enum-shape" | "enum-prose" | "ghost-rail"; + detail: string; +} + +/** + * Rails that route NOWHERE on any tool: retired from the published surface + * (ent#932 — sepa never had a catalog row; ted is unpublished until the + * public TED route ships). They must not appear, as a word, in ANY prose a + * definition publishes. Adding a rail back means deleting it from this list + * in the same change that ships the route — the two cannot drift apart. + */ +export const RETIRED_RAIL_TOKENS = ["sepa", "ted"] as const; + +/** + * "usdc" IS routable — via codespar_crypto_pay. Published prose may name it + * only (a) in a definition that declares it in a structured `enum` (the + * crypto tool owns the rail), or (b) inside a sentence that names the + * redirect target, so an agent reading the prose is pointed at the tool + * that actually routes it. + */ +const REDIRECTED_RAIL_TOKEN = "usdc"; +const REDIRECT_TARGET_TOOL = "codespar_crypto_pay"; + +/** The minimal agent-facing tool shape a runtime exposes for comparison. */ +export interface AgentFacingToolShape { + name: string; + description?: string; + input_schema: { + type: string; + properties: Record; + required?: readonly string[]; + }; +} + +/** Every prose surface a definition publishes: [where, text] pairs. */ +export function proseSurfaces(def: { + name: string; + description?: string; + input_schema: { properties: Record }; +}): Array<[string, string]> { + const surfaces: Array<[string, string]> = [ + ["tool description", String(def.description ?? "")], + ]; + const walk = (props: Record, prefix: string): void => { + for (const [name, raw] of Object.entries(props)) { + const prop = raw as MetaToolInputProperty | undefined; + if (!prop || typeof prop !== "object") continue; + surfaces.push([ + `property "${prefix}${name}" description`, + String(prop.description ?? ""), + ]); + if (prop.properties) walk(prop.properties, `${prefix}${name}.`); + } + }; + walk(def.input_schema.properties, ""); + return surfaces; +} + +/** All structured enum declarations in a schema, keyed by property path. */ +function enumsByPath( + props: Record, + prefix = "", +): Map { + const out = new Map(); + for (const [name, prop] of Object.entries(props)) { + if (prop.enum) { + out.set(`${prefix}${name}`, { + values: prop.enum, + description: String(prop.description ?? ""), + }); + } + if (prop.properties) { + for (const [path, v] of enumsByPath(prop.properties, `${prefix}${name}.`)) { + out.set(path, v); + } + } + } + return out; +} + +/** True when any structured enum in the definition carries `token`. */ +function definitionOwnsToken(def: SharedMetaToolDefinition, token: string): boolean { + for (const [, { values }] of enumsByPath(def.input_schema.properties)) { + if (values.some((v) => v.toLowerCase() === token)) return true; + } + return false; +} + +function wordRegex(token: string): RegExp { + return new RegExp(`\\b${token}\\b`, "i"); +} + +/** + * Sweep one prose surface for ghost rails. `ownsRedirectedToken` marks a + * definition that publishes the redirected token in a structured enum (the + * tool that owns the rail), which exempts its prose. + */ +function ghostRailViolations( + toolName: string, + where: string, + text: string, + ownsRedirectedToken: boolean, +): DefinitionViolation[] { + const violations: DefinitionViolation[] = []; + for (const token of RETIRED_RAIL_TOKENS) { + if (wordRegex(token).test(text)) { + violations.push({ + code: "ghost-rail", + detail: `${toolName}: "${token}" advertised in the ${where} but routes nowhere`, + }); + } + } + if (!ownsRedirectedToken) { + for (const sentence of text.split(/(?<=\.)\s+/)) { + if (wordRegex(REDIRECTED_RAIL_TOKEN).test(sentence) && !sentence.includes(REDIRECT_TARGET_TOOL)) { + violations.push({ + code: "ghost-rail", + detail: `${toolName}: "${REDIRECTED_RAIL_TOKEN}" appears in the ${where} outside a ${REDIRECT_TARGET_TOOL} redirect`, + }); + } + } + } + return violations; +} + +/** + * Verify a published definition's internal integrity: the derived contract + * matches the schema (properties, required, enums), every declared enum is + * well-formed, every enum value is visible in the property's prose, and no + * prose surface advertises a ghost rail. + * + * Returns the violations found (empty array = the definition is coherent). + */ +export function definitionViolations(def: SharedMetaToolDefinition): DefinitionViolation[] { + const violations: DefinitionViolation[] = []; + + // 1. Contract ↔ schema drift (top-level surface). + const schemaProps = Object.keys(def.input_schema.properties).sort(); + const contractProps = [...def.contract.properties].sort(); + if (JSON.stringify(schemaProps) !== JSON.stringify(contractProps)) { + violations.push({ + code: "contract-drift", + detail: `${def.name}: contract.properties [${contractProps}] != schema properties [${schemaProps}]`, + }); + } + const schemaReq = [...(def.input_schema.required ?? [])].sort(); + const contractReq = [...def.contract.required].sort(); + if (JSON.stringify(schemaReq) !== JSON.stringify(contractReq)) { + violations.push({ + code: "contract-drift", + detail: `${def.name}: contract.required [${contractReq}] != schema required [${schemaReq}]`, + }); + } + for (const r of def.contract.required) { + if (!def.contract.properties.includes(r)) { + violations.push({ + code: "contract-drift", + detail: `${def.name}: required "${r}" is not an advertised property`, + }); + } + } + + // 2. Contract ↔ schema drift (vocabularies). Only TOP-LEVEL enums are + // mirrored into contract.enums (nested ones stay pinned via the schema + // itself); a top-level schema enum missing from the contract — or a + // contract vocabulary no schema property declares — is drift. + const contractEnums = def.contract.enums ?? {}; + for (const [name, prop] of Object.entries(def.input_schema.properties)) { + if (prop.enum) { + const mirrored = contractEnums[name]; + if (!mirrored || JSON.stringify([...mirrored]) !== JSON.stringify([...prop.enum])) { + violations.push({ + code: "contract-drift", + detail: `${def.name}: schema enum on "${name}" is not mirrored in contract.enums`, + }); + } + } + } + for (const name of Object.keys(contractEnums)) { + if (!def.input_schema.properties[name]?.enum) { + violations.push({ + code: "contract-drift", + detail: `${def.name}: contract.enums["${name}"] has no schema enum backing it`, + }); + } + } + + // 3. Enum well-formedness + enum ↔ prose agreement (all levels). + for (const [path, { values, description }] of enumsByPath(def.input_schema.properties)) { + if (values.length === 0) { + violations.push({ + code: "enum-shape", + detail: `${def.name}: enum on "${path}" is empty`, + }); + continue; + } + if (new Set(values.map((v) => v.toLowerCase())).size !== values.length) { + violations.push({ + code: "enum-shape", + detail: `${def.name}: enum on "${path}" has duplicate values`, + }); + } + for (const value of values) { + if (typeof value !== "string" || value.length === 0) { + violations.push({ + code: "enum-shape", + detail: `${def.name}: enum on "${path}" has a non-string or empty value`, + }); + continue; + } + if (!description.toLowerCase().includes(value.toLowerCase())) { + violations.push({ + code: "enum-prose", + detail: `${def.name}: enum value "${value}" on "${path}" is invisible in the property description — schema and prose disagree`, + }); + } + } + } + + // 4. Ghost rails in any published prose surface. + const owns = definitionOwnsToken(def, REDIRECTED_RAIL_TOKEN); + for (const [where, text] of proseSurfaces(def)) { + violations.push(...ghostRailViolations(def.name, where, text, owns)); + } + + return violations; +} + +/** + * Compare a runtime's agent-facing tool against a shared definition — the + * cross-runtime half of the conformance surface. Extends the historical + * structural check (name, property presence, per-property type, required + * set, allowlisted extras) with the ent#933 hardening: + * + * - a shared structured enum must be honored: when the runtime property + * declares its own enum it must contain every shared value (a runtime may + * EXTEND a vocabulary the way it may add allowlisted properties — it may + * never shrink one), and the runtime property's prose must mention every + * shared value (a vocabulary item cannot be silently hidden); + * - a shared embedded shape (nested `properties`) must be present with + * matching per-property types; + * - no runtime prose surface may advertise a retired rail. + * + * `allowedExtras` names the ONLY properties the runtime may publish beyond + * the shared contract — every entry is a claim that the runtime-side + * mechanism exists. + */ +export function sharedDefinitionConformanceReasons( + tool: AgentFacingToolShape, + shared: SharedMetaToolDefinition, + allowedExtras: ReadonlySet = new Set(), +): string[] { + const reasons: string[] = []; + if (tool.name !== shared.name) { + reasons.push(`name "${tool.name}" != "${shared.name}"`); + } + + const toolProps = new Set(Object.keys(tool.input_schema.properties)); + const sharedProps = [...shared.contract.properties].sort(); + const missing = sharedProps.filter((p) => !toolProps.has(p)); + if (missing.length > 0) { + reasons.push(`missing shared properties [${missing}]`); + } + const sharedSet = new Set(sharedProps); + const unexpected = [...toolProps] + .filter((p) => !sharedSet.has(p) && !allowedExtras.has(p)) + .sort(); + if (unexpected.length > 0) { + reasons.push(`unexpected extra properties [${unexpected}] (not allowlisted)`); + } + + const asProp = (p: unknown): MetaToolInputProperty | undefined => + p && typeof p === "object" ? (p as MetaToolInputProperty) : undefined; + + for (const k of sharedProps) { + const toolProp = asProp(tool.input_schema.properties[k]); + const sharedProp = shared.input_schema.properties[k]; + if (!toolProp || !sharedProp) continue; // presence already reported + if (toolProp.type !== sharedProp.type) { + reasons.push( + `property "${k}" type ${JSON.stringify(toolProp.type)} != ${JSON.stringify(sharedProp.type)}`, + ); + } + // Shared vocabulary must be honored — structurally when the runtime + // declares one, and in the agent-visible prose always. + if (sharedProp.enum) { + if (toolProp.enum) { + const toolValues = new Set(toolProp.enum.map((v) => String(v).toLowerCase())); + const dropped = sharedProp.enum.filter((v) => !toolValues.has(v.toLowerCase())); + if (dropped.length > 0) { + reasons.push(`property "${k}" enum drops shared values [${dropped}]`); + } + } + const prose = String(toolProp.description ?? "").toLowerCase(); + for (const value of sharedProp.enum) { + if (!prose.includes(value.toLowerCase())) { + reasons.push( + `property "${k}" prose hides the shared vocabulary value "${value}"`, + ); + } + } + } + // Shared embedded shape must be present with matching types. + if (sharedProp.properties) { + const nested = toolProp.properties ?? {}; + for (const [name, sharedNested] of Object.entries(sharedProp.properties)) { + const toolNested = asProp(nested[name]); + if (!toolNested) { + reasons.push(`property "${k}" is missing the embedded property "${name}"`); + } else if (toolNested.type !== sharedNested.type) { + reasons.push( + `embedded property "${k}.${name}" type ${JSON.stringify(toolNested.type)} != ${JSON.stringify(sharedNested.type)}`, + ); + } + } + } + } + + const toolReq = [...(tool.input_schema.required ?? [])].sort(); + const sharedReq = [...shared.contract.required].sort(); + if (JSON.stringify(toolReq) !== JSON.stringify(sharedReq)) { + reasons.push(`required [${toolReq}] != [${sharedReq}]`); + } + + // Retired rails must not be advertised in the runtime's prose either — + // this is the sweep that would have flagged the pre-#932 TED + // advertisement living in the runtime's `recipient` description while the + // published contract knew nothing about it. + for (const [where, text] of proseSurfaces(tool)) { + for (const token of RETIRED_RAIL_TOKENS) { + if (wordRegex(token).test(text)) { + reasons.push(`"${token}" advertised in the runtime ${where} but routes nowhere`); + } + } + } + + return reasons; +} diff --git a/packages/types/src/meta-tool-definitions.test.ts b/packages/types/src/meta-tool-definitions.test.ts index bb38843..80c0777 100644 --- a/packages/types/src/meta-tool-definitions.test.ts +++ b/packages/types/src/meta-tool-definitions.test.ts @@ -1,40 +1,340 @@ import { describe, it, expect } from "vitest"; import { SHARED_META_TOOL_DEFINITIONS, + contractOf, + type MetaToolInputSchema, type SharedMetaToolDefinition, } from "./meta-tool-definitions.js"; +import { + definitionViolations, + sharedDefinitionConformanceReasons, + type AgentFacingToolShape, +} from "./meta-tool-definition-conformance.js"; const ALL = Object.values(SHARED_META_TOOL_DEFINITIONS) as SharedMetaToolDefinition[]; +/** Tools whose input schema is an empty object by design (no arguments). */ +const NO_ARG_TOOLS = new Set(["codespar_get_started"]); + +function defWith(overrides: { + name?: string; + description?: string; + input_schema: MetaToolInputSchema; + contract?: SharedMetaToolDefinition["contract"]; +}): SharedMetaToolDefinition { + return { + name: overrides.name ?? "codespar_fake", + description: overrides.description ?? "A fabricated definition for tripwire tests.", + input_schema: overrides.input_schema, + contract: overrides.contract ?? contractOf(overrides.input_schema), + }; +} + describe("shared meta-tool definitions", () => { - it("publishes the three demo tools keyed by wire name", () => { + it("publishes ALL fifteen meta-tools keyed by wire name (ent#933: was 3 of 15)", () => { expect(Object.keys(SHARED_META_TOOL_DEFINITIONS).sort()).toEqual([ + "codespar_charge", + "codespar_checkout", + "codespar_crypto_pay", + "codespar_discover", + "codespar_get_started", "codespar_invoice", + "codespar_issue", + "codespar_kyc", + "codespar_ledger", + "codespar_manage_connections", "codespar_notify", "codespar_pay", + "codespar_ship", + "codespar_shop", + "codespar_wallet", ]); }); - it.each(ALL)("$name carries name, description, input_schema, and contract — all non-empty", (def) => { - expect(def.name).toMatch(/^codespar_[a-z]+$/); + it.each(ALL)("$name carries name, description, input_schema, and contract — all well-formed", (def) => { + expect(def.name).toMatch(/^codespar_[a-z][a-z_]*$/); expect(def.description.length).toBeGreaterThan(0); expect(def.input_schema.type).toBe("object"); - expect(Object.keys(def.input_schema.properties).length).toBeGreaterThan(0); - // contract descriptor is non-empty and derived from the schema - expect(def.contract.properties.length).toBeGreaterThan(0); - expect(def.contract.required.length).toBeGreaterThan(0); - }); - - it.each(ALL)("$name contract matches its input_schema (no drift)", (def) => { - expect([...def.contract.properties].sort()).toEqual( - Object.keys(def.input_schema.properties).sort(), - ); - expect([...def.contract.required].sort()).toEqual( - [...(def.input_schema.required ?? [])].sort(), - ); - // every required field is an advertised property - for (const r of def.contract.required) { - expect(def.contract.properties).toContain(r); + if (!NO_ARG_TOOLS.has(def.name)) { + expect(Object.keys(def.input_schema.properties).length).toBeGreaterThan(0); + expect(def.contract.properties.length).toBeGreaterThan(0); } + // the map key is the wire name + expect(SHARED_META_TOOL_DEFINITIONS[def.name as keyof typeof SHARED_META_TOOL_DEFINITIONS]).toBe(def); + }); + + it.each(ALL)( + "$name passes the full definition check: contract matches schema, enums are visible in prose, no ghost rails", + (def) => { + const violations = definitionViolations(def); + expect(violations.map((v) => `[${v.code}] ${v.detail}`).join("; ")).toBe(""); + }, + ); + + it("codespar_pay publishes the ent#932 vocabulary: pix/card/boleto/wire, no sepa/usdc/ted", () => { + const pay = SHARED_META_TOOL_DEFINITIONS.codespar_pay; + // The rail vocabulary is a STRUCTURED enum, not prose — pinned exactly. + expect(pay.input_schema.properties.method!.enum).toEqual(["pix", "card", "boleto", "wire"]); + expect(pay.contract.enums?.method).toEqual(["pix", "card", "boleto", "wire"]); + // The shared baseline action vocabulary (managed-only extras like + // boleto_quote and the DICT lifecycle are deliberately NOT here). + expect(pay.input_schema.properties.action!.enum).toEqual(["pay", "status"]); + expect(pay.input_schema.required).toEqual(["action"]); + // The shared property surface, pinned: adding or dropping one is a + // contract change, not a drive-by. + expect(Object.keys(pay.input_schema.properties)).toEqual([ + "action", + "amount", + "currency", + "country", + "method", + "recipient", + "copia_e_cola", + "consumer_id", + "checkout_session_id", + "description", + "mandateId", + "payment_id", + "linha_digitavel", + ]); + }); + + it("codespar_pay publishes the recipient-as-object capability (ent#933 drift 1)", () => { + // The runtime accepts `recipient` as a bank-account OBJECT (manual Pix + // cash-out to a destination with no registered key). The published + // definition must say so — this pin keeps the capability from silently + // falling back out of the contract. + const recipient = SHARED_META_TOOL_DEFINITIONS.codespar_pay.input_schema.properties.recipient!; + expect(recipient.description).toMatch(/object with bank-account details/i); + expect(recipient.description).toMatch(/\{bank, account, branch, tax_id, name, account_type\?\}/); + }); + + it("codespar_wallet and codespar_kyc have published definitions with their vocabularies (ent#933 drift 3)", () => { + // The two tools the audit named as having NO published definition at all. + const wallet = SHARED_META_TOOL_DEFINITIONS.codespar_wallet; + expect(wallet.input_schema.properties.action!.enum).toEqual(["balance", "statement", "receive"]); + expect(wallet.input_schema.required).toEqual(["action"]); + + const kyc = SHARED_META_TOOL_DEFINITIONS.codespar_kyc; + expect(kyc.input_schema.properties.check_type!.enum).toEqual([ + "identity", + "document", + "risk-score", + "sanctions", + "onboarding", + "onboarding-business", + "status", + ]); + expect(kyc.input_schema.required).toEqual(["buyer", "check_type"]); + }); +}); + +describe("definitionViolations tripwires (the checks must FAIL on the drift they claim to catch)", () => { + it("prose advertising a retired rail fails, even though the structural surface is clean", () => { + const def = defWith({ + input_schema: { + type: "object", + properties: { + method: { type: "string", description: "Payment method: pix, sepa", enum: ["pix"] }, + }, + required: ["method"], + }, + }); + const violations = definitionViolations(def); + expect(violations.some((v) => v.code === "ghost-rail" && v.detail.includes('"sepa"'))).toBe(true); + }); + + it("the pre-#932 published pay prose (usdc + sepa rails) would have been flagged", () => { + // Regression pin: this is the EXACT drift the audit found — the old + // published codespar_pay advertised rails in prose that routed nowhere, + // and the old conformance check ('structural, not prose') stayed green. + const def = defWith({ + name: "codespar_pay", + input_schema: { + type: "object", + properties: { + method: { + type: "string", + description: + "Payment method: pix, card, usdc, boleto, sepa, wire. method=boleto pays/settles an EXISTING boleto (provide linha_digitavel); it does not issue new boleto charges.", + }, + }, + }, + }); + const violations = definitionViolations(def); + expect(violations.some((v) => v.detail.includes('"sepa"'))).toBe(true); + expect(violations.some((v) => v.detail.includes('"usdc"'))).toBe(true); + }); + + it("usdc inside a codespar_crypto_pay redirect sentence passes (positive control)", () => { + const def = defWith({ + input_schema: { + type: "object", + properties: { + method: { + type: "string", + description: "Payment method: pix. For USDC or any on-chain settlement use codespar_crypto_pay.", + enum: ["pix"], + }, + }, + }, + }); + expect(definitionViolations(def)).toEqual([]); + }); + + it("usdc owned by a structured enum passes (positive control: the crypto tool names its own rail)", () => { + const def = defWith({ + input_schema: { + type: "object", + properties: { + currency: { type: "string", description: "Crypto currency code: USDC, USDT", enum: ["USDC", "USDT"] }, + }, + }, + }); + expect(definitionViolations(def)).toEqual([]); + }); + + it("an enum value invisible in the property's prose fails (schema and prose must agree)", () => { + const def = defWith({ + input_schema: { + type: "object", + properties: { + action: { type: "string", description: "balance | statement", enum: ["balance", "statement", "receive"] }, + }, + }, + }); + const violations = definitionViolations(def); + expect(violations.some((v) => v.code === "enum-prose" && v.detail.includes('"receive"'))).toBe(true); + }); + + it("a hand-drifted contract fails: schema enum not mirrored, phantom property, phantom vocabulary", () => { + const schema: MetaToolInputSchema = { + type: "object", + properties: { + action: { type: "string", description: "a | b", enum: ["a", "b"] }, + }, + required: ["action"], + }; + const def = defWith({ + input_schema: schema, + contract: { + properties: ["action", "phantom"], + required: ["action"], + enums: { phantom: ["x"] }, + }, + }); + const violations = definitionViolations(def).filter((v) => v.code === "contract-drift"); + expect(violations.some((v) => v.detail.includes("contract.properties"))).toBe(true); + expect(violations.some((v) => v.detail.includes('schema enum on "action" is not mirrored'))).toBe(true); + expect(violations.some((v) => v.detail.includes('contract.enums["phantom"]'))).toBe(true); + }); +}); + +describe("sharedDefinitionConformanceReasons (the cross-runtime comparator sees enums and embedded shapes)", () => { + const shared = SHARED_META_TOOL_DEFINITIONS.codespar_pay; + + /** A runtime tool that mirrors the shared definition exactly. */ + function conformingTool(): AgentFacingToolShape { + return { + name: shared.name, + description: shared.description, + input_schema: { + type: "object", + properties: JSON.parse(JSON.stringify(shared.input_schema.properties)) as Record, + required: [...(shared.input_schema.required ?? [])], + }, + }; + } + + it("a mirroring runtime tool passes with zero reasons (positive control)", () => { + expect(sharedDefinitionConformanceReasons(conformingTool(), shared)).toEqual([]); + }); + + it("an allowlisted extra property passes; an unlisted one fails", () => { + const tool = conformingTool(); + tool.input_schema.properties.expected_amount_minor = { type: "number", description: "managed-only" }; + expect( + sharedDefinitionConformanceReasons(tool, shared, new Set(["expected_amount_minor"])), + ).toEqual([]); + expect( + sharedDefinitionConformanceReasons(tool, shared).some((r) => + r.includes("expected_amount_minor"), + ), + ).toBe(true); + }); + + it("a missing shared property and a divergent property type fail", () => { + const missing = conformingTool(); + delete (missing.input_schema.properties as Record).recipient; + expect( + sharedDefinitionConformanceReasons(missing, shared).some((r) => r.includes("missing shared properties [recipient]")), + ).toBe(true); + + const flipped = conformingTool(); + (flipped.input_schema.properties.amount as { type: string }).type = "string"; + expect( + sharedDefinitionConformanceReasons(flipped, shared).some((r) => r.includes('property "amount" type')), + ).toBe(true); + }); + + it("a runtime enum that DROPS a shared vocabulary value fails (ent#933: vocabularies are compared, not prose-trusted)", () => { + const tool = conformingTool(); + (tool.input_schema.properties.method as { enum: string[] }).enum = ["pix", "card", "boleto"]; // drops wire + expect( + sharedDefinitionConformanceReasons(tool, shared).some((r) => + r.includes('property "method" enum drops shared values [wire]'), + ), + ).toBe(true); + }); + + it("a runtime enum that EXTENDS the shared vocabulary passes (managed runtimes may extend, never shrink)", () => { + const tool = conformingTool(); + const action = tool.input_schema.properties.action as { enum: string[]; description: string }; + action.enum = [...action.enum, "boleto_quote"]; + action.description += " | boleto_quote (managed-only)"; + expect(sharedDefinitionConformanceReasons(tool, shared)).toEqual([]); + }); + + it("runtime prose that HIDES a shared vocabulary value fails", () => { + const tool = conformingTool(); + const method = tool.input_schema.properties.method as { description: string }; + method.description = "Payment method: pix, card, boleto."; // prose hides wire + expect( + sharedDefinitionConformanceReasons(tool, shared).some((r) => + r.includes('property "method" prose hides the shared vocabulary value "wire"'), + ), + ).toBe(true); + }); + + it("runtime prose advertising a retired rail fails — the pre-#932 TED-in-recipient drift is now caught", () => { + const tool = conformingTool(); + const recipient = tool.input_schema.properties.recipient as { description: string }; + recipient.description += " Also accepts a TED bank transfer destination."; + expect( + sharedDefinitionConformanceReasons(tool, shared).some((r) => r.includes('"ted"')), + ).toBe(true); + }); + + it("a shared embedded shape must be honored: missing or type-divergent nested property fails", () => { + const cryptoShared = SHARED_META_TOOL_DEFINITIONS.codespar_crypto_pay; + const tool: AgentFacingToolShape = { + name: cryptoShared.name, + input_schema: { + type: "object", + properties: JSON.parse(JSON.stringify(cryptoShared.input_schema.properties)) as Record, + required: [...(cryptoShared.input_schema.required ?? [])], + }, + }; + expect(sharedDefinitionConformanceReasons(tool, cryptoShared)).toEqual([]); + + const counterparty = tool.input_schema.properties.counterparty as { + properties?: Record; + }; + delete counterparty.properties; + expect( + sharedDefinitionConformanceReasons(tool, cryptoShared).some((r) => + r.includes('property "counterparty" is missing the embedded property "country"'), + ), + ).toBe(true); }); }); diff --git a/packages/types/src/meta-tool-definitions.ts b/packages/types/src/meta-tool-definitions.ts index ecd823a..154c548 100644 --- a/packages/types/src/meta-tool-definitions.ts +++ b/packages/types/src/meta-tool-definitions.ts @@ -7,10 +7,27 @@ * An implementation registers behind a definition (e.g. on the OSS runtime * via a `MetaToolHook`); implementations differ, but the definition the agent * reasons over does not. - * The `contract` field carries the conformance surface — the property names - * and the required subset a conforming implementation must expose — so a - * conformance test can assert any runtime's tool matches this definition - * without comparing prose. + * The `contract` field carries the conformance surface — the property names, + * the required subset, and the closed value vocabularies (`enums`) a + * conforming implementation must expose — so a conformance test can assert + * any runtime's tool matches this definition without comparing prose. + * + * Closed vocabularies (a rail list, an action set, a channel list) are + * declared as structured `enum` arrays on the property, NEVER only in the + * description prose. Prose-only vocabularies are exactly how the ent#933 + * triple drift happened: the published rails lived in a description string, + * the structural conformance check deliberately skipped prose, and the + * published contract silently advertised rails (sepa, usdc) that routed + * nowhere while missing capabilities the runtime had. The checker in + * `meta-tool-definition-conformance.ts` enforces that schema enums and + * description prose agree, and that no retired rail is advertised anywhere. + * + * The definitions here are the SHARED BASELINE contract: a runtime may + * publish additional, explicitly-allowlisted properties (and extend an + * action vocabulary) for capabilities only it has — the managed runtime's + * DICT claim lifecycle on codespar_pay is the canonical example (ent#932) — + * but it may never drop a shared property, change a shared property's type, + * or diverge on the required set. * * Definitions are data, not code: they carry no routing and import nothing * runtime-specific, so they serialize cleanly and stay portable. @@ -22,6 +39,22 @@ export interface MetaToolInputProperty { type: string; /** Human-readable description shown to the agent. */ description?: string; + /** + * Closed value vocabulary for this property. Declared structurally so a + * conformance test can compare vocabularies without parsing prose — the + * ent#933 fix. When present, every value must also appear in + * `description`, so the agent-visible prose and the machine-checked + * vocabulary cannot drift apart. + */ + enum?: readonly string[]; + /** + * Embedded object shape: the nested properties of an `object`-typed + * input, when the contract pins them (e.g. codespar_crypto_pay's + * `counterparty.country`). Published structurally for the same reason as + * `enum` — an embedded form that lives only in prose is invisible to a + * conformance test. + */ + properties?: Record; } /** The JSON-Schema-shaped input contract an agent-facing meta-tool advertises. */ @@ -34,16 +67,23 @@ export interface MetaToolInputSchema { /** * The conformance surface of a definition: the property names a conforming - * implementation must expose and the subset that is required. A conformance - * test compares a live runtime's tool against this — structural, not prose — - * so an implementation can be checked to present the same agent-facing tool as - * this shared definition. + * implementation must expose, the subset that is required, and the closed + * value vocabularies. A conformance test compares a live runtime's tool + * against this — structural, not prose — so an implementation can be checked + * to present the same agent-facing tool as this shared definition. */ export interface MetaToolConformanceContract { /** Every property name the agent-facing tool exposes. */ properties: readonly string[]; /** The subset of `properties` that is required. */ required: readonly string[]; + /** + * Closed value vocabularies, keyed by property name — derived from the + * schema's structured `enum` declarations. A conformance test reads the + * vocabulary HERE (not from prose), so a rail or action published only in + * a description string is a contract violation, not an invisible drift. + */ + enums?: Readonly>; } /** A shared, runtime-agnostic agent-facing meta-tool definition. */ @@ -54,30 +94,223 @@ export interface SharedMetaToolDefinition { description: string; /** The input schema the agent is shown. */ input_schema: MetaToolInputSchema; - /** The conformance surface (property + required names). */ + /** The conformance surface (property + required names + vocabularies). */ contract: MetaToolConformanceContract; } /** - * Derive the conformance contract from an input schema, so the property and - * required sets never drift from the schema they describe. + * Derive the conformance contract from an input schema, so the property, + * required, and vocabulary sets never drift from the schema they describe. */ -function contractOf(schema: MetaToolInputSchema): MetaToolConformanceContract { +export function contractOf(schema: MetaToolInputSchema): MetaToolConformanceContract { + const enums: Record = {}; + for (const [name, prop] of Object.entries(schema.properties)) { + if (prop.enum) enums[name] = [...prop.enum]; + } return { properties: Object.keys(schema.properties), required: [...(schema.required ?? [])], + ...(Object.keys(enums).length > 0 ? { enums } : {}), }; } +/* ── Input schemas ─────────────────────────────────────────────── */ + +const DISCOVER_INPUT: MetaToolInputSchema = { + type: "object", + properties: { + use_case: { type: "string", description: "Free-form description of what you want to accomplish (e.g. 'send an email', 'create a Pix payment')" }, + category: { type: "string", description: "Optional category filter" }, + country: { type: "string", description: "ISO-3166-1 alpha-2 country code or '*' for any" }, + limit: { type: "number", description: "Max related tools returned (1..20, default 5)" }, + }, + required: ["use_case"], +}; + +// A no-argument tool: the schema is an empty object by design (the runtime +// derives everything from the authenticated session). +const GET_STARTED_INPUT: MetaToolInputSchema = { + type: "object", + properties: {}, +}; + +const MANAGE_CONNECTIONS_INPUT: MetaToolInputSchema = { + type: "object", + properties: { + action: { + type: "string", + description: + "list | status | initiate (dashboard providers) · connect_start | connect_finish (login-walled stores: meli, ifood) · save_profile | get_profile (vaulted shopper identity for guest-checkout stores)", + enum: ["list", "status", "initiate", "connect_start", "connect_finish", "save_profile", "get_profile"], + }, + server_id: { type: "string", description: "Provider/store id — required for status, initiate, connect_start, connect_finish (e.g. asaas, nfe-io, meli, ifood)" }, + country: { type: "string", description: "ISO-3166-1 alpha-2 filter (list only)" }, + environment: { type: "string", description: "live | test (default: live)", enum: ["live", "test"] }, + return_to: { type: "string", description: "Path inside the dashboard to redirect to after the user finishes connecting (initiate only)" }, + session_id: { type: "string", description: "From connect_start — pass it back to connect_finish (login-walled stores)" }, + context_id: { type: "string", description: "From connect_start — pass it back to connect_finish to persist the buyer's login (login-walled stores)" }, + consumer_id: { type: "string", description: "Which buyer is connecting / whose profile to save. Defaults to the session's user id." }, + profile: { + type: "object", + description: + "The buyer's vaulted checkout identity (action=save_profile). Shape: { buyer: { firstName, lastName, email, document (CPF), phone }, address: { postalCode, street, number, neighborhood, city, state, complement } }. Stored encrypted; codespar_shop checkout auto-fills it.", + }, + }, +}; + +const CHECKOUT_INPUT: MetaToolInputSchema = { + type: "object", + properties: { + items: { + type: "array", + description: + "Items to purchase. Each item: { title?, price (major units, e.g. 125.5), quantity? (default 1) }. The total is the sum of price x quantity unless a top-level amount is passed.", + }, + amount: { + type: "number", + description: + "Optional explicit cart total in MAJOR currency units — wins over the items sum when provided.", + }, + paymentMethod: { + type: "string", + description: "Payment method (rail): pix (default) | boleto | card. For usdc, call codespar_crypto_pay.", + enum: ["pix", "boleto", "card"], + }, + currency: { type: "string", description: "Currency code (default BRL)" }, + description: { + type: "string", + description: "Optional charge description shown to the shopper; defaults to a summary of the items.", + }, + buyer: { + type: "object", + description: + "Optional shopper details { name, email?, document?, phone? }; defaults to a guest checkout.", + }, + metadata: { + type: "object", + description: + "Optional provider metadata (e.g. customer_id for PSPs that require a pre-created customer).", + }, + recipient: { type: "string", description: "Recipient identifier" }, + }, + required: ["items"], +}; + +const PAY_INPUT: MetaToolInputSchema = { + type: "object", + properties: { + action: { + type: "string", + description: + "pay (execute a payment/transfer) | status (read an existing payment/charge/boleto's status by id). Required — pass it explicitly on every call.", + enum: ["pay", "status"], + }, + amount: { type: "number", description: "Amount to pay, in minor units (centavos for BRL). Must match the copia-e-cola's amount when paying a QR. Required for action=pay." }, + currency: { type: "string", description: "Currency code (BRL, USD, EUR). Required for action=pay." }, + country: { type: "string", description: "ISO-3166-1 alpha-2 country code for the eligibility rail (BR, US, MX, AR, CL, CO, INTL). Defaults to BR. Set to US for cross-border USD card via ACP, INTL for hosted-checkout flows." }, + method: { + type: "string", + description: + "Payment method: pix, card, boleto, wire. method=boleto pays/settles an EXISTING boleto (provide linha_digitavel); it does not issue new boleto charges. For USDC or any on-chain settlement use codespar_crypto_pay.", + enum: ["pix", "card", "boleto", "wire"], + }, + recipient: { + type: "string", + description: + "EITHER a Pix KEY string (email, phone, CPF/CNPJ, EVP) — the common case — OR an object with bank-account details ({bank, account, branch, tax_id, name, account_type?}) to pay a destination that has no registered Pix key (Pix cash-out via initiationType MANUAL). Pass the object literally (do not JSON-stringify it) — the tool-call argument type, not this schema's declared string type, is what determines routing. For a copia-e-cola/QR use `copia_e_cola` instead.", + }, + copia_e_cola: { type: "string", description: "A Pix copia-e-cola / BR Code to PAY (a store order's QR, '0002...'). Use this to pay a checkout's pix_copia_e_cola; the rail resolves the payee. Either recipient OR copia_e_cola is required." }, + consumer_id: { type: "string", description: "Whose governed wallet pays (the payment account to debit). Defaults to the session user — but for a checkout-originated Pix you MUST pass the consumer used in the checkout, otherwise the cash-out resolves no account. Same id as codespar_shop/codespar_wallet." }, + checkout_session_id: { type: "string", description: "To pay a codespar_shop checkout: pass its checkout_session_id and the backend resolves the EXACT Pix copia-e-cola server-side. PREFER THIS over copia_e_cola for a store order — never re-type the long Pix code yourself (re-typing corrupts the CRC). Pass with consumer_id." }, + description: { type: "string", description: "Payment description. Required for action=pay." }, + mandateId: { type: "string", description: "Pre-authorized mandate ID" }, + payment_id: { type: "string", description: "The payment/charge/boleto id to read (action=status); status returns the provider status, e.g. OVERDUE for an expired/unpaid boleto" }, + linha_digitavel: { type: "string", description: "The 47/48-digit linha digitavel (or barcode) of an existing boleto to pay (action=pay, method=boleto)" }, + }, + // `action` is the only field required across both actions: a pay call needs + // amount/currency/description, a status call needs payment_id, so those are + // per-action (described above), not part of the shared required set. Making + // `action` required (rather than defaulted) matches codespar_kyc's required + // discriminator and keeps the destructive `pay` from being the implicit + // fallback of an under-specified call. The flat schema cannot express + // "amount required only when action=pay"; that per-action guard is enforced + // by the runtime + governance rails below the tool, not here. + // + // The method vocabulary is the ent#932 decision: pix, card, boleto, wire — + // no sepa/usdc/ted. sepa and usdc have no route under this tool (usdc's + // redirect target is codespar_crypto_pay) and ted is unpublished until the + // public TED route ships. A rail without a route must not be published. + // + // The managed runtime extends this baseline with allowlisted extras + // (boleto_quote, the DICT claim lifecycle, expected_amount_minor — ent#932); + // those are managed-only capabilities, deliberately NOT part of the shared + // contract the OSS runtime must implement. + required: ["action"], +}; + +const WALLET_INPUT: MetaToolInputSchema = { + type: "object", + properties: { + action: { type: "string", description: "balance | statement | receive (default: balance)", enum: ["balance", "statement", "receive"] }, + consumer_id: { type: "string", description: "Whose wallet — defaults to the session user id" }, + amount: { type: "number", description: "Top-up amount in minor units (centavos for BRL) — action=receive" }, + description: { type: "string", description: "Charge description shown to the payer — action=receive" }, + dynamic: { type: "boolean", description: "action=receive: mint a DYNAMIC copia-e-cola (location URL) instead of a static QR. Default false (static)." }, + limit: { type: "number", description: "Max ledger entries (1..100, default 20) — action=statement" }, + }, + required: ["action"], +}; + +const SHOP_INPUT: MetaToolInputSchema = { + type: "object", + properties: { + action: { type: "string", description: "search | checkout | checkout_status (default: search)", enum: ["search", "checkout", "checkout_status"] }, + merchant: { type: "string", description: "Store slug: cobasi | animale | lojaspompeia (VTEX), meli (Mercado Livre)", enum: ["cobasi", "animale", "lojaspompeia", "meli"] }, + query: { type: "string", description: "What to search for (action=search), e.g. 'tv 55 4k'" }, + limit: { type: "number", description: "Max results returned, 1..20 (action=search)" }, + items: { + type: "array", + description: "Items to buy: [{ variant_id, quantity, seller? }] (action=checkout, VTEX stores)", + }, + url: { type: "string", description: "Listing URL to buy (action=checkout, Mercado Livre — it has no buyer API)" }, + paymentMethod: { type: "string", description: "Settlement rail the store mints — pix (default)", enum: ["pix"] }, + consumer_id: { type: "string", description: "Which buyer is shopping — resolves their connected Mercado Livre login (action=checkout, meli)" }, + checkout_session_id: { type: "string", description: "From action=checkout — pass it to action=checkout_status to poll for the Pix" }, + auto_pay: { type: "boolean", description: "action=checkout_status ONLY: when the order is ready_for_payment, pay it AUTOMATICALLY server-side from the consumer's governed wallet (pass consumer_id) and return status='paid' + a payment receipt. The agent never handles the Pix code or calls codespar_pay — the backend does the payment within the consumer's mandate. Use this to complete a purchase in one shopping flow." }, + buyer: { type: "object", description: "Vaulted shopper profile (email, firstName, lastName, document, phone) — optional" }, + address: { type: "object", description: "Shipping address (postalCode, street, number, neighborhood, city, state, complement) — optional" }, + }, + required: ["action"], +}; + +const CHARGE_INPUT: MetaToolInputSchema = { + type: "object", + properties: { + amount: { type: "number", description: "Charge amount in major currency unit (R$ 125.00 → 125)" }, + currency: { type: "string", description: "Currency code (BRL, USD, EUR)" }, + country: { type: "string", description: "ISO-3166-1 alpha-2 country code for the eligibility rail (BR, US, MX, AR, CL, CO, INTL). Defaults to BR. Set to US for cross-border USD card via ACP, INTL for hosted-checkout flows." }, + method: { type: "string", description: "Payment method: pix, boleto, card, wallet", enum: ["pix", "boleto", "card", "wallet"] }, + description: { type: "string", description: "Charge description shown to buyer" }, + buyer: { + type: "object", + description: "Buyer details (name, email, document, phone)", + }, + due_date: { type: "string", description: "ISO 8601 due date (boleto only)" }, + metadata: { type: "object", description: "Provider-specific overrides" }, + }, + required: ["amount", "currency", "method", "description", "buyer"], +}; + const INVOICE_INPUT: MetaToolInputSchema = { type: "object", properties: { action: { type: "string", description: - "What to do: issue (emit a new document — the default), status (read an existing document's fiscal state), or amend (correct an existing document). Defaults to issue, so existing issue-only callers are unaffected.", + "issue (emit, default) | status (read fiscal state) | amend (correct in place via CC-e, or cancel + reissue as a substitute)", + enum: ["issue", "status", "amend"], }, - type: { type: "string", description: "Invoice type: nfe, nfse, invoice" }, + type: { type: "string", description: "Invoice type: nfe, nfse, invoice", enum: ["nfe", "nfse", "invoice"] }, recipient: { type: "object", description: "Recipient details (name, document, email). Required for action=issue." }, items: { type: "array", description: "Line items. Required for action=issue." }, dueDate: { type: "string", description: "Due date (ISO 8601)" }, @@ -94,10 +327,24 @@ const INVOICE_INPUT: MetaToolInputSchema = { required: ["type"], }; +const SHIP_INPUT: MetaToolInputSchema = { + type: "object", + properties: { + action: { type: "string", description: "label | track | quote", enum: ["label", "track", "quote"] }, + origin: { type: "object", description: "Sender address (postal_code + city + state)" }, + destination: { type: "object", description: "Recipient address" }, + items: { type: "array", description: "Items to ship — each with weight_g + dimensions" }, + service_level: { type: "string", description: "fastest | cheapest | standard", enum: ["fastest", "cheapest", "standard"] }, + tracking_code: { type: "string", description: "For action=track only" }, + metadata: { type: "object", description: "Provider-specific overrides" }, + }, + required: ["action"], +}; + const NOTIFY_INPUT: MetaToolInputSchema = { type: "object", properties: { - channel: { type: "string", description: "Notification channel: whatsapp, email, sms" }, + channel: { type: "string", description: "Notification channel: whatsapp, email, sms", enum: ["whatsapp", "email", "sms"] }, to: { type: "string", description: "Recipient phone number or email" }, template: { type: "string", description: "Message template name" }, message: { type: "string", description: "Custom message text" }, @@ -106,50 +353,172 @@ const NOTIFY_INPUT: MetaToolInputSchema = { required: ["channel", "to"], }; -const PAY_INPUT: MetaToolInputSchema = { +const CRYPTO_PAY_INPUT: MetaToolInputSchema = { type: "object", properties: { - action: { + amount: { type: "number", description: "Amount in target currency major unit" }, + currency: { type: "string", description: "Crypto currency code: USDC, USDT, BTC, ETH, MATIC", enum: ["USDC", "USDT", "BTC", "ETH", "MATIC"] }, + network: { type: "string", description: "Blockchain network: ethereum, polygon, base, solana, bitcoin", enum: ["ethereum", "polygon", "base", "solana", "bitcoin"] }, + direction: { type: "string", description: "send | receive", enum: ["send", "receive"] }, + counterparty: { + type: "object", + description: + "Recipient (send) or buyer (receive). For `direction: send` counterparty is required and must include `country` (ISO 3166-1 alpha-2) so the router + audit log can track the destination of cross-border flows. For `direction: receive` counterparty is optional — the buyer is anonymous until they hit the hosted URL.", + properties: { + country: { type: "string", description: "ISO 3166-1 alpha-2 country code of the recipient (e.g. 'US', 'BR', 'MX'). Required when direction='send'." }, + }, + }, + metadata: { type: "object", description: "Provider-specific overrides" }, + }, + required: ["amount", "currency", "direction"], +}; + +const KYC_INPUT: MetaToolInputSchema = { + type: "object", + properties: { + buyer: { type: "object", description: "Subject. For onboarding (PF): { fullName, document (CPF), email, phoneNumber (+55…; sandbox last digit 1 = auto-approve), birthDate (DD-MM-YYYY), motherName, address, country }. For onboarding-business (PJ/MEI): { document (CNPJ, 14 digits), businessName (razão social), tradingName?, businessEmail, contactNumber, businessAddress (or address), owner: [...] } — owner[] is required and its FIRST entry must be the sócio responsible for documentation (ownerType REPRESENTANTE, full PF data; their CPF is the documentoscopia target). For verification: { name, document, country, email }." }, + check_type: { type: "string", description: - "What to do: pay (execute a payment/transfer) or status (read an existing payment/charge/boleto's current status by id). Required — pass it explicitly on every call.", + "identity | document | risk-score | sanctions | onboarding (open a BR payment account — natural person, CPF) | onboarding-business (open a BR payment account — legal person, CNPJ / PJ / MEI) | status (poll a verification_id)", + enum: ["identity", "document", "risk-score", "sanctions", "onboarding", "onboarding-business", "status"], }, - amount: { type: "number", description: "Amount to pay, in minor units (centavos for BRL). Required for action=pay." }, - currency: { type: "string", description: "Currency code (BRL, USD, EUR). Required for action=pay." }, - country: { type: "string", description: "ISO-3166-1 alpha-2 country code for the eligibility rail" }, - method: { type: "string", description: "Payment method: pix, card, usdc, boleto, sepa, wire. method=boleto pays/settles an EXISTING boleto (provide linha_digitavel); it does not issue new boleto charges." }, - recipient: { type: "string", description: "Recipient identifier (e.g. a Pix key)" }, - copia_e_cola: { type: "string", description: "A Pix copia-e-cola / BR Code to pay" }, - consumer_id: { type: "string", description: "Which buyer's governed wallet pays" }, - checkout_session_id: { type: "string", description: "A store checkout session to settle" }, - description: { type: "string", description: "Payment description. Required for action=pay." }, - mandateId: { type: "string", description: "Pre-authorized mandate id" }, - payment_id: { type: "string", description: "The payment/charge/boleto id to read (action=status); status returns the provider status, e.g. OVERDUE for an expired/unpaid boleto" }, - linha_digitavel: { type: "string", description: "The 47/48-digit linha digitavel (or barcode) of an existing boleto to pay (action=pay, method=boleto)" }, + verification_id: { type: "string", description: "From a prior call — REQUIRED with check_type=status to poll completion. It names the proposal that verified the document, and it is the only thing that provisions a payment account: a status poll without it never binds an account, because a document number an agent typed is not proof the document is the consumer's" }, + document_number: { type: "string", description: "CPF (or CNPJ when polling an onboarding-business proposal) — required with check_type=status (identifies the subject; NOT on its own a licence to bind that person's account). It must be the SAME document the verification_id's proposal verified — never used to look an account up: a mismatch refuses with onboarding_document_mismatch" }, + consumer_id: { type: "string", description: "Whose account/verification — defaults to the session user id (onboarding + status)" }, + metadata: { type: "object", description: "Provider-specific overrides" }, + }, + required: ["buyer", "check_type"], +}; + +const LEDGER_INPUT: MetaToolInputSchema = { + type: "object", + properties: { + action: { type: "string", description: "entry | balance | account | receipt | receipts (default: entry)", enum: ["entry", "balance", "account", "receipt", "receipts"] }, + receipt_id: { type: "string", description: "The agentic receipt id (rcpt_...) to read (action=receipt)." }, + consumer_id: { type: "string", description: "Whose receipts to list (action=receipts). Defaults to the session user." }, + limit: { type: "number", description: "Max receipts to list (action=receipts, default 50)." }, + asset: { type: "string", description: "Asset / currency code for entry + account (BRL, USD, ...)" }, + scale: { type: "number", description: "Decimal places for the asset (default 2; JPY=0, most crypto=6/8)" }, + source: { type: "array", description: "Debit side(s) of an entry: [{ account (alias), amount (minor units) }]" }, + destination: { type: "array", description: "Credit side(s) of an entry: [{ account (alias), amount (minor units) }]" }, + description: { type: "string", description: "Transaction description (entry only)" }, + account: { type: "string", description: "Account UUID to read balances for (action=balance)" }, + alias: { type: "string", description: "Account alias, e.g. @wallet/user_123 (action=account)" }, + name: { type: "string", description: "Account display name (action=account)" }, + type: { type: "string", description: "Ledger account type: deposit, savings, external (action=account, default deposit)", enum: ["deposit", "savings", "external"] }, + metadata: { type: "object", description: "Free-form metadata stored on the entry / account" }, }, - // `action` is the only field required across both actions: a pay call needs - // amount/currency/description, a status call needs payment_id, so those are - // per-action (described above), not part of the shared required set. Making - // `action` required (rather than defaulted) matches codespar_kyc's required - // discriminator and keeps the destructive `pay` from being the implicit - // fallback of an under-specified call. NOTE: requiring `action` means a - // pre-existing action-less caller must now pass it — a deliberate, small - // contract change, NOT backward-compatible the way codespar_invoice's - // optional `action` is (see INVOICE_INPUT). The flat schema cannot express - // "amount required only when action=pay"; that per-action guard is enforced - // by the runtime + governance rails below the tool, not here. required: ["action"], }; +const ISSUE_INPUT: MetaToolInputSchema = { + type: "object", + properties: { + action: { type: "string", description: "card-virtual | card-physical | card-control | card-get (default: card-virtual)", enum: ["card-virtual", "card-physical", "card-control", "card-get"] }, + cardholder_id: { type: "string", description: "Cardholder id at the issuer. Required to issue a card." }, + program_id: { type: "string", description: "Card program / BIN (issuer affinity group). Required to issue a card." }, + card_id: { type: "string", description: "Card id — required for card-control and card-get." }, + control: { type: "string", description: "freeze | unfreeze | cancel (card-control only)", enum: ["freeze", "unfreeze", "cancel"] }, + reason: { type: "string", description: "Reason stamped on a control action" }, + shipping_address: { type: "object", description: "Shipping address (card-physical only)" }, + metadata: { type: "object", description: "Provider-specific overrides" }, + }, + required: ["action"], +}; + +/* ── Definitions ───────────────────────────────────────────────── */ + +/** Find the right catalog tool (or native meta-tool) for a free-form use case. */ +export const DISCOVER_DEFINITION: SharedMetaToolDefinition = { + name: "codespar_discover", + description: + "Find the right tool for a free-form use case. Returns the recommended catalog tool (plus its connection status, known pitfalls, recommended plan, and related tools) AND, in `meta_tools`, any native CodeSpar high-level tools that cover the same job — e.g. 'buy a TV' surfaces codespar_shop, 'send a Pix' surfaces codespar_pay, 'save my address' surfaces codespar_manage_connections. Prefer a native meta-tool when one is returned. Use discover when you don't already know the canonical tool name to call.", + input_schema: DISCOVER_INPUT, + contract: contractOf(DISCOVER_INPUT), +}; + +/** Read-only happy-path plan for the authenticated workspace. */ +export const GET_STARTED_DEFINITION: SharedMetaToolDefinition = { + name: "codespar_get_started", + description: + "Get the recommended happy path for this authenticated CodeSpar workspace. Read-only and informational — it returns a structured, ordered plan (it does NOT execute a charge or move money). In the test environment the sandbox rails (Pix in/out, wallet) ship PRE-CONNECTED, so you can run the full flow with no bank connection, CNPJ, or KYC: drive codespar_shop -> codespar_wallet -> codespar_pay under a signed mandate, or use codespar_charge for a Pix in-collection (the buyer pays you). Call this first when a user asks 'how do I start / what can you do' so you can act without a discovery detour. (Named to match the MCP server's no-key setup tool: exactly one codespar_get_started is ever visible — the no-key setup tool mints a key, this authenticated tool hands you the happy path.)", + input_schema: GET_STARTED_INPUT, + contract: contractOf(GET_STARTED_INPUT), +}; + +/** List, inspect, or connect the accounts + identity the agent needs. */ +export const MANAGE_CONNECTIONS_DEFINITION: SharedMetaToolDefinition = { + name: "codespar_manage_connections", + description: + "List, inspect, or connect the accounts + identity the agent needs. For API-key/OAuth providers (server_id like asaas, nfe-io): action=list|status|initiate surfaces a dashboard connect deep-link — NEVER pass credentials here (they travel via the dashboard or OAuth callback). For login-walled STORES that have no buyer API (server_id=meli or ifood): action=connect_start returns a live-view URL the user opens to log into their OWN account once (their 2FA); then action=connect_finish (with the session_id + context_id from connect_start) persists that login. For meli, the connected login is what codespar_shop buys with on the buyer's own account. iFood checkout is not supported yet: connecting an iFood login only stores the session for a future capability — codespar_shop CANNOT buy on iFood today, so tell the user that BEFORE asking them to log in. For GUEST-checkout stores (VTEX: cobasi/animale/…) the buyer has no login but checkout still needs their data — action=save_profile vaults the buyer's checkout identity ONCE (name, email, CPF, full address; encrypted) so codespar_shop auto-fills it; action=get_profile returns it masked. ALWAYS ASK the buyer which email to use at checkout — do NOT infer it: it's where the order confirmation goes, and an email that already has an account at the store forces a VTEX ID login/identity wall the agent can't pass, so use a dedicated checkout email NOT registered at the store (keeps checkout as guest). save_profile merges field-by-field, so you can save the address first and add the email later; the response returns needs:'email' + profile_complete:false until an email is set. Saving the profile once means later purchases don't re-ask for CEP/email/CPF.", + input_schema: MANAGE_CONNECTIONS_INPUT, + contract: contractOf(MANAGE_CONNECTIONS_INPUT), +}; + +/** SELL-side merchant checkout: assemble a cart and create a payment for a shopper. */ +export const CHECKOUT_DEFINITION: SharedMetaToolDefinition = { + name: "codespar_checkout", + description: + "SELL-side merchant checkout: as a MERCHANT, assemble a cart and create a payment for a shopper to pay YOU. Sums items into a total, dispatches it as an inbound charge on the tenant's connected payment rails, and returns the charge with a hosted payment page (charge_url) plus, on the Pix rail, the pix_copy_paste code. paymentMethod picks the rail: pix (live, BRL default), boleto/card (per catalog); for usdc use codespar_crypto_pay. Distinct from codespar_shop, the buy-side tool where the agent IS the shopper spending its own wallet.", + input_schema: CHECKOUT_INPUT, + contract: contractOf(CHECKOUT_INPUT), +}; + +/** Execute a governed payment or transfer, or read a payment's status. */ +export const PAY_DEFINITION: SharedMetaToolDefinition = { + name: "codespar_pay", + description: + "Execute a direct payment or transfer, or read a payment's status. Pass action on every call. action=pay executes a payment/transfer with full governance (policy + mandate + routing) — Pix, card, boleto, wire (for USDC or any on-chain settlement use codespar_crypto_pay). To PAY a store order's Pix copia-e-cola / QR (what codespar_shop checkout returns as pix_copia_e_cola), pass it as `copia_e_cola` — NOT as `recipient`; the rail decodes the QR (static or dynamic) and debits the governed wallet. A Pix cash-out is async: the result has `settled` + `status_message` — status=PROCESSING means ACCEPTED and settling (seconds), NOT failed, and the wallet is already debited. Relay `status_message` to the user, don't surface a bare PROCESSING. To pay an existing boleto, pass method=boleto with linha_digitavel (the boleto's 47/48-digit code or barcode); this settles an EXISTING boleto, it does not issue new boleto charges. action=status reads an existing payment/charge/boleto's current status by id (e.g. OVERDUE for an expired/unpaid boleto), so an agent can discover post-purchase state before deciding what to do next.", + input_schema: PAY_INPUT, + contract: contractOf(PAY_INPUT), +}; + +/** Programmable wallet for the agent/consumer's governed funds. */ +export const WALLET_DEFINITION: SharedMetaToolDefinition = { + name: "codespar_wallet", + description: + "Programmable wallet for the agent/consumer's governed funds. action=balance returns the wallet balance + Pix key (the spendable funds); action=statement returns the wallet ledger (funds, holds, debits, newest first); action=receive mints a Pix copia-e-cola — a QR a payer pays to TOP UP the wallet (settling credits the wallet via the inbound webhook). Scoped to the consumer (consumer_id defaults to the session user). Distinct from codespar_ledger (the double-entry books) and codespar_pay (spending OUT). Use receive to fund, then codespar_pay to spend.", + input_schema: WALLET_INPUT, + contract: contractOf(WALLET_INPUT), +}; + +/** BUY-side shopping: search a store's live catalog and buy with a real Pix checkout. */ +export const SHOP_DEFINITION: SharedMetaToolDefinition = { + name: "codespar_shop", + description: + "USE THIS TOOL for ANY request to find, search, browse, compare, or buy products at a supported Brazilian store — Cobasi, Animale, Lojas Pompeia, or Mercado Livre. It returns the store's LIVE, in-stock, actually-BUYABLE catalog (rendered as product cards) with a real Pix checkout — prefer it over a generic web search for these stores, which only returns links you can't buy from. BUY-side shopping: act as the SHOPPER. Search a store's catalog and buy a product, minting the store's REAL Pix copia-e-cola to settle from your governed wallet (codespar_pay × pix). action=search returns card-ready offers for a merchant query — each with product_id, sku_id (USE THIS as the checkout variant_id, NOT product_id), title, price, image, and variants (size options, each its own sku_id). action=checkout STARTS the store's real checkout — a ~1-2 min browser flow (VTEX guest checkout today: Cobasi, Animale, Lojas Pompeia; Mercado Livre via the buyer's connected login) — and returns IMMEDIATELY with { checkout_session_id, status:'in_progress' }; do NOT block. Then poll action=checkout_status with that checkout_session_id every ~15s until status='ready_for_payment', which returns the payable pix_copia_e_cola + total. A status='canceled' carries a structured reason: retriable=true (reason_code 'store_temporarily_unavailable' / 'checkout_failed') is a TEMPORARY store/session fault — re-call action=checkout to retry; it is NOT an out-of-stock. Only reason_code='no_shipping' is a genuine no-delivery/out-of-stock (offer alternatives then). reason_code='identity_required' → the store demands a login the agent can't pass; reason_hint says whether a fresh guest email can work or the store requires login for EVERY purchase (then do NOT retry — no email helps); 'not_connected' → connect the account first. Relay reason_hint to the user. This async checkout-session model is protocol-agnostic (ACP-aligned). Use when the AGENT is the buyer spending money. Distinct from codespar_checkout, the SELL-side merchant primitive.", + input_schema: SHOP_INPUT, + contract: contractOf(SHOP_INPUT), +}; + +/** Create an INBOUND charge — the buyer pays the merchant. */ +export const CHARGE_DEFINITION: SharedMetaToolDefinition = { + name: "codespar_charge", + description: + "Create an INBOUND charge — the buyer pays the merchant. Pix charge / boleto / hosted card checkout / digital wallet redirect. Distinct from codespar_pay (outbound transfer/payout). Routes to the tenant's connected charge-issuing providers.", + input_schema: CHARGE_INPUT, + contract: contractOf(CHARGE_INPUT), +}; + /** Issue, read, or amend invoices and fiscal documents (NF-e / NFS-e / invoice). */ export const INVOICE_DEFINITION: SharedMetaToolDefinition = { name: "codespar_invoice", description: - "Issue, read, or amend invoices and Brazilian fiscal documents (NF-e, NFS-e). action=issue emits a new document (default); action=status reads an existing document's fiscal state (autorizada / cancelada / ...); action=amend corrects an existing document — a correction letter (CC-e) in place while the SEFAZ amendment window is open, or a cancel and reissue as a substitute once it is not (the result indicates which mechanism applied).", + "Issue, read, or amend invoices, NF-e (Nota Fiscal Eletrônica), or NFS-e. action=issue emits a new document (default); action=status reads an existing document's fiscal state (autorizada / cancelada / ...); action=amend corrects an existing document — a correction letter (CC-e) in place while the SEFAZ amendment window is open, or a cancel and reissue as a substitute (tipo 3) once it is not, with the result indicating which mechanism applied.", input_schema: INVOICE_INPUT, contract: contractOf(INVOICE_INPUT), }; +/** Generate a shipping label, fetch tracking status, or quote carriers. */ +export const SHIP_DEFINITION: SharedMetaToolDefinition = { + name: "codespar_ship", + description: + "Generate a shipping label OR fetch tracking status. Routes BR domestic (Correios + private carriers) or international carriers via a unified shape: the agent passes a neutral {origin, destination, items} shape and the router picks the cheapest carrier per request.", + input_schema: SHIP_INPUT, + contract: contractOf(SHIP_INPUT), +}; + /** Send a notification over a messaging channel (WhatsApp / email / SMS). */ export const NOTIFY_DEFINITION: SharedMetaToolDefinition = { name: "codespar_notify", @@ -159,20 +528,65 @@ export const NOTIFY_DEFINITION: SharedMetaToolDefinition = { contract: contractOf(NOTIFY_INPUT), }; -/** Execute a governed payment or transfer (Pix / card / boleto / wire). */ -export const PAY_DEFINITION: SharedMetaToolDefinition = { - name: "codespar_pay", +/** Send or receive a crypto payment (USDC/USDT/BTC across mainnet + L2s). */ +export const CRYPTO_PAY_DEFINITION: SharedMetaToolDefinition = { + name: "codespar_crypto_pay", description: - "Execute a payment or transfer, or read a payment's status. Pass action on every call. action=pay executes a payment/transfer under governance — Pix, card, USDC, boleto, SEPA, wire; can pay a Pix copia-e-cola, settle a store checkout, or pay an existing boleto by its linha digitavel. action=status reads an existing payment/charge/boleto's current status by id (e.g. OVERDUE for an expired/unpaid boleto), so an agent can discover post-purchase state before deciding what to do next.", - input_schema: PAY_INPUT, - contract: contractOf(PAY_INPUT), + "Send or receive a crypto payment. USDC/USDT/BTC across mainnet + L2s. Routes to hosted-checkout, exchange, on/offramp, or x402 micropayment rails. Distinct from codespar_pay (fiat rails).", + input_schema: CRYPTO_PAY_INPUT, + contract: contractOf(CRYPTO_PAY_INPUT), +}; + +/** Run a KYC / identity verification, or open a payments account. */ +export const KYC_DEFINITION: SharedMetaToolDefinition = { + name: "codespar_kyc", + description: + "Run a KYC / identity verification, OR open a payments account. check_type=identity|document runs an identity/document verification; risk-score returns a fraud risk score; sanctions runs a sanctions screening. check_type=onboarding (BR, natural person / CPF) and check_type=onboarding-business (BR, legal person / CNPJ — PJ/MEI) route to a licensed BaaS partner and are special: they VERIFY (background check + documentoscopia) AND PROVISION a real payment account for the consumer — that account becomes the codespar_wallet funding source (so after onboarding, codespar_wallet balance/receive and codespar_pay work for the same consumer_id). For onboarding-business the documentoscopia target is the responsible partner (buyer.owner[0]), not the company itself. Every check_type returns a verification_id; poll completion with check_type=status (pass verification_id + document_number — the verification_id is required, a status poll with only a document number provisions nothing and refuses with document_ownership_unproven). For onboarding, status returns 'pending' | 'documentscopy_pending' (with a hosted_url to finish doc capture) | 'approved' (with the funding source) | 'rejected'. Sandbox: a phoneNumber ending in 1 auto-approves both gates.", + input_schema: KYC_INPUT, + contract: contractOf(KYC_INPUT), }; -/** The shared agent-facing definitions, keyed by wire tool name. */ +/** Double-entry ledger: post entries, read balances, create accounts, read receipts. */ +export const LEDGER_DEFINITION: SharedMetaToolDefinition = { + name: "codespar_ledger", + description: + "Record money movement in a double-entry ledger, read account balances, create accounts, or read the agentic receipt of a spend. Routes to the tenant's self-hosted ledger instance (multi-currency, multi-asset, immutable + auditable). action=entry posts an n:n journal entry (source debits must equal destination credits); action=balance reads an account's balances; action=account creates an account. action=receipt returns the canonical agentic receipt (the Control Record: mandate -> quote -> payment -> delivery, with a tamper-evident chain hash + any settle-time exceptions) by receipt_id; action=receipts lists a consumer's receipts (newest first). Amounts are in minor units. The ledger is asset-agnostic — no currency/country needed. Distinct from codespar_pay/charge (those move real money via PSPs); this is the system of record / books.", + input_schema: LEDGER_INPUT, + contract: contractOf(LEDGER_INPUT), +}; + +/** Issue and control payment cards for AI agents or end-users. */ +export const ISSUE_DEFINITION: SharedMetaToolDefinition = { + name: "codespar_issue", + description: + "Issue and control payment cards for AI agents or end-users. Routes to a card-issuing partner (pan-LATAM issuing-as-a-service). action=card-virtual issues a virtual card (active immediately); card-physical issues a physical card (needs shipping_address); card-control freezes/unfreezes/cancels an existing card; card-get reads a card's status. This is the agent-spend-card primitive — it creates SPEND INSTRUMENTS, distinct from codespar_pay/charge which move money.", + input_schema: ISSUE_INPUT, + contract: contractOf(ISSUE_INPUT), +}; + +/** + * The shared agent-facing definitions, keyed by wire tool name — the FULL + * meta-tool surface, all fifteen tools. (This used to publish only the three + * demo tools — invoice/notify/pay — leaving twelve agent-facing tools, + * codespar_wallet and codespar_kyc among them, with no published definition + * at all; ent#933.) + */ export const SHARED_META_TOOL_DEFINITIONS = { + codespar_discover: DISCOVER_DEFINITION, + codespar_get_started: GET_STARTED_DEFINITION, + codespar_manage_connections: MANAGE_CONNECTIONS_DEFINITION, + codespar_checkout: CHECKOUT_DEFINITION, + codespar_pay: PAY_DEFINITION, + codespar_wallet: WALLET_DEFINITION, + codespar_shop: SHOP_DEFINITION, + codespar_charge: CHARGE_DEFINITION, codespar_invoice: INVOICE_DEFINITION, + codespar_ship: SHIP_DEFINITION, codespar_notify: NOTIFY_DEFINITION, - codespar_pay: PAY_DEFINITION, + codespar_crypto_pay: CRYPTO_PAY_DEFINITION, + codespar_kyc: KYC_DEFINITION, + codespar_ledger: LEDGER_DEFINITION, + codespar_issue: ISSUE_DEFINITION, } as const; /** Wire names that have a published shared definition. */ From 7aae8cade206e5c15935289baebcb3d0ad87d9cc Mon Sep 17 00:00:00 2001 From: Fabiano Cruz Date: Thu, 3 Sep 2026 13:35:53 -0300 Subject: [PATCH 2/3] =?UTF-8?q?chore(deps):=20npm=20audit=20fix=20?= =?UTF-8?q?=E2=80=94=20bump=20transitive=20fast-uri/hono/ip-address/nanoid?= =?UTF-8?q?/qs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Unblocks the CI security-audit step, red on every PR since the new advisories landed (same failure on PR #124's run; main's last CI run predates them). Lockfile-only, all within existing semver ranges; turbo build 18/18 and test 36/36 green after the bump. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01BtvPxidmzsA22yoVJSWdba --- package-lock.json | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/package-lock.json b/package-lock.json index 45ace30..a71565d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1419,9 +1419,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz", - "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz", + "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==", "funding": [ { "type": "github", @@ -1587,9 +1587,9 @@ } }, "node_modules/hono": { - "version": "4.12.32", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.32.tgz", - "integrity": "sha512-XcuyW9qE2kJn07PkecMOBd5Vq/hMy7mmGw+idz1yblbg9N17ijJODrvPkn7/dwL3Kulj8LcRJ69DLOWf91dRUg==", + "version": "4.13.5", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.5.tgz", + "integrity": "sha512-O6+/eCYRkzzzy0rPWwKLiGBR1nFuUPZynnwjxN1MBA62NNqbT0wQEzQyK2gSO5yDIDB336sXQleAhOHrzlYyKw==", "license": "MIT", "engines": { "node": ">=16.9.0" @@ -1638,9 +1638,9 @@ "license": "ISC" }, "node_modules/ip-address": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", - "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "version": "10.7.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.0.tgz", + "integrity": "sha512-BGFsyJd5mpXp3rK6jIdADLNgpJUK1jnjzvYF8lK+VyDab9JAmqN0YOKDdP17HlgKb2+ehPgDc8EtnRLbGCAMhA==", "license": "MIT", "engines": { "node": ">= 12" @@ -1777,9 +1777,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.16", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", - "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "dev": true, "funding": [ { @@ -1957,12 +1957,13 @@ } }, "node_modules/qs": { - "version": "6.15.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", - "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", + "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.1.0" + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" }, "engines": { "node": ">=0.6" From 6d050b3f3851c8f7711514bf6e2ee8add0e585d6 Mon Sep 17 00:00:00 2001 From: Fabiano Cruz Date: Thu, 3 Sep 2026 13:54:44 -0300 Subject: [PATCH 3/3] fix(types): align the wire-shape arg types with the published vocabularies (ent#933 review) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adversarial-review fix on top of the 15-definition publish: the typed wire shapes in types.ts still carried the pre-#932 vocabularies, so the same package contradicted its own definitions — - PayArgs.method advertised "wallet" (charge's vocabulary — never a pay rail) and hid "wire"; now pix/card/boleto/wire per the ent#932 enum. - PayArgs.recipient was string-only; now string | PayBankAccountRecipient ({bank, account, branch, tax_id, name, account_type?}), the runtime's manual Pix cash-out object form the definition publishes. Also adds the mandateId + linha_digitavel fields MetaPayArgs carries. - KycArgs.check_type hid onboarding / onboarding-business / status (the account-provisioning rails); now all seven, plus the verification_id / document_number / consumer_id fields the definition publishes. - ChargeArgs.method (TS) and ChargeMethod (Python) hid "wallet", which the runtime's charge coercer accepts and the published enum names. New pin tests make the drift mechanical: Record literals fail typecheck when a TS union gains/loses a value, and fail at runtime when the published enum drifts from the same keys — both directions, for pay/charge/kyc/ship/ledger/issue/shop, plus a field-for-field pin of the bank-account recipient object against the published prose. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01BtvPxidmzsA22yoVJSWdba --- packages/python/src/codespar/types.py | 2 +- .../types/src/meta-tool-definitions.test.ts | 140 ++++++++++++++++++ packages/types/src/types.ts | 102 ++++++++++--- 3 files changed, 223 insertions(+), 21 deletions(-) diff --git a/packages/python/src/codespar/types.py b/packages/python/src/codespar/types.py index 59ae952..224a503 100644 --- a/packages/python/src/codespar/types.py +++ b/packages/python/src/codespar/types.py @@ -342,7 +342,7 @@ class ConnectionWizardOptions: # ── codespar_charge wire shape ───────────────────────────────────── -ChargeMethod = Literal["pix", "boleto", "card"] +ChargeMethod = Literal["pix", "boleto", "card", "wallet"] @dataclass(slots=True) diff --git a/packages/types/src/meta-tool-definitions.test.ts b/packages/types/src/meta-tool-definitions.test.ts index 80c0777..4f4e9d9 100644 --- a/packages/types/src/meta-tool-definitions.test.ts +++ b/packages/types/src/meta-tool-definitions.test.ts @@ -10,6 +10,16 @@ import { sharedDefinitionConformanceReasons, type AgentFacingToolShape, } from "./meta-tool-definition-conformance.js"; +import type { + ChargeArgs, + IssueArgs, + KycArgs, + LedgerArgs, + PayArgs, + PayBankAccountRecipient, + ShipArgs, + ShopArgs, +} from "./types.js"; const ALL = Object.values(SHARED_META_TOOL_DEFINITIONS) as SharedMetaToolDefinition[]; @@ -338,3 +348,133 @@ describe("sharedDefinitionConformanceReasons (the cross-runtime comparator sees ).toBe(true); }); }); + +describe("wire-shape arg unions match the published definition vocabularies (ent#933)", () => { + /** + * Two-sided pin. The `Record` literal fails to COMPILE (the + * typecheck task) when the TS union in types.ts gains or loses a value + * relative to the keys written here, and the runtime assertion fails when + * the published structured enum drifts from those same keys — so the + * union a TypeScript consumer types against and the enum an agent is + * shown cannot disagree silently in either direction. This is the check + * that would have caught PayArgs advertising "wallet" (charge's + * vocabulary) while hiding "wire", and KycArgs hiding the onboarding / + * onboarding-business / status rails. + */ + function pinned(union: Record): string[] { + return Object.keys(union).sort(); + } + + it("PayArgs.method == codespar_pay method enum (ent#932: pix/card/boleto/wire)", () => { + const union: Record, true> = { + pix: true, + card: true, + boleto: true, + wire: true, + }; + expect(pinned(union)).toEqual( + [...SHARED_META_TOOL_DEFINITIONS.codespar_pay.input_schema.properties.method!.enum!].sort(), + ); + }); + + it("ChargeArgs.method == codespar_charge method enum (wallet included)", () => { + const union: Record = { + pix: true, + boleto: true, + card: true, + wallet: true, + }; + expect(pinned(union)).toEqual( + [...SHARED_META_TOOL_DEFINITIONS.codespar_charge.input_schema.properties.method!.enum!].sort(), + ); + }); + + it("KycArgs.check_type == codespar_kyc check_type enum (onboarding rails included)", () => { + const union: Record = { + identity: true, + document: true, + "risk-score": true, + sanctions: true, + onboarding: true, + "onboarding-business": true, + status: true, + }; + expect(pinned(union)).toEqual( + [...SHARED_META_TOOL_DEFINITIONS.codespar_kyc.input_schema.properties.check_type!.enum!].sort(), + ); + }); + + it("ShipArgs.action == codespar_ship action enum", () => { + const union: Record = { label: true, track: true, quote: true }; + expect(pinned(union)).toEqual( + [...SHARED_META_TOOL_DEFINITIONS.codespar_ship.input_schema.properties.action!.enum!].sort(), + ); + }); + + it("LedgerArgs.action == codespar_ledger action enum", () => { + const union: Record = { + entry: true, + balance: true, + account: true, + receipt: true, + receipts: true, + }; + expect(pinned(union)).toEqual( + [...SHARED_META_TOOL_DEFINITIONS.codespar_ledger.input_schema.properties.action!.enum!].sort(), + ); + }); + + it("IssueArgs.action + IssueArgs.control == codespar_issue enums", () => { + const actions: Record = { + "card-virtual": true, + "card-physical": true, + "card-control": true, + "card-get": true, + }; + expect(pinned(actions)).toEqual( + [...SHARED_META_TOOL_DEFINITIONS.codespar_issue.input_schema.properties.action!.enum!].sort(), + ); + const controls: Record, true> = { + freeze: true, + unfreeze: true, + cancel: true, + }; + expect(pinned(controls)).toEqual( + [...SHARED_META_TOOL_DEFINITIONS.codespar_issue.input_schema.properties.control!.enum!].sort(), + ); + }); + + it("ShopArgs.action == codespar_shop action enum", () => { + const union: Record = { + search: true, + checkout: true, + checkout_status: true, + }; + expect(pinned(union)).toEqual( + [...SHARED_META_TOOL_DEFINITIONS.codespar_shop.input_schema.properties.action!.enum!].sort(), + ); + }); + + it("PayArgs.recipient accepts the bank-account object the definition publishes, field for field", () => { + const dest: PayBankAccountRecipient = { + bank: "13935893", + account: "41055351", + branch: "0001", + tax_id: "39588281164", + name: "Evidencia Homologacao Teste", + account_type: "CACC", + }; + // Both payee forms are assignable — a Pix key string and the object. + const viaKey: PayArgs["recipient"] = "pix@example.com"; + const viaAccount: PayArgs["recipient"] = dest; + expect(typeof viaKey).toBe("string"); + expect(typeof viaAccount).toBe("object"); + // Every field of the TS object form is visible in the published prose, + // so the type and the agent-facing description name the same shape. + const prose = + SHARED_META_TOOL_DEFINITIONS.codespar_pay.input_schema.properties.recipient!.description ?? ""; + for (const field of Object.keys(dest)) { + expect(prose).toContain(field); + } + }); +}); diff --git a/packages/types/src/types.ts b/packages/types/src/types.ts index 9585b05..65d589b 100644 --- a/packages/types/src/types.ts +++ b/packages/types/src/types.ts @@ -212,8 +212,9 @@ export interface ChargeArgs { amount: number; /** ISO-4217 currency code (BRL, USD, EUR). */ currency: string; - /** Payment method: pix, boleto, card. */ - method: "pix" | "boleto" | "card"; + /** Payment method: pix, boleto, card, wallet (digital-wallet redirect). + * Mirrors the codespar_charge definition's `method` enum. */ + method: "pix" | "boleto" | "card" | "wallet"; /** Charge description shown to the buyer. */ description: string; /** Buyer details (always required — charges are merchant-issued). */ @@ -241,14 +242,19 @@ export interface ChargeResult { /** * Outbound payment — the runtime pays a recipient (a transfer/payout). * Distinct from `codespar_charge`, which is inbound (a buyer pays the - * merchant). The discriminator is the `recipient` (a Pix key, account, - * or email) versus charge's `buyer` object. + * merchant). The discriminator is the payee address (`recipient`, + * `copia_e_cola`, or `linha_digitavel`) versus charge's `buyer` object. * - * A payee can be addressed two ways: - * - `recipient` — a Pix key / account / email the rail resolves to a payee. + * A payee can be addressed three ways: + * - `recipient` — a Pix key the rail resolves to a payee, or a + * {@link PayBankAccountRecipient} object for a destination with no + * registered Pix key (Pix cash-out via initiationType MANUAL). * - `copia_e_cola` — a Pix copia-e-cola / BR Code that already encodes the * payee. When present it identifies the payee server-side and takes - * precedence over `recipient`; at least one of the two must be given. + * precedence over `recipient`. + * - `linha_digitavel` — the digitable line of an EXISTING boleto to settle + * (`method: "boleto"`). + * At least one of the three must be given. * * `amount` is in MINOR currency units (centavos: R$ 1.25 → 125). This * differs from `ChargeArgs.amount`, which is MAJOR units — pay settles a @@ -260,20 +266,50 @@ export interface PayArgs { amount: number; /** ISO-4217 currency code (BRL, USD, ...). */ currency: string; - /** Payee address — a Pix key, account number, or email. Required unless - * `copia_e_cola` is given. */ - recipient?: string; + /** Payee address — a Pix key (email, phone, CPF/CNPJ, EVP) or a + * bank-account object for a destination with no registered Pix key. + * Required unless `copia_e_cola` or `linha_digitavel` is given. */ + recipient?: string | PayBankAccountRecipient; /** A Pix copia-e-cola / BR Code that encodes the payee. Takes precedence - * over `recipient` when present. Required unless `recipient` is given. */ + * over `recipient` when present. */ copia_e_cola?: string; + /** The 47/48-digit linha digitável (or barcode) of an existing boleto to + * settle (`method: "boleto"`). */ + linha_digitavel?: string; /** Payment description. */ description: string; - /** Payment method. Defaults to "pix" when omitted. */ - method?: "pix" | "card" | "boleto" | "wallet"; + /** Payment method. Defaults to "pix" when omitted. The vocabulary is the + * codespar_pay definition's `method` enum (ent#932): no sepa/ted, and + * for USDC or any on-chain settlement use codespar_crypto_pay. */ + method?: "pix" | "card" | "boleto" | "wire"; + /** Pre-authorized mandate id the spend runs under. */ + mandateId?: string; /** Free-form metadata forwarded to the rail. */ metadata?: Record; } +/** + * Bank-account destination for a Pix cash-out to a payee with no registered + * Pix key (initiationType MANUAL) — the object form of + * {@link PayArgs.recipient}. Mirrors the shape the runtime's `codespar_pay` + * accepts: `{bank, account, branch, tax_id, name, account_type?}`. + */ +export interface PayBankAccountRecipient { + /** Destination bank id (ISPB code). */ + bank: string; + /** Account number, with check digit. */ + account: string; + /** Branch (agência) number. */ + branch: string; + /** Recipient CPF/CNPJ. */ + tax_id: string; + /** Recipient full/legal name. */ + name: string; + /** Account type code (e.g. CACC); the rail's default applies when + * omitted. */ + account_type?: string; +} + export interface PayResult { id: string; status: string; @@ -310,20 +346,46 @@ export interface PayResult { * document, country); the rail plucks the fields it needs. * * `check_type` selects the verification rail: - * - identity Full KYC (document + selfie + database) - * - document Document-only verification - * - risk-score Behavioral risk score (returns a numeric `score`) - * - sanctions OFAC / PEP screening + * - identity Full KYC (document + selfie + database) + * - document Document-only verification + * - risk-score Behavioral risk score (returns a numeric `score`) + * - sanctions OFAC / PEP screening + * - onboarding Open a BR payment account (natural person, CPF): + * verifies AND provisions the consumer's funding + * source (codespar_wallet / codespar_pay) + * - onboarding-business Open a BR payment account (legal person, CNPJ — + * PJ/MEI); documentoscopia targets buyer.owner[0] + * - status Poll a prior verification by `verification_id` * * KYC is async: a returned result records that the verification was created * with the rail. The subject completes the hosted flow off-platform; poll - * the disposition with the `verificationStatus` correlation method. + * the disposition with the `verificationStatus` correlation method, or with + * `check_type: "status"` (pass `verification_id` + `document_number`). */ export interface KycArgs { /** Subject details the rail identifies the person from. */ buyer: Record; - /** identity | document | risk-score | sanctions. */ - check_type: "identity" | "document" | "risk-score" | "sanctions"; + /** The verification rail — the codespar_kyc definition's `check_type` + * enum. */ + check_type: + | "identity" + | "document" + | "risk-score" + | "sanctions" + | "onboarding" + | "onboarding-business" + | "status"; + /** From a prior call — REQUIRED with `check_type: "status"`. It names the + * proposal that verified the document and is the only thing that + * provisions a payment account. */ + verification_id?: string; + /** CPF (or CNPJ for an onboarding-business proposal) — required with + * `check_type: "status"`; must match the document the + * `verification_id`'s proposal verified. */ + document_number?: string; + /** Whose account/verification — defaults to the session user id + * (onboarding + status). */ + consumer_id?: string; /** Free-form metadata forwarded to the rail. */ metadata?: Record; }