@supercuts-walrus/backend 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/buyer-setup.js +2 -2
- package/dist/cli.js +6 -3
- package/dist/demo.js +70 -8
- package/dist/funding.d.ts +38 -0
- package/dist/funding.js +78 -0
- package/dist/live-runtime.d.ts +3 -0
- package/dist/live-runtime.js +80 -6
- package/dist/mandate.js +9 -8
- package/dist/mcp-bootstrap.js +24 -2
- package/dist/mcp.js +39 -19
- package/dist/onboarding-doctor.js +13 -2
- package/dist/phase3.js +35 -5
- 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-package.d.ts +9 -0
- package/dist/seller-package.js +50 -0
- package/dist/seller-service.js +9 -3
- package/dist/x402-resource-service.d.ts +2 -0
- package/dist/x402-resource-service.js +36 -17
- package/package.json +1 -1
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());
|
|
@@ -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)
|
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);
|
|
@@ -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", "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,19 @@ 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 === "X402_FACILITATOR_UNAVAILABLE")
|
|
1168
|
+
return "the signed transaction remains journaled; resume the same operation ID after facilitator health recovers";
|
|
1169
|
+
if (code === "OPERATION_ID_CONFLICT" || code === "DEAL_OPERATION_CONFLICT")
|
|
1170
|
+
return "reuse the original exact input for this operation ID or start a deliberately new deal";
|
|
1171
|
+
if (code === "X402_TRANSACTION_EXPIRY_INVALID")
|
|
1172
|
+
return "run onboarding doctor against the configured Sui RPC and retry the same operation ID after compatibility is restored";
|
|
1143
1173
|
return "inspect the stable error code and retry with corrected input";
|
|
1144
1174
|
}
|
package/dist/remote-buyer.d.ts
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import { type CapabilityManifest } from "./capability.js";
|
|
2
2
|
import { type AgentMandate } from "./mandate.js";
|
|
3
3
|
import type { Phase3App } from "./phase3.js";
|
|
4
|
+
export declare function analysisAgreementDraft(input: {
|
|
5
|
+
sessionId: string;
|
|
6
|
+
transcriptHash: string;
|
|
7
|
+
serviceHash: string;
|
|
8
|
+
paymentAmount: string;
|
|
9
|
+
buyerPublicKey: string[];
|
|
10
|
+
counterparty: string;
|
|
11
|
+
capabilityId: string;
|
|
12
|
+
manifestHash: string;
|
|
13
|
+
now?: number;
|
|
14
|
+
scalar?: () => string;
|
|
15
|
+
}): Record<string, unknown>;
|
|
4
16
|
interface Entry {
|
|
5
17
|
manifest: CapabilityManifest;
|
|
6
18
|
blobId: string;
|
|
@@ -62,6 +74,12 @@ export declare class RemoteBuyer {
|
|
|
62
74
|
sessionId: string;
|
|
63
75
|
operationId: string;
|
|
64
76
|
}): Promise<Record<string, unknown>>;
|
|
77
|
+
research(input: {
|
|
78
|
+
operationId: string;
|
|
79
|
+
runId: string;
|
|
80
|
+
topic: string;
|
|
81
|
+
budget: string;
|
|
82
|
+
}): Promise<Record<string, unknown>>;
|
|
65
83
|
events(runId: string): Promise<Record<string, unknown>>;
|
|
66
84
|
}
|
|
67
85
|
export {};
|
package/dist/remote-buyer.js
CHANGED
|
@@ -5,10 +5,23 @@ import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
|
|
|
5
5
|
import { validateCapabilityManifest, verifySignedCapability } from "./capability.js";
|
|
6
6
|
import { fail } from "./errors.js";
|
|
7
7
|
import { executeMandated } from "./mandate.js";
|
|
8
|
+
export function analysisAgreementDraft(input) {
|
|
9
|
+
const scalar = input.scalar ?? (() => BigInt(`0x${randomBytes(16).toString("hex")}`).toString());
|
|
10
|
+
return {
|
|
11
|
+
sessionId: input.sessionId, rail: "shielded", dealId: scalar(), revision: "1",
|
|
12
|
+
transcriptRootLo: BigInt(`0x${input.transcriptHash.slice(0, 32)}`).toString(),
|
|
13
|
+
transcriptRootHi: BigInt(`0x${input.transcriptHash.slice(32)}`).toString(),
|
|
14
|
+
serviceDigestLo: BigInt(`0x${input.serviceHash.slice(0, 32)}`).toString(),
|
|
15
|
+
serviceDigestHi: BigInt(`0x${input.serviceHash.slice(32)}`).toString(),
|
|
16
|
+
settlementNonceLo: scalar(), settlementNonceHi: scalar(), paymentAmount: input.paymentAmount,
|
|
17
|
+
blinding: scalar(), expiry: String((input.now ?? Date.now()) + 30 * 60_000),
|
|
18
|
+
buyerPublicKey: input.buyerPublicKey, counterparty: input.counterparty,
|
|
19
|
+
capabilityId: input.capabilityId, manifestHash: input.manifestHash,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
8
22
|
import { decryptSessionMessage, encryptSessionMessage } from "./negotiation.js";
|
|
9
23
|
const sha256 = (value) => createHash("sha256").update(value).digest("hex");
|
|
10
24
|
const num = (value, name) => typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1 ? value : fail("SELLER_PREVIEW_INVALID", `${name} must be between zero and one`);
|
|
11
|
-
const halves = (content) => { const hash = sha256(content); return [BigInt(`0x${hash.slice(0, 32)}`).toString(), BigInt(`0x${hash.slice(32)}`).toString()]; };
|
|
12
25
|
export class RemoteBuyer {
|
|
13
26
|
app;
|
|
14
27
|
mandate;
|
|
@@ -192,10 +205,15 @@ export class RemoteBuyer {
|
|
|
192
205
|
fail("BUYER_PRIVACY_REQUIRED", "saved owner privacy rule did not select shielded settlement");
|
|
193
206
|
const publicKey = this.app.store.getSecret("identity:x25519-public")?.toString("base64url") ?? fail("IDENTITY_MISSING", "buyer delivery key is missing");
|
|
194
207
|
const identity = (await this.app.execute("doctor", {})).data;
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
const
|
|
198
|
-
const
|
|
208
|
+
const serviceHash = sha256(JSON.stringify({ capabilityId: input.capabilityId, briefHash: input.briefHash, artifactSha256, deliveryPublicKey: publicKey }));
|
|
209
|
+
this.app.store.db.exec("CREATE TABLE IF NOT EXISTS remote_buyer_agreements(session_id TEXT PRIMARY KEY,context_hash TEXT NOT NULL,draft_json TEXT NOT NULL,created_at INTEGER NOT NULL)");
|
|
210
|
+
const contextHash = sha256(JSON.stringify({ capabilityId: input.capabilityId, operationId: input.operationId, sessionId: input.sessionId, runId: input.runId, briefHash: input.briefHash, transcriptHash: negotiated.transcriptHash, serviceHash, artifactSha256 }));
|
|
211
|
+
const priorDraft = this.app.store.db.prepare("SELECT context_hash AS contextHash,draft_json AS draftJson FROM remote_buyer_agreements WHERE session_id=?").get(input.sessionId);
|
|
212
|
+
if (priorDraft && priorDraft.contextHash !== contextHash)
|
|
213
|
+
fail("AGREEMENT_SESSION_CONFLICT", "analysis session identifies a different agreement context");
|
|
214
|
+
const draft = priorDraft ? JSON.parse(priorDraft.draftJson) : analysisAgreementDraft({ sessionId: input.sessionId, transcriptHash: String(negotiated.transcriptHash), serviceHash, paymentAmount: entry.manifest.priceBand.minimum, buyerPublicKey: identity.babyJubPublicKey, counterparty: entry.manifest.sellerId, capabilityId: entry.manifest.capabilityId, manifestHash: entry.manifest.manifestHash });
|
|
215
|
+
if (!priorDraft)
|
|
216
|
+
this.app.store.db.prepare("INSERT INTO remote_buyer_agreements(session_id,context_hash,draft_json,created_at) VALUES(?,?,?,?)").run(input.sessionId, contextHash, JSON.stringify(draft), Date.now());
|
|
199
217
|
const signed = await this.request("/v1/agreement", { method: "POST", body: JSON.stringify({ capabilityId: input.capabilityId, sessionId: input.sessionId, runId: input.runId, briefHash: input.briefHash, deliveryPublicKey: publicKey, agreement: Buffer.from(JSON.stringify(draft)).toString("base64url") }) });
|
|
200
218
|
if (signed.artifactSha256 !== artifactSha256 || signed.capabilityId !== input.capabilityId || signed.sessionId !== input.sessionId)
|
|
201
219
|
fail("AGREEMENT_BINDING_MISMATCH", "seller response changed the prepared artifact or session");
|
|
@@ -227,5 +245,41 @@ export class RemoteBuyer {
|
|
|
227
245
|
const recorded = (await this.app.execute("resource.fetch", { "request-id": input.sessionId, "operation-id": input.operationId, response: artifact.toString("base64url") })).data;
|
|
228
246
|
return { status: "delivered", sessionId: input.sessionId, artifactSha256: response.artifactSha256, body: artifact.toString("utf8"), record: { deliveryStatus: recorded.deliveryStatus, responseHash: recorded.responseHash } };
|
|
229
247
|
}
|
|
248
|
+
async research(input) {
|
|
249
|
+
if (!input.operationId || input.operationId.length > 256 || !/^[1-9][0-9]*$/.test(input.budget))
|
|
250
|
+
fail("BUYER_WORKFLOW_INVALID", "research requires a stable operation ID and positive total budget");
|
|
251
|
+
this.app.store.db.exec("CREATE TABLE IF NOT EXISTS remote_research_workflows(operation_id TEXT PRIMARY KEY,input_hash TEXT NOT NULL,created_at INTEGER NOT NULL,updated_at INTEGER NOT NULL,completed_at INTEGER)");
|
|
252
|
+
const inputHash = sha256(JSON.stringify({ runId: input.runId, topic: input.topic, budget: input.budget })), existing = this.app.store.db.prepare("SELECT input_hash AS inputHash,completed_at AS completedAt FROM remote_research_workflows WHERE operation_id=?").get(input.operationId);
|
|
253
|
+
if (existing && existing.inputHash !== inputHash)
|
|
254
|
+
fail("DEAL_OPERATION_CONFLICT", "research operation ID identifies another topic, budget, or run");
|
|
255
|
+
if (!existing)
|
|
256
|
+
this.app.store.db.prepare("INSERT INTO remote_research_workflows(operation_id,input_hash,created_at,updated_at) VALUES(?,?,?,?)").run(input.operationId, inputHash, Date.now(), Date.now());
|
|
257
|
+
const checkpoint = async (stage, action) => {
|
|
258
|
+
const key = `research:${input.operationId}:${stage}`, prior = this.app.store.getSecret(key);
|
|
259
|
+
if (prior)
|
|
260
|
+
return JSON.parse(prior.toString());
|
|
261
|
+
const value = await action();
|
|
262
|
+
this.app.store.putSecretOnce(key, Buffer.from(JSON.stringify(value)));
|
|
263
|
+
this.app.store.db.prepare("UPDATE remote_research_workflows SET updated_at=? WHERE operation_id=?").run(Date.now(), input.operationId);
|
|
264
|
+
return value;
|
|
265
|
+
};
|
|
266
|
+
const child = (stage) => `research:${stage}:${sha256(`${input.operationId}:${stage}`).slice(0, 40)}`;
|
|
267
|
+
const ranked = await checkpoint("selection", () => this.rankBriefs({ topic: input.topic, budget: input.budget }));
|
|
268
|
+
const winnerCapabilityId = String(ranked.winner?.capabilityId ?? "");
|
|
269
|
+
const analysisCapabilityId = String(ranked.analysisCapabilityId ?? "");
|
|
270
|
+
if (!winnerCapabilityId || !analysisCapabilityId)
|
|
271
|
+
fail("ANALYSIS_UNAVAILABLE", "ranked catalog has no affordable brief and matching analysis");
|
|
272
|
+
const brief = await checkpoint("brief", () => this.purchaseBrief({ capabilityId: winnerCapabilityId, operationId: child("brief-payment"), requestId: child("brief-request"), runId: input.runId, task: `Sui research: ${input.topic}`, budget: input.budget }));
|
|
273
|
+
const briefHash = String(brief.artifactSha256);
|
|
274
|
+
const sessionId = child("analysis-session"), paymentOperationId = child("analysis-payment");
|
|
275
|
+
const agreement = await checkpoint("agreement", () => this.agreement({ capabilityId: analysisCapabilityId, operationId: paymentOperationId, sessionId, runId: input.runId, briefHash }));
|
|
276
|
+
const settlement = await checkpoint("settlement", () => this.settleAnalysis({ sessionId, operationId: paymentOperationId }));
|
|
277
|
+
const analysis = await checkpoint("delivery", async () => {
|
|
278
|
+
const value = await this.delivery({ sessionId, operationId: paymentOperationId });
|
|
279
|
+
return value.status === "delivered" ? value : fail("DELIVERY_PENDING", "analysis payment is finalized and seller delivery is still pending; resume the same research operation");
|
|
280
|
+
});
|
|
281
|
+
this.app.store.db.prepare("UPDATE remote_research_workflows SET completed_at=COALESCE(completed_at,?),updated_at=? WHERE operation_id=?").run(Date.now(), Date.now(), input.operationId);
|
|
282
|
+
return { operationId: input.operationId, state: "complete", replayed: Boolean(existing?.completedAt), selection: ranked, brief, agreement, settlement, analysis };
|
|
283
|
+
}
|
|
230
284
|
async events(runId) { return this.request(`/v1/events?runId=${encodeURIComponent(runId)}`); }
|
|
231
285
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface RoleHomeLease {
|
|
2
|
+
acquired: boolean;
|
|
3
|
+
ownerPid?: number;
|
|
4
|
+
release(): void;
|
|
5
|
+
}
|
|
6
|
+
/** Best-effort process lease. SQLite still owns data integrity; this prevents
|
|
7
|
+
* multiple MCP event/memory workers from operating one role home at once. */
|
|
8
|
+
export declare function acquireRoleHomeLease(home: string): RoleHomeLease;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { closeSync, openSync, readFileSync, unlinkSync, writeSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
function active(pid) {
|
|
5
|
+
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
6
|
+
return false;
|
|
7
|
+
try {
|
|
8
|
+
process.kill(pid, 0);
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/** Best-effort process lease. SQLite still owns data integrity; this prevents
|
|
16
|
+
* multiple MCP event/memory workers from operating one role home at once. */
|
|
17
|
+
export function acquireRoleHomeLease(home) {
|
|
18
|
+
const path = join(home, ".mcp.lock"), token = randomUUID();
|
|
19
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
20
|
+
try {
|
|
21
|
+
const fd = openSync(path, "wx", 0o600);
|
|
22
|
+
writeSync(fd, `${JSON.stringify({ pid: process.pid, token, startedAt: Date.now() })}\n`);
|
|
23
|
+
closeSync(fd);
|
|
24
|
+
let released = false;
|
|
25
|
+
return { acquired: true, release: () => {
|
|
26
|
+
if (released)
|
|
27
|
+
return;
|
|
28
|
+
released = true;
|
|
29
|
+
try {
|
|
30
|
+
const value = JSON.parse(readFileSync(path, "utf8"));
|
|
31
|
+
if (value.token === token)
|
|
32
|
+
unlinkSync(path);
|
|
33
|
+
}
|
|
34
|
+
catch { /* already removed */ }
|
|
35
|
+
} };
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
if (error.code !== "EEXIST")
|
|
39
|
+
throw error;
|
|
40
|
+
let ownerPid;
|
|
41
|
+
try {
|
|
42
|
+
ownerPid = Number(JSON.parse(readFileSync(path, "utf8")).pid);
|
|
43
|
+
}
|
|
44
|
+
catch { /* stale malformed lock */ }
|
|
45
|
+
if (ownerPid !== undefined && active(ownerPid))
|
|
46
|
+
return { acquired: false, ownerPid, release: () => { } };
|
|
47
|
+
try {
|
|
48
|
+
unlinkSync(path);
|
|
49
|
+
}
|
|
50
|
+
catch { /* another process won the cleanup race */ }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return { acquired: false, release: () => { } };
|
|
54
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { readFileSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { decodeSuiPrivateKey } from "@mysten/sui/cryptography";
|
|
4
|
+
import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
|
|
5
|
+
import { createCapabilityManifest, validateCapabilityPayload } from "./capability.js";
|
|
6
|
+
import { fail } from "./errors.js";
|
|
7
|
+
import { executeMandated, loadMandate } from "./mandate.js";
|
|
8
|
+
import { Phase3App } from "./phase3.js";
|
|
9
|
+
const ownerFile = (path) => {
|
|
10
|
+
const value = resolve(path);
|
|
11
|
+
if ((statSync(value).mode & 0o077) !== 0)
|
|
12
|
+
fail("SELLER_CONFIG_PERMISSIONS", `${value} must be owner-only`);
|
|
13
|
+
return value;
|
|
14
|
+
};
|
|
15
|
+
export async function packageHostedSeller(input) {
|
|
16
|
+
if (![input.home, input.spec, input.artifact, input.metadata, input.registration, input.bundle].every((value) => typeof value === "string" && value.length > 0))
|
|
17
|
+
fail("ARGUMENT_REQUIRED", "seller package requires --home, --spec, --artifact, --metadata, --registration, and --bundle");
|
|
18
|
+
const specPath = ownerFile(input.spec), artifactPath = ownerFile(input.artifact), metadataPath = ownerFile(input.metadata);
|
|
19
|
+
const artifact = readFileSync(artifactPath);
|
|
20
|
+
if (!artifact.length || artifact.length > 10 * 1024 * 1024)
|
|
21
|
+
fail("SELLER_ARTIFACT_INVALID", "hosted artifact must contain 1 byte to 10 MiB");
|
|
22
|
+
const spec = JSON.parse(readFileSync(specPath, "utf8"));
|
|
23
|
+
const metadata = JSON.parse(readFileSync(metadataPath, "utf8"));
|
|
24
|
+
const home = resolve(input.home), app = new Phase3App(home);
|
|
25
|
+
try {
|
|
26
|
+
if (app.profile.role !== "seller" || app.profile.mock || app.profile.verifierVersion !== 2)
|
|
27
|
+
fail("SELLER_POOL_VERSION_MISMATCH", "hosted packaging requires a separate live v2 seller role home");
|
|
28
|
+
const encoded = app.store.getSecret("live:sui-private-key")?.toString() ?? fail("LIVE_CREDENTIAL_MISSING", "seller signer is missing");
|
|
29
|
+
const decoded = decodeSuiPrivateKey(encoded);
|
|
30
|
+
if (decoded.scheme !== "ED25519")
|
|
31
|
+
fail("SIGNER_UNSUPPORTED", "seller requires an ED25519 signer");
|
|
32
|
+
const signer = Ed25519Keypair.fromSecretKey(decoded.secretKey), manifest = createCapabilityManifest({ ...spec, sellerId: signer.toSuiAddress() });
|
|
33
|
+
validateCapabilityPayload(manifest, "output", metadata);
|
|
34
|
+
const published = await executeMandated(app, loadMandate(home), "capability.publish", { "operation-id": `publish-${manifest.manifestHash}`, manifest });
|
|
35
|
+
const manifestBlobId = String(published.blobId ?? "");
|
|
36
|
+
if (!manifestBlobId)
|
|
37
|
+
fail("WALRUS_PUBLISH_FAILED", "capability publication omitted a Walrus blob ID");
|
|
38
|
+
const transportPublicKey = app.store.getSecret("identity:x25519-public")?.toString("base64url") ?? fail("IDENTITY_MISSING", "seller transport key is missing");
|
|
39
|
+
const sellerSignature = (await signer.signPersonalMessage(Buffer.from(`${manifest.manifestHash}:${transportPublicKey}`))).signature;
|
|
40
|
+
const registration = { manifest, manifestBlobId, transportPublicKey, sellerSignature };
|
|
41
|
+
const registrationPath = resolve(input.registration), bundlePath = resolve(input.bundle);
|
|
42
|
+
writeFileSync(registrationPath, `${JSON.stringify(registration, null, 2)}\n`, { mode: 0o600, flag: "wx" });
|
|
43
|
+
const bundle = { sellers: [{ sellerId: manifest.sellerId, home, capabilities: [{ registrationFile: registrationPath, artifactFile: artifactPath, outputMetadata: metadata, contentType: input.contentType ?? "application/octet-stream" }] }] };
|
|
44
|
+
writeFileSync(bundlePath, `${JSON.stringify(bundle, null, 2)}\n`, { mode: 0o600, flag: "wx" });
|
|
45
|
+
return { sellerId: manifest.sellerId, capabilityId: manifest.capabilityId, manifestHash: manifest.manifestHash, manifestBlobId, registrationFile: registrationPath, provisioningBundle: bundlePath, nextCommand: `supercuts-seller-v2-provision ${JSON.stringify(bundlePath)}` };
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
app.close();
|
|
49
|
+
}
|
|
50
|
+
}
|
package/dist/seller-service.js
CHANGED
|
@@ -182,7 +182,13 @@ export class SellerService {
|
|
|
182
182
|
try {
|
|
183
183
|
const url = new URL(request.url ?? "/", this.origin);
|
|
184
184
|
if (request.method === "GET" && url.pathname === "/health")
|
|
185
|
-
return this.reply(response, this.#healthy ? 200 : 503, { ok: this.#healthy, service: "supercuts-sellers" });
|
|
185
|
+
return this.reply(response, this.#healthy ? 200 : 503, { ok: this.#healthy, service: "supercuts-sellers", buyerAccess: this.allowedBuyers.size ? "restricted" : "open" });
|
|
186
|
+
if (request.method === "GET" && url.pathname === "/v1/access") {
|
|
187
|
+
const buyerAddress = String(url.searchParams.get("buyerAddress") ?? "").toLowerCase();
|
|
188
|
+
if (!/^0x[0-9a-f]{64}$/.test(buyerAddress))
|
|
189
|
+
fail("SELLER_REQUEST_INVALID", "buyerAddress is invalid");
|
|
190
|
+
return this.reply(response, 200, { buyerAddress, allowed: this.allowedBuyers.size === 0 || this.allowedBuyers.has(buyerAddress), mode: this.allowedBuyers.size ? "restricted" : "open" });
|
|
191
|
+
}
|
|
186
192
|
if (request.method === "GET" && url.pathname === "/v2/directory/health") {
|
|
187
193
|
if (!this.directoryAggregator)
|
|
188
194
|
return this.reply(response, 503, { ok: false, service: "supercuts-directory", reason: "aggregator not configured" });
|
|
@@ -241,7 +247,7 @@ export class SellerService {
|
|
|
241
247
|
const sessionId = validId(input.sessionId, "sessionId"), runId = validId(input.runId, "runId"), buyerAddress = String(input.buyerAddress ?? ""), buyerPublicKey = String(input.buyerPublicKey ?? ""), ciphertext = String(input.ciphertext ?? ""), buyerSignature = String(input.buyerSignature ?? "");
|
|
242
248
|
if (!/^0x[0-9a-f]{64}$/.test(buyerAddress) || buyerPublicKey.length > 1024 || ciphertext.length > 16384 || !ciphertext || !buyerSignature)
|
|
243
249
|
fail("SELLER_REQUEST_INVALID", "buyer identity or encrypted offer is invalid");
|
|
244
|
-
if (!this.allowedBuyers.has(buyerAddress))
|
|
250
|
+
if (this.allowedBuyers.size > 0 && !this.allowedBuyers.has(buyerAddress))
|
|
245
251
|
fail("SELLER_BUYER_FORBIDDEN", "buyer is outside the seller service allowlist");
|
|
246
252
|
const signedBytes = Buffer.from(`${sessionId}:${capabilityId}:${buyerPublicKey}:${sha256(Buffer.from(ciphertext, "base64"))}`);
|
|
247
253
|
if (!(await isValidPersonalMessageSignature(signedBytes, buyerSignature, { address: buyerAddress })))
|
|
@@ -410,7 +416,7 @@ if (process.argv[1] && import.meta.url.endsWith(process.argv[1])) {
|
|
|
410
416
|
const required = (key) => process.env[key] ?? fail("SELLER_CONFIG_MISSING", `${key} is required`);
|
|
411
417
|
const port = Number(process.env.PORT ?? "3000");
|
|
412
418
|
const origin = required("SELLER_ORIGIN");
|
|
413
|
-
SellerService.create({ db: new SellerDatabase(required("SUPABASE_URL"), required("SUPABASE_SERVICE_ROLE_KEY")), snapshotKey: required("SELLER_STATE_KEY"), origin, facilitatorUrl: required("FACILITATOR_URL"), facilitatorKey: required("FACILITATOR_SHARED_KEY"), allowedBuyers: new Set(
|
|
419
|
+
SellerService.create({ db: new SellerDatabase(required("SUPABASE_URL"), required("SUPABASE_SERVICE_ROLE_KEY")), snapshotKey: required("SELLER_STATE_KEY"), origin, facilitatorUrl: required("FACILITATOR_URL"), facilitatorKey: required("FACILITATOR_SHARED_KEY"), allowedBuyers: new Set((process.env.SELLER_ALLOWED_BUYERS ?? "").split(",").map((item) => item.trim().toLowerCase()).filter(Boolean)), claudeKey: required("CLAUDE_API_KEY"), ...(process.env.DIRECTORY_WALRUS_AGGREGATOR ? { directoryAggregator: process.env.DIRECTORY_WALRUS_AGGREGATOR } : {}) })
|
|
414
420
|
.then((service) => { const server = service.createServer(); server.listen(port, "0.0.0.0"); const close = () => server.close(() => { service.close(); process.exit(0); }); process.on("SIGINT", close); process.on("SIGTERM", close); })
|
|
415
421
|
.catch((error) => { process.stderr.write(`${error instanceof Error ? error.message : "seller service failed"}\n`); process.exitCode = 1; });
|
|
416
422
|
}
|
|
@@ -11,6 +11,8 @@ export interface X402ResourceServiceConfig {
|
|
|
11
11
|
payTo: string;
|
|
12
12
|
amount: string;
|
|
13
13
|
maxTimeoutSeconds?: number;
|
|
14
|
+
/** Bounded retry schedule for transient facilitator cold starts. */
|
|
15
|
+
facilitatorRetryDelaysMs?: number[];
|
|
14
16
|
}
|
|
15
17
|
/** Seller-owned standard x402 v2 exact resource with additional request-binding replay protection. */
|
|
16
18
|
export declare class X402ResourceService {
|