@supercuts-walrus/backend 0.1.0 → 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 +6 -6
- package/dist/cli.js +9 -3
- package/dist/deal-adapter.js +0 -2
- package/dist/deal-engine.js +5 -5
- package/dist/deal.js +2 -2
- package/dist/demo.js +70 -8
- package/dist/funding.d.ts +38 -0
- package/dist/funding.js +78 -0
- 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 +4 -1
- package/dist/live-runtime.js +83 -11
- package/dist/live-seller-transport.js +5 -1
- package/dist/mandate.js +10 -9
- package/dist/mcp-bootstrap.js +24 -2
- package/dist/mcp.js +39 -19
- package/dist/onboarding-doctor.js +21 -5
- package/dist/phase3.js +38 -6
- package/dist/remote-buyer.d.ts +18 -0
- package/dist/remote-buyer.js +59 -5
- package/dist/role-lock.d.ts +8 -0
- package/dist/role-lock.js +54 -0
- 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-package.d.ts +9 -0
- package/dist/seller-package.js +50 -0
- 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 +14 -21
- package/dist/seller-setup.d.ts +3 -0
- package/dist/seller-setup.js +107 -0
- package/dist/x402-resource-service.d.ts +2 -0
- package/dist/x402-resource-service.js +36 -17
- package/package.json +2 -1
package/dist/remote-buyer.d.ts
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import { type CapabilityManifest } from "./capability.js";
|
|
2
2
|
import { type AgentMandate } from "./mandate.js";
|
|
3
3
|
import type { Phase3App } from "./phase3.js";
|
|
4
|
+
export declare function analysisAgreementDraft(input: {
|
|
5
|
+
sessionId: string;
|
|
6
|
+
transcriptHash: string;
|
|
7
|
+
serviceHash: string;
|
|
8
|
+
paymentAmount: string;
|
|
9
|
+
buyerPublicKey: string[];
|
|
10
|
+
counterparty: string;
|
|
11
|
+
capabilityId: string;
|
|
12
|
+
manifestHash: string;
|
|
13
|
+
now?: number;
|
|
14
|
+
scalar?: () => string;
|
|
15
|
+
}): Record<string, unknown>;
|
|
4
16
|
interface Entry {
|
|
5
17
|
manifest: CapabilityManifest;
|
|
6
18
|
blobId: string;
|
|
@@ -62,6 +74,12 @@ export declare class RemoteBuyer {
|
|
|
62
74
|
sessionId: string;
|
|
63
75
|
operationId: string;
|
|
64
76
|
}): Promise<Record<string, unknown>>;
|
|
77
|
+
research(input: {
|
|
78
|
+
operationId: string;
|
|
79
|
+
runId: string;
|
|
80
|
+
topic: string;
|
|
81
|
+
budget: string;
|
|
82
|
+
}): Promise<Record<string, unknown>>;
|
|
65
83
|
events(runId: string): Promise<Record<string, unknown>>;
|
|
66
84
|
}
|
|
67
85
|
export {};
|
package/dist/remote-buyer.js
CHANGED
|
@@ -5,10 +5,23 @@ import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
|
|
|
5
5
|
import { validateCapabilityManifest, verifySignedCapability } from "./capability.js";
|
|
6
6
|
import { fail } from "./errors.js";
|
|
7
7
|
import { executeMandated } from "./mandate.js";
|
|
8
|
+
export function analysisAgreementDraft(input) {
|
|
9
|
+
const scalar = input.scalar ?? (() => BigInt(`0x${randomBytes(16).toString("hex")}`).toString());
|
|
10
|
+
return {
|
|
11
|
+
sessionId: input.sessionId, rail: "shielded", dealId: scalar(), revision: "1",
|
|
12
|
+
transcriptRootLo: BigInt(`0x${input.transcriptHash.slice(0, 32)}`).toString(),
|
|
13
|
+
transcriptRootHi: BigInt(`0x${input.transcriptHash.slice(32)}`).toString(),
|
|
14
|
+
serviceDigestLo: BigInt(`0x${input.serviceHash.slice(0, 32)}`).toString(),
|
|
15
|
+
serviceDigestHi: BigInt(`0x${input.serviceHash.slice(32)}`).toString(),
|
|
16
|
+
settlementNonceLo: scalar(), settlementNonceHi: scalar(), paymentAmount: input.paymentAmount,
|
|
17
|
+
blinding: scalar(), expiry: String((input.now ?? Date.now()) + 30 * 60_000),
|
|
18
|
+
buyerPublicKey: input.buyerPublicKey, counterparty: input.counterparty,
|
|
19
|
+
capabilityId: input.capabilityId, manifestHash: input.manifestHash,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
8
22
|
import { decryptSessionMessage, encryptSessionMessage } from "./negotiation.js";
|
|
9
23
|
const sha256 = (value) => createHash("sha256").update(value).digest("hex");
|
|
10
24
|
const num = (value, name) => typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1 ? value : fail("SELLER_PREVIEW_INVALID", `${name} must be between zero and one`);
|
|
11
|
-
const halves = (content) => { const hash = sha256(content); return [BigInt(`0x${hash.slice(0, 32)}`).toString(), BigInt(`0x${hash.slice(32)}`).toString()]; };
|
|
12
25
|
export class RemoteBuyer {
|
|
13
26
|
app;
|
|
14
27
|
mandate;
|
|
@@ -192,10 +205,15 @@ export class RemoteBuyer {
|
|
|
192
205
|
fail("BUYER_PRIVACY_REQUIRED", "saved owner privacy rule did not select shielded settlement");
|
|
193
206
|
const publicKey = this.app.store.getSecret("identity:x25519-public")?.toString("base64url") ?? fail("IDENTITY_MISSING", "buyer delivery key is missing");
|
|
194
207
|
const identity = (await this.app.execute("doctor", {})).data;
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
const
|
|
198
|
-
const
|
|
208
|
+
const serviceHash = sha256(JSON.stringify({ capabilityId: input.capabilityId, briefHash: input.briefHash, artifactSha256, deliveryPublicKey: publicKey }));
|
|
209
|
+
this.app.store.db.exec("CREATE TABLE IF NOT EXISTS remote_buyer_agreements(session_id TEXT PRIMARY KEY,context_hash TEXT NOT NULL,draft_json TEXT NOT NULL,created_at INTEGER NOT NULL)");
|
|
210
|
+
const contextHash = sha256(JSON.stringify({ capabilityId: input.capabilityId, operationId: input.operationId, sessionId: input.sessionId, runId: input.runId, briefHash: input.briefHash, transcriptHash: negotiated.transcriptHash, serviceHash, artifactSha256 }));
|
|
211
|
+
const priorDraft = this.app.store.db.prepare("SELECT context_hash AS contextHash,draft_json AS draftJson FROM remote_buyer_agreements WHERE session_id=?").get(input.sessionId);
|
|
212
|
+
if (priorDraft && priorDraft.contextHash !== contextHash)
|
|
213
|
+
fail("AGREEMENT_SESSION_CONFLICT", "analysis session identifies a different agreement context");
|
|
214
|
+
const draft = priorDraft ? JSON.parse(priorDraft.draftJson) : analysisAgreementDraft({ sessionId: input.sessionId, transcriptHash: String(negotiated.transcriptHash), serviceHash, paymentAmount: entry.manifest.priceBand.minimum, buyerPublicKey: identity.babyJubPublicKey, counterparty: entry.manifest.sellerId, capabilityId: entry.manifest.capabilityId, manifestHash: entry.manifest.manifestHash });
|
|
215
|
+
if (!priorDraft)
|
|
216
|
+
this.app.store.db.prepare("INSERT INTO remote_buyer_agreements(session_id,context_hash,draft_json,created_at) VALUES(?,?,?,?)").run(input.sessionId, contextHash, JSON.stringify(draft), Date.now());
|
|
199
217
|
const signed = await this.request("/v1/agreement", { method: "POST", body: JSON.stringify({ capabilityId: input.capabilityId, sessionId: input.sessionId, runId: input.runId, briefHash: input.briefHash, deliveryPublicKey: publicKey, agreement: Buffer.from(JSON.stringify(draft)).toString("base64url") }) });
|
|
200
218
|
if (signed.artifactSha256 !== artifactSha256 || signed.capabilityId !== input.capabilityId || signed.sessionId !== input.sessionId)
|
|
201
219
|
fail("AGREEMENT_BINDING_MISMATCH", "seller response changed the prepared artifact or session");
|
|
@@ -227,5 +245,41 @@ export class RemoteBuyer {
|
|
|
227
245
|
const recorded = (await this.app.execute("resource.fetch", { "request-id": input.sessionId, "operation-id": input.operationId, response: artifact.toString("base64url") })).data;
|
|
228
246
|
return { status: "delivered", sessionId: input.sessionId, artifactSha256: response.artifactSha256, body: artifact.toString("utf8"), record: { deliveryStatus: recorded.deliveryStatus, responseHash: recorded.responseHash } };
|
|
229
247
|
}
|
|
248
|
+
async research(input) {
|
|
249
|
+
if (!input.operationId || input.operationId.length > 256 || !/^[1-9][0-9]*$/.test(input.budget))
|
|
250
|
+
fail("BUYER_WORKFLOW_INVALID", "research requires a stable operation ID and positive total budget");
|
|
251
|
+
this.app.store.db.exec("CREATE TABLE IF NOT EXISTS remote_research_workflows(operation_id TEXT PRIMARY KEY,input_hash TEXT NOT NULL,created_at INTEGER NOT NULL,updated_at INTEGER NOT NULL,completed_at INTEGER)");
|
|
252
|
+
const inputHash = sha256(JSON.stringify({ runId: input.runId, topic: input.topic, budget: input.budget })), existing = this.app.store.db.prepare("SELECT input_hash AS inputHash,completed_at AS completedAt FROM remote_research_workflows WHERE operation_id=?").get(input.operationId);
|
|
253
|
+
if (existing && existing.inputHash !== inputHash)
|
|
254
|
+
fail("DEAL_OPERATION_CONFLICT", "research operation ID identifies another topic, budget, or run");
|
|
255
|
+
if (!existing)
|
|
256
|
+
this.app.store.db.prepare("INSERT INTO remote_research_workflows(operation_id,input_hash,created_at,updated_at) VALUES(?,?,?,?)").run(input.operationId, inputHash, Date.now(), Date.now());
|
|
257
|
+
const checkpoint = async (stage, action) => {
|
|
258
|
+
const key = `research:${input.operationId}:${stage}`, prior = this.app.store.getSecret(key);
|
|
259
|
+
if (prior)
|
|
260
|
+
return JSON.parse(prior.toString());
|
|
261
|
+
const value = await action();
|
|
262
|
+
this.app.store.putSecretOnce(key, Buffer.from(JSON.stringify(value)));
|
|
263
|
+
this.app.store.db.prepare("UPDATE remote_research_workflows SET updated_at=? WHERE operation_id=?").run(Date.now(), input.operationId);
|
|
264
|
+
return value;
|
|
265
|
+
};
|
|
266
|
+
const child = (stage) => `research:${stage}:${sha256(`${input.operationId}:${stage}`).slice(0, 40)}`;
|
|
267
|
+
const ranked = await checkpoint("selection", () => this.rankBriefs({ topic: input.topic, budget: input.budget }));
|
|
268
|
+
const winnerCapabilityId = String(ranked.winner?.capabilityId ?? "");
|
|
269
|
+
const analysisCapabilityId = String(ranked.analysisCapabilityId ?? "");
|
|
270
|
+
if (!winnerCapabilityId || !analysisCapabilityId)
|
|
271
|
+
fail("ANALYSIS_UNAVAILABLE", "ranked catalog has no affordable brief and matching analysis");
|
|
272
|
+
const brief = await checkpoint("brief", () => this.purchaseBrief({ capabilityId: winnerCapabilityId, operationId: child("brief-payment"), requestId: child("brief-request"), runId: input.runId, task: `Sui research: ${input.topic}`, budget: input.budget }));
|
|
273
|
+
const briefHash = String(brief.artifactSha256);
|
|
274
|
+
const sessionId = child("analysis-session"), paymentOperationId = child("analysis-payment");
|
|
275
|
+
const agreement = await checkpoint("agreement", () => this.agreement({ capabilityId: analysisCapabilityId, operationId: paymentOperationId, sessionId, runId: input.runId, briefHash }));
|
|
276
|
+
const settlement = await checkpoint("settlement", () => this.settleAnalysis({ sessionId, operationId: paymentOperationId }));
|
|
277
|
+
const analysis = await checkpoint("delivery", async () => {
|
|
278
|
+
const value = await this.delivery({ sessionId, operationId: paymentOperationId });
|
|
279
|
+
return value.status === "delivered" ? value : fail("DELIVERY_PENDING", "analysis payment is finalized and seller delivery is still pending; resume the same research operation");
|
|
280
|
+
});
|
|
281
|
+
this.app.store.db.prepare("UPDATE remote_research_workflows SET completed_at=COALESCE(completed_at,?),updated_at=? WHERE operation_id=?").run(Date.now(), Date.now(), input.operationId);
|
|
282
|
+
return { operationId: input.operationId, state: "complete", replayed: Boolean(existing?.completedAt), selection: ranked, brief, agreement, settlement, analysis };
|
|
283
|
+
}
|
|
230
284
|
async events(runId) { return this.request(`/v1/events?runId=${encodeURIComponent(runId)}`); }
|
|
231
285
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface RoleHomeLease {
|
|
2
|
+
acquired: boolean;
|
|
3
|
+
ownerPid?: number;
|
|
4
|
+
release(): void;
|
|
5
|
+
}
|
|
6
|
+
/** Best-effort process lease. SQLite still owns data integrity; this prevents
|
|
7
|
+
* multiple long-running role workers from operating one role home at once. */
|
|
8
|
+
export declare function acquireRoleHomeLease(home: string): RoleHomeLease;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { closeSync, openSync, readFileSync, unlinkSync, writeSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
function active(pid) {
|
|
5
|
+
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
6
|
+
return false;
|
|
7
|
+
try {
|
|
8
|
+
process.kill(pid, 0);
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/** Best-effort process lease. SQLite still owns data integrity; this prevents
|
|
16
|
+
* multiple long-running role workers from operating one role home at once. */
|
|
17
|
+
export function acquireRoleHomeLease(home) {
|
|
18
|
+
const path = join(home, ".mcp.lock"), token = randomUUID();
|
|
19
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
20
|
+
try {
|
|
21
|
+
const fd = openSync(path, "wx", 0o600);
|
|
22
|
+
writeSync(fd, `${JSON.stringify({ pid: process.pid, token, startedAt: Date.now() })}\n`);
|
|
23
|
+
closeSync(fd);
|
|
24
|
+
let released = false;
|
|
25
|
+
return { acquired: true, release: () => {
|
|
26
|
+
if (released)
|
|
27
|
+
return;
|
|
28
|
+
released = true;
|
|
29
|
+
try {
|
|
30
|
+
const value = JSON.parse(readFileSync(path, "utf8"));
|
|
31
|
+
if (value.token === token)
|
|
32
|
+
unlinkSync(path);
|
|
33
|
+
}
|
|
34
|
+
catch { /* already removed */ }
|
|
35
|
+
} };
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
if (error.code !== "EEXIST")
|
|
39
|
+
throw error;
|
|
40
|
+
let ownerPid;
|
|
41
|
+
try {
|
|
42
|
+
ownerPid = Number(JSON.parse(readFileSync(path, "utf8")).pid);
|
|
43
|
+
}
|
|
44
|
+
catch { /* stale malformed lock */ }
|
|
45
|
+
if (ownerPid !== undefined && active(ownerPid))
|
|
46
|
+
return { acquired: false, ownerPid, release: () => { } };
|
|
47
|
+
try {
|
|
48
|
+
unlinkSync(path);
|
|
49
|
+
}
|
|
50
|
+
catch { /* another process won the cleanup race */ }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return { acquired: false, release: () => { } };
|
|
54
|
+
}
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { readFileSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { decodeSuiPrivateKey } from "@mysten/sui/cryptography";
|
|
4
|
+
import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
|
|
5
|
+
import { createCapabilityManifest, validateCapabilityPayload } from "./capability.js";
|
|
6
|
+
import { fail } from "./errors.js";
|
|
7
|
+
import { executeMandated, loadMandate } from "./mandate.js";
|
|
8
|
+
import { Phase3App } from "./phase3.js";
|
|
9
|
+
const ownerFile = (path) => {
|
|
10
|
+
const value = resolve(path);
|
|
11
|
+
if ((statSync(value).mode & 0o077) !== 0)
|
|
12
|
+
fail("SELLER_CONFIG_PERMISSIONS", `${value} must be owner-only`);
|
|
13
|
+
return value;
|
|
14
|
+
};
|
|
15
|
+
export async function packageHostedSeller(input) {
|
|
16
|
+
if (![input.home, input.spec, input.artifact, input.metadata, input.registration, input.bundle].every((value) => typeof value === "string" && value.length > 0))
|
|
17
|
+
fail("ARGUMENT_REQUIRED", "seller package requires --home, --spec, --artifact, --metadata, --registration, and --bundle");
|
|
18
|
+
const specPath = ownerFile(input.spec), artifactPath = ownerFile(input.artifact), metadataPath = ownerFile(input.metadata);
|
|
19
|
+
const artifact = readFileSync(artifactPath);
|
|
20
|
+
if (!artifact.length || artifact.length > 10 * 1024 * 1024)
|
|
21
|
+
fail("SELLER_ARTIFACT_INVALID", "hosted artifact must contain 1 byte to 10 MiB");
|
|
22
|
+
const spec = JSON.parse(readFileSync(specPath, "utf8"));
|
|
23
|
+
const metadata = JSON.parse(readFileSync(metadataPath, "utf8"));
|
|
24
|
+
const home = resolve(input.home), app = new Phase3App(home);
|
|
25
|
+
try {
|
|
26
|
+
if (app.profile.role !== "seller" || app.profile.mock || app.profile.verifierVersion !== 2)
|
|
27
|
+
fail("SELLER_POOL_VERSION_MISMATCH", "hosted packaging requires a separate live v2 seller role home");
|
|
28
|
+
const encoded = app.store.getSecret("live:sui-private-key")?.toString() ?? fail("LIVE_CREDENTIAL_MISSING", "seller signer is missing");
|
|
29
|
+
const decoded = decodeSuiPrivateKey(encoded);
|
|
30
|
+
if (decoded.scheme !== "ED25519")
|
|
31
|
+
fail("SIGNER_UNSUPPORTED", "seller requires an ED25519 signer");
|
|
32
|
+
const signer = Ed25519Keypair.fromSecretKey(decoded.secretKey), manifest = createCapabilityManifest({ ...spec, sellerId: signer.toSuiAddress() });
|
|
33
|
+
validateCapabilityPayload(manifest, "output", metadata);
|
|
34
|
+
const published = await executeMandated(app, loadMandate(home), "capability.publish", { "operation-id": `publish-${manifest.manifestHash}`, manifest });
|
|
35
|
+
const manifestBlobId = String(published.blobId ?? "");
|
|
36
|
+
if (!manifestBlobId)
|
|
37
|
+
fail("WALRUS_PUBLISH_FAILED", "capability publication omitted a Walrus blob ID");
|
|
38
|
+
const transportPublicKey = app.store.getSecret("identity:x25519-public")?.toString("base64url") ?? fail("IDENTITY_MISSING", "seller transport key is missing");
|
|
39
|
+
const sellerSignature = (await signer.signPersonalMessage(Buffer.from(`${manifest.manifestHash}:${transportPublicKey}`))).signature;
|
|
40
|
+
const registration = { manifest, manifestBlobId, transportPublicKey, sellerSignature };
|
|
41
|
+
const registrationPath = resolve(input.registration), bundlePath = resolve(input.bundle);
|
|
42
|
+
writeFileSync(registrationPath, `${JSON.stringify(registration, null, 2)}\n`, { mode: 0o600, flag: "wx" });
|
|
43
|
+
const bundle = { sellers: [{ sellerId: manifest.sellerId, home, capabilities: [{ registrationFile: registrationPath, artifactFile: artifactPath, outputMetadata: metadata, contentType: input.contentType ?? "application/octet-stream" }] }] };
|
|
44
|
+
writeFileSync(bundlePath, `${JSON.stringify(bundle, null, 2)}\n`, { mode: 0o600, flag: "wx" });
|
|
45
|
+
return { sellerId: manifest.sellerId, capabilityId: manifest.capabilityId, manifestHash: manifest.manifestHash, manifestBlobId, registrationFile: registrationPath, provisioningBundle: bundlePath, nextCommand: `supercuts-seller-v2-provision ${JSON.stringify(bundlePath)}` };
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
app.close();
|
|
49
|
+
}
|
|
50
|
+
}
|
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>;
|