@supercuts-walrus/backend 0.1.1 → 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.
Files changed (54) hide show
  1. package/README.md +28 -5
  2. package/config/phase1.testnet.json +20 -0
  3. package/config/proving-manifest.json +11 -0
  4. package/dist/buyer-setup.js +27 -13
  5. package/dist/cli.js +10 -2
  6. package/dist/dashboard.js +1 -1
  7. package/dist/deal-adapter.js +0 -2
  8. package/dist/deal-engine.d.ts +13 -0
  9. package/dist/deal-engine.js +64 -39
  10. package/dist/deal.d.ts +35 -0
  11. package/dist/deal.js +9 -3
  12. package/dist/embedding-adapter.d.ts +26 -0
  13. package/dist/embedding-adapter.js +116 -0
  14. package/dist/facilitator.js +2 -1
  15. package/dist/funding.js +10 -1
  16. package/dist/index.d.ts +1 -0
  17. package/dist/index.js +1 -0
  18. package/dist/live-deal-ports.d.ts +2 -1
  19. package/dist/live-deal-ports.js +31 -5
  20. package/dist/live-runtime.d.ts +1 -1
  21. package/dist/live-runtime.js +27 -11
  22. package/dist/live-seller-transport.js +5 -1
  23. package/dist/main-module.d.ts +5 -0
  24. package/dist/main-module.js +16 -0
  25. package/dist/mandate.js +1 -1
  26. package/dist/mcp-bootstrap.js +3 -3
  27. package/dist/mcp.js +2 -0
  28. package/dist/memory-scorecard.d.ts +32 -0
  29. package/dist/memory-scorecard.js +76 -0
  30. package/dist/memwal.d.ts +28 -0
  31. package/dist/memwal.js +81 -2
  32. package/dist/onboarding-doctor.js +12 -5
  33. package/dist/phase3.d.ts +43 -0
  34. package/dist/phase3.js +147 -32
  35. package/dist/role-lock.d.ts +1 -1
  36. package/dist/role-lock.js +1 -1
  37. package/dist/seam.js +2 -1
  38. package/dist/seller-catalog-generate.js +4 -4
  39. package/dist/seller-directory-cli.js +2 -1
  40. package/dist/seller-live-cli.js +10 -1
  41. package/dist/seller-provision.js +15 -1
  42. package/dist/seller-research.d.ts +50 -0
  43. package/dist/seller-research.js +210 -0
  44. package/dist/seller-scaffold.d.ts +1 -0
  45. package/dist/seller-scaffold.js +35 -0
  46. package/dist/seller-service.d.ts +1 -1
  47. package/dist/seller-service.js +8 -20
  48. package/dist/seller-setup.d.ts +3 -0
  49. package/dist/seller-setup.js +109 -0
  50. package/dist/settlement.d.ts +32 -3
  51. package/dist/settlement.js +56 -11
  52. package/dist/testnet-defaults.d.ts +21 -0
  53. package/dist/testnet-defaults.js +26 -0
  54. package/package.json +4 -2
@@ -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
+ }
@@ -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 (process.argv[1] && import.meta.url === new URL(`file://${process.argv[1]}`).href) {
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
- execFileSync("sui", ["client", "faucet", "--url", "https://faucet.testnet.sui.io", "--address", plan.address], { stdio: "inherit" });
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);
package/dist/index.d.ts CHANGED
@@ -15,6 +15,7 @@ export * from "./indexer.js";
15
15
  export * from "./memory.js";
16
16
  export * from "./settlement.js";
17
17
  export * from "./seller-runtime.js";
18
+ export * from "./seller-research.js";
18
19
  export * from "./seller-stdio.js";
19
20
  export * from "./live-index.js";
20
21
  export * from "./live-runtime.js";
package/dist/index.js CHANGED
@@ -15,6 +15,7 @@ export * from "./indexer.js";
15
15
  export * from "./memory.js";
16
16
  export * from "./settlement.js";
17
17
  export * from "./seller-runtime.js";
18
+ export * from "./seller-research.js";
18
19
  export * from "./seller-stdio.js";
19
20
  export * from "./live-index.js";
20
21
  export * from "./live-runtime.js";
@@ -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;
@@ -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";
@@ -64,7 +64,14 @@ export class LiveDealPorts {
64
64
  return Ed25519Keypair.fromSecretKey(decoded.secretKey);
65
65
  }
66
66
  async request(origin, path, body) {
67
- const response = await this.http(new URL(path, origin), { ...(body ? { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) } : {}), redirect: "error", signal: AbortSignal.timeout(90_000) });
67
+ const url = new URL(path, origin);
68
+ let response;
69
+ try {
70
+ response = await this.http(url, { ...(body ? { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) } : {}), redirect: "error", signal: AbortSignal.timeout(90_000) });
71
+ }
72
+ catch (error) {
73
+ return fail("DEAL_SELLER_UNAVAILABLE", `seller ${url.origin} is unreachable; check its health endpoint and network route`, error);
74
+ }
68
75
  let value;
69
76
  try {
70
77
  value = await response.json();
@@ -120,8 +127,21 @@ export class LiveDealPorts {
120
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);
121
128
  if (!candidates.length)
122
129
  fail("DEAL_CAPABILITY_NOT_FOUND", "configured sellers have no matching signed capability");
123
- candidates.sort((a, b) => BigInt(a.manifest.priceBand.minimum) < BigInt(b.manifest.priceBand.minimum) ? -1 : BigInt(a.manifest.priceBand.minimum) > BigInt(b.manifest.priceBand.minimum) ? 1 : a.manifest.capabilityId.localeCompare(b.manifest.capabilityId));
124
- return candidates[0];
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);
125
145
  }
126
146
  async plan(input, registration, paymentOperationId) {
127
147
  const existing = this.app.store.db.prepare("SELECT plan_json FROM settlement_plans WHERE operation_id=?").get(paymentOperationId);
@@ -130,10 +150,13 @@ export class LiveDealPorts {
130
150
  validateSettlementPlan(saved);
131
151
  if (saved.mandateHash !== settlementMandateHash(this.mandate) || saved.manifestHash !== registration.manifest.manifestHash || saved.operationId !== paymentOperationId)
132
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 } : {}) });
133
154
  return saved;
134
155
  }
135
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 } : {}) });
136
- return result;
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;
137
160
  }
138
161
  async negotiate(input, registration, plan, agreementOperationId) {
139
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");
@@ -202,6 +225,9 @@ export class LiveDealPorts {
202
225
  fail("DEAL_DELIVERY_UNVERIFIED", "seller delivery bytes or metadata differ from the receipt");
203
226
  return { blobId: receipt.blobId, ciphertextHash: receipt.ciphertextHash, artifactSha256: receipt.artifactSha256, contentType: receipt.contentType, metadata: receipt.metadata, artifactBase64: artifact.artifactBase64 };
204
227
  }
228
+ async rememberFailure(input, plan, fault) {
229
+ this.app.recordVerifiedDealFailure(input.operationId, plan.planHash, fault);
230
+ }
205
231
  async rememberOutcome(input, plan, payment, artifact, _outcomeOperationId) {
206
232
  return this.app.recordVerifiedDealOutcome(input.operationId, plan.planHash, payment.transactionDigest, artifact.artifactSha256);
207
233
  }
@@ -47,7 +47,7 @@ export declare class LiveRuntime {
47
47
  readonly prover: Groth16Prover;
48
48
  private constructor();
49
49
  static create(profile: LiveRuntimeProfile, store: RoleStore): Promise<LiveRuntime>;
50
- doctor(): Promise<Record<string, unknown>>;
50
+ doctor(proofsRequired?: boolean): Promise<Record<string, unknown>>;
51
51
  balance(): Promise<Record<string, unknown>>;
52
52
  private signed;
53
53
  private operationContext;
@@ -1,7 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { decodeSuiPrivateKey } from "@mysten/sui/cryptography";
3
3
  import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
4
- import { fail } from "./errors.js";
4
+ import { fail, SupercutsError } from "./errors.js";
5
5
  import { assertSuiFinality, assertTatumFinality } from "./finality.js";
6
6
  import { LivePoolIndex } from "./live-index.js";
7
7
  import { ProtocolEngine } from "./protocol-engine.js";
@@ -89,8 +89,18 @@ export class LiveRuntime {
89
89
  const engine = await ProtocolEngine.create({ deploymentDomain: profile.live.deploymentDomain, poolId: profile.poolId, coinTypeHash: profile.live.coinTypeHash, verifierVersion: profile.verifierVersion, initialRoot: profile.live.initialRoot });
90
90
  return new LiveRuntime(profile, store, new TatumLedger(profile.live.tatumGrpcEndpoint, tatumKey), Ed25519Keypair.fromSecretKey(decoded.secretKey), engine);
91
91
  }
92
- async doctor() {
93
- const artifacts = this.prover.doctor();
92
+ async doctor(proofsRequired = true) {
93
+ // Shielded proofs need separately installed artifacts; a public x402 buyer or seller stays usable without them.
94
+ let artifacts = { rapidSnark: false, artifacts: {} }, shieldedProofs = "ready", shieldedProofsReason;
95
+ try {
96
+ artifacts = this.prover.doctor();
97
+ }
98
+ catch (error) {
99
+ if (!(error instanceof SupercutsError) || !["PROVING_ARTIFACT_MISSING", "ARTIFACT_HASH_MISMATCH", "PROVER_TOOL_MISSING"].includes(error.code))
100
+ throw error;
101
+ shieldedProofs = "unavailable";
102
+ shieldedProofsReason = error.code;
103
+ }
94
104
  const [service, suiObject] = await Promise.all([this.tatum.getServiceInfo(), this.client.getObject({ objectId: this.profile.poolId, include: { json: true } })]);
95
105
  if (!suiObject.object)
96
106
  fail("POOL_OBJECT_MISSING", "pinned Testnet pool object is unavailable; check the Sui write RPC and deployment config");
@@ -98,9 +108,11 @@ export class LiveRuntime {
98
108
  fail("TATUM_UNAVAILABLE", "Tatum read RPC did not return service information; check its endpoint and owner-only API key");
99
109
  const babySeed = this.store.getSecret("identity:babyjub-seed") ?? fail("IDENTITY_MISSING", "BabyJub identity is missing");
100
110
  const identity = this.engine.identity(babySeed);
101
- const fixes = [];
102
- if (!artifacts.rapidSnark)
103
- fixes.push("Set an executable RapidSnark path in the role profile for v2 proofs.");
111
+ const fixes = [], shieldedFixes = [];
112
+ if (shieldedProofs === "unavailable")
113
+ shieldedFixes.push(`Shielded payments are unavailable (${shieldedProofsReason}): install the hash-pinned v2 proving artifacts, SnarkJS, and the converter. Public x402 payments do not need them.`);
114
+ else if (!artifacts.rapidSnark)
115
+ shieldedFixes.push("Set an executable RapidSnark path in the role profile for v2 proofs.");
104
116
  if (!this.profile.live.walrusPublisherUrl)
105
117
  fixes.push("Configure the Walrus publisher URL for capability and transcript writes.");
106
118
  if (!this.profile.live.walrusAggregatorUrl)
@@ -117,9 +129,7 @@ export class LiveRuntime {
117
129
  fixes.push(`Walrus ${name} is unreachable; check the configured URL and network access.`);
118
130
  }
119
131
  }
120
- if (this.profile.role === "buyer" && !this.store.getSecret("live:memory-config"))
121
- fixes.push("Configure an owner-only Walrus Memory file using the local 1536-dimensional embedding adapter.");
122
- return { role: this.profile.role, network: "testnet", packageId: this.profile.packageId, poolId: this.profile.poolId, asset: this.profile.coinType, verifierVersion: this.profile.verifierVersion, keyPermissions: "owner-only", rpc: this.profile.live.writeGrpcEndpoint, healthy: true, readyForDemo: fixes.length === 0, fixes, runtime: "testnet-live", signer: this.signer.toSuiAddress(), babyJubPublicKey: identity.publicKey.map(String), x25519PublicKey: this.store.getSecret("identity:x25519-public")?.toString("base64"), rapidSnark: artifacts.rapidSnark, artifacts: artifacts.artifacts, poolObjectVerified: true, tatumServiceVerified: true };
132
+ return { role: this.profile.role, network: "testnet", packageId: this.profile.packageId, poolId: this.profile.poolId, asset: this.profile.coinType, verifierVersion: this.profile.verifierVersion, keyPermissions: "owner-only", rpc: this.profile.live.writeGrpcEndpoint, healthy: true, readyForDemo: fixes.length === 0 && (!proofsRequired || shieldedFixes.length === 0), fixes: [...fixes, ...(proofsRequired ? shieldedFixes : [])], publicFixes: fixes, shieldedProofs, shieldedFixes, readyForPublicRail: fixes.length === 0, runtime: "testnet-live", signer: this.signer.toSuiAddress(), babyJubPublicKey: identity.publicKey.map(String), x25519PublicKey: this.store.getSecret("identity:x25519-public")?.toString("base64"), memoryConfigured: Boolean(this.store.getSecret("live:memory-config")), proofsRequired, rapidSnark: artifacts.rapidSnark, artifacts: artifacts.artifacts, poolObjectVerified: true, tatumServiceVerified: true };
123
133
  }
124
134
  async balance() {
125
135
  const state = await this.index.sync();
@@ -621,8 +631,14 @@ export class LiveRuntime {
621
631
  const paymentPayload = { x402Version: 2, resource, accepted: requirements, payload: { signature: signed.signature, transaction: Buffer.from(signed.transaction).toString("base64") } };
622
632
  const paid = await fetch(paymentUrl, { redirect: "error", headers: { "payment-signature": Buffer.from(JSON.stringify(paymentPayload)).toString("base64"), "x-supercuts-binding": Buffer.from(JSON.stringify({ ...unsignedBinding, payerSignature })).toString("base64") }, signal: AbortSignal.timeout(60_000) });
623
633
  const response = Buffer.from(await paid.arrayBuffer());
624
- if (!paid.ok)
625
- fail("X402_DELIVERY_FAILED", x402ResourceFailure(paid.status, response));
634
+ if (!paid.ok) {
635
+ const detail = x402ResourceFailure(paid.status, response);
636
+ // Settlement status is unknown: the facilitator may have broadcast before failing. Resume reuses the journaled signed
637
+ // transaction for this operation ID, and Sui executes one digest at most once, so retrying cannot pay twice.
638
+ if (paid.status >= 502 && paid.status <= 504 || /X402_FACILITATOR_UNAVAILABLE/.test(detail))
639
+ fail("X402_FACILITATOR_UNAVAILABLE", `${detail}; settlement status is unknown and the payment may already be on chain. Resume with the same operation ID: the same signed transaction is reused and cannot be paid twice`);
640
+ fail("X402_DELIVERY_FAILED", detail);
641
+ }
626
642
  const settlementHeader = paid.headers.get("payment-response") ?? fail("X402_SETTLEMENT_INVALID", "payment-response header is missing");
627
643
  let settlement;
628
644
  try {
@@ -97,6 +97,8 @@ export class LiveSellerTransport {
97
97
  async handle(request, response) {
98
98
  try {
99
99
  const url = new URL(request.url ?? "/", this.config.origin);
100
+ if (request.method === "GET" && url.pathname === "/health")
101
+ return this.respond(response, 200, { ok: true, service: "supercuts-seller", capabilities: this.runtime.catalog().length });
100
102
  if (request.method === "GET" && url.pathname === "/v2/catalog")
101
103
  return this.respond(response, 200, { capabilities: this.runtime.catalog() });
102
104
  if (request.method === "POST" && url.pathname === "/v2/prepare")
@@ -108,7 +110,9 @@ export class LiveSellerTransport {
108
110
  if (url.pathname.startsWith(`${(resource.config.pathPrefix ?? "/capability").replace(/\/$/, "")}/`))
109
111
  return resource.handle(request, response);
110
112
  }
111
- return this.respond(response, 404, { error: "NOT_FOUND" });
113
+ if (request.method === "GET" && [...this.#registrations.values()].some((item) => url.pathname === new URL(item.manifest.endpoint).pathname))
114
+ return this.respond(response, 404, { error: "NEGOTIATION_REQUIRED", message: "Protected resource URLs are issued for a negotiated session." });
115
+ return this.respond(response, 404, { error: "NOT_FOUND", message: "Seller route was not found." });
112
116
  }
113
117
  catch (error) {
114
118
  const known = error instanceof SupercutsError ? error : new SupercutsError("SELLER_REQUEST_FAILED", "seller request failed");
@@ -0,0 +1,5 @@
1
+ /**
2
+ * True when this module is the process entry point. Package managers (pnpm in particular) launch bins
3
+ * through symlinks, so compare resolved real paths rather than raw `argv[1]` strings.
4
+ */
5
+ export declare function isMainModule(importMetaUrl: string, entry?: string | undefined): boolean;
@@ -0,0 +1,16 @@
1
+ import { realpathSync } from "node:fs";
2
+ import { fileURLToPath } from "node:url";
3
+ /**
4
+ * True when this module is the process entry point. Package managers (pnpm in particular) launch bins
5
+ * through symlinks, so compare resolved real paths rather than raw `argv[1]` strings.
6
+ */
7
+ export function isMainModule(importMetaUrl, entry = process.argv[1]) {
8
+ if (!entry)
9
+ return false;
10
+ try {
11
+ return realpathSync(entry) === realpathSync(fileURLToPath(importMetaUrl));
12
+ }
13
+ catch {
14
+ return false;
15
+ }
16
+ }
package/dist/mandate.js CHANGED
@@ -125,7 +125,7 @@ export async function executeMandated(app, mandate, command, raw) {
125
125
  fail("MANDATE_COMMAND_FORBIDDEN", `mandate does not allow ${command}`);
126
126
  const args = {};
127
127
  for (const [key, value] of Object.entries(raw))
128
- args[key] = typeof value === "boolean" ? value : typeof value === "string" ? value : Buffer.from(JSON.stringify(value)).toString("base64url");
128
+ args[key] = typeof value === "boolean" ? value : typeof value === "string" ? value : Buffer.from(canonical(value)).toString("base64url");
129
129
  if (!spec.mutating)
130
130
  return (await app.execute(command, args)).data;
131
131
  const operationId = String(raw["operation-id"]), journalOperationId = command === "settlement.plan" ? `settlement.plan:${operationId}` : operationId;
@@ -37,17 +37,17 @@ else {
37
37
  return;
38
38
  if (request.method === "tools/list")
39
39
  return send({ jsonrpc: "2.0", id: request.id, result: { tools: [
40
- { name: "setup.status", description: "Explain how the owner completes one-time buyer setup locally. Do not request secrets in chat.", inputSchema: { type: "object", properties: {}, additionalProperties: false } },
40
+ { name: "setup.status", description: "Explain how the owner completes one-time buyer or seller setup locally. Do not request secrets in chat.", inputSchema: { type: "object", properties: {}, additionalProperties: false } },
41
41
  { name: "setup.refresh", description: "Reload this MCP connection after the owner completes local setup.", inputSchema: { type: "object", properties: {}, additionalProperties: false } },
42
42
  ] } });
43
43
  if (request.method === "tools/call" && request.params?.name === "setup.status") {
44
44
  const configured = existsSync(join(home, "profile.json"));
45
- const result = { schemaVersion: "supercuts.setup.v1", ok: true, configured, home, nextStep: configured ? "Call setup.refresh to load the configured buyer tools without restarting the connector." : `Run supercuts onboard buyer --home ${JSON.stringify(home)} in a terminal, then call setup.refresh. The command asks for policy limits, seller origins and local credential file paths. Do not paste keys or tokens into Codex.` };
45
+ const result = { schemaVersion: "supercuts.setup.v1", ok: true, configured, home, nextStep: configured ? "Call setup.refresh to load the configured role tools without restarting the connector." : `Run supercuts onboard buyer --home ${JSON.stringify(home)} or supercuts onboard seller --home ${JSON.stringify(home)} in a terminal, then call setup.refresh. Use a separate owner-only home for every role and identity. Do not paste keys or tokens into Codex.` };
46
46
  return send({ jsonrpc: "2.0", id: request.id, result: { content: [{ type: "text", text: JSON.stringify(result) }], structuredContent: result } });
47
47
  }
48
48
  if (request.method === "tools/call" && request.params?.name === "setup.refresh") {
49
49
  if (!existsSync(join(home, "profile.json"))) {
50
- const result = { schemaVersion: "supercuts.setup.v1", ok: false, error: { code: "SETUP_REQUIRED", message: "buyer setup is not complete", recovery: `Run supercuts onboard buyer --home ${JSON.stringify(home)} locally, then retry setup.refresh.` } };
50
+ const result = { schemaVersion: "supercuts.setup.v1", ok: false, error: { code: "SETUP_REQUIRED", message: "role setup is not complete", recovery: `Run supercuts onboard buyer --home ${JSON.stringify(home)} or supercuts onboard seller --home ${JSON.stringify(home)} locally, then retry setup.refresh.` } };
51
51
  return send({ jsonrpc: "2.0", id: request.id, result: { isError: true, content: [{ type: "text", text: JSON.stringify(result) }], structuredContent: result } });
52
52
  }
53
53
  const result = { schemaVersion: "supercuts.setup.v1", ok: true, configured: true, reloading: true };
package/dist/mcp.js CHANGED
@@ -98,6 +98,8 @@ function logTool(name, data) {
98
98
  const plan = journal.step(String(data.operationId), "planning");
99
99
  if (plan)
100
100
  append("supercuts", "settlement-plan", { phase: "after-memory", planHash: plan.planHash, capabilityId: plan.capabilityId, rail: plan.rail, openingAmount: plan.openingAmount, maximumAmount: plan.maximumAmount, riskLevel: plan.riskLevel, reasonCodes: plan.reasonCodes, blobIds: plan.citations.map((item) => item.blobId) });
101
+ if (plan && evidence.memoryInfluence)
102
+ append("supercuts", "memory-influence", { planHash: plan.planHash, changedSeller: evidence.memoryInfluence.changedSeller, changedRail: evidence.memoryInfluence.changedRail, changedRisk: evidence.memoryInfluence.changedRisk, changedMaximum: evidence.memoryInfluence.changedMaximum, withoutMemory: evidence.memoryInfluence.withoutMemory, withMemory: evidence.memoryInfluence.withMemory, scorecards: evidence.memoryRanking, blobIds: evidence.memoryInfluence.citedBlobIds });
101
103
  append("supercuts", "deal-lifecycle", { dealId: data.dealId, state: data.state, rail: data.rail, agreement: data.evidence?.agreement, payment: data.evidence?.payment, delivery: data.evidence?.delivery, outcomeMemory: data.evidence?.outcomeMemory, agreementBlobId: evidence.agreementBlobId, deliveryBlobId: evidence.deliveryBlobId, outcomeMemoryBlobId: evidence.outcomeMemoryBlobId, paymentDigest: evidence.paymentDigest });
102
104
  }
103
105
  }
@@ -0,0 +1,32 @@
1
+ import type { RecalledSettlementMemory, SettlementMemoryScope, SettlementMemoryV1 } from "./settlement.js";
2
+ /** Learned view of one counterparty, derived only from trusted Walrus Memory outcomes. */
3
+ export interface SellerScorecard {
4
+ counterparty: string;
5
+ observations: number;
6
+ successes: number;
7
+ failures: number;
8
+ unresolved: number;
9
+ /** Recency-weighted Beta(1,1) posterior mean; 0.5 when nothing has been learned. */
10
+ reliability: number;
11
+ lastOutcome?: "succeeded" | "failed" | "unresolved";
12
+ lastOutcomeAt?: number;
13
+ medianLatencyMs?: number;
14
+ citedBlobIds: string[];
15
+ }
16
+ export declare const DEFAULT_HALF_LIFE_MS: number;
17
+ /** True when every field the filter names matches the memory scope. */
18
+ export declare function scopeWithin(scope: SettlementMemoryScope, filter: SettlementMemoryScope): boolean;
19
+ /**
20
+ * Trusted deal-level outcome memories for the filter, deduplicated by evidence (one deal may be recalled twice).
21
+ * Only completed deals and verified seller faults carry `signal.rail`; stage events (agreement, settlement,
22
+ * delivery) stay as evidence but are not counted as additional deals.
23
+ */
24
+ export declare function trustedOutcomes(recalled: readonly RecalledSettlementMemory[], filter: SettlementMemoryScope): Array<{
25
+ blobId: string;
26
+ memory: SettlementMemoryV1;
27
+ }>;
28
+ export declare function buildScorecard(recalled: readonly RecalledSettlementMemory[], filter: SettlementMemoryScope & {
29
+ counterparty: string;
30
+ }, now: number, halfLifeMs?: number): SellerScorecard;
31
+ /** Sum of verified paid amounts since `since` within the filter scope; a paid-but-failed deal still spent money. */
32
+ export declare function spentSince(recalled: readonly RecalledSettlementMemory[], filter: SettlementMemoryScope, since: number): bigint;
@@ -0,0 +1,76 @@
1
+ export const DEFAULT_HALF_LIFE_MS = 7 * 86_400_000;
2
+ const TRUSTED = new Set(["owner_policy", "verified_outcome"]);
3
+ /** True when every field the filter names matches the memory scope. */
4
+ export function scopeWithin(scope, filter) {
5
+ return Object.entries(filter).every(([key, value]) => value === undefined || scope[key] === value);
6
+ }
7
+ /**
8
+ * Trusted deal-level outcome memories for the filter, deduplicated by evidence (one deal may be recalled twice).
9
+ * Only completed deals and verified seller faults carry `signal.rail`; stage events (agreement, settlement,
10
+ * delivery) stay as evidence but are not counted as additional deals.
11
+ */
12
+ export function trustedOutcomes(recalled, filter) {
13
+ const seen = new Set(), result = [];
14
+ for (const item of recalled) {
15
+ const memory = item.memory;
16
+ if (!memory ||
17
+ memory.kind !== "outcome" ||
18
+ memory.signal.rail === undefined ||
19
+ !TRUSTED.has(memory.provenance) ||
20
+ !memory.signal.outcome ||
21
+ !scopeWithin(memory.scope, filter))
22
+ continue;
23
+ const key = memory.evidenceHash ?? item.blobId;
24
+ if (seen.has(key))
25
+ continue;
26
+ seen.add(key);
27
+ result.push({ blobId: item.blobId, memory });
28
+ }
29
+ return result.sort((a, b) => a.memory.createdAt - b.memory.createdAt ||
30
+ a.blobId.localeCompare(b.blobId));
31
+ }
32
+ export function buildScorecard(recalled, filter, now, halfLifeMs = DEFAULT_HALF_LIFE_MS) {
33
+ const outcomes = trustedOutcomes(recalled, filter);
34
+ let alpha = 1, beta = 1, successes = 0, failures = 0, unresolved = 0;
35
+ const latencies = [];
36
+ for (const { memory } of outcomes) {
37
+ const weight = Math.pow(0.5, Math.max(0, now - memory.createdAt) / halfLifeMs);
38
+ if (memory.signal.outcome === "succeeded") {
39
+ successes++;
40
+ alpha += weight;
41
+ }
42
+ else if (memory.signal.outcome === "failed") {
43
+ failures++;
44
+ beta += weight;
45
+ }
46
+ else {
47
+ unresolved++;
48
+ beta += weight / 2;
49
+ }
50
+ if (memory.signal.latencyMs !== undefined)
51
+ latencies.push(memory.signal.latencyMs);
52
+ }
53
+ latencies.sort((a, b) => a - b);
54
+ const last = outcomes.at(-1)?.memory;
55
+ return {
56
+ counterparty: filter.counterparty,
57
+ observations: outcomes.length,
58
+ successes,
59
+ failures,
60
+ unresolved,
61
+ reliability: alpha / (alpha + beta),
62
+ ...(last
63
+ ? { lastOutcome: last.signal.outcome, lastOutcomeAt: last.createdAt }
64
+ : {}),
65
+ ...(latencies.length
66
+ ? { medianLatencyMs: latencies[Math.floor((latencies.length - 1) / 2)] }
67
+ : {}),
68
+ citedBlobIds: outcomes.map((item) => item.blobId),
69
+ };
70
+ }
71
+ /** Sum of verified paid amounts since `since` within the filter scope; a paid-but-failed deal still spent money. */
72
+ export function spentSince(recalled, filter, since) {
73
+ return trustedOutcomes(recalled, filter)
74
+ .filter(({ memory }) => memory.signal.amount !== undefined && memory.createdAt >= since)
75
+ .reduce((total, { memory }) => total + BigInt(memory.signal.amount), 0n);
76
+ }
package/dist/memwal.d.ts CHANGED
@@ -1,5 +1,17 @@
1
1
  import { MemoryFact } from "./memory.js";
2
2
  import { type SettlementMemoryV1 } from "./settlement.js";
3
+ export declare const SIGNED_MEMORY_FORMAT: "supercuts.signed-memory.v1";
4
+ /** Sui address that signs this owner's trusted memories; derived from the owner-only memory key. */
5
+ export declare function memorySignerAddress(secret: string): string;
6
+ /** Plain-language line placed before the JSON so embeddings rank the record near scope queries. Not signed. */
7
+ export declare function describeMemory(memory: SettlementMemoryV1): string;
8
+ /** Wraps a trusted memory in a signed envelope so its authority survives a move to another machine. */
9
+ export declare function sealMemoryText(memory: SettlementMemoryV1, secret: string): string;
10
+ /** Opens recalled memory text. `signedBy` is set only when the envelope signature verifies. */
11
+ export declare function openMemoryText(text: string): {
12
+ memory: unknown;
13
+ signedBy?: string;
14
+ };
3
15
  import { RoleStore } from "./store.js";
4
16
  export interface ManualMemoryConfig {
5
17
  key: string;
@@ -15,10 +27,26 @@ export interface ManualMemoryConfig {
15
27
  walrusAggregatorUrl: string;
16
28
  walrusPublisherUrl: string;
17
29
  }
30
+ /** Public Walrus Mainnet Memory and local-adapter values; owners only need to supply their own account secrets. */
31
+ export declare const MEMORY_MAINNET_DEFAULTS: {
32
+ readonly serverUrl: "https://relayer.memory.walrus.xyz";
33
+ readonly packageId: "0xe7c16fbea0560e7057e2bf7422feaa4fb313749fc69c9e9092fac7a33b81d7f5";
34
+ readonly registryId: "0x8bf82c9e09e36b8d1c38298f68b7cb68e7b8762887e7592add9986d5e9cf199f";
35
+ readonly embeddingApiKey: "local-adapter";
36
+ readonly embeddingApiBase: "http://127.0.0.1:8787/v1";
37
+ readonly embeddingModel: "embeddinggemma-1536";
38
+ readonly walrusAggregatorUrl: "https://aggregator.walrus-mainnet.walrus.space";
39
+ readonly walrusPublisherUrl: "https://publisher.walrus-testnet.walrus.space";
40
+ };
41
+ /** Completes an owner memory config: `key`, `accountId`, and `suiPrivateKey` are required; the rest default. */
42
+ export declare function withMemoryDefaults(raw: Partial<ManualMemoryConfig>, role: string): ManualMemoryConfig;
18
43
  /** Client-managed memory facade; plaintext never crosses the relayer boundary. */
19
44
  export declare class ManualMemory {
20
45
  private readonly store;
21
46
  private readonly client;
47
+ private readonly signingKey;
48
+ /** Address whose signature marks a recalled memory as this owner's own trusted record. */
49
+ readonly signerAddress: string | undefined;
22
50
  constructor(store: RoleStore, config: ManualMemoryConfig);
23
51
  remember(fact: MemoryFact | SettlementMemoryV1): Promise<string>;
24
52
  recall(query: string, limit?: number): Promise<Array<{