@supercuts-walrus/backend 0.1.0 → 0.1.1

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.
@@ -69,7 +69,7 @@ function writePolicy(home, config) {
69
69
  const base = { ...mandate };
70
70
  delete base.allowedCapabilityIds;
71
71
  delete base.allowedCapabilityClasses;
72
- const next = { ...base, version: "supercuts.mandate.v2", allowedCommands: [...new Set([...mandate.allowedCommands, "deal.execute", "deal.get", "deal.resume", "deal.evidence", "deal.delivery", "deal.catalog", "deal.policy", "settlement.plan", "memory.inventory"])], allowedCounterparties: config.allowedCounterparties ?? [], perDealCeiling: config.perDealCeiling, totalRunBudget: config.totalBudget, expiresAt: config.expiresAt, requiredPrivacy: "any", requireSettlementPlan: true, allowedRails: config.allowPublicX402 === true ? ["shielded", "x402"] : ["shielded"], allowPublicX402: config.allowPublicX402 === true, ...(config.allowedCapabilityIds ? { allowedCapabilityIds: config.allowedCapabilityIds } : {}), ...(config.allowedCapabilityClasses ? { allowedCapabilityClasses: config.allowedCapabilityClasses } : {}) };
72
+ const next = { ...base, version: "supercuts.mandate.v2", allowedCommands: [...new Set([...mandate.allowedCommands, "deal.execute", "deal.get", "deal.resume", "deal.evidence", "deal.delivery", "deal.catalog", "deal.policy", "settlement.plan", "memory.inventory", "remote.research"])], allowedCounterparties: config.allowedCounterparties ?? [], perDealCeiling: config.perDealCeiling, totalRunBudget: config.totalBudget, expiresAt: config.expiresAt, requiredPrivacy: "any", requireSettlementPlan: true, allowedRails: config.allowPublicX402 === true ? ["shielded", "x402"] : ["shielded"], allowPublicX402: config.allowPublicX402 === true, ...(config.allowedCapabilityIds ? { allowedCapabilityIds: config.allowedCapabilityIds } : {}), ...(config.allowedCapabilityClasses ? { allowedCapabilityClasses: config.allowedCapabilityClasses } : {}) };
73
73
  writeFileSync(path, `${JSON.stringify(next, null, 2)}\n`, { mode: 0o600 });
74
74
  chmodSync(path, 0o600);
75
75
  writeFileSync(join(home, "deal-sellers.json"), `${JSON.stringify(config.directoryUrl ? { directoryUrl: origins([config.directoryUrl])[0], origins: config.sellerOrigins?.length ? origins(config.sellerOrigins) : [] } : origins(config.sellerOrigins ?? []), null, 2)}\n`, { mode: 0o600 });
@@ -206,7 +206,7 @@ export async function approveBuyerSeller(home, capabilityId) {
206
206
  profile.policy.allowedCounterparties = updated.allowedCounterparties;
207
207
  writeFileSync(path, `${JSON.stringify(updated, null, 2)}\n`, { mode: 0o600 });
208
208
  writeFileSync(profilePath, `${JSON.stringify(profile, null, 2)}\n`, { mode: 0o600 });
209
- return { capabilityId, sellerId: registration.manifest.sellerId, approved: true };
209
+ return { capabilityId, sellerId: registration.manifest.sellerId, approved: true, manifestHash: registration.manifest.manifestHash, description: registration.manifest.description, priceBand: registration.manifest.priceBand, asset: registration.manifest.asset, supportedRails: registration.manifest.supportedRails, expiresAt: registration.manifest.expiresAt };
210
210
  }
211
211
  export async function addBuyerPrivacyRule(home, capabilityId) {
212
212
  const app = new Phase3App(home);
package/dist/cli.js CHANGED
@@ -2,18 +2,21 @@
2
2
  import { Phase3App, errorOutput, homePath, initialize, parseArgs } from "./phase3.js";
3
3
  import { addBuyerPrivacyRule, approveBuyerSeller, createBuyerSession, promptBuyerSetup, renewBuyer, setupBuyer } from "./buyer-setup.js";
4
4
  import { onboardingDoctor } from "./onboarding-doctor.js";
5
+ import { executeFunding, prepareFunding } from "./funding.js";
6
+ import { packageHostedSeller } from "./seller-package.js";
5
7
  const words = process.argv.slice(2);
6
8
  let command = words.shift() ?? "";
7
- if (["wallet", "offer", "pay", "resource", "memory", "audit", "capability", "settlement", "setup", "policy", "onboarding"].includes(command))
9
+ if (["wallet", "offer", "pay", "resource", "memory", "audit", "capability", "settlement", "setup", "policy", "onboarding", "onboard", "funding", "seller"].includes(command))
8
10
  command += `.${words.shift() ?? ""}`;
9
11
  try {
10
12
  if (!command || command === "help" || command === "--help" || command === "setup.") {
11
- process.stdout.write(`${JSON.stringify({ schemaVersion: "supercuts.help.v1", ok: true, commands: ["setup buyer", "setup renew", "setup session", "init", "doctor", "status", "wallet", "offer", "pay", "memory", "capability", "audit"], setup: "Run supercuts setup buyer --home <private-directory> for guided live Testnet setup. Run supercuts init --role <buyer|seller|auditor> --mock --home <private-directory> for a local non-funded profile. Keep each role in a separate owner-only home.", live: "Supply the pinned Testnet deployment config, owner-only credential files, Sui and Tatum RPC endpoints, Walrus endpoints, local proving artifacts, and RapidSnark. Fund only the buyer address printed by supercuts-demo setup." })}\n`);
13
+ process.stdout.write(`${JSON.stringify({ schemaVersion: "supercuts.help.v1", ok: true, commands: ["onboard buyer", "funding prepare", "funding execute", "seller package", "setup buyer", "setup renew", "setup session", "init", "doctor", "status", "wallet", "offer", "pay", "memory", "capability", "audit"], setup: "Run supercuts onboard buyer --home <private-directory> for guided live Testnet setup. Use seller package to validate and prepare an operator-assisted hosted seller bundle.", live: "Use funding prepare to calculate public gas/payment and shielded deposit requirements. Funding execute requires explicit confirmation and funds only the generated buyer address." })}\n`);
12
14
  process.exit(0);
13
15
  }
14
16
  const args = parseArgs(words);
15
17
  const home = homePath(args);
16
- const result = command === "init" ? initialize(home, args) : command === "setup.buyer" ? { schemaVersion: "supercuts.setup.v1", ok: true, command, data: typeof args.config === "string" ? setupBuyer(home, args.config) : await promptBuyerSetup(home) } : 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 === "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 {
18
+ 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"] } : {}) };
19
+ 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.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 {
17
20
  return await app.execute(command, args);
18
21
  }
19
22
  finally {
package/dist/demo.js CHANGED
@@ -23,6 +23,7 @@ const value = (name, fallback) => typeof args[name] === "string" ? args[name] :
23
23
  const stateDir = () => resolve(value("state-dir", ".supercuts-demo"));
24
24
  const homes = (root) => ({ buyer: join(root, "buyer"), publicSeller: join(root, "public-seller"), secondPublicSeller: join(root, "second-public-seller"), confidentialSeller: join(root, "confidential-seller"), auditor: join(root, "auditor") });
25
25
  const send = (data) => process.stdout.write(`${JSON.stringify({ schemaVersion: "supercuts.demo.v1", ok: true, command, data })}\n`);
26
+ const help = () => ({ commands: ["setup", "doctor", "seed-policy", "reuse-memories", "live", "replay", "check"], setup: "Run supercuts-demo setup --state-dir <owner-only-directory> with the pinned live configuration. Fund only the buyer address in funding[0].address.", live: "The live demo uses Sui Testnet and Mainnet-backed Walrus services; mock receipts are never settlement evidence." });
26
27
  function keyAddress(path) { const decoded = decodeSuiPrivateKey(readFileSync(path, "utf8").trim()); if (decoded.scheme !== "ED25519")
27
28
  fail("SIGNER_UNSUPPORTED", "demo requires ED25519 Sui keys"); return Ed25519Keypair.fromSecretKey(decoded.secretKey).toSuiAddress(); }
28
29
  function createKey(path) { if (!existsSync(path))
@@ -48,20 +49,51 @@ async function setup() {
48
49
  fail("DEMO_ROLE_PROFILE_MISMATCH", "demo setup needs fresh isolated live v2 role homes; choose a new --state-dir instead of reusing v1 or mock profiles");
49
50
  }
50
51
  const workflowPath = typeof args.workflow === "string" ? resolve(args.workflow) : !mock && typeof args["seller-url"] !== "string" ? resolve(value("workflow")) : undefined;
52
+ let activeCapabilities = [];
51
53
  if (workflowPath) {
52
- const workflow = JSON.parse(readFileSync(workflowPath, "utf8")), capabilities = workflow.capabilities ?? [];
53
- if (!capabilities.length)
54
+ const workflow = JSON.parse(readFileSync(workflowPath, "utf8")), configured = workflow.capabilities ?? [];
55
+ if (!configured.length)
54
56
  fail("DEMO_CAPABILITIES_REQUIRED", "live workflow must provide capability manifests for the buyer mandate");
55
- for (const manifest of capabilities)
57
+ for (const manifest of configured)
56
58
  validateCapabilityManifest(manifest);
59
+ let capabilities = configured;
60
+ if (!mock && typeof args["seller-url"] === "string") {
61
+ const response = await fetch(new URL("/v1/catalog", new URL(args["seller-url"])), { signal: AbortSignal.timeout(90_000) });
62
+ if (!response.ok)
63
+ fail("SELLER_SERVICE_UNAVAILABLE", `remote seller catalog returned ${response.status}`);
64
+ const served = await response.json();
65
+ const allowedIds = new Set(configured.map((item) => item.capabilityId)), entries = (served.capabilities ?? []).filter((item) => item.manifest && allowedIds.has(item.manifest.capabilityId));
66
+ if (!entries.length || entries.some((item) => !item.manifest || typeof item.blobId !== "string"))
67
+ fail("SELLER_CATALOG_INVALID", "remote seller catalog has no configured capabilities with Walrus identities");
68
+ const app = new Phase3App(roles.buyer);
69
+ try {
70
+ const discovered = (await app.execute("capability.discover", { "blob-ids": Buffer.from(JSON.stringify(entries.map((item) => item.blobId))).toString("base64url") })).data.capabilities;
71
+ const verified = new Map(discovered.map((item) => [`${item.capabilityId}:${item.blobId}`, item.manifestHash]));
72
+ if (entries.some((item) => verified.get(`${item.manifest.capabilityId}:${item.blobId}`) !== item.manifest.manifestHash))
73
+ fail("SELLER_CATALOG_INVALID", "remote seller catalog differs from its Walrus manifests");
74
+ }
75
+ finally {
76
+ app.close();
77
+ }
78
+ capabilities = entries.map((item) => item.manifest);
79
+ }
80
+ activeCapabilities = capabilities;
57
81
  const mandatePath = join(roles.buyer, "mandate.json"), mandate = JSON.parse(readFileSync(mandatePath, "utf8"));
58
82
  mandate.requireSettlementPlan = true;
59
83
  mandate.allowedCommands = [...new Set([...(Array.isArray(mandate.allowedCommands) ? mandate.allowedCommands : []), "settlement.plan"])];
84
+ mandate.allowedCounterparties = [...new Set(capabilities.map((item) => item.sellerId))];
60
85
  mandate.allowedCapabilityIds = [...new Set(capabilities.map((item) => item.capabilityId))];
61
86
  mandate.allowedCapabilityClasses = [...new Set(capabilities.map((item) => item.capability))];
62
87
  mandate.allowedRails = [...new Set(capabilities.flatMap((item) => item.supportedRails))];
63
88
  writeFileSync(mandatePath, `${JSON.stringify(mandate, null, 2)}\n`, { mode: 0o600 });
64
89
  chmodSync(mandatePath, 0o600);
90
+ const buyerProfilePath = join(roles.buyer, "profile.json"), buyerProfile = JSON.parse(readFileSync(buyerProfilePath, "utf8"));
91
+ if (!buyerProfile.policy)
92
+ fail("DEMO_ROLE_PROFILE_MISMATCH", "buyer profile has no payment policy");
93
+ const buyerPolicy = buyerProfile.policy;
94
+ buyerPolicy.allowedCounterparties = [...new Set(capabilities.map((item) => item.sellerId))];
95
+ writeFileSync(buyerProfilePath, `${JSON.stringify(buyerProfile, null, 2)}\n`, { mode: 0o600 });
96
+ chmodSync(buyerProfilePath, 0o600);
65
97
  }
66
98
  const mcpScript = join(dirname(fileURLToPath(import.meta.url)), "mcp.js");
67
99
  const mcp = { mcpServers: Object.fromEntries(Object.entries(roles).map(([name, home]) => [`supercuts-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}`, { command: process.execPath, args: [mcpScript], env: { SUPERCUTS_HOME: home } }])) };
@@ -75,8 +107,10 @@ async function setup() {
75
107
  writeFileSync(join(root, "buyer-codex.toml"), codex, { mode: 0o600 });
76
108
  }
77
109
  const remote = typeof args["seller-url"] === "string";
78
- writeFileSync(join(root, "run.json"), `${JSON.stringify({ schema: "supercuts.demo.run.v1", createdAt: new Date().toISOString(), runId, ...(remote ? { eventPath } : {}), mock, homes: roles, addresses }, null, 2)}\n`, { mode: 0o600 });
79
- return { stateDir: root, mock, funding: mock ? [] : [{ address: addresses.buyer, needs: ["Testnet SUI gas", "configured x402 asset", "shielded deposit value"] }], sellerPayTo: addresses.publicSeller, sellerAddresses: { publicA: addresses.publicSeller, publicB: addresses.secondPublicSeller, analysis: addresses.confidentialSeller }, files: { mcp: join(root, "mcp.json"), run: join(root, "run.json"), events: remote ? eventPath : join(root, "events.jsonl"), ...(remote ? { codex: join(root, "buyer-codex.toml") } : {}) } };
110
+ writeFileSync(join(root, "run.json"), `${JSON.stringify({ schema: "supercuts.demo.run.v1", createdAt: new Date().toISOString(), runId, ...(remote ? { eventPath, sellerUrl: new URL(String(args["seller-url"])).toString() } : {}), mock, homes: roles, addresses }, null, 2)}\n`, { mode: 0o600 });
111
+ const publicSellers = activeCapabilities.filter((item) => item.capability === "sui.market-brief"), analysisSeller = activeCapabilities.find((item) => item.capability === "sui.competitor-analysis");
112
+ const settlementSellers = activeCapabilities.length ? { publicA: publicSellers[0]?.sellerId, publicB: publicSellers[1]?.sellerId, analysis: analysisSeller?.sellerId } : { publicA: addresses.publicSeller, publicB: addresses.secondPublicSeller, analysis: addresses.confidentialSeller };
113
+ return { stateDir: root, mock, funding: mock ? [] : [{ address: addresses.buyer, needs: ["Testnet SUI gas", "configured x402 asset", "shielded deposit value"] }], sellerPayTo: settlementSellers.publicA, sellerAddresses: settlementSellers, localRoleAddresses: { publicA: addresses.publicSeller, publicB: addresses.secondPublicSeller, analysis: addresses.confidentialSeller, auditor: addresses.auditor }, files: { mcp: join(root, "mcp.json"), run: join(root, "run.json"), events: remote ? eventPath : join(root, "events.jsonl"), ...(remote ? { codex: join(root, "buyer-codex.toml") } : {}) } };
80
114
  }
81
115
  async function doctor() {
82
116
  if (typeof args["buyer-home"] === "string") {
@@ -103,6 +137,11 @@ async function doctor() {
103
137
  const report = (await app.execute("doctor", {})).data;
104
138
  if (report.readyForDemo === false)
105
139
  fail("DEMO_PREREQUISITES_MISSING", `${name}: ${report.fixes.join(" ")}`);
140
+ if (name === "buyer") {
141
+ const mandate = loadMandate(home), profileSellers = [...app.profile.policy.allowedCounterparties].sort(), mandateSellers = [...mandate.allowedCounterparties].sort();
142
+ if (JSON.stringify(profileSellers) !== JSON.stringify(mandateSellers))
143
+ fail("POLICY_COUNTERPARTY_MISMATCH", "buyer mandate and payment profile allow different sellers; rerun setup refresh before funding");
144
+ }
106
145
  reports[name] = report;
107
146
  }
108
147
  finally {
@@ -114,16 +153,24 @@ async function doctor() {
114
153
  const base = new URL(args["seller-url"]), health = await fetch(new URL("/health", base), { signal: AbortSignal.timeout(90_000) }), catalog = await fetch(new URL("/v1/catalog", base), { signal: AbortSignal.timeout(30_000) });
115
154
  if (!health.ok || !catalog.ok)
116
155
  fail("SELLER_SERVICE_UNAVAILABLE", "remote seller health or catalog check failed");
156
+ const healthBody = await health.json(), buyerSigner = String(reports.buyer?.signer ?? ""), accessUrl = new URL("/v1/access", base);
157
+ accessUrl.searchParams.set("buyerAddress", buyerSigner);
158
+ const access = await fetch(accessUrl, { signal: AbortSignal.timeout(30_000) });
159
+ if (!access.ok)
160
+ fail("SELLER_SERVICE_UNAVAILABLE", "remote seller access preflight failed");
161
+ const accessBody = await access.json();
162
+ if (accessBody.allowed !== true)
163
+ fail("SELLER_BUYER_FORBIDDEN", "remote seller does not currently accept this buyer; use open access or complete owner enrollment before funding");
117
164
  const body = await catalog.json(), capabilities = body.capabilities ?? [];
118
165
  if (capabilities.filter((item) => item.manifest?.capability === "sui.market-brief").length < 2 || !capabilities.some((item) => item.manifest?.capability === "sui.competitor-analysis"))
119
166
  fail("SELLER_CATALOG_INVALID", "remote catalog needs two briefs and one competitor analysis");
120
- remote = { healthy: true, origin: base.origin, capabilities: capabilities.length };
167
+ remote = { healthy: true, origin: base.origin, capabilities: capabilities.length, buyerAccess: accessBody.mode ?? healthBody.buyerAccess ?? "unknown", buyerAllowed: true };
121
168
  }
122
169
  return { healthy: true, roles: reports, isolation: new Set(Object.values(roles)).size === 5, ...(remote ? { remote } : {}) };
123
170
  }
124
171
  async function seedPolicy() {
125
172
  const source = JSON.parse(readFileSync(resolve(value("workflow")), "utf8"));
126
- const manifest = source.capabilities?.find((item) => item.capability === "sui.competitor-analysis") ?? fail("DEMO_POLICY_SCOPE_INVALID", "workflow has no competitor analysis capability");
173
+ let manifest = source.capabilities?.find((item) => item.capability === "sui.competitor-analysis") ?? fail("DEMO_POLICY_SCOPE_INVALID", "workflow has no competitor analysis capability");
127
174
  validateCapabilityManifest(manifest);
128
175
  if (manifest.capability !== "sui.competitor-analysis")
129
176
  fail("DEMO_POLICY_SCOPE_INVALID", "privacy policy must target the competitor analysis capability");
@@ -131,6 +178,21 @@ async function seedPolicy() {
131
178
  try {
132
179
  if (app.profile.mock)
133
180
  fail("LIVE_PROFILE_REQUIRED", "saved demo privacy policy requires a live buyer profile");
181
+ const runPath = join(stateDir(), "run.json"), run = existsSync(runPath) ? JSON.parse(readFileSync(runPath, "utf8")) : {};
182
+ if (run.sellerUrl) {
183
+ const response = await fetch(new URL("/v1/catalog", run.sellerUrl), { signal: AbortSignal.timeout(90_000) });
184
+ if (!response.ok)
185
+ fail("SELLER_SERVICE_UNAVAILABLE", `remote seller catalog returned ${response.status}`);
186
+ const body = await response.json();
187
+ const entry = body.capabilities?.find((item) => item.manifest?.capabilityId === manifest.capabilityId && item.manifest.capability === "sui.competitor-analysis") ?? fail("DEMO_POLICY_SCOPE_INVALID", "remote catalog has no matching competitor analysis capability");
188
+ if (!entry.manifest || typeof entry.blobId !== "string")
189
+ fail("SELLER_CATALOG_INVALID", "remote analysis capability has no Walrus identity");
190
+ const liveManifest = entry.manifest, liveBlobId = entry.blobId;
191
+ const discovered = (await app.execute("capability.discover", { "blob-ids": Buffer.from(JSON.stringify([liveBlobId])).toString("base64url") })).data.capabilities;
192
+ if (discovered.length !== 1 || discovered[0].blobId !== liveBlobId || discovered[0].manifestHash !== liveManifest.manifestHash)
193
+ fail("SELLER_CATALOG_INVALID", "remote analysis capability differs from its Walrus manifest");
194
+ manifest = liveManifest;
195
+ }
134
196
  const memory = { version: "supercuts.settlement-memory.v1", kind: "privacy_rule", scope: { capabilityId: manifest.capabilityId, counterparty: manifest.sellerId, network: "sui:testnet", asset: manifest.asset }, createdAt: Date.now(), signal: { requiresShielded: true } };
135
197
  const parameters = { "operation-id": value("operation-id", `privacy-${manifest.capabilityId}`), memory: Buffer.from(JSON.stringify(memory)).toString("base64url") };
136
198
  const token = app.approve(app.preview("memory.policy", parameters).challengeId);
@@ -440,7 +502,7 @@ async function replay() {
440
502
  return { label: "recorded funded run", freshTransaction: false, events };
441
503
  }
442
504
  try {
443
- const data = command === "setup" ? await setup() : command === "doctor" ? await doctor() : command === "seed-policy" ? await seedPolicy() : command === "reuse-memories" ? reuseMemories() : command === "check" ? await check() : command === "live" ? await live() : command === "replay" ? await replay() : fail("COMMAND_UNKNOWN", "use setup, doctor, seed-policy, reuse-memories, live, replay, or check");
505
+ const data = ["help", "--help", "-h"].includes(command) ? help() : command === "setup" ? await setup() : command === "doctor" ? await doctor() : command === "seed-policy" ? await seedPolicy() : command === "reuse-memories" ? reuseMemories() : command === "check" ? await check() : command === "live" ? await live() : command === "replay" ? await replay() : fail("COMMAND_UNKNOWN", "use setup, doctor, seed-policy, reuse-memories, live, replay, or check");
444
506
  send(data);
445
507
  }
446
508
  catch (error) {
@@ -0,0 +1,38 @@
1
+ export interface FundingPlan {
2
+ address: string;
3
+ publicBalanceMist: string;
4
+ shieldedBalanceMist: string;
5
+ publicPaymentMist: string;
6
+ shieldedPaymentMist: string;
7
+ gasReserveMist: string;
8
+ changeReserveMist: string;
9
+ poolDepositMist: string;
10
+ requiredPublicMist: string;
11
+ publicShortfallMist: string;
12
+ faucetRequired: boolean;
13
+ depositRequired: boolean;
14
+ }
15
+ export declare function calculateFundingPlan(input: {
16
+ address: string;
17
+ publicBalance: bigint;
18
+ shieldedBalance: bigint;
19
+ publicPayment: bigint;
20
+ shieldedPayment: bigint;
21
+ gasReserve: bigint;
22
+ changeReserve: bigint;
23
+ }): FundingPlan;
24
+ export declare function prepareFunding(home: string, values: {
25
+ publicPayment: string;
26
+ shieldedPayment: string;
27
+ gasReserve?: string;
28
+ changeReserve?: string;
29
+ }): Promise<FundingPlan>;
30
+ export declare function executeFunding(home: string, values: {
31
+ publicPayment: string;
32
+ shieldedPayment: string;
33
+ gasReserve?: string;
34
+ changeReserve?: string;
35
+ operationId: string;
36
+ requestFaucet: boolean;
37
+ confirmed: boolean;
38
+ }): Promise<Record<string, unknown>>;
@@ -0,0 +1,78 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { createInterface } from "node:readline/promises";
3
+ import { fail } from "./errors.js";
4
+ import { Phase3App } from "./phase3.js";
5
+ import { writeClient } from "./sui.js";
6
+ const amount = (value, name) => /^[0-9]+$/.test(value) ? BigInt(value) : fail("FUNDING_INPUT_INVALID", `${name} must be unsigned MIST`);
7
+ const POOL_DEPOSIT_DENOMINATIONS = [10000000n, 100000000n];
8
+ const standardPoolDeposit = (shortfall) => {
9
+ if (shortfall === 0n)
10
+ return 0n;
11
+ const denomination = POOL_DEPOSIT_DENOMINATIONS.find((candidate) => candidate >= shortfall);
12
+ return denomination ?? fail("FUNDING_PLAN_UNSUPPORTED", `shielded funding needs ${shortfall} MIST, above the largest supported single deposit of ${POOL_DEPOSIT_DENOMINATIONS.at(-1)} MIST`);
13
+ };
14
+ export function calculateFundingPlan(input) {
15
+ const shieldedTarget = input.shieldedPayment > 0n ? input.shieldedPayment + input.changeReserve : 0n;
16
+ const poolShortfall = shieldedTarget > input.shieldedBalance ? shieldedTarget - input.shieldedBalance : 0n;
17
+ const poolDeposit = standardPoolDeposit(poolShortfall);
18
+ const requiredPublic = input.publicPayment + poolDeposit + input.gasReserve;
19
+ const publicShortfall = requiredPublic > input.publicBalance ? requiredPublic - input.publicBalance : 0n;
20
+ return { address: input.address, publicBalanceMist: input.publicBalance.toString(), shieldedBalanceMist: input.shieldedBalance.toString(), publicPaymentMist: input.publicPayment.toString(), shieldedPaymentMist: input.shieldedPayment.toString(), gasReserveMist: input.gasReserve.toString(), changeReserveMist: input.changeReserve.toString(), poolDepositMist: poolDeposit.toString(), requiredPublicMist: requiredPublic.toString(), publicShortfallMist: publicShortfall.toString(), faucetRequired: publicShortfall > 0n, depositRequired: poolDeposit > 0n };
21
+ }
22
+ export async function prepareFunding(home, values) {
23
+ const publicPayment = amount(values.publicPayment, "public payment"), shieldedPayment = amount(values.shieldedPayment, "shielded payment");
24
+ const gasReserve = amount(values.gasReserve ?? "100000000", "gas reserve"), changeReserve = amount(values.changeReserve ?? "1000000", "change reserve");
25
+ const app = new Phase3App(home);
26
+ try {
27
+ if (app.profile.role !== "buyer" || app.profile.mock)
28
+ fail("LIVE_PROFILE_REQUIRED", "funding requires a live buyer role home");
29
+ const doctor = await app.execute("doctor", {}), address = String(doctor.data.signer ?? "");
30
+ const endpoint = app.profile.live?.writeGrpcEndpoint ?? fail("LIVE_PROFILE_INVALID", "buyer write RPC is missing");
31
+ const [{ balance }, shielded] = await Promise.all([writeClient(endpoint).getBalance({ owner: address, coinType: "0x2::sui::SUI" }), app.execute("wallet.balance", {})]);
32
+ return calculateFundingPlan({ address, publicBalance: BigInt(balance.balance), shieldedBalance: BigInt(String(shielded.data.available ?? "0")), publicPayment, shieldedPayment, gasReserve, changeReserve });
33
+ }
34
+ finally {
35
+ app.close();
36
+ }
37
+ }
38
+ export async function executeFunding(home, values) {
39
+ if (!values.confirmed)
40
+ fail("FUNDING_CONFIRMATION_REQUIRED", "funding execution requires --confirm after reviewing the plan");
41
+ let plan = await prepareFunding(home, values);
42
+ if (plan.faucetRequired) {
43
+ if (!values.requestFaucet)
44
+ 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" });
46
+ for (let attempt = 0; attempt < 15; attempt++) {
47
+ await new Promise((resolve) => setTimeout(resolve, 2_000));
48
+ plan = await prepareFunding(home, values);
49
+ if (!plan.faucetRequired)
50
+ break;
51
+ }
52
+ if (plan.faucetRequired)
53
+ fail("FAUCET_ALLOCATION_INSUFFICIENT", `the public faucet allocation did not reach the required ${plan.requiredPublicMist} MIST; wait for finality and inspect funding status before requesting another allocation`);
54
+ }
55
+ if (!plan.depositRequired)
56
+ return { funded: true, faucetRequested: values.requestFaucet, deposit: "not-required", plan };
57
+ if (!values.operationId)
58
+ fail("ARGUMENT_REQUIRED", "funding execute requires --operation-id for safe retry");
59
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
60
+ try {
61
+ if (process.stdin.isTTY) {
62
+ const answer = (await rl.question(`Deposit ${plan.poolDepositMist} MIST into the experimental Testnet pool? type yes: `)).trim().toLowerCase();
63
+ if (answer !== "yes")
64
+ fail("FUNDING_CANCELLED", "pool deposit was not confirmed");
65
+ }
66
+ }
67
+ finally {
68
+ rl.close();
69
+ }
70
+ const app = new Phase3App(home);
71
+ try {
72
+ const deposit = await app.execute("wallet.deposit", { amount: plan.poolDepositMist, "operation-id": values.operationId, authorize: true });
73
+ return { funded: true, faucetRequested: values.requestFaucet, deposit: deposit.data, plan: await prepareFunding(home, values) };
74
+ }
75
+ finally {
76
+ app.close();
77
+ }
78
+ }
@@ -5,6 +5,8 @@ import { Groth16Prover, type ProverConfig } from "./prover.js";
5
5
  import { type QueuedExit } from "./v2-exits.js";
6
6
  import { TatumLedger } from "./tatum.js";
7
7
  import type { RoleStore } from "./store.js";
8
+ export declare function x402ResourceFailure(status: number, response: Uint8Array): string;
9
+ export declare function timestampExpirationUnsupported(value: unknown): boolean;
8
10
  export interface LiveRuntimeProfile {
9
11
  role: "buyer" | "seller" | "auditor";
10
12
  packageId: string;
@@ -52,6 +54,7 @@ export declare class LiveRuntime {
52
54
  private existingSigned;
53
55
  private submit;
54
56
  private confirmTatum;
57
+ paymentStatus(operationId: string, reconcile?: boolean): Promise<Record<string, unknown>>;
55
58
  private prepareOpening;
56
59
  deposit(amount: bigint, operationId: string): Promise<Record<string, unknown>>;
57
60
  recover(): Promise<Record<string, unknown>>;
@@ -10,6 +10,38 @@ import { buildDeposit, buildPrivateTransfer, buildWithdraw, writeClient } from "
10
10
  import { V2ExitQueue } from "./v2-exits.js";
11
11
  import { TatumLedger } from "./tatum.js";
12
12
  import { paymentBindingBytes, requirementsHash } from "./x402.js";
13
+ export function x402ResourceFailure(status, response) {
14
+ const fallback = `paid resource returned ${status}`;
15
+ let value;
16
+ try {
17
+ value = JSON.parse(Buffer.from(response).toString("utf8"));
18
+ }
19
+ catch {
20
+ return fallback;
21
+ }
22
+ if (!value || typeof value !== "object" || Array.isArray(value))
23
+ return fallback;
24
+ const record = value, code = typeof record.error === "string" ? record.error.replace(/\s+/g, " ").slice(0, 128) : "", message = typeof record.message === "string" ? record.message.replace(/\s+/g, " ").slice(0, 512) : "";
25
+ return code || message ? `${fallback}: ${[code, message].filter(Boolean).join(": ")}` : fallback;
26
+ }
27
+ export function timestampExpirationUnsupported(value) {
28
+ const messages = [];
29
+ const visit = (item, depth = 0) => {
30
+ if (depth > 3 || item == null)
31
+ return;
32
+ if (typeof item === "string")
33
+ messages.push(item);
34
+ else if (item instanceof Error) {
35
+ messages.push(item.message);
36
+ visit(item.cause, depth + 1);
37
+ }
38
+ else if (typeof item === "object")
39
+ for (const nested of Object.values(item))
40
+ visit(nested, depth + 1);
41
+ };
42
+ visit(value);
43
+ return messages.some((message) => /timestamp-based transaction expiration is not yet supported|timestamp.*expiration.*not supported/i.test(message));
44
+ }
13
45
  const hex = (value) => value.toString(16).padStart(64, "0");
14
46
  const bytes32 = (value) => Buffer.from(hex(value), "hex");
15
47
  const hash = (value) => createHash("sha256").update(value).digest("hex");
@@ -87,7 +119,7 @@ export class LiveRuntime {
87
119
  }
88
120
  if (this.profile.role === "buyer" && !this.store.getSecret("live:memory-config"))
89
121
  fixes.push("Configure an owner-only Walrus Memory file using the local 1536-dimensional embedding adapter.");
90
- return { healthy: true, readyForDemo: fixes.length === 0, fixes, runtime: "testnet-live", signer: this.signer.toSuiAddress(), babyJubPublicKey: identity.publicKey.map(String), x25519PublicKey: this.store.getSecret("identity:x25519-public")?.toString("base64"), rapidSnark: artifacts.rapidSnark, artifacts: artifacts.artifacts, poolObjectVerified: true, tatumServiceVerified: true };
122
+ return { role: this.profile.role, network: "testnet", packageId: this.profile.packageId, poolId: this.profile.poolId, asset: this.profile.coinType, verifierVersion: this.profile.verifierVersion, keyPermissions: "owner-only", rpc: this.profile.live.writeGrpcEndpoint, healthy: true, readyForDemo: fixes.length === 0, fixes, runtime: "testnet-live", signer: this.signer.toSuiAddress(), babyJubPublicKey: identity.publicKey.map(String), x25519PublicKey: this.store.getSecret("identity:x25519-public")?.toString("base64"), rapidSnark: artifacts.rapidSnark, artifacts: artifacts.artifacts, poolObjectVerified: true, tatumServiceVerified: true };
91
123
  }
92
124
  async balance() {
93
125
  const state = await this.index.sync();
@@ -165,6 +197,29 @@ export class LiveRuntime {
165
197
  async confirmTatum(digest, checkpoint) {
166
198
  assertTatumFinality(await this.tatum.getTransaction(digest), digest, checkpoint);
167
199
  }
200
+ async paymentStatus(operationId, reconcile = false) {
201
+ const row = this.store.db.prepare("SELECT digest,status,updated_at AS updatedAt FROM live_signed_transactions WHERE operation_id=?").get(operationId);
202
+ if (!row)
203
+ fail("OPERATION_NOT_FOUND", "payment operation was not found in the live transaction journal");
204
+ const payment = row;
205
+ const context = this.store.db.prepare("SELECT kind FROM live_operation_context WHERE operation_id=?").get(operationId);
206
+ const delivered = Boolean(this.store.db.prepare("SELECT 1 FROM live_x402_deliveries WHERE operation_id=?").get(operationId));
207
+ if (payment.status === "finalized")
208
+ return { operationId, status: "finalized", transactionDigest: payment.digest, kind: context?.kind ?? "unknown", deliveryStatus: delivered ? "delivered" : "not-applicable-or-pending", finalized: true, safeToResumeSameOperationId: true };
209
+ if (!reconcile)
210
+ return { operationId, status: payment.status, transactionDigest: payment.digest, kind: context?.kind ?? "unknown", deliveryStatus: delivered ? "delivered" : "pending", finalized: false, chainStatus: "unchecked", safeToResumeSameOperationId: true };
211
+ let transaction;
212
+ try {
213
+ transaction = await this.client.waitForTransaction({ digest: payment.digest, timeout: 5_000, include: { effects: true } });
214
+ }
215
+ catch {
216
+ return { operationId, status: payment.status, transactionDigest: payment.digest, kind: context?.kind ?? "unknown", deliveryStatus: delivered ? "delivered" : "pending", finalized: false, chainStatus: "not-found-or-pending", safeToResumeSameOperationId: true };
217
+ }
218
+ const checkpoint = assertSuiFinality(transaction, payment.digest);
219
+ await this.confirmTatum(payment.digest, checkpoint);
220
+ this.store.db.prepare("UPDATE live_signed_transactions SET status='finalized',updated_at=? WHERE operation_id=?").run(Date.now(), operationId);
221
+ return { operationId, status: "finalized", transactionDigest: payment.digest, kind: context?.kind ?? "unknown", deliveryStatus: delivered ? "delivered" : "pending", finalized: true, checkpoint, tatumVerified: true, safeToResumeSameOperationId: true };
222
+ }
168
223
  prepareOpening(opening, role, operationId, leaves) {
169
224
  const commitment = hex(opening.commitment), key = `note:${commitment}`;
170
225
  this.store.db.exec("BEGIN IMMEDIATE");
@@ -534,21 +589,40 @@ export class LiveRuntime {
534
589
  fail("POLICY_ASSET_FORBIDDEN", "challenged asset is not allowed");
535
590
  if (!this.profile.policy.allowedCounterparties.includes(payTo))
536
591
  fail("POLICY_COUNTERPARTY_FORBIDDEN", "challenged payee is not allowed");
537
- const extra = requirements.extra, requestId = String(extra?.requestId ?? operationId), challengeOperationId = String(extra?.operationId ?? operationId), nonce = String(extra?.nonce ?? "");
538
- if (challengeOperationId !== operationId || !nonce || String(extra?.requirementsHash ?? "") !== requirementsHash({ ...requirements, extra: { requestId, operationId: challengeOperationId, nonce } }))
539
- fail("X402_CHALLENGE_INVALID", "challenge does not bind this operation and request");
592
+ const extra = requirements.extra, requestId = String(extra?.requestId ?? operationId), challengeOperationId = String(extra?.operationId ?? operationId), nonce = String(extra?.nonce ?? ""), expiresAt = Number(extra?.expiresAt);
593
+ const unsignedRequirements = { ...requirements, extra: { requestId, operationId: challengeOperationId, nonce, expiresAt } };
594
+ if (challengeOperationId !== operationId || !nonce || !Number.isSafeInteger(expiresAt) || expiresAt <= Date.now() || String(extra?.requirementsHash ?? "") !== requirementsHash(unsignedRequirements))
595
+ fail("X402_CHALLENGE_INVALID", "challenge does not bind this operation, request, and expiry");
540
596
  bind?.({ amount: amount.toString(), counterparty: payTo, asset, endpoint: resourceUrl, requestId, requirementsHash: String(extra?.requirementsHash) });
541
597
  const tx = new (await import("@mysten/sui/transactions")).Transaction();
542
598
  tx.transferObjects([tx.coin({ balance: amount, type: asset })], payTo);
599
+ tx.setSender(this.signer.toSuiAddress());
600
+ await tx.build({ client: this.client });
601
+ const resolvedExpiration = tx.getData().expiration;
602
+ const validity = resolvedExpiration?.$kind === "ValidDuring" ? resolvedExpiration.ValidDuring : fail("X402_TRANSACTION_EXPIRY_INVALID", "Sui did not resolve a bounded transaction validity window");
603
+ if (validity.minEpoch == null || validity.maxEpoch == null || !validity.chain || validity.nonce == null)
604
+ fail("X402_TRANSACTION_EXPIRY_INVALID", "Sui resolved an incomplete transaction validity window");
605
+ tx.setExpiration({ ValidDuring: { minEpoch: validity.minEpoch, maxEpoch: validity.maxEpoch, minTimestamp: validity.minTimestamp ?? null, maxTimestamp: expiresAt, chain: validity.chain, nonce: validity.nonce } });
606
+ try {
607
+ const probe = await this.client.simulateTransaction({ transaction: tx, include: { effects: true } });
608
+ const simulated = probe.Transaction ?? probe.FailedTransaction;
609
+ if (!simulated?.status.success && timestampExpirationUnsupported(simulated?.status?.error))
610
+ tx.setExpiration({ ValidDuring: { minEpoch: validity.minEpoch, maxEpoch: validity.maxEpoch, minTimestamp: validity.minTimestamp ?? null, maxTimestamp: null, chain: validity.chain, nonce: validity.nonce } });
611
+ }
612
+ catch (error) {
613
+ if (!timestampExpirationUnsupported(error))
614
+ throw error;
615
+ tx.setExpiration({ ValidDuring: { minEpoch: validity.minEpoch, maxEpoch: validity.maxEpoch, minTimestamp: validity.minTimestamp ?? null, maxTimestamp: null, chain: validity.chain, nonce: validity.nonce } });
616
+ }
543
617
  const legacyIntentHash = hash(JSON.stringify({ domain: "supercuts-x402-v2", operationId, resourceUrl, payTo, asset, amount: amount.toString() }));
544
618
  const intentHash = hash(JSON.stringify({ domain: "supercuts-x402-v2", operationId, resourceUrl, payTo, asset, amount: amount.toString(), requestId, requirementsHash: String(extra?.requirementsHash) })), signed = await this.signed(operationId, intentHash, tx, legacyIntentHash);
545
- const unsignedBinding = { requestId, nonce, requirementsHash: requirementsHash({ ...requirements, extra: { requestId, operationId: challengeOperationId, nonce } }), transactionDigest: signed.digest, payer: this.signer.toSuiAddress() };
619
+ const unsignedBinding = { requestId, nonce, requirementsHash: requirementsHash(unsignedRequirements), transactionDigest: signed.digest, payer: this.signer.toSuiAddress() };
546
620
  const payerSignature = (await this.signer.signPersonalMessage(paymentBindingBytes(unsignedBinding))).signature;
547
621
  const paymentPayload = { x402Version: 2, resource, accepted: requirements, payload: { signature: signed.signature, transaction: Buffer.from(signed.transaction).toString("base64") } };
548
622
  const paid = await fetch(paymentUrl, { redirect: "error", headers: { "payment-signature": Buffer.from(JSON.stringify(paymentPayload)).toString("base64"), "x-supercuts-binding": Buffer.from(JSON.stringify({ ...unsignedBinding, payerSignature })).toString("base64") }, signal: AbortSignal.timeout(60_000) });
549
623
  const response = Buffer.from(await paid.arrayBuffer());
550
624
  if (!paid.ok)
551
- fail("X402_DELIVERY_FAILED", `paid resource returned ${paid.status}`);
625
+ fail("X402_DELIVERY_FAILED", x402ResourceFailure(paid.status, response));
552
626
  const settlementHeader = paid.headers.get("payment-response") ?? fail("X402_SETTLEMENT_INVALID", "payment-response header is missing");
553
627
  let settlement;
554
628
  try {
package/dist/mandate.js CHANGED
@@ -14,8 +14,8 @@ const definitions = {
14
14
  "wallet.balance": object({}, [], false, "Read event-derived private-note balance."),
15
15
  "wallet.recover": object({ commitment: string("Optional payment-note commitment."), amount: string("Optional matching amount.", { pattern: "^[1-9][0-9]*$" }) }, [], true, "Recover notes from pinned finalized pool events."),
16
16
  "wallet.withdraw": object({ commitment: string("Note commitment to spend."), recipient: string("Sui recipient address.") }, ["commitment"], true, "Withdraw one owned shielded note."),
17
- "wallet.exit.queue": object({ commitment: string("Recovered note commitment."), amount: string("Public exit amount in MIST.", { pattern: "^[1-9][0-9]*$" }), "exact-exit": boolean("Approve a nonstandard amount that is publicly visible."), "window-start": string("Exit window start as Unix milliseconds."), "window-end": string("Exit window end as Unix milliseconds.") }, ["commitment", "amount", "window-start", "window-end"], true, "Queue a v2 exit with a fresh payout address and owner-selected delayed window."),
18
- "wallet.exit.status": object({ "operation-id": string("Queued exit operation ID.") }, ["operation-id"], false, "Read a queued v2 exit without exposing its proof or note opening."),
17
+ "wallet.exit.queue": object({ commitment: string("Recovered note commitment."), amount: string("Public exit amount in MIST.", { pattern: "^[1-9][0-9]*$" }), "exact-exit": boolean("Approve a nonstandard amount that is publicly visible."), "window-start": string("Exit window start as Unix milliseconds."), "window-end": string("Exit window end as Unix milliseconds.") }, ["commitment", "amount", "window-start", "window-end"], true, "Queue a delayed pool exit to a fresh payout address."),
18
+ "wallet.exit.status": object({ "operation-id": string("Queued exit operation ID.") }, ["operation-id"], false, "Read a queued pool exit without exposing its proof or note opening."),
19
19
  "wallet.exit.finalize": object({ "exit-id": string("Queued exit operation ID."), digest: string("Finalized relayer transaction digest.") }, ["exit-id", "digest"], true, "Verify Sui and Tatum exit evidence and reconcile the owner note journal."),
20
20
  "offer.open": object({ session: string("Optional stable negotiation session ID.") }, [], true, "Open a durable negotiation session."),
21
21
  "offer.propose": object({ session: string("Negotiation session ID."), revision: string("Positive revision.", { pattern: "^[1-9][0-9]*$" }), terms: string("Plain-language or canonical JSON terms.") }, ["session", "revision", "terms"], true, "Submit an offer revision."),
@@ -24,7 +24,7 @@ const definitions = {
24
24
  "offer.show": object({ session: string("Negotiation session ID.") }, ["session"], false, "Read the latest durable offer revision."),
25
25
  "pay.quote": object({ rail: string("Requested settlement rail.", { enum: ["x402", "shielded"] }), amount: string("Unsigned base-unit amount.", { pattern: "^[1-9][0-9]*$" }), counterparty: string("Mandate-approved payee."), asset: string("Sui coin type.") }, ["rail", "amount", "counterparty"], false, "Evaluate payment policy without signing."),
26
26
  "pay.shielded": object({ session: string("Accepted agreement session."), amount: string("Unsigned base-unit amount.", { pattern: "^[1-9][0-9]*$" }), counterparty: string("Mandate-approved seller."), asset: string("Sui coin type."), "request-id": string("Bound delivery request ID."), "broadcast-only": boolean("Stop after broadcast for recovery testing."), "plan-hash": string("Approved settlement plan hash.", { pattern: "^[0-9a-f]{64}$" }) }, [], true, "Settle one accepted agreement through the experimental shielded pool."),
27
- "pay.x402": object({ "resource-url": string("Seller-owned protected resource URL."), amount: string("Unsigned base-unit amount.", { pattern: "^[1-9][0-9]*$" }), counterparty: string("Mandate-approved public payee."), asset: string("Sui coin type."), "request-id": string("Bound delivery request ID."), "broadcast-only": boolean("Stop after broadcast for recovery testing."), "plan-hash": string("Approved settlement plan hash.", { pattern: "^[0-9a-f]{64}$" }) }, [], true, "Complete a standard x402 v2 exact Sui payment challenge."),
27
+ "pay.x402": object({ "resource-url": string("Seller-owned protected resource URL."), amount: string("Unsigned base-unit amount.", { pattern: "^[1-9][0-9]*$" }), counterparty: string("Mandate-approved public payee."), asset: string("Sui coin type."), "request-id": string("Bound delivery request ID."), "broadcast-only": boolean("Stop after broadcast for recovery testing."), "plan-hash": string("Approved settlement plan hash.", { pattern: "^[0-9a-f]{64}$" }) }, [], true, "Make a public Sui payment through an x402 exact payment challenge."),
28
28
  "pay.status": object({ "operation-id": string("Payment operation identity."), reconcile: boolean("Reconcile an ambiguous broadcast.") }, ["operation-id"], false, "Read or reconcile a previously journaled payment."),
29
29
  "resource.serve": object({ "request-id": string("Unique request identity."), body: string("Base64-encoded artifact bytes.") }, ["request-id", "body"], true, "Bind one generated artifact to one request."),
30
30
  "resource.fetch": object({ "request-id": string("Bound request identity."), "operation-id": string("Finalized payment operation."), response: string("Base64-encoded paid response.") }, ["request-id", "operation-id", "response"], false, "Record delivery separately from payment evidence."),
@@ -43,13 +43,14 @@ const definitions = {
43
43
  "deal.delivery": object({ dealId: string("Existing delivered deal ID.") }, ["dealId"], false, "Read a verified paid artifact from the buyer's encrypted journal."),
44
44
  "remote.catalog": object({}, [], false, "Discover the signed remote seller catalog through Walrus."),
45
45
  "remote.rank-briefs": object({ budget: string("Total budget in MIST.", { pattern: "^[1-9][0-9]*$" }), topic: string("Sui research topic.", { maxLength: 256 }) }, ["budget", "topic"], false, "Compare eligible market briefs with a visible quality and price rubric."),
46
- "remote.negotiate": object({ capabilityId: string("Selected capability ID."), sessionId: string("Stable negotiation session ID."), runId: string("Demo run ID."), task: string("Private task summary.", { maxLength: 512 }), opening: string("Opening amount in MIST.", { pattern: "^[1-9][0-9]*$" }), maximum: string("Maximum amount in MIST.", { pattern: "^[1-9][0-9]*$" }), briefHash: string("Purchased brief SHA-256 if analyzing it.", { pattern: "^[0-9a-f]{64}$" }) }, ["capabilityId", "sessionId", "runId", "task", "opening", "maximum"], false, "Exchange an encrypted buyer offer and seller response through Walrus."),
47
- "remote.quote": object({ capabilityId: string("Selected capability ID."), operationId: string("Stable public payment operation ID."), requestId: string("Stable artifact request ID."), runId: string("Demo run ID."), task: string("Brief task summary.", { maxLength: 512 }), briefHash: string("Purchased brief SHA-256 for an analysis variant.", { pattern: "^[0-9a-f]{64}$" }) }, ["capabilityId", "operationId", "requestId", "runId", "task"], false, "Ask a remote seller for its exact x402 quote and protected resource URL."),
48
- "remote.purchase-brief": object({ capabilityId: string("Selected market brief capability ID."), operationId: string("Stable public payment operation ID."), requestId: string("Stable artifact request ID."), runId: string("Demo run ID."), task: string("Brief task summary.", { maxLength: 512 }), budget: string("Maximum brief spend in MIST.", { pattern: "^[1-9][0-9]*$" }) }, ["capabilityId", "operationId", "requestId", "runId", "task", "budget"], false, "Plan, buy, verify, and deliver the selected market brief through x402."),
49
- "remote.agreement": object({ capabilityId: string("Analysis capability ID."), operationId: string("Stable shielded payment operation ID."), sessionId: string("Stable negotiation session ID."), runId: string("Demo run ID."), briefHash: string("Delivered brief SHA-256.", { pattern: "^[0-9a-f]{64}$" }) }, ["capabilityId", "operationId", "sessionId", "runId", "briefHash"], false, "Request and accept a seller-signed analysis agreement bound to the purchased brief and saved privacy rule."),
46
+ "remote.negotiate": object({ capabilityId: string("Selected capability ID."), sessionId: string("Stable negotiation session ID."), runId: string("Run ID."), task: string("Private task summary.", { maxLength: 512 }), opening: string("Opening amount in MIST.", { pattern: "^[1-9][0-9]*$" }), maximum: string("Maximum amount in MIST.", { pattern: "^[1-9][0-9]*$" }), briefHash: string("Purchased brief SHA-256 if analyzing it.", { pattern: "^[0-9a-f]{64}$" }) }, ["capabilityId", "sessionId", "runId", "task", "opening", "maximum"], false, "Exchange an encrypted buyer offer and seller response through Walrus."),
47
+ "remote.quote": object({ capabilityId: string("Selected capability ID."), operationId: string("Stable public payment operation ID."), requestId: string("Stable artifact request ID."), runId: string("Run ID."), task: string("Brief task summary.", { maxLength: 512 }), briefHash: string("Purchased brief SHA-256 for an analysis variant.", { pattern: "^[0-9a-f]{64}$" }) }, ["capabilityId", "operationId", "requestId", "runId", "task"], false, "Ask a remote seller for its exact x402 quote and protected resource URL."),
48
+ "remote.purchase-brief": object({ capabilityId: string("Selected market brief capability ID."), operationId: string("Stable public payment operation ID."), requestId: string("Stable artifact request ID."), runId: string("Run ID."), task: string("Brief task summary.", { maxLength: 512 }), budget: string("Maximum brief spend in MIST.", { pattern: "^[1-9][0-9]*$" }) }, ["capabilityId", "operationId", "requestId", "runId", "task", "budget"], false, "Plan, buy, verify, and deliver the selected market brief through x402."),
49
+ "remote.agreement": object({ capabilityId: string("Analysis capability ID."), operationId: string("Stable shielded payment operation ID."), sessionId: string("Stable negotiation session ID."), runId: string("Run ID."), briefHash: string("Delivered brief SHA-256.", { pattern: "^[0-9a-f]{64}$" }) }, ["capabilityId", "operationId", "sessionId", "runId", "briefHash"], false, "Request and accept a seller-signed analysis agreement bound to the purchased brief and saved privacy rule."),
50
50
  "remote.settle-analysis": object({ sessionId: string("Accepted analysis session ID."), operationId: string("Stable shielded payment operation ID.") }, ["sessionId", "operationId"], false, "Settle the accepted analysis agreement through the approved shielded pool plan."),
51
51
  "remote.delivery": object({ sessionId: string("Accepted analysis session ID."), operationId: string("Finalized shielded payment operation ID.") }, ["sessionId", "operationId"], false, "Recover the seller payment and decrypt the delivered analysis."),
52
- "remote.events": object({ runId: string("Demo run ID.") }, ["runId"], false, "Read safe remote seller activity for the demo."),
52
+ "remote.events": object({ runId: string("Run ID.") }, ["runId"], false, "Read redacted remote seller activity for the run."),
53
+ "remote.research": object({ topic: string("Sui research topic.", { maxLength: 256 }), budget: string("Total workflow budget in MIST.", { pattern: "^[1-9][0-9]*$" }), runId: string("Optional active run ID; normally supplied by the MCP session.") }, ["topic", "budget"], true, "Rank, purchase, and deliver a public Sui brief plus its matching privacy-aware analysis using one resumable operation."),
53
54
  disclose: object({ "deal-id": string("Deal scope."), auditor: string("Designated auditor identity."), "payment-digest": string("Finalized payment digest."), "transcript-root": string("Authenticated agreement transcript root.") }, ["deal-id", "auditor", "payment-digest", "transcript-root"], true, "Create a deal- and auditor-scoped disclosure package."),
54
55
  "audit.verify": object({ package: string("Seal ciphertext or mock disclosure package."), auditor: string("This auditor identity."), "deal-id": string("Exact deal scope.") }, ["package", "auditor", "deal-id"], false, "Verify agreement and payment only; delivery remains a separate observation."),
55
56
  };
@@ -12,6 +12,13 @@ if (existsSync(join(home, "profile.json"))) {
12
12
  else {
13
13
  const send = (value) => process.stdout.write(`${JSON.stringify(value)}\n`);
14
14
  const rl = createInterface({ input: process.stdin });
15
+ let promoting = false;
16
+ const promote = () => {
17
+ if (promoting)
18
+ return;
19
+ promoting = true;
20
+ setImmediate(() => { rl.close(); void import("./mcp.js"); });
21
+ };
15
22
  rl.on("line", (line) => {
16
23
  if (Buffer.byteLength(line) > 1024 * 1024)
17
24
  return send({ jsonrpc: "2.0", id: null, error: { code: -32600, message: "Request exceeds 1 MiB" } });
@@ -29,11 +36,26 @@ else {
29
36
  if (request.method === "notifications/initialized")
30
37
  return;
31
38
  if (request.method === "tools/list")
32
- return send({ jsonrpc: "2.0", id: request.id, result: { tools: [{ name: "setup.status", description: "Explain how the owner completes one-time buyer setup locally. Do not request secrets in chat.", inputSchema: { type: "object", properties: {}, additionalProperties: false } }] } });
39
+ return send({ jsonrpc: "2.0", id: request.id, result: { tools: [
40
+ { name: "setup.status", description: "Explain how the owner completes one-time buyer setup locally. Do not request secrets in chat.", inputSchema: { type: "object", properties: {}, additionalProperties: false } },
41
+ { name: "setup.refresh", description: "Reload this MCP connection after the owner completes local setup.", inputSchema: { type: "object", properties: {}, additionalProperties: false } },
42
+ ] } });
33
43
  if (request.method === "tools/call" && request.params?.name === "setup.status") {
34
- const result = { schemaVersion: "supercuts.setup.v1", ok: true, configured: false, home, nextStep: `Run supercuts setup buyer --home ${JSON.stringify(home)} in a terminal, then restart this MCP connection. The command asks for policy limits, seller origins and local credential file paths. Do not paste keys or tokens into Codex.` };
44
+ const configured = existsSync(join(home, "profile.json"));
45
+ const result = { schemaVersion: "supercuts.setup.v1", ok: true, configured, home, nextStep: configured ? "Call setup.refresh to load the configured buyer tools without restarting the connector." : `Run supercuts onboard buyer --home ${JSON.stringify(home)} in a terminal, then call setup.refresh. The command asks for policy limits, seller origins and local credential file paths. Do not paste keys or tokens into Codex.` };
35
46
  return send({ jsonrpc: "2.0", id: request.id, result: { content: [{ type: "text", text: JSON.stringify(result) }], structuredContent: result } });
36
47
  }
48
+ if (request.method === "tools/call" && request.params?.name === "setup.refresh") {
49
+ if (!existsSync(join(home, "profile.json"))) {
50
+ const result = { schemaVersion: "supercuts.setup.v1", ok: false, error: { code: "SETUP_REQUIRED", message: "buyer setup is not complete", recovery: `Run supercuts onboard buyer --home ${JSON.stringify(home)} locally, then retry setup.refresh.` } };
51
+ return send({ jsonrpc: "2.0", id: request.id, result: { isError: true, content: [{ type: "text", text: JSON.stringify(result) }], structuredContent: result } });
52
+ }
53
+ const result = { schemaVersion: "supercuts.setup.v1", ok: true, configured: true, reloading: true };
54
+ send({ jsonrpc: "2.0", id: request.id, result: { content: [{ type: "text", text: JSON.stringify(result) }], structuredContent: result } });
55
+ send({ jsonrpc: "2.0", method: "notifications/tools/list_changed" });
56
+ promote();
57
+ return;
58
+ }
37
59
  return send({ jsonrpc: "2.0", id: request.id, error: { code: -32601, message: "Method not found" } });
38
60
  });
39
61
  }