@supercuts-walrus/backend 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +249 -2
  3. package/dist/audit.d.ts +52 -0
  4. package/dist/audit.js +157 -0
  5. package/dist/backend.d.ts +27 -0
  6. package/dist/backend.js +62 -0
  7. package/dist/buyer-setup.d.ts +6 -0
  8. package/dist/buyer-setup.js +241 -0
  9. package/dist/capability.d.ts +32 -0
  10. package/dist/capability.js +87 -0
  11. package/dist/claude.d.ts +44 -0
  12. package/dist/claude.js +98 -0
  13. package/dist/cli.d.ts +2 -0
  14. package/dist/cli.js +29 -0
  15. package/dist/config.d.ts +20 -0
  16. package/dist/config.js +58 -0
  17. package/dist/dashboard.d.ts +5 -0
  18. package/dist/dashboard.js +21 -0
  19. package/dist/deal-adapter.d.ts +3 -0
  20. package/dist/deal-adapter.js +39 -0
  21. package/dist/deal-engine.d.ts +62 -0
  22. package/dist/deal-engine.js +211 -0
  23. package/dist/deal-wire.d.ts +16 -0
  24. package/dist/deal-wire.js +65 -0
  25. package/dist/deal.d.ts +79 -0
  26. package/dist/deal.js +228 -0
  27. package/dist/demo.d.ts +2 -0
  28. package/dist/demo.js +449 -0
  29. package/dist/errors.d.ts +6 -0
  30. package/dist/errors.js +13 -0
  31. package/dist/events.d.ts +27 -0
  32. package/dist/events.js +68 -0
  33. package/dist/facilitator.d.ts +21 -0
  34. package/dist/facilitator.js +173 -0
  35. package/dist/finality.d.ts +2 -0
  36. package/dist/finality.js +27 -0
  37. package/dist/hosted-exit-cli.d.ts +2 -0
  38. package/dist/hosted-exit-cli.js +37 -0
  39. package/dist/hosted-exit.d.ts +24 -0
  40. package/dist/hosted-exit.js +70 -0
  41. package/dist/hosted-seller-import.d.ts +2 -0
  42. package/dist/hosted-seller-import.js +70 -0
  43. package/dist/hosted-seller-repository.d.ts +31 -0
  44. package/dist/hosted-seller-repository.js +214 -0
  45. package/dist/index.d.ts +37 -0
  46. package/dist/index.js +37 -0
  47. package/dist/indexer.d.ts +63 -0
  48. package/dist/indexer.js +60 -0
  49. package/dist/live-deal-ports.d.ts +41 -0
  50. package/dist/live-deal-ports.js +212 -0
  51. package/dist/live-index.d.ts +28 -0
  52. package/dist/live-index.js +149 -0
  53. package/dist/live-runtime.d.ts +82 -0
  54. package/dist/live-runtime.js +570 -0
  55. package/dist/live-seller-transport.d.ts +30 -0
  56. package/dist/live-seller-transport.js +169 -0
  57. package/dist/live-seller-verifier.d.ts +11 -0
  58. package/dist/live-seller-verifier.js +65 -0
  59. package/dist/mandate.d.ts +34 -0
  60. package/dist/mandate.js +178 -0
  61. package/dist/mcp-bootstrap.d.ts +2 -0
  62. package/dist/mcp-bootstrap.js +39 -0
  63. package/dist/mcp.d.ts +2 -0
  64. package/dist/mcp.js +187 -0
  65. package/dist/memory-worker.d.ts +2 -0
  66. package/dist/memory-worker.js +38 -0
  67. package/dist/memory.d.ts +15 -0
  68. package/dist/memory.js +17 -0
  69. package/dist/memwal.d.ts +35 -0
  70. package/dist/memwal.js +83 -0
  71. package/dist/negotiation.d.ts +23 -0
  72. package/dist/negotiation.js +58 -0
  73. package/dist/notes.d.ts +11 -0
  74. package/dist/notes.js +26 -0
  75. package/dist/onboarding-doctor.d.ts +1 -0
  76. package/dist/onboarding-doctor.js +76 -0
  77. package/dist/phase3.d.ts +100 -0
  78. package/dist/phase3.js +1144 -0
  79. package/dist/procurement.d.ts +8 -0
  80. package/dist/procurement.js +19 -0
  81. package/dist/protocol-engine.d.ts +121 -0
  82. package/dist/protocol-engine.js +149 -0
  83. package/dist/prover.d.ts +28 -0
  84. package/dist/prover.js +70 -0
  85. package/dist/relayer.d.ts +32 -0
  86. package/dist/relayer.js +43 -0
  87. package/dist/remote-buyer.d.ts +67 -0
  88. package/dist/remote-buyer.js +231 -0
  89. package/dist/resource-server.d.ts +2 -0
  90. package/dist/resource-server.js +24 -0
  91. package/dist/resource.d.ts +22 -0
  92. package/dist/resource.js +55 -0
  93. package/dist/seam.d.ts +2 -0
  94. package/dist/seam.js +117 -0
  95. package/dist/seller-catalog-generate.d.ts +2 -0
  96. package/dist/seller-catalog-generate.js +68 -0
  97. package/dist/seller-database.d.ts +41 -0
  98. package/dist/seller-database.js +80 -0
  99. package/dist/seller-directory-cli.d.ts +2 -0
  100. package/dist/seller-directory-cli.js +24 -0
  101. package/dist/seller-directory.d.ts +5 -0
  102. package/dist/seller-directory.js +50 -0
  103. package/dist/seller-live-cli.d.ts +2 -0
  104. package/dist/seller-live-cli.js +53 -0
  105. package/dist/seller-provision.d.ts +2 -0
  106. package/dist/seller-provision.js +84 -0
  107. package/dist/seller-register-cli.d.ts +2 -0
  108. package/dist/seller-register-cli.js +40 -0
  109. package/dist/seller-runtime.d.ts +60 -0
  110. package/dist/seller-runtime.js +96 -0
  111. package/dist/seller-service.d.ts +34 -0
  112. package/dist/seller-service.js +416 -0
  113. package/dist/seller-state.d.ts +19 -0
  114. package/dist/seller-state.js +60 -0
  115. package/dist/seller-stdio.d.ts +6 -0
  116. package/dist/seller-stdio.js +29 -0
  117. package/dist/seller-v2-provision.d.ts +2 -0
  118. package/dist/seller-v2-provision.js +71 -0
  119. package/dist/seller-watch.d.ts +2 -0
  120. package/dist/seller-watch.js +45 -0
  121. package/dist/settlement.d.ts +95 -0
  122. package/dist/settlement.js +141 -0
  123. package/dist/store.d.ts +36 -0
  124. package/dist/store.js +193 -0
  125. package/dist/sui.d.ts +49 -0
  126. package/dist/sui.js +77 -0
  127. package/dist/tatum.d.ts +17 -0
  128. package/dist/tatum.js +83 -0
  129. package/dist/transcript.d.ts +31 -0
  130. package/dist/transcript.js +52 -0
  131. package/dist/v2-exit-relayer.d.ts +34 -0
  132. package/dist/v2-exit-relayer.js +102 -0
  133. package/dist/v2-exit-worker.d.ts +2 -0
  134. package/dist/v2-exit-worker.js +97 -0
  135. package/dist/v2-exits.d.ts +30 -0
  136. package/dist/v2-exits.js +78 -0
  137. package/dist/v2-prefund.d.ts +19 -0
  138. package/dist/v2-prefund.js +43 -0
  139. package/dist/v2-relayer-cli.d.ts +2 -0
  140. package/dist/v2-relayer-cli.js +39 -0
  141. package/dist/wallet.d.ts +32 -0
  142. package/dist/wallet.js +90 -0
  143. package/dist/workflow-memory.d.ts +22 -0
  144. package/dist/workflow-memory.js +49 -0
  145. package/dist/x402-resource-service.d.ts +34 -0
  146. package/dist/x402-resource-service.js +154 -0
  147. package/dist/x402.d.ts +46 -0
  148. package/dist/x402.js +136 -0
  149. package/package.json +60 -3
@@ -0,0 +1,27 @@
1
+ import { DeploymentConfig } from "./config.js";
2
+ import { PoolEvent, PoolIndex } from "./indexer.js";
3
+ import { RelayAdmission, RelayRequest } from "./relayer.js";
4
+ import { RoleStore } from "./store.js";
5
+ import { MessageEnvelope } from "./transcript.js";
6
+ import { EvidenceReader, PrivateWallet, SettlementExpectation } from "./wallet.js";
7
+ /**
8
+ * Role-local façade. Network processes inject transport/proof adapters; this
9
+ * class keeps security-sensitive lifecycle decisions in one reusable backend.
10
+ */
11
+ export declare class SupercutsBackend {
12
+ readonly store: RoleStore;
13
+ readonly poolIndex: PoolIndex;
14
+ readonly relay: RelayAdmission;
15
+ readonly wallet: PrivateWallet;
16
+ private readonly pin;
17
+ constructor(config: DeploymentConfig, store: RoleStore);
18
+ applyObservedEvents(events: PoolEvent[], poolState: {
19
+ root: string;
20
+ nextLeaf: bigint;
21
+ }): void;
22
+ verifyTranscript(messages: MessageEnvelope[]): Promise<string>;
23
+ preparePrivateTransfer(operationId: string, payloadHash: string, inputCommitment: string, expected: SettlementExpectation, proof: Uint8Array): void;
24
+ markBroadcast(operationId: string, digest: string): void;
25
+ reconcilePrivateTransfer(operationId: string, read: EvidenceReader): Promise<import("./store.js").StoredOperation>;
26
+ admitRelay(request: RelayRequest, verifyProof: (statement: Uint8Array, proof: Uint8Array) => Promise<boolean>): Promise<void>;
27
+ }
@@ -0,0 +1,62 @@
1
+ import { isValidPersonalMessageSignature } from "@mysten/sui/verify";
2
+ import { applyPoolEvent, assertPoolState, createPoolIndex } from "./indexer.js";
3
+ import { RelayAdmission } from "./relayer.js";
4
+ import { validateTranscript } from "./transcript.js";
5
+ import { PrivateWallet } from "./wallet.js";
6
+ /**
7
+ * Role-local façade. Network processes inject transport/proof adapters; this
8
+ * class keeps security-sensitive lifecycle decisions in one reusable backend.
9
+ */
10
+ export class SupercutsBackend {
11
+ store;
12
+ poolIndex;
13
+ relay;
14
+ wallet;
15
+ pin;
16
+ constructor(config, store) {
17
+ this.store = store;
18
+ this.poolIndex = createPoolIndex(config.initialRoot.replace(/^0x/, ""), config.deploymentCheckpoint);
19
+ this.pin = { packageId: config.packageId, poolId: config.poolId, coinType: config.coinType, verifierVersion: config.verifierVersion };
20
+ this.relay = new RelayAdmission(store, this.pin, (address, bytes, signature) => isValidPersonalMessageSignature(bytes, signature, { address }),
21
+ // The caller must inject the pinned Groth16 verifier before admitting a
22
+ // live relay request. A missing verifier fails closed.
23
+ async () => false);
24
+ this.wallet = new PrivateWallet(store);
25
+ }
26
+ applyObservedEvents(events, poolState) {
27
+ const candidate = {
28
+ root: this.poolIndex.root, nextLeaf: this.poolIndex.nextLeaf, checkpoint: this.poolIndex.checkpoint,
29
+ spent: new Set(this.poolIndex.spent), deals: new Set(this.poolIndex.deals), acceptedRoots: new Set(this.poolIndex.acceptedRoots),
30
+ ...(this.poolIndex.lastPosition ? { lastPosition: { ...this.poolIndex.lastPosition } } : {}),
31
+ };
32
+ for (const event of [...events].sort((a, b) => {
33
+ if (a.checkpoint !== b.checkpoint)
34
+ return a.checkpoint < b.checkpoint ? -1 : 1;
35
+ if (a.transactionIndex !== b.transactionIndex)
36
+ return a.transactionIndex < b.transactionIndex ? -1 : 1;
37
+ return a.eventIndex - b.eventIndex;
38
+ }))
39
+ applyPoolEvent(candidate, event);
40
+ assertPoolState(candidate, poolState);
41
+ this.poolIndex.root = candidate.root;
42
+ this.poolIndex.nextLeaf = candidate.nextLeaf;
43
+ this.poolIndex.checkpoint = candidate.checkpoint;
44
+ this.poolIndex.spent = candidate.spent;
45
+ this.poolIndex.deals = candidate.deals;
46
+ this.poolIndex.acceptedRoots = candidate.acceptedRoots;
47
+ if (candidate.lastPosition)
48
+ this.poolIndex.lastPosition = candidate.lastPosition;
49
+ }
50
+ async verifyTranscript(messages) {
51
+ return validateTranscript(messages, (address, bytes, signature) => isValidPersonalMessageSignature(bytes, signature, { address }));
52
+ }
53
+ preparePrivateTransfer(operationId, payloadHash, inputCommitment, expected, proof) {
54
+ this.wallet.prepare(operationId, payloadHash, inputCommitment, expected, proof);
55
+ }
56
+ markBroadcast(operationId, digest) { this.wallet.recordBroadcast(operationId, digest); }
57
+ reconcilePrivateTransfer(operationId, read) { return this.wallet.reconcile(operationId, read); }
58
+ async admitRelay(request, verifyProof) {
59
+ const admission = new RelayAdmission(this.store, this.pin, (address, bytes, signature) => isValidPersonalMessageSignature(bytes, signature, { address }), verifyProof);
60
+ return admission.admit(request);
61
+ }
62
+ }
@@ -0,0 +1,6 @@
1
+ export declare function setupBuyer(home: string, configPath: string): Record<string, unknown>;
2
+ export declare function promptBuyerSetup(home: string): Promise<Record<string, unknown>>;
3
+ export declare function createBuyerSession(home: string): Record<string, unknown>;
4
+ export declare function renewBuyer(home: string, configPath: string): Record<string, unknown>;
5
+ export declare function approveBuyerSeller(home: string, capabilityId: string): Promise<Record<string, unknown>>;
6
+ export declare function addBuyerPrivacyRule(home: string, capabilityId: string): Promise<Record<string, unknown>>;
@@ -0,0 +1,241 @@
1
+ import { chmodSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { dirname } from "node:path";
5
+ import { randomUUID } from "node:crypto";
6
+ import { createInterface } from "node:readline/promises";
7
+ import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
8
+ import { decodeSuiPrivateKey } from "@mysten/sui/cryptography";
9
+ import { fail } from "./errors.js";
10
+ import { Phase3App, initialize } from "./phase3.js";
11
+ import { loadMandate } from "./mandate.js";
12
+ import { LiveDealPorts } from "./live-deal-ports.js";
13
+ import { verifySignedCapability } from "./capability.js";
14
+ import { WalrusDealBlobs } from "./deal-wire.js";
15
+ import { canonicalJson } from "./settlement.js";
16
+ import { validateDirectoryOrigin } from "./seller-directory.js";
17
+ function ownerFile(path) {
18
+ if ((statSync(path).mode & 0o077) !== 0)
19
+ fail("SETUP_PERMISSIONS", `${path} must be owner-only`);
20
+ }
21
+ function amount(value, name) {
22
+ if (typeof value !== "string" || !/^[1-9][0-9]*$/.test(value))
23
+ fail("SETUP_INVALID", `${name} must be a positive base-unit string`);
24
+ return value;
25
+ }
26
+ function origins(values) {
27
+ if (!Array.isArray(values) || !values.length || values.length > 32)
28
+ fail("SETUP_INVALID", "sellerOrigins requires 1..32 origins");
29
+ return values.map((value) => {
30
+ const url = new URL(String(value));
31
+ 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)
32
+ fail("SETUP_INVALID", "seller origin must be HTTPS or loopback HTTP without path or credentials");
33
+ return url.origin;
34
+ });
35
+ }
36
+ function validateConfig(config) {
37
+ amount(config.perDealCeiling, "perDealCeiling");
38
+ amount(config.totalBudget, "totalBudget");
39
+ if (BigInt(config.totalBudget) < BigInt(config.perDealCeiling))
40
+ fail("SETUP_INVALID", "totalBudget must cover one permitted deal");
41
+ if (!Number.isSafeInteger(config.expiresAt) || config.expiresAt <= Date.now())
42
+ fail("SETUP_INVALID", "expiresAt must be a future Unix millisecond timestamp");
43
+ if (!Array.isArray(config.allowedCounterparties ?? []) || (config.allowedCounterparties ?? []).some((item) => typeof item !== "string" || !/^0x[0-9a-f]{64}$/i.test(item)))
44
+ fail("SETUP_INVALID", "allowedCounterparties requires seller Sui addresses");
45
+ if (config.sellerOrigins?.length)
46
+ origins(config.sellerOrigins);
47
+ if (!config.sellerOrigins?.length && !config.directoryUrl)
48
+ fail("SETUP_INVALID", "configure a directoryUrl or sellerOrigins");
49
+ if (config.directoryUrl)
50
+ origins([config.directoryUrl]);
51
+ for (const key of ["deploymentConfig", "tatumKeyFile", "writeGrpc", "tatumGrpc", "walrusPublisher", "walrusAggregator", "memoryConfigFile"])
52
+ if (typeof config[key] !== "string" || !config[key])
53
+ fail("SETUP_INVALID", `${key} is required`);
54
+ for (const path of [config.tatumKeyFile, config.memoryConfigFile, ...(config.suiKeyFile ? [config.suiKeyFile] : [])])
55
+ ownerFile(resolve(path));
56
+ const deployment = JSON.parse(readFileSync(resolve(config.deploymentConfig), "utf8"));
57
+ if (deployment.network !== "testnet" || deployment.verifierVersion !== 2)
58
+ fail("SETUP_INVALID", "buyer setup requires the pinned v2 Testnet deployment");
59
+ if (config.allowPublicX402 !== undefined && typeof config.allowPublicX402 !== "boolean")
60
+ fail("SETUP_INVALID", "allowPublicX402 must be boolean");
61
+ for (const list of [config.allowedCapabilityIds, config.allowedCapabilityClasses])
62
+ if (list !== undefined && (!Array.isArray(list) || list.some((item) => typeof item !== "string" || !item)))
63
+ fail("SETUP_INVALID", "capability allowlists must contain nonempty strings");
64
+ return config;
65
+ }
66
+ function readConfig(path) { ownerFile(path); return validateConfig(JSON.parse(readFileSync(path, "utf8"))); }
67
+ function writePolicy(home, config) {
68
+ const path = join(home, "mandate.json"), mandate = loadMandate(home);
69
+ const base = { ...mandate };
70
+ delete base.allowedCapabilityIds;
71
+ delete base.allowedCapabilityClasses;
72
+ const next = { ...base, version: "supercuts.mandate.v2", allowedCommands: [...new Set([...mandate.allowedCommands, "deal.execute", "deal.get", "deal.resume", "deal.evidence", "deal.delivery", "deal.catalog", "deal.policy", "settlement.plan", "memory.inventory"])], allowedCounterparties: config.allowedCounterparties ?? [], perDealCeiling: config.perDealCeiling, totalRunBudget: config.totalBudget, expiresAt: config.expiresAt, requiredPrivacy: "any", requireSettlementPlan: true, allowedRails: config.allowPublicX402 === true ? ["shielded", "x402"] : ["shielded"], allowPublicX402: config.allowPublicX402 === true, ...(config.allowedCapabilityIds ? { allowedCapabilityIds: config.allowedCapabilityIds } : {}), ...(config.allowedCapabilityClasses ? { allowedCapabilityClasses: config.allowedCapabilityClasses } : {}) };
73
+ writeFileSync(path, `${JSON.stringify(next, null, 2)}\n`, { mode: 0o600 });
74
+ chmodSync(path, 0o600);
75
+ writeFileSync(join(home, "deal-sellers.json"), `${JSON.stringify(config.directoryUrl ? { directoryUrl: origins([config.directoryUrl])[0], origins: config.sellerOrigins?.length ? origins(config.sellerOrigins) : [] } : origins(config.sellerOrigins ?? []), null, 2)}\n`, { mode: 0o600 });
76
+ const profilePath = join(home, "profile.json"), profile = JSON.parse(readFileSync(profilePath, "utf8"));
77
+ profile.policy.allowedCounterparties = [...next.allowedCounterparties];
78
+ profile.policy.maxAmount = config.perDealCeiling;
79
+ profile.policy.expiresAt = config.expiresAt;
80
+ writeFileSync(profilePath, `${JSON.stringify(profile, null, 2)}\n`, { mode: 0o600 });
81
+ chmodSync(profilePath, 0o600);
82
+ }
83
+ export function setupBuyer(home, configPath) { return setupBuyerConfig(home, readConfig(resolve(configPath))); }
84
+ export async function promptBuyerSetup(home) {
85
+ if (!process.stdin.isTTY)
86
+ fail("SETUP_INPUT_REQUIRED", "interactive setup requires a terminal; use --config with an owner-only JSON file");
87
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
88
+ try {
89
+ const ask = async (label) => (await rl.question(`${label}: `)).trim();
90
+ const days = Number(await ask("Policy duration in days"));
91
+ if (!Number.isSafeInteger(days) || days < 1 || days > 365)
92
+ fail("SETUP_INVALID", "policy duration must be 1..365 days");
93
+ const config = {
94
+ deploymentConfig: await ask("Pinned Testnet deployment config path"), tatumKeyFile: await ask("Tatum key file path"),
95
+ writeGrpc: await ask("Sui write gRPC URL"), tatumGrpc: await ask("Tatum read gRPC URL"),
96
+ walrusPublisher: await ask("Walrus publisher URL"), walrusAggregator: await ask("Walrus aggregator URL"),
97
+ memoryConfigFile: await ask("Owner-only Walrus Memory config path"),
98
+ directoryUrl: await ask("Shared directory origin (blank for direct sellers)"),
99
+ sellerOrigins: (await ask("Additional seller HTTPS origins, comma separated (or blank)")).split(",").map((item) => item.trim()).filter(Boolean),
100
+ allowedCounterparties: (await ask("Initially approved seller Sui addresses, comma separated (or blank)")).split(",").map((item) => item.trim()).filter(Boolean),
101
+ perDealCeiling: await ask("Maximum per deal in MIST"), totalBudget: await ask("Lifetime cumulative budget in MIST"),
102
+ expiresAt: Date.now() + days * 24 * 60 * 60 * 1000, allowPublicX402: (await ask("Allow public x402 when requested? yes/no")).toLowerCase() === "yes",
103
+ };
104
+ if (!config.directoryUrl)
105
+ delete config.directoryUrl;
106
+ return setupBuyerConfig(home, validateConfig(config));
107
+ }
108
+ finally {
109
+ rl.close();
110
+ }
111
+ }
112
+ function setupBuyerConfig(home, config) {
113
+ if (existsSync(join(home, "profile.json")))
114
+ fail("PROFILE_EXISTS", "buyer home already exists; use setup renew for a standing policy update");
115
+ mkdirSync(home, { recursive: true, mode: 0o700 });
116
+ chmodSync(home, 0o700);
117
+ const keyPath = config.suiKeyFile ? resolve(config.suiKeyFile) : join(home, "buyer.sui.key");
118
+ if (!config.suiKeyFile)
119
+ writeFileSync(keyPath, Ed25519Keypair.generate().getSecretKey(), { mode: 0o600, flag: "wx" });
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) } : {}) });
122
+ writePolicy(home, config);
123
+ const session = createBuyerSession(home);
124
+ const decoded = decodeSuiPrivateKey(readFileSync(keyPath, "utf8").trim());
125
+ if (decoded.scheme !== "ED25519")
126
+ fail("SETUP_INVALID", "buyer key must use ED25519");
127
+ return { home, address: Ed25519Keypair.fromSecretKey(decoded.secretKey).toSuiAddress(), ...session, policy: "configured" };
128
+ }
129
+ export function createBuyerSession(home) {
130
+ if (!existsSync(join(home, "profile.json")))
131
+ fail("SETUP_REQUIRED", "initialize the buyer before creating a Codex session");
132
+ const runId = randomUUID(), eventPath = join(home, `events-${runId}.jsonl`), mcpScript = join(dirname(fileURLToPath(import.meta.url)), "mcp-bootstrap.js");
133
+ const mcp = `[mcp_servers.supercuts-buyer]\ncommand = ${JSON.stringify(process.execPath)}\nargs = ${JSON.stringify([mcpScript])}\n[mcp_servers.supercuts-buyer.env]\nSUPERCUTS_HOME = ${JSON.stringify(home)}\nSUPERCUTS_RUN_ID = ${JSON.stringify(runId)}\nSUPERCUTS_EVENT_PATH = ${JSON.stringify(eventPath)}\n`;
134
+ const mcpConfig = join(home, "buyer-codex.toml");
135
+ writeFileSync(mcpConfig, mcp, { mode: 0o600 });
136
+ chmodSync(mcpConfig, 0o600);
137
+ return { runId, eventPath, mcpConfig };
138
+ }
139
+ export function renewBuyer(home, configPath) {
140
+ const config = readConfig(resolve(configPath));
141
+ const prior = loadMandate(home);
142
+ if (prior.role !== "buyer" || prior.version !== "supercuts.mandate.v2")
143
+ fail("SETUP_INVALID", "renewal requires an initialized v2 buyer home");
144
+ config.allowedCounterparties ??= [...prior.allowedCounterparties];
145
+ if (config.allowedCapabilityIds === undefined && prior.allowedCapabilityIds)
146
+ config.allowedCapabilityIds = [...prior.allowedCapabilityIds];
147
+ if (config.allowedCapabilityClasses === undefined && prior.allowedCapabilityClasses)
148
+ config.allowedCapabilityClasses = [...prior.allowedCapabilityClasses];
149
+ const spent = (readReserved(home));
150
+ if (BigInt(config.totalBudget) < spent)
151
+ fail("SETUP_INVALID", "renewed lifetime budget is below already reserved spend");
152
+ writePolicy(home, config);
153
+ return { home, policy: "renewed", reservedMist: spent.toString(), remainingMist: (BigInt(config.totalBudget) - spent).toString() };
154
+ }
155
+ export async function approveBuyerSeller(home, capabilityId) {
156
+ if (!capabilityId)
157
+ fail("ARGUMENT_REQUIRED", "--capability-id is required");
158
+ const mandate = loadMandate(home);
159
+ if (mandate.role !== "buyer" || mandate.version !== "supercuts.mandate.v2")
160
+ fail("SETUP_INVALID", "seller approval requires a v2 buyer home");
161
+ const sourcePath = join(home, "deal-sellers.json");
162
+ ownerFile(sourcePath);
163
+ const source = JSON.parse(readFileSync(sourcePath, "utf8"));
164
+ if (!source.directoryUrl)
165
+ fail("SELLER_DIRECTORY_UNAVAILABLE", "buyer has no configured shared directory");
166
+ const matches = [];
167
+ for (let offset = 0; offset <= 100_000;) {
168
+ const response = await fetch(new URL(`/v2/directory/catalog?offset=${offset}`, source.directoryUrl), { redirect: "error", signal: AbortSignal.timeout(30_000) });
169
+ if (!response.ok)
170
+ fail("SELLER_DIRECTORY_UNAVAILABLE", `directory returned HTTP ${response.status}`);
171
+ const catalog = await response.json();
172
+ if (!Array.isArray(catalog.capabilities) || catalog.capabilities.length > 100)
173
+ fail("SELLER_DIRECTORY_INVALID", "directory catalog is invalid");
174
+ matches.push(...(catalog.capabilities ?? []).filter((item) => item.manifest?.capabilityId === capabilityId));
175
+ if (catalog.nextOffset === undefined)
176
+ break;
177
+ if (catalog.nextOffset !== offset + 100 || catalog.capabilities?.length !== 100)
178
+ fail("SELLER_DIRECTORY_INVALID", "directory pagination is invalid");
179
+ offset = catalog.nextOffset;
180
+ }
181
+ if (matches.length !== 1)
182
+ fail("SELLER_DIRECTORY_INVALID", "capability must resolve to exactly one live registration");
183
+ const registration = matches[0];
184
+ await verifySignedCapability(registration);
185
+ const endpoint = await validateDirectoryOrigin(registration.manifest.endpoint);
186
+ const sellerResponse = await fetch(new URL("/v2/catalog", endpoint.origin), { redirect: "error", signal: AbortSignal.timeout(15_000) });
187
+ if (!sellerResponse.ok)
188
+ fail("SELLER_CATALOG_INVALID", "seller endpoint is unavailable");
189
+ const served = await sellerResponse.json();
190
+ if (!Array.isArray(served.capabilities) || !served.capabilities.some((item) => canonicalJson(item) === canonicalJson(registration)))
191
+ fail("SELLER_CATALOG_INVALID", "seller no longer serves this signed capability");
192
+ const app = new Phase3App(home);
193
+ try {
194
+ const readback = await new WalrusDealBlobs(app).read(registration.manifestBlobId);
195
+ if (canonicalJson(JSON.parse(Buffer.from(readback).toString())) !== canonicalJson(registration.manifest))
196
+ fail("CAPABILITY_BLOB_MISMATCH", "directory manifest differs from Walrus");
197
+ }
198
+ finally {
199
+ app.close();
200
+ }
201
+ const path = join(home, "mandate.json"), profilePath = join(home, "profile.json");
202
+ const updated = { ...mandate, allowedCounterparties: [...new Set([...mandate.allowedCounterparties, registration.manifest.sellerId])] };
203
+ if (updated.allowedCapabilityIds)
204
+ updated.allowedCapabilityIds = [...new Set([...updated.allowedCapabilityIds, capabilityId])];
205
+ const profile = JSON.parse(readFileSync(profilePath, "utf8"));
206
+ profile.policy.allowedCounterparties = updated.allowedCounterparties;
207
+ writeFileSync(path, `${JSON.stringify(updated, null, 2)}\n`, { mode: 0o600 });
208
+ writeFileSync(profilePath, `${JSON.stringify(profile, null, 2)}\n`, { mode: 0o600 });
209
+ return { capabilityId, sellerId: registration.manifest.sellerId, approved: true };
210
+ }
211
+ export async function addBuyerPrivacyRule(home, capabilityId) {
212
+ const app = new Phase3App(home);
213
+ try {
214
+ if (app.profile.role !== "buyer" || app.profile.mock)
215
+ fail("LIVE_PROFILE_REQUIRED", "privacy rules require a live buyer home");
216
+ const mandate = loadMandate(home);
217
+ const matches = (await new LiveDealPorts(app, mandate).catalog()).filter(({ registration }) => registration.manifest.capabilityId === capabilityId);
218
+ if (matches.length !== 1)
219
+ fail("POLICY_CAPABILITY_INVALID", "privacy rule requires one signed seller capability");
220
+ const manifest = matches[0].registration.manifest;
221
+ const memory = { version: "supercuts.settlement-memory.v1", kind: "privacy_rule", scope: { capabilityId: manifest.capabilityId, counterparty: manifest.sellerId, network: "sui:testnet", asset: manifest.asset }, createdAt: Date.now(), signal: { requiresShielded: true } };
222
+ const args = { "operation-id": `owner-privacy-${manifest.manifestHash}`, memory: Buffer.from(JSON.stringify(memory)).toString("base64url") };
223
+ const approval = app.approve(app.preview("memory.policy", args).challengeId);
224
+ const result = (await app.execute("memory.policy", { ...args, "approval-token": approval.token })).data;
225
+ return { capabilityId, sellerId: manifest.sellerId, blobId: result.blobId, kind: result.kind, provenance: result.provenance };
226
+ }
227
+ finally {
228
+ app.close();
229
+ }
230
+ }
231
+ function readReserved(home) {
232
+ const app = new Phase3App(home);
233
+ try {
234
+ if (!app.store.db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='mandate_operations'").get())
235
+ return 0n;
236
+ return app.store.db.prepare("SELECT amount FROM mandate_operations").all().reduce((sum, row) => sum + BigInt(row.amount), 0n);
237
+ }
238
+ finally {
239
+ app.close();
240
+ }
241
+ }
@@ -0,0 +1,32 @@
1
+ export type CapabilityRail = "x402" | "shielded";
2
+ export type JsonSchema = Readonly<Record<string, unknown>>;
3
+ export declare function validateCapabilityPayload(manifest: CapabilityManifest, direction: "input" | "output", value: unknown): void;
4
+ export interface SignedCapabilityRegistration {
5
+ manifest: CapabilityManifest;
6
+ manifestBlobId: string;
7
+ transportPublicKey: string;
8
+ sellerSignature: string;
9
+ }
10
+ export declare function verifySignedCapability(registration: SignedCapabilityRegistration): Promise<void>;
11
+ /** Framework-neutral service advertisement. This is not an A2A protocol claim. */
12
+ export interface CapabilityManifest {
13
+ protocol: "supercuts.capability.v1";
14
+ capabilityId: string;
15
+ sellerId: string;
16
+ capability: string;
17
+ description: string;
18
+ inputSchema: JsonSchema;
19
+ outputSchema: JsonSchema;
20
+ supportedRails: CapabilityRail[];
21
+ priceBand: {
22
+ minimum: string;
23
+ maximum: string;
24
+ };
25
+ asset: string;
26
+ endpoint: string;
27
+ expiresAt: number;
28
+ manifestHash: string;
29
+ }
30
+ export declare function capabilityManifestHash(manifest: Omit<CapabilityManifest, "manifestHash">): string;
31
+ export declare function createCapabilityManifest(input: Omit<CapabilityManifest, "protocol" | "manifestHash">): CapabilityManifest;
32
+ export declare function validateCapabilityManifest(value: unknown, now?: number): asserts value is CapabilityManifest;
@@ -0,0 +1,87 @@
1
+ import { createHash } from "node:crypto";
2
+ import { isValidPersonalMessageSignature } from "@mysten/sui/verify";
3
+ import { Ajv } from "ajv";
4
+ import { fail } from "./errors.js";
5
+ const schemaCompiler = new Ajv({ strict: true, allErrors: false });
6
+ const compiledSchemas = new Map();
7
+ export function validateCapabilityPayload(manifest, direction, value) {
8
+ validateCapabilityManifest(manifest);
9
+ let compiled = compiledSchemas.get(manifest.manifestHash);
10
+ if (!compiled) {
11
+ try {
12
+ compiled = { input: schemaCompiler.compile(manifest.inputSchema), output: schemaCompiler.compile(manifest.outputSchema) };
13
+ }
14
+ catch (error) {
15
+ return fail("CAPABILITY_SCHEMA_INVALID", "signed capability contains an invalid JSON Schema", error);
16
+ }
17
+ compiledSchemas.set(manifest.manifestHash, compiled);
18
+ }
19
+ if (!compiled[direction](value))
20
+ fail("CAPABILITY_PAYLOAD_INVALID", `${direction} does not match the signed capability schema`);
21
+ }
22
+ export async function verifySignedCapability(registration) {
23
+ validateCapabilityManifest(registration.manifest);
24
+ if (typeof registration.manifestBlobId !== "string" || !registration.manifestBlobId || typeof registration.transportPublicKey !== "string" || !registration.transportPublicKey || typeof registration.sellerSignature !== "string" || !registration.sellerSignature)
25
+ fail("SELLER_SIGNATURE_INVALID", "signed capability registration is incomplete");
26
+ const signed = Buffer.from(`${registration.manifest.manifestHash}:${registration.transportPublicKey}`);
27
+ if (!(await isValidPersonalMessageSignature(signed, registration.sellerSignature, { address: registration.manifest.sellerId })))
28
+ fail("SELLER_SIGNATURE_INVALID", "seller did not sign the capability and transport key");
29
+ }
30
+ function canonical(value) {
31
+ if (Array.isArray(value))
32
+ return `[${value.map(canonical).join(",")}]`;
33
+ if (value && typeof value === "object")
34
+ return `{${Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => `${JSON.stringify(key)}:${canonical(item)}`).join(",")}}`;
35
+ const encoded = JSON.stringify(value);
36
+ if (encoded === undefined)
37
+ fail("CAPABILITY_INVALID", "capability manifests must be JSON-compatible");
38
+ return encoded;
39
+ }
40
+ export function capabilityManifestHash(manifest) {
41
+ return createHash("sha256").update(canonical({ domain: "supercuts-capability-v1", ...manifest })).digest("hex");
42
+ }
43
+ export function createCapabilityManifest(input) {
44
+ const unsigned = { protocol: "supercuts.capability.v1", ...input };
45
+ const manifest = { ...unsigned, manifestHash: capabilityManifestHash(unsigned) };
46
+ validateCapabilityManifest(manifest);
47
+ return manifest;
48
+ }
49
+ export function validateCapabilityManifest(value, now = Date.now()) {
50
+ if (!value || typeof value !== "object" || Array.isArray(value))
51
+ fail("CAPABILITY_INVALID", "capability manifest must be an object");
52
+ const manifest = value;
53
+ const allowed = new Set(["protocol", "capabilityId", "sellerId", "capability", "description", "inputSchema", "outputSchema", "supportedRails", "priceBand", "asset", "endpoint", "expiresAt", "manifestHash"]);
54
+ if (Object.keys(manifest).some((key) => !allowed.has(key)))
55
+ fail("CAPABILITY_INVALID", "capability manifest contains unknown fields");
56
+ if (manifest.protocol !== "supercuts.capability.v1")
57
+ fail("CAPABILITY_VERSION_UNSUPPORTED", "capability protocol must be supercuts.capability.v1");
58
+ for (const field of ["capabilityId", "sellerId", "capability", "description", "asset", "endpoint", "manifestHash"]) {
59
+ const item = manifest[field];
60
+ if (typeof item !== "string" || !item || item.length > (field === "description" ? 4096 : 1024))
61
+ fail("CAPABILITY_INVALID", `${field} is missing or too long`);
62
+ }
63
+ if (!manifest.inputSchema || typeof manifest.inputSchema !== "object" || Array.isArray(manifest.inputSchema) || !manifest.outputSchema || typeof manifest.outputSchema !== "object" || Array.isArray(manifest.outputSchema))
64
+ fail("CAPABILITY_SCHEMA_INVALID", "inputSchema and outputSchema must be JSON Schema objects");
65
+ if (Buffer.byteLength(canonical(manifest.inputSchema)) + Buffer.byteLength(canonical(manifest.outputSchema)) > 64 * 1024)
66
+ fail("CAPABILITY_SCHEMA_INVALID", "capability schemas exceed 64 KiB");
67
+ if (!Array.isArray(manifest.supportedRails) || !manifest.supportedRails.length || new Set(manifest.supportedRails).size !== manifest.supportedRails.length || manifest.supportedRails.some((rail) => rail !== "x402" && rail !== "shielded"))
68
+ fail("CAPABILITY_RAIL_INVALID", "supportedRails must contain unique supported rails");
69
+ if (!manifest.priceBand || !/^(?:0|[1-9][0-9]*)$/.test(manifest.priceBand.minimum) || !/^(?:0|[1-9][0-9]*)$/.test(manifest.priceBand.maximum) || BigInt(manifest.priceBand.minimum) > BigInt(manifest.priceBand.maximum))
70
+ fail("CAPABILITY_PRICE_INVALID", "priceBand must contain ordered unsigned integer strings");
71
+ if (!Number.isSafeInteger(manifest.expiresAt) || manifest.expiresAt <= now)
72
+ fail("CAPABILITY_EXPIRED", "capability manifest is expired");
73
+ let endpoint;
74
+ try {
75
+ endpoint = new URL(manifest.endpoint);
76
+ }
77
+ catch (error) {
78
+ return fail("CAPABILITY_ENDPOINT_INVALID", "capability endpoint must be an absolute URL", error);
79
+ }
80
+ if (endpoint.protocol !== "https:" && !(endpoint.protocol === "http:" && ["127.0.0.1", "localhost", "::1"].includes(endpoint.hostname)))
81
+ fail("CAPABILITY_ENDPOINT_INVALID", "capability endpoint must use HTTPS except on loopback");
82
+ if (endpoint.username || endpoint.password)
83
+ fail("CAPABILITY_ENDPOINT_INVALID", "capability endpoint cannot contain credentials");
84
+ const { manifestHash, ...unsigned } = manifest;
85
+ if (!/^[0-9a-f]{64}$/.test(manifestHash) || capabilityManifestHash(unsigned) !== manifestHash)
86
+ fail("CAPABILITY_HASH_MISMATCH", "capability manifest hash does not match its canonical contents");
87
+ }
@@ -0,0 +1,44 @@
1
+ export interface ClaudeUsage {
2
+ inputTokens: number;
3
+ outputTokens: number;
4
+ }
5
+ export interface ClaudeDecision<T extends Record<string, unknown>> {
6
+ action: T;
7
+ model: string;
8
+ usage: ClaudeUsage;
9
+ sanitizedRationale: string;
10
+ }
11
+ export interface ClaudeActionSchema {
12
+ name: string;
13
+ description: string;
14
+ properties: Record<string, Record<string, unknown>>;
15
+ required: string[];
16
+ }
17
+ export interface BoundedClaudeOptions {
18
+ apiKey?: string;
19
+ model?: string;
20
+ maxTurns?: number;
21
+ maxTokens?: number;
22
+ retries?: number;
23
+ fetch?: typeof globalThis.fetch;
24
+ onDecision?: (entry: {
25
+ model: string;
26
+ usage: ClaudeUsage;
27
+ action: string;
28
+ rationale: string;
29
+ }) => void;
30
+ }
31
+ export declare function sanitizeRationale(value: string): string;
32
+ /** Bounded reasoning only. Callers must supply policy-approved choices and own all amounts and success claims. */
33
+ export declare class BoundedClaude {
34
+ #private;
35
+ private readonly options;
36
+ readonly model: string;
37
+ constructor(options?: BoundedClaudeOptions);
38
+ decide<T extends Record<string, unknown>>(input: {
39
+ system: string;
40
+ task: string;
41
+ candidates: unknown[];
42
+ schema: ClaudeActionSchema;
43
+ }): Promise<ClaudeDecision<T>>;
44
+ }
package/dist/claude.js ADDED
@@ -0,0 +1,98 @@
1
+ import { fail } from "./errors.js";
2
+ const secret = /(?:suiprivkey|sk-ant-|-----BEGIN [A-Z ]*PRIVATE KEY-----|seed phrase|note opening|approval token)/gi;
3
+ export function sanitizeRationale(value) { return value.replace(secret, "[REDACTED]").replace(/\s+/g, " ").trim().slice(0, 512); }
4
+ function validateAction(schema, input) {
5
+ if (!input || typeof input !== "object" || Array.isArray(input))
6
+ fail("CLAUDE_OUTPUT_INVALID", "Claude tool input must be an object");
7
+ const value = input;
8
+ for (const key of Object.keys(value))
9
+ if (!(key in schema.properties))
10
+ fail("CLAUDE_OUTPUT_INVALID", `Claude returned unknown field ${key}`);
11
+ for (const key of schema.required)
12
+ if (!(key in value))
13
+ fail("CLAUDE_OUTPUT_INVALID", `Claude omitted required field ${key}`);
14
+ for (const [key, item] of Object.entries(value)) {
15
+ const field = schema.properties[key];
16
+ if (field.type === "string" && typeof item !== "string")
17
+ fail("CLAUDE_OUTPUT_INVALID", `${key} must be a string`);
18
+ if (field.type === "boolean" && typeof item !== "boolean")
19
+ fail("CLAUDE_OUTPUT_INVALID", `${key} must be a boolean`);
20
+ if (field.type === "array" && !Array.isArray(item))
21
+ fail("CLAUDE_OUTPUT_INVALID", `${key} must be an array`);
22
+ if (Array.isArray(field.enum) && !field.enum.includes(item))
23
+ fail("CLAUDE_OUTPUT_INVALID", `${key} is not an allowed action value`);
24
+ if (typeof item === "string" && typeof field.maxLength === "number" && item.length > field.maxLength)
25
+ fail("CLAUDE_OUTPUT_INVALID", `${key} is too long`);
26
+ }
27
+ }
28
+ /** Bounded reasoning only. Callers must supply policy-approved choices and own all amounts and success claims. */
29
+ export class BoundedClaude {
30
+ options;
31
+ model;
32
+ #apiKey;
33
+ #maxTurns;
34
+ #maxTokens;
35
+ #retries;
36
+ #fetch;
37
+ constructor(options = {}) {
38
+ this.options = options;
39
+ this.#apiKey = options.apiKey ?? process.env.CLAUDE_API_KEY ?? "";
40
+ if (!this.#apiKey)
41
+ fail("CLAUDE_CREDENTIAL_MISSING", "CLAUDE_API_KEY is required for live agent reasoning");
42
+ this.model = options.model ?? process.env.CLAUDE_MODEL ?? "claude-haiku-5-5";
43
+ this.#maxTurns = options.maxTurns ?? 3;
44
+ this.#maxTokens = options.maxTokens ?? 700;
45
+ this.#retries = options.retries ?? 2;
46
+ this.#fetch = options.fetch ?? fetch;
47
+ if (!Number.isSafeInteger(this.#maxTurns) || this.#maxTurns < 1 || this.#maxTurns > 6 || !Number.isSafeInteger(this.#maxTokens) || this.#maxTokens < 32 || this.#maxTokens > 2048 || !Number.isSafeInteger(this.#retries) || this.#retries < 0 || this.#retries > 3)
48
+ fail("CLAUDE_LIMIT_INVALID", "Claude bounds exceed the allowed demo limits");
49
+ }
50
+ async decide(input) {
51
+ if (!input.task || input.task.length > 16_000 || input.system.length > 8_000 || input.candidates.length > 100)
52
+ fail("CLAUDE_INPUT_INVALID", "Claude input exceeds bounded limits");
53
+ const tool = { name: input.schema.name, description: input.schema.description, input_schema: { type: "object", properties: input.schema.properties, required: input.schema.required, additionalProperties: false } };
54
+ let last;
55
+ for (let turn = 0; turn < this.#maxTurns; turn++) {
56
+ for (let attempt = 0; attempt <= this.#retries; attempt++) {
57
+ let response;
58
+ try {
59
+ 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({ model: this.model, max_tokens: this.#maxTokens, system: input.system, messages: [{ role: "user", content: JSON.stringify({ task: input.task, policyApprovedCandidates: input.candidates }) }], tools: [tool], tool_choice: { type: "tool", name: input.schema.name } }), signal: AbortSignal.timeout(30_000) });
60
+ }
61
+ catch (error) {
62
+ last = error;
63
+ if (attempt < this.#retries)
64
+ continue;
65
+ return fail("CLAUDE_UNAVAILABLE", "Claude request failed closed", error);
66
+ }
67
+ if (!response.ok) {
68
+ last = new Error(`Anthropic status ${response.status}`);
69
+ if (attempt < this.#retries && (response.status === 429 || response.status >= 500)) {
70
+ await response.arrayBuffer();
71
+ continue;
72
+ }
73
+ return fail("CLAUDE_UNAVAILABLE", `Claude returned status ${response.status}`);
74
+ }
75
+ let body;
76
+ try {
77
+ body = await response.json();
78
+ }
79
+ catch (error) {
80
+ return fail("CLAUDE_OUTPUT_INVALID", "Claude returned invalid JSON", error);
81
+ }
82
+ const blocks = Array.isArray(body.content) ? body.content : [], selected = blocks.find((block) => block?.type === "tool_use" && block.name === input.schema.name);
83
+ if (!selected) {
84
+ last = new Error("required tool call missing");
85
+ break;
86
+ }
87
+ validateAction(input.schema, selected.input);
88
+ const rationaleBlock = blocks.find((block) => block?.type === "text" && typeof block.text === "string");
89
+ const rationale = sanitizeRationale(String(selected.input.rationale ?? rationaleBlock?.text ?? "bounded selection"));
90
+ const usage = { inputTokens: Number(body.usage?.input_tokens ?? 0), outputTokens: Number(body.usage?.output_tokens ?? 0) };
91
+ const actionName = String(selected.input.action ?? selected.input.choice ?? input.schema.name);
92
+ this.options.onDecision?.({ model: this.model, usage, action: actionName, rationale });
93
+ return { action: selected.input, model: this.model, usage, sanitizedRationale: rationale };
94
+ }
95
+ }
96
+ return fail("CLAUDE_OUTPUT_INVALID", "Claude did not produce a schema-valid bounded action", last);
97
+ }
98
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env node
2
+ import { Phase3App, errorOutput, homePath, initialize, parseArgs } from "./phase3.js";
3
+ import { addBuyerPrivacyRule, approveBuyerSeller, createBuyerSession, promptBuyerSetup, renewBuyer, setupBuyer } from "./buyer-setup.js";
4
+ import { onboardingDoctor } from "./onboarding-doctor.js";
5
+ const words = process.argv.slice(2);
6
+ let command = words.shift() ?? "";
7
+ if (["wallet", "offer", "pay", "resource", "memory", "audit", "capability", "settlement", "setup", "policy", "onboarding"].includes(command))
8
+ command += `.${words.shift() ?? ""}`;
9
+ try {
10
+ if (!command || command === "help" || command === "--help" || command === "setup.") {
11
+ process.stdout.write(`${JSON.stringify({ schemaVersion: "supercuts.help.v1", ok: true, commands: ["setup buyer", "setup renew", "setup session", "init", "doctor", "status", "wallet", "offer", "pay", "memory", "capability", "audit"], setup: "Run supercuts setup buyer --home <private-directory> for guided live Testnet setup. Run supercuts init --role <buyer|seller|auditor> --mock --home <private-directory> for a local non-funded profile. Keep each role in a separate owner-only home.", live: "Supply the pinned Testnet deployment config, owner-only credential files, Sui and Tatum RPC endpoints, Walrus endpoints, local proving artifacts, and RapidSnark. Fund only the buyer address printed by supercuts-demo setup." })}\n`);
12
+ process.exit(0);
13
+ }
14
+ const args = parseArgs(words);
15
+ const home = homePath(args);
16
+ const result = command === "init" ? initialize(home, args) : command === "setup.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 === "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 {
17
+ return await app.execute(command, args);
18
+ }
19
+ finally {
20
+ app.close();
21
+ } })();
22
+ if (args.authorize === true)
23
+ result.warnings = ["--authorize is a deprecated compatibility path; SDK integrations must use bound approval tokens"];
24
+ process.stdout.write(`${JSON.stringify(result)}\n`);
25
+ }
26
+ catch (error) {
27
+ process.stdout.write(`${JSON.stringify(errorOutput(error))}\n`);
28
+ process.exitCode = 1;
29
+ }