@supercuts-walrus/backend 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,6 +14,7 @@ import { verifySignedCapability } from "./capability.js";
14
14
  import { WalrusDealBlobs } from "./deal-wire.js";
15
15
  import { canonicalJson } from "./settlement.js";
16
16
  import { validateDirectoryOrigin } from "./seller-directory.js";
17
+ import { TESTNET_DEFAULTS, withTestnetDefaults } from "./testnet-defaults.js";
17
18
  function ownerFile(path) {
18
19
  if ((statSync(path).mode & 0o077) !== 0)
19
20
  fail("SETUP_PERMISSIONS", `${path} must be owner-only`);
@@ -34,6 +35,9 @@ function origins(values) {
34
35
  });
35
36
  }
36
37
  function validateConfig(config) {
38
+ withTestnetDefaults(config);
39
+ if (!config.sellerOrigins?.length && !config.directoryUrl)
40
+ config.directoryUrl = TESTNET_DEFAULTS.directoryUrl;
37
41
  amount(config.perDealCeiling, "perDealCeiling");
38
42
  amount(config.totalBudget, "totalBudget");
39
43
  if (BigInt(config.totalBudget) < BigInt(config.perDealCeiling))
@@ -86,22 +90,25 @@ export async function promptBuyerSetup(home) {
86
90
  fail("SETUP_INPUT_REQUIRED", "interactive setup requires a terminal; use --config with an owner-only JSON file");
87
91
  const rl = createInterface({ input: process.stdin, output: process.stderr });
88
92
  try {
89
- const ask = async (label) => (await rl.question(`${label}: `)).trim();
90
- const days = Number(await ask("Policy duration in days"));
93
+ const ask = async (label, fallback) => (await rl.question(fallback ? `${label} [${fallback}]: ` : `${label}: `)).trim() || fallback || "";
94
+ process.stderr.write("Press Enter to accept a bracketed default. Credentials are read from owner-only files, never typed here.\n");
95
+ const days = Number(await ask("Policy duration in days", "14"));
91
96
  if (!Number.isSafeInteger(days) || days < 1 || days > 365)
92
97
  fail("SETUP_INVALID", "policy duration must be 1..365 days");
93
98
  const config = {
94
- deploymentConfig: await ask("Pinned Testnet deployment config path"), tatumKeyFile: await ask("Tatum key file path"),
95
- writeGrpc: await ask("Sui write gRPC URL"), tatumGrpc: await ask("Tatum read gRPC URL"),
96
- walrusPublisher: await ask("Walrus publisher URL"), walrusAggregator: await ask("Walrus aggregator URL"),
99
+ deploymentConfig: await ask("Pinned Testnet deployment config path", "bundled"), tatumKeyFile: await ask("Owner-only Tatum API key file path"),
100
+ writeGrpc: await ask("Sui write gRPC URL", TESTNET_DEFAULTS.writeGrpc), tatumGrpc: await ask("Tatum read gRPC URL", TESTNET_DEFAULTS.tatumGrpc),
101
+ walrusPublisher: await ask("Walrus publisher URL", TESTNET_DEFAULTS.walrusPublisher), walrusAggregator: await ask("Walrus aggregator URL", TESTNET_DEFAULTS.walrusAggregator),
97
102
  memoryConfigFile: await ask("Owner-only Walrus Memory config path (blank to skip Memory)"),
98
- directoryUrl: await ask("Shared directory origin (blank for direct sellers)"),
103
+ directoryUrl: await ask("Shared directory origin (type none for direct sellers only)", TESTNET_DEFAULTS.directoryUrl),
99
104
  sellerOrigins: (await ask("Additional seller HTTPS origins, comma separated (or blank)")).split(",").map((item) => item.trim()).filter(Boolean),
100
105
  allowedCounterparties: (await ask("Initially approved seller Sui addresses, comma separated (or blank)")).split(",").map((item) => item.trim()).filter(Boolean),
101
- perDealCeiling: await ask("Maximum per deal in MIST"), totalBudget: await ask("Lifetime cumulative budget in MIST"),
106
+ perDealCeiling: await ask("Maximum per deal in MIST", "2000000"), totalBudget: await ask("Lifetime cumulative budget in MIST", "10000000"),
102
107
  expiresAt: Date.now() + days * 24 * 60 * 60 * 1000, allowPublicX402: (await ask("Allow public x402 when requested? yes/no")).toLowerCase() === "yes",
103
108
  };
104
- if (!config.directoryUrl)
109
+ if (config.deploymentConfig === "bundled")
110
+ config.deploymentConfig = "";
111
+ if (!config.directoryUrl || config.directoryUrl === "none")
105
112
  delete config.directoryUrl;
106
113
  return setupBuyerConfig(home, validateConfig(config));
107
114
  }
@@ -124,7 +131,14 @@ function setupBuyerConfig(home, config) {
124
131
  const decoded = decodeSuiPrivateKey(readFileSync(keyPath, "utf8").trim());
125
132
  if (decoded.scheme !== "ED25519")
126
133
  fail("SETUP_INVALID", "buyer key must use ED25519");
127
- return { home, address: Ed25519Keypair.fromSecretKey(decoded.secretKey).toSuiAddress(), ...session, policy: "configured" };
134
+ const address = Ed25519Keypair.fromSecretKey(decoded.secretKey).toSuiAddress();
135
+ return { home, address, ...session, policy: "configured", nextSteps: [
136
+ `Fund only this buyer with Testnet SUI: supercuts funding execute --home ${home} --public-payment <MIST> --shielded-payment 0 --operation-id fund-1 --request-faucet --confirm (or use https://faucet.sui.io for ${address})`,
137
+ ...(config.allowedCounterparties?.length ? [] : [`Review the directory, then approve a seller: supercuts policy approve-seller --home ${home} --capability-id <id>`]),
138
+ ...(config.memoryConfigFile ? [`Keep the Walrus Memory embedding adapter running (supercuts-embedding-adapter). An MCP agent flushes outcome memories itself; SDK-only hosts also run: supercuts-memory-worker ${home}`] : []),
139
+ `Check readiness: supercuts onboarding doctor --home ${home}`,
140
+ `Connect an agent with ${session.mcpConfig}, or call BuyerClient.executeDeal from @supercuts-walrus/sdk.`,
141
+ ] };
128
142
  }
129
143
  export function createBuyerSession(home) {
130
144
  if (!existsSync(join(home, "profile.json")))
package/dist/cli.js CHANGED
@@ -13,13 +13,18 @@ if (["wallet", "offer", "pay", "resource", "memory", "audit", "capability", "set
13
13
  command += `.${words.shift() ?? ""}`;
14
14
  try {
15
15
  if (!command || command === "help" || command === "--help" || command === "setup.") {
16
- process.stdout.write(`${JSON.stringify({ schemaVersion: "supercuts.help.v1", ok: true, commands: ["onboard buyer", "onboard seller", "seller scaffold", "seller research", "seller package", "funding prepare", "funding execute", "setup buyer", "setup seller", "setup renew", "setup session", "init", "doctor", "status", "wallet", "offer", "pay", "memory", "capability", "audit"], setup: "Run supercuts onboard buyer or supercuts onboard seller with an isolated --home. Then use seller scaffold to generate a capability. Research sellers can use seller research to create a cited owner-review draft before packaging.", live: "Fund only the generated buyer address. A seller does not need faucet funds to publish or receive a capability payment." })}\n`);
16
+ process.stdout.write(`${JSON.stringify({ schemaVersion: "supercuts.help.v1", ok: true, commands: ["onboard buyer", "onboard seller", "seller scaffold", "seller research", "seller package", "funding prepare", "funding execute", "setup buyer", "setup seller", "setup renew", "setup session", "init", "doctor", "status", "wallet", "offer", "pay", "memory", "memory insights", "capability", "audit"], setup: "Run supercuts onboard buyer or supercuts onboard seller with an isolated --home. Then use seller scaffold to generate a capability. Research sellers can use seller research to create a cited owner-review draft before packaging.", live: "Fund only the generated buyer address. A seller does not need faucet funds to publish or receive a capability payment." })}\n`);
17
17
  process.exit(0);
18
18
  }
19
19
  const args = parseArgs(words);
20
20
  const home = homePath(args);
21
21
  const fundingInput = { publicPayment: String(args["public-payment"] ?? "0"), shieldedPayment: String(args["shielded-payment"] ?? "0"), ...(typeof args["gas-reserve"] === "string" ? { gasReserve: args["gas-reserve"] } : {}), ...(typeof args["change-reserve"] === "string" ? { changeReserve: args["change-reserve"] } : {}) };
22
- const result = command === "init" ? initialize(home, args) : command === "setup.buyer" || command === "onboard.buyer" ? { schemaVersion: "supercuts.setup.v1", ok: true, command, data: typeof args.config === "string" ? setupBuyer(home, args.config) : await promptBuyerSetup(home) } : command === "setup.seller" || command === "onboard.seller" ? { schemaVersion: "supercuts.setup.v1", ok: true, command, data: typeof args.config === "string" ? setupSeller(home, args.config) : await promptSellerSetup(home) } : command === "seller.scaffold" ? { schemaVersion: "supercuts.seller-scaffold.v1", ok: true, command, data: scaffoldSeller({ ...args, home }) } : command === "seller.research" ? { schemaVersion: "supercuts.seller-research.v1", ok: true, command, data: await generateSellerResearch({ kind: String(args.kind ?? ""), topic: String(args.topic ?? ""), output: String(args.output ?? ""), ...(typeof args["context-file"] === "string" ? { contextFile: args["context-file"] } : {}), ...(typeof args["allowed-domains"] === "string" ? { allowedDomains: args["allowed-domains"] } : {}), ...(typeof args.model === "string" ? { model: args.model } : {}) }) } : command === "setup.renew" ? { schemaVersion: "supercuts.setup.v1", ok: true, command, data: renewBuyer(home, String(args.config ?? "")) } : command === "setup.session" ? { schemaVersion: "supercuts.setup.v1", ok: true, command, data: createBuyerSession(home) } : command === "funding.prepare" ? { schemaVersion: "supercuts.funding.v1", ok: true, command, data: await prepareFunding(home, fundingInput) } : command === "funding.execute" ? { schemaVersion: "supercuts.funding.v1", ok: true, command, data: await executeFunding(home, { ...fundingInput, operationId: String(args["operation-id"] ?? ""), requestFaucet: args["request-faucet"] === true, confirmed: args.confirm === true }) } : command === "seller.package" ? { schemaVersion: "supercuts.seller-package.v1", ok: true, command, data: await packageHostedSeller({ home, spec: String(args.spec ?? ""), artifact: String(args.artifact ?? ""), metadata: String(args.metadata ?? ""), registration: String(args.registration ?? ""), bundle: String(args.bundle ?? ""), ...(typeof args["content-type"] === "string" ? { contentType: args["content-type"] } : {}) }) } : command === "policy.add" ? { schemaVersion: "supercuts.setup.v1", ok: true, command, data: await addBuyerPrivacyRule(home, String(args["capability-id"] ?? "")) } : command === "policy.approve-seller" ? { schemaVersion: "supercuts.setup.v1", ok: true, command, data: await approveBuyerSeller(home, String(args["capability-id"] ?? "")) } : command === "onboarding.doctor" ? { schemaVersion: "supercuts.onboarding.v1", ok: true, command, data: await onboardingDoctor(home, typeof args.config === "string" ? args.config : undefined) } : await (async () => { const app = new Phase3App(home); try {
22
+ const result = command === "init" ? initialize(home, args) : command === "setup.buyer" || command === "onboard.buyer" ? { schemaVersion: "supercuts.setup.v1", ok: true, command, data: typeof args.config === "string" ? setupBuyer(home, args.config) : await promptBuyerSetup(home) } : command === "setup.seller" || command === "onboard.seller" ? { schemaVersion: "supercuts.setup.v1", ok: true, command, data: typeof args.config === "string" ? setupSeller(home, args.config) : await promptSellerSetup(home) } : command === "seller.scaffold" ? { schemaVersion: "supercuts.seller-scaffold.v1", ok: true, command, data: scaffoldSeller({ ...args, home }) } : command === "seller.research" ? { schemaVersion: "supercuts.seller-research.v1", ok: true, command, data: await generateSellerResearch({ kind: String(args.kind ?? ""), topic: String(args.topic ?? ""), output: String(args.output ?? ""), ...(typeof args["context-file"] === "string" ? { contextFile: args["context-file"] } : {}), ...(typeof args["allowed-domains"] === "string" ? { allowedDomains: args["allowed-domains"] } : {}), ...(typeof args.model === "string" ? { model: args.model } : {}) }) } : command === "setup.renew" ? { schemaVersion: "supercuts.setup.v1", ok: true, command, data: renewBuyer(home, String(args.config ?? "")) } : command === "setup.session" ? { schemaVersion: "supercuts.setup.v1", ok: true, command, data: createBuyerSession(home) } : command === "funding.prepare" ? { schemaVersion: "supercuts.funding.v1", ok: true, command, data: await prepareFunding(home, fundingInput) } : command === "funding.execute" ? { schemaVersion: "supercuts.funding.v1", ok: true, command, data: await executeFunding(home, { ...fundingInput, operationId: String(args["operation-id"] ?? ""), requestFaucet: args["request-faucet"] === true, confirmed: args.confirm === true }) } : command === "seller.package" ? { schemaVersion: "supercuts.seller-package.v1", ok: true, command, data: await packageHostedSeller({ home, spec: String(args.spec ?? ""), artifact: String(args.artifact ?? ""), metadata: String(args.metadata ?? ""), registration: String(args.registration ?? ""), bundle: String(args.bundle ?? ""), ...(typeof args["content-type"] === "string" ? { contentType: args["content-type"] } : {}) }) } : command === "policy.add" ? { schemaVersion: "supercuts.setup.v1", ok: true, command, data: await addBuyerPrivacyRule(home, String(args["capability-id"] ?? "")) } : command === "policy.approve-seller" ? { schemaVersion: "supercuts.setup.v1", ok: true, command, data: await approveBuyerSeller(home, String(args["capability-id"] ?? "")) } : command === "memory.insights" ? await (async () => { const app = new Phase3App(home); try {
23
+ return { schemaVersion: "supercuts.memory-insights.v1", ok: true, command, data: await app.memoryInsights() };
24
+ }
25
+ finally {
26
+ app.close();
27
+ } })() : command === "onboarding.doctor" ? { schemaVersion: "supercuts.onboarding.v1", ok: true, command, data: await onboardingDoctor(home, typeof args.config === "string" ? args.config : undefined) } : await (async () => { const app = new Phase3App(home); try {
23
28
  return await app.execute(command, args);
24
29
  }
25
30
  finally {
package/dist/dashboard.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createServer } from "node:http";
2
2
  import { readDemoEvents, recordedReplay } from "./events.js";
3
- const html = `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Supercuts funded-run evidence</title><style>body{font:15px system-ui;margin:0;background:#0b1020;color:#eef2ff}header{position:sticky;top:0;padding:18px 24px;background:#111936;border-bottom:1px solid #334155}main{max-width:980px;margin:auto;padding:24px}.event{display:grid;grid-template-columns:9rem 10rem 1fr;gap:14px;padding:14px;border-bottom:1px solid #27334f}.mode{color:#fbbf24;font-weight:700}.complete,.allowed{color:#86efac}.failed,.rejected{color:#fca5a5}code{color:#bfdbfe;white-space:pre-wrap}</style></head><body><header><b>Supercuts</b> · <span class="mode" id="mode">loading evidence…</span></header><main id="events"></main><script>const out=document.querySelector('#events');const source=new EventSource('/events');source.onmessage=e=>{const v=JSON.parse(e.data);document.querySelector('#mode').textContent=v.mode==='live'?'LIVE FUNDED RUN':'RECORDED FUNDED RUN';const n=document.createElement('div');n.className='event';n.innerHTML='<span>#'+v.sequence+' '+v.actor+'</span><span class="'+v.status+'">'+v.stage+' · '+v.status+'</span><code></code>';n.querySelector('code').textContent=JSON.stringify(v.publicDetails,null,2);out.append(n)};</script></body></html>`;
3
+ const html = `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Supercuts funded-run evidence</title><style>body{font:15px system-ui;margin:0;background:#0b1020;color:#eef2ff}header{position:sticky;top:0;padding:18px 24px;background:#111936;border-bottom:1px solid #334155}main{max-width:980px;margin:auto;padding:24px}.event{display:grid;grid-template-columns:9rem 10rem 1fr;gap:14px;padding:14px;border-bottom:1px solid #27334f}.mode{color:#fbbf24;font-weight:700}.complete,.allowed{color:#86efac}.failed,.rejected{color:#fca5a5}code{color:#bfdbfe;white-space:pre-wrap}#learned{margin:0 0 24px;padding:18px;border:1px solid #fbbf24;border-radius:10px;background:#1a1f3a}#learned h2{margin:0 0 6px;font-size:17px}#learned p{margin:0 0 12px;color:#fde68a;font-weight:600}table{width:100%;border-collapse:collapse;font-size:14px}th,td{text-align:left;padding:6px 8px;border-bottom:1px solid #27334f}th{color:#94a3b8;font-weight:500}.event.memory{background:#1a1f3a}</style></head><body><header><b>Supercuts</b> · <span class="mode" id="mode">loading evidence…</span></header><main><section id="learned" hidden><h2>What Supercuts learned from Walrus Memory</h2><p></p><table><thead><tr><th>Seller</th><th>Capability</th><th>Reliability</th><th>Succeeded / failed</th><th>Last outcome</th><th>Median latency</th></tr></thead><tbody></tbody></table></section><div id="events"></div></main><script>const out=document.querySelector('#events'),learned=document.querySelector('#learned');function renderLearned(d){learned.hidden=false;const w=d.withMemory||{},o=d.withoutMemory;learned.querySelector('p').textContent=d.changedSeller?'Memory changed who gets paid: '+(o?o.sellerId.slice(0,12):'no eligible seller')+' \u2192 '+String(w.sellerId).slice(0,12):(d.changedRail||d.changedRisk||d.changedMaximum)?'Memory changed how this payment settles: '+w.rail+', risk '+w.riskLevel+', max '+w.maximumAmount:'Memory confirmed the default choice';const t=learned.querySelector('tbody');t.textContent='';for(const s of d.scorecards||[]){const r=document.createElement('tr');for(const c of [String(s.counterparty).slice(0,12),s.capabilityId,Math.round(s.reliability*100)+'%',s.successes+' / '+s.failures,s.lastOutcome||'\u2014',s.medianLatencyMs!=null?(s.medianLatencyMs/1000).toFixed(1)+'s':'\u2014']){const td=document.createElement('td');td.textContent=c;r.append(td)}t.append(r)}}const source=new EventSource('/events');source.onmessage=e=>{const v=JSON.parse(e.data);document.querySelector('#mode').textContent=v.mode==='live'?'LIVE FUNDED RUN':'RECORDED FUNDED RUN';if(v.stage==='memory-influence')renderLearned(v.publicDetails);const n=document.createElement('div');n.className=v.stage.startsWith('memory')?'event memory':'event';n.innerHTML='<span>#'+v.sequence+' '+v.actor+'</span><span class="'+v.status+'">'+v.stage+' · '+v.status+'</span><code></code>';n.querySelector('code').textContent=JSON.stringify(v.publicDetails,null,2);out.append(n)};</script></body></html>`;
4
4
  export function serveEventDashboard(path, port, replay = false) {
5
5
  const server = createServer((request, response) => {
6
6
  if (request.url === "/") {
@@ -35,6 +35,16 @@ export interface DealOutcomeQueued {
35
35
  }
36
36
  /** A port owns transport, signing, and finality checks. It must be safe to call
37
37
  * again with the same child IDs after a process crash. */
38
+ export interface SellerFault {
39
+ stage: "negotiation" | "delivery";
40
+ code: string;
41
+ paymentDigest?: string;
42
+ }
43
+ /**
44
+ * Codes the buyer runtime verified against seller-supplied evidence. Buyer-side, network, and Walrus readback
45
+ * errors are excluded: a readback mismatch can come from an aggregator, not the seller.
46
+ */
47
+ export declare const SELLER_FAULT_CODES: Set<string>;
38
48
  export interface DealPorts {
39
49
  readBlob(blobId: string): Promise<Uint8Array>;
40
50
  discover(input: ExecuteDealInput): Promise<SignedCapabilityRegistration>;
@@ -50,6 +60,8 @@ export interface DealPorts {
50
60
  unresolved: true;
51
61
  }>;
52
62
  inventory(blobId: string): Promise<boolean>;
63
+ /** Optional: queue a verified seller-fault memory so later discovery demotes this seller. Best effort. */
64
+ rememberFailure?(input: ExecuteDealInput, plan: SettlementPlanV1, fault: SellerFault): Promise<void>;
53
65
  }
54
66
  export declare class DealEngine {
55
67
  private readonly journal;
@@ -58,5 +70,6 @@ export declare class DealEngine {
58
70
  constructor(journal: DealJournal, ports: DealPorts, mandate: AgentMandate);
59
71
  execute(input: ExecuteDealInput): Promise<DealView>;
60
72
  resume(operationId: string): Promise<DealView>;
73
+ private rememberSellerFault;
61
74
  private finishOutcome;
62
75
  }
@@ -3,6 +3,11 @@ import { validateCapabilityPayload, verifySignedCapability } from "./capability.
3
3
  import { dealChildId } from "./deal.js";
4
4
  import { fail, SupercutsError } from "./errors.js";
5
5
  import { canonicalJson, validateSettlementPlan } from "./settlement.js";
6
+ /**
7
+ * Codes the buyer runtime verified against seller-supplied evidence. Buyer-side, network, and Walrus readback
8
+ * errors are excluded: a readback mismatch can come from an aggregator, not the seller.
9
+ */
10
+ export const SELLER_FAULT_CODES = new Set(["AGREEMENT_BINDING_MISMATCH", "DEAL_RESOURCE_MISMATCH", "DEAL_AGREEMENT_INVALID", "DEAL_DELIVERY_UNVERIFIED", "CAPABILITY_PAYLOAD_INVALID"]);
6
11
  const hash = (value) => createHash("sha256").update(canonicalJson(value)).digest("hex");
7
12
  export class DealEngine {
8
13
  journal;
@@ -91,20 +96,26 @@ export class DealEngine {
91
96
  let negotiation = this.journal.step(operationId, "negotiation");
92
97
  if (view.state === "planned")
93
98
  view = this.journal.advance(operationId, "planned", "negotiating");
94
- if (!negotiation) {
95
- const started = Date.now();
96
- negotiation = await this.ports.negotiate(input, registration, plan, dealChildId(operationId, "agreement"));
97
- if (!negotiation.agreementBlobId || !/^[0-9a-f]{64}$/.test(negotiation.agreementCiphertextHash) || !Array.isArray(negotiation.transcriptBlobs) || !negotiation.transcriptBlobs.length)
98
- fail("DEAL_AGREEMENT_INVALID", "seller agreement or Walrus transcript evidence is incomplete");
99
- this.journal.saveStep(operationId, "negotiation", { planHash: plan.planHash }, negotiation);
100
- this.journal.recordTiming(operationId, "negotiation", Date.now() - started);
99
+ try {
100
+ if (!negotiation) {
101
+ const started = Date.now();
102
+ negotiation = await this.ports.negotiate(input, registration, plan, dealChildId(operationId, "agreement"));
103
+ if (!negotiation.agreementBlobId || !/^[0-9a-f]{64}$/.test(negotiation.agreementCiphertextHash) || !Array.isArray(negotiation.transcriptBlobs) || !negotiation.transcriptBlobs.length)
104
+ fail("DEAL_AGREEMENT_INVALID", "seller agreement or Walrus transcript evidence is incomplete");
105
+ this.journal.saveStep(operationId, "negotiation", { planHash: plan.planHash }, negotiation);
106
+ this.journal.recordTiming(operationId, "negotiation", Date.now() - started);
107
+ }
108
+ const agreementReadStarted = Date.now();
109
+ for (const item of [...negotiation.transcriptBlobs, { blobId: negotiation.agreementBlobId, ciphertextHash: negotiation.agreementCiphertextHash }]) {
110
+ if (!item.blobId || !/^[0-9a-f]{64}$/.test(item.ciphertextHash) || createHash("sha256").update(await this.ports.readBlob(item.blobId)).digest("hex") !== item.ciphertextHash)
111
+ fail("WALRUS_BLOB_MISMATCH", "encrypted negotiation or agreement differs from Walrus readback");
112
+ }
113
+ this.journal.recordTiming(operationId, "agreement_readback", Date.now() - agreementReadStarted);
101
114
  }
102
- const agreementReadStarted = Date.now();
103
- for (const item of [...negotiation.transcriptBlobs, { blobId: negotiation.agreementBlobId, ciphertextHash: negotiation.agreementCiphertextHash }]) {
104
- if (!item.blobId || !/^[0-9a-f]{64}$/.test(item.ciphertextHash) || createHash("sha256").update(await this.ports.readBlob(item.blobId)).digest("hex") !== item.ciphertextHash)
105
- fail("WALRUS_BLOB_MISMATCH", "encrypted negotiation or agreement differs from Walrus readback");
115
+ catch (error) {
116
+ await this.rememberSellerFault(input, plan, "negotiation", undefined, error);
117
+ throw error;
106
118
  }
107
- this.journal.recordTiming(operationId, "agreement_readback", Date.now() - agreementReadStarted);
108
119
  if (view.state === "negotiating")
109
120
  view = this.journal.advance(operationId, "negotiating", "agreed");
110
121
  this.journal.recordBlob(operationId, "agreement", negotiation.agreementCiphertextHash, negotiation.agreementBlobId);
@@ -139,29 +150,35 @@ export class DealEngine {
139
150
  let artifact = this.journal.step(operationId, "delivery");
140
151
  if (view.state === "paid")
141
152
  view = this.journal.advance(operationId, "paid", "delivery_pending");
142
- if (!artifact) {
143
- const started = Date.now();
144
- const candidate = await this.ports.deliver(input, plan, negotiation, payment, dealChildId(operationId, "delivery"));
145
- if ("pending" in candidate)
146
- return this.journal.get(operationId);
147
- if (!candidate.blobId || !/^[0-9a-f]{64}$/.test(candidate.ciphertextHash) || !/^[0-9a-f]{64}$/.test(candidate.artifactSha256))
148
- fail("DEAL_DELIVERY_UNVERIFIED", "seller delivery evidence is incomplete");
149
- if (typeof candidate.artifactBase64 !== "string" || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(candidate.artifactBase64))
150
- fail("DEAL_DELIVERY_UNVERIFIED", "seller artifact encoding is invalid");
151
- const artifactBytes = Buffer.from(candidate.artifactBase64, "base64");
152
- if (!artifactBytes.length || artifactBytes.length > 512 * 1024 || createHash("sha256").update(artifactBytes).digest("hex") !== candidate.artifactSha256)
153
- fail("DEAL_DELIVERY_UNVERIFIED", "seller artifact hash or size is invalid");
154
- validateCapabilityPayload(registration.manifest, "output", candidate.metadata);
155
- artifact = candidate;
156
- this.journal.saveStep(operationId, "delivery", { paymentDigest: payment.transactionDigest }, artifact);
157
- this.journal.recordTiming(operationId, "delivery", Date.now() - started);
153
+ try {
154
+ if (!artifact) {
155
+ const started = Date.now();
156
+ const candidate = await this.ports.deliver(input, plan, negotiation, payment, dealChildId(operationId, "delivery"));
157
+ if ("pending" in candidate)
158
+ return this.journal.get(operationId);
159
+ if (!candidate.blobId || !/^[0-9a-f]{64}$/.test(candidate.ciphertextHash) || !/^[0-9a-f]{64}$/.test(candidate.artifactSha256))
160
+ fail("DEAL_DELIVERY_UNVERIFIED", "seller delivery evidence is incomplete");
161
+ if (typeof candidate.artifactBase64 !== "string" || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(candidate.artifactBase64))
162
+ fail("DEAL_DELIVERY_UNVERIFIED", "seller artifact encoding is invalid");
163
+ const artifactBytes = Buffer.from(candidate.artifactBase64, "base64");
164
+ if (!artifactBytes.length || artifactBytes.length > 512 * 1024 || createHash("sha256").update(artifactBytes).digest("hex") !== candidate.artifactSha256)
165
+ fail("DEAL_DELIVERY_UNVERIFIED", "seller artifact hash or size is invalid");
166
+ validateCapabilityPayload(registration.manifest, "output", candidate.metadata);
167
+ artifact = candidate;
168
+ this.journal.saveStep(operationId, "delivery", { paymentDigest: payment.transactionDigest }, artifact);
169
+ this.journal.recordTiming(operationId, "delivery", Date.now() - started);
170
+ }
171
+ if (createHash("sha256").update(Buffer.from(artifact.artifactBase64, "base64")).digest("hex") !== artifact.artifactSha256)
172
+ fail("DEAL_DELIVERY_UNVERIFIED", "saved artifact hash differs from delivery evidence");
173
+ const deliveryReadStarted = Date.now();
174
+ if (createHash("sha256").update(await this.ports.readBlob(artifact.blobId)).digest("hex") !== artifact.ciphertextHash)
175
+ fail("WALRUS_BLOB_MISMATCH", "encrypted delivery differs from Walrus readback");
176
+ this.journal.recordTiming(operationId, "delivery_readback", Date.now() - deliveryReadStarted);
177
+ }
178
+ catch (error) {
179
+ await this.rememberSellerFault(input, plan, "delivery", payment.transactionDigest, error);
180
+ throw error;
158
181
  }
159
- if (createHash("sha256").update(Buffer.from(artifact.artifactBase64, "base64")).digest("hex") !== artifact.artifactSha256)
160
- fail("DEAL_DELIVERY_UNVERIFIED", "saved artifact hash differs from delivery evidence");
161
- const deliveryReadStarted = Date.now();
162
- if (createHash("sha256").update(await this.ports.readBlob(artifact.blobId)).digest("hex") !== artifact.ciphertextHash)
163
- fail("WALRUS_BLOB_MISMATCH", "encrypted delivery differs from Walrus readback");
164
- this.journal.recordTiming(operationId, "delivery_readback", Date.now() - deliveryReadStarted);
165
182
  this.journal.recordBlob(operationId, "delivery", artifact.ciphertextHash, artifact.blobId);
166
183
  if (view.state === "delivery_pending")
167
184
  view = this.journal.advance(operationId, "delivery_pending", "delivered");
@@ -169,6 +186,14 @@ export class DealEngine {
169
186
  view = this.journal.advance(operationId, "delivered", "outcome_pending");
170
187
  return this.finishOutcome(operationId, input);
171
188
  }
189
+ async rememberSellerFault(input, plan, stage, paymentDigest, error) {
190
+ if (!(error instanceof SupercutsError) || !SELLER_FAULT_CODES.has(error.code) || !this.ports.rememberFailure)
191
+ return;
192
+ try {
193
+ await this.ports.rememberFailure(input, plan, { stage, code: error.code, ...(paymentDigest ? { paymentDigest } : {}) });
194
+ }
195
+ catch { /* the original seller fault stays the reported error */ }
196
+ }
172
197
  async finishOutcome(operationId, input) {
173
198
  const view = this.journal.get(operationId);
174
199
  if (view.evidence.outcomeMemory === "indexed")
package/dist/deal.d.ts CHANGED
@@ -43,6 +43,41 @@ export interface DealEvidenceView {
43
43
  inventory: "pending" | "indexed" | "missing" | "unresolved";
44
44
  paymentFinalized: boolean;
45
45
  paymentDigest?: string;
46
+ /** What Walrus Memory changed about this payment versus the same mandate with no memory. */
47
+ memoryInfluence?: {
48
+ changedSeller: boolean;
49
+ changedRail: boolean;
50
+ changedRisk: boolean;
51
+ changedMaximum: boolean;
52
+ withoutMemory?: {
53
+ sellerId: string;
54
+ capabilityId: string;
55
+ rail: string;
56
+ riskLevel: string;
57
+ maximumAmount: string;
58
+ };
59
+ withMemory: {
60
+ sellerId: string;
61
+ capabilityId: string;
62
+ rail: string;
63
+ riskLevel: string;
64
+ maximumAmount: string;
65
+ };
66
+ citedBlobIds: string[];
67
+ };
68
+ /** Learned seller scorecards used for discovery; counts and blob IDs only, never memory plaintext. */
69
+ memoryRanking?: Array<{
70
+ capabilityId: string;
71
+ counterparty: string;
72
+ reliability: number;
73
+ observations: number;
74
+ successes: number;
75
+ failures: number;
76
+ unresolved: number;
77
+ lastOutcome?: string;
78
+ medianLatencyMs?: number;
79
+ priceMinimum: string;
80
+ }>;
46
81
  }
47
82
  export interface DealDelivery {
48
83
  dealId: string;
package/dist/deal.js CHANGED
@@ -176,7 +176,13 @@ export class DealJournal {
176
176
  const blobs = this.store.db.prepare("SELECT kind,blob_id,status FROM deal_blobs WHERE operation_id=?").all(row.operation_id);
177
177
  const memory = this.store.db.prepare("SELECT blob_id,status FROM deal_memory_outbox WHERE operation_id=? AND event_kind='outcome'").get(row.operation_id);
178
178
  const transcript = this.step(row.operation_id, "negotiation");
179
- return { version: "supercuts.deal-evidence.v1", dealId, transcriptBlobIds: transcript?.transcriptBlobs?.map((item) => item.blobId) ?? [], ...(blobs.find((item) => item.kind === "agreement")?.blob_id ? { agreementBlobId: blobs.find((item) => item.kind === "agreement").blob_id } : {}), ...(blobs.find((item) => item.kind === "delivery")?.blob_id ? { deliveryBlobId: blobs.find((item) => item.kind === "delivery").blob_id } : {}), ...(memory?.blob_id ? { outcomeMemoryBlobId: memory.blob_id } : {}), ciphertextReadback: blobs.some((item) => item.status === "unresolved") ? "unresolved" : ["agreement", "delivery"].every((kind) => blobs.some((item) => item.kind === kind && item.status === "verified")) ? "verified" : "pending", inventory: memory?.status === "indexed" ? "indexed" : memory?.status === "unresolved" ? "unresolved" : "pending", paymentFinalized: view.evidence.payment === "finalized", ...(view.paymentDigest ? { paymentDigest: view.paymentDigest } : {}) };
179
+ const hasRecords = Boolean(this.store.db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='app_records'").get());
180
+ const record = (scope, id) => { if (!hasRecords)
181
+ return undefined; const found = this.store.db.prepare("SELECT value FROM app_records WHERE scope=? AND id=?").get(scope, id); return found ? JSON.parse(found.value) : undefined; };
182
+ const influence = record("memory-influence", `deal:${row.operation_id}`);
183
+ const ranking = record("deal-ranking", row.operation_id);
184
+ const memoryRanking = ranking?.scorecards.map(({ capabilityId, counterparty, reliability, observations, successes, failures, unresolved, lastOutcome, medianLatencyMs, priceMinimum }) => ({ capabilityId, counterparty, reliability, observations, successes, failures, unresolved, ...(lastOutcome ? { lastOutcome } : {}), ...(medianLatencyMs !== undefined ? { medianLatencyMs } : {}), priceMinimum }));
185
+ return { version: "supercuts.deal-evidence.v1", dealId, transcriptBlobIds: transcript?.transcriptBlobs?.map((item) => item.blobId) ?? [], ...(blobs.find((item) => item.kind === "agreement")?.blob_id ? { agreementBlobId: blobs.find((item) => item.kind === "agreement").blob_id } : {}), ...(blobs.find((item) => item.kind === "delivery")?.blob_id ? { deliveryBlobId: blobs.find((item) => item.kind === "delivery").blob_id } : {}), ...(memory?.blob_id ? { outcomeMemoryBlobId: memory.blob_id } : {}), ciphertextReadback: blobs.some((item) => item.status === "unresolved") ? "unresolved" : ["agreement", "delivery"].every((kind) => blobs.some((item) => item.kind === kind && item.status === "verified")) ? "verified" : "pending", inventory: memory?.status === "indexed" ? "indexed" : memory?.status === "unresolved" ? "unresolved" : "pending", paymentFinalized: view.evidence.payment === "finalized", ...(view.paymentDigest ? { paymentDigest: view.paymentDigest } : {}), ...(influence ? { memoryInfluence: influence } : {}), ...(memoryRanking ? { memoryRanking } : {}) };
180
186
  }
181
187
  delivery(dealId) {
182
188
  const row = this.store.db.prepare("SELECT operation_id FROM deal_operations WHERE deal_id=?").get(dealId);
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * OpenAI-compatible embedding adapter for local models (`supercuts-embedding-adapter`).
4
+ *
5
+ * Walrus Mainnet Memory indexes 1536-dimensional embeddings. This loopback proxy forwards requests to a
6
+ * local OpenAI-compatible endpoint (Ollama by default) and zero-pads shorter vectors to 1536 dimensions.
7
+ * Zero-padding preserves cosine similarity and Euclidean distance as long as every write and recall uses
8
+ * this same adapter and upstream model. Memory plaintext only reaches the configured upstream.
9
+ */
10
+ import { type Server } from "node:http";
11
+ export declare const TARGET_DIMENSIONS = 1536;
12
+ export declare function padEmbedding(embedding: unknown): number[];
13
+ interface EmbeddingPayload {
14
+ data: Array<{
15
+ embedding: unknown;
16
+ } & Record<string, unknown>>;
17
+ }
18
+ export declare function transformEmbeddingResponse(payload: unknown): EmbeddingPayload;
19
+ export interface EmbeddingAdapterOptions {
20
+ upstreamBaseUrl: string;
21
+ upstreamModel: string;
22
+ upstreamApiKey?: string;
23
+ fetchImpl?: typeof fetch;
24
+ }
25
+ export declare function createEmbeddingAdapter({ fetchImpl, upstreamBaseUrl, upstreamApiKey, upstreamModel, }: EmbeddingAdapterOptions): Server;
26
+ export {};
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * OpenAI-compatible embedding adapter for local models (`supercuts-embedding-adapter`).
4
+ *
5
+ * Walrus Mainnet Memory indexes 1536-dimensional embeddings. This loopback proxy forwards requests to a
6
+ * local OpenAI-compatible endpoint (Ollama by default) and zero-pads shorter vectors to 1536 dimensions.
7
+ * Zero-padding preserves cosine similarity and Euclidean distance as long as every write and recall uses
8
+ * this same adapter and upstream model. Memory plaintext only reaches the configured upstream.
9
+ */
10
+ import { createServer } from "node:http";
11
+ import { isMainModule } from "./main-module.js";
12
+ export const TARGET_DIMENSIONS = 1536;
13
+ const MAX_REQUEST_BYTES = 1024 * 1024;
14
+ export function padEmbedding(embedding) {
15
+ if (!Array.isArray(embedding) ||
16
+ embedding.length < 1 ||
17
+ embedding.length > TARGET_DIMENSIONS) {
18
+ throw new Error(`expected an embedding with 1 to ${TARGET_DIMENSIONS} dimensions, received ${Array.isArray(embedding) ? embedding.length : "a non-array value"}`);
19
+ }
20
+ if (!embedding.every((value) => typeof value === "number" && Number.isFinite(value)))
21
+ throw new Error("upstream embedding contains a non-finite value");
22
+ return [
23
+ ...embedding,
24
+ ...Array(TARGET_DIMENSIONS - embedding.length).fill(0),
25
+ ];
26
+ }
27
+ export function transformEmbeddingResponse(payload) {
28
+ const value = payload;
29
+ if (!value || !Array.isArray(value.data) || !value.data.length)
30
+ throw new Error("upstream response has no embeddings");
31
+ const dimension = value.data[0]?.embedding?.length;
32
+ if (value.data.some((item) => item?.embedding?.length !== dimension))
33
+ throw new Error("upstream embeddings have inconsistent dimensions");
34
+ return {
35
+ ...value,
36
+ data: value.data.map((item) => ({
37
+ ...item,
38
+ embedding: padEmbedding(item?.embedding),
39
+ })),
40
+ };
41
+ }
42
+ export function createEmbeddingAdapter({ fetchImpl = fetch, upstreamBaseUrl, upstreamApiKey, upstreamModel, }) {
43
+ if (!upstreamBaseUrl)
44
+ throw new Error("EMBEDDING_UPSTREAM_BASE_URL is required");
45
+ if (!upstreamModel)
46
+ throw new Error("EMBEDDING_UPSTREAM_MODEL is required");
47
+ const endpoint = `${upstreamBaseUrl.replace(/\/$/, "")}/embeddings`;
48
+ let sourceDimensions;
49
+ const send = (response, status, body) => {
50
+ response.writeHead(status, { "content-type": "application/json" });
51
+ response.end(typeof body === "string" ? body : JSON.stringify(body));
52
+ };
53
+ return createServer(async (request, response) => {
54
+ // Loopback only: reject other Host headers so a DNS-rebinding page cannot drive the local embedding model.
55
+ const host = (request.headers.host ?? "").replace(/:\d+$/, "").replace(/^\[|\]$/g, "");
56
+ if (!["127.0.0.1", "localhost", "::1"].includes(host))
57
+ return send(response, 403, { error: { message: "loopback host required" } });
58
+ if (request.method === "GET" && request.url === "/health")
59
+ return send(response, 200, {
60
+ status: "ok",
61
+ targetDimensions: TARGET_DIMENSIONS,
62
+ upstreamModel,
63
+ });
64
+ if (request.method !== "POST" || request.url !== "/v1/embeddings")
65
+ return send(response, 404, { error: { message: "not found" } });
66
+ try {
67
+ let body = "";
68
+ for await (const chunk of request) {
69
+ body += chunk;
70
+ if (body.length > MAX_REQUEST_BYTES)
71
+ throw new Error("embedding request is too large");
72
+ }
73
+ const requested = JSON.parse(body);
74
+ const upstream = await fetchImpl(endpoint, {
75
+ method: "POST",
76
+ headers: {
77
+ "content-type": "application/json",
78
+ ...(upstreamApiKey
79
+ ? { authorization: `Bearer ${upstreamApiKey}` }
80
+ : {}),
81
+ },
82
+ body: JSON.stringify({ ...requested, model: upstreamModel }),
83
+ signal: AbortSignal.timeout(60_000),
84
+ });
85
+ const upstreamBody = await upstream.text();
86
+ if (!upstream.ok)
87
+ return send(response, upstream.status, upstreamBody);
88
+ const payload = JSON.parse(upstreamBody), result = transformEmbeddingResponse(payload);
89
+ const dimension = payload.data[0].embedding.length;
90
+ if (sourceDimensions !== undefined && sourceDimensions !== dimension)
91
+ throw new Error("upstream embedding dimensions changed");
92
+ sourceDimensions = dimension;
93
+ send(response, 200, result);
94
+ }
95
+ catch (error) {
96
+ send(response, 502, {
97
+ error: {
98
+ message: error instanceof Error ? error.message : "embedding adapter failed",
99
+ },
100
+ });
101
+ }
102
+ });
103
+ }
104
+ if (isMainModule(import.meta.url)) {
105
+ const port = Number(process.env.EMBEDDING_ADAPTER_PORT ?? 8787);
106
+ const upstreamBaseUrl = process.env.EMBEDDING_UPSTREAM_BASE_URL ?? "http://127.0.0.1:11434/v1";
107
+ const upstreamModel = process.env.EMBEDDING_UPSTREAM_MODEL ?? "embeddinggemma";
108
+ const server = createEmbeddingAdapter({
109
+ upstreamBaseUrl,
110
+ upstreamModel,
111
+ ...(process.env.EMBEDDING_UPSTREAM_API_KEY
112
+ ? { upstreamApiKey: process.env.EMBEDDING_UPSTREAM_API_KEY }
113
+ : {}),
114
+ });
115
+ server.listen(port, "127.0.0.1", () => process.stdout.write(`${JSON.stringify({ ok: true, listening: `http://127.0.0.1:${port}/v1`, upstream: upstreamBaseUrl, upstreamModel, targetDimensions: TARGET_DIMENSIONS, memoryConfig: { embeddingApiBase: `http://127.0.0.1:${port}/v1`, embeddingModel: `${upstreamModel}-1536` } })}\n`));
116
+ }
@@ -3,6 +3,7 @@ import { timingSafeEqual } from "node:crypto";
3
3
  import { SuiGrpcClient } from "@mysten/sui/grpc";
4
4
  import { Transaction } from "@mysten/sui/transactions";
5
5
  import { isValidTransactionSignature } from "@mysten/sui/verify";
6
+ import { isMainModule } from "./main-module.js";
6
7
  const NETWORK = "sui:testnet";
7
8
  const SUI = "0x2::sui::SUI";
8
9
  /** Sui RPC may expand a type's address to 32 bytes; policy identifiers use its canonical short form. */
@@ -164,7 +165,7 @@ export class SuiTestnetFacilitator {
164
165
  }
165
166
  send(response, status, body) { const encoded = Buffer.from(JSON.stringify(body)); response.writeHead(status, { "content-type": "application/json", "content-length": String(encoded.length) }); response.end(encoded); }
166
167
  }
167
- if (process.argv[1] && import.meta.url === new URL(`file://${process.argv[1]}`).href) {
168
+ if (isMainModule(import.meta.url)) {
168
169
  const key = process.env.FACILITATOR_SHARED_KEY ?? "";
169
170
  const grpc = process.env.SUI_TESTNET_GRPC ?? "https://fullnode.testnet.sui.io:443";
170
171
  const port = Number(process.env.PORT ?? "3000");
package/dist/funding.js CHANGED
@@ -27,6 +27,8 @@ export async function prepareFunding(home, values) {
27
27
  if (app.profile.role !== "buyer" || app.profile.mock)
28
28
  fail("LIVE_PROFILE_REQUIRED", "funding requires a live buyer role home");
29
29
  const doctor = await app.execute("doctor", {}), address = String(doctor.data.signer ?? "");
30
+ if (shieldedPayment > 0n && doctor.data.shieldedProofs !== "ready")
31
+ fail("SHIELDED_PROOFS_UNAVAILABLE", String(doctor.data.shieldedFixes?.[0] ?? "shielded proving artifacts are not installed"));
30
32
  const endpoint = app.profile.live?.writeGrpcEndpoint ?? fail("LIVE_PROFILE_INVALID", "buyer write RPC is missing");
31
33
  const [{ balance }, shielded] = await Promise.all([writeClient(endpoint).getBalance({ owner: address, coinType: "0x2::sui::SUI" }), app.execute("wallet.balance", {})]);
32
34
  return calculateFundingPlan({ address, publicBalance: BigInt(balance.balance), shieldedBalance: BigInt(String(shielded.data.available ?? "0")), publicPayment, shieldedPayment, gasReserve, changeReserve });
@@ -42,7 +44,14 @@ export async function executeFunding(home, values) {
42
44
  if (plan.faucetRequired) {
43
45
  if (!values.requestFaucet)
44
46
  fail("BUYER_FUNDING_REQUIRED", `buyer needs ${plan.publicShortfallMist} additional MIST; rerun with --request-faucet --confirm or fund ${plan.address} manually`);
45
- execFileSync("sui", ["client", "faucet", "--url", "https://faucet.testnet.sui.io", "--address", plan.address], { stdio: "inherit" });
47
+ try {
48
+ execFileSync("sui", ["client", "faucet", "--url", "https://faucet.testnet.sui.io", "--address", plan.address], { stdio: "inherit" });
49
+ }
50
+ catch (error) {
51
+ if (error.code === "ENOENT")
52
+ fail("SUI_CLI_MISSING", `the Sui CLI is not installed; request Testnet SUI for ${plan.address} at https://faucet.sui.io, then rerun funding execute without --request-faucet`);
53
+ throw error;
54
+ }
46
55
  for (let attempt = 0; attempt < 15; attempt++) {
47
56
  await new Promise((resolve) => setTimeout(resolve, 2_000));
48
57
  plan = await prepareFunding(home, values);
@@ -1,5 +1,5 @@
1
1
  import { type SignedCapabilityRegistration } from "./capability.js";
2
- import type { DealArtifact, DealNegotiation, DealPayment, DealPorts } from "./deal-engine.js";
2
+ import type { DealArtifact, DealNegotiation, DealPayment, DealPorts, SellerFault } from "./deal-engine.js";
3
3
  import type { ExecuteDealInput } from "./deal.js";
4
4
  import { type AgentMandate } from "./mandate.js";
5
5
  import type { Phase3App } from "./phase3.js";
@@ -32,6 +32,7 @@ export declare class LiveDealPorts implements DealPorts {
32
32
  negotiate(input: ExecuteDealInput, registration: SignedCapabilityRegistration, plan: SettlementPlanV1, agreementOperationId: string): Promise<LiveNegotiation>;
33
33
  pay(input: ExecuteDealInput, plan: SettlementPlanV1, negotiation: DealNegotiation, paymentOperationId: string): Promise<DealPayment>;
34
34
  deliver(input: ExecuteDealInput, plan: SettlementPlanV1, negotiation: DealNegotiation, payment: DealPayment, _deliveryOperationId: string): Promise<DealArtifact>;
35
+ rememberFailure(input: ExecuteDealInput, plan: SettlementPlanV1, fault: SellerFault): Promise<void>;
35
36
  rememberOutcome(input: ExecuteDealInput, plan: SettlementPlanV1, payment: DealPayment, artifact: DealArtifact, _outcomeOperationId: string): Promise<{
36
37
  queued: true;
37
38
  payloadHash: string;
@@ -3,7 +3,7 @@ import { join } from "node:path";
3
3
  import { randomBytes } from "node:crypto";
4
4
  import { decodeSuiPrivateKey } from "@mysten/sui/cryptography";
5
5
  import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
6
- import { verifySignedCapability } from "./capability.js";
6
+ import { verifySignedCapability, validateCapabilityPayload } from "./capability.js";
7
7
  import { WalrusDealBlobs, wireDecrypt, wireEncrypt, wireHash, wireJsonHash, wireKey } from "./deal-wire.js";
8
8
  import { fail } from "./errors.js";
9
9
  import { LiveRuntime } from "./live-runtime.js";
@@ -127,8 +127,21 @@ export class LiveDealPorts {
127
127
  const candidates = (await this.catalog()).map(({ registration }) => registration).filter((registration) => (input.capabilityId && registration.manifest.capabilityId === input.capabilityId || input.capabilityClass && registration.manifest.capability === input.capabilityClass) && registration.manifest.asset === input.budget.asset);
128
128
  if (!candidates.length)
129
129
  fail("DEAL_CAPABILITY_NOT_FOUND", "configured sellers have no matching signed capability");
130
- candidates.sort((a, b) => BigInt(a.manifest.priceBand.minimum) < BigInt(b.manifest.priceBand.minimum) ? -1 : BigInt(a.manifest.priceBand.minimum) > BigInt(b.manifest.priceBand.minimum) ? 1 : a.manifest.capabilityId.localeCompare(b.manifest.capabilityId));
131
- return candidates[0];
130
+ // Same capability class can carry different signed input schemas; only rank sellers that accept this task.
131
+ const accepting = candidates.filter((registration) => { try {
132
+ validateCapabilityPayload(registration.manifest, "input", input.input);
133
+ return true;
134
+ }
135
+ catch {
136
+ return false;
137
+ } });
138
+ if (!accepting.length)
139
+ fail("DEAL_INPUT_UNSUPPORTED", "no matching seller's signed input schema accepts this task input; check deal.catalog for the expected fields");
140
+ candidates.splice(0, candidates.length, ...accepting);
141
+ // Walrus Memory decides which seller gets paid: verified outcomes outrank a lower list price.
142
+ const ranking = await this.app.rankDealCandidates(input.operationId, candidates.map((item) => item.manifest), this.mandate, { budgetMaximum: input.budget.maximum, ...(input.requestedRail ? { requestedRail: input.requestedRail } : {}), ...(input.privacyHint ? { privacyHint: input.privacyHint } : {}) });
143
+ const chosen = ranking.ordered[0];
144
+ return candidates.find((item) => item.manifest.manifestHash === chosen.manifestHash);
132
145
  }
133
146
  async plan(input, registration, paymentOperationId) {
134
147
  const existing = this.app.store.db.prepare("SELECT plan_json FROM settlement_plans WHERE operation_id=?").get(paymentOperationId);
@@ -137,10 +150,13 @@ export class LiveDealPorts {
137
150
  validateSettlementPlan(saved);
138
151
  if (saved.mandateHash !== settlementMandateHash(this.mandate) || saved.manifestHash !== registration.manifest.manifestHash || saved.operationId !== paymentOperationId)
139
152
  fail("DEAL_PLAN_MISMATCH", "persisted plan differs from the owner mandate or seller capability");
153
+ this.app.recordDealMemoryInfluence(input.operationId, saved, this.mandate, { ...(input.requestedRail ? { requestedRail: input.requestedRail } : {}), ...(input.privacyHint ? { privacyHint: input.privacyHint } : {}) });
140
154
  return saved;
141
155
  }
142
156
  const result = await executeMandated(this.app, this.mandate, "settlement.plan", { "operation-id": paymentOperationId, manifest: registration.manifest, "requested-amount": registration.manifest.priceBand.minimum, ...(input.privacyHint ? { "privacy-hint": input.privacyHint } : {}), ...(input.requestedRail ? { "requested-rail": input.requestedRail } : {}) });
143
- return result;
157
+ const plan = result;
158
+ this.app.recordDealMemoryInfluence(input.operationId, plan, this.mandate, { ...(input.requestedRail ? { requestedRail: input.requestedRail } : {}), ...(input.privacyHint ? { privacyHint: input.privacyHint } : {}) });
159
+ return plan;
144
160
  }
145
161
  async negotiate(input, registration, plan, agreementOperationId) {
146
162
  const sessionId = agreementOperationId, capabilityId = registration.manifest.capabilityId, buyerPublicKey = this.app.store.getSecret("identity:x25519-public")?.toString("base64url") ?? fail("IDENTITY_MISSING", "buyer transport key is missing");
@@ -209,6 +225,9 @@ export class LiveDealPorts {
209
225
  fail("DEAL_DELIVERY_UNVERIFIED", "seller delivery bytes or metadata differ from the receipt");
210
226
  return { blobId: receipt.blobId, ciphertextHash: receipt.ciphertextHash, artifactSha256: receipt.artifactSha256, contentType: receipt.contentType, metadata: receipt.metadata, artifactBase64: artifact.artifactBase64 };
211
227
  }
228
+ async rememberFailure(input, plan, fault) {
229
+ this.app.recordVerifiedDealFailure(input.operationId, plan.planHash, fault);
230
+ }
212
231
  async rememberOutcome(input, plan, payment, artifact, _outcomeOperationId) {
213
232
  return this.app.recordVerifiedDealOutcome(input.operationId, plan.planHash, payment.transactionDigest, artifact.artifactSha256);
214
233
  }