@supercuts-walrus/backend 0.1.2 → 0.1.3
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 +23 -9
- package/dist/cli.js +7 -2
- package/dist/dashboard.js +1 -1
- package/dist/deal-engine.d.ts +13 -0
- package/dist/deal-engine.js +59 -34
- package/dist/deal.d.ts +35 -0
- package/dist/deal.js +7 -1
- package/dist/embedding-adapter.d.ts +26 -0
- package/dist/embedding-adapter.js +116 -0
- package/dist/facilitator.js +2 -1
- package/dist/funding.js +10 -1
- package/dist/live-deal-ports.d.ts +2 -1
- package/dist/live-deal-ports.js +23 -4
- package/dist/live-runtime.js +26 -8
- package/dist/main-module.d.ts +5 -0
- package/dist/main-module.js +16 -0
- package/dist/mcp.js +2 -0
- package/dist/memory-scorecard.d.ts +32 -0
- package/dist/memory-scorecard.js +76 -0
- package/dist/memwal.d.ts +28 -0
- package/dist/memwal.js +81 -2
- package/dist/onboarding-doctor.js +7 -5
- package/dist/phase3.d.ts +43 -0
- package/dist/phase3.js +143 -30
- package/dist/seam.js +2 -1
- package/dist/seller-service.js +2 -1
- package/dist/seller-setup.js +9 -7
- package/dist/settlement.d.ts +32 -3
- package/dist/settlement.js +56 -11
- package/dist/testnet-defaults.d.ts +21 -0
- package/dist/testnet-defaults.js +26 -0
- package/package.json +3 -2
package/dist/buyer-setup.js
CHANGED
|
@@ -14,6 +14,7 @@ import { verifySignedCapability } from "./capability.js";
|
|
|
14
14
|
import { WalrusDealBlobs } from "./deal-wire.js";
|
|
15
15
|
import { canonicalJson } from "./settlement.js";
|
|
16
16
|
import { validateDirectoryOrigin } from "./seller-directory.js";
|
|
17
|
+
import { TESTNET_DEFAULTS, withTestnetDefaults } from "./testnet-defaults.js";
|
|
17
18
|
function ownerFile(path) {
|
|
18
19
|
if ((statSync(path).mode & 0o077) !== 0)
|
|
19
20
|
fail("SETUP_PERMISSIONS", `${path} must be owner-only`);
|
|
@@ -34,6 +35,9 @@ function origins(values) {
|
|
|
34
35
|
});
|
|
35
36
|
}
|
|
36
37
|
function validateConfig(config) {
|
|
38
|
+
withTestnetDefaults(config);
|
|
39
|
+
if (!config.sellerOrigins?.length && !config.directoryUrl)
|
|
40
|
+
config.directoryUrl = TESTNET_DEFAULTS.directoryUrl;
|
|
37
41
|
amount(config.perDealCeiling, "perDealCeiling");
|
|
38
42
|
amount(config.totalBudget, "totalBudget");
|
|
39
43
|
if (BigInt(config.totalBudget) < BigInt(config.perDealCeiling))
|
|
@@ -86,22 +90,25 @@ export async function promptBuyerSetup(home) {
|
|
|
86
90
|
fail("SETUP_INPUT_REQUIRED", "interactive setup requires a terminal; use --config with an owner-only JSON file");
|
|
87
91
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
88
92
|
try {
|
|
89
|
-
const ask = async (label) => (await rl.question(`${label}: `)).trim();
|
|
90
|
-
|
|
93
|
+
const ask = async (label, fallback) => (await rl.question(fallback ? `${label} [${fallback}]: ` : `${label}: `)).trim() || fallback || "";
|
|
94
|
+
process.stderr.write("Press Enter to accept a bracketed default. Credentials are read from owner-only files, never typed here.\n");
|
|
95
|
+
const days = Number(await ask("Policy duration in days", "14"));
|
|
91
96
|
if (!Number.isSafeInteger(days) || days < 1 || days > 365)
|
|
92
97
|
fail("SETUP_INVALID", "policy duration must be 1..365 days");
|
|
93
98
|
const config = {
|
|
94
|
-
deploymentConfig: await ask("Pinned Testnet deployment config path"), tatumKeyFile: await ask("Tatum key file path"),
|
|
95
|
-
writeGrpc: await ask("Sui write gRPC URL"), tatumGrpc: await ask("Tatum read gRPC URL"),
|
|
96
|
-
walrusPublisher: await ask("Walrus publisher URL"), walrusAggregator: await ask("Walrus aggregator URL"),
|
|
99
|
+
deploymentConfig: await ask("Pinned Testnet deployment config path", "bundled"), tatumKeyFile: await ask("Owner-only Tatum API key file path"),
|
|
100
|
+
writeGrpc: await ask("Sui write gRPC URL", TESTNET_DEFAULTS.writeGrpc), tatumGrpc: await ask("Tatum read gRPC URL", TESTNET_DEFAULTS.tatumGrpc),
|
|
101
|
+
walrusPublisher: await ask("Walrus publisher URL", TESTNET_DEFAULTS.walrusPublisher), walrusAggregator: await ask("Walrus aggregator URL", TESTNET_DEFAULTS.walrusAggregator),
|
|
97
102
|
memoryConfigFile: await ask("Owner-only Walrus Memory config path (blank to skip Memory)"),
|
|
98
|
-
directoryUrl: await ask("Shared directory origin (
|
|
103
|
+
directoryUrl: await ask("Shared directory origin (type none for direct sellers only)", TESTNET_DEFAULTS.directoryUrl),
|
|
99
104
|
sellerOrigins: (await ask("Additional seller HTTPS origins, comma separated (or blank)")).split(",").map((item) => item.trim()).filter(Boolean),
|
|
100
105
|
allowedCounterparties: (await ask("Initially approved seller Sui addresses, comma separated (or blank)")).split(",").map((item) => item.trim()).filter(Boolean),
|
|
101
|
-
perDealCeiling: await ask("Maximum per deal in MIST"), totalBudget: await ask("Lifetime cumulative budget in MIST"),
|
|
106
|
+
perDealCeiling: await ask("Maximum per deal in MIST", "2000000"), totalBudget: await ask("Lifetime cumulative budget in MIST", "10000000"),
|
|
102
107
|
expiresAt: Date.now() + days * 24 * 60 * 60 * 1000, allowPublicX402: (await ask("Allow public x402 when requested? yes/no")).toLowerCase() === "yes",
|
|
103
108
|
};
|
|
104
|
-
if (
|
|
109
|
+
if (config.deploymentConfig === "bundled")
|
|
110
|
+
config.deploymentConfig = "";
|
|
111
|
+
if (!config.directoryUrl || config.directoryUrl === "none")
|
|
105
112
|
delete config.directoryUrl;
|
|
106
113
|
return setupBuyerConfig(home, validateConfig(config));
|
|
107
114
|
}
|
|
@@ -124,7 +131,14 @@ function setupBuyerConfig(home, config) {
|
|
|
124
131
|
const decoded = decodeSuiPrivateKey(readFileSync(keyPath, "utf8").trim());
|
|
125
132
|
if (decoded.scheme !== "ED25519")
|
|
126
133
|
fail("SETUP_INVALID", "buyer key must use ED25519");
|
|
127
|
-
|
|
134
|
+
const address = Ed25519Keypair.fromSecretKey(decoded.secretKey).toSuiAddress();
|
|
135
|
+
return { home, address, ...session, policy: "configured", nextSteps: [
|
|
136
|
+
`Fund only this buyer with Testnet SUI: supercuts funding execute --home ${home} --public-payment <MIST> --shielded-payment 0 --operation-id fund-1 --request-faucet --confirm (or use https://faucet.sui.io for ${address})`,
|
|
137
|
+
...(config.allowedCounterparties?.length ? [] : [`Review the directory, then approve a seller: supercuts policy approve-seller --home ${home} --capability-id <id>`]),
|
|
138
|
+
...(config.memoryConfigFile ? [`Keep the Walrus Memory embedding adapter running (supercuts-embedding-adapter). An MCP agent flushes outcome memories itself; SDK-only hosts also run: supercuts-memory-worker ${home}`] : []),
|
|
139
|
+
`Check readiness: supercuts onboarding doctor --home ${home}`,
|
|
140
|
+
`Connect an agent with ${session.mcpConfig}, or call BuyerClient.executeDeal from @supercuts-walrus/sdk.`,
|
|
141
|
+
] };
|
|
128
142
|
}
|
|
129
143
|
export function createBuyerSession(home) {
|
|
130
144
|
if (!existsSync(join(home, "profile.json")))
|
package/dist/cli.js
CHANGED
|
@@ -13,13 +13,18 @@ if (["wallet", "offer", "pay", "resource", "memory", "audit", "capability", "set
|
|
|
13
13
|
command += `.${words.shift() ?? ""}`;
|
|
14
14
|
try {
|
|
15
15
|
if (!command || command === "help" || command === "--help" || command === "setup.") {
|
|
16
|
-
process.stdout.write(`${JSON.stringify({ schemaVersion: "supercuts.help.v1", ok: true, commands: ["onboard buyer", "onboard seller", "seller scaffold", "seller research", "seller package", "funding prepare", "funding execute", "setup buyer", "setup seller", "setup renew", "setup session", "init", "doctor", "status", "wallet", "offer", "pay", "memory", "capability", "audit"], setup: "Run supercuts onboard buyer or supercuts onboard seller with an isolated --home. Then use seller scaffold to generate a capability. Research sellers can use seller research to create a cited owner-review draft before packaging.", live: "Fund only the generated buyer address. A seller does not need faucet funds to publish or receive a capability payment." })}\n`);
|
|
16
|
+
process.stdout.write(`${JSON.stringify({ schemaVersion: "supercuts.help.v1", ok: true, commands: ["onboard buyer", "onboard seller", "seller scaffold", "seller research", "seller package", "funding prepare", "funding execute", "setup buyer", "setup seller", "setup renew", "setup session", "init", "doctor", "status", "wallet", "offer", "pay", "memory", "memory insights", "capability", "audit"], setup: "Run supercuts onboard buyer or supercuts onboard seller with an isolated --home. Then use seller scaffold to generate a capability. Research sellers can use seller research to create a cited owner-review draft before packaging.", live: "Fund only the generated buyer address. A seller does not need faucet funds to publish or receive a capability payment." })}\n`);
|
|
17
17
|
process.exit(0);
|
|
18
18
|
}
|
|
19
19
|
const args = parseArgs(words);
|
|
20
20
|
const home = homePath(args);
|
|
21
21
|
const fundingInput = { publicPayment: String(args["public-payment"] ?? "0"), shieldedPayment: String(args["shielded-payment"] ?? "0"), ...(typeof args["gas-reserve"] === "string" ? { gasReserve: args["gas-reserve"] } : {}), ...(typeof args["change-reserve"] === "string" ? { changeReserve: args["change-reserve"] } : {}) };
|
|
22
|
-
const result = command === "init" ? initialize(home, args) : command === "setup.buyer" || command === "onboard.buyer" ? { schemaVersion: "supercuts.setup.v1", ok: true, command, data: typeof args.config === "string" ? setupBuyer(home, args.config) : await promptBuyerSetup(home) } : command === "setup.seller" || command === "onboard.seller" ? { schemaVersion: "supercuts.setup.v1", ok: true, command, data: typeof args.config === "string" ? setupSeller(home, args.config) : await promptSellerSetup(home) } : command === "seller.scaffold" ? { schemaVersion: "supercuts.seller-scaffold.v1", ok: true, command, data: scaffoldSeller({ ...args, home }) } : command === "seller.research" ? { schemaVersion: "supercuts.seller-research.v1", ok: true, command, data: await generateSellerResearch({ kind: String(args.kind ?? ""), topic: String(args.topic ?? ""), output: String(args.output ?? ""), ...(typeof args["context-file"] === "string" ? { contextFile: args["context-file"] } : {}), ...(typeof args["allowed-domains"] === "string" ? { allowedDomains: args["allowed-domains"] } : {}), ...(typeof args.model === "string" ? { model: args.model } : {}) }) } : command === "setup.renew" ? { schemaVersion: "supercuts.setup.v1", ok: true, command, data: renewBuyer(home, String(args.config ?? "")) } : command === "setup.session" ? { schemaVersion: "supercuts.setup.v1", ok: true, command, data: createBuyerSession(home) } : command === "funding.prepare" ? { schemaVersion: "supercuts.funding.v1", ok: true, command, data: await prepareFunding(home, fundingInput) } : command === "funding.execute" ? { schemaVersion: "supercuts.funding.v1", ok: true, command, data: await executeFunding(home, { ...fundingInput, operationId: String(args["operation-id"] ?? ""), requestFaucet: args["request-faucet"] === true, confirmed: args.confirm === true }) } : command === "seller.package" ? { schemaVersion: "supercuts.seller-package.v1", ok: true, command, data: await packageHostedSeller({ home, spec: String(args.spec ?? ""), artifact: String(args.artifact ?? ""), metadata: String(args.metadata ?? ""), registration: String(args.registration ?? ""), bundle: String(args.bundle ?? ""), ...(typeof args["content-type"] === "string" ? { contentType: args["content-type"] } : {}) }) } : command === "policy.add" ? { schemaVersion: "supercuts.setup.v1", ok: true, command, data: await addBuyerPrivacyRule(home, String(args["capability-id"] ?? "")) } : command === "policy.approve-seller" ? { schemaVersion: "supercuts.setup.v1", ok: true, command, data: await approveBuyerSeller(home, String(args["capability-id"] ?? "")) } : command === "
|
|
22
|
+
const result = command === "init" ? initialize(home, args) : command === "setup.buyer" || command === "onboard.buyer" ? { schemaVersion: "supercuts.setup.v1", ok: true, command, data: typeof args.config === "string" ? setupBuyer(home, args.config) : await promptBuyerSetup(home) } : command === "setup.seller" || command === "onboard.seller" ? { schemaVersion: "supercuts.setup.v1", ok: true, command, data: typeof args.config === "string" ? setupSeller(home, args.config) : await promptSellerSetup(home) } : command === "seller.scaffold" ? { schemaVersion: "supercuts.seller-scaffold.v1", ok: true, command, data: scaffoldSeller({ ...args, home }) } : command === "seller.research" ? { schemaVersion: "supercuts.seller-research.v1", ok: true, command, data: await generateSellerResearch({ kind: String(args.kind ?? ""), topic: String(args.topic ?? ""), output: String(args.output ?? ""), ...(typeof args["context-file"] === "string" ? { contextFile: args["context-file"] } : {}), ...(typeof args["allowed-domains"] === "string" ? { allowedDomains: args["allowed-domains"] } : {}), ...(typeof args.model === "string" ? { model: args.model } : {}) }) } : command === "setup.renew" ? { schemaVersion: "supercuts.setup.v1", ok: true, command, data: renewBuyer(home, String(args.config ?? "")) } : command === "setup.session" ? { schemaVersion: "supercuts.setup.v1", ok: true, command, data: createBuyerSession(home) } : command === "funding.prepare" ? { schemaVersion: "supercuts.funding.v1", ok: true, command, data: await prepareFunding(home, fundingInput) } : command === "funding.execute" ? { schemaVersion: "supercuts.funding.v1", ok: true, command, data: await executeFunding(home, { ...fundingInput, operationId: String(args["operation-id"] ?? ""), requestFaucet: args["request-faucet"] === true, confirmed: args.confirm === true }) } : command === "seller.package" ? { schemaVersion: "supercuts.seller-package.v1", ok: true, command, data: await packageHostedSeller({ home, spec: String(args.spec ?? ""), artifact: String(args.artifact ?? ""), metadata: String(args.metadata ?? ""), registration: String(args.registration ?? ""), bundle: String(args.bundle ?? ""), ...(typeof args["content-type"] === "string" ? { contentType: args["content-type"] } : {}) }) } : command === "policy.add" ? { schemaVersion: "supercuts.setup.v1", ok: true, command, data: await addBuyerPrivacyRule(home, String(args["capability-id"] ?? "")) } : command === "policy.approve-seller" ? { schemaVersion: "supercuts.setup.v1", ok: true, command, data: await approveBuyerSeller(home, String(args["capability-id"] ?? "")) } : command === "memory.insights" ? await (async () => { const app = new Phase3App(home); try {
|
|
23
|
+
return { schemaVersion: "supercuts.memory-insights.v1", ok: true, command, data: await app.memoryInsights() };
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
app.close();
|
|
27
|
+
} })() : command === "onboarding.doctor" ? { schemaVersion: "supercuts.onboarding.v1", ok: true, command, data: await onboardingDoctor(home, typeof args.config === "string" ? args.config : undefined) } : await (async () => { const app = new Phase3App(home); try {
|
|
23
28
|
return await app.execute(command, args);
|
|
24
29
|
}
|
|
25
30
|
finally {
|
package/dist/dashboard.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createServer } from "node:http";
|
|
2
2
|
import { readDemoEvents, recordedReplay } from "./events.js";
|
|
3
|
-
const html = `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Supercuts funded-run evidence</title><style>body{font:15px system-ui;margin:0;background:#0b1020;color:#eef2ff}header{position:sticky;top:0;padding:18px 24px;background:#111936;border-bottom:1px solid #334155}main{max-width:980px;margin:auto;padding:24px}.event{display:grid;grid-template-columns:9rem 10rem 1fr;gap:14px;padding:14px;border-bottom:1px solid #27334f}.mode{color:#fbbf24;font-weight:700}.complete,.allowed{color:#86efac}.failed,.rejected{color:#fca5a5}code{color:#bfdbfe;white-space:pre-wrap}</style></head><body><header><b>Supercuts</b> · <span class="mode" id="mode">loading evidence…</span></header><main id="events"></main><script>const out=document.querySelector('#events');const source=new EventSource('/events');source.onmessage=e=>{const v=JSON.parse(e.data);document.querySelector('#mode').textContent=v.mode==='live'?'LIVE FUNDED RUN':'RECORDED FUNDED RUN';const n=document.createElement('div');n.className='event';n.innerHTML='<span>#'+v.sequence+' '+v.actor+'</span><span class="'+v.status+'">'+v.stage+' · '+v.status+'</span><code></code>';n.querySelector('code').textContent=JSON.stringify(v.publicDetails,null,2);out.append(n)};</script></body></html>`;
|
|
3
|
+
const html = `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Supercuts funded-run evidence</title><style>body{font:15px system-ui;margin:0;background:#0b1020;color:#eef2ff}header{position:sticky;top:0;padding:18px 24px;background:#111936;border-bottom:1px solid #334155}main{max-width:980px;margin:auto;padding:24px}.event{display:grid;grid-template-columns:9rem 10rem 1fr;gap:14px;padding:14px;border-bottom:1px solid #27334f}.mode{color:#fbbf24;font-weight:700}.complete,.allowed{color:#86efac}.failed,.rejected{color:#fca5a5}code{color:#bfdbfe;white-space:pre-wrap}#learned{margin:0 0 24px;padding:18px;border:1px solid #fbbf24;border-radius:10px;background:#1a1f3a}#learned h2{margin:0 0 6px;font-size:17px}#learned p{margin:0 0 12px;color:#fde68a;font-weight:600}table{width:100%;border-collapse:collapse;font-size:14px}th,td{text-align:left;padding:6px 8px;border-bottom:1px solid #27334f}th{color:#94a3b8;font-weight:500}.event.memory{background:#1a1f3a}</style></head><body><header><b>Supercuts</b> · <span class="mode" id="mode">loading evidence…</span></header><main><section id="learned" hidden><h2>What Supercuts learned from Walrus Memory</h2><p></p><table><thead><tr><th>Seller</th><th>Capability</th><th>Reliability</th><th>Succeeded / failed</th><th>Last outcome</th><th>Median latency</th></tr></thead><tbody></tbody></table></section><div id="events"></div></main><script>const out=document.querySelector('#events'),learned=document.querySelector('#learned');function renderLearned(d){learned.hidden=false;const w=d.withMemory||{},o=d.withoutMemory;learned.querySelector('p').textContent=d.changedSeller?'Memory changed who gets paid: '+(o?o.sellerId.slice(0,12):'no eligible seller')+' \u2192 '+String(w.sellerId).slice(0,12):(d.changedRail||d.changedRisk||d.changedMaximum)?'Memory changed how this payment settles: '+w.rail+', risk '+w.riskLevel+', max '+w.maximumAmount:'Memory confirmed the default choice';const t=learned.querySelector('tbody');t.textContent='';for(const s of d.scorecards||[]){const r=document.createElement('tr');for(const c of [String(s.counterparty).slice(0,12),s.capabilityId,Math.round(s.reliability*100)+'%',s.successes+' / '+s.failures,s.lastOutcome||'\u2014',s.medianLatencyMs!=null?(s.medianLatencyMs/1000).toFixed(1)+'s':'\u2014']){const td=document.createElement('td');td.textContent=c;r.append(td)}t.append(r)}}const source=new EventSource('/events');source.onmessage=e=>{const v=JSON.parse(e.data);document.querySelector('#mode').textContent=v.mode==='live'?'LIVE FUNDED RUN':'RECORDED FUNDED RUN';if(v.stage==='memory-influence')renderLearned(v.publicDetails);const n=document.createElement('div');n.className=v.stage.startsWith('memory')?'event memory':'event';n.innerHTML='<span>#'+v.sequence+' '+v.actor+'</span><span class="'+v.status+'">'+v.stage+' · '+v.status+'</span><code></code>';n.querySelector('code').textContent=JSON.stringify(v.publicDetails,null,2);out.append(n)};</script></body></html>`;
|
|
4
4
|
export function serveEventDashboard(path, port, replay = false) {
|
|
5
5
|
const server = createServer((request, response) => {
|
|
6
6
|
if (request.url === "/") {
|
package/dist/deal-engine.d.ts
CHANGED
|
@@ -35,6 +35,16 @@ export interface DealOutcomeQueued {
|
|
|
35
35
|
}
|
|
36
36
|
/** A port owns transport, signing, and finality checks. It must be safe to call
|
|
37
37
|
* again with the same child IDs after a process crash. */
|
|
38
|
+
export interface SellerFault {
|
|
39
|
+
stage: "negotiation" | "delivery";
|
|
40
|
+
code: string;
|
|
41
|
+
paymentDigest?: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Codes the buyer runtime verified against seller-supplied evidence. Buyer-side, network, and Walrus readback
|
|
45
|
+
* errors are excluded: a readback mismatch can come from an aggregator, not the seller.
|
|
46
|
+
*/
|
|
47
|
+
export declare const SELLER_FAULT_CODES: Set<string>;
|
|
38
48
|
export interface DealPorts {
|
|
39
49
|
readBlob(blobId: string): Promise<Uint8Array>;
|
|
40
50
|
discover(input: ExecuteDealInput): Promise<SignedCapabilityRegistration>;
|
|
@@ -50,6 +60,8 @@ export interface DealPorts {
|
|
|
50
60
|
unresolved: true;
|
|
51
61
|
}>;
|
|
52
62
|
inventory(blobId: string): Promise<boolean>;
|
|
63
|
+
/** Optional: queue a verified seller-fault memory so later discovery demotes this seller. Best effort. */
|
|
64
|
+
rememberFailure?(input: ExecuteDealInput, plan: SettlementPlanV1, fault: SellerFault): Promise<void>;
|
|
53
65
|
}
|
|
54
66
|
export declare class DealEngine {
|
|
55
67
|
private readonly journal;
|
|
@@ -58,5 +70,6 @@ export declare class DealEngine {
|
|
|
58
70
|
constructor(journal: DealJournal, ports: DealPorts, mandate: AgentMandate);
|
|
59
71
|
execute(input: ExecuteDealInput): Promise<DealView>;
|
|
60
72
|
resume(operationId: string): Promise<DealView>;
|
|
73
|
+
private rememberSellerFault;
|
|
61
74
|
private finishOutcome;
|
|
62
75
|
}
|
package/dist/deal-engine.js
CHANGED
|
@@ -3,6 +3,11 @@ import { validateCapabilityPayload, verifySignedCapability } from "./capability.
|
|
|
3
3
|
import { dealChildId } from "./deal.js";
|
|
4
4
|
import { fail, SupercutsError } from "./errors.js";
|
|
5
5
|
import { canonicalJson, validateSettlementPlan } from "./settlement.js";
|
|
6
|
+
/**
|
|
7
|
+
* Codes the buyer runtime verified against seller-supplied evidence. Buyer-side, network, and Walrus readback
|
|
8
|
+
* errors are excluded: a readback mismatch can come from an aggregator, not the seller.
|
|
9
|
+
*/
|
|
10
|
+
export const SELLER_FAULT_CODES = new Set(["AGREEMENT_BINDING_MISMATCH", "DEAL_RESOURCE_MISMATCH", "DEAL_AGREEMENT_INVALID", "DEAL_DELIVERY_UNVERIFIED", "CAPABILITY_PAYLOAD_INVALID"]);
|
|
6
11
|
const hash = (value) => createHash("sha256").update(canonicalJson(value)).digest("hex");
|
|
7
12
|
export class DealEngine {
|
|
8
13
|
journal;
|
|
@@ -91,20 +96,26 @@ export class DealEngine {
|
|
|
91
96
|
let negotiation = this.journal.step(operationId, "negotiation");
|
|
92
97
|
if (view.state === "planned")
|
|
93
98
|
view = this.journal.advance(operationId, "planned", "negotiating");
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
99
|
+
try {
|
|
100
|
+
if (!negotiation) {
|
|
101
|
+
const started = Date.now();
|
|
102
|
+
negotiation = await this.ports.negotiate(input, registration, plan, dealChildId(operationId, "agreement"));
|
|
103
|
+
if (!negotiation.agreementBlobId || !/^[0-9a-f]{64}$/.test(negotiation.agreementCiphertextHash) || !Array.isArray(negotiation.transcriptBlobs) || !negotiation.transcriptBlobs.length)
|
|
104
|
+
fail("DEAL_AGREEMENT_INVALID", "seller agreement or Walrus transcript evidence is incomplete");
|
|
105
|
+
this.journal.saveStep(operationId, "negotiation", { planHash: plan.planHash }, negotiation);
|
|
106
|
+
this.journal.recordTiming(operationId, "negotiation", Date.now() - started);
|
|
107
|
+
}
|
|
108
|
+
const agreementReadStarted = Date.now();
|
|
109
|
+
for (const item of [...negotiation.transcriptBlobs, { blobId: negotiation.agreementBlobId, ciphertextHash: negotiation.agreementCiphertextHash }]) {
|
|
110
|
+
if (!item.blobId || !/^[0-9a-f]{64}$/.test(item.ciphertextHash) || createHash("sha256").update(await this.ports.readBlob(item.blobId)).digest("hex") !== item.ciphertextHash)
|
|
111
|
+
fail("WALRUS_BLOB_MISMATCH", "encrypted negotiation or agreement differs from Walrus readback");
|
|
112
|
+
}
|
|
113
|
+
this.journal.recordTiming(operationId, "agreement_readback", Date.now() - agreementReadStarted);
|
|
101
114
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
fail("WALRUS_BLOB_MISMATCH", "encrypted negotiation or agreement differs from Walrus readback");
|
|
115
|
+
catch (error) {
|
|
116
|
+
await this.rememberSellerFault(input, plan, "negotiation", undefined, error);
|
|
117
|
+
throw error;
|
|
106
118
|
}
|
|
107
|
-
this.journal.recordTiming(operationId, "agreement_readback", Date.now() - agreementReadStarted);
|
|
108
119
|
if (view.state === "negotiating")
|
|
109
120
|
view = this.journal.advance(operationId, "negotiating", "agreed");
|
|
110
121
|
this.journal.recordBlob(operationId, "agreement", negotiation.agreementCiphertextHash, negotiation.agreementBlobId);
|
|
@@ -139,29 +150,35 @@ export class DealEngine {
|
|
|
139
150
|
let artifact = this.journal.step(operationId, "delivery");
|
|
140
151
|
if (view.state === "paid")
|
|
141
152
|
view = this.journal.advance(operationId, "paid", "delivery_pending");
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
153
|
+
try {
|
|
154
|
+
if (!artifact) {
|
|
155
|
+
const started = Date.now();
|
|
156
|
+
const candidate = await this.ports.deliver(input, plan, negotiation, payment, dealChildId(operationId, "delivery"));
|
|
157
|
+
if ("pending" in candidate)
|
|
158
|
+
return this.journal.get(operationId);
|
|
159
|
+
if (!candidate.blobId || !/^[0-9a-f]{64}$/.test(candidate.ciphertextHash) || !/^[0-9a-f]{64}$/.test(candidate.artifactSha256))
|
|
160
|
+
fail("DEAL_DELIVERY_UNVERIFIED", "seller delivery evidence is incomplete");
|
|
161
|
+
if (typeof candidate.artifactBase64 !== "string" || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(candidate.artifactBase64))
|
|
162
|
+
fail("DEAL_DELIVERY_UNVERIFIED", "seller artifact encoding is invalid");
|
|
163
|
+
const artifactBytes = Buffer.from(candidate.artifactBase64, "base64");
|
|
164
|
+
if (!artifactBytes.length || artifactBytes.length > 512 * 1024 || createHash("sha256").update(artifactBytes).digest("hex") !== candidate.artifactSha256)
|
|
165
|
+
fail("DEAL_DELIVERY_UNVERIFIED", "seller artifact hash or size is invalid");
|
|
166
|
+
validateCapabilityPayload(registration.manifest, "output", candidate.metadata);
|
|
167
|
+
artifact = candidate;
|
|
168
|
+
this.journal.saveStep(operationId, "delivery", { paymentDigest: payment.transactionDigest }, artifact);
|
|
169
|
+
this.journal.recordTiming(operationId, "delivery", Date.now() - started);
|
|
170
|
+
}
|
|
171
|
+
if (createHash("sha256").update(Buffer.from(artifact.artifactBase64, "base64")).digest("hex") !== artifact.artifactSha256)
|
|
172
|
+
fail("DEAL_DELIVERY_UNVERIFIED", "saved artifact hash differs from delivery evidence");
|
|
173
|
+
const deliveryReadStarted = Date.now();
|
|
174
|
+
if (createHash("sha256").update(await this.ports.readBlob(artifact.blobId)).digest("hex") !== artifact.ciphertextHash)
|
|
175
|
+
fail("WALRUS_BLOB_MISMATCH", "encrypted delivery differs from Walrus readback");
|
|
176
|
+
this.journal.recordTiming(operationId, "delivery_readback", Date.now() - deliveryReadStarted);
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
await this.rememberSellerFault(input, plan, "delivery", payment.transactionDigest, error);
|
|
180
|
+
throw error;
|
|
158
181
|
}
|
|
159
|
-
if (createHash("sha256").update(Buffer.from(artifact.artifactBase64, "base64")).digest("hex") !== artifact.artifactSha256)
|
|
160
|
-
fail("DEAL_DELIVERY_UNVERIFIED", "saved artifact hash differs from delivery evidence");
|
|
161
|
-
const deliveryReadStarted = Date.now();
|
|
162
|
-
if (createHash("sha256").update(await this.ports.readBlob(artifact.blobId)).digest("hex") !== artifact.ciphertextHash)
|
|
163
|
-
fail("WALRUS_BLOB_MISMATCH", "encrypted delivery differs from Walrus readback");
|
|
164
|
-
this.journal.recordTiming(operationId, "delivery_readback", Date.now() - deliveryReadStarted);
|
|
165
182
|
this.journal.recordBlob(operationId, "delivery", artifact.ciphertextHash, artifact.blobId);
|
|
166
183
|
if (view.state === "delivery_pending")
|
|
167
184
|
view = this.journal.advance(operationId, "delivery_pending", "delivered");
|
|
@@ -169,6 +186,14 @@ export class DealEngine {
|
|
|
169
186
|
view = this.journal.advance(operationId, "delivered", "outcome_pending");
|
|
170
187
|
return this.finishOutcome(operationId, input);
|
|
171
188
|
}
|
|
189
|
+
async rememberSellerFault(input, plan, stage, paymentDigest, error) {
|
|
190
|
+
if (!(error instanceof SupercutsError) || !SELLER_FAULT_CODES.has(error.code) || !this.ports.rememberFailure)
|
|
191
|
+
return;
|
|
192
|
+
try {
|
|
193
|
+
await this.ports.rememberFailure(input, plan, { stage, code: error.code, ...(paymentDigest ? { paymentDigest } : {}) });
|
|
194
|
+
}
|
|
195
|
+
catch { /* the original seller fault stays the reported error */ }
|
|
196
|
+
}
|
|
172
197
|
async finishOutcome(operationId, input) {
|
|
173
198
|
const view = this.journal.get(operationId);
|
|
174
199
|
if (view.evidence.outcomeMemory === "indexed")
|
package/dist/deal.d.ts
CHANGED
|
@@ -43,6 +43,41 @@ export interface DealEvidenceView {
|
|
|
43
43
|
inventory: "pending" | "indexed" | "missing" | "unresolved";
|
|
44
44
|
paymentFinalized: boolean;
|
|
45
45
|
paymentDigest?: string;
|
|
46
|
+
/** What Walrus Memory changed about this payment versus the same mandate with no memory. */
|
|
47
|
+
memoryInfluence?: {
|
|
48
|
+
changedSeller: boolean;
|
|
49
|
+
changedRail: boolean;
|
|
50
|
+
changedRisk: boolean;
|
|
51
|
+
changedMaximum: boolean;
|
|
52
|
+
withoutMemory?: {
|
|
53
|
+
sellerId: string;
|
|
54
|
+
capabilityId: string;
|
|
55
|
+
rail: string;
|
|
56
|
+
riskLevel: string;
|
|
57
|
+
maximumAmount: string;
|
|
58
|
+
};
|
|
59
|
+
withMemory: {
|
|
60
|
+
sellerId: string;
|
|
61
|
+
capabilityId: string;
|
|
62
|
+
rail: string;
|
|
63
|
+
riskLevel: string;
|
|
64
|
+
maximumAmount: string;
|
|
65
|
+
};
|
|
66
|
+
citedBlobIds: string[];
|
|
67
|
+
};
|
|
68
|
+
/** Learned seller scorecards used for discovery; counts and blob IDs only, never memory plaintext. */
|
|
69
|
+
memoryRanking?: Array<{
|
|
70
|
+
capabilityId: string;
|
|
71
|
+
counterparty: string;
|
|
72
|
+
reliability: number;
|
|
73
|
+
observations: number;
|
|
74
|
+
successes: number;
|
|
75
|
+
failures: number;
|
|
76
|
+
unresolved: number;
|
|
77
|
+
lastOutcome?: string;
|
|
78
|
+
medianLatencyMs?: number;
|
|
79
|
+
priceMinimum: string;
|
|
80
|
+
}>;
|
|
46
81
|
}
|
|
47
82
|
export interface DealDelivery {
|
|
48
83
|
dealId: string;
|
package/dist/deal.js
CHANGED
|
@@ -176,7 +176,13 @@ export class DealJournal {
|
|
|
176
176
|
const blobs = this.store.db.prepare("SELECT kind,blob_id,status FROM deal_blobs WHERE operation_id=?").all(row.operation_id);
|
|
177
177
|
const memory = this.store.db.prepare("SELECT blob_id,status FROM deal_memory_outbox WHERE operation_id=? AND event_kind='outcome'").get(row.operation_id);
|
|
178
178
|
const transcript = this.step(row.operation_id, "negotiation");
|
|
179
|
-
|
|
179
|
+
const hasRecords = Boolean(this.store.db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='app_records'").get());
|
|
180
|
+
const record = (scope, id) => { if (!hasRecords)
|
|
181
|
+
return undefined; const found = this.store.db.prepare("SELECT value FROM app_records WHERE scope=? AND id=?").get(scope, id); return found ? JSON.parse(found.value) : undefined; };
|
|
182
|
+
const influence = record("memory-influence", `deal:${row.operation_id}`);
|
|
183
|
+
const ranking = record("deal-ranking", row.operation_id);
|
|
184
|
+
const memoryRanking = ranking?.scorecards.map(({ capabilityId, counterparty, reliability, observations, successes, failures, unresolved, lastOutcome, medianLatencyMs, priceMinimum }) => ({ capabilityId, counterparty, reliability, observations, successes, failures, unresolved, ...(lastOutcome ? { lastOutcome } : {}), ...(medianLatencyMs !== undefined ? { medianLatencyMs } : {}), priceMinimum }));
|
|
185
|
+
return { version: "supercuts.deal-evidence.v1", dealId, transcriptBlobIds: transcript?.transcriptBlobs?.map((item) => item.blobId) ?? [], ...(blobs.find((item) => item.kind === "agreement")?.blob_id ? { agreementBlobId: blobs.find((item) => item.kind === "agreement").blob_id } : {}), ...(blobs.find((item) => item.kind === "delivery")?.blob_id ? { deliveryBlobId: blobs.find((item) => item.kind === "delivery").blob_id } : {}), ...(memory?.blob_id ? { outcomeMemoryBlobId: memory.blob_id } : {}), ciphertextReadback: blobs.some((item) => item.status === "unresolved") ? "unresolved" : ["agreement", "delivery"].every((kind) => blobs.some((item) => item.kind === kind && item.status === "verified")) ? "verified" : "pending", inventory: memory?.status === "indexed" ? "indexed" : memory?.status === "unresolved" ? "unresolved" : "pending", paymentFinalized: view.evidence.payment === "finalized", ...(view.paymentDigest ? { paymentDigest: view.paymentDigest } : {}), ...(influence ? { memoryInfluence: influence } : {}), ...(memoryRanking ? { memoryRanking } : {}) };
|
|
180
186
|
}
|
|
181
187
|
delivery(dealId) {
|
|
182
188
|
const row = this.store.db.prepare("SELECT operation_id FROM deal_operations WHERE deal_id=?").get(dealId);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* OpenAI-compatible embedding adapter for local models (`supercuts-embedding-adapter`).
|
|
4
|
+
*
|
|
5
|
+
* Walrus Mainnet Memory indexes 1536-dimensional embeddings. This loopback proxy forwards requests to a
|
|
6
|
+
* local OpenAI-compatible endpoint (Ollama by default) and zero-pads shorter vectors to 1536 dimensions.
|
|
7
|
+
* Zero-padding preserves cosine similarity and Euclidean distance as long as every write and recall uses
|
|
8
|
+
* this same adapter and upstream model. Memory plaintext only reaches the configured upstream.
|
|
9
|
+
*/
|
|
10
|
+
import { type Server } from "node:http";
|
|
11
|
+
export declare const TARGET_DIMENSIONS = 1536;
|
|
12
|
+
export declare function padEmbedding(embedding: unknown): number[];
|
|
13
|
+
interface EmbeddingPayload {
|
|
14
|
+
data: Array<{
|
|
15
|
+
embedding: unknown;
|
|
16
|
+
} & Record<string, unknown>>;
|
|
17
|
+
}
|
|
18
|
+
export declare function transformEmbeddingResponse(payload: unknown): EmbeddingPayload;
|
|
19
|
+
export interface EmbeddingAdapterOptions {
|
|
20
|
+
upstreamBaseUrl: string;
|
|
21
|
+
upstreamModel: string;
|
|
22
|
+
upstreamApiKey?: string;
|
|
23
|
+
fetchImpl?: typeof fetch;
|
|
24
|
+
}
|
|
25
|
+
export declare function createEmbeddingAdapter({ fetchImpl, upstreamBaseUrl, upstreamApiKey, upstreamModel, }: EmbeddingAdapterOptions): Server;
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* OpenAI-compatible embedding adapter for local models (`supercuts-embedding-adapter`).
|
|
4
|
+
*
|
|
5
|
+
* Walrus Mainnet Memory indexes 1536-dimensional embeddings. This loopback proxy forwards requests to a
|
|
6
|
+
* local OpenAI-compatible endpoint (Ollama by default) and zero-pads shorter vectors to 1536 dimensions.
|
|
7
|
+
* Zero-padding preserves cosine similarity and Euclidean distance as long as every write and recall uses
|
|
8
|
+
* this same adapter and upstream model. Memory plaintext only reaches the configured upstream.
|
|
9
|
+
*/
|
|
10
|
+
import { createServer } from "node:http";
|
|
11
|
+
import { isMainModule } from "./main-module.js";
|
|
12
|
+
export const TARGET_DIMENSIONS = 1536;
|
|
13
|
+
const MAX_REQUEST_BYTES = 1024 * 1024;
|
|
14
|
+
export function padEmbedding(embedding) {
|
|
15
|
+
if (!Array.isArray(embedding) ||
|
|
16
|
+
embedding.length < 1 ||
|
|
17
|
+
embedding.length > TARGET_DIMENSIONS) {
|
|
18
|
+
throw new Error(`expected an embedding with 1 to ${TARGET_DIMENSIONS} dimensions, received ${Array.isArray(embedding) ? embedding.length : "a non-array value"}`);
|
|
19
|
+
}
|
|
20
|
+
if (!embedding.every((value) => typeof value === "number" && Number.isFinite(value)))
|
|
21
|
+
throw new Error("upstream embedding contains a non-finite value");
|
|
22
|
+
return [
|
|
23
|
+
...embedding,
|
|
24
|
+
...Array(TARGET_DIMENSIONS - embedding.length).fill(0),
|
|
25
|
+
];
|
|
26
|
+
}
|
|
27
|
+
export function transformEmbeddingResponse(payload) {
|
|
28
|
+
const value = payload;
|
|
29
|
+
if (!value || !Array.isArray(value.data) || !value.data.length)
|
|
30
|
+
throw new Error("upstream response has no embeddings");
|
|
31
|
+
const dimension = value.data[0]?.embedding?.length;
|
|
32
|
+
if (value.data.some((item) => item?.embedding?.length !== dimension))
|
|
33
|
+
throw new Error("upstream embeddings have inconsistent dimensions");
|
|
34
|
+
return {
|
|
35
|
+
...value,
|
|
36
|
+
data: value.data.map((item) => ({
|
|
37
|
+
...item,
|
|
38
|
+
embedding: padEmbedding(item?.embedding),
|
|
39
|
+
})),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export function createEmbeddingAdapter({ fetchImpl = fetch, upstreamBaseUrl, upstreamApiKey, upstreamModel, }) {
|
|
43
|
+
if (!upstreamBaseUrl)
|
|
44
|
+
throw new Error("EMBEDDING_UPSTREAM_BASE_URL is required");
|
|
45
|
+
if (!upstreamModel)
|
|
46
|
+
throw new Error("EMBEDDING_UPSTREAM_MODEL is required");
|
|
47
|
+
const endpoint = `${upstreamBaseUrl.replace(/\/$/, "")}/embeddings`;
|
|
48
|
+
let sourceDimensions;
|
|
49
|
+
const send = (response, status, body) => {
|
|
50
|
+
response.writeHead(status, { "content-type": "application/json" });
|
|
51
|
+
response.end(typeof body === "string" ? body : JSON.stringify(body));
|
|
52
|
+
};
|
|
53
|
+
return createServer(async (request, response) => {
|
|
54
|
+
// Loopback only: reject other Host headers so a DNS-rebinding page cannot drive the local embedding model.
|
|
55
|
+
const host = (request.headers.host ?? "").replace(/:\d+$/, "").replace(/^\[|\]$/g, "");
|
|
56
|
+
if (!["127.0.0.1", "localhost", "::1"].includes(host))
|
|
57
|
+
return send(response, 403, { error: { message: "loopback host required" } });
|
|
58
|
+
if (request.method === "GET" && request.url === "/health")
|
|
59
|
+
return send(response, 200, {
|
|
60
|
+
status: "ok",
|
|
61
|
+
targetDimensions: TARGET_DIMENSIONS,
|
|
62
|
+
upstreamModel,
|
|
63
|
+
});
|
|
64
|
+
if (request.method !== "POST" || request.url !== "/v1/embeddings")
|
|
65
|
+
return send(response, 404, { error: { message: "not found" } });
|
|
66
|
+
try {
|
|
67
|
+
let body = "";
|
|
68
|
+
for await (const chunk of request) {
|
|
69
|
+
body += chunk;
|
|
70
|
+
if (body.length > MAX_REQUEST_BYTES)
|
|
71
|
+
throw new Error("embedding request is too large");
|
|
72
|
+
}
|
|
73
|
+
const requested = JSON.parse(body);
|
|
74
|
+
const upstream = await fetchImpl(endpoint, {
|
|
75
|
+
method: "POST",
|
|
76
|
+
headers: {
|
|
77
|
+
"content-type": "application/json",
|
|
78
|
+
...(upstreamApiKey
|
|
79
|
+
? { authorization: `Bearer ${upstreamApiKey}` }
|
|
80
|
+
: {}),
|
|
81
|
+
},
|
|
82
|
+
body: JSON.stringify({ ...requested, model: upstreamModel }),
|
|
83
|
+
signal: AbortSignal.timeout(60_000),
|
|
84
|
+
});
|
|
85
|
+
const upstreamBody = await upstream.text();
|
|
86
|
+
if (!upstream.ok)
|
|
87
|
+
return send(response, upstream.status, upstreamBody);
|
|
88
|
+
const payload = JSON.parse(upstreamBody), result = transformEmbeddingResponse(payload);
|
|
89
|
+
const dimension = payload.data[0].embedding.length;
|
|
90
|
+
if (sourceDimensions !== undefined && sourceDimensions !== dimension)
|
|
91
|
+
throw new Error("upstream embedding dimensions changed");
|
|
92
|
+
sourceDimensions = dimension;
|
|
93
|
+
send(response, 200, result);
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
send(response, 502, {
|
|
97
|
+
error: {
|
|
98
|
+
message: error instanceof Error ? error.message : "embedding adapter failed",
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
if (isMainModule(import.meta.url)) {
|
|
105
|
+
const port = Number(process.env.EMBEDDING_ADAPTER_PORT ?? 8787);
|
|
106
|
+
const upstreamBaseUrl = process.env.EMBEDDING_UPSTREAM_BASE_URL ?? "http://127.0.0.1:11434/v1";
|
|
107
|
+
const upstreamModel = process.env.EMBEDDING_UPSTREAM_MODEL ?? "embeddinggemma";
|
|
108
|
+
const server = createEmbeddingAdapter({
|
|
109
|
+
upstreamBaseUrl,
|
|
110
|
+
upstreamModel,
|
|
111
|
+
...(process.env.EMBEDDING_UPSTREAM_API_KEY
|
|
112
|
+
? { upstreamApiKey: process.env.EMBEDDING_UPSTREAM_API_KEY }
|
|
113
|
+
: {}),
|
|
114
|
+
});
|
|
115
|
+
server.listen(port, "127.0.0.1", () => process.stdout.write(`${JSON.stringify({ ok: true, listening: `http://127.0.0.1:${port}/v1`, upstream: upstreamBaseUrl, upstreamModel, targetDimensions: TARGET_DIMENSIONS, memoryConfig: { embeddingApiBase: `http://127.0.0.1:${port}/v1`, embeddingModel: `${upstreamModel}-1536` } })}\n`));
|
|
116
|
+
}
|
package/dist/facilitator.js
CHANGED
|
@@ -3,6 +3,7 @@ import { timingSafeEqual } from "node:crypto";
|
|
|
3
3
|
import { SuiGrpcClient } from "@mysten/sui/grpc";
|
|
4
4
|
import { Transaction } from "@mysten/sui/transactions";
|
|
5
5
|
import { isValidTransactionSignature } from "@mysten/sui/verify";
|
|
6
|
+
import { isMainModule } from "./main-module.js";
|
|
6
7
|
const NETWORK = "sui:testnet";
|
|
7
8
|
const SUI = "0x2::sui::SUI";
|
|
8
9
|
/** Sui RPC may expand a type's address to 32 bytes; policy identifiers use its canonical short form. */
|
|
@@ -164,7 +165,7 @@ export class SuiTestnetFacilitator {
|
|
|
164
165
|
}
|
|
165
166
|
send(response, status, body) { const encoded = Buffer.from(JSON.stringify(body)); response.writeHead(status, { "content-type": "application/json", "content-length": String(encoded.length) }); response.end(encoded); }
|
|
166
167
|
}
|
|
167
|
-
if (
|
|
168
|
+
if (isMainModule(import.meta.url)) {
|
|
168
169
|
const key = process.env.FACILITATOR_SHARED_KEY ?? "";
|
|
169
170
|
const grpc = process.env.SUI_TESTNET_GRPC ?? "https://fullnode.testnet.sui.io:443";
|
|
170
171
|
const port = Number(process.env.PORT ?? "3000");
|
package/dist/funding.js
CHANGED
|
@@ -27,6 +27,8 @@ export async function prepareFunding(home, values) {
|
|
|
27
27
|
if (app.profile.role !== "buyer" || app.profile.mock)
|
|
28
28
|
fail("LIVE_PROFILE_REQUIRED", "funding requires a live buyer role home");
|
|
29
29
|
const doctor = await app.execute("doctor", {}), address = String(doctor.data.signer ?? "");
|
|
30
|
+
if (shieldedPayment > 0n && doctor.data.shieldedProofs !== "ready")
|
|
31
|
+
fail("SHIELDED_PROOFS_UNAVAILABLE", String(doctor.data.shieldedFixes?.[0] ?? "shielded proving artifacts are not installed"));
|
|
30
32
|
const endpoint = app.profile.live?.writeGrpcEndpoint ?? fail("LIVE_PROFILE_INVALID", "buyer write RPC is missing");
|
|
31
33
|
const [{ balance }, shielded] = await Promise.all([writeClient(endpoint).getBalance({ owner: address, coinType: "0x2::sui::SUI" }), app.execute("wallet.balance", {})]);
|
|
32
34
|
return calculateFundingPlan({ address, publicBalance: BigInt(balance.balance), shieldedBalance: BigInt(String(shielded.data.available ?? "0")), publicPayment, shieldedPayment, gasReserve, changeReserve });
|
|
@@ -42,7 +44,14 @@ export async function executeFunding(home, values) {
|
|
|
42
44
|
if (plan.faucetRequired) {
|
|
43
45
|
if (!values.requestFaucet)
|
|
44
46
|
fail("BUYER_FUNDING_REQUIRED", `buyer needs ${plan.publicShortfallMist} additional MIST; rerun with --request-faucet --confirm or fund ${plan.address} manually`);
|
|
45
|
-
|
|
47
|
+
try {
|
|
48
|
+
execFileSync("sui", ["client", "faucet", "--url", "https://faucet.testnet.sui.io", "--address", plan.address], { stdio: "inherit" });
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (error.code === "ENOENT")
|
|
52
|
+
fail("SUI_CLI_MISSING", `the Sui CLI is not installed; request Testnet SUI for ${plan.address} at https://faucet.sui.io, then rerun funding execute without --request-faucet`);
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
46
55
|
for (let attempt = 0; attempt < 15; attempt++) {
|
|
47
56
|
await new Promise((resolve) => setTimeout(resolve, 2_000));
|
|
48
57
|
plan = await prepareFunding(home, values);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type SignedCapabilityRegistration } from "./capability.js";
|
|
2
|
-
import type { DealArtifact, DealNegotiation, DealPayment, DealPorts } from "./deal-engine.js";
|
|
2
|
+
import type { DealArtifact, DealNegotiation, DealPayment, DealPorts, SellerFault } from "./deal-engine.js";
|
|
3
3
|
import type { ExecuteDealInput } from "./deal.js";
|
|
4
4
|
import { type AgentMandate } from "./mandate.js";
|
|
5
5
|
import type { Phase3App } from "./phase3.js";
|
|
@@ -32,6 +32,7 @@ export declare class LiveDealPorts implements DealPorts {
|
|
|
32
32
|
negotiate(input: ExecuteDealInput, registration: SignedCapabilityRegistration, plan: SettlementPlanV1, agreementOperationId: string): Promise<LiveNegotiation>;
|
|
33
33
|
pay(input: ExecuteDealInput, plan: SettlementPlanV1, negotiation: DealNegotiation, paymentOperationId: string): Promise<DealPayment>;
|
|
34
34
|
deliver(input: ExecuteDealInput, plan: SettlementPlanV1, negotiation: DealNegotiation, payment: DealPayment, _deliveryOperationId: string): Promise<DealArtifact>;
|
|
35
|
+
rememberFailure(input: ExecuteDealInput, plan: SettlementPlanV1, fault: SellerFault): Promise<void>;
|
|
35
36
|
rememberOutcome(input: ExecuteDealInput, plan: SettlementPlanV1, payment: DealPayment, artifact: DealArtifact, _outcomeOperationId: string): Promise<{
|
|
36
37
|
queued: true;
|
|
37
38
|
payloadHash: string;
|
package/dist/live-deal-ports.js
CHANGED
|
@@ -3,7 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import { randomBytes } from "node:crypto";
|
|
4
4
|
import { decodeSuiPrivateKey } from "@mysten/sui/cryptography";
|
|
5
5
|
import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
|
|
6
|
-
import { verifySignedCapability } from "./capability.js";
|
|
6
|
+
import { verifySignedCapability, validateCapabilityPayload } from "./capability.js";
|
|
7
7
|
import { WalrusDealBlobs, wireDecrypt, wireEncrypt, wireHash, wireJsonHash, wireKey } from "./deal-wire.js";
|
|
8
8
|
import { fail } from "./errors.js";
|
|
9
9
|
import { LiveRuntime } from "./live-runtime.js";
|
|
@@ -127,8 +127,21 @@ export class LiveDealPorts {
|
|
|
127
127
|
const candidates = (await this.catalog()).map(({ registration }) => registration).filter((registration) => (input.capabilityId && registration.manifest.capabilityId === input.capabilityId || input.capabilityClass && registration.manifest.capability === input.capabilityClass) && registration.manifest.asset === input.budget.asset);
|
|
128
128
|
if (!candidates.length)
|
|
129
129
|
fail("DEAL_CAPABILITY_NOT_FOUND", "configured sellers have no matching signed capability");
|
|
130
|
-
|
|
131
|
-
|
|
130
|
+
// Same capability class can carry different signed input schemas; only rank sellers that accept this task.
|
|
131
|
+
const accepting = candidates.filter((registration) => { try {
|
|
132
|
+
validateCapabilityPayload(registration.manifest, "input", input.input);
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return false;
|
|
137
|
+
} });
|
|
138
|
+
if (!accepting.length)
|
|
139
|
+
fail("DEAL_INPUT_UNSUPPORTED", "no matching seller's signed input schema accepts this task input; check deal.catalog for the expected fields");
|
|
140
|
+
candidates.splice(0, candidates.length, ...accepting);
|
|
141
|
+
// Walrus Memory decides which seller gets paid: verified outcomes outrank a lower list price.
|
|
142
|
+
const ranking = await this.app.rankDealCandidates(input.operationId, candidates.map((item) => item.manifest), this.mandate, { budgetMaximum: input.budget.maximum, ...(input.requestedRail ? { requestedRail: input.requestedRail } : {}), ...(input.privacyHint ? { privacyHint: input.privacyHint } : {}) });
|
|
143
|
+
const chosen = ranking.ordered[0];
|
|
144
|
+
return candidates.find((item) => item.manifest.manifestHash === chosen.manifestHash);
|
|
132
145
|
}
|
|
133
146
|
async plan(input, registration, paymentOperationId) {
|
|
134
147
|
const existing = this.app.store.db.prepare("SELECT plan_json FROM settlement_plans WHERE operation_id=?").get(paymentOperationId);
|
|
@@ -137,10 +150,13 @@ export class LiveDealPorts {
|
|
|
137
150
|
validateSettlementPlan(saved);
|
|
138
151
|
if (saved.mandateHash !== settlementMandateHash(this.mandate) || saved.manifestHash !== registration.manifest.manifestHash || saved.operationId !== paymentOperationId)
|
|
139
152
|
fail("DEAL_PLAN_MISMATCH", "persisted plan differs from the owner mandate or seller capability");
|
|
153
|
+
this.app.recordDealMemoryInfluence(input.operationId, saved, this.mandate, { ...(input.requestedRail ? { requestedRail: input.requestedRail } : {}), ...(input.privacyHint ? { privacyHint: input.privacyHint } : {}) });
|
|
140
154
|
return saved;
|
|
141
155
|
}
|
|
142
156
|
const result = await executeMandated(this.app, this.mandate, "settlement.plan", { "operation-id": paymentOperationId, manifest: registration.manifest, "requested-amount": registration.manifest.priceBand.minimum, ...(input.privacyHint ? { "privacy-hint": input.privacyHint } : {}), ...(input.requestedRail ? { "requested-rail": input.requestedRail } : {}) });
|
|
143
|
-
|
|
157
|
+
const plan = result;
|
|
158
|
+
this.app.recordDealMemoryInfluence(input.operationId, plan, this.mandate, { ...(input.requestedRail ? { requestedRail: input.requestedRail } : {}), ...(input.privacyHint ? { privacyHint: input.privacyHint } : {}) });
|
|
159
|
+
return plan;
|
|
144
160
|
}
|
|
145
161
|
async negotiate(input, registration, plan, agreementOperationId) {
|
|
146
162
|
const sessionId = agreementOperationId, capabilityId = registration.manifest.capabilityId, buyerPublicKey = this.app.store.getSecret("identity:x25519-public")?.toString("base64url") ?? fail("IDENTITY_MISSING", "buyer transport key is missing");
|
|
@@ -209,6 +225,9 @@ export class LiveDealPorts {
|
|
|
209
225
|
fail("DEAL_DELIVERY_UNVERIFIED", "seller delivery bytes or metadata differ from the receipt");
|
|
210
226
|
return { blobId: receipt.blobId, ciphertextHash: receipt.ciphertextHash, artifactSha256: receipt.artifactSha256, contentType: receipt.contentType, metadata: receipt.metadata, artifactBase64: artifact.artifactBase64 };
|
|
211
227
|
}
|
|
228
|
+
async rememberFailure(input, plan, fault) {
|
|
229
|
+
this.app.recordVerifiedDealFailure(input.operationId, plan.planHash, fault);
|
|
230
|
+
}
|
|
212
231
|
async rememberOutcome(input, plan, payment, artifact, _outcomeOperationId) {
|
|
213
232
|
return this.app.recordVerifiedDealOutcome(input.operationId, plan.planHash, payment.transactionDigest, artifact.artifactSha256);
|
|
214
233
|
}
|