@supercuts-walrus/backend 0.1.0 → 0.1.2

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.
Files changed (43) hide show
  1. package/README.md +28 -5
  2. package/config/phase1.testnet.json +20 -0
  3. package/config/proving-manifest.json +11 -0
  4. package/dist/buyer-setup.js +6 -6
  5. package/dist/cli.js +9 -3
  6. package/dist/deal-adapter.js +0 -2
  7. package/dist/deal-engine.js +5 -5
  8. package/dist/deal.js +2 -2
  9. package/dist/demo.js +70 -8
  10. package/dist/funding.d.ts +38 -0
  11. package/dist/funding.js +78 -0
  12. package/dist/index.d.ts +1 -0
  13. package/dist/index.js +1 -0
  14. package/dist/live-deal-ports.js +8 -1
  15. package/dist/live-runtime.d.ts +4 -1
  16. package/dist/live-runtime.js +83 -11
  17. package/dist/live-seller-transport.js +5 -1
  18. package/dist/mandate.js +10 -9
  19. package/dist/mcp-bootstrap.js +24 -2
  20. package/dist/mcp.js +39 -19
  21. package/dist/onboarding-doctor.js +21 -5
  22. package/dist/phase3.js +38 -6
  23. package/dist/remote-buyer.d.ts +18 -0
  24. package/dist/remote-buyer.js +59 -5
  25. package/dist/role-lock.d.ts +8 -0
  26. package/dist/role-lock.js +54 -0
  27. package/dist/seller-catalog-generate.js +4 -4
  28. package/dist/seller-directory-cli.js +2 -1
  29. package/dist/seller-live-cli.js +10 -1
  30. package/dist/seller-package.d.ts +9 -0
  31. package/dist/seller-package.js +50 -0
  32. package/dist/seller-provision.js +15 -1
  33. package/dist/seller-research.d.ts +50 -0
  34. package/dist/seller-research.js +210 -0
  35. package/dist/seller-scaffold.d.ts +1 -0
  36. package/dist/seller-scaffold.js +35 -0
  37. package/dist/seller-service.d.ts +1 -1
  38. package/dist/seller-service.js +14 -21
  39. package/dist/seller-setup.d.ts +3 -0
  40. package/dist/seller-setup.js +107 -0
  41. package/dist/x402-resource-service.d.ts +2 -0
  42. package/dist/x402-resource-service.js +36 -17
  43. package/package.json +2 -1
@@ -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");
@@ -57,8 +89,8 @@ export class LiveRuntime {
57
89
  const engine = await ProtocolEngine.create({ deploymentDomain: profile.live.deploymentDomain, poolId: profile.poolId, coinTypeHash: profile.live.coinTypeHash, verifierVersion: profile.verifierVersion, initialRoot: profile.live.initialRoot });
58
90
  return new LiveRuntime(profile, store, new TatumLedger(profile.live.tatumGrpcEndpoint, tatumKey), Ed25519Keypair.fromSecretKey(decoded.secretKey), engine);
59
91
  }
60
- async doctor() {
61
- const artifacts = this.prover.doctor();
92
+ async doctor(proofsRequired = true) {
93
+ const artifacts = proofsRequired ? this.prover.doctor() : { rapidSnark: false, artifacts: {} };
62
94
  const [service, suiObject] = await Promise.all([this.tatum.getServiceInfo(), this.client.getObject({ objectId: this.profile.poolId, include: { json: true } })]);
63
95
  if (!suiObject.object)
64
96
  fail("POOL_OBJECT_MISSING", "pinned Testnet pool object is unavailable; check the Sui write RPC and deployment config");
@@ -67,7 +99,7 @@ export class LiveRuntime {
67
99
  const babySeed = this.store.getSecret("identity:babyjub-seed") ?? fail("IDENTITY_MISSING", "BabyJub identity is missing");
68
100
  const identity = this.engine.identity(babySeed);
69
101
  const fixes = [];
70
- if (!artifacts.rapidSnark)
102
+ if (proofsRequired && !artifacts.rapidSnark)
71
103
  fixes.push("Set an executable RapidSnark path in the role profile for v2 proofs.");
72
104
  if (!this.profile.live.walrusPublisherUrl)
73
105
  fixes.push("Configure the Walrus publisher URL for capability and transcript writes.");
@@ -85,9 +117,7 @@ export class LiveRuntime {
85
117
  fixes.push(`Walrus ${name} is unreachable; check the configured URL and network access.`);
86
118
  }
87
119
  }
88
- if (this.profile.role === "buyer" && !this.store.getSecret("live:memory-config"))
89
- 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 };
120
+ 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"), memoryConfigured: Boolean(this.store.getSecret("live:memory-config")), proofsRequired, rapidSnark: artifacts.rapidSnark, artifacts: artifacts.artifacts, poolObjectVerified: true, tatumServiceVerified: true };
91
121
  }
92
122
  async balance() {
93
123
  const state = await this.index.sync();
@@ -165,6 +195,29 @@ export class LiveRuntime {
165
195
  async confirmTatum(digest, checkpoint) {
166
196
  assertTatumFinality(await this.tatum.getTransaction(digest), digest, checkpoint);
167
197
  }
198
+ async paymentStatus(operationId, reconcile = false) {
199
+ const row = this.store.db.prepare("SELECT digest,status,updated_at AS updatedAt FROM live_signed_transactions WHERE operation_id=?").get(operationId);
200
+ if (!row)
201
+ fail("OPERATION_NOT_FOUND", "payment operation was not found in the live transaction journal");
202
+ const payment = row;
203
+ const context = this.store.db.prepare("SELECT kind FROM live_operation_context WHERE operation_id=?").get(operationId);
204
+ const delivered = Boolean(this.store.db.prepare("SELECT 1 FROM live_x402_deliveries WHERE operation_id=?").get(operationId));
205
+ if (payment.status === "finalized")
206
+ return { operationId, status: "finalized", transactionDigest: payment.digest, kind: context?.kind ?? "unknown", deliveryStatus: delivered ? "delivered" : "not-applicable-or-pending", finalized: true, safeToResumeSameOperationId: true };
207
+ if (!reconcile)
208
+ return { operationId, status: payment.status, transactionDigest: payment.digest, kind: context?.kind ?? "unknown", deliveryStatus: delivered ? "delivered" : "pending", finalized: false, chainStatus: "unchecked", safeToResumeSameOperationId: true };
209
+ let transaction;
210
+ try {
211
+ transaction = await this.client.waitForTransaction({ digest: payment.digest, timeout: 5_000, include: { effects: true } });
212
+ }
213
+ catch {
214
+ 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 };
215
+ }
216
+ const checkpoint = assertSuiFinality(transaction, payment.digest);
217
+ await this.confirmTatum(payment.digest, checkpoint);
218
+ this.store.db.prepare("UPDATE live_signed_transactions SET status='finalized',updated_at=? WHERE operation_id=?").run(Date.now(), operationId);
219
+ return { operationId, status: "finalized", transactionDigest: payment.digest, kind: context?.kind ?? "unknown", deliveryStatus: delivered ? "delivered" : "pending", finalized: true, checkpoint, tatumVerified: true, safeToResumeSameOperationId: true };
220
+ }
168
221
  prepareOpening(opening, role, operationId, leaves) {
169
222
  const commitment = hex(opening.commitment), key = `note:${commitment}`;
170
223
  this.store.db.exec("BEGIN IMMEDIATE");
@@ -534,21 +587,40 @@ export class LiveRuntime {
534
587
  fail("POLICY_ASSET_FORBIDDEN", "challenged asset is not allowed");
535
588
  if (!this.profile.policy.allowedCounterparties.includes(payTo))
536
589
  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");
590
+ const extra = requirements.extra, requestId = String(extra?.requestId ?? operationId), challengeOperationId = String(extra?.operationId ?? operationId), nonce = String(extra?.nonce ?? ""), expiresAt = Number(extra?.expiresAt);
591
+ const unsignedRequirements = { ...requirements, extra: { requestId, operationId: challengeOperationId, nonce, expiresAt } };
592
+ if (challengeOperationId !== operationId || !nonce || !Number.isSafeInteger(expiresAt) || expiresAt <= Date.now() || String(extra?.requirementsHash ?? "") !== requirementsHash(unsignedRequirements))
593
+ fail("X402_CHALLENGE_INVALID", "challenge does not bind this operation, request, and expiry");
540
594
  bind?.({ amount: amount.toString(), counterparty: payTo, asset, endpoint: resourceUrl, requestId, requirementsHash: String(extra?.requirementsHash) });
541
595
  const tx = new (await import("@mysten/sui/transactions")).Transaction();
542
596
  tx.transferObjects([tx.coin({ balance: amount, type: asset })], payTo);
597
+ tx.setSender(this.signer.toSuiAddress());
598
+ await tx.build({ client: this.client });
599
+ const resolvedExpiration = tx.getData().expiration;
600
+ const validity = resolvedExpiration?.$kind === "ValidDuring" ? resolvedExpiration.ValidDuring : fail("X402_TRANSACTION_EXPIRY_INVALID", "Sui did not resolve a bounded transaction validity window");
601
+ if (validity.minEpoch == null || validity.maxEpoch == null || !validity.chain || validity.nonce == null)
602
+ fail("X402_TRANSACTION_EXPIRY_INVALID", "Sui resolved an incomplete transaction validity window");
603
+ tx.setExpiration({ ValidDuring: { minEpoch: validity.minEpoch, maxEpoch: validity.maxEpoch, minTimestamp: validity.minTimestamp ?? null, maxTimestamp: expiresAt, chain: validity.chain, nonce: validity.nonce } });
604
+ try {
605
+ const probe = await this.client.simulateTransaction({ transaction: tx, include: { effects: true } });
606
+ const simulated = probe.Transaction ?? probe.FailedTransaction;
607
+ if (!simulated?.status.success && timestampExpirationUnsupported(simulated?.status?.error))
608
+ tx.setExpiration({ ValidDuring: { minEpoch: validity.minEpoch, maxEpoch: validity.maxEpoch, minTimestamp: validity.minTimestamp ?? null, maxTimestamp: null, chain: validity.chain, nonce: validity.nonce } });
609
+ }
610
+ catch (error) {
611
+ if (!timestampExpirationUnsupported(error))
612
+ throw error;
613
+ tx.setExpiration({ ValidDuring: { minEpoch: validity.minEpoch, maxEpoch: validity.maxEpoch, minTimestamp: validity.minTimestamp ?? null, maxTimestamp: null, chain: validity.chain, nonce: validity.nonce } });
614
+ }
543
615
  const legacyIntentHash = hash(JSON.stringify({ domain: "supercuts-x402-v2", operationId, resourceUrl, payTo, asset, amount: amount.toString() }));
544
616
  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() };
617
+ const unsignedBinding = { requestId, nonce, requirementsHash: requirementsHash(unsignedRequirements), transactionDigest: signed.digest, payer: this.signer.toSuiAddress() };
546
618
  const payerSignature = (await this.signer.signPersonalMessage(paymentBindingBytes(unsignedBinding))).signature;
547
619
  const paymentPayload = { x402Version: 2, resource, accepted: requirements, payload: { signature: signed.signature, transaction: Buffer.from(signed.transaction).toString("base64") } };
548
620
  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
621
  const response = Buffer.from(await paid.arrayBuffer());
550
622
  if (!paid.ok)
551
- fail("X402_DELIVERY_FAILED", `paid resource returned ${paid.status}`);
623
+ fail("X402_DELIVERY_FAILED", x402ResourceFailure(paid.status, response));
552
624
  const settlementHeader = paid.headers.get("payment-response") ?? fail("X402_SETTLEMENT_INVALID", "payment-response header is missing");
553
625
  let settlement;
554
626
  try {
@@ -97,6 +97,8 @@ export class LiveSellerTransport {
97
97
  async handle(request, response) {
98
98
  try {
99
99
  const url = new URL(request.url ?? "/", this.config.origin);
100
+ if (request.method === "GET" && url.pathname === "/health")
101
+ return this.respond(response, 200, { ok: true, service: "supercuts-seller", capabilities: this.runtime.catalog().length });
100
102
  if (request.method === "GET" && url.pathname === "/v2/catalog")
101
103
  return this.respond(response, 200, { capabilities: this.runtime.catalog() });
102
104
  if (request.method === "POST" && url.pathname === "/v2/prepare")
@@ -108,7 +110,9 @@ export class LiveSellerTransport {
108
110
  if (url.pathname.startsWith(`${(resource.config.pathPrefix ?? "/capability").replace(/\/$/, "")}/`))
109
111
  return resource.handle(request, response);
110
112
  }
111
- return this.respond(response, 404, { error: "NOT_FOUND" });
113
+ if (request.method === "GET" && [...this.#registrations.values()].some((item) => url.pathname === new URL(item.manifest.endpoint).pathname))
114
+ return this.respond(response, 404, { error: "NEGOTIATION_REQUIRED", message: "Protected resource URLs are issued for a negotiated session." });
115
+ return this.respond(response, 404, { error: "NOT_FOUND", message: "Seller route was not found." });
112
116
  }
113
117
  catch (error) {
114
118
  const known = error instanceof SupercutsError ? error : new SupercutsError("SELLER_REQUEST_FAILED", "seller request failed");
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
  };
@@ -124,7 +125,7 @@ export async function executeMandated(app, mandate, command, raw) {
124
125
  fail("MANDATE_COMMAND_FORBIDDEN", `mandate does not allow ${command}`);
125
126
  const args = {};
126
127
  for (const [key, value] of Object.entries(raw))
127
- args[key] = typeof value === "boolean" ? value : typeof value === "string" ? value : Buffer.from(JSON.stringify(value)).toString("base64url");
128
+ args[key] = typeof value === "boolean" ? value : typeof value === "string" ? value : Buffer.from(canonical(value)).toString("base64url");
128
129
  if (!spec.mutating)
129
130
  return (await app.execute(command, args)).data;
130
131
  const operationId = String(raw["operation-id"]), journalOperationId = command === "settlement.plan" ? `settlement.plan:${operationId}` : operationId;
@@ -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 or seller 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 role tools without restarting the connector." : `Run supercuts onboard buyer --home ${JSON.stringify(home)} or supercuts onboard seller --home ${JSON.stringify(home)} in a terminal, then call setup.refresh. Use a separate owner-only home for every role and identity. 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: "role setup is not complete", recovery: `Run supercuts onboard buyer --home ${JSON.stringify(home)} or supercuts onboard seller --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
  }
package/dist/mcp.js CHANGED
@@ -8,13 +8,16 @@ import { DealJournal } from "./deal.js";
8
8
  import { createConfiguredDealEngine } from "./deal-adapter.js";
9
9
  import { LiveDealPorts } from "./live-deal-ports.js";
10
10
  import { createRequire } from "node:module";
11
+ import { fail } from "./errors.js";
12
+ import { acquireRoleHomeLease } from "./role-lock.js";
11
13
  const PACKAGE_VERSION = createRequire(import.meta.url)("../package.json").version;
12
14
  const MAX_REQUEST_BYTES = 1024 * 1024;
13
15
  const home = homePath({});
14
16
  const app = new Phase3App(home);
17
+ const roleLease = acquireRoleHomeLease(home);
15
18
  const mandate = loadMandate(home);
16
- const remote = app.profile.role === "buyer" && !app.profile.mock && process.env.SUPERCUTS_SELLER_URL ? new RemoteBuyer(app, mandate, process.env.SUPERCUTS_SELLER_URL) : undefined;
17
- const eventLog = process.env.SUPERCUTS_EVENT_PATH && process.env.SUPERCUTS_RUN_ID ? new DemoEventLog(process.env.SUPERCUTS_EVENT_PATH, process.env.SUPERCUTS_RUN_ID) : undefined;
19
+ const remote = roleLease.acquired && app.profile.role === "buyer" && !app.profile.mock && process.env.SUPERCUTS_SELLER_URL ? new RemoteBuyer(app, mandate, process.env.SUPERCUTS_SELLER_URL) : undefined;
20
+ const eventLog = roleLease.acquired && process.env.SUPERCUTS_EVENT_PATH && process.env.SUPERCUTS_RUN_ID ? new DemoEventLog(process.env.SUPERCUTS_EVENT_PATH, process.env.SUPERCUTS_RUN_ID) : undefined;
18
21
  if (eventLog)
19
22
  eventLog.append({ actor: "operator", stage: "run", status: "started", publicDetails: { flow: remote ? "codex-remote" : "codex-deal-v2" }, evidenceHashes: [], explorerLinks: [] });
20
23
  let memoryTickRunning = false;
@@ -32,7 +35,7 @@ const memoryTick = async () => {
32
35
  memoryTickRunning = false;
33
36
  }
34
37
  };
35
- const memoryConfigured = app.profile.role === "buyer" && !app.profile.mock && Boolean(app.store.getSecret("live:memory-config"));
38
+ const memoryConfigured = roleLease.acquired && app.profile.role === "buyer" && !app.profile.mock && Boolean(app.store.getSecret("live:memory-config"));
36
39
  if (memoryConfigured)
37
40
  app.recoverStaleMemorySubmissions();
38
41
  const memoryTimer = memoryConfigured ? setInterval(() => { void memoryTick(); }, 30_000) : undefined;
@@ -41,7 +44,7 @@ if (memoryTimer) {
41
44
  void memoryTick();
42
45
  }
43
46
  const tools = {
44
- buyer: "status,doctor,deal.catalog,deal.policy,deal.execute,deal.get,deal.resume,deal.reconcile,deal.evidence,deal.delivery,capability.discover,settlement.plan,wallet.deposit,wallet.balance,wallet.recover,wallet.withdraw,wallet.exit.queue,wallet.exit.status,wallet.exit.finalize,offer.open,offer.propose,offer.accept,offer.show,pay.quote,pay.shielded,pay.x402,pay.status,resource.fetch,memory.remember,memory.recall,memory.status,memory.inventory,remote.catalog,remote.rank-briefs,remote.negotiate,remote.quote,remote.purchase-brief,remote.agreement,remote.settle-analysis,remote.delivery,remote.events,disclose",
47
+ buyer: "status,doctor,deal.catalog,deal.policy,deal.execute,deal.get,deal.resume,deal.reconcile,deal.evidence,deal.delivery,capability.discover,settlement.plan,wallet.deposit,wallet.balance,wallet.recover,wallet.withdraw,wallet.exit.queue,wallet.exit.status,wallet.exit.finalize,offer.open,offer.propose,offer.accept,offer.show,pay.quote,pay.shielded,pay.x402,pay.status,resource.fetch,memory.remember,memory.recall,memory.status,memory.inventory,remote.catalog,remote.rank-briefs,remote.negotiate,remote.quote,remote.purchase-brief,remote.agreement,remote.settle-analysis,remote.delivery,remote.events,remote.research,disclose",
45
48
  seller: "status,doctor,capability.publish,wallet.balance,wallet.recover,wallet.withdraw,wallet.exit.queue,wallet.exit.status,wallet.exit.finalize,offer.counter,offer.show,pay.status,resource.serve,resource.fetch,memory.remember,memory.recall,memory.status",
46
49
  auditor: "status,doctor,audit.verify",
47
50
  };
@@ -50,6 +53,16 @@ function logTool(name, data) {
50
53
  if (!eventLog)
51
54
  return;
52
55
  const append = (actor, stage, details, hashes = []) => eventLog.append({ actor, stage, status: "complete", publicDetails: details, evidenceHashes: hashes, explorerLinks: [] });
56
+ if (name === "remote.research") {
57
+ if (data.replayed === true)
58
+ return;
59
+ logTool("remote.rank-briefs", data.selection);
60
+ logTool("remote.purchase-brief", data.brief);
61
+ logTool("remote.agreement", data.agreement);
62
+ logTool("remote.settle-analysis", data.settlement);
63
+ logTool("remote.delivery", data.analysis);
64
+ return;
65
+ }
53
66
  if (name === "remote.rank-briefs")
54
67
  append("buyer", "seller-selection", { winner: data.winner?.capabilityId, rankings: data.rankings?.map((item) => ({ capabilityId: item.capabilityId, score: item.score, priceMist: item.priceMist })) });
55
68
  if (name === "remote.negotiate")
@@ -108,7 +121,7 @@ rl.on("line", (line) => {
108
121
  return send({ jsonrpc: "2.0", id: request.id, result: { protocolVersion: "2025-06-18", capabilities: { tools: {} }, serverInfo: { name: `supercuts-${app.profile.role}`, version: PACKAGE_VERSION } } });
109
122
  if (request.method === "notifications/initialized")
110
123
  return;
111
- const available = tools[app.profile.role].split(",").filter((name) => (remote || !name.startsWith("remote.")) && (app.profile.mock || memoryConfigured || !name.startsWith("memory.")) && (!name.startsWith("deal.") || mandate.version === "supercuts.mandate.v2") && (["status", "doctor"].includes(name) || mandate.allowedCommands.includes(name === "deal.reconcile" ? "deal.resume" : name)));
124
+ const available = tools[app.profile.role].split(",").filter((name) => (roleLease.acquired || name === "status") && (remote || !name.startsWith("remote.")) && (app.profile.mock || memoryConfigured || !name.startsWith("memory.")) && (!name.startsWith("deal.") || mandate.version === "supercuts.mandate.v2") && (["status", "doctor"].includes(name) || mandate.allowedCommands.includes(name === "deal.reconcile" ? "deal.resume" : name)));
112
125
  if (request.method === "tools/list")
113
126
  return send({ jsonrpc: "2.0", id: request.id, result: { tools: available.map((name) => { const spec = toolSchema(name); return { name, description: spec.description, inputSchema: spec.inputSchema }; }) } });
114
127
  if (request.method === "tools/call") {
@@ -123,7 +136,7 @@ rl.on("line", (line) => {
123
136
  const { validateToolArguments } = await import("./mandate.js");
124
137
  validateToolArguments(spec, raw);
125
138
  if (mandate.version !== "supercuts.mandate.v2" || !mandate.allowedCommands.includes(name === "deal.reconcile" ? "deal.resume" : name) || (name === "deal.execute" || name === "deal.resume") && Date.now() > mandate.expiresAt)
126
- throw new Error("deal tool is not authorized by an active v2 mandate");
139
+ fail("DEAL_MANDATE_INACTIVE", "deal tool is not authorized by an active v2 mandate");
127
140
  if (name === "deal.catalog")
128
141
  data = { capabilities: (await new LiveDealPorts(app, mandate).catalog()).map(({ origin, registration }) => ({ origin, manifest: registration.manifest, manifestBlobId: registration.manifestBlobId })) };
129
142
  else if (name === "deal.policy") {
@@ -151,25 +164,31 @@ rl.on("line", (line) => {
151
164
  data = new DealJournal(app.store).get(String(raw.operationId));
152
165
  }
153
166
  else if (name.startsWith("remote.")) {
154
- if (!remote || !mandate.allowedCommands.includes(name) || Date.now() > mandate.expiresAt)
155
- throw new Error("remote buyer tool is not authorized or configured");
167
+ const buyer = remote ?? fail("REMOTE_MANDATE_INACTIVE", "remote buyer tool is not configured");
168
+ if (!mandate.allowedCommands.includes(name) || Date.now() > mandate.expiresAt)
169
+ fail("REMOTE_MANDATE_INACTIVE", "remote buyer tool is not authorized by an active mandate");
170
+ if (!["remote.catalog", "remote.rank-briefs"].includes(name) && raw.runId === undefined && process.env.SUPERCUTS_RUN_ID)
171
+ raw.runId = process.env.SUPERCUTS_RUN_ID;
156
172
  const spec = toolSchema(name);
157
173
  const { validateToolArguments } = await import("./mandate.js");
158
174
  validateToolArguments(spec, raw);
159
175
  if (process.env.SUPERCUTS_RUN_ID && name !== "remote.catalog" && name !== "remote.rank-briefs" && raw.runId !== undefined && raw.runId !== process.env.SUPERCUTS_RUN_ID)
160
- throw new Error("remote tool run ID differs from this buyer demo run");
161
- data = name === "remote.catalog" ? await remote.catalog() :
162
- name === "remote.rank-briefs" ? await remote.rankBriefs(raw) :
163
- name === "remote.negotiate" ? await remote.negotiate(raw) :
164
- name === "remote.quote" ? await remote.quote(raw) :
165
- name === "remote.purchase-brief" ? await remote.purchaseBrief(raw) :
166
- name === "remote.agreement" ? await remote.agreement(raw) :
167
- name === "remote.settle-analysis" ? await remote.settleAnalysis(raw) :
168
- name === "remote.delivery" ? await remote.delivery(raw) :
169
- name === "remote.events" ? await remote.events(String(raw.runId)) : {};
176
+ fail("SESSION_MISMATCH", "remote tool run ID differs from the active buyer session");
177
+ data = name === "remote.catalog" ? await buyer.catalog() :
178
+ name === "remote.rank-briefs" ? await buyer.rankBriefs(raw) :
179
+ name === "remote.negotiate" ? await buyer.negotiate(raw) :
180
+ name === "remote.quote" ? await buyer.quote(raw) :
181
+ name === "remote.purchase-brief" ? await buyer.purchaseBrief(raw) :
182
+ name === "remote.agreement" ? await buyer.agreement(raw) :
183
+ name === "remote.settle-analysis" ? await buyer.settleAnalysis(raw) :
184
+ name === "remote.delivery" ? await buyer.delivery(raw) :
185
+ name === "remote.events" ? await buyer.events(String(raw.runId)) :
186
+ name === "remote.research" ? await buyer.research({ operationId: String(raw["operation-id"]), runId: String(raw.runId), topic: String(raw.topic), budget: String(raw.budget) }) : {};
170
187
  }
171
188
  else
172
189
  data = await executeMandated(app, mandate, name, raw);
190
+ if (name === "status")
191
+ data = { ...data, session: { runId: process.env.SUPERCUTS_RUN_ID ?? null }, roleHomeLease: roleLease.acquired ? "acquired" : "busy", ...(roleLease.ownerPid ? { roleHomeOwnerPid: roleLease.ownerPid } : {}) };
173
192
  logTool(name, data);
174
193
  const result = { schemaVersion: "supercuts.v1", ok: true, command: name, data };
175
194
  return send({ jsonrpc: "2.0", id: request.id, result: { content: [{ type: "text", text: JSON.stringify(result) }], structuredContent: result } });
@@ -182,6 +201,7 @@ rl.on("line", (line) => {
182
201
  }).catch((error) => send({ jsonrpc: "2.0", error: { code: -32603, message: error instanceof Error ? error.message : "Internal error" }, id: null }));
183
202
  });
184
203
  const close = () => { if (memoryTimer)
185
- clearInterval(memoryTimer); app.close(); process.exit(0); };
204
+ clearInterval(memoryTimer); roleLease.release(); app.close(); process.exit(0); };
186
205
  process.on("SIGINT", close);
187
206
  process.on("SIGTERM", close);
207
+ process.on("exit", () => roleLease.release());
@@ -19,10 +19,10 @@ export async function onboardingDoctor(home, sellerConfig) {
19
19
  try {
20
20
  if (app.profile.mock)
21
21
  fail("LIVE_PROFILE_REQUIRED", "onboarding doctor requires a live Testnet role home");
22
- const runtime = await app.execute("doctor", {});
23
- if (runtime.data.readyForDemo === false)
24
- fail("ONBOARDING_RUNTIME_NOT_READY", "live runtime doctor reports missing prerequisites");
25
22
  if (app.profile.role === "buyer") {
23
+ const runtime = await app.execute("doctor", {});
24
+ if (runtime.data.readyForDemo === false)
25
+ fail("ONBOARDING_RUNTIME_NOT_READY", "live runtime doctor reports missing prerequisites");
26
26
  const mandate = loadMandate(home);
27
27
  if (mandate.version !== "supercuts.mandate.v2")
28
28
  fail("DEAL_MANDATE_VERSION", "buyer needs a v2 standing mandate");
@@ -42,8 +42,19 @@ export async function onboardingDoctor(home, sellerConfig) {
42
42
  }
43
43
  const endpoint = app.profile.live?.writeGrpcEndpoint ?? fail("LIVE_PROFILE_INVALID", "buyer write RPC is missing");
44
44
  const signer = String(runtime.data.signer ?? "");
45
- const { balance } = await writeClient(endpoint).getBalance({ owner: signer, coinType: "0x2::sui::SUI" });
46
- return { role: "buyer", healthy: true, runtime: runtime.data, approvedSellers: mandate.allowedCounterparties.length, nativeSuiMist: String(balance.balance), funded: BigInt(balance.balance) > 0n, ...(directoryListings !== undefined ? { directoryListings } : {}) };
45
+ const [{ balance }, shielded] = await Promise.all([
46
+ writeClient(endpoint).getBalance({ owner: signer, coinType: "0x2::sui::SUI" }),
47
+ app.execute("wallet.balance", {}),
48
+ ]);
49
+ const nativeSuiMist = String(balance.balance), shieldedMist = String(shielded.data.available ?? "0");
50
+ const checks = [
51
+ { id: "runtime", status: "ready", detail: "Pinned deployment, prover, RPC, and Walrus prerequisites verified." },
52
+ { id: "public-balance", status: BigInt(nativeSuiMist) > 0n ? "ready" : "action_required", detail: BigInt(nativeSuiMist) > 0n ? `${nativeSuiMist} MIST available.` : "Fund only this generated buyer address before a public payment." },
53
+ { id: "shielded-balance", status: BigInt(shieldedMist) > 0n ? "ready" : "action_required", detail: BigInt(shieldedMist) > 0n ? `${shieldedMist} MIST available in the experimental pool.` : "Create a buyer-owned pool deposit before a shielded deal." },
54
+ { id: "seller-approval", status: mandate.allowedCounterparties.length > 0 ? "ready" : "action_required", detail: mandate.allowedCounterparties.length > 0 ? `${mandate.allowedCounterparties.length} seller identity or identities approved.` : "Review and approve a signed seller capability locally." },
55
+ ...(directoryListings !== undefined ? [{ id: "directory", status: directoryListings > 0 ? "ready" : "action_required", detail: `${directoryListings} live directory listing or listings.` }] : []),
56
+ ];
57
+ return { role: "buyer", healthy: true, readyForPublicPayment: BigInt(nativeSuiMist) > 0n && mandate.allowedCounterparties.length > 0, readyForShieldedPayment: BigInt(shieldedMist) > 0n && mandate.allowedCounterparties.length > 0, runtime: runtime.data, checks, approvedSellers: mandate.allowedCounterparties.length, nativeSuiMist, shieldedMist, funded: BigInt(nativeSuiMist) > 0n, ...(directoryListings !== undefined ? { directoryListings } : {}) };
47
58
  }
48
59
  if (app.profile.role === "seller") {
49
60
  if (!sellerConfig)
@@ -57,15 +68,20 @@ export async function onboardingDoctor(home, sellerConfig) {
57
68
  const served = await json(new URL("/v2/catalog", config.origin));
58
69
  if (!Array.isArray(served.capabilities))
59
70
  fail("SELLER_CATALOG_INVALID", "seller endpoint has no valid catalog");
71
+ let proofsRequired = false;
60
72
  for (const item of config.capabilities) {
61
73
  const registrationPath = resolve(item.registrationFile);
62
74
  if ((statSync(registrationPath).mode & 0o077) !== 0)
63
75
  fail("SELLER_CONFIG_PERMISSIONS", "registration file must be owner-only");
64
76
  const registration = JSON.parse(readFileSync(registrationPath, "utf8"));
77
+ proofsRequired ||= registration.manifest.supportedRails.includes("shielded");
65
78
  await verifySignedCapability(registration);
66
79
  if (!served.capabilities.some((entry) => JSON.stringify(entry) === JSON.stringify(registration)))
67
80
  fail("SELLER_CATALOG_INVALID", "seller endpoint does not serve its configured registration");
68
81
  }
82
+ const runtime = await app.execute("doctor", { "proofs-required": proofsRequired });
83
+ if (runtime.data.readyForDemo === false)
84
+ fail("ONBOARDING_RUNTIME_NOT_READY", "live runtime doctor reports missing prerequisites");
69
85
  return { role: "seller", healthy: true, runtime: runtime.data, capabilities: config.capabilities.length, origin: config.origin };
70
86
  }
71
87
  return fail("ROLE_FORBIDDEN", "onboarding doctor supports buyer and seller homes");
package/dist/phase3.js CHANGED
@@ -19,7 +19,7 @@ const V2_RELAYER = "0x405f473ed1f7f609b16d3d7dabc7cd04831cfc73b4a7fcebe9fa502d72
19
19
  const COIN = "0x2::sui::SUI";
20
20
  const WRITE = new Set(["wallet.deposit", "wallet.recover", "wallet.withdraw", "wallet.exit.queue", "wallet.exit.finalize", "offer.open", "offer.propose", "offer.counter", "offer.accept", "pay.shielded", "pay.x402", "resource.serve", "memory.remember", "disclose", "capability.publish", "settlement.plan", "settlement.approve-review", "memory.policy"]);
21
21
  const ROLE_COMMANDS = {
22
- buyer: new Set(["status", "doctor", "capability.discover", "settlement.plan", "settlement.approve-review", "memory.policy", "wallet.deposit", "wallet.balance", "wallet.recover", "wallet.withdraw", "wallet.exit.queue", "wallet.exit.status", "wallet.exit.finalize", "offer.open", "offer.propose", "offer.accept", "offer.show", "pay.quote", "pay.shielded", "pay.x402", "pay.status", "resource.fetch", "memory.remember", "memory.recall", "memory.status", "memory.inventory", "remote.catalog", "remote.rank-briefs", "remote.negotiate", "remote.quote", "remote.purchase-brief", "remote.agreement", "remote.settle-analysis", "remote.delivery", "remote.events", "disclose"]),
22
+ buyer: new Set(["status", "doctor", "capability.discover", "settlement.plan", "settlement.approve-review", "memory.policy", "wallet.deposit", "wallet.balance", "wallet.recover", "wallet.withdraw", "wallet.exit.queue", "wallet.exit.status", "wallet.exit.finalize", "offer.open", "offer.propose", "offer.accept", "offer.show", "pay.quote", "pay.shielded", "pay.x402", "pay.status", "resource.fetch", "memory.remember", "memory.recall", "memory.status", "memory.inventory", "remote.catalog", "remote.rank-briefs", "remote.negotiate", "remote.quote", "remote.purchase-brief", "remote.agreement", "remote.settle-analysis", "remote.delivery", "remote.events", "remote.research", "disclose"]),
23
23
  seller: new Set(["status", "doctor", "capability.publish", "wallet.balance", "wallet.recover", "wallet.withdraw", "wallet.exit.queue", "wallet.exit.status", "wallet.exit.finalize", "offer.counter", "offer.show", "pay.status", "resource.serve", "resource.fetch", "memory.remember", "memory.recall", "memory.status", "memory.inventory"]),
24
24
  auditor: new Set(["status", "doctor", "audit.verify"]),
25
25
  };
@@ -444,6 +444,18 @@ export class Phase3App {
444
444
  fail("CAPABILITY_LIST_INVALID", "alternatives must be an array");
445
445
  for (const item of alternatives)
446
446
  validateCapabilityManifest(item);
447
+ const operationId = required(args, "operation-id"), mandate = this.planningMandate(args);
448
+ const persisted = this.store.db.prepare("SELECT plan_hash FROM settlement_plans WHERE operation_id=?").get(operationId);
449
+ if (persisted) {
450
+ const existing = this.storedPlan(persisted.plan_hash), manifests = [primary, ...alternatives];
451
+ const requestedAmount = typeof args["requested-amount"] === "string" ? BigInt(args["requested-amount"]) : BigInt(existing.manifestMinimum);
452
+ const boundedAmount = requestedAmount < BigInt(existing.manifestMinimum) ? BigInt(existing.manifestMinimum) : requestedAmount > BigInt(existing.maximumAmount) ? BigInt(existing.maximumAmount) : requestedAmount;
453
+ const requestedRail = args["requested-rail"] === "x402" || args["requested-rail"] === "shielded" ? args["requested-rail"] : undefined;
454
+ if (existing.mandateHash !== settlementMandateHash(mandate) || !manifests.some((manifest) => manifest.manifestHash === existing.manifestHash) || requestedRail && requestedRail !== existing.rail || args["privacy-hint"] === "shielded" && existing.rail !== "shielded" || boundedAmount.toString() !== existing.openingAmount) {
455
+ fail("OPERATION_ID_CONFLICT", "planning operation already identifies another immutable plan");
456
+ }
457
+ return existing;
458
+ }
447
459
  let recalled = this.recalledMockMemories();
448
460
  if (!this.profile.mock && this.store.getSecret("live:memory-config")) {
449
461
  const encoded = this.store.getSecret("live:memory-config");
@@ -467,7 +479,7 @@ export class Phase3App {
467
479
  manual.destroy();
468
480
  }
469
481
  }
470
- const plan = createSettlementPlan({ operationId: required(args, "operation-id"), mandate: this.planningMandate(args), manifests: [primary, ...alternatives], ...(typeof args["requested-amount"] === "string" ? { requestedAmount: args["requested-amount"] } : {}), ...(args["privacy-hint"] === "shielded" ? { privacyHint: "shielded" } : {}), ...(args["requested-rail"] === "x402" || args["requested-rail"] === "shielded" ? { requestedRail: args["requested-rail"] } : {}), recalled, maxMemoryDistance: this.profile.policy.maxMemoryDistance ?? 0.35 });
482
+ const plan = createSettlementPlan({ operationId, mandate, manifests: [primary, ...alternatives], ...(typeof args["requested-amount"] === "string" ? { requestedAmount: args["requested-amount"] } : {}), ...(args["privacy-hint"] === "shielded" ? { privacyHint: "shielded" } : {}), ...(args["requested-rail"] === "x402" || args["requested-rail"] === "shielded" ? { requestedRail: args["requested-rail"] } : {}), recalled, maxMemoryDistance: this.profile.policy.maxMemoryDistance ?? 0.35 });
471
483
  return this.persistPlan(plan);
472
484
  }
473
485
  typedMemory(args, provenance) {
@@ -691,8 +703,6 @@ export class Phase3App {
691
703
  const plan = this.storedPlan(supplied), rail = command === "pay.x402" ? "x402" : "shielded", now = Date.now();
692
704
  if (plan.status !== "approved" || plan.riskLevel === "review_required")
693
705
  fail("SETTLEMENT_PLAN_REVIEW_REQUIRED", "settlement plan requires owner review");
694
- if (now >= plan.expiresAt)
695
- fail("SETTLEMENT_PLAN_EXPIRED", "settlement plan has expired");
696
706
  if (plan.mandateHash !== settlementMandateHash(mandate))
697
707
  fail("SETTLEMENT_PLAN_MANDATE_MISMATCH", "owner mandate changed after planning");
698
708
  const operationId = required(args, "operation-id");
@@ -712,6 +722,8 @@ export class Phase3App {
712
722
  const bindingHash = hash(canonicalJson({ domain: "supercuts-settlement-execution-v1", binding })), existing = this.store.db.prepare("SELECT binding_hash FROM settlement_execution_bindings WHERE operation_id=? OR plan_hash=?").get(operationId, plan.planHash);
713
723
  if (existing && existing.binding_hash !== bindingHash)
714
724
  fail("SETTLEMENT_PLAN_CONSUMED", "plan or operation is already bound to another execution");
725
+ if (!existing && now >= plan.expiresAt)
726
+ fail("SETTLEMENT_PLAN_EXPIRED", "settlement plan has expired");
715
727
  if (!existing) {
716
728
  this.store.db.prepare("INSERT INTO settlement_execution_bindings(operation_id,plan_hash,binding_hash,binding_json,created_at) VALUES(?,?,?,?,?)").run(operationId, plan.planHash, bindingHash, canonicalJson(binding), now);
717
729
  this.store.db.prepare("UPDATE settlement_plans SET consumed_at=? WHERE plan_hash=?").run(now, plan.planHash);
@@ -748,7 +760,7 @@ export class Phase3App {
748
760
  return output(command, await this.liveMemory(command, args));
749
761
  const runtime = await LiveRuntime.create(this.profile, this.store);
750
762
  if (command === "doctor")
751
- return output(command, await runtime.doctor());
763
+ return output(command, await runtime.doctor(args["proofs-required"] !== false));
752
764
  if (command === "status")
753
765
  return output(command, { role: this.profile.role, network: this.profile.network, mock: false, runtime: "testnet-live" });
754
766
  if (command === "capability.publish")
@@ -819,6 +831,8 @@ export class Phase3App {
819
831
  this.enqueueVerifiedOutcome("settlement", required(args, "operation-id"), evidence, typeof args["plan-hash"] === "string" ? args["plan-hash"] : undefined, { counterparty: String(receipt.counterparty), asset: String(receipt.asset) });
820
832
  return output(command, receipt);
821
833
  }
834
+ if (command === "pay.status")
835
+ return output(command, await runtime.paymentStatus(required(args, "operation-id"), args.reconcile === true));
822
836
  if (command === "resource.serve") {
823
837
  const requestId = required(args, "request-id"), body = required(args, "body"), resource = { requestId, responseHash: hash(Buffer.from(body, "base64")), body, deliveryStatus: "ready" };
824
838
  record(this.store, "resource", requestId, resource);
@@ -1129,7 +1143,9 @@ export class Phase3App {
1129
1143
  export function output(command, data) { return { schemaVersion: "supercuts.v1", ok: true, command, data }; }
1130
1144
  export function errorOutput(error) {
1131
1145
  const known = error instanceof SupercutsError ? error : new SupercutsError("INTERNAL_ERROR", error instanceof Error ? error.message : "unknown error");
1132
- return { schemaVersion: "supercuts.v1", ok: false, error: { code: known.code, message: known.message, recovery: recovery(known.code) } };
1146
+ const retryable = ["BROADCAST_UNRESOLVED", "DELIVERY_IN_PROGRESS", "DELIVERY_PENDING", "DEAL_SELLER_UNAVAILABLE", "ONBOARDING_SERVICE_UNAVAILABLE", "SELLER_DIRECTORY_UNAVAILABLE", "WALRUS_WRITE_FAILED", "WALRUS_READ_FAILED", "X402_FACILITATOR_UNAVAILABLE"].includes(known.code);
1147
+ const sameOperation = retryable || ["OPERATION_ID_CONFLICT", "DEAL_OPERATION_CONFLICT"].includes(known.code);
1148
+ return { schemaVersion: "supercuts.v1", ok: false, error: { code: known.code, message: known.message, recovery: recovery(known.code), retryable, retryWithSameOperationId: sameOperation } };
1133
1149
  }
1134
1150
  function recovery(code) {
1135
1151
  if (code === "LOCAL_AUTH_REQUIRED")
@@ -1140,5 +1156,21 @@ function recovery(code) {
1140
1156
  return "change the request or update the owner-controlled profile policy";
1141
1157
  if (code === "ARTIFACT_HASH_MISMATCH")
1142
1158
  return "restore the pinned artifact or reinitialize with an audited manifest";
1159
+ if (code === "SESSION_MISMATCH")
1160
+ return "read status.session.runId and retry with that session, or omit runId on the v2 deal path";
1161
+ if (code === "DEAL_MANDATE_INACTIVE")
1162
+ return "renew the owner mandate locally, then resume the same deal operation ID";
1163
+ if (code === "BROADCAST_UNRESOLVED")
1164
+ return "reconcile or resume with the same operation ID; never create a replacement payment";
1165
+ if (code === "DELIVERY_PENDING")
1166
+ return "resume the same root operation ID; completed selection, payment, and agreement stages will not run again";
1167
+ if (code === "DEAL_SELLER_UNAVAILABLE")
1168
+ return "check the seller /health endpoint, then resume the same root operation ID";
1169
+ if (code === "X402_FACILITATOR_UNAVAILABLE")
1170
+ return "the signed transaction remains journaled; resume the same operation ID after facilitator health recovers";
1171
+ if (code === "OPERATION_ID_CONFLICT" || code === "DEAL_OPERATION_CONFLICT")
1172
+ return "reuse the original exact input for this operation ID or start a deliberately new deal";
1173
+ if (code === "X402_TRANSACTION_EXPIRY_INVALID")
1174
+ return "run onboarding doctor against the configured Sui RPC and retry the same operation ID after compatibility is restored";
1143
1175
  return "inspect the stable error code and retry with corrected input";
1144
1176
  }