@supercuts-walrus/backend 0.0.0-stage → 0.1.0
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/LICENSE +201 -0
- package/README.md +249 -2
- package/dist/audit.d.ts +52 -0
- package/dist/audit.js +157 -0
- package/dist/backend.d.ts +27 -0
- package/dist/backend.js +62 -0
- package/dist/buyer-setup.d.ts +6 -0
- package/dist/buyer-setup.js +241 -0
- package/dist/capability.d.ts +32 -0
- package/dist/capability.js +87 -0
- package/dist/claude.d.ts +44 -0
- package/dist/claude.js +98 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +29 -0
- package/dist/config.d.ts +20 -0
- package/dist/config.js +58 -0
- package/dist/dashboard.d.ts +5 -0
- package/dist/dashboard.js +21 -0
- package/dist/deal-adapter.d.ts +3 -0
- package/dist/deal-adapter.js +39 -0
- package/dist/deal-engine.d.ts +62 -0
- package/dist/deal-engine.js +211 -0
- package/dist/deal-wire.d.ts +16 -0
- package/dist/deal-wire.js +65 -0
- package/dist/deal.d.ts +79 -0
- package/dist/deal.js +228 -0
- package/dist/demo.d.ts +2 -0
- package/dist/demo.js +449 -0
- package/dist/errors.d.ts +6 -0
- package/dist/errors.js +13 -0
- package/dist/events.d.ts +27 -0
- package/dist/events.js +68 -0
- package/dist/facilitator.d.ts +21 -0
- package/dist/facilitator.js +173 -0
- package/dist/finality.d.ts +2 -0
- package/dist/finality.js +27 -0
- package/dist/hosted-exit-cli.d.ts +2 -0
- package/dist/hosted-exit-cli.js +37 -0
- package/dist/hosted-exit.d.ts +24 -0
- package/dist/hosted-exit.js +70 -0
- package/dist/hosted-seller-import.d.ts +2 -0
- package/dist/hosted-seller-import.js +70 -0
- package/dist/hosted-seller-repository.d.ts +31 -0
- package/dist/hosted-seller-repository.js +214 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.js +37 -0
- package/dist/indexer.d.ts +63 -0
- package/dist/indexer.js +60 -0
- package/dist/live-deal-ports.d.ts +41 -0
- package/dist/live-deal-ports.js +212 -0
- package/dist/live-index.d.ts +28 -0
- package/dist/live-index.js +149 -0
- package/dist/live-runtime.d.ts +82 -0
- package/dist/live-runtime.js +570 -0
- package/dist/live-seller-transport.d.ts +30 -0
- package/dist/live-seller-transport.js +169 -0
- package/dist/live-seller-verifier.d.ts +11 -0
- package/dist/live-seller-verifier.js +65 -0
- package/dist/mandate.d.ts +34 -0
- package/dist/mandate.js +178 -0
- package/dist/mcp-bootstrap.d.ts +2 -0
- package/dist/mcp-bootstrap.js +39 -0
- package/dist/mcp.d.ts +2 -0
- package/dist/mcp.js +187 -0
- package/dist/memory-worker.d.ts +2 -0
- package/dist/memory-worker.js +38 -0
- package/dist/memory.d.ts +15 -0
- package/dist/memory.js +17 -0
- package/dist/memwal.d.ts +35 -0
- package/dist/memwal.js +83 -0
- package/dist/negotiation.d.ts +23 -0
- package/dist/negotiation.js +58 -0
- package/dist/notes.d.ts +11 -0
- package/dist/notes.js +26 -0
- package/dist/onboarding-doctor.d.ts +1 -0
- package/dist/onboarding-doctor.js +76 -0
- package/dist/phase3.d.ts +100 -0
- package/dist/phase3.js +1144 -0
- package/dist/procurement.d.ts +8 -0
- package/dist/procurement.js +19 -0
- package/dist/protocol-engine.d.ts +121 -0
- package/dist/protocol-engine.js +149 -0
- package/dist/prover.d.ts +28 -0
- package/dist/prover.js +70 -0
- package/dist/relayer.d.ts +32 -0
- package/dist/relayer.js +43 -0
- package/dist/remote-buyer.d.ts +67 -0
- package/dist/remote-buyer.js +231 -0
- package/dist/resource-server.d.ts +2 -0
- package/dist/resource-server.js +24 -0
- package/dist/resource.d.ts +22 -0
- package/dist/resource.js +55 -0
- package/dist/seam.d.ts +2 -0
- package/dist/seam.js +117 -0
- package/dist/seller-catalog-generate.d.ts +2 -0
- package/dist/seller-catalog-generate.js +68 -0
- package/dist/seller-database.d.ts +41 -0
- package/dist/seller-database.js +80 -0
- package/dist/seller-directory-cli.d.ts +2 -0
- package/dist/seller-directory-cli.js +24 -0
- package/dist/seller-directory.d.ts +5 -0
- package/dist/seller-directory.js +50 -0
- package/dist/seller-live-cli.d.ts +2 -0
- package/dist/seller-live-cli.js +53 -0
- package/dist/seller-provision.d.ts +2 -0
- package/dist/seller-provision.js +84 -0
- package/dist/seller-register-cli.d.ts +2 -0
- package/dist/seller-register-cli.js +40 -0
- package/dist/seller-runtime.d.ts +60 -0
- package/dist/seller-runtime.js +96 -0
- package/dist/seller-service.d.ts +34 -0
- package/dist/seller-service.js +416 -0
- package/dist/seller-state.d.ts +19 -0
- package/dist/seller-state.js +60 -0
- package/dist/seller-stdio.d.ts +6 -0
- package/dist/seller-stdio.js +29 -0
- package/dist/seller-v2-provision.d.ts +2 -0
- package/dist/seller-v2-provision.js +71 -0
- package/dist/seller-watch.d.ts +2 -0
- package/dist/seller-watch.js +45 -0
- package/dist/settlement.d.ts +95 -0
- package/dist/settlement.js +141 -0
- package/dist/store.d.ts +36 -0
- package/dist/store.js +193 -0
- package/dist/sui.d.ts +49 -0
- package/dist/sui.js +77 -0
- package/dist/tatum.d.ts +17 -0
- package/dist/tatum.js +83 -0
- package/dist/transcript.d.ts +31 -0
- package/dist/transcript.js +52 -0
- package/dist/v2-exit-relayer.d.ts +34 -0
- package/dist/v2-exit-relayer.js +102 -0
- package/dist/v2-exit-worker.d.ts +2 -0
- package/dist/v2-exit-worker.js +97 -0
- package/dist/v2-exits.d.ts +30 -0
- package/dist/v2-exits.js +78 -0
- package/dist/v2-prefund.d.ts +19 -0
- package/dist/v2-prefund.js +43 -0
- package/dist/v2-relayer-cli.d.ts +2 -0
- package/dist/v2-relayer-cli.js +39 -0
- package/dist/wallet.d.ts +32 -0
- package/dist/wallet.js +90 -0
- package/dist/workflow-memory.d.ts +22 -0
- package/dist/workflow-memory.js +49 -0
- package/dist/x402-resource-service.d.ts +34 -0
- package/dist/x402-resource-service.js +154 -0
- package/dist/x402.d.ts +46 -0
- package/dist/x402.js +136 -0
- package/package.json +60 -3
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { decodeSuiPrivateKey } from "@mysten/sui/cryptography";
|
|
3
|
+
import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
|
|
4
|
+
import { isValidPersonalMessageSignature } from "@mysten/sui/verify";
|
|
5
|
+
import { validateCapabilityPayload } from "./capability.js";
|
|
6
|
+
import { fail, SupercutsError } from "./errors.js";
|
|
7
|
+
import { LiveRuntime } from "./live-runtime.js";
|
|
8
|
+
import { LiveSellerPaymentVerifier } from "./live-seller-verifier.js";
|
|
9
|
+
import { SellerRuntime } from "./seller-runtime.js";
|
|
10
|
+
import { canonicalJson } from "./settlement.js";
|
|
11
|
+
import { WalrusDealBlobs, wireDecrypt, wireEncrypt, wireHash, wireJsonHash, wireKey } from "./deal-wire.js";
|
|
12
|
+
import { X402ResourceService } from "./x402-resource-service.js";
|
|
13
|
+
import { dealChildId } from "./deal.js";
|
|
14
|
+
const id = (value) => typeof value === "string" && /^[A-Za-z0-9._:-]{1,128}$/.test(value) ? value : fail("SELLER_REQUEST_INVALID", "deal wire identity is invalid");
|
|
15
|
+
const object = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : fail("SELLER_REQUEST_INVALID", "deal wire object is invalid");
|
|
16
|
+
const halves = (hash) => [BigInt(`0x${hash.slice(0, 32)}`).toString(), BigInt(`0x${hash.slice(32)}`).toString()];
|
|
17
|
+
const signedOffer = (sessionId, capabilityId, buyerPublicKey, ciphertext) => Buffer.from(`${sessionId}:${capabilityId}:${buyerPublicKey}:${wireHash(Buffer.from(ciphertext, "base64"))}`);
|
|
18
|
+
export class LiveSellerTransport {
|
|
19
|
+
app;
|
|
20
|
+
config;
|
|
21
|
+
runtime;
|
|
22
|
+
#blobs;
|
|
23
|
+
#verifier;
|
|
24
|
+
#resources = new Map();
|
|
25
|
+
#registrations = new Map();
|
|
26
|
+
constructor(app, config, runtimeFactory) {
|
|
27
|
+
this.app = app;
|
|
28
|
+
this.config = config;
|
|
29
|
+
if (app.profile.role !== "seller" || app.profile.mock)
|
|
30
|
+
fail("SELLER_LIVE_REQUIRED", "generic seller transport requires a live seller role home");
|
|
31
|
+
const origin = new URL(config.origin);
|
|
32
|
+
if (origin.protocol !== "https:" && !(origin.protocol === "http:" && ["localhost", "127.0.0.1", "::1"].includes(origin.hostname)))
|
|
33
|
+
fail("SELLER_ORIGIN_INVALID", "seller transport requires HTTPS except on loopback");
|
|
34
|
+
this.#blobs = new WalrusDealBlobs(app);
|
|
35
|
+
this.#verifier = new LiveSellerPaymentVerifier(app, runtimeFactory);
|
|
36
|
+
app.store.db.exec("CREATE TABLE IF NOT EXISTS seller_deal_prepares(session_id TEXT PRIMARY KEY,capability_id TEXT NOT NULL,buyer_address TEXT NOT NULL,buyer_public_key TEXT NOT NULL,offer_hash TEXT NOT NULL,offer_ciphertext TEXT NOT NULL,reply_ciphertext TEXT NOT NULL,offer_blob_id TEXT,reply_blob_id TEXT,payment_operation_id TEXT NOT NULL)");
|
|
37
|
+
this.runtime = new SellerRuntime(app.store, {
|
|
38
|
+
readBlob: (blobId) => this.#blobs.read(blobId),
|
|
39
|
+
verifyPayment: (request) => this.#verifier.verify(request),
|
|
40
|
+
encryptAndPublish: async (request, result) => {
|
|
41
|
+
const row = this.prepared(request.requestId);
|
|
42
|
+
const key = wireKey(app, row.buyer_public_key, request.requestId);
|
|
43
|
+
const encoded = wireEncrypt(key, request.requestId, "delivery", { artifactBase64: Buffer.from(result.artifact).toString("base64"), contentType: result.contentType, metadata: result.metadata });
|
|
44
|
+
const ciphertext = Buffer.from(encoded, "base64"), published = await this.#blobs.publish(ciphertext);
|
|
45
|
+
return { blobId: published.blobId, ciphertext };
|
|
46
|
+
},
|
|
47
|
+
serve: async () => fail("SELLER_TRANSPORT_START", "start the seller HTTP service with createServer"),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
prepared(sessionId) { return this.app.store.db.prepare("SELECT * FROM seller_deal_prepares WHERE session_id=?").get(sessionId) ?? fail("NEGOTIATION_REQUIRED", "seller has no matching prepared deal"); }
|
|
51
|
+
async register(registration, handler) {
|
|
52
|
+
if (registration.manifest.sellerId.toLowerCase() !== (await this.signer()).toSuiAddress().toLowerCase())
|
|
53
|
+
fail("SELLER_IDENTITY_MISMATCH", "capability seller does not match this role signer");
|
|
54
|
+
if (new URL(registration.manifest.endpoint).origin !== new URL(this.config.origin).origin)
|
|
55
|
+
fail("SELLER_ENDPOINT_MISMATCH", "capability endpoint must be hosted by this seller transport");
|
|
56
|
+
await this.runtime.registerCapability(registration, handler);
|
|
57
|
+
this.#registrations.set(registration.manifest.capabilityId, registration);
|
|
58
|
+
if (registration.manifest.supportedRails.includes("x402")) {
|
|
59
|
+
const facilitatorUrl = this.config.facilitatorUrl ?? fail("SELLER_FACILITATOR_REQUIRED", "x402 capability requires a facilitator URL");
|
|
60
|
+
const facilitatorKey = this.config.facilitatorKey ?? fail("SELLER_FACILITATOR_REQUIRED", "x402 capability requires a seller-only facilitator key");
|
|
61
|
+
const endpoint = new URL(registration.manifest.endpoint);
|
|
62
|
+
const resource = new X402ResourceService(this.app.store, { origin: this.config.origin, pathPrefix: endpoint.pathname, facilitatorUrl, facilitatorKey, network: "sui:testnet", asset: registration.manifest.asset, payTo: registration.manifest.sellerId, amount: registration.manifest.priceBand.minimum }, fetch, this.config.persist, this.config.claimPayment);
|
|
63
|
+
this.#resources.set(registration.manifest.capabilityId, resource);
|
|
64
|
+
const rows = this.app.store.db.prepare("SELECT session_id,payment_operation_id FROM seller_deal_prepares WHERE capability_id=?").all(registration.manifest.capabilityId);
|
|
65
|
+
for (const row of rows)
|
|
66
|
+
resource.register(row.session_id, row.payment_operation_id, Buffer.from(canonicalJson({ status: "paid", requestId: row.session_id })));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async signer() {
|
|
70
|
+
const encoded = this.app.store.getSecret("live:sui-private-key")?.toString() ?? fail("LIVE_CREDENTIAL_MISSING", "seller signer is missing");
|
|
71
|
+
const decoded = decodeSuiPrivateKey(encoded);
|
|
72
|
+
if (decoded.scheme !== "ED25519")
|
|
73
|
+
fail("SIGNER_UNSUPPORTED", "seller requires an ED25519 signer");
|
|
74
|
+
return Ed25519Keypair.fromSecretKey(decoded.secretKey);
|
|
75
|
+
}
|
|
76
|
+
async body(request) {
|
|
77
|
+
const chunks = [];
|
|
78
|
+
let size = 0;
|
|
79
|
+
for await (const chunk of request) {
|
|
80
|
+
size += chunk.length;
|
|
81
|
+
if (size > 256 * 1024)
|
|
82
|
+
fail("SELLER_REQUEST_INVALID", "seller request exceeds 256 KiB");
|
|
83
|
+
chunks.push(chunk);
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
return object(JSON.parse(Buffer.concat(chunks).toString()));
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
return fail("SELLER_REQUEST_INVALID", "seller request is not JSON", error);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
respond(response, code, value) {
|
|
93
|
+
const bytes = Buffer.from(JSON.stringify(value));
|
|
94
|
+
response.writeHead(code, { "content-type": "application/json", "content-length": String(bytes.length), "cache-control": "no-store" });
|
|
95
|
+
response.end(bytes);
|
|
96
|
+
}
|
|
97
|
+
async handle(request, response) {
|
|
98
|
+
try {
|
|
99
|
+
const url = new URL(request.url ?? "/", this.config.origin);
|
|
100
|
+
if (request.method === "GET" && url.pathname === "/v2/catalog")
|
|
101
|
+
return this.respond(response, 200, { capabilities: this.runtime.catalog() });
|
|
102
|
+
if (request.method === "POST" && url.pathname === "/v2/prepare")
|
|
103
|
+
return this.respond(response, 200, await this.prepareRequest(await this.body(request)));
|
|
104
|
+
if (request.method === "POST" && url.pathname === "/v2/fulfill")
|
|
105
|
+
return this.respond(response, 200, await this.fulfillRequest(await this.body(request)));
|
|
106
|
+
if (request.method === "GET") {
|
|
107
|
+
for (const resource of this.#resources.values())
|
|
108
|
+
if (url.pathname.startsWith(`${(resource.config.pathPrefix ?? "/capability").replace(/\/$/, "")}/`))
|
|
109
|
+
return resource.handle(request, response);
|
|
110
|
+
}
|
|
111
|
+
return this.respond(response, 404, { error: "NOT_FOUND" });
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
const known = error instanceof SupercutsError ? error : new SupercutsError("SELLER_REQUEST_FAILED", "seller request failed");
|
|
115
|
+
if (!response.headersSent)
|
|
116
|
+
this.respond(response, 400, { error: known.code, message: known.message });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
createServer() { return createServer((request, response) => void this.handle(request, response)); }
|
|
120
|
+
async prepareRequest(body) { const result = await this.prepare(body); await this.config.persist?.(); return result; }
|
|
121
|
+
async fulfillRequest(body) { const result = await this.fulfill(body); await this.config.persist?.(); return result; }
|
|
122
|
+
async prepare(body) {
|
|
123
|
+
const sessionId = id(body.sessionId), capabilityId = id(body.capabilityId);
|
|
124
|
+
const registration = this.#registrations.get(capabilityId) ?? fail("SELLER_CAPABILITY_UNKNOWN", "capability is not registered");
|
|
125
|
+
const buyerAddress = String(body.buyerAddress ?? ""), buyerPublicKey = String(body.buyerPublicKey ?? ""), ciphertext = String(body.ciphertext ?? ""), signature = String(body.buyerSignature ?? "");
|
|
126
|
+
if (!/^0x[0-9a-f]{64}$/i.test(buyerAddress) || !buyerPublicKey || ciphertext.length > 192 * 1024 || !ciphertext || !signature)
|
|
127
|
+
fail("SELLER_REQUEST_INVALID", "buyer identity or ciphertext is invalid");
|
|
128
|
+
if (this.config.allowedBuyers && !this.config.allowedBuyers.some((item) => item.toLowerCase() === buyerAddress.toLowerCase()))
|
|
129
|
+
fail("SELLER_BUYER_FORBIDDEN", "buyer is outside the seller allowlist");
|
|
130
|
+
if (!(await isValidPersonalMessageSignature(signedOffer(sessionId, capabilityId, buyerPublicKey, ciphertext), signature, { address: buyerAddress })))
|
|
131
|
+
fail("SELLER_BUYER_SIGNATURE_INVALID", "buyer did not authorize the encrypted offer");
|
|
132
|
+
const key = wireKey(this.app, buyerPublicKey, sessionId), offer = object(wireDecrypt(key, sessionId, "offer", ciphertext));
|
|
133
|
+
const manifest = registration.manifest, draft = object(offer.draft), amount = manifest.priceBand.minimum;
|
|
134
|
+
validateCapabilityPayload(manifest, "input", offer.input);
|
|
135
|
+
if (offer.amount !== amount || !/^[1-9][0-9]*$/.test(offer.maximum) || BigInt(offer.maximum) < BigInt(amount) || BigInt(offer.maximum) > BigInt(manifest.priceBand.maximum) || !manifest.supportedRails.includes(offer.rail) || !/^[0-9a-f]{64}$/.test(offer.planHash))
|
|
136
|
+
fail("SELLER_OFFER_INVALID", "offer is outside signed price or rail terms");
|
|
137
|
+
const taskHash = wireJsonHash(offer.input), transcript = wireJsonHash({ domain: "supercuts-deal-transcript-v1", sessionId, taskHash, planHash: offer.planHash, amount, rail: offer.rail });
|
|
138
|
+
const service = wireJsonHash({ domain: "supercuts-deal-service-v1", taskHash, planHash: offer.planHash, capabilityId, rail: offer.rail, buyerPublicKey });
|
|
139
|
+
const [transcriptRootLo, transcriptRootHi] = halves(transcript), [serviceDigestLo, serviceDigestHi] = halves(service);
|
|
140
|
+
const dealOperationId = typeof body.dealOperationId === "string" && body.dealOperationId.length <= 256 && body.dealOperationId.length > 0 ? body.dealOperationId : fail("SELLER_REQUEST_INVALID", "deal operation ID is invalid");
|
|
141
|
+
if (draft.sessionId !== sessionId || draft.dealOperationId !== dealOperationId || draft.capabilityId !== capabilityId || draft.manifestHash !== manifest.manifestHash || draft.counterparty !== manifest.sellerId || draft.paymentAmount !== amount || draft.rail !== offer.rail || draft.taskHash !== taskHash || draft.planHash !== offer.planHash || draft.transcriptRootLo !== transcriptRootLo || draft.transcriptRootHi !== transcriptRootHi || draft.serviceDigestLo !== serviceDigestLo || draft.serviceDigestHi !== serviceDigestHi || offer.paymentOperationId !== dealChildId(dealOperationId, "payment"))
|
|
142
|
+
fail("AGREEMENT_BINDING_MISMATCH", "draft does not bind the encrypted task and signed capability");
|
|
143
|
+
const prior = this.app.store.db.prepare("SELECT offer_hash FROM seller_deal_prepares WHERE session_id=?").get(sessionId);
|
|
144
|
+
const offerHash = wireHash(Buffer.from(ciphertext, "base64"));
|
|
145
|
+
if (prior && prior.offer_hash !== offerHash)
|
|
146
|
+
fail("NEGOTIATION_SESSION_CONFLICT", "session identifies a different encrypted offer");
|
|
147
|
+
const runtime = await LiveRuntime.create(this.app.profile, this.app.store);
|
|
148
|
+
const signed = runtime.prepareSellerAgreement(sessionId, Buffer.from(canonicalJson(draft)).toString("base64url"));
|
|
149
|
+
const agreement = Buffer.from(String(signed.agreement), "base64url").toString(), agreementHash = wireHash(agreement);
|
|
150
|
+
const resource = offer.rail === "x402" ? this.#resources.get(capabilityId) ?? fail("SELLER_RAIL_UNSUPPORTED", "seller has no x402 resource") : undefined;
|
|
151
|
+
const resourceUrl = resource?.register(sessionId, offer.paymentOperationId, Buffer.from(canonicalJson({ status: "paid", requestId: sessionId }))).url;
|
|
152
|
+
const replyCiphertext = prior ? this.prepared(sessionId).reply_ciphertext : wireEncrypt(key, sessionId, "reply", { agreement: signed.agreement, agreementHash, dealCommitment: signed.dealCommitment, ...(resourceUrl ? { resourceUrl } : {}) });
|
|
153
|
+
if (!prior)
|
|
154
|
+
this.app.store.db.prepare("INSERT INTO seller_deal_prepares(session_id,capability_id,buyer_address,buyer_public_key,offer_hash,offer_ciphertext,reply_ciphertext,payment_operation_id) VALUES(?,?,?,?,?,?,?,?)").run(sessionId, capabilityId, buyerAddress, buyerPublicKey, offerHash, ciphertext, replyCiphertext, offer.paymentOperationId);
|
|
155
|
+
const offerBlob = await this.#blobs.publish(Buffer.from(ciphertext, "base64")), replyBlob = await this.#blobs.publish(Buffer.from(replyCiphertext, "base64"));
|
|
156
|
+
this.app.store.db.prepare("UPDATE seller_deal_prepares SET offer_blob_id=?,reply_blob_id=? WHERE session_id=?").run(offerBlob.blobId, replyBlob.blobId, sessionId);
|
|
157
|
+
return { sessionId, capabilityId, offerBlob, replyBlob, agreementBlob: replyBlob, ciphertext: replyCiphertext };
|
|
158
|
+
}
|
|
159
|
+
async fulfill(body) {
|
|
160
|
+
const sessionId = id(body.sessionId), row = this.prepared(sessionId), ciphertext = String(body.ciphertext ?? ""), signature = String(body.buyerSignature ?? "");
|
|
161
|
+
if (!ciphertext || ciphertext.length > 192 * 1024 || !(await isValidPersonalMessageSignature(Buffer.from(`${sessionId}:${wireHash(Buffer.from(ciphertext, "base64"))}`), signature, { address: row.buyer_address })))
|
|
162
|
+
fail("SELLER_BUYER_SIGNATURE_INVALID", "buyer did not authorize the delivery request");
|
|
163
|
+
const key = wireKey(this.app, row.buyer_public_key, sessionId), serviceRequest = wireDecrypt(key, sessionId, "fulfill", ciphertext);
|
|
164
|
+
if (serviceRequest.requestId !== sessionId || serviceRequest.capabilityId !== row.capability_id)
|
|
165
|
+
fail("SELLER_REQUEST_INVALID", "fulfillment differs from prepared seller request");
|
|
166
|
+
const delivery = await this.runtime.fulfill(serviceRequest);
|
|
167
|
+
return { sessionId, ciphertext: wireEncrypt(key, sessionId, "receipt", delivery) };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Phase3App } from "./phase3.js";
|
|
2
|
+
import { LiveRuntime } from "./live-runtime.js";
|
|
3
|
+
import type { ServiceRequest, VerifiedSellerPayment } from "./seller-runtime.js";
|
|
4
|
+
/** Reads independently finalized chain and Tatum evidence from the seller home.
|
|
5
|
+
* The signed agreement was already persisted by prepareSellerAgreement. */
|
|
6
|
+
export declare class LiveSellerPaymentVerifier {
|
|
7
|
+
private readonly app;
|
|
8
|
+
private readonly runtimeFactory;
|
|
9
|
+
constructor(app: Phase3App, runtimeFactory?: () => Promise<LiveRuntime>);
|
|
10
|
+
verify(request: ServiceRequest): Promise<VerifiedSellerPayment>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { LiveRuntime } from "./live-runtime.js";
|
|
3
|
+
import { fail } from "./errors.js";
|
|
4
|
+
import { assertSuiFinality, assertTatumFinality } from "./finality.js";
|
|
5
|
+
import { canonicalJson } from "./settlement.js";
|
|
6
|
+
const sha256 = (value) => createHash("sha256").update(value).digest("hex");
|
|
7
|
+
const canonicalType = (value) => value.replace(/^0x0*2::/, "0x2::").toLowerCase();
|
|
8
|
+
function bytes32(value) {
|
|
9
|
+
if (typeof value !== "string")
|
|
10
|
+
return fail("SELLER_PAYMENT_UNVERIFIED", "pool event commitment is missing");
|
|
11
|
+
const bytes = /^(?:0x)?[0-9a-f]{64}$/i.test(value) ? Buffer.from(value.replace(/^0x/, ""), "hex") : Buffer.from(value, "base64");
|
|
12
|
+
if (bytes.length !== 32)
|
|
13
|
+
fail("SELLER_PAYMENT_UNVERIFIED", "pool event commitment is invalid");
|
|
14
|
+
return bytes.toString("hex");
|
|
15
|
+
}
|
|
16
|
+
/** Reads independently finalized chain and Tatum evidence from the seller home.
|
|
17
|
+
* The signed agreement was already persisted by prepareSellerAgreement. */
|
|
18
|
+
export class LiveSellerPaymentVerifier {
|
|
19
|
+
app;
|
|
20
|
+
runtimeFactory;
|
|
21
|
+
constructor(app, runtimeFactory = () => LiveRuntime.create(app.profile, app.store)) {
|
|
22
|
+
this.app = app;
|
|
23
|
+
this.runtimeFactory = runtimeFactory;
|
|
24
|
+
if (app.profile.role !== "seller" || app.profile.mock)
|
|
25
|
+
fail("SELLER_LIVE_REQUIRED", "seller payment verification requires a live seller role home");
|
|
26
|
+
app.store.db.exec("CREATE TABLE IF NOT EXISTS seller_payment_claims(digest TEXT PRIMARY KEY,request_id TEXT NOT NULL,agreement_hash TEXT NOT NULL,created_at INTEGER NOT NULL)");
|
|
27
|
+
}
|
|
28
|
+
async verify(request) {
|
|
29
|
+
const row = this.app.store.db.prepare("SELECT agreement FROM live_seller_agreements WHERE session_id=?").get(request.requestId);
|
|
30
|
+
if (!row || sha256(row.agreement) !== request.agreementHash)
|
|
31
|
+
return fail("SELLER_AGREEMENT_MISMATCH", "request does not match the locally signed agreement");
|
|
32
|
+
const agreement = JSON.parse(row.agreement);
|
|
33
|
+
if (agreement.capabilityId !== request.capabilityId || agreement.paymentAmount !== request.agreedAmount || agreement.rail !== request.rail || agreement.sessionId && agreement.sessionId !== request.requestId || agreement.dealOperationId !== request.dealId || agreement.taskHash !== sha256(canonicalJson(request.input)))
|
|
34
|
+
fail("SELLER_AGREEMENT_MISMATCH", "request differs from signed agreement terms");
|
|
35
|
+
const runtime = await this.runtimeFactory();
|
|
36
|
+
const result = await runtime.client.getTransaction({ digest: request.paymentDigest, include: { effects: true, events: true, balanceChanges: true, transaction: true } });
|
|
37
|
+
const tx = result.Transaction ?? result.FailedTransaction;
|
|
38
|
+
const checkpoint = assertSuiFinality(result, request.paymentDigest);
|
|
39
|
+
if (tx.transaction?.sender?.toLowerCase() !== this.app.store.db.prepare("SELECT buyer_address FROM seller_deal_prepares WHERE session_id=?").get(request.requestId)?.buyer_address.toLowerCase())
|
|
40
|
+
fail("SELLER_PAYMENT_UNVERIFIED", "payment sender differs from the authenticated buyer");
|
|
41
|
+
assertTatumFinality(await runtime.tatum.getTransaction(request.paymentDigest), request.paymentDigest, checkpoint);
|
|
42
|
+
if (request.rail === "x402") {
|
|
43
|
+
if (!tx.balanceChanges?.some((change) => change.address?.toLowerCase() === String(agreement.counterparty).toLowerCase() && canonicalType(String(change.coinType)) === canonicalType(this.app.profile.coinType) && BigInt(String(change.amount)) === BigInt(request.agreedAmount)))
|
|
44
|
+
fail("SELLER_PAYMENT_UNVERIFIED", "x402 transaction does not credit the exact seller and amount");
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
const values = runtime.engine.agreementValues({ dealId: BigInt(String(agreement.dealId)), revision: BigInt(String(agreement.revision)), transcriptRootLo: BigInt(String(agreement.transcriptRootLo)), transcriptRootHi: BigInt(String(agreement.transcriptRootHi)), serviceDigestLo: BigInt(String(agreement.serviceDigestLo)), serviceDigestHi: BigInt(String(agreement.serviceDigestHi)), settlementNonceLo: BigInt(String(agreement.settlementNonceLo)), settlementNonceHi: BigInt(String(agreement.settlementNonceHi)), paymentAmount: BigInt(String(agreement.paymentAmount)), blinding: BigInt(String(agreement.blinding)), expiry: BigInt(String(agreement.expiry)), buyer: { seed: new Uint8Array(32), publicKey: agreement.buyerPublicKey.map(BigInt) }, sellerPublicKey: agreement.sellerPublicKey.map(BigInt), recipientSpendTag: BigInt(String(agreement.recipientSpendTag)) });
|
|
48
|
+
const expected = values.dealCommitment.toString(16).padStart(64, "0");
|
|
49
|
+
if (!/^[0-9a-f]{64}$/.test(expected) || !tx.events?.some((event) => event.packageId?.toLowerCase() === this.app.profile.packageId.toLowerCase() && event.module === (this.app.profile.verifierVersion === 2 ? "pool_v2" : "pool") && event.eventType?.endsWith("::TransferEvent") && event.json && bytes32(event.json.deal_commitment) === expected))
|
|
50
|
+
fail("SELLER_PAYMENT_UNVERIFIED", "pool transfer does not contain the signed deal commitment");
|
|
51
|
+
await runtime.recover();
|
|
52
|
+
const note = this.app.store.db.prepare("SELECT commitment FROM notes WHERE operation_id=? AND role='payment'").get(request.requestId);
|
|
53
|
+
if (!note)
|
|
54
|
+
return fail("SELLER_PAYMENT_UNVERIFIED", "seller could not recover the payment note from the pinned pool");
|
|
55
|
+
const leaf = this.app.store.db.prepare("SELECT transaction_digest FROM live_pool_leaves WHERE commitment=?").get(note.commitment);
|
|
56
|
+
if (leaf?.transaction_digest !== request.paymentDigest)
|
|
57
|
+
fail("SELLER_PAYMENT_UNVERIFIED", "recovered note does not belong to the supplied payment transaction");
|
|
58
|
+
}
|
|
59
|
+
const prior = this.app.store.db.prepare("SELECT request_id,agreement_hash FROM seller_payment_claims WHERE digest=?").get(request.paymentDigest);
|
|
60
|
+
if (prior && (prior.request_id !== request.requestId || prior.agreement_hash !== request.agreementHash))
|
|
61
|
+
fail("SELLER_PAYMENT_REPLAY", "payment digest is already bound to another seller request");
|
|
62
|
+
this.app.store.db.prepare("INSERT OR IGNORE INTO seller_payment_claims(digest,request_id,agreement_hash,created_at) VALUES(?,?,?,?)").run(request.paymentDigest, request.requestId, request.agreementHash, Date.now());
|
|
63
|
+
return { finalized: true, tatumVerified: true, digest: request.paymentDigest, agreementHash: request.agreementHash, sellerId: String(agreement.counterparty), asset: this.app.profile.coinType, amount: request.agreedAmount, rail: request.rail };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Phase3App, Role } from "./phase3.js";
|
|
2
|
+
export interface AgentMandate {
|
|
3
|
+
version: "supercuts.mandate.v1" | "supercuts.mandate.v2";
|
|
4
|
+
owner: string;
|
|
5
|
+
role: Role;
|
|
6
|
+
allowedCommands: string[];
|
|
7
|
+
allowedCounterparties: string[];
|
|
8
|
+
allowedAssets: string[];
|
|
9
|
+
perDealCeiling: string;
|
|
10
|
+
totalRunBudget: string;
|
|
11
|
+
requiredPrivacy: "any" | "shielded";
|
|
12
|
+
expiresAt: number;
|
|
13
|
+
requireSettlementPlan?: boolean;
|
|
14
|
+
allowedCapabilityIds?: string[];
|
|
15
|
+
allowedCapabilityClasses?: string[];
|
|
16
|
+
allowedRails?: Array<"x402" | "shielded">;
|
|
17
|
+
allowPublicX402?: boolean;
|
|
18
|
+
}
|
|
19
|
+
export interface ToolSchema {
|
|
20
|
+
name: string;
|
|
21
|
+
description: string;
|
|
22
|
+
mutating: boolean;
|
|
23
|
+
inputSchema: {
|
|
24
|
+
type: "object";
|
|
25
|
+
properties: Record<string, Record<string, unknown>>;
|
|
26
|
+
required?: string[];
|
|
27
|
+
additionalProperties: false;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export declare function toolSchema(name: string): ToolSchema;
|
|
31
|
+
export declare function validateToolArguments(spec: ToolSchema, raw: Record<string, unknown>): void;
|
|
32
|
+
export declare function loadMandate(home: string): AgentMandate;
|
|
33
|
+
/** Turns an allowed MCP invocation into a backend exact-intent approval without exposing its token. */
|
|
34
|
+
export declare function executeMandated(app: Phase3App, mandate: AgentMandate, command: string, raw: Record<string, unknown>): Promise<Record<string, unknown>>;
|
package/dist/mandate.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { fail } from "./errors.js";
|
|
5
|
+
const string = (description, extra = {}) => ({ type: "string", minLength: 1, description, ...extra });
|
|
6
|
+
const boolean = (description) => ({ type: "boolean", description });
|
|
7
|
+
const object = (properties, required = [], mutating = false, description = "") => ({ properties: { ...properties, ...(mutating ? { "operation-id": string("Stable idempotency identity for this exact mutation.", { maxLength: 256 }) } : {}) }, required: [...required, ...(mutating ? ["operation-id"] : [])], mutating, description });
|
|
8
|
+
const definitions = {
|
|
9
|
+
status: object({}, [], false, "Read this isolated agent's public runtime status."),
|
|
10
|
+
doctor: object({}, [], false, "Verify pinned deployment, artifacts, identities, and configured services."),
|
|
11
|
+
"capability.discover": object({ capabilities: { type: "array", description: "Validated supercuts.capability.v1 manifests.", items: { type: "object" } }, "blob-ids": { type: "array", description: "Walrus blob IDs containing capability manifests.", items: { type: "string" } } }, [], false, "Discover seller capabilities without assuming an agent framework."),
|
|
12
|
+
"capability.publish": object({ manifest: { type: "object", description: "A complete supercuts.capability.v1 manifest." } }, ["manifest"], true, "Publish an owner-approved, versioned capability manifest."),
|
|
13
|
+
"wallet.deposit": object({ amount: string("Unsigned base-unit amount.", { pattern: "^[1-9][0-9]*$" }) }, ["amount"], true, "Deposit funds into the shielded pool."),
|
|
14
|
+
"wallet.balance": object({}, [], false, "Read event-derived private-note balance."),
|
|
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
|
+
"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."),
|
|
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
|
+
"offer.open": object({ session: string("Optional stable negotiation session ID.") }, [], true, "Open a durable negotiation session."),
|
|
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."),
|
|
22
|
+
"offer.counter": object({ session: string("Negotiation session ID."), revision: string("Positive revision.", { pattern: "^[1-9][0-9]*$" }), terms: string("Plain-language or canonical JSON terms."), agreement: string("Backend-only encoded agreement draft.") }, ["session"], true, "Counter an offer under deterministic price policy."),
|
|
23
|
+
"offer.accept": object({ session: string("Negotiation session ID."), revision: string("Positive revision.", { pattern: "^[1-9][0-9]*$" }), terms: string("Accepted terms."), agreement: string("Backend-only encoded signed agreement."), "plan-hash": string("Shielded settlement plan hash.", { pattern: "^[0-9a-f]{64}$" }) }, ["session"], true, "Accept policy-approved final terms and optionally bind it to a shielded plan."),
|
|
24
|
+
"offer.show": object({ session: string("Negotiation session ID.") }, ["session"], false, "Read the latest durable offer revision."),
|
|
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
|
+
"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."),
|
|
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
|
+
"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
|
+
"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."),
|
|
31
|
+
"settlement.plan": object({ manifest: { type: "object", description: "Primary supercuts.capability.v1 manifest." }, alternatives: { type: "array", description: "Optional alternative manifests.", items: { type: "object" } }, "requested-amount": string("Desired opening amount in base units.", { pattern: "^(?:0|[1-9][0-9]*)$" }), "privacy-hint": string("Agent hint that may only increase privacy.", { enum: ["any", "shielded"] }), "requested-rail": string("Explicit settlement rail request.", { enum: ["x402", "shielded"] }) }, ["manifest"], true, "Create and persist a deterministic mandate- and memory-bound settlement plan."),
|
|
32
|
+
"memory.remember": object({ fact: string("Legacy advisory fact, never a secret or full transcript.", { maxLength: 4096 }), memory: { type: "object", description: "Typed settlement memory; provenance is assigned as agent_observation." } }, [], true, "Write one advisory Mainnet Walrus memory without granting authority."),
|
|
33
|
+
"memory.recall": object({ query: string("Policy or experience query.", { maxLength: 1024 }) }, ["query"], false, "Recall relevant prior experience and privacy rules."),
|
|
34
|
+
"memory.status": object({ "operation-id": string("Optional memory job ID to reconcile without draining unrelated jobs.") }, [], false, "List completed memory blob identities or reconcile one job."),
|
|
35
|
+
"memory.inventory": object({ "blob-id": string("Optional role-owned outcome memory blob ID.") }, [], false, "Synchronize Walrus Memory metadata for this role and namespace."),
|
|
36
|
+
"deal.reconcile": object({ operationId: string("Existing verified deal operation ID.") }, ["operationId"], true, "Reconcile an ambiguous encrypted outcome memory using an exact local plaintext match; never resubmit payment or memory."),
|
|
37
|
+
"deal.execute": object({ input: { type: "object", description: "Versioned composite deal input." } }, ["input"], true, "Execute a protocol deal under the owner v2 mandate and configured seller adapter."),
|
|
38
|
+
"deal.get": object({ operationId: string("Existing deal operation ID.") }, ["operationId"], false, "Read a redacted composite deal status."),
|
|
39
|
+
"deal.resume": object({ operationId: string("Existing deal operation ID.") }, ["operationId"], false, "Resume the same journaled deal without changing task or payment intent."),
|
|
40
|
+
"deal.catalog": object({}, [], false, "List signed, Walrus-verified seller capabilities allowed by the standing buyer policy."),
|
|
41
|
+
"deal.policy": object({}, [], false, "Read the active buyer policy and remaining lifetime budget without exposing credentials."),
|
|
42
|
+
"deal.evidence": object({ dealId: string("Existing deal ID.") }, ["dealId"], false, "Read redacted Walrus and payment evidence for one deal."),
|
|
43
|
+
"deal.delivery": object({ dealId: string("Existing delivered deal ID.") }, ["dealId"], false, "Read a verified paid artifact from the buyer's encrypted journal."),
|
|
44
|
+
"remote.catalog": object({}, [], false, "Discover the signed remote seller catalog through Walrus."),
|
|
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."),
|
|
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
|
+
"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."),
|
|
53
|
+
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
|
+
"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
|
+
export function toolSchema(name) {
|
|
57
|
+
const definition = definitions[name] ?? fail("COMMAND_UNKNOWN", `unknown MCP tool ${name}`);
|
|
58
|
+
return { name, description: definition.description, mutating: definition.mutating, inputSchema: { type: "object", properties: definition.properties, ...(definition.required.length ? { required: definition.required } : {}), additionalProperties: false } };
|
|
59
|
+
}
|
|
60
|
+
function validateValue(name, schema, value) {
|
|
61
|
+
if (schema.type === "string" && typeof value !== "string")
|
|
62
|
+
fail("MCP_ARGUMENT_INVALID", `${name} must be a string`);
|
|
63
|
+
if (schema.type === "boolean" && typeof value !== "boolean")
|
|
64
|
+
fail("MCP_ARGUMENT_INVALID", `${name} must be a boolean`);
|
|
65
|
+
if (schema.type === "array" && !Array.isArray(value))
|
|
66
|
+
fail("MCP_ARGUMENT_INVALID", `${name} must be an array`);
|
|
67
|
+
if (schema.type === "object" && (!value || typeof value !== "object" || Array.isArray(value)))
|
|
68
|
+
fail("MCP_ARGUMENT_INVALID", `${name} must be an object`);
|
|
69
|
+
if (typeof value === "string" && typeof schema.pattern === "string" && !(new RegExp(schema.pattern).test(value)))
|
|
70
|
+
fail("MCP_ARGUMENT_INVALID", `${name} has an invalid format`);
|
|
71
|
+
if (typeof value === "string" && Array.isArray(schema.enum) && !schema.enum.includes(value))
|
|
72
|
+
fail("MCP_ARGUMENT_INVALID", `${name} is not an allowed value`);
|
|
73
|
+
}
|
|
74
|
+
export function validateToolArguments(spec, raw) {
|
|
75
|
+
for (const name of Object.keys(raw))
|
|
76
|
+
if (!(name in spec.inputSchema.properties))
|
|
77
|
+
fail("MCP_ARGUMENT_UNKNOWN", `${spec.name} does not accept ${name}`);
|
|
78
|
+
for (const name of spec.inputSchema.required ?? [])
|
|
79
|
+
if (!(name in raw))
|
|
80
|
+
fail("MCP_ARGUMENT_REQUIRED", `${spec.name} requires ${name}`);
|
|
81
|
+
for (const [name, value] of Object.entries(raw))
|
|
82
|
+
validateValue(name, spec.inputSchema.properties[name], value);
|
|
83
|
+
}
|
|
84
|
+
export function loadMandate(home) {
|
|
85
|
+
const path = join(home, "mandate.json");
|
|
86
|
+
if (!existsSync(path))
|
|
87
|
+
fail("MANDATE_NOT_FOUND", "MCP requires an owner-issued mandate.json");
|
|
88
|
+
if ((statSync(path).mode & 0o077) !== 0)
|
|
89
|
+
fail("MANDATE_PERMISSIONS", "mandate.json must be owner-only");
|
|
90
|
+
let mandate;
|
|
91
|
+
try {
|
|
92
|
+
mandate = JSON.parse(readFileSync(path, "utf8"));
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
return fail("MANDATE_INVALID", "mandate.json is not valid JSON", error);
|
|
96
|
+
}
|
|
97
|
+
if (!["supercuts.mandate.v1", "supercuts.mandate.v2"].includes(mandate.version) || !mandate.owner || !Array.isArray(mandate.allowedCommands) || !Array.isArray(mandate.allowedCounterparties) || !Array.isArray(mandate.allowedAssets) || !/^(?:0|[1-9][0-9]*)$/.test(mandate.perDealCeiling) || !/^(?:0|[1-9][0-9]*)$/.test(mandate.totalRunBudget) || !Number.isSafeInteger(mandate.expiresAt))
|
|
98
|
+
fail("MANDATE_INVALID", "mandate.json has an invalid shape");
|
|
99
|
+
if (mandate.version === "supercuts.mandate.v1" && mandate.allowPublicX402 !== undefined || mandate.version === "supercuts.mandate.v2" && mandate.allowPublicX402 !== undefined && typeof mandate.allowPublicX402 !== "boolean")
|
|
100
|
+
fail("MANDATE_INVALID", "public x402 permission has an invalid version or value");
|
|
101
|
+
if (mandate.requireSettlementPlan !== undefined && typeof mandate.requireSettlementPlan !== "boolean")
|
|
102
|
+
fail("MANDATE_INVALID", "requireSettlementPlan must be boolean");
|
|
103
|
+
for (const list of [mandate.allowedCapabilityIds, mandate.allowedCapabilityClasses, mandate.allowedRails])
|
|
104
|
+
if (list !== undefined && (!Array.isArray(list) || list.some((item) => typeof item !== "string" || !item)))
|
|
105
|
+
fail("MANDATE_INVALID", "mandate allowlists must contain non-empty strings");
|
|
106
|
+
if (mandate.allowedRails?.some((rail) => rail !== "x402" && rail !== "shielded"))
|
|
107
|
+
fail("MANDATE_INVALID", "mandate contains an unsupported rail");
|
|
108
|
+
return mandate;
|
|
109
|
+
}
|
|
110
|
+
function canonical(value) {
|
|
111
|
+
if (Array.isArray(value))
|
|
112
|
+
return `[${value.map(canonical).join(",")}]`;
|
|
113
|
+
if (value && typeof value === "object")
|
|
114
|
+
return `{${Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => `${JSON.stringify(key)}:${canonical(item)}`).join(",")}}`;
|
|
115
|
+
return JSON.stringify(value);
|
|
116
|
+
}
|
|
117
|
+
/** Turns an allowed MCP invocation into a backend exact-intent approval without exposing its token. */
|
|
118
|
+
export async function executeMandated(app, mandate, command, raw) {
|
|
119
|
+
const spec = toolSchema(command);
|
|
120
|
+
validateToolArguments(spec, raw);
|
|
121
|
+
if (mandate.role !== app.profile.role || Date.now() > mandate.expiresAt)
|
|
122
|
+
fail(Date.now() > mandate.expiresAt ? "MANDATE_EXPIRED" : "MANDATE_ROLE_MISMATCH", "owner mandate does not authorize this agent");
|
|
123
|
+
if (!mandate.allowedCommands.includes(command))
|
|
124
|
+
fail("MANDATE_COMMAND_FORBIDDEN", `mandate does not allow ${command}`);
|
|
125
|
+
const args = {};
|
|
126
|
+
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
|
+
if (!spec.mutating)
|
|
129
|
+
return (await app.execute(command, args)).data;
|
|
130
|
+
const operationId = String(raw["operation-id"]), journalOperationId = command === "settlement.plan" ? `settlement.plan:${operationId}` : operationId;
|
|
131
|
+
let amount = typeof raw.amount === "string" ? BigInt(raw.amount) : 0n;
|
|
132
|
+
if ((command === "pay.x402" || command === "pay.shielded") && typeof raw.amount !== "string" && typeof raw["plan-hash"] === "string") {
|
|
133
|
+
const row = app.store.db.prepare("SELECT plan_json FROM settlement_plans WHERE plan_hash=?").get(raw["plan-hash"]) ?? fail("SETTLEMENT_PLAN_NOT_FOUND", "payment plan is missing");
|
|
134
|
+
const plan = JSON.parse(row.plan_json);
|
|
135
|
+
amount = BigInt(plan.maximumAmount);
|
|
136
|
+
}
|
|
137
|
+
if ((command === "pay.x402" || command === "pay.shielded") && amount === 0n)
|
|
138
|
+
fail("MANDATE_AMOUNT_REQUIRED", "payment amount or approved plan is required for budget reservation");
|
|
139
|
+
if (amount > BigInt(mandate.perDealCeiling))
|
|
140
|
+
fail("MANDATE_DEAL_CEILING_EXCEEDED", "operation exceeds the owner mandate per-deal ceiling");
|
|
141
|
+
if (typeof raw.counterparty === "string" && !mandate.allowedCounterparties.includes(raw.counterparty))
|
|
142
|
+
fail("MANDATE_COUNTERPARTY_FORBIDDEN", "counterparty is outside the owner mandate");
|
|
143
|
+
if (typeof raw.asset === "string" && !mandate.allowedAssets.includes(raw.asset))
|
|
144
|
+
fail("MANDATE_ASSET_FORBIDDEN", "asset is outside the owner mandate");
|
|
145
|
+
if (mandate.requiredPrivacy === "shielded" && command === "pay.x402")
|
|
146
|
+
fail("MANDATE_PRIVACY_REQUIRED", "owner mandate requires shielded settlement");
|
|
147
|
+
if (mandate.version === "supercuts.mandate.v2" && command === "pay.x402" && mandate.allowPublicX402 !== true)
|
|
148
|
+
fail("DEAL_PUBLIC_RAIL_FORBIDDEN", "owner mandate did not opt into public x402");
|
|
149
|
+
const rail = command === "pay.x402" ? "x402" : command === "pay.shielded" ? "shielded" : undefined;
|
|
150
|
+
if (rail && mandate.allowedRails && !mandate.allowedRails.includes(rail))
|
|
151
|
+
fail("MANDATE_RAIL_FORBIDDEN", "settlement rail is outside the owner mandate");
|
|
152
|
+
if (rail && mandate.requireSettlementPlan && typeof raw["plan-hash"] !== "string")
|
|
153
|
+
fail("SETTLEMENT_PLAN_REQUIRED", "owner mandate requires an approved settlement plan");
|
|
154
|
+
const intentHash = createHash("sha256").update(canonical({ command, raw })).digest("hex");
|
|
155
|
+
app.store.db.exec("CREATE TABLE IF NOT EXISTS mandate_operations(operation_id TEXT PRIMARY KEY,intent_hash TEXT NOT NULL,amount TEXT NOT NULL,created_at INTEGER NOT NULL)");
|
|
156
|
+
app.store.db.exec("BEGIN IMMEDIATE");
|
|
157
|
+
try {
|
|
158
|
+
const existing = app.store.db.prepare("SELECT intent_hash FROM mandate_operations WHERE operation_id=?").get(journalOperationId);
|
|
159
|
+
if (existing && existing.intent_hash !== intentHash)
|
|
160
|
+
fail("OPERATION_ID_CONFLICT", "operation ID already identifies another mandated mutation");
|
|
161
|
+
const spent = app.store.db.prepare("SELECT amount FROM mandate_operations").all().reduce((sum, row) => sum + BigInt(row.amount), 0n);
|
|
162
|
+
if (!existing && spent + amount > BigInt(mandate.totalRunBudget))
|
|
163
|
+
fail("MANDATE_RUN_BUDGET_EXCEEDED", "operation exceeds the owner mandate total run budget");
|
|
164
|
+
if (!existing)
|
|
165
|
+
app.store.db.prepare("INSERT INTO mandate_operations(operation_id,intent_hash,amount,created_at) VALUES(?,?,?,?)").run(journalOperationId, intentHash, amount.toString(), Date.now());
|
|
166
|
+
app.store.db.exec("COMMIT");
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
app.store.db.exec("ROLLBACK");
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
172
|
+
// Keep the reservation on every failure: a thrown transport error can occur
|
|
173
|
+
// after signing or broadcast, so releasing budget here could authorize a
|
|
174
|
+
// second payment. Exact retries reconcile this same operation identity.
|
|
175
|
+
const executionArgs = command === "settlement.plan" ? { ...args, "mandate-json": Buffer.from(JSON.stringify(mandate)).toString("base64url") } : args;
|
|
176
|
+
const preview = app.preview(command, executionArgs), approval = app.approve(preview.challengeId);
|
|
177
|
+
return (await app.execute(command, { ...executionArgs, "approval-token": approval.token })).data;
|
|
178
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { createInterface } from "node:readline";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
6
|
+
import { homePath } from "./phase3.js";
|
|
7
|
+
const PACKAGE_VERSION = createRequire(import.meta.url)("../package.json").version;
|
|
8
|
+
const home = homePath({});
|
|
9
|
+
if (existsSync(join(home, "profile.json"))) {
|
|
10
|
+
await import("./mcp.js");
|
|
11
|
+
}
|
|
12
|
+
else {
|
|
13
|
+
const send = (value) => process.stdout.write(`${JSON.stringify(value)}\n`);
|
|
14
|
+
const rl = createInterface({ input: process.stdin });
|
|
15
|
+
rl.on("line", (line) => {
|
|
16
|
+
if (Buffer.byteLength(line) > 1024 * 1024)
|
|
17
|
+
return send({ jsonrpc: "2.0", id: null, error: { code: -32600, message: "Request exceeds 1 MiB" } });
|
|
18
|
+
let request;
|
|
19
|
+
try {
|
|
20
|
+
request = JSON.parse(line);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return send({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error" } });
|
|
24
|
+
}
|
|
25
|
+
if (!request || typeof request !== "object" || typeof request.method !== "string" || request.params !== undefined && (!request.params || typeof request.params !== "object" || Array.isArray(request.params)))
|
|
26
|
+
return send({ jsonrpc: "2.0", id: null, error: { code: -32600, message: "Invalid JSON-RPC request" } });
|
|
27
|
+
if (request.method === "initialize")
|
|
28
|
+
return send({ jsonrpc: "2.0", id: request.id, result: { protocolVersion: "2025-06-18", capabilities: { tools: {} }, serverInfo: { name: "supercuts-setup", version: PACKAGE_VERSION } } });
|
|
29
|
+
if (request.method === "notifications/initialized")
|
|
30
|
+
return;
|
|
31
|
+
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 } }] } });
|
|
33
|
+
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.` };
|
|
35
|
+
return send({ jsonrpc: "2.0", id: request.id, result: { content: [{ type: "text", text: JSON.stringify(result) }], structuredContent: result } });
|
|
36
|
+
}
|
|
37
|
+
return send({ jsonrpc: "2.0", id: request.id, error: { code: -32601, message: "Method not found" } });
|
|
38
|
+
});
|
|
39
|
+
}
|
package/dist/mcp.d.ts
ADDED