@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.
- package/README.md +28 -5
- package/config/phase1.testnet.json +20 -0
- package/config/proving-manifest.json +11 -0
- package/dist/buyer-setup.js +6 -6
- package/dist/cli.js +9 -3
- package/dist/deal-adapter.js +0 -2
- package/dist/deal-engine.js +5 -5
- package/dist/deal.js +2 -2
- package/dist/demo.js +70 -8
- package/dist/funding.d.ts +38 -0
- package/dist/funding.js +78 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/live-deal-ports.js +8 -1
- package/dist/live-runtime.d.ts +4 -1
- package/dist/live-runtime.js +83 -11
- package/dist/live-seller-transport.js +5 -1
- package/dist/mandate.js +10 -9
- package/dist/mcp-bootstrap.js +24 -2
- package/dist/mcp.js +39 -19
- package/dist/onboarding-doctor.js +21 -5
- package/dist/phase3.js +38 -6
- package/dist/remote-buyer.d.ts +18 -0
- package/dist/remote-buyer.js +59 -5
- package/dist/role-lock.d.ts +8 -0
- package/dist/role-lock.js +54 -0
- package/dist/seller-catalog-generate.js +4 -4
- package/dist/seller-directory-cli.js +2 -1
- package/dist/seller-live-cli.js +10 -1
- package/dist/seller-package.d.ts +9 -0
- package/dist/seller-package.js +50 -0
- package/dist/seller-provision.js +15 -1
- package/dist/seller-research.d.ts +50 -0
- package/dist/seller-research.js +210 -0
- package/dist/seller-scaffold.d.ts +1 -0
- package/dist/seller-scaffold.js +35 -0
- package/dist/seller-service.d.ts +1 -1
- package/dist/seller-service.js +14 -21
- package/dist/seller-setup.d.ts +3 -0
- package/dist/seller-setup.js +107 -0
- package/dist/x402-resource-service.d.ts +2 -0
- package/dist/x402-resource-service.js +36 -17
- package/package.json +2 -1
package/dist/live-runtime.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
539
|
-
|
|
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(
|
|
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",
|
|
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
|
-
|
|
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
|
|
18
|
-
"wallet.exit.status": object({ "operation-id": string("Queued exit operation ID.") }, ["operation-id"], false, "Read a queued
|
|
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, "
|
|
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("
|
|
47
|
-
"remote.quote": object({ capabilityId: string("Selected capability ID."), operationId: string("Stable public payment operation ID."), requestId: string("Stable artifact request ID."), runId: string("
|
|
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("
|
|
49
|
-
"remote.agreement": object({ capabilityId: string("Analysis capability ID."), operationId: string("Stable shielded payment operation ID."), sessionId: string("Stable negotiation session ID."), runId: string("
|
|
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("
|
|
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(
|
|
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;
|
package/dist/mcp-bootstrap.js
CHANGED
|
@@ -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: [
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
155
|
-
|
|
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
|
-
|
|
161
|
-
data = name === "remote.catalog" ? await
|
|
162
|
-
name === "remote.rank-briefs" ? await
|
|
163
|
-
name === "remote.negotiate" ? await
|
|
164
|
-
name === "remote.quote" ? await
|
|
165
|
-
name === "remote.purchase-brief" ? await
|
|
166
|
-
name === "remote.agreement" ? await
|
|
167
|
-
name === "remote.settle-analysis" ? await
|
|
168
|
-
name === "remote.delivery" ? await
|
|
169
|
-
name === "remote.events" ? await
|
|
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
|
|
46
|
-
|
|
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
|
|
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
|
-
|
|
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
|
}
|