@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
package/dist/memwal.js CHANGED
@@ -1,13 +1,91 @@
1
+ import { createPrivateKey, createPublicKey, sign as edSign, verify as edVerify } from "node:crypto";
1
2
  import { MemWalManual } from "@mysten-incubation/memwal/manual";
3
+ import { decodeSuiPrivateKey } from "@mysten/sui/cryptography";
4
+ import { Ed25519PublicKey } from "@mysten/sui/keypairs/ed25519";
2
5
  import { MemWal } from "@mysten-incubation/memwal";
3
6
  import { validateMemoryFact } from "./memory.js";
4
- import { SETTLEMENT_MEMORY_VERSION, validateSettlementMemory } from "./settlement.js";
7
+ import { canonicalJson, SETTLEMENT_MEMORY_VERSION, validateSettlementMemory } from "./settlement.js";
8
+ export const SIGNED_MEMORY_FORMAT = "supercuts.signed-memory.v1";
9
+ const PKCS8_ED25519 = Buffer.from("302e020100300506032b657004220420", "hex"), SPKI_ED25519 = Buffer.from("302a300506032b6570032100", "hex");
10
+ const memoryMessage = (memory) => Buffer.from(`supercuts-memory-v1\n${canonicalJson(memory)}`);
11
+ function memorySeed(secret) {
12
+ const decoded = decodeSuiPrivateKey(secret);
13
+ if (decoded.scheme !== "ED25519")
14
+ throw new TypeError("memory signing requires an ED25519 key");
15
+ return Buffer.from(decoded.secretKey);
16
+ }
17
+ function memoryPublicKey(secret) {
18
+ return Buffer.from(createPublicKey(createPrivateKey({ key: Buffer.concat([PKCS8_ED25519, memorySeed(secret)]), format: "der", type: "pkcs8" })).export({ format: "der", type: "spki" })).subarray(SPKI_ED25519.length);
19
+ }
20
+ /** Sui address that signs this owner's trusted memories; derived from the owner-only memory key. */
21
+ export function memorySignerAddress(secret) { return new Ed25519PublicKey(memoryPublicKey(secret)).toSuiAddress(); }
22
+ /** Plain-language line placed before the JSON so embeddings rank the record near scope queries. Not signed. */
23
+ export function describeMemory(memory) {
24
+ const scope = [memory.scope.capabilityId, memory.scope.capabilityClass, memory.scope.counterparty ? `seller ${memory.scope.counterparty}` : undefined].filter(Boolean).join(" ");
25
+ const signal = memory.signal;
26
+ const fact = memory.kind === "outcome" ? `deal ${signal.outcome ?? "recorded"}${signal.rail ? ` via ${signal.rail}` : ""}${signal.amount ? ` for ${signal.amount} MIST` : ""}${signal.latencyMs !== undefined ? ` in ${Math.round(signal.latencyMs / 1000)}s` : ""}`
27
+ : memory.kind === "privacy_rule" ? (signal.requiresShielded ? "owner requires shielded settlement" : "owner allows public settlement")
28
+ : memory.kind === "budget_limit" ? `owner budget limit ${signal.maximumAmount} MIST${signal.periodMs ? ` per ${Math.round(signal.periodMs / 3_600_000)}h` : " per deal"}`
29
+ : `${memory.kind} ${signal.reliability ?? signal.quality ?? ""}`.trim();
30
+ return `Supercuts ${memory.provenance.replace("_", " ")}: ${fact}${scope ? ` for ${scope}` : ""}.`;
31
+ }
32
+ /** Wraps a trusted memory in a signed envelope so its authority survives a move to another machine. */
33
+ export function sealMemoryText(memory, secret) {
34
+ const key = createPrivateKey({ key: Buffer.concat([PKCS8_ED25519, memorySeed(secret)]), format: "der", type: "pkcs8" });
35
+ return JSON.stringify({ format: SIGNED_MEMORY_FORMAT, summary: describeMemory(memory), memory, publicKey: memoryPublicKey(secret).toString("base64"), signature: edSign(null, memoryMessage(memory), key).toString("base64") });
36
+ }
37
+ /** Opens recalled memory text. `signedBy` is set only when the envelope signature verifies. */
38
+ export function openMemoryText(text) {
39
+ const value = JSON.parse(text);
40
+ if (!value || typeof value !== "object" || value.format !== SIGNED_MEMORY_FORMAT)
41
+ return { memory: value };
42
+ try {
43
+ const raw = Buffer.from(String(value.publicKey), "base64");
44
+ if (raw.length !== 32)
45
+ return { memory: value.memory };
46
+ const key = createPublicKey({ key: Buffer.concat([SPKI_ED25519, raw]), format: "der", type: "spki" });
47
+ return edVerify(null, memoryMessage(value.memory), key, Buffer.from(String(value.signature), "base64")) ? { memory: value.memory, signedBy: new Ed25519PublicKey(raw).toSuiAddress() } : { memory: value.memory };
48
+ }
49
+ catch {
50
+ return { memory: value.memory };
51
+ }
52
+ }
53
+ /** Public Walrus Mainnet Memory and local-adapter values; owners only need to supply their own account secrets. */
54
+ export const MEMORY_MAINNET_DEFAULTS = {
55
+ serverUrl: "https://relayer.memory.walrus.xyz",
56
+ packageId: "0xe7c16fbea0560e7057e2bf7422feaa4fb313749fc69c9e9092fac7a33b81d7f5",
57
+ registryId: "0x8bf82c9e09e36b8d1c38298f68b7cb68e7b8762887e7592add9986d5e9cf199f",
58
+ embeddingApiKey: "local-adapter",
59
+ embeddingApiBase: "http://127.0.0.1:8787/v1",
60
+ embeddingModel: "embeddinggemma-1536",
61
+ walrusAggregatorUrl: "https://aggregator.walrus-mainnet.walrus.space",
62
+ walrusPublisherUrl: "https://publisher.walrus-testnet.walrus.space",
63
+ };
64
+ /** Completes an owner memory config: `key`, `accountId`, and `suiPrivateKey` are required; the rest default. */
65
+ export function withMemoryDefaults(raw, role) {
66
+ for (const field of ["key", "accountId", "suiPrivateKey"])
67
+ if (typeof raw[field] !== "string" || !raw[field])
68
+ throw new TypeError(`memory config requires ${field}`);
69
+ return { ...MEMORY_MAINNET_DEFAULTS, namespace: `supercuts:${role}`, ...Object.fromEntries(Object.entries(raw).filter(([, value]) => value !== undefined && value !== "")) };
70
+ }
5
71
  /** Client-managed memory facade; plaintext never crosses the relayer boundary. */
6
72
  export class ManualMemory {
7
73
  store;
8
74
  client;
75
+ signingKey;
76
+ /** Address whose signature marks a recalled memory as this owner's own trusted record. */
77
+ signerAddress;
9
78
  constructor(store, config) {
10
79
  this.store = store;
80
+ try {
81
+ this.signingKey = config.suiPrivateKey;
82
+ this.signerAddress = memorySignerAddress(config.suiPrivateKey);
83
+ }
84
+ catch {
85
+ this.signingKey = undefined;
86
+ this.signerAddress = undefined;
87
+ process.stderr.write("MEMORY_SIGNING_UNAVAILABLE: trusted memories will rely on local authority only\n");
88
+ }
11
89
  let embedding;
12
90
  try {
13
91
  embedding = new URL(config.embeddingApiBase);
@@ -24,7 +102,8 @@ export class ManualMemory {
24
102
  validateSettlementMemory(fact);
25
103
  else
26
104
  validateMemoryFact(fact);
27
- const text = JSON.stringify(fact);
105
+ const trusted = fact.version === SETTLEMENT_MEMORY_VERSION && ["owner_policy", "verified_outcome"].includes(fact.provenance);
106
+ const text = trusted && this.signingKey ? sealMemoryText(fact, this.signingKey) : JSON.stringify(fact);
28
107
  const result = await this.client.rememberManual(text);
29
108
  this.store.putSecret(`memory:${result.blob_id}`, Buffer.from(text));
30
109
  return result.blob_id;
@@ -19,10 +19,11 @@ export async function onboardingDoctor(home, sellerConfig) {
19
19
  try {
20
20
  if (app.profile.mock)
21
21
  fail("LIVE_PROFILE_REQUIRED", "onboarding doctor requires a live Testnet role home");
22
- const runtime = await app.execute("doctor", {});
23
- if (runtime.data.readyForDemo === false)
24
- fail("ONBOARDING_RUNTIME_NOT_READY", "live runtime doctor reports missing prerequisites");
25
22
  if (app.profile.role === "buyer") {
23
+ const runtime = await app.execute("doctor", {});
24
+ if (runtime.data.publicFixes?.length)
25
+ fail("ONBOARDING_RUNTIME_NOT_READY", `live runtime doctor reports missing prerequisites: ${runtime.data.publicFixes.join(" ")}`);
26
+ const shieldedProofsReady = runtime.data.shieldedProofs === "ready";
26
27
  const mandate = loadMandate(home);
27
28
  if (mandate.version !== "supercuts.mandate.v2")
28
29
  fail("DEAL_MANDATE_VERSION", "buyer needs a v2 standing mandate");
@@ -48,13 +49,14 @@ export async function onboardingDoctor(home, sellerConfig) {
48
49
  ]);
49
50
  const nativeSuiMist = String(balance.balance), shieldedMist = String(shielded.data.available ?? "0");
50
51
  const checks = [
51
- { id: "runtime", status: "ready", detail: "Pinned deployment, prover, RPC, and Walrus prerequisites verified." },
52
+ { id: "runtime", status: "ready", detail: "Pinned deployment, RPC, and Walrus prerequisites verified." },
53
+ { id: "shielded-proofs", status: shieldedProofsReady ? "ready" : "unavailable", detail: shieldedProofsReady ? "Pinned v2 proving artifacts verified." : String(runtime.data.shieldedFixes?.[0] ?? "Shielded proving artifacts are not installed.") },
52
54
  { id: "public-balance", status: BigInt(nativeSuiMist) > 0n ? "ready" : "action_required", detail: BigInt(nativeSuiMist) > 0n ? `${nativeSuiMist} MIST available.` : "Fund only this generated buyer address before a public payment." },
53
55
  { id: "shielded-balance", status: BigInt(shieldedMist) > 0n ? "ready" : "action_required", detail: BigInt(shieldedMist) > 0n ? `${shieldedMist} MIST available in the experimental pool.` : "Create a buyer-owned pool deposit before a shielded deal." },
54
56
  { id: "seller-approval", status: mandate.allowedCounterparties.length > 0 ? "ready" : "action_required", detail: mandate.allowedCounterparties.length > 0 ? `${mandate.allowedCounterparties.length} seller identity or identities approved.` : "Review and approve a signed seller capability locally." },
55
57
  ...(directoryListings !== undefined ? [{ id: "directory", status: directoryListings > 0 ? "ready" : "action_required", detail: `${directoryListings} live directory listing or listings.` }] : []),
56
58
  ];
57
- return { role: "buyer", healthy: true, readyForPublicPayment: BigInt(nativeSuiMist) > 0n && mandate.allowedCounterparties.length > 0, readyForShieldedPayment: BigInt(shieldedMist) > 0n && mandate.allowedCounterparties.length > 0, runtime: runtime.data, checks, approvedSellers: mandate.allowedCounterparties.length, nativeSuiMist, shieldedMist, funded: BigInt(nativeSuiMist) > 0n, ...(directoryListings !== undefined ? { directoryListings } : {}) };
59
+ return { role: "buyer", healthy: true, readyForPublicPayment: BigInt(nativeSuiMist) > 0n && mandate.allowedCounterparties.length > 0, readyForShieldedPayment: shieldedProofsReady && BigInt(shieldedMist) > 0n && mandate.allowedCounterparties.length > 0, runtime: runtime.data, checks, approvedSellers: mandate.allowedCounterparties.length, nativeSuiMist, shieldedMist, funded: BigInt(nativeSuiMist) > 0n, ...(directoryListings !== undefined ? { directoryListings } : {}) };
58
60
  }
59
61
  if (app.profile.role === "seller") {
60
62
  if (!sellerConfig)
@@ -68,15 +70,20 @@ export async function onboardingDoctor(home, sellerConfig) {
68
70
  const served = await json(new URL("/v2/catalog", config.origin));
69
71
  if (!Array.isArray(served.capabilities))
70
72
  fail("SELLER_CATALOG_INVALID", "seller endpoint has no valid catalog");
73
+ let proofsRequired = false;
71
74
  for (const item of config.capabilities) {
72
75
  const registrationPath = resolve(item.registrationFile);
73
76
  if ((statSync(registrationPath).mode & 0o077) !== 0)
74
77
  fail("SELLER_CONFIG_PERMISSIONS", "registration file must be owner-only");
75
78
  const registration = JSON.parse(readFileSync(registrationPath, "utf8"));
79
+ proofsRequired ||= registration.manifest.supportedRails.includes("shielded");
76
80
  await verifySignedCapability(registration);
77
81
  if (!served.capabilities.some((entry) => JSON.stringify(entry) === JSON.stringify(registration)))
78
82
  fail("SELLER_CATALOG_INVALID", "seller endpoint does not serve its configured registration");
79
83
  }
84
+ const runtime = await app.execute("doctor", { "proofs-required": proofsRequired });
85
+ if (runtime.data.readyForDemo === false)
86
+ fail("ONBOARDING_RUNTIME_NOT_READY", `live runtime doctor reports missing prerequisites: ${(runtime.data.fixes ?? []).join(" ")}`);
80
87
  return { role: "seller", healthy: true, runtime: runtime.data, capabilities: config.capabilities.length, origin: config.origin };
81
88
  }
82
89
  return fail("ROLE_FORBIDDEN", "onboarding doctor supports buyer and seller homes");
package/dist/phase3.d.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  import { RoleStore } from "./store.js";
2
2
  import { type LiveRuntimeProfile } from "./live-runtime.js";
3
+ import { type CapabilityManifest, type CapabilityRail } from "./capability.js";
4
+ import { type SellerScorecard } from "./memory-scorecard.js";
5
+ import { type MemoryInfluence, type PlanningMandate, type SettlementPlanV1 } from "./settlement.js";
3
6
  export type Role = "buyer" | "seller" | "auditor";
4
7
  export interface Profile {
5
8
  version: 1 | 2;
@@ -63,12 +66,52 @@ export declare class Phase3App {
63
66
  private storedPlan;
64
67
  private persistPlan;
65
68
  private recalledMockMemories;
69
+ /** Trusted provenance needs local authority or this owner's memory-key signature (portable across machines). */
66
70
  private authenticatedMemory;
67
71
  private registerMemoryAuthority;
68
72
  private planSettlement;
73
+ /** Recall settlement memories for several scoped queries; plaintext stays in this process. */
74
+ private recallSettlementMemories;
75
+ /**
76
+ * Verified outcomes this runtime wrote itself, including ones still queued for Walrus. They are authoritative
77
+ * for spend and reliability on this machine; semantic recall alone can miss or lag them.
78
+ */
79
+ private localVerifiedOutcomes;
80
+ /**
81
+ * Orders candidate sellers by what Walrus Memory has verified about them, instead of by price alone.
82
+ * Sellers outside the owner mandate are dropped first so memory can never widen authority.
83
+ */
84
+ rankDealCandidates(operationId: string, candidates: CapabilityManifest[], mandate: PlanningMandate, request?: {
85
+ budgetMaximum?: string;
86
+ requestedRail?: CapabilityRail;
87
+ privacyHint?: "any" | "shielded";
88
+ }): Promise<{
89
+ ordered: CapabilityManifest[];
90
+ scorecards: Array<SellerScorecard & {
91
+ capabilityId: string;
92
+ priceMinimum: string;
93
+ }>;
94
+ }>;
95
+ /** Stores the counterfactual (same mandate, no memory, every candidate) next to the binding deal plan. */
96
+ recordDealMemoryInfluence(dealOperationId: string, plan: SettlementPlanV1, mandate: PlanningMandate, request?: {
97
+ requestedRail?: CapabilityRail;
98
+ privacyHint?: "any" | "shielded";
99
+ }): MemoryInfluence | undefined;
100
+ memoryInfluence(id: string): MemoryInfluence | undefined;
101
+ /**
102
+ * Owner view of what Supercuts has learned: fresh scorecards recalled from Walrus Memory for every seller
103
+ * it has ranked, plus how often memory changed a payment decision. Counts and blob IDs only, never plaintext.
104
+ */
105
+ memoryInsights(): Promise<Record<string, unknown>>;
69
106
  private typedMemory;
70
107
  private rememberTyped;
71
108
  private enqueueVerifiedOutcome;
109
+ /** Queues a verified seller fault as a Walrus outcome memory so later discovery demotes that seller. */
110
+ recordVerifiedDealFailure(operationId: string, planHash: string, fault: {
111
+ stage: string;
112
+ code: string;
113
+ paymentDigest?: string;
114
+ }): void;
72
115
  recordVerifiedDealOutcome(operationId: string, planHash: string, paymentDigest: string, artifactSha256: string): {
73
116
  payloadHash: string;
74
117
  queued: true;
package/dist/phase3.js CHANGED
@@ -8,9 +8,10 @@ import { DealJournal } from "./deal.js";
8
8
  import { LiveRuntime } from "./live-runtime.js";
9
9
  import { V2ExitQueue } from "./v2-exits.js";
10
10
  import { validateCapabilityManifest } from "./capability.js";
11
- import { ManualMemory } from "./memwal.js";
11
+ import { ManualMemory, openMemoryText, withMemoryDefaults } from "./memwal.js";
12
12
  import { WorkflowMemory } from "./workflow-memory.js";
13
- import { canonicalJson, createSettlementPlan, settlementMandateHash, settlementPlanHash, validateSettlementMemory, validateSettlementPlan } from "./settlement.js";
13
+ import { buildScorecard } from "./memory-scorecard.js";
14
+ import { canonicalJson, createSettlementPlan, explainMemoryInfluence, isRelevantMemory, settlementMandateHash, settlementPlanHash, validateSettlementMemory, validateSettlementPlan } from "./settlement.js";
14
15
  const PACKAGE = "0xf887433ddfd1068f6df3020cd757939b99ceb418f0d8b9924092b01c7aee846e";
15
16
  const POOL = "0x873e37b4745316f21a2780893f5fc36f87da559b08923fb4dad6237cd2e95d98";
16
17
  const V2_PACKAGE = "0x32d104c1ab422d497df4ae3a6861258694949c86428f886ca338e07bd055cf76";
@@ -56,6 +57,7 @@ function matchesPlannedEndpoint(planned, actual, requestId) {
56
57
  return false;
57
58
  }
58
59
  }
60
+ const memoryQuery = (manifest) => `${manifest.capabilityId} ${manifest.capability} ${manifest.sellerId}`;
59
61
  function record(store, scope, id, value) {
60
62
  store.db.prepare("INSERT INTO app_records(scope,id,value,updated_at) VALUES(?,?,?,?) ON CONFLICT(scope,id) DO UPDATE SET value=excluded.value,updated_at=excluded.updated_at")
61
63
  .run(scope, id, json(value), Date.now());
@@ -118,11 +120,12 @@ export function initialize(home, args) {
118
120
  if (liveCredentials?.memory) {
119
121
  let memoryConfig;
120
122
  try {
121
- memoryConfig = JSON.parse(liveCredentials.memory.toString());
123
+ memoryConfig = withMemoryDefaults(JSON.parse(liveCredentials.memory.toString()), role);
122
124
  }
123
125
  catch (error) {
124
- return fail("LIVE_MEMORY_CONFIG_INVALID", "memory config is not valid JSON", error);
126
+ return fail("LIVE_MEMORY_CONFIG_INVALID", `memory config is invalid: ${error instanceof TypeError ? error.message : "not valid JSON"}`, error);
125
127
  }
128
+ liveCredentials.memory = Buffer.from(JSON.stringify(memoryConfig));
126
129
  let embedding;
127
130
  try {
128
131
  embedding = new URL(memoryConfig.embeddingApiBase);
@@ -403,9 +406,14 @@ export class Phase3App {
403
406
  return { blobId: String(item.blobId), distance: 0, legacy: true };
404
407
  });
405
408
  }
406
- authenticatedMemory(blobId, memory) {
409
+ /** Trusted provenance needs local authority or this owner's memory-key signature (portable across machines). */
410
+ authenticatedMemory(blobId, memory, signedByOwner = false) {
407
411
  if (memory.provenance !== "owner_policy" && memory.provenance !== "verified_outcome")
408
412
  return memory;
413
+ if (signedByOwner && (memory.provenance !== "verified_outcome" || memory.evidenceHash)) {
414
+ this.registerMemoryAuthority(blobId, memory);
415
+ return memory;
416
+ }
409
417
  const authority = this.store.db.prepare("SELECT fact_hash,provenance,evidence_hash FROM memory_authority WHERE blob_id=?").get(blobId);
410
418
  if (authority?.fact_hash === hash(canonicalJson(memory)) && authority.provenance === memory.provenance && (memory.provenance !== "verified_outcome" || authority.evidence_hash === memory.evidenceHash))
411
419
  return memory;
@@ -456,31 +464,126 @@ export class Phase3App {
456
464
  }
457
465
  return existing;
458
466
  }
459
- let recalled = this.recalledMockMemories();
460
- if (!this.profile.mock && this.store.getSecret("live:memory-config")) {
461
- const encoded = this.store.getSecret("live:memory-config");
462
- let config;
467
+ const manifests = [primary, ...alternatives], recalled = await this.recallSettlementMemories(manifests.map(memoryQuery));
468
+ const input = { operationId, mandate, manifests, ...(typeof args["requested-amount"] === "string" ? { requestedAmount: args["requested-amount"] } : {}), ...(args["privacy-hint"] === "shielded" ? { privacyHint: "shielded" } : {}), ...(args["requested-rail"] === "x402" || args["requested-rail"] === "shielded" ? { requestedRail: args["requested-rail"] } : {}), recalled, maxMemoryDistance: this.profile.policy.maxMemoryDistance ?? 0.35 };
469
+ const plan = this.persistPlan(createSettlementPlan(input));
470
+ if (!get(this.store, "memory-influence", plan.planHash))
471
+ record(this.store, "memory-influence", plan.planHash, explainMemoryInfluence(input, plan));
472
+ return plan;
473
+ }
474
+ /** Recall settlement memories for several scoped queries; plaintext stays in this process. */
475
+ async recallSettlementMemories(queries, limit = 20) {
476
+ const local = this.localVerifiedOutcomes(), merge = (recalled) => [...local, ...recalled.filter((item) => !local.some((other) => other.blobId === item.blobId))]; // the local authoritative copy wins
477
+ const encoded = this.profile.mock ? undefined : this.store.getSecret("live:memory-config");
478
+ if (!encoded)
479
+ return merge(this.recalledMockMemories());
480
+ let config;
481
+ try {
482
+ config = JSON.parse(encoded.toString());
483
+ }
484
+ catch (error) {
485
+ return fail("LIVE_MEMORY_CONFIG_INVALID", "encrypted memory config is invalid", error);
486
+ }
487
+ const manual = new ManualMemory(this.store, config), byBlob = new Map();
488
+ try {
489
+ for (const query of [...new Set(queries)])
490
+ for (const item of await manual.recall(query, limit)) {
491
+ const prior = byBlob.get(item.blobId);
492
+ if (prior && prior.distance <= item.distance)
493
+ continue;
494
+ let memory;
495
+ try {
496
+ const opened = openMemoryText(item.text);
497
+ validateSettlementMemory(opened.memory);
498
+ memory = this.authenticatedMemory(item.blobId, opened.memory, Boolean(opened.signedBy && opened.signedBy === manual.signerAddress));
499
+ }
500
+ catch { /* malformed or legacy memory remains advisory */ }
501
+ byBlob.set(item.blobId, { blobId: item.blobId, distance: item.distance, ...(memory ? { memory } : { legacy: true }) });
502
+ }
503
+ }
504
+ finally {
505
+ manual.destroy();
506
+ }
507
+ return merge([...byBlob.values()]);
508
+ }
509
+ /**
510
+ * Verified outcomes this runtime wrote itself, including ones still queued for Walrus. They are authoritative
511
+ * for spend and reliability on this machine; semantic recall alone can miss or lag them.
512
+ */
513
+ localVerifiedOutcomes() {
514
+ const rows = this.store.db.prepare("SELECT operation_id,blob_id FROM memory_jobs WHERE operation_id LIKE 'verified:deal:%' OR operation_id LIKE 'verified:deal-failure:%' ORDER BY created_at").all();
515
+ return rows.flatMap((row) => {
516
+ const payload = this.store.getSecret(`memory-outbox:${row.operation_id}`);
517
+ if (!payload)
518
+ return [];
463
519
  try {
464
- config = JSON.parse(encoded.toString());
520
+ const memory = JSON.parse(payload.toString());
521
+ validateSettlementMemory(memory, "verified_outcome");
522
+ return [{ blobId: row.blob_id ?? `local:${row.operation_id}`, distance: 0, memory }];
465
523
  }
466
- catch (error) {
467
- return fail("LIVE_MEMORY_CONFIG_INVALID", "encrypted memory config is invalid", error);
524
+ catch {
525
+ return [];
468
526
  }
469
- const manual = new ManualMemory(this.store, config);
527
+ });
528
+ }
529
+ /**
530
+ * Orders candidate sellers by what Walrus Memory has verified about them, instead of by price alone.
531
+ * Sellers outside the owner mandate are dropped first so memory can never widen authority.
532
+ */
533
+ async rankDealCandidates(operationId, candidates, mandate, request = {}) {
534
+ const permitted = candidates.filter((item) => mandate.allowedCounterparties.includes(item.sellerId) && (!mandate.allowedCapabilityIds || mandate.allowedCapabilityIds.includes(item.capabilityId)) && (!mandate.allowedCapabilityClasses || mandate.allowedCapabilityClasses.includes(item.capability)));
535
+ const allowed = permitted.length ? permitted : candidates;
536
+ const recalled = await this.recallSettlementMemories(allowed.map(memoryQuery)), now = Date.now(), maxDistance = this.profile.policy.maxMemoryDistance ?? 0.35;
537
+ const relevant = recalled.filter((item) => isRelevantMemory(item, maxDistance));
538
+ // Only rank sellers that the deal could actually plan and pay: budget, rail, privacy, and price bounds.
539
+ const payable = allowed.filter((manifest) => {
470
540
  try {
471
- recalled = (await manual.recall(`${primary.capabilityId} ${primary.capability} ${primary.sellerId}`, 20)).map((item) => { let memory; try {
472
- const value = JSON.parse(item.text);
473
- validateSettlementMemory(value);
474
- memory = this.authenticatedMemory(item.blobId, value);
475
- }
476
- catch { /* malformed or legacy memory remains advisory */ } return { blobId: item.blobId, distance: item.distance, ...(memory ? { memory } : { legacy: true }) }; });
541
+ const plan = createSettlementPlan({ operationId: `rank:${operationId}`, mandate, manifests: [manifest], requestedAmount: manifest.priceBand.minimum, recalled, maxMemoryDistance: maxDistance, now, ...(request.requestedRail ? { requestedRail: request.requestedRail } : {}), ...(request.privacyHint ? { privacyHint: request.privacyHint } : {}) });
542
+ if (request.budgetMaximum !== undefined && BigInt(plan.maximumAmount) > BigInt(request.budgetMaximum))
543
+ return false;
544
+ return request.requestedRail ? plan.rail === request.requestedRail : mandate.version !== "supercuts.mandate.v2" || !manifest.supportedRails.includes("shielded") || plan.rail === "shielded";
477
545
  }
478
- finally {
479
- manual.destroy();
546
+ catch {
547
+ return false;
480
548
  }
481
- }
482
- const plan = createSettlementPlan({ operationId, mandate, manifests: [primary, ...alternatives], ...(typeof args["requested-amount"] === "string" ? { requestedAmount: args["requested-amount"] } : {}), ...(args["privacy-hint"] === "shielded" ? { privacyHint: "shielded" } : {}), ...(args["requested-rail"] === "x402" || args["requested-rail"] === "shielded" ? { requestedRail: args["requested-rail"] } : {}), recalled, maxMemoryDistance: this.profile.policy.maxMemoryDistance ?? 0.35 });
483
- return this.persistPlan(plan);
549
+ });
550
+ const pool = payable.length ? payable : allowed;
551
+ const scored = pool.map((manifest) => ({ manifest, card: buildScorecard(relevant, { counterparty: manifest.sellerId, capabilityClass: manifest.capability }, now) }));
552
+ const review = (card) => Number(card.lastOutcome === "unresolved" || card.observations > 0 && card.reliability < 0.5);
553
+ scored.sort((a, b) => review(a.card) - review(b.card) || b.card.reliability - a.card.reliability || (BigInt(a.manifest.priceBand.minimum) < BigInt(b.manifest.priceBand.minimum) ? -1 : BigInt(a.manifest.priceBand.minimum) > BigInt(b.manifest.priceBand.minimum) ? 1 : 0) || a.manifest.capabilityId.localeCompare(b.manifest.capabilityId));
554
+ const result = { ordered: scored.map((item) => item.manifest), scorecards: scored.map((item) => ({ ...item.card, capabilityId: item.manifest.capabilityId, priceMinimum: item.manifest.priceBand.minimum })) };
555
+ record(this.store, "deal-ranking", operationId, { candidates: pool, scorecards: result.scorecards, chosen: result.ordered[0]?.capabilityId, createdAt: now });
556
+ return result;
557
+ }
558
+ /** Stores the counterfactual (same mandate, no memory, every candidate) next to the binding deal plan. */
559
+ recordDealMemoryInfluence(dealOperationId, plan, mandate, request = {}) {
560
+ const ranking = get(this.store, "deal-ranking", dealOperationId);
561
+ if (!ranking)
562
+ return undefined;
563
+ const existing = get(this.store, "memory-influence", `deal:${dealOperationId}`);
564
+ if (existing)
565
+ return existing;
566
+ const influence = explainMemoryInfluence({ operationId: plan.operationId, mandate, manifests: ranking.candidates, requestedAmount: plan.openingAmount, ...request }, plan);
567
+ const cited = [...new Set([...influence.citedBlobIds, ...ranking.scorecards.flatMap((item) => item.citedBlobIds)])].sort();
568
+ const value = { ...influence, citedBlobIds: cited };
569
+ record(this.store, "memory-influence", `deal:${dealOperationId}`, value);
570
+ return value;
571
+ }
572
+ memoryInfluence(id) { return get(this.store, "memory-influence", id); }
573
+ /**
574
+ * Owner view of what Supercuts has learned: fresh scorecards recalled from Walrus Memory for every seller
575
+ * it has ranked, plus how often memory changed a payment decision. Counts and blob IDs only, never plaintext.
576
+ */
577
+ async memoryInsights() {
578
+ const rankings = all(this.store, "deal-ranking");
579
+ const manifests = [...new Map(rankings.flatMap((item) => item.candidates).map((item) => [item.capabilityId, item])).values()];
580
+ const recalled = manifests.length ? await this.recallSettlementMemories(manifests.map(memoryQuery)) : [], now = Date.now(), maxDistance = this.profile.policy.maxMemoryDistance ?? 0.35;
581
+ const relevant = recalled.filter((item) => isRelevantMemory(item, maxDistance));
582
+ const sellers = manifests.map((manifest) => { const card = buildScorecard(relevant, { counterparty: manifest.sellerId, capabilityClass: manifest.capability }, now); return { capabilityId: manifest.capabilityId, capabilityClass: manifest.capability, priceMinimum: manifest.priceBand.minimum, ...card }; }).sort((a, b) => b.reliability - a.reliability || a.capabilityId.localeCompare(b.capabilityId));
583
+ const influences = this.store.db.prepare("SELECT value FROM app_records WHERE scope='memory-influence' AND id LIKE 'deal:%'").all().map((row) => JSON.parse(row.value));
584
+ const jobs = this.store.db.prepare("SELECT status, COUNT(*) AS count FROM memory_jobs GROUP BY status").all();
585
+ const indexed = this.store.db.prepare("SELECT COUNT(*) AS count FROM memory_inventory WHERE deleted=0").get().count;
586
+ return { version: "supercuts.memory-insights.v1", memories: { byStatus: Object.fromEntries(jobs.map((row) => [row.status, row.count])), indexed, recalled: recalled.length }, decisions: { deals: influences.length, changedSeller: influences.filter((item) => item.changedSeller).length, changedRail: influences.filter((item) => item.changedRail).length, changedRisk: influences.filter((item) => item.changedRisk).length, changedMaximum: influences.filter((item) => item.changedMaximum).length }, sellers };
484
587
  }
485
588
  typedMemory(args, provenance) {
486
589
  const encoded = required(args, "memory");
@@ -545,7 +648,7 @@ export class Phase3App {
545
648
  manual.destroy();
546
649
  }
547
650
  }
548
- enqueueVerifiedOutcome(event, operationId, evidence, planHash, fallback) {
651
+ enqueueVerifiedOutcome(event, operationId, evidence, planHash, fallback, telemetry = {}) {
549
652
  const plan = planHash ? this.storedPlan(planHash) : undefined, jobId = `verified:${event}:${operationId}`, evidenceHash = hash(canonicalJson(evidence));
550
653
  const prior = this.store.getSecret(`memory-outbox:${jobId}`);
551
654
  if (prior) {
@@ -555,7 +658,7 @@ export class Phase3App {
555
658
  fail("MEMORY_OPERATION_CONFLICT", "verified outcome ID identifies different evidence or scope");
556
659
  return;
557
660
  }
558
- const memory = { version: "supercuts.settlement-memory.v1", kind: "outcome", provenance: "verified_outcome", scope: { ...(plan ? { capabilityId: plan.capabilityId, capabilityClass: plan.capabilityClass, counterparty: plan.sellerId, network: plan.network, asset: plan.asset } : { ...(fallback?.counterparty ? { counterparty: fallback.counterparty } : {}), ...(fallback?.asset ? { asset: fallback.asset } : {}), network: "sui:testnet" }) }, createdAt: Date.now(), evidenceHash, signal: { outcome: String(evidence.status ?? "") === "unresolved" ? "unresolved" : String(evidence.status ?? "") === "failed" ? "failed" : "succeeded" } };
661
+ const memory = { version: "supercuts.settlement-memory.v1", kind: "outcome", provenance: "verified_outcome", scope: { ...(plan ? { capabilityId: plan.capabilityId, capabilityClass: plan.capabilityClass, counterparty: plan.sellerId, network: plan.network, asset: plan.asset } : { ...(fallback?.counterparty ? { counterparty: fallback.counterparty } : {}), ...(fallback?.asset ? { asset: fallback.asset } : {}), network: "sui:testnet" }) }, createdAt: Date.now(), evidenceHash, signal: { outcome: String(evidence.status ?? "") === "unresolved" ? "unresolved" : String(evidence.status ?? "") === "failed" ? "failed" : "succeeded", ...telemetry } };
559
662
  validateSettlementMemory(memory, "verified_outcome");
560
663
  const factHash = hash(canonicalJson(memory));
561
664
  if (this.profile.mock) {
@@ -567,11 +670,21 @@ export class Phase3App {
567
670
  this.store.putSecretOnce(`memory-outbox:${jobId}`, Buffer.from(canonicalJson(memory)));
568
671
  this.store.db.prepare("INSERT OR IGNORE INTO memory_jobs(operation_id,kind,fact_hash,blob_id,status,created_at) VALUES(?,?,?,NULL,'pending',?)").run(jobId, memory.kind, factHash, Date.now());
569
672
  }
673
+ /** Queues a verified seller fault as a Walrus outcome memory so later discovery demotes that seller. */
674
+ recordVerifiedDealFailure(operationId, planHash, fault) {
675
+ if (this.profile.mock || this.profile.role !== "buyer")
676
+ fail("DEAL_LIVE_REQUIRED", "verified deal failures require a live buyer role");
677
+ if (this.store.getSecret(`memory-outbox:verified:deal-failure:${operationId}`))
678
+ return;
679
+ const plan = this.storedPlan(planHash);
680
+ this.enqueueVerifiedOutcome("deal-failure", operationId, { status: "failed", stage: fault.stage, code: fault.code, ...(fault.paymentDigest ? { paymentDigest: fault.paymentDigest } : {}) }, planHash, undefined, { rail: plan.rail, ...(fault.paymentDigest ? { amount: plan.openingAmount } : {}) });
681
+ }
570
682
  recordVerifiedDealOutcome(operationId, planHash, paymentDigest, artifactSha256) {
571
683
  if (this.profile.mock || this.profile.role !== "buyer")
572
684
  fail("DEAL_LIVE_REQUIRED", "verified deal outcome requires a live buyer role");
573
685
  const jobId = `verified:deal:${operationId}`;
574
- this.enqueueVerifiedOutcome("deal", operationId, { status: "succeeded", paymentDigest, artifactSha256 }, planHash);
686
+ const plan = this.storedPlan(planHash), timings = this.store.db.prepare("SELECT COALESCE(SUM(duration_ms),0) AS total FROM deal_timings WHERE operation_id=? AND stage IN ('negotiation','payment','delivery')").get(operationId);
687
+ this.enqueueVerifiedOutcome("deal", operationId, { status: "succeeded", paymentDigest, artifactSha256 }, planHash, undefined, { amount: plan.openingAmount, rail: plan.rail, ...(Number.isSafeInteger(timings.total) && timings.total > 0 ? { latencyMs: timings.total } : {}) });
575
688
  const payload = this.store.getSecret(`memory-outbox:${jobId}`) ?? fail("DEAL_MEMORY_INVALID", "verified outcome payload is missing");
576
689
  return { payloadHash: hash(payload), queued: true };
577
690
  }
@@ -645,7 +758,7 @@ export class Phase3App {
645
758
  const query = [memory.scope.capabilityId, memory.scope.counterparty, memory.signal.outcome].filter(Boolean).join(" ");
646
759
  const recalled = await manual.recall(query, 100);
647
760
  blobId = recalled.find((item) => { try {
648
- return canonicalJson(JSON.parse(item.text)) === canonicalJson(memory);
761
+ return canonicalJson(openMemoryText(item.text).memory) === canonicalJson(memory);
649
762
  }
650
763
  catch {
651
764
  return false;
@@ -760,7 +873,7 @@ export class Phase3App {
760
873
  return output(command, await this.liveMemory(command, args));
761
874
  const runtime = await LiveRuntime.create(this.profile, this.store);
762
875
  if (command === "doctor")
763
- return output(command, await runtime.doctor());
876
+ return output(command, await runtime.doctor(args["proofs-required"] !== false));
764
877
  if (command === "status")
765
878
  return output(command, { role: this.profile.role, network: this.profile.network, mock: false, runtime: "testnet-live" });
766
879
  if (command === "capability.publish")
@@ -945,9 +1058,9 @@ export class Phase3App {
945
1058
  if (command === "memory.recall") {
946
1059
  const recalled = await manual.recall(required(args, "query"));
947
1060
  const citations = recalled.filter((item) => item.distance <= (this.profile.policy.maxMemoryDistance ?? 0.35)).map((item) => { try {
948
- const memory = JSON.parse(item.text);
1061
+ const opened = openMemoryText(item.text), memory = opened.memory;
949
1062
  validateSettlementMemory(memory);
950
- const trusted = this.authenticatedMemory(item.blobId, memory);
1063
+ const trusted = this.authenticatedMemory(item.blobId, memory, Boolean(opened.signedBy && opened.signedBy === manual.signerAddress));
951
1064
  return { blobId: item.blobId, kind: trusted.kind, provenance: trusted.provenance, distance: item.distance };
952
1065
  }
953
1066
  catch {
@@ -1143,7 +1256,7 @@ export class Phase3App {
1143
1256
  export function output(command, data) { return { schemaVersion: "supercuts.v1", ok: true, command, data }; }
1144
1257
  export function errorOutput(error) {
1145
1258
  const known = error instanceof SupercutsError ? error : new SupercutsError("INTERNAL_ERROR", error instanceof Error ? error.message : "unknown error");
1146
- const retryable = ["BROADCAST_UNRESOLVED", "DELIVERY_IN_PROGRESS", "DELIVERY_PENDING", "ONBOARDING_SERVICE_UNAVAILABLE", "SELLER_DIRECTORY_UNAVAILABLE", "WALRUS_WRITE_FAILED", "WALRUS_READ_FAILED", "X402_FACILITATOR_UNAVAILABLE"].includes(known.code);
1259
+ const retryable = ["BROADCAST_UNRESOLVED", "DELIVERY_IN_PROGRESS", "DELIVERY_PENDING", "DEAL_SELLER_UNAVAILABLE", "ONBOARDING_SERVICE_UNAVAILABLE", "SELLER_DIRECTORY_UNAVAILABLE", "WALRUS_WRITE_FAILED", "WALRUS_READ_FAILED", "X402_FACILITATOR_UNAVAILABLE"].includes(known.code);
1147
1260
  const sameOperation = retryable || ["OPERATION_ID_CONFLICT", "DEAL_OPERATION_CONFLICT"].includes(known.code);
1148
1261
  return { schemaVersion: "supercuts.v1", ok: false, error: { code: known.code, message: known.message, recovery: recovery(known.code), retryable, retryWithSameOperationId: sameOperation } };
1149
1262
  }
@@ -1164,6 +1277,8 @@ function recovery(code) {
1164
1277
  return "reconcile or resume with the same operation ID; never create a replacement payment";
1165
1278
  if (code === "DELIVERY_PENDING")
1166
1279
  return "resume the same root operation ID; completed selection, payment, and agreement stages will not run again";
1280
+ if (code === "DEAL_SELLER_UNAVAILABLE")
1281
+ return "check the seller /health endpoint, then resume the same root operation ID";
1167
1282
  if (code === "X402_FACILITATOR_UNAVAILABLE")
1168
1283
  return "the signed transaction remains journaled; resume the same operation ID after facilitator health recovers";
1169
1284
  if (code === "OPERATION_ID_CONFLICT" || code === "DEAL_OPERATION_CONFLICT")
@@ -4,5 +4,5 @@ export interface RoleHomeLease {
4
4
  release(): void;
5
5
  }
6
6
  /** Best-effort process lease. SQLite still owns data integrity; this prevents
7
- * multiple MCP event/memory workers from operating one role home at once. */
7
+ * multiple long-running role workers from operating one role home at once. */
8
8
  export declare function acquireRoleHomeLease(home: string): RoleHomeLease;
package/dist/role-lock.js CHANGED
@@ -13,7 +13,7 @@ function active(pid) {
13
13
  }
14
14
  }
15
15
  /** Best-effort process lease. SQLite still owns data integrity; this prevents
16
- * multiple MCP event/memory workers from operating one role home at once. */
16
+ * multiple long-running role workers from operating one role home at once. */
17
17
  export function acquireRoleHomeLease(home) {
18
18
  const path = join(home, ".mcp.lock"), token = randomUUID();
19
19
  for (let attempt = 0; attempt < 2; attempt++) {
package/dist/seam.js CHANGED
@@ -112,6 +112,7 @@ process.stdin.on("end", async () => {
112
112
  catch (error) {
113
113
  const shaped = errorOutput(error);
114
114
  const code = String(shaped.error.code ?? "INTERNAL_ERROR");
115
- return write(envelope(request.requestId, { ok: false, error: { ...shaped.error, retryable: ["DELIVERY_IN_PROGRESS", "MEMORY_JOB_PENDING", "TATUM_TIMEOUT"].includes(code) } }), 1);
115
+ const retryable = ["DELIVERY_IN_PROGRESS", "MEMORY_JOB_PENDING", "TATUM_TIMEOUT", "X402_FACILITATOR_UNAVAILABLE"].includes(code);
116
+ return write(envelope(request.requestId, { ok: false, error: { ...shaped.error, retryable, ...(retryable ? { recovery: "transient: retry or resume later with the same operation ID; never start a second payment", retryWithSameOperationId: true } : {}) } }), 1);
116
117
  }
117
118
  });
@@ -35,8 +35,8 @@ try {
35
35
  if (item.data.reviewStatus !== "approved")
36
36
  fail("ARTIFACT_REVIEW_REQUIRED", `${item.path} must be owner-reviewed before live provisioning`);
37
37
  for (const brief of [briefA, briefB]) {
38
- if (typeof brief.data.title !== "string" || typeof brief.data.body !== "string" || !Array.isArray(brief.data.citations) || brief.data.citations.length < 2 || typeof brief.data.preview !== "object")
39
- fail("BRIEF_INVALID", "brief reports need title, body, two citations, and preview metadata");
38
+ if (typeof brief.data.title !== "string" || typeof brief.data.body !== "string" || !Array.isArray(brief.data.citations) || brief.data.citations.length < 2 || brief.data.citations.some((value) => typeof value !== "string" || !/^https:\/\//.test(value)) || new Set(brief.data.citations).size !== brief.data.citations.length || typeof brief.data.preview !== "object")
39
+ fail("BRIEF_INVALID", "brief reports need title, body, unique HTTPS citations, and preview metadata");
40
40
  for (const key of ["relevance", "sourceQuality"])
41
41
  if (typeof brief.data.preview[key] !== "number" || brief.data.preview[key] < 0 || brief.data.preview[key] > 1)
42
42
  fail("BRIEF_INVALID", `preview ${key} must be in [0,1]`);
@@ -44,8 +44,8 @@ try {
44
44
  fail("BRIEF_INVALID", "preview freshnessDays is invalid");
45
45
  }
46
46
  for (const analysis of [analysisA, analysisB])
47
- if (typeof analysis.data.title !== "string" || typeof analysis.data.body !== "string")
48
- fail("ANALYSIS_INVALID", "analysis reports need a title and body");
47
+ if (typeof analysis.data.title !== "string" || typeof analysis.data.body !== "string" || analysis.data.citations !== undefined && (!Array.isArray(analysis.data.citations) || analysis.data.citations.some((value) => typeof value !== "string" || !/^https:\/\//.test(value))))
48
+ fail("ANALYSIS_INVALID", "analysis reports need a title, body, and HTTPS citations when provided");
49
49
  const expiresAt = Date.now() + 7 * 24 * 60 * 60 * 1000;
50
50
  const capability = (id, sellerId, description, rails, amount) => createCapabilityManifest({ capabilityId: id, sellerId, capability: id === "competitor-analysis" ? "sui.competitor-analysis" : "sui.market-brief", description, inputSchema: { type: "object", properties: { topic: { type: "string" } } }, outputSchema: { type: "object", properties: { title: { type: "string" }, body: { type: "string" }, citations: { type: "array" } } }, supportedRails: rails, priceBand: { minimum: amount, maximum: amount }, asset: "0x2::sui::SUI", endpoint: new URL(`/resource/${id}`, origin).toString(), expiresAt });
51
51
  const publicA = run.addresses.publicSeller ?? fail("SELLER_CONFIG_INVALID", "first brief seller is missing"), publicB = run.addresses.secondPublicSeller ?? fail("SELLER_CONFIG_INVALID", "second brief seller is missing"), analysisSeller = run.addresses.confidentialSeller ?? fail("SELLER_CONFIG_INVALID", "analysis seller is missing");
@@ -3,6 +3,7 @@ import { readFileSync, statSync } from "node:fs";
3
3
  import { resolve } from "node:path";
4
4
  import { verifySignedCapability } from "./capability.js";
5
5
  import { fail } from "./errors.js";
6
+ import { errorOutput } from "./phase3.js";
6
7
  async function main() {
7
8
  const [command, file, directory] = process.argv.slice(2);
8
9
  if (command !== "publish" || !file || !directory)
@@ -21,4 +22,4 @@ async function main() {
21
22
  fail("SELLER_DIRECTORY_UNAVAILABLE", String(result.message ?? `directory returned HTTP ${response.status}`));
22
23
  process.stdout.write(`${JSON.stringify(result)}\n`);
23
24
  }
24
- main().catch((error) => { process.stderr.write(`${error instanceof Error ? error.message : "directory publication failed"}\n`); process.exitCode = 1; });
25
+ main().catch((error) => { process.stdout.write(`${JSON.stringify(errorOutput(error))}\n`); process.exitCode = 1; });
@@ -5,6 +5,7 @@ import { pathToFileURL } from "node:url";
5
5
  import { fail } from "./errors.js";
6
6
  import { Phase3App, errorOutput } from "./phase3.js";
7
7
  import { LiveSellerTransport } from "./live-seller-transport.js";
8
+ import { acquireRoleHomeLease } from "./role-lock.js";
8
9
  async function main() {
9
10
  const home = process.argv[2], configPath = process.argv[3];
10
11
  if (!home || !configPath)
@@ -12,8 +13,15 @@ async function main() {
12
13
  if ((statSync(configPath).mode & 0o077) !== 0)
13
14
  fail("SELLER_CONFIG_PERMISSIONS", "seller config must be owner-only");
14
15
  const config = JSON.parse(readFileSync(configPath, "utf8"));
16
+ if (config.facilitatorUrlEnv)
17
+ config.facilitatorUrl = process.env[config.facilitatorUrlEnv] ?? fail("SELLER_FACILITATOR_REQUIRED", `environment variable ${config.facilitatorUrlEnv} is required`);
18
+ if (config.facilitatorKeyEnv)
19
+ config.facilitatorKey = process.env[config.facilitatorKeyEnv] ?? fail("SELLER_FACILITATOR_REQUIRED", `environment variable ${config.facilitatorKeyEnv} is required`);
15
20
  if (!Array.isArray(config.capabilities) || !config.capabilities.length)
16
21
  fail("SELLER_CONFIG_INVALID", "seller config must name registered capabilities and handlers");
22
+ const lease = acquireRoleHomeLease(resolve(home));
23
+ if (!lease.acquired)
24
+ fail("ROLE_HOME_BUSY", `seller home is already in use${lease.ownerPid ? ` by process ${lease.ownerPid}` : ""}`);
17
25
  const app = new Phase3App(home), transport = new LiveSellerTransport(app, config);
18
26
  try {
19
27
  for (const item of config.capabilities) {
@@ -41,11 +49,12 @@ async function main() {
41
49
  fail("SELLER_CONFIG_INVALID", "seller listenPort must be a valid TCP port");
42
50
  await new Promise((done) => server.listen(port, host, done));
43
51
  process.stdout.write(`${JSON.stringify({ ok: true, service: "supercuts-seller-live", origin: config.origin, capabilities: transport.runtime.catalog().map((item) => item.manifest.capabilityId) })}\n`);
44
- const stop = () => server.close(() => { app.close(); process.exit(0); });
52
+ const stop = () => server.close(() => { lease.release(); app.close(); process.exit(0); });
45
53
  process.on("SIGINT", stop);
46
54
  process.on("SIGTERM", stop);
47
55
  }
48
56
  catch (error) {
57
+ lease.release();
49
58
  app.close();
50
59
  throw error;
51
60
  }