@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 +28 -5
- package/config/phase1.testnet.json +20 -0
- package/config/proving-manifest.json +11 -0
- package/dist/buyer-setup.js +4 -4
- package/dist/cli.js +5 -2
- package/dist/deal-adapter.js +0 -2
- package/dist/deal-engine.js +5 -5
- package/dist/deal.js +2 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/live-deal-ports.js +8 -1
- package/dist/live-runtime.d.ts +1 -1
- package/dist/live-runtime.js +4 -6
- package/dist/live-seller-transport.js +5 -1
- package/dist/mandate.js +1 -1
- package/dist/mcp-bootstrap.js +3 -3
- package/dist/onboarding-doctor.js +8 -3
- package/dist/phase3.js +4 -2
- package/dist/role-lock.d.ts +1 -1
- package/dist/role-lock.js +1 -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 +6 -19
- package/dist/seller-setup.d.ts +3 -0
- package/dist/seller-setup.js +107 -0
- package/package.json +2 -1
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-
|
|
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
|
|
120
|
-
only the model, action, and token counts.
|
|
121
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/dist/buyer-setup.js
CHANGED
|
@@ -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"
|
|
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", "
|
|
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 {
|
package/dist/deal-adapter.js
CHANGED
|
@@ -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)
|
package/dist/deal-engine.js
CHANGED
|
@@ -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
|
|
182
|
+
return this.journal.advance(operationId, "outcome_pending", "complete");
|
|
183
183
|
}
|
|
184
184
|
if (existing)
|
|
185
|
-
return
|
|
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.
|
|
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.
|
|
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
|
|
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" &&
|
|
217
|
-
fail("
|
|
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";
|
package/dist/live-deal-ports.js
CHANGED
|
@@ -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
|
|
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();
|
package/dist/live-runtime.d.ts
CHANGED
|
@@ -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;
|
package/dist/live-runtime.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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(
|
|
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;
|
package/dist/mcp-bootstrap.js
CHANGED
|
@@ -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
|
|
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: "
|
|
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")
|
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++) {
|
|
@@ -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
|
}
|
package/dist/seller-provision.js
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|
package/dist/seller-service.d.ts
CHANGED
package/dist/seller-service.js
CHANGED
|
@@ -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,
|
|
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.
|
|
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,
|
|
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,
|
|
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,
|
|
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)),
|
|
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,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.
|
|
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",
|