@supercuts-walrus/backend 0.1.1 → 0.1.2

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 CHANGED
@@ -77,7 +77,7 @@ bytes under that operation ID; retrying another payload with the same ID fails.
77
77
 
78
78
  ## Procurement demo
79
79
 
80
- For the Codex buyer with three remote Claude-backed sellers, Supabase-backed seller state, and the separate Render facilitator, follow [the remote demo guide](../docs/remote-codex-demo.md). The remote path is live-only and gives Codex a buyer role MCP server; the three seller panes use the safe `supercuts-seller-watch` event stream.
80
+ For the Codex buyer with three remote sellers, Claude-researched and owner-reviewed artifacts, Supabase-backed seller state, and the separate Render facilitator, follow [the remote demo guide](../docs/remote-codex-demo.md). The remote path is live-only and gives Codex a buyer role MCP server; the three seller panes use the safe `supercuts-seller-watch` event stream.
81
81
 
82
82
  `supercuts-demo` is the operator entry point:
83
83
 
@@ -116,9 +116,11 @@ For a Claude-agent recording, export `CLAUDE_API_KEY` or provide
116
116
  `live --claude-key-file <owner-only-file>`; a repository `.env` file is not
117
117
  loaded automatically. Validate the credential against Anthropic before starting
118
118
  a funded take.
119
- The live path calls Claude for bounded buyer and seller decisions and records
120
- only the model, action, and token counts. The backend still enforces capability,
121
- amount, rail, and agreement bindings. Validate that recording with
119
+ The recorded workflow can call Claude for bounded buyer decisions and records
120
+ only the model, action, and token counts. Hosted seller price and agreement
121
+ decisions are deterministic after backend validation; Claude web research runs
122
+ before provisioning so model availability cannot interrupt a paid request. The
123
+ backend still enforces capability, amount, rail, and agreement bindings. Validate that recording with
122
124
  `supercuts-demo check --require-claude`; an earlier funded run without these
123
125
  agent-decision events does not satisfy that gate.
124
126
 
@@ -185,7 +187,28 @@ signer, pool notes, Walrus and Walrus Memory configuration. A missing file
185
187
  returns `DEAL_SELLERS_NOT_CONFIGURED`. An owner-only `deal-ports.mjs` exporting
186
188
  `createDealPorts(app, mandate)` remains available as an advanced override.
187
189
 
188
- The seller service is `supercuts-seller-live <seller-home> <config.json>`.
190
+ Create a seller home with `supercuts onboard seller --home <seller-home>` or
191
+ its noninteractive `--config <owner-only-setup.json>` form. It generates a
192
+ fresh seller signer by default, writes a seller MCP stanza, and explicitly
193
+ reports that the seller should not receive faucet funds. The seller service is
194
+ `supercuts-seller-live <seller-home> <config.json>`.
195
+ Use `supercuts seller scaffold` to generate the offer spec, replaceable handler,
196
+ future expiry, environment-based facilitator config, and exact next commands.
197
+ The npm tarball includes pinned metadata for x402-only sellers; shielded sellers
198
+ still install the proof artifacts required for recovery and exits.
199
+
200
+ Research sellers can create a source-backed draft before packaging:
201
+
202
+ ```sh
203
+ CLAUDE_API_KEY=... supercuts seller research --kind market-brief \
204
+ --topic "Current Sui ecosystem market structure and risks" \
205
+ --output <owner-only-draft.json>
206
+ ```
207
+
208
+ The command uses Anthropic web search, rejects citations absent from the search
209
+ evidence, and writes a mode-0600 draft marked `draft-needs-owner-review`.
210
+ Optional `--allowed-domains` constrains sources. It never publishes or changes
211
+ live seller state; an owner must review and approve the immutable bytes first.
189
212
  Register each capability first with
190
213
  `supercuts-seller-register <seller-home> <spec.json> <registration.json>`.
191
214
  The spec contains `capabilityId`, `capability`, `description`, `inputSchema`,
@@ -0,0 +1,20 @@
1
+ {
2
+ "network": "testnet",
3
+ "sender": "0x66fdfe84ff521627c93910c93ec0bc26934ee1fef3a1a0a5ce14b7dc4567ec00",
4
+ "coinType": "0x2::sui::SUI",
5
+ "coinTypeHash": "be4507e04f555448ca05c0c9f8345c5ea9f3e40e8616a20f5d5c18b40ef399eb",
6
+ "packageId": "0x32d104c1ab422d497df4ae3a6861258694949c86428f886ca338e07bd055cf76",
7
+ "poolId": "0x8fd22140c906495bb5334a497dcdd9112479c638e21c5df4b4cfb66f633a106b",
8
+ "poolCreateTransaction": "BnzMqokgQVWwSkRYCCrMTyWwHN6HVdDKWwVDJXTZqC6b",
9
+ "deploymentCheckpoint": "401762744",
10
+ "deploymentDomain": "3082f331660eb38075e250a810a701bc4b0de2ca329eda70b2d76e3795517305",
11
+ "initialRoot": "2134e76ac5d21aab186c2be1dd8f84ee880a1e46eaf712f9d371b6df22191f3e",
12
+ "verifierVersion": 2,
13
+ "testOnly": true,
14
+ "artifactManifest": "config/proving-manifest.json",
15
+ "artifacts": {
16
+ "deposit": { "r1csSha256": "f36a1a18bf9d243e089e6ab14436e5a921d2b839d81bea95db319a4cdb80a197", "zkeySha256": "d398776a54628c390e8bf822007e47ed8fac00e92e8ea95bb17d20db517998dc", "verifyingKeySha256": "838404ff0ae3a9fc82da0856b68a11428cf3ffef335b4c6539fa1860d8e1ba26" },
17
+ "transfer": { "r1csSha256": "055ea2ec8f689c48e59f89fe06db6fb980dc575982214bfe60f675e8f08485c3", "zkeySha256": "6e7bcac98b8d2a78fd1811e2caadd83d0d6a301951f8cf27ac55a67006b001c2", "verifyingKeySha256": "8a6896f3f330769aa69e0dc1251a19fb69c4d6264b5729d0104df8d262e573ba" },
18
+ "withdraw": { "r1csSha256": "85cf1542ab2f2ece4bbd8757def48d526ba0951a832a2e0ebbf97ace2c611308", "zkeySha256": "fd303dd0dd53b9b752dac0de546dabdd03198a7a4defde5e8d6018bccd419c56", "verifyingKeySha256": "445c1982bea5e465fb0f5665023bb279637d820a9425f16a4c2f2ee7460e7e4a" }
19
+ }
20
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "version": "supercuts-pool-v2",
3
+ "testOnly": true,
4
+ "warning": "Per-circuit contributions are locally generated and unaudited; never use these keys with production assets.",
5
+ "powersOfTauSha256": "3f428d1a407e4704ef906960e000b03089e5e6ec29bf65b07bb5e3de005f4700",
6
+ "circuits": {
7
+ "deposit": { "r1csSha256": "f36a1a18bf9d243e089e6ab14436e5a921d2b839d81bea95db319a4cdb80a197", "zkeySha256": "d398776a54628c390e8bf822007e47ed8fac00e92e8ea95bb17d20db517998dc", "verificationKeySha256": "838404ff0ae3a9fc82da0856b68a11428cf3ffef335b4c6539fa1860d8e1ba26", "proofSha256": "9b7835852cd9ced7c02828e5c563191ac597f7175b04fa9bb01c66cc93a93e58", "publicInputsSha256": "23569e5994ddd08b555dcbcf12c735ec303e000f6e7e90c9b63667dec60b6aeb" },
8
+ "transfer": { "r1csSha256": "055ea2ec8f689c48e59f89fe06db6fb980dc575982214bfe60f675e8f08485c3", "zkeySha256": "6e7bcac98b8d2a78fd1811e2caadd83d0d6a301951f8cf27ac55a67006b001c2", "verificationKeySha256": "8a6896f3f330769aa69e0dc1251a19fb69c4d6264b5729d0104df8d262e573ba", "proofSha256": "ff9a902e91e7d7b64e278cc7e17c22ad3f031f350aacb43f610771adafbf3f35", "publicInputsSha256": "903861e7ee9f792a17194f96649736d4916f45605918acac78e5d40a5b8f6170" },
9
+ "withdraw": { "r1csSha256": "85cf1542ab2f2ece4bbd8757def48d526ba0951a832a2e0ebbf97ace2c611308", "zkeySha256": "fd303dd0dd53b9b752dac0de546dabdd03198a7a4defde5e8d6018bccd419c56", "verificationKeySha256": "445c1982bea5e465fb0f5665023bb279637d820a9425f16a4c2f2ee7460e7e4a", "proofSha256": "4746ee54c04389929aad48c0c5a0cf18985b9dd6bcd7a3b83aa717d8847973d3", "publicInputsSha256": "d18d5852ac3c636c7b081bc52d367c896c95bd5a89feacc91014afefe3dcff54" }
10
+ }
11
+ }
@@ -48,10 +48,10 @@ function validateConfig(config) {
48
48
  fail("SETUP_INVALID", "configure a directoryUrl or sellerOrigins");
49
49
  if (config.directoryUrl)
50
50
  origins([config.directoryUrl]);
51
- for (const key of ["deploymentConfig", "tatumKeyFile", "writeGrpc", "tatumGrpc", "walrusPublisher", "walrusAggregator", "memoryConfigFile"])
51
+ for (const key of ["deploymentConfig", "tatumKeyFile", "writeGrpc", "tatumGrpc", "walrusPublisher", "walrusAggregator"])
52
52
  if (typeof config[key] !== "string" || !config[key])
53
53
  fail("SETUP_INVALID", `${key} is required`);
54
- for (const path of [config.tatumKeyFile, config.memoryConfigFile, ...(config.suiKeyFile ? [config.suiKeyFile] : [])])
54
+ for (const path of [config.tatumKeyFile, ...(config.memoryConfigFile ? [config.memoryConfigFile] : []), ...(config.suiKeyFile ? [config.suiKeyFile] : [])])
55
55
  ownerFile(resolve(path));
56
56
  const deployment = JSON.parse(readFileSync(resolve(config.deploymentConfig), "utf8"));
57
57
  if (deployment.network !== "testnet" || deployment.verifierVersion !== 2)
@@ -94,7 +94,7 @@ export async function promptBuyerSetup(home) {
94
94
  deploymentConfig: await ask("Pinned Testnet deployment config path"), tatumKeyFile: await ask("Tatum key file path"),
95
95
  writeGrpc: await ask("Sui write gRPC URL"), tatumGrpc: await ask("Tatum read gRPC URL"),
96
96
  walrusPublisher: await ask("Walrus publisher URL"), walrusAggregator: await ask("Walrus aggregator URL"),
97
- memoryConfigFile: await ask("Owner-only Walrus Memory config path"),
97
+ memoryConfigFile: await ask("Owner-only Walrus Memory config path (blank to skip Memory)"),
98
98
  directoryUrl: await ask("Shared directory origin (blank for direct sellers)"),
99
99
  sellerOrigins: (await ask("Additional seller HTTPS origins, comma separated (or blank)")).split(",").map((item) => item.trim()).filter(Boolean),
100
100
  allowedCounterparties: (await ask("Initially approved seller Sui addresses, comma separated (or blank)")).split(",").map((item) => item.trim()).filter(Boolean),
@@ -118,7 +118,7 @@ function setupBuyerConfig(home, config) {
118
118
  if (!config.suiKeyFile)
119
119
  writeFileSync(keyPath, Ed25519Keypair.generate().getSecretKey(), { mode: 0o600, flag: "wx" });
120
120
  ownerFile(keyPath);
121
- initialize(home, { role: "buyer", "deployment-config": resolve(config.deploymentConfig), "sui-key-file": keyPath, "tatum-key-file": resolve(config.tatumKeyFile), "write-grpc": config.writeGrpc, "tatum-grpc": config.tatumGrpc, "walrus-publisher": config.walrusPublisher, "walrus-aggregator": config.walrusAggregator, "memory-config-file": resolve(config.memoryConfigFile), "allow-counterparty": (config.allowedCounterparties ?? []).join(","), "max-amount": config.perDealCeiling, ...(config.rapidsnark ? { rapidsnark: resolve(config.rapidsnark) } : {}) });
121
+ initialize(home, { role: "buyer", "deployment-config": resolve(config.deploymentConfig), "sui-key-file": keyPath, "tatum-key-file": resolve(config.tatumKeyFile), "write-grpc": config.writeGrpc, "tatum-grpc": config.tatumGrpc, "walrus-publisher": config.walrusPublisher, "walrus-aggregator": config.walrusAggregator, ...(config.memoryConfigFile ? { "memory-config-file": resolve(config.memoryConfigFile) } : {}), "allow-counterparty": (config.allowedCounterparties ?? []).join(","), "max-amount": config.perDealCeiling, ...(config.rapidsnark ? { rapidsnark: resolve(config.rapidsnark) } : {}) });
122
122
  writePolicy(home, config);
123
123
  const session = createBuyerSession(home);
124
124
  const decoded = decodeSuiPrivateKey(readFileSync(keyPath, "utf8").trim());
package/dist/cli.js CHANGED
@@ -4,19 +4,22 @@ import { addBuyerPrivacyRule, approveBuyerSeller, createBuyerSession, promptBuye
4
4
  import { onboardingDoctor } from "./onboarding-doctor.js";
5
5
  import { executeFunding, prepareFunding } from "./funding.js";
6
6
  import { packageHostedSeller } from "./seller-package.js";
7
+ import { promptSellerSetup, setupSeller } from "./seller-setup.js";
8
+ import { scaffoldSeller } from "./seller-scaffold.js";
9
+ import { generateSellerResearch } from "./seller-research.js";
7
10
  const words = process.argv.slice(2);
8
11
  let command = words.shift() ?? "";
9
12
  if (["wallet", "offer", "pay", "resource", "memory", "audit", "capability", "settlement", "setup", "policy", "onboarding", "onboard", "funding", "seller"].includes(command))
10
13
  command += `.${words.shift() ?? ""}`;
11
14
  try {
12
15
  if (!command || command === "help" || command === "--help" || command === "setup.") {
13
- process.stdout.write(`${JSON.stringify({ schemaVersion: "supercuts.help.v1", ok: true, commands: ["onboard buyer", "funding prepare", "funding execute", "seller package", "setup buyer", "setup renew", "setup session", "init", "doctor", "status", "wallet", "offer", "pay", "memory", "capability", "audit"], setup: "Run supercuts onboard buyer --home <private-directory> for guided live Testnet setup. Use seller package to validate and prepare an operator-assisted hosted seller bundle.", live: "Use funding prepare to calculate public gas/payment and shielded deposit requirements. Funding execute requires explicit confirmation and funds only the generated buyer address." })}\n`);
16
+ process.stdout.write(`${JSON.stringify({ schemaVersion: "supercuts.help.v1", ok: true, commands: ["onboard buyer", "onboard seller", "seller scaffold", "seller research", "seller package", "funding prepare", "funding execute", "setup buyer", "setup seller", "setup renew", "setup session", "init", "doctor", "status", "wallet", "offer", "pay", "memory", "capability", "audit"], setup: "Run supercuts onboard buyer or supercuts onboard seller with an isolated --home. Then use seller scaffold to generate a capability. Research sellers can use seller research to create a cited owner-review draft before packaging.", live: "Fund only the generated buyer address. A seller does not need faucet funds to publish or receive a capability payment." })}\n`);
14
17
  process.exit(0);
15
18
  }
16
19
  const args = parseArgs(words);
17
20
  const home = homePath(args);
18
21
  const fundingInput = { publicPayment: String(args["public-payment"] ?? "0"), shieldedPayment: String(args["shielded-payment"] ?? "0"), ...(typeof args["gas-reserve"] === "string" ? { gasReserve: args["gas-reserve"] } : {}), ...(typeof args["change-reserve"] === "string" ? { changeReserve: args["change-reserve"] } : {}) };
19
- const result = command === "init" ? initialize(home, args) : command === "setup.buyer" || command === "onboard.buyer" ? { schemaVersion: "supercuts.setup.v1", ok: true, command, data: typeof args.config === "string" ? setupBuyer(home, args.config) : await promptBuyerSetup(home) } : command === "setup.renew" ? { schemaVersion: "supercuts.setup.v1", ok: true, command, data: renewBuyer(home, String(args.config ?? "")) } : command === "setup.session" ? { schemaVersion: "supercuts.setup.v1", ok: true, command, data: createBuyerSession(home) } : command === "funding.prepare" ? { schemaVersion: "supercuts.funding.v1", ok: true, command, data: await prepareFunding(home, fundingInput) } : command === "funding.execute" ? { schemaVersion: "supercuts.funding.v1", ok: true, command, data: await executeFunding(home, { ...fundingInput, operationId: String(args["operation-id"] ?? ""), requestFaucet: args["request-faucet"] === true, confirmed: args.confirm === true }) } : command === "seller.package" ? { schemaVersion: "supercuts.seller-package.v1", ok: true, command, data: await packageHostedSeller({ home, spec: String(args.spec ?? ""), artifact: String(args.artifact ?? ""), metadata: String(args.metadata ?? ""), registration: String(args.registration ?? ""), bundle: String(args.bundle ?? ""), ...(typeof args["content-type"] === "string" ? { contentType: args["content-type"] } : {}) }) } : command === "policy.add" ? { schemaVersion: "supercuts.setup.v1", ok: true, command, data: await addBuyerPrivacyRule(home, String(args["capability-id"] ?? "")) } : command === "policy.approve-seller" ? { schemaVersion: "supercuts.setup.v1", ok: true, command, data: await approveBuyerSeller(home, String(args["capability-id"] ?? "")) } : command === "onboarding.doctor" ? { schemaVersion: "supercuts.onboarding.v1", ok: true, command, data: await onboardingDoctor(home, typeof args.config === "string" ? args.config : undefined) } : await (async () => { const app = new Phase3App(home); try {
22
+ const result = command === "init" ? initialize(home, args) : command === "setup.buyer" || command === "onboard.buyer" ? { schemaVersion: "supercuts.setup.v1", ok: true, command, data: typeof args.config === "string" ? setupBuyer(home, args.config) : await promptBuyerSetup(home) } : command === "setup.seller" || command === "onboard.seller" ? { schemaVersion: "supercuts.setup.v1", ok: true, command, data: typeof args.config === "string" ? setupSeller(home, args.config) : await promptSellerSetup(home) } : command === "seller.scaffold" ? { schemaVersion: "supercuts.seller-scaffold.v1", ok: true, command, data: scaffoldSeller({ ...args, home }) } : command === "seller.research" ? { schemaVersion: "supercuts.seller-research.v1", ok: true, command, data: await generateSellerResearch({ kind: String(args.kind ?? ""), topic: String(args.topic ?? ""), output: String(args.output ?? ""), ...(typeof args["context-file"] === "string" ? { contextFile: args["context-file"] } : {}), ...(typeof args["allowed-domains"] === "string" ? { allowedDomains: args["allowed-domains"] } : {}), ...(typeof args.model === "string" ? { model: args.model } : {}) }) } : command === "setup.renew" ? { schemaVersion: "supercuts.setup.v1", ok: true, command, data: renewBuyer(home, String(args.config ?? "")) } : command === "setup.session" ? { schemaVersion: "supercuts.setup.v1", ok: true, command, data: createBuyerSession(home) } : command === "funding.prepare" ? { schemaVersion: "supercuts.funding.v1", ok: true, command, data: await prepareFunding(home, fundingInput) } : command === "funding.execute" ? { schemaVersion: "supercuts.funding.v1", ok: true, command, data: await executeFunding(home, { ...fundingInput, operationId: String(args["operation-id"] ?? ""), requestFaucet: args["request-faucet"] === true, confirmed: args.confirm === true }) } : command === "seller.package" ? { schemaVersion: "supercuts.seller-package.v1", ok: true, command, data: await packageHostedSeller({ home, spec: String(args.spec ?? ""), artifact: String(args.artifact ?? ""), metadata: String(args.metadata ?? ""), registration: String(args.registration ?? ""), bundle: String(args.bundle ?? ""), ...(typeof args["content-type"] === "string" ? { contentType: args["content-type"] } : {}) }) } : command === "policy.add" ? { schemaVersion: "supercuts.setup.v1", ok: true, command, data: await addBuyerPrivacyRule(home, String(args["capability-id"] ?? "")) } : command === "policy.approve-seller" ? { schemaVersion: "supercuts.setup.v1", ok: true, command, data: await approveBuyerSeller(home, String(args["capability-id"] ?? "")) } : command === "onboarding.doctor" ? { schemaVersion: "supercuts.onboarding.v1", ok: true, command, data: await onboardingDoctor(home, typeof args.config === "string" ? args.config : undefined) } : await (async () => { const app = new Phase3App(home); try {
20
23
  return await app.execute(command, args);
21
24
  }
22
25
  finally {
@@ -16,8 +16,6 @@ export async function createConfiguredDealEngine(app) {
16
16
  fail("MANDATE_COMMAND_FORBIDDEN", "owner mandate does not allow composite deal execution");
17
17
  const path = join(app.home, "deal-ports.mjs");
18
18
  if (!existsSync(path)) {
19
- if (!app.store.getSecret("live:memory-config"))
20
- fail("LIVE_MEMORY_CONFIG_MISSING", "live composite deals require an owner-only Walrus Memory configuration");
21
19
  return new DealEngine(new DealJournal(app.store), new LiveDealPorts(app, mandate), mandate);
22
20
  }
23
21
  if ((statSync(path).mode & 0o077) !== 0)
@@ -179,10 +179,10 @@ export class DealEngine {
179
179
  if (existing?.blobId) {
180
180
  const indexed = await this.ports.inventory(existing.blobId);
181
181
  this.journal.recordOutcome(operationId, existing.payloadHash, existing.blobId, indexed);
182
- return indexed ? this.journal.advance(operationId, "outcome_pending", "complete") : this.journal.get(operationId);
182
+ return this.journal.advance(operationId, "outcome_pending", "complete");
183
183
  }
184
184
  if (existing)
185
- return view;
185
+ return this.journal.advance(operationId, "outcome_pending", "complete");
186
186
  const plan = this.journal.step(operationId, "planning") ?? fail("DEAL_PLAN_MISSING", "saved deal plan is missing");
187
187
  const payment = this.journal.step(operationId, "payment") ?? fail("DEAL_PAYMENT_UNVERIFIED", "saved deal payment is missing");
188
188
  const artifact = this.journal.step(operationId, "delivery") ?? fail("DEAL_DELIVERY_UNVERIFIED", "saved deal delivery is missing");
@@ -193,11 +193,11 @@ export class DealEngine {
193
193
  this.journal.recordTiming(operationId, "outcome_queue", Date.now() - started);
194
194
  if ("unresolved" in candidate) {
195
195
  this.journal.setOutcomeUnresolved(operationId);
196
- return this.journal.get(operationId);
196
+ return this.journal.advance(operationId, "outcome_pending", "complete");
197
197
  }
198
198
  if ("queued" in candidate) {
199
199
  this.journal.queueOutcome(operationId, candidate.payloadHash);
200
- return this.journal.get(operationId);
200
+ return this.journal.advance(operationId, "outcome_pending", "complete");
201
201
  }
202
202
  if (!candidate.blobId || !/^[0-9a-f]{64}$/.test(candidate.payloadHash))
203
203
  fail("DEAL_MEMORY_INVALID", "outcome memory is incomplete");
@@ -206,6 +206,6 @@ export class DealEngine {
206
206
  }
207
207
  const indexed = await this.ports.inventory(outcome.blobId);
208
208
  this.journal.recordOutcome(operationId, outcome.payloadHash, outcome.blobId, indexed);
209
- return indexed ? this.journal.advance(operationId, "outcome_pending", "complete") : this.journal.get(operationId);
209
+ return this.journal.advance(operationId, "outcome_pending", "complete");
210
210
  }
211
211
  }
package/dist/deal.js CHANGED
@@ -213,8 +213,8 @@ export class DealJournal {
213
213
  fail("DEAL_PAYMENT_UNVERIFIED", "confirmed payment evidence is required");
214
214
  if (next === "delivered" && !(this.store.db.prepare("SELECT 1 FROM deal_blobs WHERE operation_id=? AND kind='delivery' AND status='verified'").get(operationId)))
215
215
  fail("DEAL_DELIVERY_UNVERIFIED", "verified delivery evidence is required");
216
- if (next === "complete" && !(this.store.db.prepare("SELECT 1 FROM deal_memory_outbox WHERE operation_id=? AND event_kind='outcome' AND status='indexed'").get(operationId)))
217
- fail("DEAL_MEMORY_UNVERIFIED", "indexed outcome memory is required");
216
+ if (next === "complete" && (this.get(operationId).evidence.payment !== "finalized" || this.get(operationId).evidence.delivery !== "verified"))
217
+ fail("DEAL_DELIVERY_UNVERIFIED", "finalized payment and verified delivery are required before deal completion");
218
218
  this.store.db.prepare("UPDATE deal_operations SET state=?,rail=COALESCE(?,rail),plan_hash=COALESCE(?,plan_hash),updated_at=? WHERE operation_id=?")
219
219
  .run(next, result?.rail ?? null, result?.planHash ?? null, Date.now(), operationId);
220
220
  this.store.db.exec("COMMIT");
package/dist/index.d.ts CHANGED
@@ -15,6 +15,7 @@ export * from "./indexer.js";
15
15
  export * from "./memory.js";
16
16
  export * from "./settlement.js";
17
17
  export * from "./seller-runtime.js";
18
+ export * from "./seller-research.js";
18
19
  export * from "./seller-stdio.js";
19
20
  export * from "./live-index.js";
20
21
  export * from "./live-runtime.js";
package/dist/index.js CHANGED
@@ -15,6 +15,7 @@ export * from "./indexer.js";
15
15
  export * from "./memory.js";
16
16
  export * from "./settlement.js";
17
17
  export * from "./seller-runtime.js";
18
+ export * from "./seller-research.js";
18
19
  export * from "./seller-stdio.js";
19
20
  export * from "./live-index.js";
20
21
  export * from "./live-runtime.js";
@@ -64,7 +64,14 @@ export class LiveDealPorts {
64
64
  return Ed25519Keypair.fromSecretKey(decoded.secretKey);
65
65
  }
66
66
  async request(origin, path, body) {
67
- const response = await this.http(new URL(path, origin), { ...(body ? { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) } : {}), redirect: "error", signal: AbortSignal.timeout(90_000) });
67
+ const url = new URL(path, origin);
68
+ let response;
69
+ try {
70
+ response = await this.http(url, { ...(body ? { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) } : {}), redirect: "error", signal: AbortSignal.timeout(90_000) });
71
+ }
72
+ catch (error) {
73
+ return fail("DEAL_SELLER_UNAVAILABLE", `seller ${url.origin} is unreachable; check its health endpoint and network route`, error);
74
+ }
68
75
  let value;
69
76
  try {
70
77
  value = await response.json();
@@ -47,7 +47,7 @@ export declare class LiveRuntime {
47
47
  readonly prover: Groth16Prover;
48
48
  private constructor();
49
49
  static create(profile: LiveRuntimeProfile, store: RoleStore): Promise<LiveRuntime>;
50
- doctor(): Promise<Record<string, unknown>>;
50
+ doctor(proofsRequired?: boolean): Promise<Record<string, unknown>>;
51
51
  balance(): Promise<Record<string, unknown>>;
52
52
  private signed;
53
53
  private operationContext;
@@ -89,8 +89,8 @@ export class LiveRuntime {
89
89
  const engine = await ProtocolEngine.create({ deploymentDomain: profile.live.deploymentDomain, poolId: profile.poolId, coinTypeHash: profile.live.coinTypeHash, verifierVersion: profile.verifierVersion, initialRoot: profile.live.initialRoot });
90
90
  return new LiveRuntime(profile, store, new TatumLedger(profile.live.tatumGrpcEndpoint, tatumKey), Ed25519Keypair.fromSecretKey(decoded.secretKey), engine);
91
91
  }
92
- async doctor() {
93
- const artifacts = this.prover.doctor();
92
+ async doctor(proofsRequired = true) {
93
+ const artifacts = proofsRequired ? this.prover.doctor() : { rapidSnark: false, artifacts: {} };
94
94
  const [service, suiObject] = await Promise.all([this.tatum.getServiceInfo(), this.client.getObject({ objectId: this.profile.poolId, include: { json: true } })]);
95
95
  if (!suiObject.object)
96
96
  fail("POOL_OBJECT_MISSING", "pinned Testnet pool object is unavailable; check the Sui write RPC and deployment config");
@@ -99,7 +99,7 @@ export class LiveRuntime {
99
99
  const babySeed = this.store.getSecret("identity:babyjub-seed") ?? fail("IDENTITY_MISSING", "BabyJub identity is missing");
100
100
  const identity = this.engine.identity(babySeed);
101
101
  const fixes = [];
102
- if (!artifacts.rapidSnark)
102
+ if (proofsRequired && !artifacts.rapidSnark)
103
103
  fixes.push("Set an executable RapidSnark path in the role profile for v2 proofs.");
104
104
  if (!this.profile.live.walrusPublisherUrl)
105
105
  fixes.push("Configure the Walrus publisher URL for capability and transcript writes.");
@@ -117,9 +117,7 @@ export class LiveRuntime {
117
117
  fixes.push(`Walrus ${name} is unreachable; check the configured URL and network access.`);
118
118
  }
119
119
  }
120
- if (this.profile.role === "buyer" && !this.store.getSecret("live:memory-config"))
121
- fixes.push("Configure an owner-only Walrus Memory file using the local 1536-dimensional embedding adapter.");
122
- return { role: this.profile.role, network: "testnet", packageId: this.profile.packageId, poolId: this.profile.poolId, asset: this.profile.coinType, verifierVersion: this.profile.verifierVersion, keyPermissions: "owner-only", rpc: this.profile.live.writeGrpcEndpoint, healthy: true, readyForDemo: fixes.length === 0, fixes, runtime: "testnet-live", signer: this.signer.toSuiAddress(), babyJubPublicKey: identity.publicKey.map(String), x25519PublicKey: this.store.getSecret("identity:x25519-public")?.toString("base64"), rapidSnark: artifacts.rapidSnark, artifacts: artifacts.artifacts, poolObjectVerified: true, tatumServiceVerified: true };
120
+ return { role: this.profile.role, network: "testnet", packageId: this.profile.packageId, poolId: this.profile.poolId, asset: this.profile.coinType, verifierVersion: this.profile.verifierVersion, keyPermissions: "owner-only", rpc: this.profile.live.writeGrpcEndpoint, healthy: true, readyForDemo: fixes.length === 0, fixes, runtime: "testnet-live", signer: this.signer.toSuiAddress(), babyJubPublicKey: identity.publicKey.map(String), x25519PublicKey: this.store.getSecret("identity:x25519-public")?.toString("base64"), memoryConfigured: Boolean(this.store.getSecret("live:memory-config")), proofsRequired, rapidSnark: artifacts.rapidSnark, artifacts: artifacts.artifacts, poolObjectVerified: true, tatumServiceVerified: true };
123
121
  }
124
122
  async balance() {
125
123
  const state = await this.index.sync();
@@ -97,6 +97,8 @@ export class LiveSellerTransport {
97
97
  async handle(request, response) {
98
98
  try {
99
99
  const url = new URL(request.url ?? "/", this.config.origin);
100
+ if (request.method === "GET" && url.pathname === "/health")
101
+ return this.respond(response, 200, { ok: true, service: "supercuts-seller", capabilities: this.runtime.catalog().length });
100
102
  if (request.method === "GET" && url.pathname === "/v2/catalog")
101
103
  return this.respond(response, 200, { capabilities: this.runtime.catalog() });
102
104
  if (request.method === "POST" && url.pathname === "/v2/prepare")
@@ -108,7 +110,9 @@ export class LiveSellerTransport {
108
110
  if (url.pathname.startsWith(`${(resource.config.pathPrefix ?? "/capability").replace(/\/$/, "")}/`))
109
111
  return resource.handle(request, response);
110
112
  }
111
- return this.respond(response, 404, { error: "NOT_FOUND" });
113
+ if (request.method === "GET" && [...this.#registrations.values()].some((item) => url.pathname === new URL(item.manifest.endpoint).pathname))
114
+ return this.respond(response, 404, { error: "NEGOTIATION_REQUIRED", message: "Protected resource URLs are issued for a negotiated session." });
115
+ return this.respond(response, 404, { error: "NOT_FOUND", message: "Seller route was not found." });
112
116
  }
113
117
  catch (error) {
114
118
  const known = error instanceof SupercutsError ? error : new SupercutsError("SELLER_REQUEST_FAILED", "seller request failed");
package/dist/mandate.js CHANGED
@@ -125,7 +125,7 @@ export async function executeMandated(app, mandate, command, raw) {
125
125
  fail("MANDATE_COMMAND_FORBIDDEN", `mandate does not allow ${command}`);
126
126
  const args = {};
127
127
  for (const [key, value] of Object.entries(raw))
128
- args[key] = typeof value === "boolean" ? value : typeof value === "string" ? value : Buffer.from(JSON.stringify(value)).toString("base64url");
128
+ args[key] = typeof value === "boolean" ? value : typeof value === "string" ? value : Buffer.from(canonical(value)).toString("base64url");
129
129
  if (!spec.mutating)
130
130
  return (await app.execute(command, args)).data;
131
131
  const operationId = String(raw["operation-id"]), journalOperationId = command === "settlement.plan" ? `settlement.plan:${operationId}` : operationId;
@@ -37,17 +37,17 @@ else {
37
37
  return;
38
38
  if (request.method === "tools/list")
39
39
  return send({ jsonrpc: "2.0", id: request.id, result: { tools: [
40
- { name: "setup.status", description: "Explain how the owner completes one-time buyer setup locally. Do not request secrets in chat.", inputSchema: { type: "object", properties: {}, additionalProperties: false } },
40
+ { name: "setup.status", description: "Explain how the owner completes one-time buyer or seller setup locally. Do not request secrets in chat.", inputSchema: { type: "object", properties: {}, additionalProperties: false } },
41
41
  { name: "setup.refresh", description: "Reload this MCP connection after the owner completes local setup.", inputSchema: { type: "object", properties: {}, additionalProperties: false } },
42
42
  ] } });
43
43
  if (request.method === "tools/call" && request.params?.name === "setup.status") {
44
44
  const configured = existsSync(join(home, "profile.json"));
45
- const result = { schemaVersion: "supercuts.setup.v1", ok: true, configured, home, nextStep: configured ? "Call setup.refresh to load the configured buyer tools without restarting the connector." : `Run supercuts onboard buyer --home ${JSON.stringify(home)} in a terminal, then call setup.refresh. The command asks for policy limits, seller origins and local credential file paths. Do not paste keys or tokens into Codex.` };
45
+ const result = { schemaVersion: "supercuts.setup.v1", ok: true, configured, home, nextStep: configured ? "Call setup.refresh to load the configured role tools without restarting the connector." : `Run supercuts onboard buyer --home ${JSON.stringify(home)} or supercuts onboard seller --home ${JSON.stringify(home)} in a terminal, then call setup.refresh. Use a separate owner-only home for every role and identity. Do not paste keys or tokens into Codex.` };
46
46
  return send({ jsonrpc: "2.0", id: request.id, result: { content: [{ type: "text", text: JSON.stringify(result) }], structuredContent: result } });
47
47
  }
48
48
  if (request.method === "tools/call" && request.params?.name === "setup.refresh") {
49
49
  if (!existsSync(join(home, "profile.json"))) {
50
- const result = { schemaVersion: "supercuts.setup.v1", ok: false, error: { code: "SETUP_REQUIRED", message: "buyer setup is not complete", recovery: `Run supercuts onboard buyer --home ${JSON.stringify(home)} locally, then retry setup.refresh.` } };
50
+ const result = { schemaVersion: "supercuts.setup.v1", ok: false, error: { code: "SETUP_REQUIRED", message: "role setup is not complete", recovery: `Run supercuts onboard buyer --home ${JSON.stringify(home)} or supercuts onboard seller --home ${JSON.stringify(home)} locally, then retry setup.refresh.` } };
51
51
  return send({ jsonrpc: "2.0", id: request.id, result: { isError: true, content: [{ type: "text", text: JSON.stringify(result) }], structuredContent: result } });
52
52
  }
53
53
  const result = { schemaVersion: "supercuts.setup.v1", ok: true, configured: true, reloading: true };
@@ -19,10 +19,10 @@ 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.readyForDemo === false)
25
+ fail("ONBOARDING_RUNTIME_NOT_READY", "live runtime doctor reports missing prerequisites");
26
26
  const mandate = loadMandate(home);
27
27
  if (mandate.version !== "supercuts.mandate.v2")
28
28
  fail("DEAL_MANDATE_VERSION", "buyer needs a v2 standing mandate");
@@ -68,15 +68,20 @@ export async function onboardingDoctor(home, sellerConfig) {
68
68
  const served = await json(new URL("/v2/catalog", config.origin));
69
69
  if (!Array.isArray(served.capabilities))
70
70
  fail("SELLER_CATALOG_INVALID", "seller endpoint has no valid catalog");
71
+ let proofsRequired = false;
71
72
  for (const item of config.capabilities) {
72
73
  const registrationPath = resolve(item.registrationFile);
73
74
  if ((statSync(registrationPath).mode & 0o077) !== 0)
74
75
  fail("SELLER_CONFIG_PERMISSIONS", "registration file must be owner-only");
75
76
  const registration = JSON.parse(readFileSync(registrationPath, "utf8"));
77
+ proofsRequired ||= registration.manifest.supportedRails.includes("shielded");
76
78
  await verifySignedCapability(registration);
77
79
  if (!served.capabilities.some((entry) => JSON.stringify(entry) === JSON.stringify(registration)))
78
80
  fail("SELLER_CATALOG_INVALID", "seller endpoint does not serve its configured registration");
79
81
  }
82
+ const runtime = await app.execute("doctor", { "proofs-required": proofsRequired });
83
+ if (runtime.data.readyForDemo === false)
84
+ fail("ONBOARDING_RUNTIME_NOT_READY", "live runtime doctor reports missing prerequisites");
80
85
  return { role: "seller", healthy: true, runtime: runtime.data, capabilities: config.capabilities.length, origin: config.origin };
81
86
  }
82
87
  return fail("ROLE_FORBIDDEN", "onboarding doctor supports buyer and seller homes");
package/dist/phase3.js CHANGED
@@ -760,7 +760,7 @@ export class Phase3App {
760
760
  return output(command, await this.liveMemory(command, args));
761
761
  const runtime = await LiveRuntime.create(this.profile, this.store);
762
762
  if (command === "doctor")
763
- return output(command, await runtime.doctor());
763
+ return output(command, await runtime.doctor(args["proofs-required"] !== false));
764
764
  if (command === "status")
765
765
  return output(command, { role: this.profile.role, network: this.profile.network, mock: false, runtime: "testnet-live" });
766
766
  if (command === "capability.publish")
@@ -1143,7 +1143,7 @@ export class Phase3App {
1143
1143
  export function output(command, data) { return { schemaVersion: "supercuts.v1", ok: true, command, data }; }
1144
1144
  export function errorOutput(error) {
1145
1145
  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);
1146
+ 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
1147
  const sameOperation = retryable || ["OPERATION_ID_CONFLICT", "DEAL_OPERATION_CONFLICT"].includes(known.code);
1148
1148
  return { schemaVersion: "supercuts.v1", ok: false, error: { code: known.code, message: known.message, recovery: recovery(known.code), retryable, retryWithSameOperationId: sameOperation } };
1149
1149
  }
@@ -1164,6 +1164,8 @@ function recovery(code) {
1164
1164
  return "reconcile or resume with the same operation ID; never create a replacement payment";
1165
1165
  if (code === "DELIVERY_PENDING")
1166
1166
  return "resume the same root operation ID; completed selection, payment, and agreement stages will not run again";
1167
+ if (code === "DEAL_SELLER_UNAVAILABLE")
1168
+ return "check the seller /health endpoint, then resume the same root operation ID";
1167
1169
  if (code === "X402_FACILITATOR_UNAVAILABLE")
1168
1170
  return "the signed transaction remains journaled; resume the same operation ID after facilitator health recovers";
1169
1171
  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++) {
@@ -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
  }
@@ -48,6 +48,8 @@ async function main() {
48
48
  catch (error) {
49
49
  return fail("BRIEF_INVALID", "market brief must be JSON", error);
50
50
  }
51
+ if (report.reviewStatus !== "approved")
52
+ fail("ARTIFACT_REVIEW_REQUIRED", "market brief must be owner-reviewed before provisioning");
51
53
  if (typeof report.title !== "string" || typeof report.body !== "string" || !Array.isArray(report.citations) || report.citations.length < 2 || report.citations.some((value) => typeof value !== "string" || !/^https:\/\//.test(value)))
52
54
  fail("BRIEF_INVALID", "market brief needs a title, body, and at least two HTTPS citations");
53
55
  }
@@ -56,7 +58,19 @@ async function main() {
56
58
  for (const [briefHash, artifactPath] of Object.entries(capability.variants ?? {})) {
57
59
  if (!/^[0-9a-f]{64}$/.test(briefHash))
58
60
  fail("SELLER_CONFIG_INVALID", "variant brief hash is invalid");
59
- artifacts[`${manifest.capabilityId}:${briefHash}`] = readFileSync(resolve(artifactPath)).toString("base64url");
61
+ const body = readFileSync(resolve(artifactPath));
62
+ if (manifest.capability === "sui.competitor-analysis") {
63
+ let report;
64
+ try {
65
+ report = JSON.parse(body.toString());
66
+ }
67
+ catch (error) {
68
+ return fail("ANALYSIS_INVALID", "competitor analysis must be JSON", error);
69
+ }
70
+ if (report.reviewStatus !== "approved")
71
+ fail("ARTIFACT_REVIEW_REQUIRED", "competitor analysis must be owner-reviewed before provisioning");
72
+ }
73
+ artifacts[`${manifest.capabilityId}:${briefHash}`] = body.toString("base64url");
60
74
  }
61
75
  if (!capability.artifact && !Object.keys(capability.variants ?? {}).length)
62
76
  fail("SELLER_ARTIFACT_MISSING", "capability needs a prepared artifact or brief variants");
@@ -0,0 +1,50 @@
1
+ export type SellerResearchKind = "market-brief" | "competitor-analysis";
2
+ export interface SellerResearchDraft {
3
+ title: string;
4
+ body: string;
5
+ citations: string[];
6
+ preview: {
7
+ summary: string;
8
+ relevance: number;
9
+ sourceQuality: number;
10
+ freshnessDays: number;
11
+ };
12
+ reviewStatus: "draft-needs-owner-review";
13
+ research: {
14
+ model: string;
15
+ generatedAt: string;
16
+ sourceCount: number;
17
+ topic: string;
18
+ kind: SellerResearchKind;
19
+ };
20
+ }
21
+ export interface SellerResearchOptions {
22
+ apiKey?: string;
23
+ model?: string;
24
+ fetch?: typeof globalThis.fetch;
25
+ maxSearches?: number;
26
+ maxTokens?: number;
27
+ allowedDomains?: string[];
28
+ now?: Date;
29
+ }
30
+ /** Generates an owner-review draft. It never publishes or mutates a live seller. */
31
+ export declare class ClaudeSellerResearcher {
32
+ #private;
33
+ readonly model: string;
34
+ constructor(options?: SellerResearchOptions);
35
+ research(input: {
36
+ kind: SellerResearchKind;
37
+ topic: string;
38
+ context?: Record<string, unknown>;
39
+ }): Promise<SellerResearchDraft>;
40
+ }
41
+ export declare function generateSellerResearch(input: {
42
+ kind: string;
43
+ topic: string;
44
+ output: string;
45
+ contextFile?: string;
46
+ allowedDomains?: string;
47
+ apiKey?: string;
48
+ model?: string;
49
+ fetch?: typeof globalThis.fetch;
50
+ }): Promise<Record<string, unknown>>;
@@ -0,0 +1,210 @@
1
+ import { readFileSync, statSync, writeFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { fail } from "./errors.js";
4
+ const asObject = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : fail("SELLER_RESEARCH_INVALID", "Claude research output must be an object");
5
+ const submittedTool = (blocks) => {
6
+ for (const block of blocks)
7
+ if (block?.type === "tool_use" && block.name === "submit_research") {
8
+ if (block.input && typeof block.input === "object" && !Array.isArray(block.input))
9
+ return block;
10
+ if (typeof block.input === "string")
11
+ try {
12
+ const input = JSON.parse(block.input);
13
+ if (input && typeof input === "object" && !Array.isArray(input))
14
+ return { ...block, input };
15
+ }
16
+ catch { }
17
+ }
18
+ return undefined;
19
+ };
20
+ const normalizedUrl = (value) => { const url = new URL(value); url.hash = ""; if (url.pathname !== "/")
21
+ url.pathname = url.pathname.replace(/\/$/, ""); return url.toString(); };
22
+ function sourceUrls(value, found = new Set()) {
23
+ if (Array.isArray(value))
24
+ for (const item of value)
25
+ sourceUrls(item, found);
26
+ else if (value && typeof value === "object")
27
+ for (const [key, item] of Object.entries(value)) {
28
+ if (key === "url" && typeof item === "string") {
29
+ try {
30
+ if (new URL(item).protocol === "https:")
31
+ found.add(normalizedUrl(item));
32
+ }
33
+ catch { }
34
+ }
35
+ sourceUrls(item, found);
36
+ }
37
+ return found;
38
+ }
39
+ function validateDraft(value, evidence, allowedDomains) {
40
+ const draft = asObject(value), preview = asObject(draft.preview);
41
+ if (typeof draft.title !== "string" || draft.title.trim().length < 8 || draft.title.length > 180)
42
+ fail("SELLER_RESEARCH_INVALID", "research title must contain 8 to 180 characters");
43
+ if (typeof draft.body !== "string" || draft.body.trim().length < 800 || draft.body.length > 16_000)
44
+ fail("SELLER_RESEARCH_INVALID", "research body must contain 800 to 16,000 characters");
45
+ if (!Array.isArray(draft.citations))
46
+ fail("SELLER_RESEARCH_INVALID", "research citations must be an array");
47
+ if (draft.citations.length < 3 || draft.citations.length > 32)
48
+ fail("SELLER_RESEARCH_INVALID", `research must contain 3 to 32 citations; received ${draft.citations.length}`);
49
+ let citations = draft.citations.map((item) => {
50
+ if (typeof item !== "string")
51
+ return fail("SELLER_RESEARCH_INVALID", "every citation must be an HTTPS URL");
52
+ let url;
53
+ try {
54
+ url = new URL(item);
55
+ }
56
+ catch (error) {
57
+ return fail("SELLER_RESEARCH_INVALID", "every citation must be a valid URL", error);
58
+ }
59
+ if (url.protocol !== "https:" || url.username || url.password)
60
+ fail("SELLER_RESEARCH_INVALID", "every citation must be a public HTTPS URL without credentials");
61
+ if (allowedDomains.length && !allowedDomains.some((domain) => url.hostname === domain || url.hostname.endsWith(`.${domain}`)))
62
+ fail("SELLER_RESEARCH_SOURCE_UNAPPROVED", `citation host ${url.hostname} is outside --allowed-domains`);
63
+ const normalized = normalizedUrl(item);
64
+ if (!evidence.has(normalized))
65
+ fail("SELLER_RESEARCH_SOURCE_UNVERIFIED", "Claude cited a URL that was not returned by web search");
66
+ return normalized;
67
+ });
68
+ if (new Set(citations).size !== citations.length)
69
+ fail("SELLER_RESEARCH_INVALID", "research citations must be unique");
70
+ let reportBody = draft.body.trim();
71
+ const markers = [...reportBody.matchAll(/\[([1-9][0-9]*)\]/g)].map((match) => Number(match[1]));
72
+ if (markers.some((index) => index > citations.length))
73
+ fail("SELLER_RESEARCH_INVALID", "research body contains an out-of-range citation marker");
74
+ const used = [...new Set(markers)].sort((a, b) => a - b);
75
+ if (used.length < 3)
76
+ fail("SELLER_RESEARCH_INVALID", "research body must reference at least three listed sources inline");
77
+ if (used.length !== citations.length) {
78
+ const remap = new Map(used.map((oldIndex, newIndex) => [oldIndex, newIndex + 1]));
79
+ reportBody = reportBody.replace(/\[([1-9][0-9]*)\]/g, (_marker, raw) => `[${remap.get(Number(raw))}]`);
80
+ citations = used.map((index) => citations[index - 1]);
81
+ }
82
+ if (typeof preview.summary !== "string" || preview.summary.trim().length < 30 || preview.summary.length > 400)
83
+ fail("SELLER_RESEARCH_INVALID", "preview summary must contain 30 to 400 characters");
84
+ for (const key of ["relevance", "sourceQuality"])
85
+ if (typeof preview[key] !== "number" || !Number.isFinite(preview[key]) || preview[key] < 0 || preview[key] > 1)
86
+ fail("SELLER_RESEARCH_INVALID", `preview ${key} must be in [0,1]`);
87
+ if (!Number.isInteger(preview.freshnessDays) || preview.freshnessDays < 0 || preview.freshnessDays > 365)
88
+ fail("SELLER_RESEARCH_INVALID", "preview freshnessDays must be an integer in [0,365]");
89
+ return { title: draft.title.trim(), body: reportBody, citations, preview: { summary: preview.summary.trim(), relevance: preview.relevance, sourceQuality: preview.sourceQuality, freshnessDays: preview.freshnessDays } };
90
+ }
91
+ /** Generates an owner-review draft. It never publishes or mutates a live seller. */
92
+ export class ClaudeSellerResearcher {
93
+ model;
94
+ #apiKey;
95
+ #fetch;
96
+ #maxSearches;
97
+ #maxTokens;
98
+ #allowedDomains;
99
+ #now;
100
+ constructor(options = {}) {
101
+ this.#apiKey = options.apiKey ?? process.env.CLAUDE_API_KEY ?? "";
102
+ if (!this.#apiKey)
103
+ fail("CLAUDE_CREDENTIAL_MISSING", "CLAUDE_API_KEY is required to generate a seller research draft");
104
+ this.model = options.model ?? process.env.CLAUDE_RESEARCH_MODEL ?? process.env.CLAUDE_MODEL ?? "claude-haiku-5-5";
105
+ this.#fetch = options.fetch ?? fetch;
106
+ this.#maxSearches = options.maxSearches ?? 8;
107
+ this.#maxTokens = options.maxTokens ?? 5_000;
108
+ this.#allowedDomains = (options.allowedDomains ?? []).map((item) => item.trim().toLowerCase()).filter(Boolean);
109
+ this.#now = options.now ?? new Date();
110
+ if (!Number.isInteger(this.#maxSearches) || this.#maxSearches < 3 || this.#maxSearches > 12 || !Number.isInteger(this.#maxTokens) || this.#maxTokens < 1_500 || this.#maxTokens > 8_192)
111
+ fail("CLAUDE_LIMIT_INVALID", "seller research bounds are invalid");
112
+ if (this.#allowedDomains.some((item) => !/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)*[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(item)))
113
+ fail("SELLER_RESEARCH_INVALID", "allowed domains must be bare DNS names");
114
+ }
115
+ async research(input) {
116
+ if (!["market-brief", "competitor-analysis"].includes(input.kind) || !input.topic.trim() || input.topic.length > 500)
117
+ fail("SELLER_RESEARCH_INVALID", "research needs a valid kind and a topic of at most 500 characters");
118
+ const context = input.context ? JSON.stringify(input.context) : "none";
119
+ if (context.length > 40_000)
120
+ fail("SELLER_RESEARCH_INVALID", "matching brief context is too large");
121
+ const schema = {
122
+ type: "object", additionalProperties: false,
123
+ properties: {
124
+ title: { type: "string", maxLength: 180 }, body: { type: "string", maxLength: 16000 }, citations: { type: "array", minItems: 3, maxItems: 32, items: { type: "string" } },
125
+ preview: { type: "object", additionalProperties: false, properties: { summary: { type: "string", maxLength: 400 }, relevance: { type: "number", minimum: 0, maximum: 1 }, sourceQuality: { type: "number", minimum: 0, maximum: 1 }, freshnessDays: { type: "integer", minimum: 0, maximum: 365 } }, required: ["summary", "relevance", "sourceQuality", "freshnessDays"] },
126
+ }, required: ["title", "body", "citations", "preview"],
127
+ };
128
+ const webSearch = { type: "web_search_20250305", name: "web_search", max_uses: this.#maxSearches, allowed_callers: ["direct"] };
129
+ if (this.#allowedDomains.length)
130
+ webSearch.allowed_domains = this.#allowedDomains;
131
+ const system = `<role>You are the research desk for a paid Sui ecosystem intelligence seller.</role>
132
+ <quality_bar>Use web search before writing. Prefer primary sources, official documentation, project announcements, repositories, and dated first-party metrics. Cross-check material claims across independent sources. Separate observed facts from analysis, name dates and measurement windows, surface uncertainty and counter-evidence, and never invent a fact, quotation, number, or URL. The report must be useful to a technically sophisticated buyer, not promotional copy.</quality_bar>
133
+ <format>After research and a final self-check, call submit_research exactly once. Write a cohesive 800-16000 character report. Include inline source markers such as [1] that correspond in order to the citations array. Use only HTTPS URLs actually returned by web_search. Provide at least 3 unique sources and include additional material sources when useful. Preview scores are seller estimates, not independent guarantees.</format>`;
134
+ const prompt = `<today>${this.#now.toISOString().slice(0, 10)}</today><kind>${input.kind}</kind><topic>${input.topic.trim()}</topic><matching_brief_context>${context}</matching_brief_context><instructions>Research the topic broadly enough to identify market structure, concrete evidence, important competitors or alternatives, risks, and what would change the conclusion. For competitor analysis, directly compare the purchased brief's thesis with named competing projects or approaches. Before submission, verify every citation was observed in search results and every quantitative claim is qualified by date and source.</instructions>`;
135
+ const request = async (payload) => {
136
+ let response;
137
+ try {
138
+ response = await this.#fetch("https://api.anthropic.com/v1/messages", { method: "POST", headers: { "content-type": "application/json", "anthropic-version": "2023-06-01", "x-api-key": this.#apiKey }, body: JSON.stringify(payload), signal: AbortSignal.timeout(90_000) });
139
+ }
140
+ catch (error) {
141
+ return fail("CLAUDE_UNAVAILABLE", "Claude research request failed closed", error);
142
+ }
143
+ if (!response.ok) {
144
+ await response.arrayBuffer();
145
+ return fail("CLAUDE_UNAVAILABLE", `Claude research returned status ${response.status}`);
146
+ }
147
+ try {
148
+ return await response.json();
149
+ }
150
+ catch (error) {
151
+ return fail("CLAUDE_OUTPUT_INVALID", "Claude research returned invalid JSON", error);
152
+ }
153
+ };
154
+ const submitTool = { name: "submit_research", description: "Submit the source-checked seller research draft for owner review.", input_schema: schema };
155
+ let messages = [{ role: "user", content: prompt }], selected;
156
+ const blocks = [];
157
+ for (let turn = 0; turn < 3; turn++) {
158
+ const body = await request({ model: this.model, max_tokens: this.#maxTokens, system, messages, tools: [webSearch, submitTool] });
159
+ const current = Array.isArray(body.content) ? body.content : [];
160
+ blocks.push(...current);
161
+ selected = submittedTool(current);
162
+ if (selected || current.some((block) => block?.type === "text" && typeof block.text === "string" && block.text.trim()))
163
+ break;
164
+ if (!current.length || turn === 2)
165
+ break;
166
+ // Anthropic can pause a server-side search turn. Replay its opaque content
167
+ // unchanged so the model can synthesize the already-returned evidence.
168
+ messages = [...messages, { role: "assistant", content: current }, { role: "user", content: "Continue the paused research turn using the returned search evidence, then submit the structured draft." }];
169
+ }
170
+ const searchFailure = blocks.some((block) => block?.type === "web_search_tool_result" && (block.is_error === true || block.content?.type === "web_search_tool_result_error"));
171
+ if (searchFailure)
172
+ fail("SELLER_RESEARCH_SEARCH_FAILED", "Claude web search failed; no draft was written");
173
+ const evidence = sourceUrls(blocks);
174
+ if (evidence.size < 3)
175
+ fail("SELLER_RESEARCH_SOURCE_UNVERIFIED", "Claude returned fewer than three verifiable web-search sources");
176
+ if (selected)
177
+ try {
178
+ const draft = validateDraft(selected.input, evidence, this.#allowedDomains);
179
+ return { ...draft, reviewStatus: "draft-needs-owner-review", research: { model: this.model, generatedAt: this.#now.toISOString(), sourceCount: evidence.size, topic: input.topic.trim(), kind: input.kind } };
180
+ }
181
+ catch { }
182
+ const researchedText = blocks.filter((block) => block?.type === "text" && typeof block.text === "string").map((block) => block.text).join("\n").slice(0, 30_000) || (selected ? JSON.stringify(selected.input).slice(0, 30_000) : "");
183
+ if (!researchedText)
184
+ fail("CLAUDE_OUTPUT_INVALID", "Claude returned search evidence without a research answer");
185
+ const editorSystem = `<role>You are a strict research editor.</role><instructions>Convert or repair the supplied web-researched answer into the submit_research schema. Preserve material uncertainty, dates, comparisons, and counter-evidence. Use only URLs in verified_sources, include at least 3 unique citations and all additional material sources, and do not add facts or sources. The body must be 800-16000 characters and use [1], [2], and subsequent inline markers corresponding to citation order. Ensure preview is an object with summary, relevance, sourceQuality, and integer freshnessDays. Call submit_research exactly once.</instructions>`;
186
+ const edited = await request({ model: this.model, max_tokens: this.#maxTokens, system: editorSystem, messages: [{ role: "user", content: JSON.stringify({ kind: input.kind, topic: input.topic.trim(), researchedAnswer: researchedText, verifiedSources: [...evidence].slice(0, 32) }) }], tools: [submitTool], tool_choice: { type: "tool", name: "submit_research" } });
187
+ const editedBlocks = Array.isArray(edited.content) ? edited.content : [];
188
+ selected = submittedTool(editedBlocks);
189
+ if (!selected)
190
+ fail("CLAUDE_OUTPUT_INVALID", "Claude did not submit a structured research draft");
191
+ const draft = validateDraft(selected.input, evidence, this.#allowedDomains);
192
+ return { ...draft, reviewStatus: "draft-needs-owner-review", research: { model: this.model, generatedAt: this.#now.toISOString(), sourceCount: evidence.size, topic: input.topic.trim(), kind: input.kind } };
193
+ }
194
+ }
195
+ export async function generateSellerResearch(input) {
196
+ const kind = input.kind;
197
+ const output = resolve(input.output || fail("ARGUMENT_REQUIRED", "seller research requires --output"));
198
+ let context;
199
+ if (input.contextFile) {
200
+ const path = resolve(input.contextFile);
201
+ if ((statSync(path).mode & 0o077) !== 0)
202
+ fail("SELLER_CONFIG_PERMISSIONS", "research context file must be owner-only");
203
+ context = asObject(JSON.parse(readFileSync(path, "utf8")));
204
+ }
205
+ const allowedDomains = input.allowedDomains?.split(",").map((item) => item.trim()).filter(Boolean);
206
+ const researcher = new ClaudeSellerResearcher({ ...(input.apiKey ? { apiKey: input.apiKey } : {}), ...(input.model ? { model: input.model } : {}), ...(input.fetch ? { fetch: input.fetch } : {}), ...(allowedDomains?.length ? { allowedDomains } : {}) });
207
+ const draft = await researcher.research({ kind, topic: input.topic, ...(context ? { context } : {}) });
208
+ writeFileSync(output, `${JSON.stringify(draft, null, 2)}\n`, { mode: 0o600, flag: "wx" });
209
+ return { output, kind, topic: input.topic, model: researcher.model, citations: draft.citations.length, reviewStatus: draft.reviewStatus, nextStep: "Review every claim and citation, then set reviewStatus to approved before catalog generation or seller packaging." };
210
+ }
@@ -0,0 +1 @@
1
+ export declare function scaffoldSeller(input: Record<string, string | boolean>): Record<string, unknown>;
@@ -0,0 +1,35 @@
1
+ import { chmodSync, mkdirSync, writeFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { fail } from "./errors.js";
4
+ const idPattern = /^[a-z0-9][a-z0-9._-]{1,127}$/;
5
+ export function scaffoldSeller(input) {
6
+ const home = typeof input.home === "string" ? resolve(input.home) : fail("ARGUMENT_REQUIRED", "seller scaffold requires --home");
7
+ const output = typeof input.output === "string" ? resolve(input.output) : fail("ARGUMENT_REQUIRED", "seller scaffold requires --output");
8
+ const capabilityId = String(input["capability-id"] ?? ""), capability = String(input.capability ?? ""), description = String(input.description ?? ""), origin = String(input.origin ?? ""), price = String(input.price ?? "");
9
+ if (!idPattern.test(capabilityId) || !idPattern.test(capability))
10
+ fail("SELLER_SCAFFOLD_INVALID", "capability identifiers must use 2..128 lowercase letters, digits, dot, underscore, or dash");
11
+ if (!description || description.length > 512 || !/^[1-9][0-9]*$/.test(price))
12
+ fail("SELLER_SCAFFOLD_INVALID", "description and positive MIST --price are required");
13
+ const url = new URL(origin);
14
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && ["localhost", "127.0.0.1", "::1"].includes(url.hostname)) || url.username || url.password || url.pathname !== "/" || url.search || url.hash)
15
+ fail("SELLER_SCAFFOLD_INVALID", "origin must be an HTTPS or loopback HTTP origin without path or credentials");
16
+ const rails = String(input.rails ?? "x402").split(",").map((rail) => rail.trim()).filter(Boolean);
17
+ if (!rails.length || rails.some((rail) => rail !== "x402" && rail !== "shielded") || new Set(rails).size !== rails.length)
18
+ fail("SELLER_SCAFFOLD_INVALID", "--rails must be x402, shielded, or x402,shielded");
19
+ const days = Number(input["expires-in-days"] ?? "30");
20
+ if (!Number.isSafeInteger(days) || days < 1 || days > 365)
21
+ fail("SELLER_SCAFFOLD_INVALID", "--expires-in-days must be 1..365");
22
+ const listenPort = Number(input["listen-port"] ?? (url.protocol === "http:" && url.port ? url.port : "8788"));
23
+ if (!Number.isSafeInteger(listenPort) || listenPort < 1 || listenPort > 65535)
24
+ fail("SELLER_SCAFFOLD_INVALID", "--listen-port must be 1..65535");
25
+ mkdirSync(output, { recursive: false, mode: 0o700 });
26
+ chmodSync(output, 0o700);
27
+ const specPath = resolve(output, "capability.json"), handlerPath = resolve(output, "handler.mjs"), registrationPath = resolve(output, "registration.json"), servicePath = resolve(output, "seller-service.json");
28
+ const spec = { capabilityId, capability, description, inputSchema: { type: "object", required: ["task"], properties: { task: { type: "string", minLength: 1, maxLength: 4096 } }, additionalProperties: false }, outputSchema: { type: "object", required: ["kind"], properties: { kind: { const: capability } }, additionalProperties: false }, supportedRails: rails, priceBand: { minimum: price, maximum: price }, asset: "0x2::sui::SUI", endpoint: `${url.origin}/v2/resource/${capabilityId}`, expiresAt: Date.now() + days * 86_400_000 };
29
+ const handler = `export async function handle(request) {\n const result = { kind: ${JSON.stringify(capability)}, task: request.input.task, message: "Replace this template with your seller implementation." };\n return { artifact: Buffer.from(JSON.stringify(result)), contentType: "application/json", metadata: { kind: ${JSON.stringify(capability)} } };\n}\n`;
30
+ const service = { origin: url.origin, listenHost: "127.0.0.1", listenPort, ...(rails.includes("x402") ? { facilitatorUrlEnv: "X402_FACILITATOR_URL", facilitatorKeyEnv: "FACILITATOR_SHARED_KEY" } : {}), capabilities: [{ registrationFile: registrationPath, handler: handlerPath }] };
31
+ writeFileSync(specPath, `${JSON.stringify(spec, null, 2)}\n`, { mode: 0o600, flag: "wx" });
32
+ writeFileSync(handlerPath, handler, { mode: 0o600, flag: "wx" });
33
+ writeFileSync(servicePath, `${JSON.stringify(service, null, 2)}\n`, { mode: 0o600, flag: "wx" });
34
+ return { home, output, specFile: specPath, handlerFile: handlerPath, registrationFile: registrationPath, serviceFile: servicePath, expiresAt: spec.expiresAt, nextCommands: [`supercuts-seller-register ${JSON.stringify(home)} ${JSON.stringify(specPath)} ${JSON.stringify(registrationPath)}`, `supercuts-seller-live ${JSON.stringify(home)} ${JSON.stringify(servicePath)}`], note: "Review and replace the generated handler before serving production work." };
35
+ }
@@ -18,7 +18,7 @@ export declare class SellerService {
18
18
  facilitatorUrl: string;
19
19
  facilitatorKey: string;
20
20
  allowedBuyers: ReadonlySet<string>;
21
- claudeKey: string;
21
+ claudeKey?: string;
22
22
  directoryAggregator?: string;
23
23
  }): Promise<SellerService>;
24
24
  private artifact;
@@ -4,7 +4,6 @@ import { createServer } from "node:http";
4
4
  import { mkdtempSync, rmSync } from "node:fs";
5
5
  import { tmpdir } from "node:os";
6
6
  import { join } from "node:path";
7
- import { BoundedClaude } from "./claude.js";
8
7
  import { decodeSuiPrivateKey } from "@mysten/sui/cryptography";
9
8
  import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
10
9
  import { isValidPersonalMessageSignature } from "@mysten/sui/verify";
@@ -35,10 +34,9 @@ export class SellerService {
35
34
  #v2 = new Map();
36
35
  #v2BySeller = new Map();
37
36
  #root = mkdtempSync(join(tmpdir(), "supercuts-sellers-"));
38
- #claude;
39
37
  #queue = Promise.resolve();
40
38
  #healthy = true;
41
- constructor(db, snapshotKey, origin, facilitatorUrl, facilitatorKey, allowedBuyers, claudeKey, directoryAggregator) {
39
+ constructor(db, snapshotKey, origin, facilitatorUrl, facilitatorKey, allowedBuyers, directoryAggregator) {
42
40
  this.db = db;
43
41
  this.snapshotKey = snapshotKey;
44
42
  this.origin = origin;
@@ -46,10 +44,9 @@ export class SellerService {
46
44
  this.facilitatorKey = facilitatorKey;
47
45
  this.allowedBuyers = allowedBuyers;
48
46
  this.directoryAggregator = directoryAggregator;
49
- this.#claude = new BoundedClaude({ apiKey: claudeKey });
50
47
  }
51
48
  static async create(config) {
52
- const service = new SellerService(config.db, config.snapshotKey, config.origin, config.facilitatorUrl, config.facilitatorKey, config.allowedBuyers, config.claudeKey, config.directoryAggregator);
49
+ const service = new SellerService(config.db, config.snapshotKey, config.origin, config.facilitatorUrl, config.facilitatorKey, config.allowedBuyers, config.directoryAggregator);
53
50
  const rows = (await config.db.listCatalog()).filter((row) => !(isV2(row) && row.manifest.expiresAt <= Date.now()));
54
51
  for (const row of rows) {
55
52
  validateCapabilityManifest(row.manifest);
@@ -271,10 +268,6 @@ export class SellerService {
271
268
  const opening = BigInt(String(offer.opening ?? "0")), maximum = BigInt(String(offer.maximum ?? "0")), minimum = BigInt(row.manifest.priceBand.minimum), briefHash = typeof offer.briefHash === "string" ? offer.briefHash : undefined;
272
269
  if (opening <= 0n || opening > maximum || maximum < minimum || maximum > BigInt(row.manifest.priceBand.maximum) || briefHash !== undefined && !/^[0-9a-f]{64}$/.test(briefHash))
273
270
  fail("NEGOTIATION_BOUNDS_INVALID", "buyer offer exceeds approved price bounds");
274
- const task = String(offer.task ?? "").slice(0, 512);
275
- const decision = await this.#claude.decide({ system: "You are a bounded research seller. Present only the fixed price and service terms approved by Supercuts.", task, candidates: [{ action: "counteroffer", capabilityId, amount: minimum.toString() }], schema: { name: "respond_to_private_offer", description: "Present the approved exact price.", properties: { action: { type: "string", enum: ["counteroffer"] } }, required: ["action"] } });
276
- if (decision.action.action !== "counteroffer")
277
- fail("SELLER_DECISION_INVALID", "seller did not choose the approved response");
278
271
  const reply = { capabilityId, amount: minimum.toString(), asset: row.manifest.asset, railOptions: row.manifest.supportedRails, accepted: true };
279
272
  const replyCiphertext = encryptSessionMessage(key, sessionId, `${sessionId}:seller:2`, Buffer.from(JSON.stringify(reply)));
280
273
  const requestBlobId = await this.publishCiphertext(slot, ciphertext), replyBlobId = await this.publishCiphertext(slot, replyCiphertext);
@@ -291,7 +284,7 @@ export class SellerService {
291
284
  this.#healthy = false;
292
285
  throw error;
293
286
  }
294
- await this.db.addEvent({ run_id: runId, seller_id: slot.sellerId, stage: "negotiation", status: "complete", details: { capabilityId, sessionId, requestBlobId, replyBlobId, transcriptHash, model: decision.model } });
287
+ await this.db.addEvent({ run_id: runId, seller_id: slot.sellerId, stage: "negotiation", status: "complete", details: { capabilityId, sessionId, requestBlobId, replyBlobId, transcriptHash, decision: "policy-validated-fixed-price" } });
295
288
  return this.reply(response, 200, { sessionId, capabilityId, ciphertext: replyCiphertext, signature, requestBlobId, replyBlobId, transcriptHash });
296
289
  });
297
290
  }
@@ -316,9 +309,6 @@ export class SellerService {
316
309
  const prior = slot.app.store.db.prepare("SELECT operation_id AS operationId,capability_id AS capabilityId,amount,artifact_key AS artifactKey,run_id AS runId FROM remote_quotes WHERE request_id=?").get(requestId);
317
310
  if (prior && JSON.stringify(prior) !== JSON.stringify({ operationId, capabilityId, amount: quote.amount, artifactKey: quote.artifactKey, runId }))
318
311
  fail("SELLER_QUOTE_CONFLICT", "request ID identifies another quote");
319
- const decision = await this.#claude.decide({ system: "You are a bounded seller agent. You may present only the fixed approved quote.", task: String(input.task ?? "Sui market brief").slice(0, 512), candidates: [{ action: "present_quote", capabilityId, amount: quote.amount }], schema: { name: "respond_to_buyer", description: "Present the approved exact price.", properties: { action: { type: "string", enum: ["present_quote"] } }, required: ["action"] } });
320
- if (decision.action.action !== "present_quote")
321
- fail("SELLER_DECISION_INVALID", "seller did not choose the approved quote");
322
312
  slot.app.store.db.prepare("INSERT OR IGNORE INTO remote_quotes(request_id,operation_id,capability_id,amount,artifact_key,run_id) VALUES(?,?,?,?,?,?)").run(requestId, operationId, capabilityId, quote.amount, quote.artifactKey, runId);
323
313
  const artifact = this.artifact(slot, quote.artifactKey), registration = this.resource(slot, row).register(requestId, operationId, artifact);
324
314
  try {
@@ -328,7 +318,7 @@ export class SellerService {
328
318
  this.#healthy = false;
329
319
  throw error;
330
320
  }
331
- await this.db.addEvent({ run_id: runId, seller_id: slot.sellerId, stage: "quote", status: "complete", details: { capabilityId, amount: quote.amount, model: decision.model } });
321
+ await this.db.addEvent({ run_id: runId, seller_id: slot.sellerId, stage: "quote", status: "complete", details: { capabilityId, amount: quote.amount, decision: "manifest-fixed-price" } });
332
322
  return this.reply(response, 200, { capabilityId, sellerId: slot.sellerId, amount: quote.amount, asset: row.manifest.asset, requestId, operationId, resourceUrl: registration.url, artifactSha256: registration.artifactSha256 });
333
323
  });
334
324
  }
@@ -356,9 +346,6 @@ export class SellerService {
356
346
  const prior = slot.app.store.db.prepare("SELECT capability_id AS capabilityId,artifact_key AS artifactKey,delivery_public_key AS deliveryPublicKey,run_id AS runId FROM remote_deliveries WHERE session_id=?").get(sessionId);
357
347
  if (prior && JSON.stringify(prior) !== JSON.stringify({ capabilityId, artifactKey, deliveryPublicKey, runId }))
358
348
  fail("AGREEMENT_SESSION_CONFLICT", "session identifies another delivery");
359
- const decision = await this.#claude.decide({ system: "You are a bounded seller agent. Sign only the backend-verified agreement.", task: `Respond to accepted competitor analysis request for ${capabilityId}.`, candidates: [{ action: "sign_approved_agreement", capabilityId }], schema: { name: "respond_to_private_offer", description: "Approve the backend-verified agreement.", properties: { action: { type: "string", enum: ["sign_approved_agreement"] } }, required: ["action"] } });
360
- if (decision.action.action !== "sign_approved_agreement")
361
- fail("SELLER_DECISION_INVALID", "seller did not choose the approved agreement");
362
349
  const signed = await executeMandated(slot.app, loadMandate(slot.home), "offer.counter", { "operation-id": `remote-counter-${sessionId}`, session: sessionId, agreement: encoded });
363
350
  slot.app.store.db.prepare("INSERT OR IGNORE INTO remote_deliveries(session_id,capability_id,artifact_key,delivery_public_key,run_id) VALUES(?,?,?,?,?)").run(sessionId, capabilityId, artifactKey, deliveryPublicKey, runId);
364
351
  try {
@@ -368,7 +355,7 @@ export class SellerService {
368
355
  this.#healthy = false;
369
356
  throw error;
370
357
  }
371
- await this.db.addEvent({ run_id: runId, seller_id: slot.sellerId, stage: "agreement", status: "complete", details: { capabilityId, sessionId, model: decision.model, dealCommitment: signed.dealCommitment } });
358
+ await this.db.addEvent({ run_id: runId, seller_id: slot.sellerId, stage: "agreement", status: "complete", details: { capabilityId, sessionId, decision: "backend-verified-agreement", dealCommitment: signed.dealCommitment } });
372
359
  return this.reply(response, 200, { sessionId, capabilityId, agreement: signed.agreement, dealCommitment: signed.dealCommitment, artifactSha256 });
373
360
  });
374
361
  }
@@ -416,7 +403,7 @@ if (process.argv[1] && import.meta.url.endsWith(process.argv[1])) {
416
403
  const required = (key) => process.env[key] ?? fail("SELLER_CONFIG_MISSING", `${key} is required`);
417
404
  const port = Number(process.env.PORT ?? "3000");
418
405
  const origin = required("SELLER_ORIGIN");
419
- SellerService.create({ db: new SellerDatabase(required("SUPABASE_URL"), required("SUPABASE_SERVICE_ROLE_KEY")), snapshotKey: required("SELLER_STATE_KEY"), origin, facilitatorUrl: required("FACILITATOR_URL"), facilitatorKey: required("FACILITATOR_SHARED_KEY"), allowedBuyers: new Set((process.env.SELLER_ALLOWED_BUYERS ?? "").split(",").map((item) => item.trim().toLowerCase()).filter(Boolean)), claudeKey: required("CLAUDE_API_KEY"), ...(process.env.DIRECTORY_WALRUS_AGGREGATOR ? { directoryAggregator: process.env.DIRECTORY_WALRUS_AGGREGATOR } : {}) })
406
+ SellerService.create({ db: new SellerDatabase(required("SUPABASE_URL"), required("SUPABASE_SERVICE_ROLE_KEY")), snapshotKey: required("SELLER_STATE_KEY"), origin, facilitatorUrl: required("FACILITATOR_URL"), facilitatorKey: required("FACILITATOR_SHARED_KEY"), allowedBuyers: new Set((process.env.SELLER_ALLOWED_BUYERS ?? "").split(",").map((item) => item.trim().toLowerCase()).filter(Boolean)), ...(process.env.DIRECTORY_WALRUS_AGGREGATOR ? { directoryAggregator: process.env.DIRECTORY_WALRUS_AGGREGATOR } : {}) })
420
407
  .then((service) => { const server = service.createServer(); server.listen(port, "0.0.0.0"); const close = () => server.close(() => { service.close(); process.exit(0); }); process.on("SIGINT", close); process.on("SIGTERM", close); })
421
408
  .catch((error) => { process.stderr.write(`${error instanceof Error ? error.message : "seller service failed"}\n`); process.exitCode = 1; });
422
409
  }
@@ -0,0 +1,3 @@
1
+ export declare function createSellerSession(home: string): Record<string, unknown>;
2
+ export declare function setupSeller(home: string, configPath: string): Record<string, unknown>;
3
+ export declare function promptSellerSetup(home: string): Promise<Record<string, unknown>>;
@@ -0,0 +1,107 @@
1
+ import { chmodSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { createInterface } from "node:readline/promises";
5
+ import { decodeSuiPrivateKey } from "@mysten/sui/cryptography";
6
+ import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
7
+ import { fail } from "./errors.js";
8
+ import { initialize } from "./phase3.js";
9
+ function ownerFile(path) {
10
+ if ((statSync(path).mode & 0o077) !== 0)
11
+ fail("SETUP_PERMISSIONS", `${path} must be owner-only`);
12
+ }
13
+ function validateConfig(config) {
14
+ config.deploymentConfig ||= join(dirname(fileURLToPath(import.meta.url)), "../config/phase1.testnet.json");
15
+ for (const key of ["deploymentConfig", "tatumKeyFile", "writeGrpc", "tatumGrpc", "walrusPublisher", "walrusAggregator"]) {
16
+ if (typeof config[key] !== "string" || !config[key])
17
+ fail("SETUP_INVALID", `${key} is required`);
18
+ }
19
+ ownerFile(resolve(config.tatumKeyFile));
20
+ if (config.suiKeyFile)
21
+ ownerFile(resolve(config.suiKeyFile));
22
+ const deployment = JSON.parse(readFileSync(resolve(config.deploymentConfig), "utf8"));
23
+ if (deployment.network !== "testnet" || deployment.testOnly !== true || deployment.verifierVersion !== 2)
24
+ fail("SETUP_INVALID", "seller setup requires the pinned experimental v2 Testnet deployment");
25
+ return config;
26
+ }
27
+ function readConfig(path) {
28
+ ownerFile(path);
29
+ return validateConfig(JSON.parse(readFileSync(path, "utf8")));
30
+ }
31
+ export function createSellerSession(home) {
32
+ if (!existsSync(join(home, "profile.json")))
33
+ fail("SETUP_REQUIRED", "initialize the seller before creating an agent session");
34
+ const mcpScript = join(dirname(fileURLToPath(import.meta.url)), "mcp-bootstrap.js");
35
+ const mcp = `[mcp_servers.supercuts-seller]\ncommand = ${JSON.stringify(process.execPath)}\nargs = ${JSON.stringify([mcpScript])}\n[mcp_servers.supercuts-seller.env]\nSUPERCUTS_HOME = ${JSON.stringify(home)}\n`;
36
+ const mcpConfig = join(home, "seller-codex.toml");
37
+ writeFileSync(mcpConfig, mcp, { mode: 0o600 });
38
+ chmodSync(mcpConfig, 0o600);
39
+ return { mcpConfig };
40
+ }
41
+ function setupSellerConfig(home, config) {
42
+ if (existsSync(join(home, "profile.json")))
43
+ fail("PROFILE_EXISTS", "seller home already exists; create a separate home for another seller identity");
44
+ mkdirSync(home, { recursive: true, mode: 0o700 });
45
+ chmodSync(home, 0o700);
46
+ const keyPath = config.suiKeyFile ? resolve(config.suiKeyFile) : join(home, "seller.sui.key");
47
+ if (!config.suiKeyFile)
48
+ writeFileSync(keyPath, Ed25519Keypair.generate().getSecretKey(), { mode: 0o600, flag: "wx" });
49
+ ownerFile(keyPath);
50
+ initialize(home, {
51
+ role: "seller",
52
+ "deployment-config": resolve(config.deploymentConfig),
53
+ "sui-key-file": keyPath,
54
+ "tatum-key-file": resolve(config.tatumKeyFile),
55
+ "write-grpc": config.writeGrpc,
56
+ "tatum-grpc": config.tatumGrpc,
57
+ "walrus-publisher": config.walrusPublisher,
58
+ "walrus-aggregator": config.walrusAggregator,
59
+ ...(config.snarkjs ? { snarkjs: resolve(config.snarkjs) } : {}),
60
+ ...(config.converter ? { converter: resolve(config.converter) } : {}),
61
+ ...(config.rapidsnark ? { rapidsnark: resolve(config.rapidsnark) } : {}),
62
+ });
63
+ const decoded = decodeSuiPrivateKey(readFileSync(keyPath, "utf8").trim());
64
+ if (decoded.scheme !== "ED25519")
65
+ fail("SETUP_INVALID", "seller key must use ED25519");
66
+ const address = Ed25519Keypair.fromSecretKey(decoded.secretKey).toSuiAddress();
67
+ return {
68
+ home,
69
+ address,
70
+ fundingRequired: false,
71
+ warning: "Do not faucet-fund the seller. Only a generated buyer address is funded for live purchases.",
72
+ ...createSellerSession(home),
73
+ nextStep: `Run supercuts seller scaffold --home ${JSON.stringify(home)} --output <offer-dir> with the capability, origin, price, rails, and expiry options. Review the handler, then run the printed registration and service commands.`,
74
+ };
75
+ }
76
+ export function setupSeller(home, configPath) {
77
+ return setupSellerConfig(home, readConfig(resolve(configPath)));
78
+ }
79
+ export async function promptSellerSetup(home) {
80
+ if (!process.stdin.isTTY)
81
+ fail("SETUP_INPUT_REQUIRED", "interactive seller setup requires a terminal; use --config with an owner-only JSON file");
82
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
83
+ try {
84
+ const ask = async (label) => (await rl.question(`${label}: `)).trim();
85
+ const config = {
86
+ deploymentConfig: await ask("Pinned v2 Testnet deployment config path (blank for bundled config)"),
87
+ tatumKeyFile: await ask("Owner-only Tatum key file path"),
88
+ writeGrpc: await ask("Sui write gRPC URL"),
89
+ tatumGrpc: await ask("Tatum read gRPC URL"),
90
+ walrusPublisher: await ask("Walrus publisher URL"),
91
+ walrusAggregator: await ask("Walrus aggregator URL"),
92
+ };
93
+ const snarkjs = await ask("SnarkJS executable path (blank for deployment-relative default)");
94
+ const converter = await ask("Groth16 converter path (blank for deployment-relative default)");
95
+ const rapidsnark = await ask("RapidSnark executable path (blank to use SnarkJS)");
96
+ if (snarkjs)
97
+ config.snarkjs = snarkjs;
98
+ if (converter)
99
+ config.converter = converter;
100
+ if (rapidsnark)
101
+ config.rapidsnark = rapidsnark;
102
+ return setupSellerConfig(home, validateConfig(config));
103
+ }
104
+ finally {
105
+ rl.close();
106
+ }
107
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supercuts-walrus/backend",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "exports": {
@@ -29,6 +29,7 @@
29
29
  },
30
30
  "files": [
31
31
  "dist",
32
+ "config",
32
33
  "README.md"
33
34
  ],
34
35
  "license": "Apache-2.0",