@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.
- package/README.md +28 -5
- package/config/phase1.testnet.json +20 -0
- package/config/proving-manifest.json +11 -0
- package/dist/buyer-setup.js +27 -13
- package/dist/cli.js +10 -2
- package/dist/dashboard.js +1 -1
- package/dist/deal-adapter.js +0 -2
- package/dist/deal-engine.d.ts +13 -0
- package/dist/deal-engine.js +64 -39
- package/dist/deal.d.ts +35 -0
- package/dist/deal.js +9 -3
- 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/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/live-deal-ports.d.ts +2 -1
- package/dist/live-deal-ports.js +31 -5
- package/dist/live-runtime.d.ts +1 -1
- package/dist/live-runtime.js +27 -11
- package/dist/live-seller-transport.js +5 -1
- package/dist/main-module.d.ts +5 -0
- package/dist/main-module.js +16 -0
- package/dist/mandate.js +1 -1
- package/dist/mcp-bootstrap.js +3 -3
- 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 +12 -5
- package/dist/phase3.d.ts +43 -0
- package/dist/phase3.js +147 -32
- package/dist/role-lock.d.ts +1 -1
- package/dist/role-lock.js +1 -1
- package/dist/seam.js +2 -1
- package/dist/seller-catalog-generate.js +4 -4
- package/dist/seller-directory-cli.js +2 -1
- package/dist/seller-live-cli.js +10 -1
- package/dist/seller-provision.js +15 -1
- package/dist/seller-research.d.ts +50 -0
- package/dist/seller-research.js +210 -0
- package/dist/seller-scaffold.d.ts +1 -0
- package/dist/seller-scaffold.js +35 -0
- package/dist/seller-service.d.ts +1 -1
- package/dist/seller-service.js +8 -20
- package/dist/seller-setup.d.ts +3 -0
- package/dist/seller-setup.js +109 -0
- 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 +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
|
|
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,
|
|
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 {
|
|
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",
|
|
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
|
-
|
|
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
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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
|
-
|
|
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
|
|
467
|
-
return
|
|
524
|
+
catch {
|
|
525
|
+
return [];
|
|
468
526
|
}
|
|
469
|
-
|
|
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
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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
|
-
|
|
479
|
-
|
|
546
|
+
catch {
|
|
547
|
+
return false;
|
|
480
548
|
}
|
|
481
|
-
}
|
|
482
|
-
const
|
|
483
|
-
|
|
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.
|
|
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(
|
|
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
|
|
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")
|
package/dist/role-lock.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
|
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.
|
|
25
|
+
main().catch((error) => { process.stdout.write(`${JSON.stringify(errorOutput(error))}\n`); process.exitCode = 1; });
|
package/dist/seller-live-cli.js
CHANGED
|
@@ -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
|
}
|