@flop-labs/cli 0.4.3 → 0.5.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 (4) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/README.md +22 -1
  3. package/dist/cli.js +1438 -467
  4. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -402,12 +402,12 @@ var init_state = __esm({
402
402
  return computeChecksum(snapshotData);
403
403
  }
404
404
  /** Transition to a new lifecycle state. */
405
- transitionTo(state, reason2) {
405
+ transitionTo(state, reason3) {
406
406
  this.data.lifecycle.state = state;
407
407
  this.data.lifecycle.stateChangedAt = Date.now();
408
408
  this.data.lifecycle.lastActiveAt = Date.now();
409
- if (reason2) {
410
- this.data.lifecycle.terminationReason = reason2;
409
+ if (reason3) {
410
+ this.data.lifecycle.terminationReason = reason3;
411
411
  }
412
412
  this.data.checksum = this.computeChecksumInternal();
413
413
  }
@@ -724,14 +724,14 @@ var init_task_runner = __esm({
724
724
  }
725
725
  }
726
726
  withTimeout(promise, timeoutMs) {
727
- return new Promise((resolve10, reject) => {
727
+ return new Promise((resolve11, reject) => {
728
728
  const timer = setTimeout(
729
729
  () => reject(new AgentError("T002" /* TASK_TIMEOUT */, `Task timed out after ${timeoutMs}ms`, true)),
730
730
  timeoutMs
731
731
  );
732
732
  promise.then((result) => {
733
733
  clearTimeout(timer);
734
- resolve10(result);
734
+ resolve11(result);
735
735
  }).catch((err) => {
736
736
  clearTimeout(timer);
737
737
  reject(err);
@@ -739,7 +739,7 @@ var init_task_runner = __esm({
739
739
  });
740
740
  }
741
741
  sleep(ms) {
742
- return new Promise((resolve10) => setTimeout(resolve10, ms));
742
+ return new Promise((resolve11) => setTimeout(resolve11, ms));
743
743
  }
744
744
  };
745
745
  }
@@ -829,10 +829,10 @@ var init_worker = __esm({
829
829
  await this.processLoop(options.api, options.signer);
830
830
  }
831
831
  /** Request graceful shutdown. */
832
- async stop(reason2) {
832
+ async stop(reason3) {
833
833
  this.stopRequested = true;
834
- this.state.transitionTo("draining", reason2);
835
- this.logger.info("Shutdown requested", { reason: reason2 });
834
+ this.state.transitionTo("draining", reason3);
835
+ this.logger.info("Shutdown requested", { reason: reason3 });
836
836
  }
837
837
  /** Main processing loop. */
838
838
  async processLoop(api, signer) {
@@ -904,7 +904,7 @@ var init_worker = __esm({
904
904
  return this.identity;
905
905
  }
906
906
  sleep(ms) {
907
- return new Promise((resolve10) => setTimeout(resolve10, ms));
907
+ return new Promise((resolve11) => setTimeout(resolve11, ms));
908
908
  }
909
909
  };
910
910
  }
@@ -912,10 +912,10 @@ var init_worker = __esm({
912
912
 
913
913
  // ../agent/src/chain-apis.ts
914
914
  async function submitTx(tx, signer) {
915
- return new Promise((resolve10, reject) => {
915
+ return new Promise((resolve11, reject) => {
916
916
  tx.signAndSend(signer, ({ status, dispatchError }) => {
917
917
  if (dispatchError) reject(new Error(`Tx failed: ${dispatchError.toString()}`));
918
- else if (status.isInBlock || status.isFinalized) resolve10(status.asFinalized.toString());
918
+ else if (status.isInBlock || status.isFinalized) resolve11(status.asFinalized.toString());
919
919
  }).catch(reject);
920
920
  });
921
921
  }
@@ -1219,7 +1219,7 @@ async function waitForTaskCompletion(api, taskId, timeoutMs = DEFAULT_TIMEOUT, p
1219
1219
  return receipt;
1220
1220
  }
1221
1221
  }
1222
- await new Promise((resolve10) => setTimeout(resolve10, pollIntervalMs));
1222
+ await new Promise((resolve11) => setTimeout(resolve11, pollIntervalMs));
1223
1223
  }
1224
1224
  throw new AgentError(
1225
1225
  "T002" /* TASK_TIMEOUT */,
@@ -1657,18 +1657,18 @@ var init_fee_estimation = __esm({
1657
1657
  const outOfBudget = candidates.filter((m) => !affordable.has(m.hash)).map((m) => ({ model: m, fee: feeMap.get(m.hash) }));
1658
1658
  const selectedModel = selected ?? candidates[0];
1659
1659
  const estimatedFee = feeMap.get(selectedModel.hash);
1660
- let reason2;
1660
+ let reason3;
1661
1661
  if (selected) {
1662
- reason2 = preferBestValue ? `Best value model within ${maxBudget} FLOP budget` : `Highest quality model within ${maxBudget} FLOP budget`;
1662
+ reason3 = preferBestValue ? `Best value model within ${maxBudget} FLOP budget` : `Highest quality model within ${maxBudget} FLOP budget`;
1663
1663
  } else {
1664
- reason2 = `Best available model (cheapest) \u2014 all options exceed ${maxBudget} FLOP budget`;
1664
+ reason3 = `Best available model (cheapest) \u2014 all options exceed ${maxBudget} FLOP budget`;
1665
1665
  }
1666
1666
  return {
1667
1667
  selectedModel,
1668
1668
  estimatedFee,
1669
1669
  alternatives,
1670
1670
  outOfBudget,
1671
- reason: reason2
1671
+ reason: reason3
1672
1672
  };
1673
1673
  }
1674
1674
  /**
@@ -1819,69 +1819,13 @@ import { Keyring as Keyring3 } from "@polkadot/keyring";
1819
1819
  import { cryptoWaitReady as cryptoWaitReady3, randomAsHex } from "@polkadot/util-crypto";
1820
1820
  import {
1821
1821
  FLOP_NETWORK_PROFILES,
1822
- DEFAULT_FAUCET_AMOUNT as SDK_DEFAULT_FAUCET_AMOUNT
1822
+ DEFAULT_FAUCET_AMOUNT as SDK_DEFAULT_FAUCET_AMOUNT,
1823
+ fundFromFaucetDetailed,
1824
+ FaucetUnconfirmedError
1823
1825
  } from "@flop-labs/sdk";
1824
1826
  function isFaucetAvailable(network) {
1825
1827
  return FAUCET_ENDPOINTS[network] !== null;
1826
1828
  }
1827
- async function requestFaucetFunding(address, network, timeout = 3e4, referral) {
1828
- const endpoint = FAUCET_ENDPOINTS[network];
1829
- if (!endpoint) {
1830
- return {
1831
- success: false,
1832
- error: `No faucet available for network: ${network}`
1833
- };
1834
- }
1835
- try {
1836
- const response = await fetch(`${endpoint}/fund`, {
1837
- method: "POST",
1838
- headers: {
1839
- "Content-Type": "application/json"
1840
- },
1841
- body: JSON.stringify({
1842
- address,
1843
- amount: DEFAULT_FAUCET_AMOUNT,
1844
- ...referral === void 0 ? {} : { ref: referral, source: "sdk" }
1845
- }),
1846
- signal: AbortSignal.timeout(timeout)
1847
- });
1848
- if (!response.ok) {
1849
- const errorText = await response.text();
1850
- return {
1851
- success: false,
1852
- error: `Faucet request failed: ${response.status} - ${errorText}`
1853
- };
1854
- }
1855
- const data = await response.json();
1856
- return {
1857
- success: true,
1858
- amount: data.amount ?? DEFAULT_FAUCET_AMOUNT,
1859
- txHash: data.txHash
1860
- };
1861
- } catch (error) {
1862
- return {
1863
- success: false,
1864
- error: error instanceof Error ? error.message : String(error)
1865
- };
1866
- }
1867
- }
1868
- async function waitForBalance(api, address, minBalance, timeoutMs = 6e4) {
1869
- const deadline = Date.now() + timeoutMs;
1870
- const pollInterval = 2e3;
1871
- while (Date.now() < deadline) {
1872
- try {
1873
- const accountInfo = await api.query.system.account(address);
1874
- const data = accountInfo.data;
1875
- const freeBalance = BigInt(data.free.toString());
1876
- if (freeBalance >= minBalance) {
1877
- return true;
1878
- }
1879
- } catch {
1880
- }
1881
- await new Promise((resolve10) => setTimeout(resolve10, pollInterval));
1882
- }
1883
- return false;
1884
- }
1885
1829
  async function getBalance(api, address) {
1886
1830
  const accountInfo = await api.query.system.account(address);
1887
1831
  const data = accountInfo.data;
@@ -2001,9 +1945,9 @@ var init_quickstart = __esm({
2001
1945
  /**
2002
1946
  * Stop the agent worker gracefully.
2003
1947
  */
2004
- async stop(reason2) {
2005
- await this.worker.stop(reason2);
2006
- this.logger.info("QuickStart agent stopped", { reason: reason2 });
1948
+ async stop(reason3) {
1949
+ await this.worker.stop(reason3);
1950
+ this.logger.info("QuickStart agent stopped", { reason: reason3 });
2007
1951
  }
2008
1952
  /**
2009
1953
  * Shutdown agent and disconnect from network.
@@ -2376,6 +2320,10 @@ var init_quickstart = __esm({
2376
2320
  * @returns QuickStart agent result with agent wrapper and metadata
2377
2321
  */
2378
2322
  static async create(config = {}) {
2323
+ const shouldFaucet = config.faucet ?? (config.network ?? "devnet") === "devnet";
2324
+ if (shouldFaucet && isFaucetAvailable(config.network ?? "devnet") && !config.keypair && !config.signerUri?.trim()) {
2325
+ throw new Error("Faucet funding requires a caller-owned signer or keypair");
2326
+ }
2379
2327
  await cryptoWaitReady3();
2380
2328
  const network = config.network ?? "devnet";
2381
2329
  const endpoint = config.rpcEndpoint ?? NETWORK_ENDPOINTS[network];
@@ -2383,15 +2331,13 @@ var init_quickstart = __esm({
2383
2331
  const keypair = resolveOrCreateKeypair(config);
2384
2332
  const api = await connectToNetwork(endpoint, timeout);
2385
2333
  let initialBalance = await getBalance(api, keypair.address);
2386
- const shouldFaucet = config.faucet ?? network === "devnet";
2387
2334
  if (shouldFaucet && isFaucetAvailable(network) && initialBalance < DEFAULT_FAUCET_AMOUNT) {
2388
- const faucetResult = await requestFaucetFunding(keypair.address, network, timeout, config.referral);
2389
- if (faucetResult.success) {
2390
- const minBalance = BigInt(Math.round((faucetResult.amount ?? DEFAULT_FAUCET_AMOUNT) * 1e18));
2391
- const funded = await waitForBalance(api, keypair.address, minBalance, timeout * 2);
2392
- if (funded) {
2393
- initialBalance = await getBalance(api, keypair.address);
2394
- }
2335
+ const funding = await fundFromFaucetDetailed(api, keypair.address, { network, timeout, referral: config.referral });
2336
+ if (funding.status !== "refused") {
2337
+ const error = new FaucetUnconfirmedError(funding);
2338
+ const cleanup = await Promise.allSettled([api.disconnect()]);
2339
+ error.cleanupErrors = cleanup.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
2340
+ throw error;
2395
2341
  }
2396
2342
  }
2397
2343
  const agentId = generateAgentId();
@@ -2456,7 +2402,7 @@ var init_quickstart = __esm({
2456
2402
  * ```ts
2457
2403
  * const result = await FlopAgent.inference('Write a Python function to sort a list', {
2458
2404
  * network: 'devnet',
2459
- * faucet: true,
2405
+ * signerUri: process.env.FLOP_SIGNER_URI!, // Pre-funded, caller-owned signer
2460
2406
  * });
2461
2407
  * console.log(result.output);
2462
2408
  * ```
@@ -4941,9 +4887,9 @@ var init_src = __esm({
4941
4887
  });
4942
4888
 
4943
4889
  // src/cli.ts
4944
- import { Command as Command49, Option as Option8 } from "commander";
4890
+ import { Command as Command51, Option as Option8 } from "commander";
4945
4891
  import { readFileSync as readFileSync2 } from "node:fs";
4946
- import { join as join11 } from "node:path";
4892
+ import { join as join12 } from "node:path";
4947
4893
 
4948
4894
  // src/output.ts
4949
4895
  var activeOutputMode = "human";
@@ -5512,8 +5458,8 @@ function sessionListCmd() {
5512
5458
  import { Command as Command10, Option } from "commander";
5513
5459
  function parseSessionMaxFlop(value) {
5514
5460
  if (!/^\d+(?:\.\d{1,18})?$/.test(value)) throw new Error("--max-flop must be a positive decimal with at most 18 fractional digits");
5515
- const [whole, fraction = ""] = value.split(".");
5516
- const raw = BigInt(whole) * 10n ** 18n + BigInt(fraction.padEnd(18, "0"));
5461
+ const [whole, fraction2 = ""] = value.split(".");
5462
+ const raw = BigInt(whole) * 10n ** 18n + BigInt(fraction2.padEnd(18, "0"));
5517
5463
  if (raw <= 0n || raw >= 1n << 128n) throw new Error("--max-flop must be positive and fit the chain balance type");
5518
5464
  return raw;
5519
5465
  }
@@ -5528,6 +5474,7 @@ function sessionRunCmd() {
5528
5474
  return new Command10("run").description("Run verified inference directly on a named miner and settle its channel").requiredOption("--miner <ss58>", "Serving miner account").requiredOption("--model <slug>", "Model slug").requiredOption("--prompt <text>", "Prompt").requiredOption("--max-flop <N>", "Maximum escrow in FLOP").option("--max-tokens <N>", "Maximum completion tokens (SDK default: 256)").addOption(new Option("--via <path>", "Open path").choices(["auto", "offer", "force"]).default("auto")).option("--endpoint <URL>", "Reachable miner session endpoint", "http://127.0.0.1:8645").option("--auth-token-env <VAR>", "Environment variable holding the miner bearer (legacy/broker auth)").option("--new-wallet", "Save a fresh owner-only devnet wallet and fund it from the devnet faucet").option("--json", "Output as JSON").action(async (opts, command) => {
5529
5475
  let client;
5530
5476
  let walletPath;
5477
+ let funding;
5531
5478
  try {
5532
5479
  const maxFlop = parseSessionMaxFlop(opts.maxFlop);
5533
5480
  const maxTokens = opts.maxTokens === void 0 ? void 0 : parseSessionMaxTokens(opts.maxTokens);
@@ -5564,9 +5511,9 @@ function sessionRunCmd() {
5564
5511
  client = await sdk.FlopClient.createWithKeypair(resolved.rpc, pair);
5565
5512
  const faucet = await sdk.getFaucetChainInfo({ endpoint: resolved.profile.faucet });
5566
5513
  if (faucet.genesisHash.toLowerCase() !== client.api.genesisHash.toHex().toLowerCase()) throw new Error("faucet/RPC genesis mismatch");
5567
- const funded = await sdk.requestFaucetFunding(pair.address, { network: "devnet", endpoint: resolved.profile.faucet, maxRetries: 1 });
5568
- if (!funded.success) throw new Error(funded.error ?? "devnet faucet funding failed");
5569
- if (!await sdk.waitForFaucetFunding(client.api, pair.address, Number(opts.maxFlop))) {
5514
+ funding = await sdk.requestFaucetFunding(pair.address, { network: "devnet", endpoint: resolved.profile.faucet, maxRetries: 1 });
5515
+ if (!funding.success) throw new Error(funding.error ?? "devnet faucet funding failed");
5516
+ if (!await sdk.waitForAvailableBalance(client.api, pair.address, Number(opts.maxFlop))) {
5570
5517
  throw new Error("faucet balance did not arrive before the funding deadline");
5571
5518
  }
5572
5519
  } else {
@@ -5598,11 +5545,18 @@ function sessionRunCmd() {
5598
5545
  }, getJson(command));
5599
5546
  } catch (error) {
5600
5547
  const channelId = error && typeof error === "object" && "channelId" in error && error.channelId instanceof Uint8Array ? `0x${Buffer.from(error.channelId).toString("hex")}` : null;
5548
+ const fundingUnknown = funding && (funding.outcome === "unknown" || funding.success);
5601
5549
  printError({
5602
5550
  code: "SESSION_FAILED",
5603
5551
  message: error instanceof Error ? error.message : String(error),
5604
5552
  channel: channelId,
5605
- recovery: channelId ? "inspect finalized channel and terminal events before retrying" : "no channel reported",
5553
+ recovery: channelId ? "inspect finalized channel and terminal events before retrying" : fundingUnknown ? "funding is unconfirmed; reconcile the original request before requesting again" : "no channel reported",
5554
+ ...funding ? { funding: {
5555
+ outcome: fundingUnknown ? "unknown" : funding.outcome,
5556
+ ...funding.txHash ? { txHash: funding.txHash } : {},
5557
+ ...funding.amount !== void 0 ? { amount: funding.amount } : {},
5558
+ ...funding.status !== void 0 ? { status: funding.status } : {}
5559
+ } } : {},
5606
5560
  ...walletPath ? { wallet: walletPath } : {}
5607
5561
  }, "json");
5608
5562
  process.exitCode = 1;
@@ -6127,12 +6081,12 @@ function pathCheck(id, target, required = true) {
6127
6081
  };
6128
6082
  }
6129
6083
  async function probeHttpUrl(id, url, required = true, timeoutMs = 3e3) {
6130
- return new Promise((resolve10) => {
6084
+ return new Promise((resolve11) => {
6131
6085
  let parsed;
6132
6086
  try {
6133
6087
  parsed = new URL(url);
6134
6088
  } catch {
6135
- resolve10({
6089
+ resolve11({
6136
6090
  id,
6137
6091
  status: "fail",
6138
6092
  required,
@@ -6145,7 +6099,7 @@ async function probeHttpUrl(id, url, required = true, timeoutMs = 3e3) {
6145
6099
  const request3 = transport.request(parsed, { method: "GET", timeout: timeoutMs }, (response) => {
6146
6100
  response.resume();
6147
6101
  const reachable = (response.statusCode ?? 500) < 500;
6148
- resolve10({
6102
+ resolve11({
6149
6103
  id,
6150
6104
  status: reachable ? "pass" : required ? "fail" : "warn",
6151
6105
  required,
@@ -6155,7 +6109,7 @@ async function probeHttpUrl(id, url, required = true, timeoutMs = 3e3) {
6155
6109
  });
6156
6110
  request3.on("timeout", () => request3.destroy(new Error("timeout")));
6157
6111
  request3.on("error", (error) => {
6158
- resolve10({
6112
+ resolve11({
6159
6113
  id,
6160
6114
  status: required ? "fail" : "warn",
6161
6115
  required,
@@ -6199,7 +6153,7 @@ function unwrapStorage(raw) {
6199
6153
 
6200
6154
  // src/commands/launch.ts
6201
6155
  function checkGateway(url) {
6202
- return new Promise((resolve10) => {
6156
+ return new Promise((resolve11) => {
6203
6157
  try {
6204
6158
  const parsedUrl = new URL(url);
6205
6159
  const req = http2.request(
@@ -6211,17 +6165,17 @@ function checkGateway(url) {
6211
6165
  timeout: 1e3
6212
6166
  },
6213
6167
  (res) => {
6214
- resolve10(res.statusCode === 200);
6168
+ resolve11(res.statusCode === 200);
6215
6169
  }
6216
6170
  );
6217
- req.on("error", () => resolve10(false));
6171
+ req.on("error", () => resolve11(false));
6218
6172
  req.on("timeout", () => {
6219
6173
  req.destroy();
6220
- resolve10(false);
6174
+ resolve11(false);
6221
6175
  });
6222
6176
  req.end();
6223
6177
  } catch {
6224
- resolve10(false);
6178
+ resolve11(false);
6225
6179
  }
6226
6180
  });
6227
6181
  }
@@ -6462,15 +6416,15 @@ function ownerOnly(mode) {
6462
6416
  }
6463
6417
  async function privateDirectory(path5) {
6464
6418
  await mkdir5(path5, { recursive: true, mode: 448 });
6465
- const stat = await lstat(path5);
6466
- if (!stat.isDirectory() || !ownerOnly(stat.mode) || process.getuid && stat.uid !== process.getuid()) {
6419
+ const stat2 = await lstat(path5);
6420
+ if (!stat2.isDirectory() || !ownerOnly(stat2.mode) || process.getuid && stat2.uid !== process.getuid()) {
6467
6421
  throw new Error("FLOP credential directory must be owned by this user with mode 0700");
6468
6422
  }
6469
6423
  }
6470
6424
  async function cachedCredential(path5, gateway) {
6471
6425
  try {
6472
- const stat = await lstat(path5);
6473
- if (!stat.isFile() || !ownerOnly(stat.mode) || process.getuid && stat.uid !== process.getuid()) {
6426
+ const stat2 = await lstat(path5);
6427
+ if (!stat2.isFile() || !ownerOnly(stat2.mode) || process.getuid && stat2.uid !== process.getuid()) {
6474
6428
  throw new Error("FLOP credential cache must be an owner-only regular file (mode 0600)");
6475
6429
  }
6476
6430
  const cached = JSON.parse(await readFile3(path5, "utf8"));
@@ -6529,7 +6483,7 @@ async function withCredentialLock(path5, action) {
6529
6483
  continue;
6530
6484
  }
6531
6485
  if (Date.now() >= deadline) throw new Error(`FLOP credential lock is busy at ${lock}`);
6532
- await new Promise((resolve10) => setTimeout(resolve10, 100));
6486
+ await new Promise((resolve11) => setTimeout(resolve11, 100));
6533
6487
  }
6534
6488
  }
6535
6489
  try {
@@ -6551,8 +6505,8 @@ async function enrollmentWallet(path5) {
6551
6505
  await cryptoWaitReady4();
6552
6506
  const keyring = new Keyring4({ type: "sr25519", ss58Format: 42 });
6553
6507
  try {
6554
- const stat = await lstat(path5);
6555
- if (!stat.isFile() || !ownerOnly(stat.mode) || process.getuid && stat.uid !== process.getuid()) {
6508
+ const stat2 = await lstat(path5);
6509
+ if (!stat2.isFile() || !ownerOnly(stat2.mode) || process.getuid && stat2.uid !== process.getuid()) {
6556
6510
  throw new Error("FLOP devnet wallet must be an owner-only regular file (mode 0600)");
6557
6511
  }
6558
6512
  const stored = JSON.parse(await readFile3(path5, "utf8"));
@@ -6597,17 +6551,35 @@ function walletEnrollment(options) {
6597
6551
  const pair = await enrollmentWallet(options.walletPath ?? devnetWalletPath());
6598
6552
  const minBalance = options.minBalance ?? DEFAULT_MIN_BALANCE;
6599
6553
  if (await options.balance(pair.address) < minBalance) {
6600
- const funded = await fetch(`${options.faucet}/fund`, {
6601
- method: "POST",
6602
- headers: { "content-type": "application/json" },
6603
- body: JSON.stringify({ address: pair.address }),
6604
- signal: AbortSignal.timeout(6e4)
6605
- });
6606
- if (!funded.ok) throw await failure(funded, "devnet faucet");
6554
+ let funded;
6555
+ try {
6556
+ funded = await fetch(`${options.faucet}/fund`, {
6557
+ method: "POST",
6558
+ headers: { "content-type": "application/json" },
6559
+ body: JSON.stringify({ address: pair.address }),
6560
+ signal: AbortSignal.timeout(6e4)
6561
+ });
6562
+ } catch {
6563
+ throw new Error("devnet faucet outcome unknown; reconcile the original request for the saved wallet before requesting again");
6564
+ }
6565
+ if (!funded.ok) {
6566
+ if (funded.status >= 500 || funded.status === 408) throw new Error("devnet faucet outcome unknown; reconcile the original request for the saved wallet before requesting again");
6567
+ throw await failure(funded, "devnet faucet");
6568
+ }
6569
+ const response = await funded.json().catch(() => null);
6570
+ const hint = typeof response?.txHash === "string" && /^0x[0-9a-f]{64}$/i.test(response.txHash) ? ` (transaction ${response.txHash})` : "";
6607
6571
  const deadline = Date.now() + (options.fundTimeoutMs ?? 12e4);
6608
- while (await options.balance(pair.address) < minBalance) {
6609
- if (Date.now() >= deadline) throw new Error("devnet faucet grant did not finalize in time; retry later");
6610
- await new Promise((resolve10) => setTimeout(resolve10, options.pollMs ?? 3e3));
6572
+ const unconfirmed = () => new Error(`devnet balance did not reach the required floor${hint}; the faucet request is unconfirmed, reconcile it before requesting again`);
6573
+ while (true) {
6574
+ let available;
6575
+ try {
6576
+ available = await options.balance(pair.address);
6577
+ } catch {
6578
+ throw unconfirmed();
6579
+ }
6580
+ if (available >= minBalance) break;
6581
+ if (Date.now() >= deadline) throw unconfirmed();
6582
+ await new Promise((resolve11) => setTimeout(resolve11, options.pollMs ?? 3e3));
6611
6583
  }
6612
6584
  }
6613
6585
  const challenge = await fetch(
@@ -6760,8 +6732,8 @@ async function savePublicProfile(model, key) {
6760
6732
  const path5 = publicHermesProfilePath(model, key);
6761
6733
  for (const directory of [join2(homedir2(), ".flop"), publicHermesProfileRoot(), path5]) {
6762
6734
  await mkdir6(directory, { recursive: true, mode: 448 });
6763
- const stat = await lstat2(directory);
6764
- if (!stat.isDirectory() || process.getuid && stat.uid !== process.getuid() || process.platform !== "win32" && (stat.mode & 63) !== 0) {
6735
+ const stat2 = await lstat2(directory);
6736
+ if (!stat2.isDirectory() || process.getuid && stat2.uid !== process.getuid() || process.platform !== "win32" && (stat2.mode & 63) !== 0) {
6765
6737
  throw new Error("FLOP Hermes profile and parent directories must be owner-only regular directories");
6766
6738
  }
6767
6739
  }
@@ -6882,8 +6854,8 @@ import http3 from "http";
6882
6854
 
6883
6855
  // src/proxy/openai.ts
6884
6856
  var GFLOPS_PER_TOKEN = 10;
6885
- function estimateTokens2(text3) {
6886
- return Math.max(1, Math.ceil((text3?.length ?? 0) / 4));
6857
+ function estimateTokens2(text4) {
6858
+ return Math.max(1, Math.ceil((text4?.length ?? 0) / 4));
6887
6859
  }
6888
6860
  function chatMessagesToPrompt(messages) {
6889
6861
  return messages.map((m) => `${m.role}: ${m.content}`).join("\n");
@@ -6944,8 +6916,8 @@ function modelsListResponse(models) {
6944
6916
  function streamMeta(outcome) {
6945
6917
  return { id: outcome.taskId, model: outcome.model, created: Math.floor(Date.now() / 1e3) };
6946
6918
  }
6947
- function chunkText(text3) {
6948
- return text3 ? text3.match(/\s+|\S+/g) ?? [] : [];
6919
+ function chunkText(text4) {
6920
+ return text4 ? text4.match(/\s+|\S+/g) ?? [] : [];
6949
6921
  }
6950
6922
  function buildChatChunk(meta, delta, finishReason) {
6951
6923
  return {
@@ -6956,13 +6928,13 @@ function buildChatChunk(meta, delta, finishReason) {
6956
6928
  choices: [{ index: 0, delta, finish_reason: finishReason }]
6957
6929
  };
6958
6930
  }
6959
- function buildTextChunk(meta, text3, finishReason) {
6931
+ function buildTextChunk(meta, text4, finishReason) {
6960
6932
  return {
6961
6933
  id: meta.id,
6962
6934
  object: "text_completion",
6963
6935
  created: meta.created,
6964
6936
  model: meta.model,
6965
- choices: [{ index: 0, text: text3, finish_reason: finishReason }]
6937
+ choices: [{ index: 0, text: text4, finish_reason: finishReason }]
6966
6938
  };
6967
6939
  }
6968
6940
  function buildUsageChunk(meta, outcome, promptTokens) {
@@ -7014,14 +6986,14 @@ var CORS_HEADERS = {
7014
6986
  "Access-Control-Allow-Headers": "Authorization, Content-Type"
7015
6987
  };
7016
6988
  function readBody(req) {
7017
- return new Promise((resolve10) => {
6989
+ return new Promise((resolve11) => {
7018
6990
  let data = "";
7019
6991
  req.on("data", (c) => data += c);
7020
6992
  req.on("end", () => {
7021
6993
  try {
7022
- resolve10(data ? JSON.parse(data) : {});
6994
+ resolve11(data ? JSON.parse(data) : {});
7023
6995
  } catch {
7024
- resolve10({});
6996
+ resolve11({});
7025
6997
  }
7026
6998
  });
7027
6999
  });
@@ -7175,8 +7147,8 @@ function createLocalEmbedder(model) {
7175
7147
  extractor = await transformers.pipeline("feature-extraction", model);
7176
7148
  }
7177
7149
  const out = [];
7178
- for (const text3 of inputs) {
7179
- const t = await extractor(text3, { pooling: "mean", normalize: true });
7150
+ for (const text4 of inputs) {
7151
+ const t = await extractor(text4, { pooling: "mean", normalize: true });
7180
7152
  out.push(Array.from(t.data));
7181
7153
  }
7182
7154
  return out;
@@ -7258,7 +7230,7 @@ async function startProxyServer(o) {
7258
7230
  embeddingModel: o.embeddingModel,
7259
7231
  embed: createLocalEmbedder(o.embeddingModel)
7260
7232
  });
7261
- await new Promise((resolve10) => server.listen(o.port, o.host, () => resolve10()));
7233
+ await new Promise((resolve11) => server.listen(o.port, o.host, () => resolve11()));
7262
7234
  const addr = server.address();
7263
7235
  const url = `http://${o.host}:${addr.port}`;
7264
7236
  const models = await listModels();
@@ -7395,9 +7367,9 @@ function launchExternal2(command, args, envOverrides) {
7395
7367
  function runHermesConfig(launcher, commands) {
7396
7368
  return commands.reduce(
7397
7369
  (p, args) => p.then(
7398
- () => new Promise((resolve10, reject) => {
7370
+ () => new Promise((resolve11, reject) => {
7399
7371
  const c = spawnLauncher(launcher, args, { stdio: "inherit" });
7400
- c.on("exit", (code) => code === 0 ? resolve10() : reject(new Error(`hermes ${args.join(" ")} exited ${code}`)));
7372
+ c.on("exit", (code) => code === 0 ? resolve11() : reject(new Error(`hermes ${args.join(" ")} exited ${code}`)));
7401
7373
  c.on("error", reject);
7402
7374
  })
7403
7375
  ),
@@ -7624,14 +7596,18 @@ function daWithdrawCmd() {
7624
7596
 
7625
7597
  // src/commands/model-upload.ts
7626
7598
  import { Command as Command30, Option as Option2 } from "commander";
7627
- import { copyFile, lstat as lstat7, mkdir as mkdir11, mkdtemp, rm as rm3, writeFile as writeFile9 } from "node:fs/promises";
7628
- import { tmpdir } from "node:os";
7599
+ import { createWriteStream } from "node:fs";
7600
+ import { copyFile, lstat as lstat7, mkdir as mkdir11, mkdtemp as mkdtemp2, rm as rm4, writeFile as writeFile9 } from "node:fs/promises";
7601
+ import { pipeline as pipeline2 } from "node:stream/promises";
7602
+ import { tmpdir as tmpdir2 } from "node:os";
7629
7603
  import { basename as basename2, join as join6, resolve as resolve8 } from "node:path";
7630
7604
 
7631
7605
  // src/model.ts
7632
7606
  import { resolve as resolve5, relative, sep } from "node:path";
7633
- import { mkdir as mkdir7, readFile as readFile4, readdir, lstat as lstat3, writeFile as writeFile7 } from "node:fs/promises";
7607
+ import { mkdir as mkdir7, mkdtemp, readFile as readFile4, readdir, lstat as lstat3, rm as rm2, writeFile as writeFile7 } from "node:fs/promises";
7634
7608
  import { execFile as execFile3, spawn as spawn4 } from "node:child_process";
7609
+ import { createHash as createHash2 } from "node:crypto";
7610
+ import { tmpdir } from "node:os";
7635
7611
  import { pipeline } from "node:stream/promises";
7636
7612
  import { promisify as promisify3 } from "node:util";
7637
7613
  function modelToOutput(model) {
@@ -7678,6 +7654,26 @@ function parseModelSource(raw) {
7678
7654
  `unsupported model source "${raw}"; use hf:<repo>, modelscope:<repo>, https://..., or da:<commitment>`
7679
7655
  );
7680
7656
  }
7657
+ function parseRevision(raw) {
7658
+ if (raw !== void 0 && !/^[0-9a-f]{40}$/.test(raw)) throw new Error("--revision must be an immutable 40-hex commit");
7659
+ return raw;
7660
+ }
7661
+ async function downloadPinnedHuggingFace(repo, dir, expectedRoot, opts) {
7662
+ const sdk = await import("@flop-labs/sdk");
7663
+ const hub = { revision: opts.revision, token: opts.token };
7664
+ const preflight = await sdk.preflightHuggingFace(repo, hub);
7665
+ if (sdk.normalizeRoot(preflight.contentRoot) !== sdk.normalizeRoot(expectedRoot)) {
7666
+ const found = `${repo} at ${preflight.revision} has content_root 0x${sdk.normalizeRoot(preflight.contentRoot)}, not the registered root ${expectedRoot}`;
7667
+ if (!opts.revision) {
7668
+ throw new Error(
7669
+ `${found}; nothing was downloaded. A repo whose main moved needs the registered --revision (the onboarding identity's sourceRevision), and a verity-v1 model's root cannot be checked from hub metadata, so it downloads only at an explicit --revision and is measured after transfer`
7670
+ );
7671
+ }
7672
+ process.stderr.write(`${found}; a verity-v1 root is checkable only after download, so the bytes are measured once fetched
7673
+ `);
7674
+ }
7675
+ return sdk.fetchHuggingFaceToDir(repo, dir, { ...hub, revision: preflight.revision, files: preflight.files });
7676
+ }
7681
7677
  function daHttpSource(endpoint) {
7682
7678
  if (!endpoint) return void 0;
7683
7679
  const base = endpoint.replace(/\/+$/, "");
@@ -7707,12 +7703,12 @@ async function writeModelFiles(outputDir, files) {
7707
7703
  }
7708
7704
  async function readLocalModelFiles(path5) {
7709
7705
  const root = resolve5(path5);
7710
- const stat = await lstat3(root);
7711
- if (stat.isSymbolicLink()) throw new Error(`model source must not be a symlink: ${path5}`);
7712
- if (stat.isFile()) {
7706
+ const stat2 = await lstat3(root);
7707
+ if (stat2.isSymbolicLink()) throw new Error(`model source must not be a symlink: ${path5}`);
7708
+ if (stat2.isFile()) {
7713
7709
  return [{ path: root.split(sep).pop() ?? "weights.bin", bytes: await readFile4(root) }];
7714
7710
  }
7715
- if (!stat.isDirectory()) throw new Error(`model source is not a file or directory: ${path5}`);
7711
+ if (!stat2.isDirectory()) throw new Error(`model source is not a file or directory: ${path5}`);
7716
7712
  const files = [];
7717
7713
  async function walk(dir) {
7718
7714
  for (const entry of await readdir(dir, { withFileTypes: true })) {
@@ -7877,31 +7873,82 @@ async function submitDaBlob(executable, rpc, blobFile, publisherKey) {
7877
7873
  }
7878
7874
  return parseCustodyResult(stdout2);
7879
7875
  }
7876
+ async function daRetrieveChunk(executable, rpc, commitment, j) {
7877
+ const dir = await mkdtemp(resolve5(tmpdir(), "flop-da-chunk-"));
7878
+ const output = resolve5(dir, "chunk.bin");
7879
+ try {
7880
+ let stdout2;
7881
+ try {
7882
+ ({ stdout: stdout2 } = await promisify3(execFile3)(executable, [
7883
+ "retrieve",
7884
+ "--chain-rpc",
7885
+ rpc,
7886
+ "--commitment",
7887
+ commitment,
7888
+ "--output",
7889
+ output,
7890
+ "--chunk",
7891
+ String(j),
7892
+ "--json"
7893
+ ], { maxBuffer: 1024 * 1024 }));
7894
+ } catch (error) {
7895
+ const stderr = String(error.stderr ?? "");
7896
+ if (/unexpected argument '--chunk'/.test(stderr)) {
7897
+ throw new Error("this da-validator has no `retrieve --chunk`; install one built from flop-core with it to spot-check the DA image");
7898
+ }
7899
+ throw new Error(`da-validator retrieve --chunk ${j} failed: ${stderr.trim() || error.message}`);
7900
+ }
7901
+ const line = lastJsonLine(stdout2);
7902
+ if (!line) throw new Error("da-validator retrieve --chunk returned no JSON receipt");
7903
+ const receipt = JSON.parse(line);
7904
+ const bytes4 = new Uint8Array(await readFile4(output));
7905
+ if (receipt.chunk !== j || receipt.length !== bytes4.length || receipt.sha256 !== createHash2("sha256").update(bytes4).digest("hex")) {
7906
+ throw new Error(`da-validator chunk ${j} receipt does not match the retrieved bytes`);
7907
+ }
7908
+ return { chunk: j, offset: Number(receipt.offset), sizeBytes: Number(receipt.size_bytes), chunkSize: Number(receipt.chunk_size), bytes: bytes4 };
7909
+ } finally {
7910
+ await rm2(dir, { recursive: true, force: true });
7911
+ }
7912
+ }
7913
+ async function readHuggingFaceRange(repo, revision, path5, start, end, size, token) {
7914
+ const url = `https://huggingface.co/${repo}/resolve/${revision}/${path5.split("/").map(encodeURIComponent).join("/")}`;
7915
+ const response = await fetch(url, {
7916
+ headers: { Range: `bytes=${start}-${end}`, ...token ? { Authorization: `Bearer ${token}` } : {} },
7917
+ signal: AbortSignal.timeout(12e4)
7918
+ });
7919
+ if (response.status !== 206 || response.headers.get("content-range") !== `bytes ${start}-${end}/${size}`) {
7920
+ await response.body?.cancel();
7921
+ throw new Error(`Hugging Face did not honor byte range ${start}-${end} of ${path5} at ${revision} (HTTP ${response.status})`);
7922
+ }
7923
+ const bytes4 = new Uint8Array(await response.arrayBuffer());
7924
+ if (bytes4.length !== end - start + 1) throw new Error(`Hugging Face returned ${bytes4.length} bytes for range ${start}-${end} of ${path5}`);
7925
+ return bytes4;
7926
+ }
7880
7927
 
7881
7928
  // src/commands/model-onboard.ts
7882
7929
  import { Command as Command29 } from "commander";
7883
- import { createHash as createHash3 } from "node:crypto";
7930
+ import { createHash as createHash5 } from "node:crypto";
7884
7931
  import { execFile as execFile4 } from "node:child_process";
7885
7932
  import { access, lstat as lstat6, mkdir as mkdir10, readFile as readFile7, writeFile as writeFile8 } from "node:fs/promises";
7886
7933
  import { basename, isAbsolute, join as join5, resolve as resolve7 } from "node:path";
7887
7934
  import { promisify as promisify4 } from "node:util";
7888
- import { computeChannel as computeChannel4 } from "@flop-labs/sdk";
7935
+ import { computeChannel as computeChannel5 } from "@flop-labs/sdk";
7889
7936
 
7890
7937
  // src/model-onboard-state.ts
7891
- import { createHash as createHash2 } from "node:crypto";
7938
+ import { createHash as createHash3 } from "node:crypto";
7892
7939
  import { createReadStream } from "node:fs";
7893
- import { chmod as chmod3, lstat as lstat4, mkdir as mkdir8, open, readFile as readFile5, rename as rename6, rm as rm2 } from "node:fs/promises";
7940
+ import { chmod as chmod3, lstat as lstat4, mkdir as mkdir8, open, readFile as readFile5, rename as rename6, rm as rm3 } from "node:fs/promises";
7894
7941
  import { homedir as homedir3, hostname as hostname2 } from "node:os";
7895
7942
  import { join as join3, resolve as resolve6 } from "node:path";
7896
7943
  import { collectModelFiles } from "@flop-labs/sdk";
7897
- var digest = (value) => createHash2("sha256").update(value).digest("hex");
7944
+ var digest = (value) => createHash3("sha256").update(value).digest("hex");
7898
7945
  function onboardingDirectory(binding, override) {
7899
7946
  return override ? resolve6(override) : join3(process.env.XDG_STATE_HOME ?? join3(homedir3(), ".local/state"), "flop/model-onboarding", digest(JSON.stringify(binding)));
7900
7947
  }
7901
7948
  async function hashFile(path5) {
7902
- const stat = await lstat4(path5);
7903
- if (!stat.isFile() || stat.isSymbolicLink()) throw new Error("onboarding artifact must be a regular file");
7904
- const hash2 = createHash2("sha256");
7949
+ const stat2 = await lstat4(path5);
7950
+ if (!stat2.isFile() || stat2.isSymbolicLink()) throw new Error("onboarding artifact must be a regular file");
7951
+ const hash2 = createHash3("sha256");
7905
7952
  for await (const chunk of createReadStream(path5)) hash2.update(chunk);
7906
7953
  return hash2.digest("hex");
7907
7954
  }
@@ -7925,7 +7972,7 @@ async function openOnboardingState(dir, binding) {
7925
7972
  const statePath = join3(dir, "state.json");
7926
7973
  const close = async () => {
7927
7974
  await lock.close();
7928
- await rm2(lockPath);
7975
+ await rm3(lockPath);
7929
7976
  };
7930
7977
  try {
7931
7978
  await lock.writeFile(JSON.stringify({ pid: process.pid, host: hostname2() }));
@@ -8066,10 +8113,10 @@ import { computeChannel as computeChannel2, attestation } from "@flop-labs/sdk";
8066
8113
  import { randomUUID as randomUUID3 } from "node:crypto";
8067
8114
  import { chmod as chmod4, lstat as lstat5, mkdir as mkdir9, open as open2, readFile as readFile6, rename as rename7 } from "node:fs/promises";
8068
8115
  import { join as join4 } from "node:path";
8069
- var bytes = (hex4) => {
8070
- if (!/^0x(?:[0-9a-f]{2})*$/.test(hex4))
8116
+ var bytes = (hex5) => {
8117
+ if (!/^0x(?:[0-9a-f]{2})*$/.test(hex5))
8071
8118
  throw new Error("invalid private replay bytes");
8072
- return new Uint8Array(Buffer.from(hex4.slice(2), "hex"));
8119
+ return new Uint8Array(Buffer.from(hex5.slice(2), "hex"));
8073
8120
  };
8074
8121
  var hex = (v) => `0x${Buffer.from(v).toString("hex")}`;
8075
8122
  var privateJson = (value) => JSON.stringify(
@@ -8077,8 +8124,8 @@ var privateJson = (value) => JSON.stringify(
8077
8124
  (_k, v) => typeof v === "bigint" ? v.toString() : v instanceof Uint8Array ? hex(v) : v
8078
8125
  );
8079
8126
  async function readPrivateJson(path5) {
8080
- const stat = await lstat5(path5);
8081
- if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 63) !== 0)
8127
+ const stat2 = await lstat5(path5);
8128
+ if (!stat2.isFile() || stat2.isSymbolicLink() || (stat2.mode & 63) !== 0)
8082
8129
  throw new Error("replay files must be private regular files (0600)");
8083
8130
  return JSON.parse(await readFile6(path5, "utf8"));
8084
8131
  }
@@ -8406,8 +8453,8 @@ async function checkPaidAdmission(api, endpoint, identity, token, completion, pa
8406
8453
  await anonymous.body?.cancel();
8407
8454
  const info = await computeChannel3.fetchMinerSessionInfo(endpoint, { Authorization: `Bearer ${token}` }, AbortSignal.timeout(3e4));
8408
8455
  const actual = info.modelIdentity;
8409
- const hex4 = (v) => `0x${Buffer.from(v).toString("hex")}`;
8410
- if (!actual || actual.registryModel !== identity.registryModel || actual.registryVersion !== identity.registryVersion || actual.backendModelName !== identity.backendModelName || hex4(actual.modelHash) !== identity.modelHash || hex4(actual.measuredRoot) !== identity.measuredRoot || hex4(actual.decodePolicyHash) !== identity.decodePolicyHash || actual.precision !== identity.precision) throw new Error("paid admission authenticated model identity differs from prepared checkpoint");
8456
+ const hex5 = (v) => `0x${Buffer.from(v).toString("hex")}`;
8457
+ if (!actual || actual.registryModel !== identity.registryModel || actual.registryVersion !== identity.registryVersion || actual.backendModelName !== identity.backendModelName || hex5(actual.modelHash) !== identity.modelHash || hex5(actual.measuredRoot) !== identity.measuredRoot || hex5(actual.decodePolicyHash) !== identity.decodePolicyHash || actual.precision !== identity.precision) throw new Error("paid admission authenticated model identity differs from prepared checkpoint");
8411
8458
  if (!info.isReady || !info.calibration?.ready) throw new Error("paid admission miner is not calibrated and serving");
8412
8459
  const head = await api.rpc.chain.getFinalizedHead(), view = await api.at(head);
8413
8460
  const registration = await view.query.minerStaking.miners(info.minerAddress);
@@ -8416,7 +8463,7 @@ async function checkPaidAdmission(api, endpoint, identity, token, completion, pa
8416
8463
  if (key.length !== 32) throw new Error("paid admission requires the runtime AccountId32 encoding");
8417
8464
  const result = await api.rpc.state.call(
8418
8465
  "TrustRootApi_accepted_calibration_cap",
8419
- hex4(Uint8Array.from([128, ...key])),
8466
+ hex5(Uint8Array.from([128, ...key])),
8420
8467
  head
8421
8468
  );
8422
8469
  const value = result.toU8a(true);
@@ -8432,7 +8479,7 @@ async function checkPaidAdmission(api, endpoint, identity, token, completion, pa
8432
8479
  hash: (await api.rpc.chain.getBlockHash((BigInt(anchor.receipt.number) + 1n).toString())).toHex()
8433
8480
  };
8434
8481
  const session = await rpc.session(anchor, seed, finalized);
8435
- if (!payer || session.agent !== hex4(api.createType("AccountId", payer).toU8a()) || session.miner !== hex4(key) || session.model.modelHash !== identity.modelHash || session.model.measuredRoot !== identity.measuredRoot || session.model.precision !== identity.precision || session.model.decodePolicyHash !== identity.decodePolicyHash) throw new Error("historical paid completion differs from onboarding model/payer/miner");
8482
+ if (!payer || session.agent !== hex5(api.createType("AccountId", payer).toU8a()) || session.miner !== hex5(key) || session.model.modelHash !== identity.modelHash || session.model.measuredRoot !== identity.measuredRoot || session.model.precision !== identity.precision || session.model.decodePolicyHash !== identity.decodePolicyHash) throw new Error("historical paid completion differs from onboarding model/payer/miner");
8436
8483
  historicalCompletion = await verifyStoredReplay(api, completion);
8437
8484
  }
8438
8485
  return {
@@ -8444,6 +8491,215 @@ async function checkPaidAdmission(api, endpoint, identity, token, completion, pa
8444
8491
  };
8445
8492
  }
8446
8493
 
8494
+ // src/throughput-ceiling.ts
8495
+ import { computeChannel as computeChannel4 } from "@flop-labs/sdk";
8496
+ function parseMinTokensPerSecond(raw) {
8497
+ const text4 = raw.trim();
8498
+ if (!/^\d+(?:\.\d{1,6})?$/.test(text4) || !/[1-9]/.test(text4)) {
8499
+ throw new Error("--min-tokens-per-second must be a positive decimal with at most 6 fractional digits");
8500
+ }
8501
+ return text4;
8502
+ }
8503
+ function fraction(minimum) {
8504
+ const [whole, frac = ""] = minimum.split(".");
8505
+ return [BigInt(whole + frac), 10n ** BigInt(frac.length)];
8506
+ }
8507
+ var FLOPS_PER_GN = 1000000000n;
8508
+ function perTokenWork(checkpoint) {
8509
+ const hybrid = computeChannel4.isDenseGdnArch(checkpoint.arch);
8510
+ return {
8511
+ flopsPerToken: hybrid ? checkpoint.gnPerCompletionToken * FLOPS_PER_GN : 2n * checkpoint.activeParams,
8512
+ gnPerToken: checkpoint.gnPerCompletionToken
8513
+ };
8514
+ }
8515
+ function centiTokensPerSecond(capGnPerBlock, blockTimeMs, flopsPerToken) {
8516
+ if (flopsPerToken <= 0n || blockTimeMs <= 0n) throw new RangeError("flopsPerToken and blockTimeMs must be positive");
8517
+ return capGnPerBlock * FLOPS_PER_GN * 1000n * 100n / (blockTimeMs * flopsPerToken);
8518
+ }
8519
+ function exactGn(flops) {
8520
+ const frac = (flops % FLOPS_PER_GN).toString().padStart(9, "0").replace(/0+$/, "");
8521
+ return `${flops / FLOPS_PER_GN}${frac ? `.${frac}` : ""}`;
8522
+ }
8523
+ function decimal(centi) {
8524
+ return `${centi / 100n}.${(centi % 100n).toString().padStart(2, "0")}`;
8525
+ }
8526
+ function unknown(work, minimum, reason3) {
8527
+ return {
8528
+ status: "unknown",
8529
+ tokensPerSecond: `unknown (${reason3})`,
8530
+ gnPerToken: work ? exactGn(work.flopsPerToken) : null,
8531
+ flopsPerToken: work?.flopsPerToken.toString() ?? null,
8532
+ capGnPerBlock: null,
8533
+ capSource: null,
8534
+ miner: null,
8535
+ bestTokensPerSecond: null,
8536
+ bestCapGnPerBlock: null,
8537
+ blockTimeMs: null,
8538
+ calibratedMiners: null,
8539
+ finalizedBlock: null,
8540
+ minTokensPerSecond: minimum === void 0 ? null : Number(minimum),
8541
+ meetsMinimum: minimum === void 0 ? null : false,
8542
+ reason: reason3
8543
+ };
8544
+ }
8545
+ function throughputCeiling(work, chain, minimum) {
8546
+ if (typeof work === "string") return unknown(null, minimum, work);
8547
+ if (work.flopsPerToken <= 0n) return unknown(work, minimum, "metering reports no work per completion token");
8548
+ if (chain === null) return unknown(work, minimum, "no chain connection");
8549
+ if (typeof chain === "string") return unknown(work, minimum, chain);
8550
+ if (chain.capGnPerBlock === null) {
8551
+ return unknown(work, minimum, chain.miner === null ? "no miner holds an accepted calibration cap at finality" : `miner ${chain.miner} holds no accepted calibration cap at finality`);
8552
+ }
8553
+ const flops = work.flopsPerToken;
8554
+ const centi = centiTokensPerSecond(chain.capGnPerBlock, chain.blockTimeMs, flops);
8555
+ let meets = null;
8556
+ if (minimum !== void 0) {
8557
+ const [num, den] = fraction(minimum);
8558
+ meets = chain.capGnPerBlock * FLOPS_PER_GN * 1000n * den >= num * chain.blockTimeMs * flops;
8559
+ }
8560
+ return {
8561
+ status: "known",
8562
+ tokensPerSecond: decimal(centi),
8563
+ gnPerToken: exactGn(flops),
8564
+ flopsPerToken: flops.toString(),
8565
+ capGnPerBlock: chain.capGnPerBlock.toString(),
8566
+ capSource: chain.capSource,
8567
+ miner: chain.miner,
8568
+ bestTokensPerSecond: chain.bestCapGnPerBlock === null ? null : decimal(centiTokensPerSecond(chain.bestCapGnPerBlock, chain.blockTimeMs, flops)),
8569
+ bestCapGnPerBlock: chain.bestCapGnPerBlock?.toString() ?? null,
8570
+ blockTimeMs: chain.blockTimeMs.toString(),
8571
+ calibratedMiners: chain.calibratedMiners,
8572
+ finalizedBlock: chain.finalizedBlock.toString(),
8573
+ minTokensPerSecond: minimum === void 0 ? null : Number(minimum),
8574
+ meetsMinimum: meets
8575
+ };
8576
+ }
8577
+ async function readChainCapInputs(api, miner) {
8578
+ const block = api.consts?.babe?.expectedBlockTime;
8579
+ const lease = api.consts?.hwCalibration?.calibrationLeaseBlocks;
8580
+ if (block == null) throw new Error("runtime constant babe.expectedBlockTime is missing");
8581
+ if (lease == null) throw new Error("runtime constant hwCalibration.calibrationLeaseBlocks is missing");
8582
+ const head = await api.rpc.chain.getFinalizedHead();
8583
+ const [view, header] = await Promise.all([api.at(head), api.rpc.chain.getHeader(head)]);
8584
+ const caps = view.query?.hwCalibration?.benchmarkBurstCaps;
8585
+ if (!caps?.entries) throw new Error("runtime has no hwCalibration.benchmarkBurstCaps");
8586
+ const now = BigInt(header.number.toString());
8587
+ const leaseBlocks = BigInt(lease.toString());
8588
+ const live = (raw) => {
8589
+ if (!raw?.isSome) return null;
8590
+ const cap = raw.unwrap();
8591
+ const acceptedAt = BigInt((cap.acceptedAt ?? cap.accepted_at).toString());
8592
+ const value = BigInt((cap.capGflopsPerSec ?? cap.cap_gflops_per_sec).toString());
8593
+ return now - acceptedAt >= leaseBlocks || value <= 0n ? null : value;
8594
+ };
8595
+ const entries = await caps.entries();
8596
+ const values = entries.map(([, raw]) => live(raw)).filter((value) => value !== null).sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
8597
+ const mid = Math.floor(values.length / 2);
8598
+ const median = values.length === 0 ? null : values.length % 2 === 1 ? values[mid] : (values[mid - 1] + values[mid]) / 2n;
8599
+ const capGnPerBlock = miner === void 0 ? median : live(await caps(miner));
8600
+ return {
8601
+ capGnPerBlock,
8602
+ capSource: miner === void 0 ? "chain-median" : "miner",
8603
+ miner: miner ?? null,
8604
+ bestCapGnPerBlock: values[values.length - 1] ?? null,
8605
+ blockTimeMs: BigInt(block.toString()),
8606
+ calibratedMiners: values.length,
8607
+ finalizedBlock: now
8608
+ };
8609
+ }
8610
+ var CEILING_READ_TIMEOUT_MS = 15e3;
8611
+ async function chainCapInputsOrReason(api, miner, timeoutMs = CEILING_READ_TIMEOUT_MS) {
8612
+ let timer;
8613
+ try {
8614
+ return await Promise.race([
8615
+ readChainCapInputs(api, miner),
8616
+ new Promise((_, reject) => {
8617
+ timer = setTimeout(() => reject(new Error(`timed out after ${timeoutMs} ms`)), timeoutMs);
8618
+ })
8619
+ ]);
8620
+ } catch (error) {
8621
+ return `chain read failed: ${error.message}`;
8622
+ } finally {
8623
+ clearTimeout(timer);
8624
+ }
8625
+ }
8626
+ function ceilingWarning(ceiling) {
8627
+ if (ceiling.status === "unknown") return `warning: paid throughput ceiling ${ceiling.tokensPerSecond}
8628
+ `;
8629
+ const at = ceiling.capSource === "miner" ? `miner ${ceiling.miner}'s cap` : "the median live cap";
8630
+ const detail = `${ceiling.gnPerToken} G_n/token at ${at} ${ceiling.capGnPerBlock} G_n/block; best of ${ceiling.calibratedMiners} calibrated miner(s) ${ceiling.bestTokensPerSecond ?? "n/a"} tok/s (not gated)`;
8631
+ const tail = "per channel, completion tokens only; prompt and attention G_n lower it";
8632
+ if (ceiling.meetsMinimum === false) {
8633
+ return `warning: paid throughput ceiling ${ceiling.tokensPerSecond} tok/s (${detail}) is below --min-tokens-per-second ${ceiling.minTokensPerSecond}; ${tail}
8634
+ `;
8635
+ }
8636
+ return `paid throughput ceiling: ${ceiling.tokensPerSecond} tok/s (${detail}); ${tail}
8637
+ `;
8638
+ }
8639
+ function enforceCeiling(ceiling) {
8640
+ if (ceiling.meetsMinimum !== false) return;
8641
+ throw new Error(
8642
+ ceiling.status === "unknown" ? `paid throughput ceiling is ${ceiling.tokensPerSecond}; --min-tokens-per-second ${ceiling.minTokensPerSecond} cannot be shown to hold` : `paid throughput ceiling ${ceiling.tokensPerSecond} tok/s is below --min-tokens-per-second ${ceiling.minTokensPerSecond}`
8643
+ );
8644
+ }
8645
+ function snakeCeiling(ceiling) {
8646
+ return Object.fromEntries(Object.entries(ceiling).map(([k, v]) => [k.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`), v]));
8647
+ }
8648
+
8649
+ // src/model-da-spot-check.ts
8650
+ import { createHash as createHash4, randomInt } from "node:crypto";
8651
+ var SPOT_CHECK_CHUNKS = 2;
8652
+ var BLOCK_SIZE = 4096;
8653
+ function randomChunks(n, k) {
8654
+ const picked = /* @__PURE__ */ new Set();
8655
+ while (picked.size < Math.min(n, k)) picked.add(randomInt(n));
8656
+ return [...picked].sort((a, b) => a - b);
8657
+ }
8658
+ async function spotCheckImage(files, imageSize, io) {
8659
+ const ordered = [...files].sort((a, b) => Buffer.from(a.path).compare(Buffer.from(b.path)));
8660
+ let cursor = 0;
8661
+ const layout = ordered.map((file) => {
8662
+ const start = cursor;
8663
+ cursor += Math.ceil(file.size / BLOCK_SIZE) * BLOCK_SIZE;
8664
+ return { file, start, dataEnd: start + file.size, end: cursor };
8665
+ });
8666
+ if (cursor !== imageSize) throw new Error(`file table packs to ${cursor} bytes, not the bound image's ${imageSize}`);
8667
+ const n = Math.max(1, Math.ceil(imageSize / io.chunkSize));
8668
+ const chunks = (io.pick ?? randomChunks)(n, SPOT_CHECK_CHUNKS);
8669
+ for (const j of chunks) {
8670
+ const chunkStart = j * io.chunkSize;
8671
+ const chunkEnd = Math.min(chunkStart + io.chunkSize, imageSize);
8672
+ const got = await io.readChunk(j);
8673
+ const expectedChunkSize = n === 1 ? imageSize : io.chunkSize;
8674
+ if (got.chunkSize !== expectedChunkSize || got.sizeBytes !== imageSize || got.offset !== chunkStart || got.bytes.length !== chunkEnd - chunkStart) {
8675
+ throw new Error(`DA chunk ${j} layout differs from the ${io.chunkSize}-byte chunking of a ${imageSize}-byte image`);
8676
+ }
8677
+ const at = (offset) => offset - chunkStart;
8678
+ for (const { file, start, dataEnd, end } of layout) {
8679
+ if (end <= chunkStart || start >= chunkEnd) continue;
8680
+ const from = Math.max(start, chunkStart), to = Math.min(dataEnd, chunkEnd);
8681
+ if (from < to) {
8682
+ const slice = got.bytes.subarray(at(from), at(to));
8683
+ if (start >= chunkStart && dataEnd <= chunkEnd) {
8684
+ if (createHash4("sha256").update(slice).digest("hex") !== file.sha256.toLowerCase().replace(/^0x/, "")) {
8685
+ throw new Error(`DA spot check: chunk ${j} of the bound image does not hold ${file.path} (sha256 differs)`);
8686
+ }
8687
+ } else {
8688
+ const hub = await io.readHubRange(file.path, from - start, to - start - 1, file.size);
8689
+ if (!Buffer.from(hub).equals(Buffer.from(slice))) {
8690
+ throw new Error(`DA spot check: chunk ${j} of the bound image differs from ${file.path} at the hub revision`);
8691
+ }
8692
+ }
8693
+ }
8694
+ const padFrom = Math.max(dataEnd, chunkStart), padTo = Math.min(end, chunkEnd);
8695
+ if (padFrom < padTo && got.bytes.subarray(at(padFrom), at(padTo)).some((b) => b !== 0)) {
8696
+ throw new Error(`DA spot check: chunk ${j} of the bound image has nonzero padding after ${file.path}`);
8697
+ }
8698
+ }
8699
+ }
8700
+ return { chunks, of: n };
8701
+ }
8702
+
8447
8703
  // src/commands/model-onboard.ts
8448
8704
  var run = promisify4(execFile4);
8449
8705
  var PRECISIONS = ["FP32", "FP16", "BF16", "INT8", "INT4", "Mixed"];
@@ -8455,7 +8711,7 @@ async function requireHybridApproval(api, arch) {
8455
8711
  async function requireHybridApprovalAt(view, arch) {
8456
8712
  const query = view.query.meteringVerifier?.hybridProfiles;
8457
8713
  if (!query) throw new Error("runtime has no hybrid proof profile approval; deploy the reviewed runtime first");
8458
- const hash2 = `0x${Buffer.from(computeChannel4.meteringArchHash(arch)).toString("hex")}`;
8714
+ const hash2 = `0x${Buffer.from(computeChannel5.meteringArchHash(arch)).toString("hex")}`;
8459
8715
  const value = await query(hash2);
8460
8716
  if (!value || value.isNone) throw new Error(`hybrid proof profile ${hash2} is not approved`);
8461
8717
  const profile = value.unwrap();
@@ -8467,6 +8723,9 @@ function activeHybridProfile(profile) {
8467
8723
  const image = optionValue(profile.imageId);
8468
8724
  return image !== null && codecHash(image) !== `0x${"00".repeat(32)}` && profile.epoch.toString() !== "0";
8469
8725
  }
8726
+ function modelBackupHandoff(modelHash, precision, revision) {
8727
+ return `flop model backup ${modelHash} --model-dir <path> --precision ${precision}${revision ? ` --revision ${revision}` : ""}`;
8728
+ }
8470
8729
  function normalizePrecision(raw) {
8471
8730
  const upper = raw.trim().toUpperCase();
8472
8731
  const precision = PRECISIONS.find((candidate) => candidate.toUpperCase() === upper);
@@ -8519,7 +8778,7 @@ function resolveParamsBillion(checkpoint, override) {
8519
8778
  throw new Error(`--params-billion must be a positive integer (got ${override})`);
8520
8779
  }
8521
8780
  if (override === registered) return override;
8522
- if (computeChannel4.isDenseGdnArch(checkpoint.arch)) throw new Error("--params-billion must match the complete checkpoint census");
8781
+ if (computeChannel5.isDenseGdnArch(checkpoint.arch)) throw new Error("--params-billion must match the complete checkpoint census");
8523
8782
  if (Math.abs(override - derived) > 0.05 * derived) {
8524
8783
  throw new Error(
8525
8784
  `--params-billion ${override} disagrees with config.json's ${derived.toFixed(2)}B parameters by more than 5%`
@@ -8549,9 +8808,9 @@ function onboardTeePlatforms(soft, raw) {
8549
8808
  return normalizeTeePlatforms(raw);
8550
8809
  }
8551
8810
  function normalizeHash(raw, name) {
8552
- const hex4 = raw.trim().replace(/^0x/i, "").toLowerCase();
8553
- if (!/^[0-9a-f]{64}$/.test(hex4)) throw new Error(`${name} must be 32-byte hex`);
8554
- return `0x${hex4}`;
8811
+ const hex5 = raw.trim().replace(/^0x/i, "").toLowerCase();
8812
+ if (!/^[0-9a-f]{64}$/.test(hex5)) throw new Error(`${name} must be 32-byte hex`);
8813
+ return `0x${hex5}`;
8555
8814
  }
8556
8815
  function identityFromPack(registryModel2, registryVersion, backendModelName, precision, decodePolicyHash, contentRoot, checkpoint, sourceRevision) {
8557
8816
  const measuredRoot = normalizeHash(contentRoot, "modelpack content_root");
@@ -8563,7 +8822,7 @@ function identityFromPack(registryModel2, registryVersion, backendModelName, pre
8563
8822
  decodePolicyHash: normalizeHash(decodePolicyHash, "decode policy hash"),
8564
8823
  measuredRoot,
8565
8824
  backendModelName,
8566
- archHash: `0x${Buffer.from(computeChannel4.meteringArchHash(checkpoint.arch)).toString("hex")}`,
8825
+ archHash: `0x${Buffer.from(computeChannel5.meteringArchHash(checkpoint.arch)).toString("hex")}`,
8567
8826
  quantization: catalogQuantization(checkpoint),
8568
8827
  gnPerPromptToken: checkpoint.gnPerPromptToken.toString(),
8569
8828
  gnPerCompletionToken: checkpoint.gnPerCompletionToken.toString(),
@@ -8632,10 +8891,10 @@ async function exists(path5) {
8632
8891
  async function materializeSource(raw, outputDir, token, sourceRevision, resumeState, files) {
8633
8892
  const local = await exists(raw);
8634
8893
  if (local) {
8635
- const stat = await lstat6(raw);
8636
- if (stat.isSymbolicLink()) throw new Error(`model source must not be a symlink: ${raw}`);
8637
- const modelDir = stat.isDirectory() ? resolve7(raw) : outputDir;
8638
- if (!stat.isDirectory()) {
8894
+ const stat2 = await lstat6(raw);
8895
+ if (stat2.isSymbolicLink()) throw new Error(`model source must not be a symlink: ${raw}`);
8896
+ const modelDir = stat2.isDirectory() ? resolve7(raw) : outputDir;
8897
+ if (!stat2.isDirectory()) {
8639
8898
  await writeModelFiles(outputDir, await readLocalModelFiles(raw));
8640
8899
  }
8641
8900
  const name = basename(resolve7(raw));
@@ -8716,7 +8975,7 @@ function boundCensus(stdout2, configHash) {
8716
8975
  return { total: BigInt(result.total_params), sourceFiles: result.source_files };
8717
8976
  }
8718
8977
  async function validateCapture(endpoint, identity, checkpoint, modelDir) {
8719
- const hybrid = computeChannel4.isDenseGdnArch(checkpoint.arch);
8978
+ const hybrid = computeChannel5.isDenseGdnArch(checkpoint.arch);
8720
8979
  const script = join5(repoRoot(), "infra/miner/vllm-toploc", hybrid ? "validate_hybrid_capture.py" : "validate_capture.py");
8721
8980
  try {
8722
8981
  await run("uv", ["run", "--no-project", "python", script], {
@@ -8744,8 +9003,8 @@ function optionValue(value) {
8744
9003
  }
8745
9004
  function codecText(value) {
8746
9005
  if (value?.toUtf8) return value.toUtf8();
8747
- const text3 = value?.toString?.() ?? String(value ?? "");
8748
- return /^0x(?:[0-9a-f]{2})+$/i.test(text3) ? Buffer.from(text3.slice(2), "hex").toString("utf8") : text3;
9006
+ const text4 = value?.toString?.() ?? String(value ?? "");
9007
+ return /^0x(?:[0-9a-f]{2})+$/i.test(text4) ? Buffer.from(text4.slice(2), "hex").toString("utf8") : text4;
8749
9008
  }
8750
9009
  function codecHash(value) {
8751
9010
  return normalizeHash(value?.toHex?.() ?? value?.toString?.() ?? String(value), "on-chain hash");
@@ -8794,7 +9053,7 @@ async function requireApprovalEvidence(api, modelHash, precision, signer, imageS
8794
9053
  if (!Array.from(members).some((member) => sameAccount(member, signer))) throw new Error("approval signer is not an active registrar");
8795
9054
  if (sameAccount(model.publisher, signer)) throw new Error("registrar cannot approve its own published model");
8796
9055
  }
8797
- const requiresHybrid = computeChannel4.isDenseGdnArch(arch);
9056
+ const requiresHybrid = computeChannel5.isDenseGdnArch(arch);
8798
9057
  if (!root) {
8799
9058
  const requirements = optionValue(await registry.admissionRequirements(modelHash));
8800
9059
  if (!requirements) throw new Error("approval requires model admission requirements");
@@ -8808,7 +9067,7 @@ async function requireApprovalEvidence(api, modelHash, precision, signer, imageS
8808
9067
  }
8809
9068
  const archHash = optionValue(rawArch);
8810
9069
  if (!archHash) throw new Error("approval requires a model architecture hash");
8811
- const expectedArchHash = `0x${Buffer.from(computeChannel4.meteringArchHash(arch)).toString("hex")}`;
9070
+ const expectedArchHash = `0x${Buffer.from(computeChannel5.meteringArchHash(arch)).toString("hex")}`;
8812
9071
  if (codecHash(archHash) !== expectedArchHash) throw new Error("approval architecture hash differs from the checkpoint");
8813
9072
  const policies = optionValue(rawPolicies);
8814
9073
  if (!policies || policies.length === 0) throw new Error("approval requires a nonempty decode-policy allowlist");
@@ -8826,6 +9085,7 @@ async function requireApprovalEvidence(api, modelHash, precision, signer, imageS
8826
9085
  const leaseEnd = metadata.leaseEnd ?? metadata.lease_end;
8827
9086
  const header = await api.rpc.chain.getHeader(head);
8828
9087
  if (leaseEnd === null || leaseEnd === void 0 || BigInt(leaseEnd) <= BigInt(header.number.toString())) throw new Error("approval requires a live DA lease");
9088
+ let boundTable = null;
8829
9089
  if (files) {
8830
9090
  const sdk = await import("@flop-labs/sdk");
8831
9091
  const { blake2AsHex: blake2AsHex2 } = await import("@polkadot/util-crypto");
@@ -8834,7 +9094,15 @@ async function requireApprovalEvidence(api, modelHash, precision, signer, imageS
8834
9094
  throw new Error("approval file table does not hash to model_hash");
8835
9095
  }
8836
9096
  const tableCommitment = normalizeHash(blake2AsHex2(table2, 256), "file table commitment");
8837
- const mirrors = await sdk.readModelMirrors(view, modelHash);
9097
+ boundTable = tableCommitment;
9098
+ if (!registry.modelMirrors) throw new Error("cannot read ModelMirrors at finality: the runtime has no modelRegistry.modelMirrors");
9099
+ let rawMirrors;
9100
+ try {
9101
+ rawMirrors = await registry.modelMirrors(modelHash);
9102
+ } catch (error) {
9103
+ throw new Error(`cannot read ModelMirrors at finality: ${error.message}`);
9104
+ }
9105
+ const mirrors = rawMirrors?.isSome ? sdk.decodeModelSources(rawMirrors.unwrap().toJSON()) : [];
8838
9106
  if (!mirrors.some((m) => m.kind === "da" && normalizeHash(String(m.commitment), "DA mirror commitment") === tableCommitment)) {
8839
9107
  throw new Error("approval requires the DA mirror to name the model's file table");
8840
9108
  }
@@ -8855,7 +9123,7 @@ async function requireApprovalEvidence(api, modelHash, precision, signer, imageS
8855
9123
  }
8856
9124
  const status = String(model.status);
8857
9125
  if (status !== "Pending" && status !== "Verified") throw new Error(`model cannot be approved in ${status} status`);
8858
- return { approved: status === "Verified" };
9126
+ return { approved: status === "Verified", weights: ref.commitment, table: boundTable };
8859
9127
  }
8860
9128
  function decodeDataRef(value) {
8861
9129
  const ref = optionValue(value);
@@ -8920,12 +9188,19 @@ function missingMirrors(registered, desired) {
8920
9188
  return !registered.some((actual) => sourceMatches(actual, comparable));
8921
9189
  });
8922
9190
  }
8923
- async function verifiedHuggingFaceMirror(repo, verityRoot, verify) {
9191
+ async function verifiedHuggingFaceMirror(repo, contentRoot, hub) {
8924
9192
  if (!/^[^/\s]+\/[^/\s]+$/.test(repo)) throw new Error(`--hf-repo must be <owner>/<model> (got ${repo})`);
8925
- const result = await verify({ kind: "huggingface", repo }, verityRoot);
9193
+ const bare = (hex5) => hex5.toLowerCase().replace(/^0x/, "");
9194
+ const table2 = await hub.preflight(repo);
9195
+ if (bare(table2.contentRoot) !== bare(contentRoot)) {
9196
+ throw new Error(
9197
+ `Hugging Face ${repo} at ${table2.revision} has content_root 0x${bare(table2.contentRoot)}, not the measured root ${contentRoot}; nothing was downloaded. Pass the --revision the local checkpoint came from, onboard from hf:${repo} so the registered files are the repo's, or omit --hf-repo`
9198
+ );
9199
+ }
9200
+ const result = await hub.verify({ kind: "huggingface", repo }, contentRoot, table2.revision);
8926
9201
  if (!result.ok) {
8927
9202
  throw new Error(
8928
- `Hugging Face ${repo} does not reproduce the measured root ${verityRoot} (${result.reason ?? "no match"}); onboard from hf:${repo} so the registered files are the repo's, or omit --hf-repo`
9203
+ `Hugging Face ${repo} does not reproduce the measured root ${contentRoot} (${result.reason ?? "no match"}); onboard from hf:${repo} so the registered files are the repo's, or omit --hf-repo`
8929
9204
  );
8930
9205
  }
8931
9206
  return { mirror: { kind: "huggingface", repo }, revision: result.revision ?? null };
@@ -8969,7 +9244,14 @@ function modelOnboardCmd() {
8969
9244
  ).option("--registry-model <name>", "Registry model name").option("--registry-version <version>", "Registry version", "1").option("--backend-model-name <name>", "Exact model name returned by the serving backend").option("--da-validator <path>", "da-validator executable", "da-validator").option(
8970
9245
  "--hf-repo <owner/model>",
8971
9246
  "Hugging Face repo a local --source came from; registered as a mirror only if it reproduces the measured root"
8972
- ).option("--revision <sha>", "Immutable Hugging Face commit; first resolution is retained when omitted").option("--state-dir <path>", "Private resumable workspace (defaults to XDG_STATE_HOME/flop/model-onboarding)").option("--registrar-keystore <path>", "Independent registrar keystore for registration pins and approval").option("--registrar-password-env <name>", "Environment variable containing the registrar keystore password").option("--approve", "Approve after rechecking all admission evidence at one finalized head").option("--paid-admission", "Check paid devnet prerequisites after registration; fail closed at an unavailable independent audit gate").option("--session-endpoint <url>", "Authenticated provisioned miner session endpoint for paid admission").option("--token <token>", "Hugging Face bearer token (prefer HF_TOKEN)").option("--json", "Output the identity block as JSON").action(async (opts, command) => {
9247
+ ).option(
9248
+ "--revision <sha>",
9249
+ "Immutable Hugging Face commit (of --source, or of --hf-repo for a local source); first resolution is retained when omitted"
9250
+ ).option("--state-dir <path>", "Private resumable workspace (defaults to XDG_STATE_HOME/flop/model-onboarding)").option("--registrar-keystore <path>", "Independent registrar keystore for registration pins and approval").option("--registrar-password-env <name>", "Environment variable containing the registrar keystore password").option("--approve", "Approve after rechecking all admission evidence at one finalized head").option("--paid-admission", "Check paid devnet prerequisites after registration; fail closed at an unavailable independent audit gate").option("--session-endpoint <url>", "Authenticated provisioned miner session endpoint for paid admission").option("--token <token>", "Hugging Face bearer token (prefer HF_TOKEN)").option(
9251
+ "--min-tokens-per-second <n>",
9252
+ "Fail before any chain write unless the paid completion-token ceiling per channel at the median live calibration cap is at least n; an unknown ceiling fails",
9253
+ parseMinTokensPerSecond
9254
+ ).option("--miner <address>", "Compute the ceiling at this miner's accepted calibration cap instead of the median live one; the best is reported, never gated").option("--json", "Output the identity block as JSON").action(async (opts, command) => {
8973
9255
  let client;
8974
9256
  let registrar;
8975
9257
  let workDir;
@@ -8986,15 +9268,12 @@ function modelOnboardCmd() {
8986
9268
  const local = await exists(opts.source);
8987
9269
  if (local && opts.download) throw new Error("--download is for a Hugging Face source; a local source is always packed");
8988
9270
  const metadataOnly = !local && !opts.download;
8989
- if (metadataOnly && opts.approve) {
8990
- throw new Error("--approve needs the model's DA backup (packed image and file table); rerun with --download to upload it in the same run");
8991
- }
8992
9271
  const binding = {
8993
9272
  source: opts.source,
8994
9273
  revision: opts.revision ?? null,
8995
9274
  network,
8996
- rpcHash: createHash3("sha256").update(getRpc(command)).digest("hex"),
8997
- captureEndpointHash: createHash3("sha256").update(opts.captureEndpoint).digest("hex"),
9275
+ rpcHash: createHash5("sha256").update(getRpc(command)).digest("hex"),
9276
+ captureEndpointHash: createHash5("sha256").update(opts.captureEndpoint).digest("hex"),
8998
9277
  registryModel: opts.registryModel ?? null,
8999
9278
  registryVersion: opts.registryVersion,
9000
9279
  backendModelName: opts.backendModelName ?? null,
@@ -9005,7 +9284,7 @@ function modelOnboardCmd() {
9005
9284
  hfRepo: opts.hfRepo ?? null,
9006
9285
  packContract: metadataOnly ? "content-v1-metadata" : "modelpack-v1-strict",
9007
9286
  paidAdmission: opts.paidAdmission === true,
9008
- sessionEndpointHash: opts.sessionEndpoint ? createHash3("sha256").update(opts.sessionEndpoint).digest("hex") : null
9287
+ sessionEndpointHash: opts.sessionEndpoint ? createHash5("sha256").update(opts.sessionEndpoint).digest("hex") : null
9009
9288
  };
9010
9289
  workDir = onboardingDirectory(binding, opts.stateDir);
9011
9290
  job = await openOnboardingState(workDir, binding);
@@ -9019,17 +9298,32 @@ function modelOnboardCmd() {
9019
9298
  preflight = await sdk2.preflightHuggingFace(hubRepo, { revision: job.state.sourceRevision ?? opts.revision, token: opts.token ?? process.env.HF_TOKEN });
9020
9299
  if (!preflight.configJson) throw new Error("checkpoint has no root config.json; a workload-specific ingestion contract is required");
9021
9300
  job.state.sourceRevision = preflight.revision;
9022
- job.state.configHash = createHash3("sha256").update(preflight.configJson).digest("hex");
9301
+ job.state.configHash = createHash5("sha256").update(preflight.configJson).digest("hex");
9023
9302
  job.state.phase = "preflight";
9024
9303
  await job.save();
9025
9304
  process.stderr.write(`content_root ${preflight.contentRoot}: ${preflight.files.length} files, image ${preflight.imageSize} bytes, ${preflight.daChunks} DA chunk(s) at ${preflight.revision}
9026
9305
  `);
9027
- const candidate = computeChannel4.parseMeteringConfig(preflight.configJson, preflight.hfQuantConfigJson);
9028
- if (computeChannel4.isDenseGdnArch(candidate.arch)) {
9306
+ const candidate = computeChannel5.parseMeteringConfig(preflight.configJson, preflight.hfQuantConfigJson);
9307
+ if (computeChannel5.isDenseGdnArch(candidate.arch)) {
9029
9308
  client = await createClient(getRpc(command), getSigner(command));
9030
9309
  await requireHybridApproval(client.api, candidate.arch);
9031
9310
  }
9032
- } else if (opts.revision) throw new Error("--revision is only for a Hugging Face source");
9311
+ } else if (opts.revision && !opts.hfRepo) throw new Error("--revision is only for a Hugging Face source or a local source's --hf-repo");
9312
+ const checkCeiling = async (load) => {
9313
+ let work;
9314
+ try {
9315
+ work = perTokenWork(computeChannel5.parseMeteringConfig(...await load()));
9316
+ } catch (error) {
9317
+ work = `no metering arch: ${error.message}`;
9318
+ }
9319
+ client ??= await createClient(getRpc(command), getSigner(command));
9320
+ const result = throughputCeiling(work, await chainCapInputsOrReason(client.api, opts.miner), opts.minTokensPerSecond);
9321
+ process.stderr.write(ceilingWarning(result));
9322
+ enforceCeiling(result);
9323
+ return result;
9324
+ };
9325
+ let ceiling;
9326
+ if (preflight) ceiling = await checkCeiling(async () => [preflight.configJson, preflight.hfQuantConfigJson ?? null]);
9033
9327
  let source;
9034
9328
  if (metadataOnly) {
9035
9329
  const modelDir = join5(workDir, "metadata");
@@ -9047,14 +9341,15 @@ function modelOnboardCmd() {
9047
9341
  } else {
9048
9342
  source = await materializeSource(opts.source, sourceDir, opts.token, job.state.sourceRevision, join5(workDir, "download.json"), preflight?.files);
9049
9343
  }
9344
+ ceiling ??= await checkCeiling(async () => [await readFile7(join5(source.modelDir, "config.json")), await readHfQuantConfig(source.modelDir)]);
9050
9345
  let checkpoint;
9051
9346
  let parameterFiles;
9052
9347
  try {
9053
9348
  const raw = await readFile7(join5(source.modelDir, "config.json"), "utf8");
9054
- if (job.state.configHash && createHash3("sha256").update(raw).digest("hex") !== job.state.configHash) throw new Error("downloaded config differs from preflight");
9055
- checkpoint = computeChannel4.parseMeteringConfig(Buffer.from(raw), await readHfQuantConfig(source.modelDir));
9349
+ if (job.state.configHash && createHash5("sha256").update(raw).digest("hex") !== job.state.configHash) throw new Error("downloaded config differs from preflight");
9350
+ checkpoint = computeChannel5.parseMeteringConfig(Buffer.from(raw), await readHfQuantConfig(source.modelDir));
9056
9351
  if (checkpoint.totalParams === null) {
9057
- const configHash = createHash3("sha256").update(raw).digest("hex");
9352
+ const configHash = createHash5("sha256").update(raw).digest("hex");
9058
9353
  const census = metadataOnly ? await censusHeaders(workDir, preflight, await (await import("@flop-labs/sdk")).readSafetensorsHeaders(
9059
9354
  hubRepo,
9060
9355
  preflight.revision,
@@ -9101,11 +9396,11 @@ function modelOnboardCmd() {
9101
9396
  let { mirror, revision } = source;
9102
9397
  if (opts.hfRepo) {
9103
9398
  if (mirror) throw new Error("--hf-repo is for a local --source; a Hugging Face --source registers its own mirror");
9104
- ({ mirror, revision } = await verifiedHuggingFaceMirror(
9105
- opts.hfRepo,
9106
- measured.contentRoot,
9107
- (hf, root) => sdk.verifyMirror(hf, root, { token: opts.token ?? process.env.HF_TOKEN })
9108
- ));
9399
+ const token = opts.token ?? process.env.HF_TOKEN;
9400
+ ({ mirror, revision } = await verifiedHuggingFaceMirror(opts.hfRepo, measured.contentRoot, {
9401
+ preflight: (repo) => sdk.preflightHuggingFace(repo, { revision: opts.revision, token }),
9402
+ verify: (hf, root, at) => sdk.verifyMirror(hf, root, { revision: at, token })
9403
+ }));
9109
9404
  }
9110
9405
  const identity = identityFromPack(
9111
9406
  opts.registryModel ?? source.registryModel,
@@ -9135,10 +9430,15 @@ function modelOnboardCmd() {
9135
9430
  if (job.state.chain && JSON.stringify(job.state.chain) !== JSON.stringify(chain)) throw new Error("onboarding chain/publisher resume conflict");
9136
9431
  job.state.chain = chain;
9137
9432
  await job.save();
9138
- if (computeChannel4.isDenseGdnArch(checkpoint.arch)) await requireHybridApproval(client.api, checkpoint.arch);
9433
+ if (computeChannel5.isDenseGdnArch(checkpoint.arch)) await requireHybridApproval(client.api, checkpoint.arch);
9139
9434
  const submit = (tx) => submitOnboardingTx(client.api, tx, client.signer, job);
9140
9435
  let state = await readState(client.api, sdk, identity.modelHash, precision);
9141
- const checkedParams = computeChannel4.isDenseGdnArch(checkpoint.arch) ? paramsBillion : void 0;
9436
+ if (metadataOnly && opts.approve && (!state.registered || !state.weightsRef)) {
9437
+ throw new Error(
9438
+ `--approve on a metadata run needs the bound DA backup of ${identity.modelHash}; register without --approve, run \`${modelBackupHandoff(identity.modelHash, precision, preflight?.revision)}\` on a box with room for the checkpoint, bind it with \`flop model upload --commitment\` and \`flop model mirrors set --append --source da:<table_ref>\`, then rerun with --approve (or rerun with --download --approve from a fresh --state-dir)`
9439
+ );
9440
+ }
9441
+ const checkedParams = computeChannel5.isDenseGdnArch(checkpoint.arch) ? paramsBillion : void 0;
9142
9442
  planOnboard(state, identity, precision, teePlatforms, checkedParams);
9143
9443
  await validateCapture(opts.captureEndpoint, identity, checkpoint, source.modelDir);
9144
9444
  job.state.phase = "capture-verified";
@@ -9149,21 +9449,46 @@ function modelOnboardCmd() {
9149
9449
  throw new Error("pending registration parameter count differs from the checkpoint census; original transaction retained");
9150
9450
  }
9151
9451
  }
9152
- await recoverOnboardingTx(client.api, job, async (original) => {
9153
- if (!containsApproval(original.method)) return;
9154
- if (!opts.approve) throw new Error("pending approval requires --approve before rebroadcast; signed transaction retained");
9155
- const signer = original.signer.toString();
9156
- const mode = await registryAuthorityMode(client.api, signer, network);
9157
- await requireApprovalEvidence(
9158
- client.api,
9452
+ let daSpotCheck;
9453
+ const approvalEvidence = async (api, signer, root) => {
9454
+ const evidence = await requireApprovalEvidence(
9455
+ api,
9159
9456
  identity.modelHash,
9160
9457
  precision,
9161
9458
  signer,
9162
9459
  measured.imageSize,
9163
9460
  checkpoint.arch,
9164
- mode === "root",
9461
+ root,
9165
9462
  measured.files
9166
9463
  );
9464
+ if (!metadataOnly || evidence.approved) return evidence;
9465
+ const token = opts.token ?? process.env.HF_TOKEN;
9466
+ daSpotCheck = await spotCheckImage(measured.files, measured.imageSize, {
9467
+ chunkSize: sdk.DA_CHUNK_SIZE,
9468
+ readChunk: (j) => daRetrieveChunk(opts.daValidator, getRpc(command), evidence.weights, j),
9469
+ readHubRange: (path5, start, end, size) => readHuggingFaceRange(hubRepo, preflight.revision, path5, start, end, size, token)
9470
+ });
9471
+ const again = await requireApprovalEvidence(
9472
+ api,
9473
+ identity.modelHash,
9474
+ precision,
9475
+ signer,
9476
+ measured.imageSize,
9477
+ checkpoint.arch,
9478
+ root,
9479
+ measured.files
9480
+ );
9481
+ if (again.weights !== evidence.weights || again.table !== evidence.table) {
9482
+ throw new Error("the bound DA image or file table changed during the spot check; nothing was submitted");
9483
+ }
9484
+ return again;
9485
+ };
9486
+ await recoverOnboardingTx(client.api, job, async (original) => {
9487
+ if (!containsApproval(original.method)) return;
9488
+ if (!opts.approve) throw new Error("pending approval requires --approve before rebroadcast; signed transaction retained");
9489
+ const signer = original.signer.toString();
9490
+ const mode = await registryAuthorityMode(client.api, signer, network);
9491
+ await approvalEvidence(client.api, signer, mode === "root");
9167
9492
  });
9168
9493
  state = await readState(client.api, sdk, identity.modelHash, precision);
9169
9494
  const plan = planOnboard(state, identity, precision, teePlatforms, checkedParams);
@@ -9295,7 +9620,7 @@ function modelOnboardCmd() {
9295
9620
  const view = await authority.api.at(await authority.api.rpc.chain.getFinalizedHead());
9296
9621
  const raw = await view.query.modelRegistry.admissionRequirements(identity.modelHash);
9297
9622
  const requirements = optionValue(raw);
9298
- const expectedHybrid = computeChannel4.isDenseGdnArch(checkpoint.arch);
9623
+ const expectedHybrid = computeChannel5.isDenseGdnArch(checkpoint.arch);
9299
9624
  if (requirements) {
9300
9625
  const metadata = requirements.toJSON?.() ?? requirements;
9301
9626
  if (BigInt(metadata.weightsSizeBytes ?? metadata.weights_size_bytes) !== BigInt(measured.imageSize) || (metadata.requiresHybridProfile ?? metadata.requires_hybrid_profile) !== expectedHybrid) {
@@ -9310,16 +9635,7 @@ function modelOnboardCmd() {
9310
9635
  }
9311
9636
  }
9312
9637
  if (opts.approve) {
9313
- const evidence = await requireApprovalEvidence(
9314
- authority.api,
9315
- identity.modelHash,
9316
- precision,
9317
- authority.signer.address,
9318
- measured.imageSize,
9319
- checkpoint.arch,
9320
- mode === "root",
9321
- measured.files
9322
- );
9638
+ const evidence = await approvalEvidence(authority.api, authority.signer.address, mode === "root");
9323
9639
  if (!evidence.approved) {
9324
9640
  const call = authority.api.tx.modelRegistry.approveModel(identity.modelHash, authority.signer.address);
9325
9641
  await submitOnboardingTx(authority.api, registryAuthorityCall(authority.api, call, mode), authority.signer, job);
@@ -9352,7 +9668,7 @@ function modelOnboardCmd() {
9352
9668
  if (metadataOnly && !state.weightsRef) {
9353
9669
  process.stderr.write(
9354
9670
  `next: approval needs the DA backup of ${identity.modelHash} (packed image + file table, ${measured.imageSize} bytes in ${Math.max(1, Math.ceil(measured.imageSize / sdk.DA_CHUNK_SIZE))} chunk(s)). On a box with room for the checkpoint, run
9355
- flop model backup ${identity.modelHash} --model-dir <path>${preflight ? ` --revision ${preflight.revision}` : ""}
9671
+ ${modelBackupHandoff(identity.modelHash, precision, preflight?.revision)}
9356
9672
  then bind the DataRef it prints. Rerunning this command with --download packs the image from disk instead, which needs about twice the checkpoint's size.
9357
9673
  `
9358
9674
  );
@@ -9362,7 +9678,9 @@ then bind the DataRef it prints. Rerunning this command with --download packs th
9362
9678
  ...identity,
9363
9679
  registered: state.registered,
9364
9680
  approved: state.approved === true,
9365
- ...metadataOnly ? { contentRoot: measured.contentRoot, imageSize: measured.imageSize, daBackup: state.weightsRef ? "present" : "pending" } : {}
9681
+ throughputCeiling: ceiling,
9682
+ ...metadataOnly ? { contentRoot: measured.contentRoot, imageSize: measured.imageSize, daBackup: state.weightsRef ? "present" : "pending" } : {},
9683
+ ...daSpotCheck ? { daSpotCheck } : {}
9366
9684
  },
9367
9685
  getJson(command)
9368
9686
  );
@@ -9381,7 +9699,10 @@ then bind the DataRef it prints. Rerunning this command with --download packs th
9381
9699
  // src/commands/model-upload.ts
9382
9700
  var PRECISIONS2 = ["FP32", "FP16", "BF16", "INT8", "INT4", "Mixed"];
9383
9701
  function modelUploadCmd() {
9384
- return new Command30("upload").description("Push verified weights to validator DA, register the lease, and bind the model").requiredOption("--model-hash <hex>", "Registered model hash").option("--path <path>", "Local weight file or directory (packed and streamed from disk)").option("--source <source>", "Remote source: hf:<repo>, modelscope:<repo>, https://..., da:<commitment>").addOption(new Option2("--precision <precision>", "Measured-root precision").choices(PRECISIONS2).default("FP16")).option("--da-endpoint <url>", "DA blob gateway for a da:<commitment> source").option("--da-validator <path>", "da-validator executable", "da-validator").option("--token <token>", "HF/ModelScope bearer token (prefer HF_TOKEN)").option("--commitment <hex>", "Existing DA commitment (commitment-only recovery mode)").option("--size <bytes>", "Existing DA blob size (required with --commitment)").option("--json", "Output as JSON").action(async (opts, command) => {
9702
+ return new Command30("upload").description("Push verified weights to validator DA, register the lease, and bind the model").requiredOption("--model-hash <hex>", "Registered model hash").option("--path <path>", "Local weight file or directory (packed and streamed from disk)").option("--source <source>", "Remote source: hf:<repo>, modelscope:<repo>, https://..., da:<commitment>").option(
9703
+ "--revision <sha>",
9704
+ "Immutable 40-hex commit for an hf: --source; when omitted, main is used only if its sha256 table reproduces the registered content-v1 root"
9705
+ ).addOption(new Option2("--precision <precision>", "Measured-root precision").choices(PRECISIONS2).default("FP16")).option("--da-endpoint <url>", "DA blob gateway for a da:<commitment> source").option("--da-validator <path>", "da-validator executable", "da-validator").option("--token <token>", "HF/ModelScope bearer token (prefer HF_TOKEN)").option("--commitment <hex>", "Existing DA commitment (commitment-only recovery mode)").option("--size <bytes>", "Existing DA blob size (required with --commitment)").option("--json", "Output as JSON").action(async (opts, command) => {
9385
9706
  let client;
9386
9707
  let tempDir;
9387
9708
  try {
@@ -9391,6 +9712,9 @@ function modelUploadCmd() {
9391
9712
  }
9392
9713
  if (opts.commitment && !opts.size) throw new Error("--size is required with --commitment");
9393
9714
  if (opts.size && !opts.commitment) throw new Error("--commitment is required with --size");
9715
+ const revision = parseRevision(opts.revision);
9716
+ const remote = opts.source ? parseModelSource(opts.source) : void 0;
9717
+ if (revision && remote?.kind !== "huggingface") throw new Error("--revision is only for an hf: --source");
9394
9718
  const sdk = await import("@flop-labs/sdk");
9395
9719
  const rpc = getRpc(command);
9396
9720
  client = await createClient(rpc, getSigner(command));
@@ -9403,20 +9727,32 @@ function modelUploadCmd() {
9403
9727
  let imagePath;
9404
9728
  let custody;
9405
9729
  if (!commitment) {
9406
- tempDir = await mkdtemp(join6(tmpdir(), "flop-model-upload-"));
9730
+ tempDir = await mkdtemp2(join6(tmpdir2(), "flop-model-upload-"));
9407
9731
  imagePath = join6(tempDir, "weights.img");
9732
+ const expected = await sdk.readMeasuredRoot(client.api, opts.modelHash, opts.precision);
9733
+ if (!expected) throw new Error(`no measured root for ${opts.modelHash} at ${opts.precision}`);
9408
9734
  if (opts.path) {
9409
9735
  let modelDir = resolve8(opts.path);
9410
- const stat = await lstat7(modelDir);
9411
- if (stat.isSymbolicLink()) throw new Error(`model source must not be a symlink: ${opts.path}`);
9412
- if (stat.isFile()) {
9736
+ const stat2 = await lstat7(modelDir);
9737
+ if (stat2.isSymbolicLink()) throw new Error(`model source must not be a symlink: ${opts.path}`);
9738
+ if (stat2.isFile()) {
9413
9739
  modelDir = join6(tempDir, "model");
9414
9740
  await mkdir11(modelDir);
9415
9741
  await copyFile(resolve8(opts.path), join6(modelDir, basename2(opts.path)));
9416
9742
  }
9417
9743
  measured = await packModel(modelDir, imagePath, join6(tempDir, "manifest.json"));
9744
+ } else if (remote.kind === "huggingface" && !remote.path) {
9745
+ const modelDir = join6(tempDir, "model");
9746
+ await mkdir11(modelDir);
9747
+ await downloadPinnedHuggingFace(remote.repo, modelDir, expected, {
9748
+ revision,
9749
+ token: opts.token ?? process.env.HF_TOKEN
9750
+ });
9751
+ const roots = await sdk.measureModelDir(modelDir);
9752
+ measured = { content_root: roots.contentRoot, verity_root: roots.verityRoot };
9753
+ await pipeline2(sdk.packedImageStream(modelDir, roots.files), createWriteStream(imagePath));
9418
9754
  } else {
9419
- const files = (await sdk.fetchWeights(parseModelSource(opts.source), {
9755
+ const files = (await sdk.fetchWeights(remote, {
9420
9756
  token: opts.token ?? process.env.HF_TOKEN,
9421
9757
  daSource: daHttpSource(opts.daEndpoint)
9422
9758
  })).files;
@@ -9424,8 +9760,6 @@ function modelUploadCmd() {
9424
9760
  measured = { content_root: roots.contentRoot, verity_root: roots.verityRoot };
9425
9761
  await writeFile9(imagePath, sdk.buildImage(files));
9426
9762
  }
9427
- const expected = await sdk.readMeasuredRoot(client.api, opts.modelHash, opts.precision);
9428
- if (!expected) throw new Error(`no measured root for ${opts.modelHash} at ${opts.precision}`);
9429
9763
  const scheme = sdk.rootScheme({ contentRoot: measured.content_root, verityRoot: measured.verity_root }, expected);
9430
9764
  if (!scheme) {
9431
9765
  throw new Error(`source content_root ${measured.content_root} and verity_root ${measured.verity_root} != registered ${expected}`);
@@ -9477,7 +9811,7 @@ function modelUploadCmd() {
9477
9811
  next: "rerun the same command to resume the DA push; to abandon a registered blob, run the printed `flop da withdraw` recovery"
9478
9812
  });
9479
9813
  } finally {
9480
- if (tempDir) await rm3(tempDir, { recursive: true, force: true });
9814
+ if (tempDir) await rm4(tempDir, { recursive: true, force: true });
9481
9815
  await client?.disconnect();
9482
9816
  }
9483
9817
  });
@@ -9522,12 +9856,187 @@ function modelAuditCmd() {
9522
9856
  });
9523
9857
  }
9524
9858
 
9525
- // src/commands/model-preflight.ts
9859
+ // src/commands/model-mirrors.ts
9526
9860
  import { Command as Command32 } from "commander";
9861
+ var MIRROR_URI_MAX_BYTES = 512;
9862
+ var MAX_MODEL_MIRRORS = 8;
9863
+ var REPO = /^[^/\s]+\/[^/\s]+$/;
9864
+ function boundedUri(value, what) {
9865
+ if (Buffer.byteLength(value, "utf8") > MIRROR_URI_MAX_BYTES) {
9866
+ throw new Error(`${what} exceeds the registry's ${MIRROR_URI_MAX_BYTES}-byte bound`);
9867
+ }
9868
+ return value;
9869
+ }
9870
+ function parseMirrorSource(raw) {
9871
+ if (raw.startsWith("https://")) {
9872
+ let url;
9873
+ try {
9874
+ url = new URL(raw);
9875
+ } catch {
9876
+ throw new Error(`invalid https mirror URL: ${raw}`);
9877
+ }
9878
+ if (url.protocol !== "https:" || !url.hostname) throw new Error(`invalid https mirror URL: ${raw}`);
9879
+ if (url.username || url.password) throw new Error("an https mirror URL must not carry credentials");
9880
+ return { kind: "https", url: boundedUri(raw, "https mirror URL") };
9881
+ }
9882
+ for (const [prefix, kind] of [["hf:", "huggingface"], ["huggingface:", "huggingface"], ["modelscope:", "modelscope"]]) {
9883
+ if (raw.startsWith(prefix)) {
9884
+ const repo = raw.slice(prefix.length);
9885
+ if (!REPO.test(repo)) throw new Error(`${kind} mirror must be ${prefix}<owner>/<model> (got ${raw})`);
9886
+ return { kind, repo: boundedUri(repo, `${kind} repo id`) };
9887
+ }
9888
+ }
9889
+ if (raw.startsWith("da:")) return { kind: "da", commitment: normalizeHash(raw.slice(3), "DA mirror commitment") };
9890
+ throw new Error(`unsupported mirror "${raw}"; use https://..., hf:<owner>/<model>, modelscope:<owner>/<model> or da:<commitment>`);
9891
+ }
9892
+ function mirrorKey(entry) {
9893
+ if (entry.kind === "https") return `https:${entry.url}`;
9894
+ if (entry.kind === "da") return `da:${entry.commitment}@${entry.providerId ?? "?"}`;
9895
+ return `${entry.kind}:${entry.repo}`;
9896
+ }
9897
+ function chainModelSource(entry) {
9898
+ switch (entry.kind) {
9899
+ case "https":
9900
+ return { Https: entry.url };
9901
+ case "huggingface":
9902
+ return { HuggingFace: entry.repo };
9903
+ case "modelscope":
9904
+ return { ModelScope: entry.repo };
9905
+ case "da":
9906
+ if (entry.providerId === void 0 || entry.retention === void 0) {
9907
+ throw new Error(`DA mirror ${entry.commitment} has no resolved provider or retention`);
9908
+ }
9909
+ return { DaLayer: { commitment: entry.commitment, provider_id: entry.providerId, retention: entry.retention } };
9910
+ }
9911
+ }
9912
+ function decodeRegisteredMirror(json) {
9913
+ const text4 = (hex5) => {
9914
+ const value = String(hex5);
9915
+ return /^0x([0-9a-f]{2})*$/i.test(value) ? Buffer.from(value.slice(2), "hex").toString("utf8") : value;
9916
+ };
9917
+ if (json?.https !== void 0) return { kind: "https", url: text4(json.https) };
9918
+ if (json?.huggingFace !== void 0) return { kind: "huggingface", repo: text4(json.huggingFace) };
9919
+ if (json?.modelScope !== void 0) return { kind: "modelscope", repo: text4(json.modelScope) };
9920
+ if (json?.daLayer !== void 0) {
9921
+ const ref = json.daLayer;
9922
+ return {
9923
+ kind: "da",
9924
+ commitment: normalizeHash(String(ref.commitment), "registered DA mirror commitment"),
9925
+ providerId: Number(ref.providerId ?? ref.provider_id),
9926
+ retention: String(ref.retention)
9927
+ };
9928
+ }
9929
+ throw new Error(`registered mirror has an unknown shape: ${JSON.stringify(json)}`);
9930
+ }
9931
+ function planMirrorList(requested, opts) {
9932
+ if (opts.clear) {
9933
+ if (requested.length > 0 || opts.append) throw new Error("--clear takes no --source and no --append");
9934
+ return { sources: [], added: [] };
9935
+ }
9936
+ if (requested.length === 0) throw new Error("pass at least one --source, or --clear to remove every mirror");
9937
+ const seen = /* @__PURE__ */ new Set();
9938
+ for (const entry of requested) {
9939
+ const key = mirrorKey(entry);
9940
+ if (seen.has(key)) throw new Error(`duplicate mirror ${key}`);
9941
+ seen.add(key);
9942
+ }
9943
+ const registered = opts.append ? opts.registered ?? [] : [];
9944
+ const kept = new Set(registered.map(mirrorKey));
9945
+ const added = requested.filter((entry) => !kept.has(mirrorKey(entry)));
9946
+ const sources = [...registered, ...added];
9947
+ if (sources.length > MAX_MODEL_MIRRORS) {
9948
+ throw new Error(`${sources.length} mirrors exceed the registry's limit of ${MAX_MODEL_MIRRORS}`);
9949
+ }
9950
+ return { sources, added };
9951
+ }
9952
+ function display(entry) {
9953
+ if (entry.kind === "da") return { kind: "da", commitment: entry.commitment, provider_id: entry.providerId, retention: entry.retention };
9954
+ return entry.kind === "https" ? { kind: "https", url: entry.url } : { kind: entry.kind, repo: entry.repo };
9955
+ }
9956
+ var collect = (value, previous) => [...previous, value];
9957
+ function modelMirrorsSetCmd() {
9958
+ return new Command32("set").description(
9959
+ "Replace a registered model's mirror hint list (modelRegistry.setModelMirrors). Only the model's publisher may sign it: the call is signed-origin and checks publisher == signer, so neither a registrar nor Root (sudo) can set mirrors. Mirrors are hints: a miner serves the model only if the bytes it fetches reproduce the registered root"
9960
+ ).requiredOption("--model-hash <hex>", "Registered model hash").option(
9961
+ "--source <mirror>",
9962
+ `Mirror, repeatable (at most ${MAX_MODEL_MIRRORS} in the final list): https://<url>, hf:<owner>/<model>, modelscope:<owner>/<model>, or da:<commitment> of a registered Leased DA blob (its provider is read from chain)`,
9963
+ collect,
9964
+ []
9965
+ ).option(
9966
+ "--append",
9967
+ "Keep the finalized registered mirrors verbatim and add the new ones after them (a DA entry counts as present only at the same commitment and provider). The list is read at finality and then replaced, so a concurrent mirror change by the same publisher between the two is overwritten"
9968
+ ).option("--clear", "Remove every mirror (an empty list)").option("--dry-run", "Validate and print the encoded call without signing or submitting it").option("--json", "Output as JSON").action(async (opts, command) => {
9969
+ let client;
9970
+ try {
9971
+ const modelHash = normalizeHash(opts.modelHash, "--model-hash");
9972
+ const requested = opts.source.map(parseMirrorSource);
9973
+ planMirrorList(requested, { clear: opts.clear, append: opts.append, registered: [] });
9974
+ client = await createClient(getRpc(command), getSigner(command));
9975
+ const api = client.api;
9976
+ const head = await api.rpc.chain.getFinalizedHead();
9977
+ const [view, header] = await Promise.all([api.at(head), api.rpc.chain.getHeader(head)]);
9978
+ const rawModel = await view.query.modelRegistry.models(modelHash);
9979
+ if (!rawModel?.isSome) throw new Error(`model not registered at finality: ${modelHash}`);
9980
+ const publisher = rawModel.unwrap().publisher;
9981
+ if (!sameAccount(publisher, client.signer.address)) {
9982
+ throw new Error(`only the model publisher ${publisher.toString()} may set its mirrors; the signer is ${client.signer.address}`);
9983
+ }
9984
+ const finalized = { query: view.query, rpc: { chain: { getHeader: async () => header } } };
9985
+ for (const entry of requested) {
9986
+ if (entry.kind !== "da") continue;
9987
+ const blob = await view.query.daRegistry.blobs(entry.commitment);
9988
+ if (!blob?.isSome) throw new Error(`DA mirror ${entry.commitment} is not a registered DA blob at finality`);
9989
+ const size = blob.unwrap().toJSON().sizeBytes;
9990
+ const ref = await existingLeasedRef(finalized, entry.commitment, Number(size));
9991
+ if (!ref) throw new Error(`DA mirror ${entry.commitment} is not a registered DA blob at finality`);
9992
+ entry.providerId = ref.providerId;
9993
+ entry.retention = ref.retention;
9994
+ }
9995
+ const raw = await view.query.modelRegistry.modelMirrors(modelHash);
9996
+ const registered = raw?.isSome ? raw.unwrap().toJSON().map(decodeRegisteredMirror) : [];
9997
+ const { sources, added } = planMirrorList(requested, { clear: opts.clear, append: opts.append, registered });
9998
+ const result = {
9999
+ model_hash: modelHash,
10000
+ publisher: publisher.toString(),
10001
+ registered: registered.map(display),
10002
+ sources: sources.map(display),
10003
+ added: added.map(display),
10004
+ finalized_block: header.number.toString()
10005
+ };
10006
+ if (opts.append && added.length === 0) {
10007
+ print({ ...result, unchanged: true }, getJson(command));
10008
+ return;
10009
+ }
10010
+ const tx = api.tx.modelRegistry.setModelMirrors(modelHash, sources.map(chainModelSource));
10011
+ if (opts.dryRun) {
10012
+ print({ ...result, dry_run: true, call: tx.method.toHex(), next: "rerun without --dry-run to sign and submit" }, getJson(command));
10013
+ return;
10014
+ }
10015
+ const sdk = await import("@flop-labs/sdk");
10016
+ const submitted = await sdk.watchTransaction(tx, client.signer, { api });
10017
+ print({ ...result, tx_hash: (submitted.txHash ?? tx.hash).toHex() }, getJson(command));
10018
+ } catch (error) {
10019
+ commandError(error, { next: "fix the reported mirror or signer and rerun; after a submit timeout, rerun the same command with --dry-run and compare its `registered` (the finalized list) with `sources` before resubmitting" });
10020
+ } finally {
10021
+ await client?.disconnect();
10022
+ }
10023
+ });
10024
+ }
10025
+ function modelMirrorsCmd() {
10026
+ return new Command32("mirrors").description("Model mirror hints (publisher-signed)").addCommand(modelMirrorsSetCmd());
10027
+ }
10028
+
10029
+ // src/commands/model-preflight.ts
10030
+ import { Command as Command33 } from "commander";
9527
10031
  function modelPreflightCmd() {
9528
- return new Command32("preflight").description(
10032
+ return new Command33("preflight").description(
9529
10033
  "Print a Hugging Face checkpoint's content_root (content-v1 model_hash), packed image size, DA chunk count and stored tensor elements from hub metadata and Range-read safetensors headers; no weights are downloaded"
9530
- ).requiredOption("--source <hf-id>", "Hugging Face id (owner/model or hf:owner/model)").option("--revision <sha>", "Immutable 40-hex commit; main is resolved to one when omitted").option("--token <token>", "Hugging Face bearer token (prefer HF_TOKEN)").option("--json", "Output as JSON").action(async (opts, command) => {
10034
+ ).requiredOption("--source <hf-id>", "Hugging Face id (owner/model or hf:owner/model)").option("--revision <sha>", "Immutable 40-hex commit; main is resolved to one when omitted").option("--token <token>", "Hugging Face bearer token (prefer HF_TOKEN)").option(
10035
+ "--min-tokens-per-second <n>",
10036
+ "Fail unless the paid completion-token ceiling per channel at the median live calibration cap (or --miner's) is at least n; an unknown ceiling fails. The chain is read only with an explicit --rpc",
10037
+ parseMinTokensPerSecond
10038
+ ).option("--miner <address>", "Compute the ceiling at this miner's accepted calibration cap instead of the median live one; the best is reported, never gated").option("--json", "Output as JSON").action(async (opts, command) => {
10039
+ let client;
9531
10040
  try {
9532
10041
  if (opts.revision && !/^[0-9a-f]{40}$/.test(opts.revision)) throw new Error("--revision must be an immutable 40-hex commit");
9533
10042
  const repo = opts.source.startsWith("hf:") ? opts.source.slice(3) : opts.source;
@@ -9535,6 +10044,25 @@ function modelPreflightCmd() {
9535
10044
  const hub = { revision: opts.revision, token: opts.token ?? process.env.HF_TOKEN };
9536
10045
  const preflight = await sdk.preflightHuggingFace(repo, hub);
9537
10046
  const headers = await sdk.readSafetensorsHeaders(repo, preflight.revision, preflight.files, hub);
10047
+ let work = "no root config.json";
10048
+ if (preflight.configJson) {
10049
+ try {
10050
+ work = perTokenWork(sdk.computeChannel.parseMeteringConfig(preflight.configJson, preflight.hfQuantConfigJson));
10051
+ } catch (error) {
10052
+ work = `no metering arch: ${error.message}`;
10053
+ }
10054
+ }
10055
+ let chain = null;
10056
+ if (resolveNetwork(command).rpcOverridden) {
10057
+ try {
10058
+ client = await createReadOnlyClient(getRpc(command));
10059
+ chain = await chainCapInputsOrReason(client.api, opts.miner);
10060
+ } catch (error) {
10061
+ chain = `chain connection failed: ${error.message}`;
10062
+ }
10063
+ }
10064
+ const ceiling = throughputCeiling(work, chain, opts.minTokensPerSecond);
10065
+ process.stderr.write(ceilingWarning(ceiling));
9538
10066
  print({
9539
10067
  repo,
9540
10068
  revision: preflight.revision,
@@ -9545,18 +10073,23 @@ function modelPreflightCmd() {
9545
10073
  fetched_metadata_files: preflight.metadataFiles.length,
9546
10074
  safetensors_shards: headers.length,
9547
10075
  // Not a parameter count for packed sub-byte weights, and scale tensors are included.
9548
- stored_tensor_elements: sdk.storedTensorElements(headers).toString()
10076
+ stored_tensor_elements: sdk.storedTensorElements(headers).toString(),
10077
+ throughput_ceiling: snakeCeiling(ceiling)
9549
10078
  }, getJson(command));
10079
+ enforceCeiling(ceiling);
9550
10080
  } catch (error) {
9551
10081
  commandError(error);
10082
+ } finally {
10083
+ await client?.disconnect();
9552
10084
  }
9553
10085
  });
9554
10086
  }
9555
10087
 
9556
10088
  // src/commands/model-backup.ts
9557
- import { Command as Command33, Option as Option3 } from "commander";
9558
- import { mkdir as mkdir12, rm as rm4, writeFile as writeFile10 } from "node:fs/promises";
9559
- import { join as join8, resolve as resolve9 } from "node:path";
10089
+ import { Command as Command34, Option as Option3 } from "commander";
10090
+ import { createHash as createHash6 } from "node:crypto";
10091
+ import { mkdir as mkdir12, rm as rm5, stat, writeFile as writeFile10 } from "node:fs/promises";
10092
+ import { dirname as dirname6, join as join8, resolve as resolve9 } from "node:path";
9560
10093
  var PRECISIONS3 = ["FP32", "FP16", "BF16", "INT8", "INT4", "Mixed"];
9561
10094
  async function finalizedLeasedRef2(api, commitment, size) {
9562
10095
  const hash2 = await api.rpc.chain.getFinalizedHead();
@@ -9572,6 +10105,17 @@ async function awaitFinalizedLeasedRef(api, commitment, size) {
9572
10105
  await new Promise((wake) => setTimeout(wake, REGISTRATION_POLL_MS));
9573
10106
  }
9574
10107
  }
10108
+ function downloadStateDir(binding, override) {
10109
+ if (override) return resolve9(override);
10110
+ const key = createHash6("sha256").update(JSON.stringify(binding)).digest("hex");
10111
+ return join8(dirname6(binding.modelDir), `.flop-backup-state-${key.slice(0, 16)}`);
10112
+ }
10113
+ async function assertSameFilesystem(stateDir, modelDir) {
10114
+ if ((await stat(stateDir)).dev === (await stat(modelDir)).dev) return;
10115
+ throw new Error(
10116
+ `download state ${stateDir} is not on the same filesystem as ${modelDir}; pass a --state-dir on that filesystem outside the checkpoint tree (if --model-dir is a mount point, use a subdirectory of it)`
10117
+ );
10118
+ }
9575
10119
  function registeredHubRepo(mirrors) {
9576
10120
  const repos = [...new Set(mirrors.filter((m) => m.kind === "huggingface").map((m) => m.repo))];
9577
10121
  if (repos.length > 1) {
@@ -9582,9 +10126,9 @@ function registeredHubRepo(mirrors) {
9582
10126
  return repos[0];
9583
10127
  }
9584
10128
  function modelBackupCmd() {
9585
- return new Command33("backup").description(
10129
+ return new Command34("backup").description(
9586
10130
  "Stream a registered content-v1 checkpoint from Hugging Face into validator DA under this box's DA key, without ever writing the packed image out, and print the DataRefs its publisher then binds"
9587
- ).argument("<model_hash>", "Registered model hash (0x-hex)").requiredOption("--model-dir <path>", "Directory the checkpoint streams into (needs room for it)").option("--source <hf-id>", "Hugging Face id (owner/model or hf:owner/model); the model's registered mirror when omitted").option("--revision <sha>", "Immutable 40-hex commit; main is resolved to one when omitted").addOption(new Option3("--precision <precision>", "Measured-root precision").choices(PRECISIONS3).default("FP16")).option("--da-validator <path>", "da-validator executable", "da-validator").option("--token <token>", "Hugging Face bearer token (prefer HF_TOKEN)").option("--state-dir <path>", "Download resume receipts (outside the checkpoint tree)").option("--dry-run", "Print the plan and the DA deposit it would lock, and stop before any transfer").option("--json", "Output as JSON").action(async (modelHash, opts, command) => {
10131
+ ).argument("<model_hash>", "Registered model hash (0x-hex)").requiredOption("--model-dir <path>", "Directory the checkpoint streams into (needs room for it)").option("--source <hf-id>", "Hugging Face id (owner/model or hf:owner/model); the model's registered mirror when omitted").option("--revision <sha>", "Immutable 40-hex commit; main is resolved to one when omitted").addOption(new Option3("--precision <precision>", "Measured-root precision").choices(PRECISIONS3).default("FP16")).option("--da-validator <path>", "da-validator executable", "da-validator").option("--token <token>", "Hugging Face bearer token (prefer HF_TOKEN)").option("--state-dir <path>", "Download resume receipts, on the checkpoint's filesystem but outside its tree (defaults to beside --model-dir)").option("--dry-run", "Print the plan and the DA deposit it would lock, and stop before any transfer").option("--json", "Output as JSON").action(async (modelHash, opts, command) => {
9588
10132
  let client;
9589
10133
  let workDir;
9590
10134
  try {
@@ -9637,10 +10181,12 @@ function modelBackupCmd() {
9637
10181
  workDir = join8(resolve9(modelDir, ".."), `.flop-backup-${modelHash.replace(/^0x/, "").slice(0, 16)}`);
9638
10182
  await mkdir12(workDir, { recursive: true, mode: 448 });
9639
10183
  await mkdir12(modelDir, { recursive: true });
9640
- const resumeState = opts.stateDir ? join8(resolve9(opts.stateDir), "download.json") : void 0;
9641
- if (resumeState) await mkdir12(resolve9(opts.stateDir), { recursive: true, mode: 448 });
10184
+ const stateDir = downloadStateDir({ modelHash, modelDir, repo, revision: preflight.revision }, opts.stateDir);
10185
+ await mkdir12(stateDir, { recursive: true, mode: 448 });
10186
+ await assertSameFilesystem(stateDir, modelDir);
10187
+ const resumeState = join8(stateDir, "download.json");
9642
10188
  process.stderr.write(
9643
- `backing up ${repo}@${preflight.revision} (${preflight.files.length} files, image ${imageSize} bytes, ${preflight.daChunks} DA chunk(s)) as ${modelHash}
10189
+ `backing up ${repo}@${preflight.revision} (${preflight.files.length} files, image ${imageSize} bytes, ${preflight.daChunks} DA chunk(s)) as ${modelHash}; download receipts in ${stateDir}
9644
10190
  `
9645
10191
  );
9646
10192
  await sdk.fetchHuggingFaceToDir(repo, modelDir, {
@@ -9696,7 +10242,7 @@ function modelBackupCmd() {
9696
10242
  table_register_tx: table2.registerTx,
9697
10243
  // The binding is the model publisher's own transaction: this box holds no
9698
10244
  // model-registry authority, and `set_model_weights_ref` takes any Leased ref from it.
9699
- next: `the model publisher binds the backup with \`flop model upload --model-hash ${modelHash} --commitment ${weights.ref.commitment} --size ${imageSize}\`, then advertises the file table as a da:${table2.ref.commitment} mirror with \`modelRegistry.setModelMirrors\``,
10245
+ next: `the model publisher binds the backup with \`flop model upload --model-hash ${modelHash} --commitment ${weights.ref.commitment} --size ${imageSize}\`, then advertises the file table with \`flop model mirrors set --model-hash ${modelHash} --append --source da:${table2.ref.commitment}\``,
9700
10246
  recovery: `flop da withdraw --commitment ${weights.ref.commitment} (and ${table2.ref.commitment}) refunds both deposits while nothing pins them`
9701
10247
  }, getJson(command));
9702
10248
  } catch (error) {
@@ -9704,16 +10250,16 @@ function modelBackupCmd() {
9704
10250
  next: "rerun the same command to resume the download and the DA push; to abandon a registered blob, run the printed `flop da withdraw` recovery"
9705
10251
  });
9706
10252
  } finally {
9707
- if (workDir) await rm4(workDir, { recursive: true, force: true });
10253
+ if (workDir) await rm5(workDir, { recursive: true, force: true });
9708
10254
  await client?.disconnect();
9709
10255
  }
9710
10256
  });
9711
10257
  }
9712
10258
 
9713
10259
  // src/commands/model-list.ts
9714
- import { Command as Command34 } from "commander";
10260
+ import { Command as Command35 } from "commander";
9715
10261
  function modelListCmd() {
9716
- return new Command34("list").description("List models registered on the selected FLOP chain").option("--name <text>", "Filter model names").option("--limit <n>", "Maximum rows returned", "100").option("--json", "Output as JSON").action(async (opts, command) => {
10262
+ return new Command35("list").description("List models registered on the selected FLOP chain").option("--name <text>", "Filter model names").option("--limit <n>", "Maximum rows returned", "100").option("--json", "Output as JSON").action(async (opts, command) => {
9717
10263
  let client;
9718
10264
  try {
9719
10265
  const sdk = await import("@flop-labs/sdk");
@@ -9743,9 +10289,9 @@ function modelListCmd() {
9743
10289
  }
9744
10290
 
9745
10291
  // src/commands/model-get.ts
9746
- import { Command as Command35 } from "commander";
10292
+ import { Command as Command36 } from "commander";
9747
10293
  function modelGetCmd() {
9748
- return new Command35("get").description("Get a registered model by hash or exact name").argument("<identifier>", "Model hash or name").option("--precision <precision>", "Measured-root precision", "FP16").option("--json", "Output as JSON").action(async (identifier, opts, command) => {
10294
+ return new Command36("get").description("Get a registered model by hash or exact name").argument("<identifier>", "Model hash or name").option("--precision <precision>", "Measured-root precision", "FP16").option("--json", "Output as JSON").action(async (identifier, opts, command) => {
9749
10295
  let client;
9750
10296
  try {
9751
10297
  const sdk = await import("@flop-labs/sdk");
@@ -9776,12 +10322,25 @@ function modelGetCmd() {
9776
10322
  }
9777
10323
 
9778
10324
  // src/commands/model-fetch.ts
9779
- import { Command as Command36, Option as Option4 } from "commander";
10325
+ import { Command as Command37, Option as Option4 } from "commander";
10326
+ import { mkdir as mkdir13, mkdtemp as mkdtemp3, readdir as readdir2, rename as rename8, rm as rm6, rmdir } from "node:fs/promises";
10327
+ import { basename as basename3, dirname as dirname7, join as join9, resolve as resolve10 } from "node:path";
9780
10328
  var PRECISIONS4 = ["FP32", "FP16", "BF16", "INT8", "INT4", "Mixed"];
10329
+ async function requireEmptyOrAbsent(dir) {
10330
+ try {
10331
+ if ((await readdir2(dir)).length > 0) throw new Error(`--output-dir ${dir} is not empty; a Hugging Face checkpoint is written only into an empty or new directory`);
10332
+ } catch (error) {
10333
+ if (error.code !== "ENOENT") throw error;
10334
+ }
10335
+ }
9781
10336
  function modelFetchCmd() {
9782
- return new Command36("fetch").description("Fetch model weights and accept only a registered dm-verity root").argument("<identifier>", "Registered model hash or exact name").addOption(new Option4("--precision <precision>", "Weights precision").choices(PRECISIONS4).default("FP16")).option("--source <source...>", "Override mirrors: hf:<repo>, modelscope:<repo>, https://..., da:<commitment>").option("--da-endpoint <url>", "DA blob gateway exposing GET /blob/<commitment>").option("--output-dir <path>", "Destination directory", "./flop-model").option("--token <token>", "HF/ModelScope bearer token (prefer HF_TOKEN)").option("--json", "Output as JSON").action(async (identifier, opts, command) => {
10337
+ return new Command37("fetch").description("Fetch model weights and accept only a registered dm-verity root").argument("<identifier>", "Registered model hash or exact name").addOption(new Option4("--precision <precision>", "Weights precision").choices(PRECISIONS4).default("FP16")).option("--source <source...>", "Override mirrors: hf:<repo>, modelscope:<repo>, https://..., da:<commitment>").option(
10338
+ "--revision <sha>",
10339
+ "Immutable 40-hex Hugging Face commit for hf: mirrors; when omitted, main is used only if its sha256 table reproduces the registered content-v1 root"
10340
+ ).option("--da-endpoint <url>", "DA blob gateway exposing GET /blob/<commitment>").option("--output-dir <path>", "Destination directory", "./flop-model").option("--token <token>", "HF/ModelScope bearer token (prefer HF_TOKEN)").option("--json", "Output as JSON").action(async (identifier, opts, command) => {
9783
10341
  let client;
9784
10342
  try {
10343
+ const revision = parseRevision(opts.revision);
9785
10344
  const sdk = await import("@flop-labs/sdk");
9786
10345
  client = await createClient(getRpc(command));
9787
10346
  const registry = new sdk.OnChainModelRegistry(client.api);
@@ -9793,30 +10352,56 @@ function modelFetchCmd() {
9793
10352
  }
9794
10353
  const sources = opts.source?.map(parseModelSource) ?? await sdk.readModelMirrors(client.api, model.hash);
9795
10354
  if (sources.length === 0) throw new Error(`no mirrors registered for model: ${model.hash}`);
10355
+ const hub = sources.slice(0, 16).some((s) => s.kind === "huggingface");
10356
+ if (revision && !hub) throw new Error("--revision is only for a Hugging Face source");
10357
+ const outputDir = resolve10(opts.outputDir);
10358
+ const token = opts.token ?? process.env.HF_TOKEN;
9796
10359
  const attempts = [];
9797
10360
  for (const source of sources.slice(0, 16)) {
10361
+ let staged;
9798
10362
  try {
9799
- const fetched = await sdk.fetchWeights(source, {
9800
- token: opts.token ?? process.env.HF_TOKEN,
9801
- daSource: daHttpSource(opts.daEndpoint)
9802
- }, expected);
9803
- const result = sdk.verifyModelWeights(fetched.files, expected);
10363
+ let result;
10364
+ let write;
10365
+ let at;
10366
+ if (source.kind === "huggingface" && !source.path) {
10367
+ await requireEmptyOrAbsent(outputDir);
10368
+ await mkdir13(dirname7(outputDir), { recursive: true });
10369
+ const dir = await mkdtemp3(join9(dirname7(outputDir), `.${basename3(outputDir)}.fetch-`));
10370
+ staged = dir;
10371
+ const download = await downloadPinnedHuggingFace(source.repo, dir, expected, { revision, token });
10372
+ at = download.revision;
10373
+ result = await sdk.verifyModelDir(dir, expected);
10374
+ write = async () => {
10375
+ await rmdir(outputDir).catch((error) => {
10376
+ if (error.code !== "ENOENT") throw error;
10377
+ });
10378
+ await rename8(dir, outputDir);
10379
+ staged = void 0;
10380
+ return download.paths.map((p) => join9(outputDir, ...p.split("/")));
10381
+ };
10382
+ } else {
10383
+ const fetched = await sdk.fetchWeights(source, { token, daSource: daHttpSource(opts.daEndpoint) }, expected);
10384
+ result = sdk.verifyModelWeights(fetched.files, expected);
10385
+ write = () => writeModelFiles(opts.outputDir, fetched.files);
10386
+ }
9804
10387
  const verified = result.scheme === "content-v1" ? result.computedContentRoot : result.computedVerityRoot;
9805
10388
  attempts.push({
9806
10389
  source,
10390
+ ...at ? { revision: at } : {},
9807
10391
  ok: result.ok,
9808
10392
  scheme: result.scheme,
9809
10393
  computed_content_root: result.computedContentRoot,
9810
10394
  computed_verity_root: result.computedVerityRoot
9811
10395
  });
9812
10396
  if (!result.ok) continue;
9813
- const files = await writeModelFiles(opts.outputDir, fetched.files);
10397
+ const files = await write();
9814
10398
  print({
9815
10399
  model_hash: model.hash,
9816
10400
  precision: opts.precision,
9817
10401
  verified_root: verified,
9818
10402
  scheme: result.scheme,
9819
10403
  source,
10404
+ ...at ? { revision: at } : {},
9820
10405
  output_dir: opts.outputDir,
9821
10406
  file_count: files.length,
9822
10407
  files: files.slice(0, 20),
@@ -9826,6 +10411,8 @@ function modelFetchCmd() {
9826
10411
  return;
9827
10412
  } catch (error) {
9828
10413
  attempts.push({ source, ok: false, error: error instanceof Error ? error.message : String(error) });
10414
+ } finally {
10415
+ if (staged) await rm6(staged, { recursive: true, force: true });
9829
10416
  }
9830
10417
  }
9831
10418
  throw new Error(`no mirror reproduced registered root ${expected}: ${JSON.stringify(attempts)}`);
@@ -9842,7 +10429,7 @@ function modelFetchCmd() {
9842
10429
 
9843
10430
  // src/commands/agent.ts
9844
10431
  import path2 from "node:path";
9845
- import { Command as Command37, Option as Option5 } from "commander";
10432
+ import { Command as Command38, Option as Option5 } from "commander";
9846
10433
  import { getFlopNetworkProfile as getFlopNetworkProfile3 } from "@flop-labs/sdk";
9847
10434
  var DEFAULT_MCP_URL = "https://mcp.flop.network/mcp";
9848
10435
  var DEVNET_MCP_URL = "https://mcp.flop.network/mcp";
@@ -9930,7 +10517,7 @@ function buildAgentInitPlan(client, mode, mcpUrl2 = DEFAULT_MCP_URL) {
9930
10517
  };
9931
10518
  }
9932
10519
  function agentInitCmd() {
9933
- return new Command37("init").description("Print a safe agent onboarding plan and MCP configuration").addOption(new Option5("--client <client>", "Agent client").choices([
10520
+ return new Command38("init").description("Print a safe agent onboarding plan and MCP configuration").addOption(new Option5("--client <client>", "Agent client").choices([
9934
10521
  "claude-code",
9935
10522
  "codex",
9936
10523
  "cursor",
@@ -9948,7 +10535,7 @@ function agentInitCmd() {
9948
10535
  });
9949
10536
  }
9950
10537
  function agentDoctorCmd() {
9951
- return new Command37("doctor").description("Check agent MCP, gateway, and optional chain connectivity").addOption(new Option5("--mode <mode>", "MCP mode").choices(["remote", "local"]).default("remote")).option("--mcp-url <url>", "MCP URL").option("--gateway <url>", "Optional OpenAI-compatible gateway URL").option("--repo <path>", "flop-core checkout for local mode", process.cwd()).option("--check-rpc", "Also require the configured chain RPC").option("--json", "Output as JSON").action(async (opts, command) => {
10538
+ return new Command38("doctor").description("Check agent MCP, gateway, and optional chain connectivity").addOption(new Option5("--mode <mode>", "MCP mode").choices(["remote", "local"]).default("remote")).option("--mcp-url <url>", "MCP URL").option("--gateway <url>", "Optional OpenAI-compatible gateway URL").option("--repo <path>", "flop-core checkout for local mode", process.cwd()).option("--check-rpc", "Also require the configured chain RPC").option("--json", "Output as JSON").action(async (opts, command) => {
9952
10539
  try {
9953
10540
  const checks = [executableCheck("node")];
9954
10541
  if (opts.mode === "remote") {
@@ -9975,7 +10562,7 @@ function agentDoctorCmd() {
9975
10562
  });
9976
10563
  }
9977
10564
  function agentCredentialCmd() {
9978
- return new Command37("credential").description("Provision or reuse a devnet gateway key for agent clients (wallet-signed by default)").option("--print-key", "Print only the bearer key for a local client auth helper").option("--tailnet", "Enroll through the private tailnet lane (approved Tailscale users only)").option("--json", "Output non-secret credential metadata as JSON").action(async (opts, command) => {
10565
+ return new Command38("credential").description("Provision or reuse a devnet gateway key for agent clients (wallet-signed by default)").option("--print-key", "Print only the bearer key for a local client auth helper").option("--tailnet", "Enroll through the private tailnet lane (approved Tailscale users only)").option("--json", "Output non-secret credential metadata as JSON").action(async (opts, command) => {
9979
10566
  try {
9980
10567
  if (getNetwork(command) !== "devnet") {
9981
10568
  throw new Error("agent credential requires --network devnet");
@@ -10008,20 +10595,20 @@ function agentCredentialCmd() {
10008
10595
  });
10009
10596
  }
10010
10597
  function agentCmd() {
10011
- const command = new Command37("agent").description("Onboard and operate FLOP-backed agents");
10598
+ const command = new Command38("agent").description("Onboard and operate FLOP-backed agents");
10012
10599
  command.addCommand(agentInitCmd());
10013
10600
  command.addCommand(agentDoctorCmd());
10014
10601
  command.addCommand(agentCredentialCmd());
10015
10602
  command.addCommand(launchCmd());
10016
10603
  command.addCommand(proxyCmd());
10017
10604
  command.addCommand(hermesCmd());
10018
- const keys = new Command37("keys").description("Manage agent keys");
10605
+ const keys = new Command38("keys").description("Manage agent keys");
10019
10606
  keys.addCommand(keysGenerateCmd());
10020
10607
  command.addCommand(keys);
10021
- const wallet = new Command37("wallet").description("Manage agent wallets");
10608
+ const wallet = new Command38("wallet").description("Manage agent wallets");
10022
10609
  wallet.addCommand(walletAgentCreateCmd());
10023
10610
  command.addCommand(wallet);
10024
- const session = new Command37("session").description("Manage agent session keys");
10611
+ const session = new Command38("session").description("Manage agent session keys");
10025
10612
  session.addCommand(sessionCreateCmd());
10026
10613
  session.addCommand(sessionInfoCmd());
10027
10614
  session.addCommand(sessionRevokeCmd());
@@ -10033,7 +10620,7 @@ function agentCmd() {
10033
10620
  // src/commands/miner.ts
10034
10621
  import { readFileSync } from "node:fs";
10035
10622
  import path3 from "node:path";
10036
- import { Command as Command38, Option as Option6 } from "commander";
10623
+ import { Command as Command40, Option as Option6 } from "commander";
10037
10624
  import { formatFlop as formatFlop2 } from "@flop-labs/sdk";
10038
10625
 
10039
10626
  // src/gpu-inventory-remark.ts
@@ -10110,6 +10697,85 @@ function gpusFromDetection(devices) {
10110
10697
  return aggregate(devices.map((device) => ({ name: device.deviceName.trim(), memoryMib: device.memoryTotal })));
10111
10698
  }
10112
10699
 
10700
+ // src/commands/miner-capacity-rate.ts
10701
+ import { Command as Command39 } from "commander";
10702
+ var U64_MAX = (1n << 64n) - 1n;
10703
+ function parseReservationRate(value) {
10704
+ if (!/^\d+$/.test(value.trim())) throw new Error("rate must be a positive whole number of G_n/s");
10705
+ const rate = BigInt(value.trim());
10706
+ if (rate <= 0n) throw new Error("rate must be positive; use --whole-cap to restore the default");
10707
+ if (rate > U64_MAX) throw new Error("rate exceeds u64");
10708
+ return rate;
10709
+ }
10710
+ function capacityRateCall(api, rate) {
10711
+ const call = api.tx.computeChannel?.setCapacityReservationRate;
10712
+ if (typeof call !== "function") {
10713
+ throw new Error(`runtime spec ${api.runtimeVersion?.specVersion?.toString() ?? "?"} has no computeChannel.setCapacityReservationRate`);
10714
+ }
10715
+ return call(rate);
10716
+ }
10717
+ var text = (value) => value === void 0 || value === null ? null : value.toString();
10718
+ function showCmd() {
10719
+ return new Command39("show").description("Show the miner's reservation rate and free calibrated capacity at the finalized block").argument("[address]", "Miner SS58 address; defaults to the configured signer").option("--json", "Output as JSON").action(async (address, _opts, command) => {
10720
+ let client;
10721
+ try {
10722
+ const rpc = getRpc(command);
10723
+ client = address ? await createReadOnlyClient(rpc) : await createClient(rpc, getSigner(command));
10724
+ const miner = address ?? client.address;
10725
+ const { computeChannel: computeChannel9 } = await import("@flop-labs/sdk");
10726
+ const state = await computeChannel9.readMinerCapacityState(client.api, miner);
10727
+ if (!state) {
10728
+ print({ address: miner, rpc, supported: false }, getJson(command));
10729
+ return;
10730
+ }
10731
+ const capacity = computeChannel9.minerCapacity(state);
10732
+ print({
10733
+ address: miner,
10734
+ rpc,
10735
+ supported: true,
10736
+ block: text(capacity.block),
10737
+ rate_set: text(state.rate),
10738
+ cap_gflops_per_sec: text(capacity.cap),
10739
+ reserved_gn_per_sec: text(capacity.reserved),
10740
+ free_gn_per_sec: text(capacity.free),
10741
+ next_open_reserves: text(capacity.rate),
10742
+ next_open_fits: capacity.fits,
10743
+ ...capacity.reason ? { reason: capacity.reason } : {},
10744
+ live_reservations: state.reservations.filter((r) => r.lapsesAt > state.block).length
10745
+ }, getJson(command));
10746
+ } catch (error) {
10747
+ commandError(error);
10748
+ } finally {
10749
+ await client?.disconnect();
10750
+ }
10751
+ });
10752
+ }
10753
+ function setCmd() {
10754
+ return new Command39("set").description("Set the G_n/s each new channel reserves from the signer's cap (miner call set_capacity_reservation_rate)").argument("[rate]", "Rate in G_n/s, a positive integer").option("--whole-cap", "Clear the rate: each new channel reserves the whole current cap").option("--dry-run", "Print the encoded call without signing").option("--json", "Output as JSON").action(async (value, opts, command) => {
10755
+ let client;
10756
+ try {
10757
+ if (value === void 0 === !opts.wholeCap) throw new Error("pass a rate or --whole-cap, not both");
10758
+ const rate = opts.wholeCap ? null : parseReservationRate(value);
10759
+ const rpc = getRpc(command);
10760
+ client = opts.dryRun ? await createReadOnlyClient(rpc) : await createClient(rpc, getSigner(command));
10761
+ const call = capacityRateCall(client.api, rate);
10762
+ if (opts.dryRun) {
10763
+ print({ action: "capacity_rate_call", rate_gn_per_sec: text(rate), call_data: call.method.toHex() }, getJson(command));
10764
+ return;
10765
+ }
10766
+ const txHash = await submitExtrinsic(client.api, client.signer, call);
10767
+ print({ action: "capacity_rate_set", address: client.address, rate_gn_per_sec: text(rate), tx_hash: txHash }, getJson(command));
10768
+ } catch (error) {
10769
+ commandError(error);
10770
+ } finally {
10771
+ await client?.disconnect();
10772
+ }
10773
+ });
10774
+ }
10775
+ function minerCapacityRateCmd() {
10776
+ return new Command39("capacity-rate").description("Show or set the capacity each channel opened against this miner reserves").addCommand(showCmd()).addCommand(setCmd());
10777
+ }
10778
+
10113
10779
  // src/commands/miner-controller-info.ts
10114
10780
  async function probeMinerSessionInfo(url, token, required = true) {
10115
10781
  try {
@@ -10146,6 +10812,159 @@ async function probeMinerSessionInfo(url, token, required = true) {
10146
10812
  }
10147
10813
  }
10148
10814
 
10815
+ // src/commands/miner-running.ts
10816
+ import { decodeAddress } from "@polkadot/util-crypto";
10817
+ import { computeChannel as computeChannel6 } from "@flop-labs/sdk";
10818
+ var hex2 = (bytes4) => `0x${Buffer.from(bytes4).toString("hex")}`;
10819
+ var reason = (error) => error instanceof Error ? error.message : String(error);
10820
+ async function readLocalIdentity(sessionUrl, token, fetchImpl = fetch) {
10821
+ const base = sessionUrl.replace(/\/session\/info\/?$/, "");
10822
+ try {
10823
+ const info = await computeChannel6.fetchMinerSessionInfo(
10824
+ base,
10825
+ token ? { Authorization: `Bearer ${token}` } : {},
10826
+ AbortSignal.timeout(2e4),
10827
+ fetchImpl
10828
+ );
10829
+ if (!info.modelIdentity) {
10830
+ return { check: {
10831
+ id: "session:identity",
10832
+ status: "fail",
10833
+ required: true,
10834
+ detail: "worker reports no model identity",
10835
+ next: "check the worker logs for the model gate"
10836
+ } };
10837
+ }
10838
+ return { info, check: {
10839
+ id: "session:identity",
10840
+ status: "pass",
10841
+ required: true,
10842
+ detail: `${info.minerAddress} serves ${info.modelIdentity.registryModel} (${hex2(info.modelIdentity.modelHash)}), session key ${hex2(info.enclaveKey)}`
10843
+ } };
10844
+ } catch {
10845
+ return { check: {
10846
+ id: "session:identity",
10847
+ status: "fail",
10848
+ required: true,
10849
+ detail: "authenticated session info unavailable",
10850
+ next: "check --session-url and SESSION_AUTH_TOKEN"
10851
+ } };
10852
+ }
10853
+ }
10854
+ async function probeMinerChain(api, address) {
10855
+ try {
10856
+ const account = decodeAddress(address);
10857
+ const head = await api.rpc.chain.getFinalizedHead();
10858
+ const at = await api.at(head);
10859
+ const registration = await at.query.minerStaking.miners(account);
10860
+ const active = registration?.isSome ? registration.unwrap().isActive : void 0;
10861
+ const block = (await api.rpc.chain.getHeader(head)).number.toString();
10862
+ if (!(active?.isTrue ?? active === true)) {
10863
+ return {
10864
+ id: "chain:miner",
10865
+ status: "fail",
10866
+ required: true,
10867
+ detail: `${address} is not registered and active at finalized #${block}`,
10868
+ next: "flop miner register; flop miner status"
10869
+ };
10870
+ }
10871
+ const cap = (await api.rpc.state.call(
10872
+ "TrustRootApi_accepted_calibration_cap",
10873
+ hex2(Uint8Array.from([128, ...account])),
10874
+ head
10875
+ )).toU8a(true);
10876
+ if (cap.length === 1 && cap[0] === 0) {
10877
+ return {
10878
+ id: "chain:miner",
10879
+ status: "fail",
10880
+ required: true,
10881
+ detail: `${address} has no accepted calibration at finalized #${block}`,
10882
+ next: "check the worker's calibration logs"
10883
+ };
10884
+ }
10885
+ if (cap.length !== 9 || cap[0] !== 1) throw new Error("TrustRootApi_accepted_calibration_cap did not return an Option<u64>");
10886
+ const gflops = new DataView(cap.buffer, cap.byteOffset + 1, 8).getBigUint64(0, true);
10887
+ if (gflops === 0n) {
10888
+ return {
10889
+ id: "chain:miner",
10890
+ status: "fail",
10891
+ required: true,
10892
+ detail: `${address} has a zero calibration cap at finalized #${block}`,
10893
+ next: "check the worker's calibration logs"
10894
+ };
10895
+ }
10896
+ return {
10897
+ id: "chain:miner",
10898
+ status: "pass",
10899
+ required: true,
10900
+ detail: `registered, active, accepted calibration cap ${gflops} GFLOP/s at finalized #${block} (capacity bond not checked)`
10901
+ };
10902
+ } catch (error) {
10903
+ return { id: "chain:miner", status: "fail", required: true, detail: reason(error), next: "check --address and --rpc" };
10904
+ }
10905
+ }
10906
+ async function probePublicEndpoint(api, endpoint, address, local, fetchImpl = fetch) {
10907
+ let url;
10908
+ try {
10909
+ url = new URL(endpoint);
10910
+ } catch {
10911
+ }
10912
+ if (!url || url.protocol !== "https:" || url.pathname !== "/" || url.search || url.hash || url.username || url.password) {
10913
+ return [{
10914
+ id: "endpoint",
10915
+ status: "fail",
10916
+ required: true,
10917
+ detail: `${endpoint} is not a bare https://host[:port] endpoint`,
10918
+ next: "pass the endpoint the worker advertises"
10919
+ }];
10920
+ }
10921
+ const base = url.origin;
10922
+ const checks = [];
10923
+ try {
10924
+ const anonymous = await fetchImpl(`${base}/session/info`, { redirect: "error", signal: AbortSignal.timeout(1e4) });
10925
+ await anonymous.body?.cancel();
10926
+ checks.push(anonymous.status === 401 ? { id: "endpoint:auth", status: "pass", required: true, detail: `${base}/session/info refuses anonymous requests (401)` } : {
10927
+ id: "endpoint:auth",
10928
+ status: "fail",
10929
+ required: true,
10930
+ detail: anonymous.status === 200 ? `${base}/session/info answers without authentication` : `${base}/session/info answered ${anonymous.status}`,
10931
+ next: "route the public name to the worker; it must require the session token"
10932
+ });
10933
+ } catch (error) {
10934
+ checks.push({
10935
+ id: "endpoint:auth",
10936
+ status: "fail",
10937
+ required: true,
10938
+ detail: `${base} unreachable: ${reason(error)}`,
10939
+ next: "check DNS, TLS and the firewall from outside this host"
10940
+ });
10941
+ }
10942
+ try {
10943
+ const proven = await computeChannel6.fetchProvenSessionInfo(base, {
10944
+ genesisHash: api.genesisHash.toU8a(),
10945
+ account: decodeAddress(address),
10946
+ minerAddress: address,
10947
+ enclaveKey: local.enclaveKey,
10948
+ modelIdentity: local.modelIdentity
10949
+ }, fetchImpl, AbortSignal.timeout(1e4));
10950
+ checks.push({
10951
+ id: "endpoint:info-proof",
10952
+ status: "pass",
10953
+ required: true,
10954
+ detail: `fresh ready proof from the local session key for the local model (image ${proven.image?.version ?? "?"} ${proven.image?.revision ?? "?"})`
10955
+ });
10956
+ } catch (error) {
10957
+ checks.push({
10958
+ id: "endpoint:info-proof",
10959
+ status: "fail",
10960
+ required: true,
10961
+ detail: reason(error),
10962
+ next: "the public name must reach this worker, ready, on this chain; a probe from this host can differ from an outside one (hairpin NAT)"
10963
+ });
10964
+ }
10965
+ return checks;
10966
+ }
10967
+
10149
10968
  // src/commands/miner.ts
10150
10969
  function collectGpu(value, previous) {
10151
10970
  return [...previous, value];
@@ -10251,7 +11070,7 @@ function buildMinerInitPlan(options) {
10251
11070
  };
10252
11071
  }
10253
11072
  function minerInitCmd() {
10254
- return new Command38("init").description("Print a phased miner onboarding plan").addOption(new Option6("--mode <mode>", "Miner mode").choices(["sim", "tdx", "cloud"]).default("sim")).option("--instance <number>", "Local guest instance number", "0").option("--provider <name>", "Cloud provider", "runpod").option(
11073
+ return new Command40("init").description("Print a phased miner onboarding plan").addOption(new Option6("--mode <mode>", "Miner mode").choices(["sim", "tdx", "cloud"]).default("sim")).option("--instance <number>", "Local guest instance number", "0").option("--provider <name>", "Cloud provider", "runpod").option(
10255
11074
  "--gpu <SKU[:COUNT]>",
10256
11075
  "Expected GPU inventory; repeat for mixed-SKU hosts",
10257
11076
  collectGpu,
@@ -10294,10 +11113,20 @@ async function gpuCheck(mode) {
10294
11113
  }
10295
11114
  }
10296
11115
  function minerDoctorCmd() {
10297
- return new Command38("doctor").description("Check miner runtime, hardware, chain, and serving prerequisites").addOption(new Option6("--mode <mode>", "Miner mode").choices(["sim", "tdx", "cloud"]).default("sim")).option("--repo <path>", "flop-core checkout", process.cwd()).option("--session-url <url>", "Optional miner session info URL").option("--offline", "Skip chain RPC check").option("--json", "Output as JSON").action(async (opts, command) => {
11116
+ return new Command40("doctor").description("Check miner runtime, hardware, chain, and serving prerequisites").addOption(new Option6("--mode <mode>", "Miner mode").choices(["sim", "tdx", "cloud"]).default("sim")).option("--repo <path>", "flop-core checkout", process.cwd()).option("--session-url <url>", "Optional miner session info URL").option("--offline", "Skip chain RPC check").option("--running", "Check a running miner instead of setup prerequisites (needs --address or --session-url)").option("--address <ss58>", "Miner account: registered, active and calibrated at finality").option("--endpoint <url>", "Public https endpoint: refuses anonymous info and answers a fresh info-proof (needs --session-url)").addHelpText("after", [
11117
+ "",
11118
+ "--running skips the docker/just/checkout/GPU checks, so it runs from a release pack or another host.",
11119
+ "The session token is read from SESSION_AUTH_TOKEN, never a flag. --endpoint compares the public",
11120
+ "proof with the worker's own authenticated identity; the gateway listing itself is not visible here.",
11121
+ "From the pack's CLI image use --network host to reach the worker on 127.0.0.1."
11122
+ ].join("\n")).option("--json", "Output as JSON").action(async (opts, command) => {
10298
11123
  try {
11124
+ if (opts.running && !opts.address && !opts.sessionUrl) throw new Error("--running needs --address or --session-url");
11125
+ if (opts.endpoint && !opts.sessionUrl) throw new Error("--endpoint needs --session-url: the public proof is checked against the worker's own identity");
11126
+ if (opts.offline && (opts.address || opts.endpoint || opts.running)) throw new Error("--offline cannot check chain state; drop --offline");
10299
11127
  const checks = [];
10300
- if (opts.mode === "cloud") {
11128
+ if (opts.running) {
11129
+ } else if (opts.mode === "cloud") {
10301
11130
  checks.push(executableCheck("mise"), executableCheck("uv"));
10302
11131
  checks.push(pathCheck(
10303
11132
  "gpu-providers",
@@ -10315,14 +11144,34 @@ function minerDoctorCmd() {
10315
11144
  checks.push(pathCheck("device:kvm", "/dev/kvm"));
10316
11145
  }
10317
11146
  }
11147
+ if (opts.sessionUrl) checks.push(await probeMinerSessionInfo(opts.sessionUrl, process.env.SESSION_AUTH_TOKEN));
11148
+ const local = opts.sessionUrl && (opts.running || opts.endpoint) ? await readLocalIdentity(opts.sessionUrl, process.env.SESSION_AUTH_TOKEN) : void 0;
11149
+ if (local) checks.push(local.check);
11150
+ const address = opts.address ?? local?.info?.minerAddress;
11151
+ if (opts.address && local?.info && local.info.minerAddress !== opts.address) {
11152
+ checks.push({
11153
+ id: "session:account",
11154
+ status: "fail",
11155
+ required: true,
11156
+ detail: `worker signs as ${local.info.minerAddress}, not ${opts.address}`,
11157
+ next: "check the miner key in the pack configuration"
11158
+ });
11159
+ }
10318
11160
  if (!opts.offline) {
10319
11161
  const rpc = getRpc(command);
10320
11162
  const client = await createClient(rpc);
10321
- checks.push(await probeRpc(client.api, rpc));
10322
- await client.disconnect();
11163
+ try {
11164
+ const rpcCheck = await probeRpc(client.api, rpc);
11165
+ checks.push(rpcCheck);
11166
+ if (rpcCheck.status === "pass" && address) checks.push(await probeMinerChain(client.api, address));
11167
+ if (rpcCheck.status === "pass" && opts.endpoint && address && local?.info?.modelIdentity) {
11168
+ checks.push(...await probePublicEndpoint(client.api, opts.endpoint, address, local.info));
11169
+ }
11170
+ } finally {
11171
+ await client.disconnect();
11172
+ }
10323
11173
  }
10324
- if (opts.sessionUrl) checks.push(await probeMinerSessionInfo(opts.sessionUrl, process.env.SESSION_AUTH_TOKEN));
10325
- const report = finalizeDoctor("miner", opts.mode, checks);
11174
+ const report = finalizeDoctor("miner", opts.running ? "running" : opts.mode, checks);
10326
11175
  printDoctor(report, getJson(command));
10327
11176
  if (!report.ready) process.exitCode = 1;
10328
11177
  } catch (error) {
@@ -10331,7 +11180,7 @@ function minerDoctorCmd() {
10331
11180
  });
10332
11181
  }
10333
11182
  function minerStatusCmd() {
10334
- return new Command38("status").description("Show miner registration, calibration, and optional session status").argument("[address]", "Miner SS58 address; defaults to the configured signer").option("--session-url <url>", "Optional miner session info URL").option(
11183
+ return new Command40("status").description("Show miner registration, calibration, and optional session status").argument("[address]", "Miner SS58 address; defaults to the configured signer").option("--session-url <url>", "Optional miner session info URL").option(
10335
11184
  "--expect-gpu <SKU[:COUNT]>",
10336
11185
  "Expected inventory; repeat to check attested state",
10337
11186
  collectGpu,
@@ -10381,7 +11230,7 @@ function minerStatusCmd() {
10381
11230
  });
10382
11231
  }
10383
11232
  function minerSkusCmd() {
10384
- return new Command38("skus").description("List GPU SKUs with active governance calibration ceilings").option("--json", "Output as JSON").action(async (_opts, command) => {
11233
+ return new Command40("skus").description("List GPU SKUs with active governance calibration ceilings").option("--json", "Output as JSON").action(async (_opts, command) => {
10385
11234
  let client;
10386
11235
  try {
10387
11236
  const rpc = getRpc(command);
@@ -10405,7 +11254,7 @@ function minerSkusCmd() {
10405
11254
  });
10406
11255
  }
10407
11256
  function minerRegisterCmd() {
10408
- return new Command38("register").description("Register the signer as a miner and lock the runtime-defined self-stake").option("--commission <percent>", "Commission percent (0-20)", "10").option("--json", "Output as JSON").action(async (opts, command) => {
11257
+ return new Command40("register").description("Register the signer as a miner and lock the runtime-defined self-stake").option("--commission <percent>", "Commission percent (0-20)", "10").option("--json", "Output as JSON").action(async (opts, command) => {
10409
11258
  let client;
10410
11259
  try {
10411
11260
  const commission = Number(opts.commission);
@@ -10432,7 +11281,7 @@ function minerRegisterCmd() {
10432
11281
  });
10433
11282
  }
10434
11283
  function minerDeclareGpuCmd() {
10435
- return new Command38("declare-gpu").description("Publish the signer's self-reported GPU inventory as a flop.gpu-inventory.v1 remark (display-only)").option("--nvidia-smi <path>", "Output of `nvidia-smi --query-gpu=name,memory.total --format=csv,noheader,nounits` (- for stdin)").option("--detect", "Run nvidia-smi here (on the GPU host)").option("--retract", "Publish an empty inventory, withdrawing an earlier declaration").option("--dry-run", "Print the remark without signing").option("--json", "Output as JSON").action(async (opts, command) => {
11284
+ return new Command40("declare-gpu").description("Publish the signer's self-reported GPU inventory as a flop.gpu-inventory.v1 remark (display-only)").option("--nvidia-smi <path>", "Output of `nvidia-smi --query-gpu=name,memory.total --format=csv,noheader,nounits` (- for stdin)").option("--detect", "Run nvidia-smi here (on the GPU host)").option("--retract", "Publish an empty inventory, withdrawing an earlier declaration").option("--dry-run", "Print the remark without signing").option("--json", "Output as JSON").action(async (opts, command) => {
10436
11285
  let client;
10437
11286
  try {
10438
11287
  if ([opts.nvidiaSmi, opts.detect, opts.retract].filter(Boolean).length !== 1) {
@@ -10468,20 +11317,21 @@ function minerDeclareGpuCmd() {
10468
11317
  });
10469
11318
  }
10470
11319
  function minerCmd() {
10471
- const command = new Command38("miner").description("Onboard and operate inference miners");
11320
+ const command = new Command40("miner").description("Onboard and operate inference miners");
10472
11321
  command.addCommand(minerInitCmd());
10473
11322
  command.addCommand(minerDoctorCmd());
10474
11323
  command.addCommand(minerStatusCmd());
10475
11324
  command.addCommand(minerSkusCmd());
10476
11325
  command.addCommand(minerRegisterCmd());
10477
11326
  command.addCommand(minerDeclareGpuCmd());
11327
+ command.addCommand(minerCapacityRateCmd());
10478
11328
  command.addCommand(stakeInfoCmd());
10479
11329
  return command;
10480
11330
  }
10481
11331
 
10482
11332
  // src/commands/validator.ts
10483
11333
  import path4 from "node:path";
10484
- import { Command as Command39, Option as Option7 } from "commander";
11334
+ import { Command as Command41, Option as Option7 } from "commander";
10485
11335
  import { formatFlop as formatFlop3 } from "@flop-labs/sdk";
10486
11336
  function buildValidatorInitPlan(mode, minimumStake) {
10487
11337
  const bringUp = mode === "local" ? "just run-node" : "infra/vultr/create-vm.sh <args>; infra/vultr/deploy-core.sh --vm-ip <TAILSCALE-IP>";
@@ -10531,7 +11381,7 @@ function buildValidatorInitPlan(mode, minimumStake) {
10531
11381
  };
10532
11382
  }
10533
11383
  function validatorInitCmd() {
10534
- return new Command39("init").description("Print a validator onboarding plan with the live minimum stake").addOption(new Option7("--mode <mode>", "Validator deployment mode").choices(["local", "vultr"]).default("local")).option("--offline", "Do not query the configured chain").option("--json", "Output as JSON").action(async (opts, command) => {
11384
+ return new Command41("init").description("Print a validator onboarding plan with the live minimum stake").addOption(new Option7("--mode <mode>", "Validator deployment mode").choices(["local", "vultr"]).default("local")).option("--offline", "Do not query the configured chain").option("--json", "Output as JSON").action(async (opts, command) => {
10535
11385
  let client;
10536
11386
  try {
10537
11387
  let minimumStake;
@@ -10550,7 +11400,7 @@ function validatorInitCmd() {
10550
11400
  });
10551
11401
  }
10552
11402
  function validatorDoctorCmd() {
10553
- return new Command39("doctor").description("Check validator node and on-chain eligibility prerequisites").addOption(new Option7("--mode <mode>", "Validator deployment mode").choices(["local", "vultr"]).default("local")).option("--repo <path>", "flop-core checkout", process.cwd()).option("--offline", "Skip chain and account checks").option("--json", "Output as JSON").action(async (opts, command) => {
11403
+ return new Command41("doctor").description("Check validator node and on-chain eligibility prerequisites").addOption(new Option7("--mode <mode>", "Validator deployment mode").choices(["local", "vultr"]).default("local")).option("--repo <path>", "flop-core checkout", process.cwd()).option("--offline", "Skip chain and account checks").option("--json", "Output as JSON").action(async (opts, command) => {
10554
11404
  let client;
10555
11405
  try {
10556
11406
  const checks = [
@@ -10621,7 +11471,7 @@ function validatorDoctorCmd() {
10621
11471
  });
10622
11472
  }
10623
11473
  function validatorStatusCmd() {
10624
- return new Command39("status").description("Show validator stake, jail, performance, and verified-work state").argument("[address]", "Validator SS58 address; defaults to the configured signer").option("--json", "Output as JSON").action(async (address, opts, command) => {
11474
+ return new Command41("status").description("Show validator stake, jail, performance, and verified-work state").argument("[address]", "Validator SS58 address; defaults to the configured signer").option("--json", "Output as JSON").action(async (address, opts, command) => {
10625
11475
  let client;
10626
11476
  try {
10627
11477
  const rpc = getRpc(command);
@@ -10666,7 +11516,7 @@ function validatorStatusCmd() {
10666
11516
  });
10667
11517
  }
10668
11518
  function validatorRegisterCmd() {
10669
- return new Command39("register").description("Register the signer as a validator, bonding the effective minimum").option("--json", "Output as JSON").action(async (opts, command) => {
11519
+ return new Command41("register").description("Register the signer as a validator, bonding the effective minimum").option("--json", "Output as JSON").action(async (opts, command) => {
10670
11520
  let client;
10671
11521
  try {
10672
11522
  const rpc = getRpc(command);
@@ -10696,7 +11546,7 @@ function validatorRegisterCmd() {
10696
11546
  });
10697
11547
  }
10698
11548
  function validatorCommissionCmd() {
10699
- return new Command39("commission").description("Set validator commission").argument("<percent>", "Commission percent (0-100)").option("--json", "Output as JSON").action(async (percent, opts, command) => {
11549
+ return new Command41("commission").description("Set validator commission").argument("<percent>", "Commission percent (0-100)").option("--json", "Output as JSON").action(async (percent, opts, command) => {
10700
11550
  let client;
10701
11551
  try {
10702
11552
  const value = Number(percent);
@@ -10716,7 +11566,7 @@ function validatorCommissionCmd() {
10716
11566
  });
10717
11567
  }
10718
11568
  function validatorLifecycleCmd(name, description, sdkMethod) {
10719
- return new Command39(name).description(description).option("--json", "Output as JSON").action(async (opts, command) => {
11569
+ return new Command41(name).description(description).option("--json", "Output as JSON").action(async (opts, command) => {
10720
11570
  let client;
10721
11571
  try {
10722
11572
  const rpc = getRpc(command);
@@ -10732,7 +11582,7 @@ function validatorLifecycleCmd(name, description, sdkMethod) {
10732
11582
  });
10733
11583
  }
10734
11584
  function validatorCmd() {
10735
- const command = new Command39("validator").description("Onboard and operate validator nodes");
11585
+ const command = new Command41("validator").description("Onboard and operate validator nodes");
10736
11586
  command.addCommand(validatorInitCmd());
10737
11587
  command.addCommand(validatorDoctorCmd());
10738
11588
  command.addCommand(validatorStatusCmd());
@@ -10745,7 +11595,7 @@ function validatorCmd() {
10745
11595
  }
10746
11596
 
10747
11597
  // src/commands/network-status.ts
10748
- import { Command as Command40 } from "commander";
11598
+ import { Command as Command42 } from "commander";
10749
11599
 
10750
11600
  // src/network.ts
10751
11601
  async function probeRpcChain(network, rpc) {
@@ -10779,7 +11629,7 @@ function networkStatusCmd(probeRpc2 = probeRpcChain, probeFaucet = async (endpoi
10779
11629
  return { ...identity, ready: null, readinessError: error instanceof Error ? error.message : String(error) };
10780
11630
  }
10781
11631
  }) {
10782
- return new Command40("status").description("Show resolved network endpoints and live chain identity").option("--offline", "Resolve configured endpoints without connecting").option("--json", "Output as JSON").action(async (opts, command) => {
11632
+ return new Command42("status").description("Show resolved network endpoints and live chain identity").option("--offline", "Resolve configured endpoints without connecting").option("--json", "Output as JSON").action(async (opts, command) => {
10783
11633
  const resolved = resolveNetwork(command);
10784
11634
  let rpcInfo = null;
10785
11635
  let rpcError;
@@ -10833,9 +11683,9 @@ function networkStatusCmd(probeRpc2 = probeRpcChain, probeFaucet = async (endpoi
10833
11683
  }
10834
11684
 
10835
11685
  // src/commands/faucet-request.ts
10836
- import { Command as Command41 } from "commander";
11686
+ import { Command as Command43 } from "commander";
10837
11687
  function faucetRequestCmd(probeRpc2 = probeRpcChain) {
10838
- return new Command41("request").description("Request dev/test FLOP after verifying faucet and RPC chain identity").argument("<address>", "SS58 address to fund").option("--amount <flop>", "Requested FLOP amount; server policy may cap it").option("--endpoint <url>", "Explicit faucet endpoint override").option("--json", "Output as JSON").action(async (address, opts, command) => {
11688
+ return new Command43("request").description("Request dev/test FLOP after verifying faucet and RPC chain identity").argument("<address>", "SS58 address to fund").option("--amount <flop>", "Requested FLOP amount; server policy may cap it").option("--endpoint <url>", "Explicit faucet endpoint override").option("--json", "Output as JSON").action(async (address, opts, command) => {
10839
11689
  try {
10840
11690
  const resolved = resolveNetwork(command);
10841
11691
  const endpoint = opts.endpoint ?? resolved.profile.faucet;
@@ -10862,6 +11712,25 @@ function faucetRequestCmd(probeRpc2 = probeRpcChain) {
10862
11712
  amount,
10863
11713
  maxRetries: 1
10864
11714
  });
11715
+ if (!result.success && result.outcome === "unknown") {
11716
+ printError({
11717
+ code: "FAUCET_OUTCOME_UNKNOWN",
11718
+ message: `${result.error ?? "Faucet outcome unknown"}; reconcile the original request before requesting again`,
11719
+ retryable: false,
11720
+ next: "reconcile the original request before requesting again",
11721
+ network: resolved.network,
11722
+ address,
11723
+ faucet: endpoint,
11724
+ genesis_hash: rpcChain.genesisHash,
11725
+ ...result.status !== void 0 ? { status: result.status } : {},
11726
+ ...result.reason ? { reason: result.reason } : {},
11727
+ ...result.retryAfterSeconds !== void 0 ? { retry_after_seconds: result.retryAfterSeconds } : {},
11728
+ ...result.txHash ? { tx_hash: result.txHash } : {},
11729
+ ...result.amount !== void 0 ? { amount: result.amount } : {}
11730
+ }, getJson(command));
11731
+ process.exitCode = 1;
11732
+ return;
11733
+ }
10865
11734
  if (!result.success) {
10866
11735
  commandError(new Error(result.error ?? "Faucet request failed"), {
10867
11736
  code: result.status === 429 ? "RATE_LIMITED" : "FAUCET_REQUEST_FAILED",
@@ -10876,7 +11745,9 @@ function faucetRequestCmd(probeRpc2 = probeRpcChain) {
10876
11745
  amount: result.amount,
10877
11746
  tx_hash: result.txHash,
10878
11747
  faucet: endpoint,
10879
- genesis_hash: rpcChain.genesisHash
11748
+ genesis_hash: rpcChain.genesisHash,
11749
+ status: "unconfirmed",
11750
+ recovery: "reconcile the original request before requesting again"
10880
11751
  }, getJson(command));
10881
11752
  } catch (error) {
10882
11753
  commandError(error, {
@@ -10888,16 +11759,16 @@ function faucetRequestCmd(probeRpc2 = probeRpcChain) {
10888
11759
  }
10889
11760
 
10890
11761
  // src/commands/chat.ts
10891
- import { Command as Command42 } from "commander";
11762
+ import { Command as Command44 } from "commander";
10892
11763
  import { randomUUID as randomUUID4 } from "node:crypto";
10893
11764
  import { homedir as homedir4 } from "node:os";
10894
- import { join as join9 } from "node:path";
11765
+ import { join as join10 } from "node:path";
10895
11766
  import { createInterface } from "node:readline/promises";
10896
11767
  import { stdin, stdout } from "node:process";
10897
11768
 
10898
11769
  // src/conversations.ts
10899
- import { mkdir as mkdir13, readFile as readFile8, rename as rename8, writeFile as writeFile11 } from "node:fs/promises";
10900
- import { dirname as dirname6 } from "node:path";
11770
+ import { mkdir as mkdir14, readFile as readFile8, rename as rename9, writeFile as writeFile11 } from "node:fs/promises";
11771
+ import { dirname as dirname8 } from "node:path";
10901
11772
  var MAX_CONVERSATIONS = 100;
10902
11773
  var MAX_CONVERSATION_MESSAGES = 100;
10903
11774
  var MAX_MESSAGE_CHARS = 16384;
@@ -10916,11 +11787,11 @@ var ConversationStore = class {
10916
11787
  }
10917
11788
  }
10918
11789
  async write(data) {
10919
- await mkdir13(dirname6(this.path), { recursive: true, mode: 448 });
11790
+ await mkdir14(dirname8(this.path), { recursive: true, mode: 448 });
10920
11791
  const temp = `${this.path}.${process.pid}.tmp`;
10921
11792
  await writeFile11(temp, `${JSON.stringify(data, null, 2)}
10922
11793
  `, { mode: 384 });
10923
- await rename8(temp, this.path);
11794
+ await rename9(temp, this.path);
10924
11795
  }
10925
11796
  async list() {
10926
11797
  return Object.values((await this.read()).conversations).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
@@ -10955,7 +11826,7 @@ function conversationPrompt(messages) {
10955
11826
 
10956
11827
  // src/commands/chat.ts
10957
11828
  function chatCmd() {
10958
- return new Command42("chat").description("Start or resume a persistent FLOP chat, optionally through a bounded session").argument("[prompt]", "One-shot prompt; omit for interactive chat").option("--conversation <id>", "Resume a conversation ID").option("--model <id>", "32-byte model hash or exact registered name from flop model list").option("--session-owner <address>", "Use --signer as this owner's session delegate").option("--max-flop <n>", "Per-turn FLOP cap", "5").option("--tee-requirements <hex>", "Registered HTLC appraisal commitments (100-byte SCALE hex)", process.env.FLOP_HTLC_TEE_REQUIREMENTS).option("--skip-model-check", "Allow an unregistered model (local development only)").option("--history", "List stored conversations without connecting").option("--delete <id>", "Delete a stored conversation without connecting").option("--store <path>", "Conversation store path", join9(homedir4(), ".flop", "conversations.json")).option("--json", "Output as JSON").action(async (prompt, opts, command) => {
11829
+ return new Command44("chat").description("Start or resume a persistent FLOP chat, optionally through a bounded session").argument("[prompt]", "One-shot prompt; omit for interactive chat").option("--conversation <id>", "Resume a conversation ID").option("--model <id>", "32-byte model hash or exact registered name from flop model list").option("--session-owner <address>", "Use --signer as this owner's session delegate").option("--max-flop <n>", "Per-turn FLOP cap", "5").option("--tee-requirements <hex>", "Registered HTLC appraisal commitments (100-byte SCALE hex)", process.env.FLOP_HTLC_TEE_REQUIREMENTS).option("--skip-model-check", "Allow an unregistered model (local development only)").option("--history", "List stored conversations without connecting").option("--delete <id>", "Delete a stored conversation without connecting").option("--store <path>", "Conversation store path", join10(homedir4(), ".flop", "conversations.json")).option("--json", "Output as JSON").action(async (prompt, opts, command) => {
10959
11830
  const store = new ConversationStore(opts.store);
10960
11831
  let agent;
10961
11832
  try {
@@ -11080,7 +11951,7 @@ function chatCmd() {
11080
11951
  }
11081
11952
 
11082
11953
  // src/commands/tx-build.ts
11083
- import { Command as Command43 } from "commander";
11954
+ import { Command as Command45 } from "commander";
11084
11955
  import { writeFile as writeFile12 } from "node:fs/promises";
11085
11956
  import { execFile as execFile5 } from "node:child_process";
11086
11957
  import { promisify as promisify5 } from "node:util";
@@ -11147,7 +12018,7 @@ function assertEnvelopeChain(api, envelope) {
11147
12018
 
11148
12019
  // src/commands/tx-build.ts
11149
12020
  function txBuildCmd() {
11150
- return new Command43("build").description("Build an immortal unsigned transaction envelope for air-gapped signing").requiredOption("--address <address>", "Signing account").requiredOption("--call <hex>", "SCALE call hex").option("--out <path>", "Write envelope JSON").option("--qr", "Render the signing payload using installed qrencode").option("--json", "Output as JSON").action(async (opts, command) => {
12021
+ return new Command45("build").description("Build an immortal unsigned transaction envelope for air-gapped signing").requiredOption("--address <address>", "Signing account").requiredOption("--call <hex>", "SCALE call hex").option("--out <path>", "Write envelope JSON").option("--qr", "Render the signing payload using installed qrencode").option("--json", "Output as JSON").action(async (opts, command) => {
11151
12022
  let client;
11152
12023
  try {
11153
12024
  const resolved = resolveNetwork(command);
@@ -11192,10 +12063,10 @@ function txBuildCmd() {
11192
12063
  }
11193
12064
 
11194
12065
  // src/commands/tx-broadcast.ts
11195
- import { Command as Command44 } from "commander";
12066
+ import { Command as Command46 } from "commander";
11196
12067
  import { readFile as readFile9 } from "node:fs/promises";
11197
12068
  function txBroadcastCmd() {
11198
- return new Command44("broadcast").description("Import an offline signature, reject chain drift, and broadcast").argument("<envelope>", "Unsigned envelope JSON").requiredOption("--signature <hex>", "SCALE MultiSignature from the offline signer").option("--allow-nonce-drift", "Broadcast even if account nonce has changed").option("--json", "Output as JSON").action(async (path5, opts, command) => {
12069
+ return new Command46("broadcast").description("Import an offline signature, reject chain drift, and broadcast").argument("<envelope>", "Unsigned envelope JSON").requiredOption("--signature <hex>", "SCALE MultiSignature from the offline signer").option("--allow-nonce-drift", "Broadcast even if account nonce has changed").option("--json", "Output as JSON").action(async (path5, opts, command) => {
11199
12070
  let client;
11200
12071
  try {
11201
12072
  if (!/^0x[0-9a-f]+$/i.test(opts.signature)) throw new Error("--signature must be 0x hex");
@@ -11231,7 +12102,7 @@ function txBroadcastCmd() {
11231
12102
  }
11232
12103
 
11233
12104
  // src/commands/multisig.ts
11234
- import { Command as Command45 } from "commander";
12105
+ import { Command as Command47 } from "commander";
11235
12106
  import { createKeyMulti, encodeAddress, sortAddresses } from "@polkadot/util-crypto";
11236
12107
  import { blake2AsHex } from "@polkadot/util-crypto";
11237
12108
  function otherSignatories(signer, values) {
@@ -11252,8 +12123,8 @@ function timepoint(opts) {
11252
12123
  return { height: parsePositiveInt(opts.height, "--height"), index };
11253
12124
  }
11254
12125
  function multisigCmd() {
11255
- const command = new Command45("multisig").description("Participate in Substrate multisig transactions");
11256
- command.addCommand(new Command45("address").description("Derive the deterministic multisig address").requiredOption("--threshold <n>", "Approval threshold").requiredOption("--signatory <address...>", "All signatory addresses").option("--json", "Output as JSON").action(async (opts, cmd) => {
12126
+ const command = new Command47("multisig").description("Participate in Substrate multisig transactions");
12127
+ command.addCommand(new Command47("address").description("Derive the deterministic multisig address").requiredOption("--threshold <n>", "Approval threshold").requiredOption("--signatory <address...>", "All signatory addresses").option("--json", "Output as JSON").action(async (opts, cmd) => {
11257
12128
  try {
11258
12129
  const threshold = parsePositiveInt(opts.threshold, "--threshold");
11259
12130
  const sorted = sortAddresses(opts.signatory, 42);
@@ -11268,7 +12139,7 @@ function multisigCmd() {
11268
12139
  commandError(error);
11269
12140
  }
11270
12141
  }));
11271
- command.addCommand(new Command45("propose").description("Create or execute a multisig call").requiredOption("--threshold <n>", "Approval threshold").requiredOption("--other <address...>", "Other signatories (exclude configured signer)").requiredOption("--call <hex>", "SCALE call hex").option("--max-ref-time <n>", "Maximum call refTime").option("--max-proof-size <n>", "Maximum call proofSize").option("--json", "Output as JSON").action(async (opts, cmd) => {
12142
+ command.addCommand(new Command47("propose").description("Create or execute a multisig call").requiredOption("--threshold <n>", "Approval threshold").requiredOption("--other <address...>", "Other signatories (exclude configured signer)").requiredOption("--call <hex>", "SCALE call hex").option("--max-ref-time <n>", "Maximum call refTime").option("--max-proof-size <n>", "Maximum call proofSize").option("--json", "Output as JSON").action(async (opts, cmd) => {
11272
12143
  let client;
11273
12144
  try {
11274
12145
  client = await createClient(getRpc(cmd), getSigner(cmd));
@@ -11291,7 +12162,7 @@ function multisigCmd() {
11291
12162
  await client?.disconnect();
11292
12163
  }
11293
12164
  }));
11294
- command.addCommand(new Command45("approve").description("Approve a pending multisig call hash").requiredOption("--threshold <n>", "Approval threshold").requiredOption("--other <address...>", "Other signatories").requiredOption("--call-hash <hex>", "Pending call hash").requiredOption("--height <n>", "Timepoint block height").requiredOption("--index <n>", "Timepoint extrinsic index").option("--max-ref-time <n>", "Maximum call refTime").option("--max-proof-size <n>", "Maximum call proofSize").option("--json", "Output as JSON").action(async (opts, cmd) => {
12165
+ command.addCommand(new Command47("approve").description("Approve a pending multisig call hash").requiredOption("--threshold <n>", "Approval threshold").requiredOption("--other <address...>", "Other signatories").requiredOption("--call-hash <hex>", "Pending call hash").requiredOption("--height <n>", "Timepoint block height").requiredOption("--index <n>", "Timepoint extrinsic index").option("--max-ref-time <n>", "Maximum call refTime").option("--max-proof-size <n>", "Maximum call proofSize").option("--json", "Output as JSON").action(async (opts, cmd) => {
11295
12166
  let client;
11296
12167
  try {
11297
12168
  client = await createClient(getRpc(cmd), getSigner(cmd));
@@ -11310,7 +12181,7 @@ function multisigCmd() {
11310
12181
  await client?.disconnect();
11311
12182
  }
11312
12183
  }));
11313
- command.addCommand(new Command45("cancel").description("Cancel a pending multisig as its depositor").requiredOption("--threshold <n>", "Approval threshold").requiredOption("--other <address...>", "Other signatories").requiredOption("--call-hash <hex>", "Pending call hash").requiredOption("--height <n>", "Timepoint block height").requiredOption("--index <n>", "Timepoint extrinsic index").option("--json", "Output as JSON").action(async (opts, cmd) => {
12184
+ command.addCommand(new Command47("cancel").description("Cancel a pending multisig as its depositor").requiredOption("--threshold <n>", "Approval threshold").requiredOption("--other <address...>", "Other signatories").requiredOption("--call-hash <hex>", "Pending call hash").requiredOption("--height <n>", "Timepoint block height").requiredOption("--index <n>", "Timepoint extrinsic index").option("--json", "Output as JSON").action(async (opts, cmd) => {
11314
12185
  let client;
11315
12186
  try {
11316
12187
  client = await createClient(getRpc(cmd), getSigner(cmd));
@@ -11327,7 +12198,7 @@ function multisigCmd() {
11327
12198
  await client?.disconnect();
11328
12199
  }
11329
12200
  }));
11330
- command.addCommand(new Command45("list").alias("status").description("List pending calls for a multisig account").requiredOption("--account <address>", "Multisig account").option("--json", "Output as JSON").action(async (opts, cmd) => {
12201
+ command.addCommand(new Command47("list").alias("status").description("List pending calls for a multisig account").requiredOption("--account <address>", "Multisig account").option("--json", "Output as JSON").action(async (opts, cmd) => {
11331
12202
  let client;
11332
12203
  try {
11333
12204
  client = await createClient(getRpc(cmd));
@@ -11347,10 +12218,10 @@ function multisigCmd() {
11347
12218
  }
11348
12219
 
11349
12220
  // src/commands/channel.ts
11350
- import { Command as Command46 } from "commander";
12221
+ import { Command as Command48 } from "commander";
11351
12222
  function channelCmd() {
11352
- const command = new Command46("channel").description("Inspect and finalize compute channels");
11353
- command.addCommand(new Command46("list").description("List on-chain compute channels").option("--agent <address>", "Filter by paying agent").option("--miner <address>", "Filter by serving miner").option("--include-closed", "Include terminal channels").option("--json", "Output as JSON").action(async (opts, cmd) => {
12223
+ const command = new Command48("channel").description("Inspect and finalize compute channels");
12224
+ command.addCommand(new Command48("list").description("List on-chain compute channels").option("--agent <address>", "Filter by paying agent").option("--miner <address>", "Filter by serving miner").option("--include-closed", "Include terminal channels").option("--json", "Output as JSON").action(async (opts, cmd) => {
11354
12225
  let client;
11355
12226
  try {
11356
12227
  client = await createClient(getRpc(cmd));
@@ -11378,7 +12249,7 @@ function channelCmd() {
11378
12249
  await client?.disconnect();
11379
12250
  }
11380
12251
  }));
11381
- command.addCommand(new Command46("finalize").description("Finalize a force-settled channel after its challenge window").argument("<channel-id>", "0x channel ID").option("--json", "Output as JSON").action(async (channelId, _opts, cmd) => {
12252
+ command.addCommand(new Command48("finalize").description("Finalize a force-settled channel after its challenge window").argument("<channel-id>", "0x channel ID").option("--json", "Output as JSON").action(async (channelId, _opts, cmd) => {
11382
12253
  let client;
11383
12254
  try {
11384
12255
  client = await createClient(getRpc(cmd), getSigner(cmd));
@@ -11420,7 +12291,7 @@ function timestamp(value, label) {
11420
12291
  if (!Number.isFinite(parsed)) throw new Error(`${label} must be an ISO-8601 timestamp`);
11421
12292
  return parsed;
11422
12293
  }
11423
- function decimal(value, label) {
12294
+ function decimal2(value, label) {
11424
12295
  if (!DECIMAL.test(value)) throw new Error(`${label} must be a non-negative decimal string`);
11425
12296
  }
11426
12297
  function positiveInt(value, label) {
@@ -11448,6 +12319,12 @@ function parseCanonicalCatalog(value, options = {}) {
11448
12319
  if (!HASH.test(record.decode_policy_hash)) throw new Error(`${label}.decode_policy_hash must be lowercase 32-byte hex`);
11449
12320
  if (!Number.isSafeInteger(record.precision) || record.precision < 0 || record.precision > 5) throw new Error(`${label}.precision is invalid`);
11450
12321
  if (!QUANTIZATIONS.has(record.quantization)) throw new Error(`${label}.quantization is invalid`);
12322
+ if (record.precision_detail !== void 0) {
12323
+ if (!record.precision_detail || typeof record.precision_detail !== "object" || Array.isArray(record.precision_detail)) throw new Error(`${label}.precision_detail is invalid`);
12324
+ for (const field2 of ["activations", "kv_cache"]) {
12325
+ if (record.precision_detail[field2] !== void 0 && !QUANTIZATIONS.has(record.precision_detail[field2])) throw new Error(`${label}.precision_detail.${field2} is invalid`);
12326
+ }
12327
+ }
11451
12328
  if (!Array.isArray(record.input_modalities) || record.input_modalities.length < 1 || record.input_modalities.some((v) => !INPUT_MODALITIES.has(v))) throw new Error(`${label}.input_modalities is invalid`);
11452
12329
  if (!Array.isArray(record.output_modalities) || record.output_modalities.length < 1 || record.output_modalities.some((v) => !OUTPUT_MODALITIES.has(v))) throw new Error(`${label}.output_modalities is invalid`);
11453
12330
  if (!Array.isArray(record.supported_sampling_parameters) || record.supported_sampling_parameters.some((v) => !SAMPLING.has(v))) throw new Error(`${label}.supported_sampling_parameters is invalid`);
@@ -11478,9 +12355,9 @@ function parseCanonicalCatalog(value, options = {}) {
11478
12355
  if (!price.version || !price.miner_address || !price.flop_usd_source) throw new Error(`${priceLabel} provenance is incomplete`);
11479
12356
  if (pricedMiners.has(price.miner_address)) throw new Error(`${priceLabel} duplicates a miner price`);
11480
12357
  pricedMiners.add(price.miner_address);
11481
- for (const field2 of ["flop_per_gn", "gn_per_prompt_token", "gn_per_completion_token", "flop_usd"]) decimal(price[field2], `${priceLabel}.${field2}`);
11482
- if (price.request_usd !== void 0) decimal(price.request_usd, `${priceLabel}.request_usd`);
11483
- if (price.image_usd !== void 0) decimal(price.image_usd, `${priceLabel}.image_usd`);
12358
+ for (const field2 of ["flop_per_gn", "gn_per_prompt_token", "gn_per_completion_token", "flop_usd"]) decimal2(price[field2], `${priceLabel}.${field2}`);
12359
+ if (price.request_usd !== void 0) decimal2(price.request_usd, `${priceLabel}.request_usd`);
12360
+ if (price.image_usd !== void 0) decimal2(price.image_usd, `${priceLabel}.image_usd`);
11484
12361
  timestamp(price.ratified_at, `${priceLabel}.ratified_at`);
11485
12362
  if (timestamp(price.effective_at, `${priceLabel}.effective_at`) >= timestamp(price.expires_at, `${priceLabel}.expires_at`)) throw new Error(`${priceLabel} has an empty validity window`);
11486
12363
  if (price.rounding?.mode !== "ceil" || !Number.isSafeInteger(price.rounding.decimal_places) || price.rounding.decimal_places < 0 || price.rounding.decimal_places > 18) throw new Error(`${priceLabel}.rounding is invalid`);
@@ -11494,7 +12371,7 @@ function parseCanonicalCatalog(value, options = {}) {
11494
12371
  const timed = Number.isSafeInteger(override.utc_start) && Number.isSafeInteger(override.utc_end) && Number(override.utc_start) >= 0 && Number(override.utc_start) <= 2359 && Number(override.utc_end) >= 0 && Number(override.utc_end) <= 2359;
11495
12372
  if (longContext === timed) throw new Error(`${overrideLabel} must have exactly one supported condition kind`);
11496
12373
  for (const field2 of ["prompt", "completion", "request", "image", "input_cache_read", "internal_reasoning"]) {
11497
- if (override[field2] !== void 0) decimal(String(override[field2]), `${overrideLabel}.${field2}`);
12374
+ if (override[field2] !== void 0) decimal2(String(override[field2]), `${overrideLabel}.${field2}`);
11498
12375
  }
11499
12376
  }
11500
12377
  }
@@ -11505,8 +12382,8 @@ function parseCanonicalCatalog(value, options = {}) {
11505
12382
  });
11506
12383
  }
11507
12384
  function parts(value) {
11508
- const [whole, fraction = ""] = value.split(".");
11509
- return { value: BigInt(whole + fraction), scale: fraction.length };
12385
+ const [whole, fraction2 = ""] = value.split(".");
12386
+ return { value: BigInt(whole + fraction2), scale: fraction2.length };
11510
12387
  }
11511
12388
  function multiply(values, places) {
11512
12389
  const product = values.map(parts).reduce((acc, item) => ({ value: acc.value * item.value, scale: acc.scale + item.scale }), { value: 1n, scale: 0 });
@@ -11573,13 +12450,20 @@ function buildLiveCatalog(records, sessions, now = Date.now(), policy) {
11573
12450
  for (const field2 of ["prompt", "completion", "request", "image", "input_cache_read", "internal_reasoning"]) if (compareDecimal(candidate[field2], current[field2]) > 0) next[field2] = candidate[field2];
11574
12451
  return next;
11575
12452
  }, void 0);
12453
+ const ready = is_ready && capacity_tpm > 0;
11576
12454
  return {
11577
12455
  record,
11578
- is_ready: is_ready && capacity_tpm > 0,
11579
- capacity_tpm: is_ready && capacity_tpm > 0 ? capacity_tpm : null,
12456
+ is_ready: ready,
12457
+ capacity_tpm: ready ? capacity_tpm : null,
12458
+ // Report the oldest contributing observation and earliest evidence expiry.
12459
+ // These describe evidence freshness, not a guarantee of continued availability.
12460
+ ...ready ? {
12461
+ capacity_observed_at: priced.reduce((oldest, item) => timestamp(item.session.capacity_observed_at, "capacity_observed_at") < timestamp(oldest, "capacity_observed_at") ? item.session.capacity_observed_at : oldest, priced[0].session.capacity_observed_at),
12462
+ capacity_expires_at: priced.reduce((earliest, item) => timestamp(item.session.capacity_expires_at, "capacity_expires_at") < timestamp(earliest, "capacity_expires_at") ? item.session.capacity_expires_at : earliest, priced[0].session.capacity_expires_at)
12463
+ } : {},
11580
12464
  datacenters: is_ready ? [...new Set(priced.map((item) => item.session.datacenter.country_code))].sort().map((country_code) => ({ country_code })) : [],
11581
12465
  ...is_ready && conservative ? { pricing: conservative } : {},
11582
- price_sources: is_ready ? priced.map(({ session, price }) => ({ miner_address: session.miner_address, version: price.version, source: price.flop_usd_source, effective_at: price.effective_at, expires_at: price.expires_at })).sort((a, b) => a.miner_address.localeCompare(b.miner_address)) : [],
12466
+ price_sources: is_ready ? priced.map(({ session, price }) => ({ miner_address: session.miner_address, version: price.version, source: price.flop_usd_source, effective_at: price.effective_at, expires_at: price.expires_at, ratified_at: price.ratified_at, flop_per_gn: price.flop_per_gn, gn_per_prompt_token: price.gn_per_prompt_token, gn_per_completion_token: price.gn_per_completion_token, flop_usd: price.flop_usd, rounding: price.rounding })).sort((a, b) => a.miner_address.localeCompare(b.miner_address)) : [],
11583
12467
  admitted_sessions: is_ready ? priced.map((item) => item.session.session_id).sort() : []
11584
12468
  };
11585
12469
  });
@@ -11824,12 +12708,12 @@ function generateGatewayConfig(input) {
11824
12708
  }
11825
12709
 
11826
12710
  // src/commands/gateway-allowlist.ts
11827
- import { Command as Command47 } from "commander";
12711
+ import { Command as Command49 } from "commander";
11828
12712
  var PRECISIONS5 = ["FP32", "FP16", "BF16", "INT8", "INT4", "Mixed"];
11829
12713
  function field(value, camel, snake) {
11830
12714
  return value?.[camel] ?? value?.[snake];
11831
12715
  }
11832
- function text(value, label) {
12716
+ function text2(value, label) {
11833
12717
  const json = value?.toJSON?.() ?? value;
11834
12718
  if (typeof json === "string") {
11835
12719
  return json.startsWith("0x") ? Buffer.from(json.slice(2), "hex").toString("utf8") : json;
@@ -11837,7 +12721,7 @@ function text(value, label) {
11837
12721
  if (Array.isArray(json)) return Buffer.from(json).toString("utf8");
11838
12722
  throw new Error(`on-chain ${label} is invalid`);
11839
12723
  }
11840
- function hex2(value, label) {
12724
+ function hex3(value, label) {
11841
12725
  const rendered = value?.toHex?.() ?? value?.toJSON?.() ?? value;
11842
12726
  if (typeof rendered !== "string" || !/^0x[0-9a-fA-F]{64}$/.test(rendered)) {
11843
12727
  throw new Error(`on-chain ${label} is not a 32-byte hash`);
@@ -11857,8 +12741,8 @@ async function fetchMinerSessionInfo(endpoint, authToken, signal) {
11857
12741
  if (anonymous.status !== 401) throw new Error("miner /session/info does not enforce bearer authentication");
11858
12742
  const response = await fetch(infoUrl, { redirect: "error", headers: { Authorization: `Bearer ${authToken}` }, signal });
11859
12743
  if (!response.ok) throw new Error(`miner /session/info returned HTTP ${response.status}`);
11860
- const { computeChannel: computeChannel7 } = await import("@flop-labs/sdk");
11861
- return JSON.parse(await computeChannel7.readMinerResponse(response, 16 * 1024));
12744
+ const { computeChannel: computeChannel9 } = await import("@flop-labs/sdk");
12745
+ return JSON.parse(await computeChannel9.readMinerResponse(response, 16 * 1024));
11862
12746
  }
11863
12747
  async function readChainSnapshot(api, miner, pinnedHead) {
11864
12748
  const identity = miner.modelIdentity;
@@ -11890,16 +12774,16 @@ async function readChainSnapshot(api, miner, pinnedHead) {
11890
12774
  `model ${identity.modelHash} is ${status}, not Verified; refusing to generate a gateway catalog`
11891
12775
  );
11892
12776
  }
11893
- const measuredRoot = rootRaw?.isSome ? hex2(rootRaw.unwrap(), "measuredRoot") : null;
11894
- const allowedDecodePolicyHashes = policiesRaw?.isSome ? Array.from(policiesRaw.unwrap(), (item) => hex2(item, "allowedDecodePolicies")) : [];
12777
+ const measuredRoot = rootRaw?.isSome ? hex3(rootRaw.unwrap(), "measuredRoot") : null;
12778
+ const allowedDecodePolicyHashes = policiesRaw?.isSome ? Array.from(policiesRaw.unwrap(), (item) => hex3(item, "allowedDecodePolicies")) : [];
11895
12779
  const lease = BigInt(at.consts.hwCalibration.calibrationLeaseBlocks.toString());
11896
12780
  const calibration = calibrationRaw?.isSome ? calibrationRaw.unwrap() : null;
11897
12781
  const acceptedAt = calibration ? BigInt(field(calibration, "acceptedAt", "accepted_at").toString()) : 0n;
11898
12782
  return {
11899
12783
  model: {
11900
- modelHash: hex2(field(modelJson, "modelHash", "model_hash"), "modelHash"),
11901
- registryModel: text(field(modelJson, "modelName", "model_name"), "modelName"),
11902
- registryVersion: text(field(modelJson, "version", "version"), "version")
12784
+ modelHash: hex3(field(modelJson, "modelHash", "model_hash"), "modelHash"),
12785
+ registryModel: text2(field(modelJson, "modelName", "model_name"), "modelName"),
12786
+ registryVersion: text2(field(modelJson, "version", "version"), "version")
11903
12787
  },
11904
12788
  measuredRoot,
11905
12789
  allowedDecodePolicyHashes,
@@ -11913,7 +12797,7 @@ async function readChainSnapshot(api, miner, pinnedHead) {
11913
12797
  };
11914
12798
  }
11915
12799
  function gatewayAllowlistCmd() {
11916
- return new Command47("allowlist").description("Generate gateway allowlist and model catalog from a live miner and chain").requiredOption("--miner <url>", "Authenticated miner base URL").requiredOption("--auth-token-env <name>", "Environment variable containing the miner bearer token").requiredOption("--attestation <path>", "Operator/provider deployment, catalog, and price evidence JSON").option("--json", "Output as JSON").action(async (opts, command) => {
12800
+ return new Command49("allowlist").description("Generate gateway allowlist and model catalog from a live miner and chain").requiredOption("--miner <url>", "Authenticated miner base URL").requiredOption("--auth-token-env <name>", "Environment variable containing the miner bearer token").requiredOption("--attestation <path>", "Operator/provider deployment, catalog, and price evidence JSON").option("--json", "Output as JSON").action(async (opts, command) => {
11917
12801
  let client;
11918
12802
  try {
11919
12803
  const authToken = process.env[opts.authTokenEnv];
@@ -11939,13 +12823,13 @@ function gatewayAllowlistCmd() {
11939
12823
  }
11940
12824
 
11941
12825
  // src/commands/gateway-renew.ts
11942
- import { open as open3, readFile as readFile11, rename as rename9, rm as rm5, writeFile as writeFile13 } from "node:fs/promises";
11943
- import { createHash as createHash6 } from "node:crypto";
12826
+ import { open as open3, readFile as readFile11, rename as rename10, rm as rm7, writeFile as writeFile13 } from "node:fs/promises";
12827
+ import { createHash as createHash9 } from "node:crypto";
11944
12828
  import { request as request2 } from "node:http";
11945
12829
  import { setTimeout as delay } from "node:timers/promises";
11946
12830
  import { Keyring as Keyring5 } from "@polkadot/keyring";
11947
- import { decodeAddress as decodeAddress2, randomAsU8a } from "@polkadot/util-crypto";
11948
- import { computeChannel as computeChannel6 } from "@flop-labs/sdk";
12831
+ import { decodeAddress as decodeAddress3, randomAsU8a } from "@polkadot/util-crypto";
12832
+ import { computeChannel as computeChannel8 } from "@flop-labs/sdk";
11949
12833
 
11950
12834
  // ../gateway-core/src/gateway-evidence.ts
11951
12835
  var GATEWAY_EVIDENCE_VERSION = "flop.gateway-evidence.v1";
@@ -11965,7 +12849,7 @@ function instant(value, label) {
11965
12849
  if (!Number.isFinite(parsed)) throw new Error(`${label} must be an ISO-8601 timestamp`);
11966
12850
  return parsed;
11967
12851
  }
11968
- function text2(value, label) {
12852
+ function text3(value, label) {
11969
12853
  if (typeof value !== "string" || !value) throw new Error(`${label} is required`);
11970
12854
  }
11971
12855
  function parseGatewayEvidence(value) {
@@ -11977,15 +12861,15 @@ function parseGatewayEvidence(value) {
11977
12861
  }
11978
12862
  for (const [index, entry] of evidence.deployments.entries()) {
11979
12863
  const label = `deployments[${index}]`;
11980
- text2(entry?.miner_address, `${label}.miner_address`);
11981
- text2(entry.endpoint, `${label}.endpoint`);
12864
+ text3(entry?.miner_address, `${label}.miner_address`);
12865
+ text3(entry.endpoint, `${label}.endpoint`);
11982
12866
  if (!HASH3.test(entry.enclave_key)) throw new Error(`${label}.enclave_key is invalid`);
11983
12867
  const identity = entry.model_identity;
11984
12868
  if (!identity || IDENTITY_FIELDS.some((field2) => field2 === "precision" ? !Number.isSafeInteger(identity.precision) : typeof identity[field2] !== "string" || !identity[field2])) {
11985
12869
  throw new Error(`${label}.model_identity is incomplete`);
11986
12870
  }
11987
- text2(entry.source, `${label}.source`);
11988
- text2(entry.observer, `${label}.observer`);
12871
+ text3(entry.source, `${label}.source`);
12872
+ text3(entry.observer, `${label}.observer`);
11989
12873
  if (!/^[A-Z]{2}$/.test(entry.country_code)) throw new Error(`${label}.country_code is invalid`);
11990
12874
  if (instant(entry.observed_at, `${label}.observed_at`) >= instant(entry.expires_at, `${label}.expires_at`)) {
11991
12875
  throw new Error(`${label} has an empty validity window`);
@@ -11993,10 +12877,10 @@ function parseGatewayEvidence(value) {
11993
12877
  }
11994
12878
  for (const [index, entry] of evidence.conformance.entries()) {
11995
12879
  const label = `conformance[${index}]`;
11996
- text2(entry?.openrouter_slug, `${label}.openrouter_slug`);
11997
- text2(entry.feature, `${label}.feature`);
11998
- text2(entry.registry_version, `${label}.registry_version`);
11999
- text2(entry.observer, `${label}.observer`);
12880
+ text3(entry?.openrouter_slug, `${label}.openrouter_slug`);
12881
+ text3(entry.feature, `${label}.feature`);
12882
+ text3(entry.registry_version, `${label}.registry_version`);
12883
+ text3(entry.observer, `${label}.observer`);
12000
12884
  if (!HASH3.test(entry.model_hash) || !HASH3.test(entry.decode_policy_hash)) throw new Error(`${label} identity hashes are invalid`);
12001
12885
  if (entry.passed !== true) throw new Error(`${label} must record a pass`);
12002
12886
  if (instant(entry.tested_at, `${label}.tested_at`) >= instant(entry.expires_at, `${label}.expires_at`)) {
@@ -12037,11 +12921,11 @@ function renewConformance(record, evidence, now = Date.now()) {
12037
12921
  }
12038
12922
 
12039
12923
  // ../gateway-core/src/tools-qualification.ts
12040
- import { createHash as createHash4 } from "node:crypto";
12924
+ import { createHash as createHash7 } from "node:crypto";
12041
12925
  import { isDeepStrictEqual } from "node:util";
12042
12926
  var OWNER_TOOLS_QUALIFICATION = "flop.owner-tools-qualification.v1";
12043
12927
  function qualificationHash(value) {
12044
- return createHash4("sha256").update(JSON.stringify(value)).digest("hex");
12928
+ return createHash7("sha256").update(JSON.stringify(value)).digest("hex");
12045
12929
  }
12046
12930
  function toolsQualificationEnvelope(model, version, scope) {
12047
12931
  return {
@@ -12139,12 +13023,13 @@ function qualificationSignedToolCalls(frames) {
12139
13023
 
12140
13024
  // ../gateway-core/src/miner-admissions.ts
12141
13025
  import { closeSync, fstatSync, openSync, readSync, statSync } from "node:fs";
12142
- import { createHash as createHash5 } from "node:crypto";
13026
+ import { createHash as createHash8 } from "node:crypto";
12143
13027
  var MAX_ADMITTED_MINERS = 256;
13028
+ var ADMISSION_CONCURRENCY = 4;
12144
13029
  var MAX_FILE_BYTES = 512 * 1024;
12145
13030
  var HASH4 = /^0x[0-9a-f]{64}$/;
12146
13031
  var minerSlot = (miner) => `${miner.minerAddress}:${miner.modelIdentity.requestedSlug}`;
12147
- var staticMinerSha256 = (miner) => createHash5("sha256").update(JSON.stringify(miner)).digest("hex");
13032
+ var staticMinerSha256 = (miner) => createHash8("sha256").update(JSON.stringify(miner)).digest("hex");
12148
13033
  var sameModel = (a, b) => Object.keys(a.modelIdentity).every((key) => a.modelIdentity[key] === b.modelIdentity[key]);
12149
13034
  var MinerRouting = class {
12150
13035
  constructor(statics, path5) {
@@ -12161,8 +13046,8 @@ var MinerRouting = class {
12161
13046
  if (this.path) {
12162
13047
  let stamp;
12163
13048
  try {
12164
- const stat = statSync(this.path);
12165
- stamp = `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeMs}`;
13049
+ const stat2 = statSync(this.path);
13050
+ stamp = `${stat2.dev}:${stat2.ino}:${stat2.size}:${stat2.mtimeMs}`;
12166
13051
  } catch {
12167
13052
  stamp = "missing";
12168
13053
  }
@@ -12174,8 +13059,8 @@ var MinerRouting = class {
12174
13059
  let fd;
12175
13060
  try {
12176
13061
  fd = openSync(this.path, "r");
12177
- const stat = fstatSync(fd);
12178
- if (!stat.isFile() || stat.size > MAX_FILE_BYTES) throw new Error();
13062
+ const stat2 = fstatSync(fd);
13063
+ if (!stat2.isFile() || stat2.size > MAX_FILE_BYTES) throw new Error();
12179
13064
  const buffer = Buffer.alloc(MAX_FILE_BYTES + 1);
12180
13065
  const size = readSync(fd, buffer, 0, buffer.length, 0);
12181
13066
  if (size > MAX_FILE_BYTES) throw new Error();
@@ -12237,8 +13122,8 @@ var MinerAdmissions = class {
12237
13122
  read(now = Date.now()) {
12238
13123
  let stamp;
12239
13124
  try {
12240
- const stat = statSync(this.path);
12241
- stamp = `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeMs}`;
13125
+ const stat2 = statSync(this.path);
13126
+ stamp = `${stat2.dev}:${stat2.ino}:${stat2.size}:${stat2.mtimeMs}`;
12242
13127
  } catch {
12243
13128
  stamp = "missing";
12244
13129
  }
@@ -12249,24 +13134,29 @@ var MinerAdmissions = class {
12249
13134
  let fd;
12250
13135
  try {
12251
13136
  fd = openSync(this.path, "r");
12252
- const stat = fstatSync(fd);
12253
- if (!stat.isFile() || stat.size > MAX_FILE_BYTES) throw new Error("admissions file too large");
13137
+ const stat2 = fstatSync(fd);
13138
+ if (!stat2.isFile() || stat2.size > MAX_FILE_BYTES) throw new Error("admissions file too large");
12254
13139
  const buffer = Buffer.alloc(MAX_FILE_BYTES + 1);
12255
13140
  const size = readSync(fd, buffer, 0, buffer.length, 0);
12256
13141
  if (size > MAX_FILE_BYTES) throw new Error("admissions file too large");
12257
13142
  const file = JSON.parse(buffer.subarray(0, size).toString("utf8"));
12258
13143
  if (file.schema !== "flop-miner-admissions/v1" || !Number.isFinite(Date.parse(file.generatedAt)) || !Array.isArray(file.entries) || file.entries.length > MAX_ADMITTED_MINERS) throw new Error("invalid admissions snapshot");
12259
13144
  const slots = /* @__PURE__ */ new Set();
12260
- const entries = file.entries.map((entry) => {
13145
+ const entries = file.entries.flatMap((entry) => {
12261
13146
  const id = entry.modelIdentity;
12262
13147
  const url = new URL(entry.endpoint);
12263
13148
  const accepted = Date.parse(entry.adAcceptedAt), expiry = Date.parse(entry.admissionExpiresAt);
12264
13149
  if (url.protocol !== "https:" || url.username || url.password || url.pathname !== "/" || url.search || url.hash || typeof entry.minerAddress !== "string" || !entry.minerAddress || !HASH4.test(entry.enclaveKey) || !id || ![id.modelHash, id.decodePolicyHash, id.measuredRoot].every((h) => HASH4.test(h)) || ![id.requestedSlug, id.registryModel, id.registryVersion, id.backendModelName].every((s) => typeof s === "string" && s.length > 0) || !Number.isInteger(id.precision) || id.precision < 0 || id.precision > 5 || !/^[1-9]\d*$/.test(entry.calibrationCapGflopsPerSec) || !Number.isFinite(accepted) || !Number.isFinite(expiry) || accepted > now || expiry <= accepted || expiry > accepted + 36e5 || entry.observation?.countryCode !== null && !/^[A-Z]{2}$/.test(entry.observation?.countryCode) || entry.observation?.regional !== false || entry.observation.observedAt !== entry.adAcceptedAt) throw new Error("invalid admission entry");
12265
- requireWorkerMinimum(entry.worker?.version, entry.worker?.revision);
13150
+ try {
13151
+ requireWorkerMinimum(entry.worker?.version, entry.worker?.revision);
13152
+ } catch (error) {
13153
+ if (error instanceof WorkerUpgradeRequired) return [];
13154
+ throw error;
13155
+ }
12266
13156
  const slot = minerSlot(entry);
12267
13157
  if (slots.has(slot)) throw new Error("duplicate admission slot");
12268
13158
  slots.add(slot);
12269
- return {
13159
+ return [{
12270
13160
  endpoint: entry.endpoint,
12271
13161
  authToken: "",
12272
13162
  channelSigned: true,
@@ -12281,7 +13171,7 @@ var MinerAdmissions = class {
12281
13171
  observedAt: entry.adAcceptedAt,
12282
13172
  expiresAt: entry.admissionExpiresAt
12283
13173
  }
12284
- };
13174
+ }];
12285
13175
  });
12286
13176
  this.entries = entries;
12287
13177
  } catch {
@@ -12292,6 +13182,17 @@ var MinerAdmissions = class {
12292
13182
  return this.entries.filter((entry) => Date.parse(entry.admissionExpiresAt) > now);
12293
13183
  }
12294
13184
  };
13185
+ async function mapAdmissions(items, run2, limit = ADMISSION_CONCURRENCY) {
13186
+ const results = new Array(items.length);
13187
+ let next = 0;
13188
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, async () => {
13189
+ while (next < items.length) {
13190
+ const index = next++;
13191
+ results[index] = await run2(items[index]);
13192
+ }
13193
+ }));
13194
+ return results;
13195
+ }
12295
13196
 
12296
13197
  // ../gateway-core/src/miner-peer.ts
12297
13198
  import { BlockList, isIP } from "node:net";
@@ -12358,16 +13259,16 @@ async function resolveDnsAddresses(host, v4 = resolve42, v6 = resolve62) {
12358
13259
  }
12359
13260
  }
12360
13261
  var systemResolve = resolveDnsAddresses;
12361
- async function resolvePublic(host, resolve10 = systemResolve) {
13262
+ async function resolvePublic(host, resolve11 = systemResolve) {
12362
13263
  const family = isIP(host);
12363
- const addresses = family ? [{ address: host, family }] : await resolve10(host);
13264
+ const addresses = family ? [{ address: host, family }] : await resolve11(host);
12364
13265
  if (addresses.length === 0 || !addresses.every((a) => isPublicAddress(a.address))) {
12365
13266
  throw new EndpointRefusal(`${host} does not resolve to public addresses only`);
12366
13267
  }
12367
13268
  return addresses[0];
12368
13269
  }
12369
13270
  function minerPeerFetch(endpoint, dynamic, before = () => {
12370
- }, resolve10 = systemResolve, send2 = request) {
13271
+ }, resolve11 = systemResolve, send2 = request) {
12371
13272
  return async (input, init = {}) => {
12372
13273
  before();
12373
13274
  const url = new URL(String(input));
@@ -12378,7 +13279,7 @@ function minerPeerFetch(endpoint, dynamic, before = () => {
12378
13279
  const pinned = await new Promise((accept, reject) => {
12379
13280
  const abort = () => reject(signal.reason);
12380
13281
  signal.addEventListener("abort", abort, { once: true });
12381
- resolvePublic(url.hostname.replace(/^\[|\]$/g, ""), resolve10).then(accept, reject).finally(() => signal.removeEventListener("abort", abort));
13282
+ resolvePublic(url.hostname.replace(/^\[|\]$/g, ""), resolve11).then(accept, reject).finally(() => signal.removeEventListener("abort", abort));
12382
13283
  if (signal.aborted) abort();
12383
13284
  });
12384
13285
  before();
@@ -12414,11 +13315,11 @@ function minerPeerFetch(endpoint, dynamic, before = () => {
12414
13315
  }
12415
13316
 
12416
13317
  // src/commands/gateway-renew.ts
12417
- import { Command as Command48 } from "commander";
13318
+ import { Command as Command50 } from "commander";
12418
13319
 
12419
13320
  // src/commands/gateway-tools-qualification.ts
12420
- import { decodeAddress } from "@polkadot/util-crypto";
12421
- import { computeChannel as computeChannel5 } from "@flop-labs/sdk";
13321
+ import { decodeAddress as decodeAddress2 } from "@polkadot/util-crypto";
13322
+ import { computeChannel as computeChannel7 } from "@flop-labs/sdk";
12422
13323
  function require2(value, message) {
12423
13324
  if (!value) throw new Error(message);
12424
13325
  }
@@ -12447,36 +13348,36 @@ async function verifyOwnedToolsQualification(value, record, terminal, now = Date
12447
13348
  require2(wire, "qualification wire policy is unsupported");
12448
13349
  const envelope = toolsQualificationEnvelope(record.openrouter_slug, wire, q);
12449
13350
  assertQualificationEnvelope(envelope, q);
12450
- const input = computeChannel5.encodeChatTurnEnvelope(envelope);
12451
- const receipt = computeChannel5.decodeTurnReceipt(evidence.receipt);
13351
+ const input = computeChannel7.encodeChatTurnEnvelope(envelope);
13352
+ const receipt = computeChannel7.decodeTurnReceipt(evidence.receipt);
12452
13353
  require2(
12453
13354
  receipt.modelIdentity && receipt.modelIdentity.requestedSlug === record.openrouter_slug && receipt.modelIdentity.registryModel === record.registry_model && receipt.modelIdentity.registryVersion === record.registry_version && u8aToHex(receipt.modelIdentity.modelHash) === record.model_hash && receipt.modelIdentity.precision === record.precision && u8aToHex(receipt.modelIdentity.decodePolicyHash) === record.decode_policy_hash && receipt.modelIdentity.backendModelName === record.backend_model_name && receipt.decodePolicyHash && u8aToHex(receipt.decodePolicyHash) === record.decode_policy_hash,
12454
13355
  "qualification receipt model identity differs"
12455
13356
  );
12456
- require2(equal(computeChannel5.blake2_256(input), receipt.hIn), "qualification purpose/envelope preimage differs");
13357
+ require2(equal(computeChannel7.blake2_256(input), receipt.hIn), "qualification purpose/envelope preimage differs");
12457
13358
  require2(
12458
13359
  receipt.promptTokens !== void 0 && receipt.completionTokens !== void 0 && receipt.finishReason,
12459
13360
  "qualification authoritative provider usage is missing"
12460
13361
  );
12461
- require2(equal(computeChannel5.turnEvidenceHash(
13362
+ require2(equal(computeChannel7.turnEvidenceHash(
12462
13363
  receipt.output,
12463
13364
  receipt.promptTokens,
12464
13365
  receipt.completionTokens,
12465
13366
  receipt.finishReason,
12466
13367
  receipt.modelIdentity
12467
13368
  ), receipt.hOut), "qualification output evidence hash differs");
12468
- computeChannel5.validateRenderedChatReceipt(input, receipt);
13369
+ computeChannel7.validateRenderedChatReceipt(input, receipt);
12469
13370
  require2(Array.isArray(evidence.frames) && evidence.frames.length > 0, "qualification signed frames are missing");
12470
13371
  const frames = evidence.frames.map((frame) => ({ kind: frame.kind, data: bytes2(frame.data) }));
12471
- computeChannel5.reconstructSignedTurnOutput(frames, receipt);
13372
+ computeChannel7.reconstructSignedTurnOutput(frames, receipt);
12472
13373
  assertQualificationToolCalls(qualificationSignedToolCalls(frames), receipt.finishReason);
12473
13374
  require2(
12474
13375
  Array.isArray(evidence.prefix) && evidence.prefix.length === receipt.turnIndex + 1 && evidence.prefix.length <= 1024,
12475
13376
  "qualification signed prefix is incomplete"
12476
13377
  );
12477
- const accumulator = new computeChannel5.MerkleAccumulator();
13378
+ const accumulator = new computeChannel7.MerkleAccumulator();
12478
13379
  let aggregate2 = 0n;
12479
- const selectedLeaf = computeChannel5.reconstructLeafVersioned(
13380
+ const selectedLeaf = computeChannel7.reconstructLeafVersioned(
12480
13381
  receipt.leafVersion,
12481
13382
  bytes2(evidence.channel_id),
12482
13383
  receipt.turnIndex,
@@ -12494,7 +13395,7 @@ async function verifyOwnedToolsQualification(value, record, terminal, now = Date
12494
13395
  const t = row, gn = BigInt(t.gN), timing = [BigInt(t.minerRecvMs), BigInt(t.minerDoneMs), BigInt(t.latencyMs)];
12495
13396
  require2(t.turnIndex === index && gn > 0n && gn < 1n << 64n && t.decodePolicyHash === record.decode_policy_hash, "qualification prefix sequence/work differs");
12496
13397
  require2(
12497
- computeChannel5.verifyEnclaveLeaf(
13398
+ computeChannel7.verifyEnclaveLeaf(
12498
13399
  bytes2(evidence.enclave_key),
12499
13400
  t.leafVersion,
12500
13401
  bytes2(evidence.channel_id),
@@ -12510,7 +13411,7 @@ async function verifyOwnedToolsQualification(value, record, terminal, now = Date
12510
13411
  ),
12511
13412
  "qualification prefix signature is invalid"
12512
13413
  );
12513
- const leaf = computeChannel5.reconstructLeafVersioned(
13414
+ const leaf = computeChannel7.reconstructLeafVersioned(
12514
13415
  t.leafVersion,
12515
13416
  bytes2(evidence.channel_id),
12516
13417
  index,
@@ -12529,7 +13430,7 @@ async function verifyOwnedToolsQualification(value, record, terminal, now = Date
12529
13430
  }
12530
13431
  }
12531
13432
  const ack = evidence.ack;
12532
- require2(ack && ack.turnIndex === receipt.turnIndex && ack.leafHash === u8aToHex(selectedLeaf) && computeChannel5.verifyTurnAck(
13433
+ require2(ack && ack.turnIndex === receipt.turnIndex && ack.leafHash === u8aToHex(selectedLeaf) && computeChannel7.verifyTurnAck(
12533
13434
  bytes2(evidence.agent_key),
12534
13435
  bytes2(evidence.channel_id),
12535
13436
  receipt.turnIndex,
@@ -12539,7 +13440,7 @@ async function verifyOwnedToolsQualification(value, record, terminal, now = Date
12539
13440
  bytes2(ack.agentSig)
12540
13441
  ), "qualification turn ACK is invalid");
12541
13442
  const checkpoint = evidence.checkpoint;
12542
- require2(equal(accumulator.root(), receipt.runningRoot) && u8aToHex(accumulator.root()) === checkpoint.finalRoot && aggregate2.toString() === checkpoint.aggregateGn && computeChannel5.verifyReceipt(
13443
+ require2(equal(accumulator.root(), receipt.runningRoot) && u8aToHex(accumulator.root()) === checkpoint.finalRoot && aggregate2.toString() === checkpoint.aggregateGn && computeChannel7.verifyReceipt(
12543
13444
  bytes2(evidence.agent_key),
12544
13445
  bytes2(evidence.channel_id),
12545
13446
  bytes2(checkpoint.finalRoot),
@@ -12610,10 +13511,10 @@ async function readQualificationTerminal(api, eventId, channelId) {
12610
13511
  aggregateGn: data[3].toString(),
12611
13512
  enclaveKey: channel.enclaveKey.toHex(),
12612
13513
  agentKey: channel.agentKey.toHex(),
12613
- minerAccountId: u8aToHex(decodeAddress(channel.miner.toString())),
13514
+ minerAccountId: u8aToHex(decodeAddress2(channel.miner.toString())),
12614
13515
  modelHash: channel.modelHash.toHex(),
12615
13516
  measuredRoot: channel.measuredRoot.toHex(),
12616
- precision: computeChannel5.modelPrecisionCode(channel.precision.toString()),
13517
+ precision: computeChannel7.modelPrecisionCode(channel.precision.toString()),
12617
13518
  timestampMs: Number(stamp.toString()),
12618
13519
  grossRaw: gross.toString(),
12619
13520
  debitRaw: debit.toString()
@@ -12678,7 +13579,7 @@ function tailnetNodeRef(source) {
12678
13579
  return match ? { host: match[1], egress: match[2], asn: match[3] } : null;
12679
13580
  }
12680
13581
  function readTailscaleStatus(socketPath) {
12681
- return new Promise((resolve10, reject) => {
13582
+ return new Promise((resolve11, reject) => {
12682
13583
  const req = request2({
12683
13584
  socketPath,
12684
13585
  path: "/localapi/v0/status",
@@ -12693,7 +13594,7 @@ function readTailscaleStatus(socketPath) {
12693
13594
  res.on("end", () => {
12694
13595
  if (res.statusCode !== 200) return reject(new Error(`tailscaled status returned HTTP ${res.statusCode}`));
12695
13596
  try {
12696
- resolve10(JSON.parse(body));
13597
+ resolve11(JSON.parse(body));
12697
13598
  } catch {
12698
13599
  reject(new Error("tailscaled status is not JSON"));
12699
13600
  }
@@ -12763,7 +13664,7 @@ function conformanceWindows(record) {
12763
13664
  }
12764
13665
  var ProbeSignerUnderfunded = class extends Error {
12765
13666
  };
12766
- function reason(error) {
13667
+ function reason2(error) {
12767
13668
  return error instanceof Error ? error.message : String(error);
12768
13669
  }
12769
13670
  async function renewGatewayEvidence(miners, records, observe) {
@@ -12772,7 +13673,7 @@ async function renewGatewayEvidence(miners, records, observe) {
12772
13673
  const withdrawn = [];
12773
13674
  const observed = [];
12774
13675
  let underfunded;
12775
- for (const miner of miners) {
13676
+ const observations = await mapAdmissions(miners, async (miner) => {
12776
13677
  const subject = `${miner.minerAddress}:${miner.modelIdentity.requestedSlug}`;
12777
13678
  try {
12778
13679
  const declared = miner.deployment;
@@ -12784,8 +13685,7 @@ async function renewGatewayEvidence(miners, records, observe) {
12784
13685
  if (miner.admissionExpiresAt) {
12785
13686
  await observe.miner(miner);
12786
13687
  if (Date.parse(miner.admissionExpiresAt) <= observe.now()) throw new Error("miner admission expired");
12787
- observed.push(miner);
12788
- continue;
13688
+ return { miner };
12789
13689
  }
12790
13690
  if (node) {
12791
13691
  session = await observe.miner(miner);
@@ -12795,7 +13695,7 @@ async function renewGatewayEvidence(miners, records, observe) {
12795
13695
  session = await observe.miner(miner);
12796
13696
  }
12797
13697
  const observedAt = observe.now();
12798
- deployments.push({
13698
+ const deployment = {
12799
13699
  miner_address: miner.minerAddress,
12800
13700
  endpoint: miner.endpoint,
12801
13701
  enclave_key: miner.enclaveKey,
@@ -12805,11 +13705,16 @@ async function renewGatewayEvidence(miners, records, observe) {
12805
13705
  observed_at: new Date(observedAt).toISOString(),
12806
13706
  expires_at: new Date(observedAt + ttl).toISOString(),
12807
13707
  observer: `${provider}; ${session}`
12808
- });
12809
- observed.push(miner);
13708
+ };
13709
+ return { miner, deployment };
12810
13710
  } catch (error) {
12811
- withdrawn.push({ subject, reason: `deployment not renewed: ${reason(error)}` });
13711
+ return { failure: { subject, reason: `deployment not renewed: ${reason2(error)}` } };
12812
13712
  }
13713
+ });
13714
+ for (const { miner, deployment, failure: failure2 } of observations) {
13715
+ if (failure2) withdrawn.push(failure2);
13716
+ if (miner) observed.push(miner);
13717
+ if (deployment) deployments.push(deployment);
12813
13718
  }
12814
13719
  for (const record of records) {
12815
13720
  for (const [feature, ttl] of conformanceWindows(record)) {
@@ -12848,7 +13753,7 @@ async function renewGatewayEvidence(miners, records, observe) {
12848
13753
  break;
12849
13754
  } catch (error) {
12850
13755
  if (error instanceof ProbeSignerUnderfunded) underfunded = error.message;
12851
- else failures.push(`${miner.minerAddress}: ${reason(error)}`);
13756
+ else failures.push(`${miner.minerAddress}: ${reason2(error)}`);
12852
13757
  }
12853
13758
  }
12854
13759
  if (underfunded) withdrawn.push({ subject, reason: `probe not run: ${underfunded}` });
@@ -12868,8 +13773,8 @@ async function renewGatewayEvidence(miners, records, observe) {
12868
13773
  }
12869
13774
  function planckToFlop(planck) {
12870
13775
  const scale = 10n ** BigInt(FLOP_DECIMALS);
12871
- const fraction = (planck % scale).toString().padStart(FLOP_DECIMALS, "0").replace(/0+$/, "");
12872
- return fraction ? `${planck / scale}.${fraction}` : `${planck / scale}`;
13776
+ const fraction2 = (planck % scale).toString().padStart(FLOP_DECIMALS, "0").replace(/0+$/, "");
13777
+ return fraction2 ? `${planck / scale}.${fraction2}` : `${planck / scale}`;
12873
13778
  }
12874
13779
  async function assertProbeSignerFunded(api, address, escrow) {
12875
13780
  const head = await api.rpc.chain.getFinalizedHead();
@@ -12909,10 +13814,10 @@ async function writeEvidenceFile(path5, evidence, appendQualification = false) {
12909
13814
  parseGatewayEvidence(evidence);
12910
13815
  await writeFile13(temporary, `${JSON.stringify(evidence, null, 2)}
12911
13816
  `, { mode: 420 });
12912
- await rename9(temporary, path5);
13817
+ await rename10(temporary, path5);
12913
13818
  } finally {
12914
13819
  await lock.close();
12915
- await rm5(`${path5}.lock`);
13820
+ await rm7(`${path5}.lock`);
12916
13821
  }
12917
13822
  }
12918
13823
  async function finishRenewal(report, out, output) {
@@ -12961,7 +13866,7 @@ function assertToolsConformance(calls, finishReason) {
12961
13866
  }
12962
13867
  return `signed ${PROBE_TOOL.function.name} tool call`;
12963
13868
  }
12964
- function hex3(bytes4) {
13869
+ function hex4(bytes4) {
12965
13870
  return `0x${Buffer.from(bytes4).toString("hex")}`;
12966
13871
  }
12967
13872
  function bytes3(value) {
@@ -12987,7 +13892,7 @@ async function waitForSettlementAllowance(api, channelId, aggregateGn, deadline)
12987
13892
  const snapshot = await query.computeChannel.channelCalibrationSnapshots.at(head, channelId);
12988
13893
  const current = (await api.rpc.state.call(
12989
13894
  "TrustRootApi_accepted_calibration_cap",
12990
- hex3(Uint8Array.from([128, ...decodeAddress2(opened.miner.toString())])),
13895
+ hex4(Uint8Array.from([128, ...decodeAddress3(opened.miner.toString())])),
12991
13896
  head
12992
13897
  )).toU8a(true);
12993
13898
  if (!snapshot?.isSome || current.length !== 9 || current[0] !== 1) throw new Error("settlement calibration is unavailable");
@@ -13001,13 +13906,80 @@ async function waitForSettlementAllowance(api, channelId, aggregateGn, deadline)
13001
13906
  }
13002
13907
  throw new Error("settlement allowance did not arrive before the probe deadline");
13003
13908
  }
13909
+ async function reconcileProbeClose(api, channelId, aggregateGn, deadline, submit) {
13910
+ const bounded = async (operation) => {
13911
+ const remaining = deadline - Date.now();
13912
+ if (remaining <= 0) throw new Error("probe deadline elapsed");
13913
+ let timer;
13914
+ try {
13915
+ return await Promise.race([operation, new Promise((_, reject) => {
13916
+ timer = setTimeout(() => reject(new Error("probe deadline elapsed")), remaining);
13917
+ })]);
13918
+ } finally {
13919
+ if (timer) clearTimeout(timer);
13920
+ }
13921
+ };
13922
+ let next;
13923
+ try {
13924
+ const head = await bounded(api.rpc.chain.getFinalizedHead());
13925
+ next = BigInt((await bounded(api.rpc.chain.getHeader(head))).number.toString());
13926
+ } catch {
13927
+ return "settlement unknown (finalized start unavailable; do not resubmit)";
13928
+ }
13929
+ if (Date.now() >= deadline) return "settlement not submitted (probe deadline elapsed)";
13930
+ try {
13931
+ await bounded(submit());
13932
+ } catch {
13933
+ }
13934
+ try {
13935
+ while (Date.now() < deadline) {
13936
+ const head = await bounded(api.rpc.chain.getFinalizedHead());
13937
+ const height = BigInt((await bounded(api.rpc.chain.getHeader(head))).number.toString());
13938
+ while (next <= height) {
13939
+ const blockHash = await bounded(api.rpc.chain.getBlockHash(next.toString()));
13940
+ const at = await bounded(api.at(blockHash));
13941
+ const events = [...await bounded(at.query.system.events())];
13942
+ for (const row of events) {
13943
+ const event = row.event;
13944
+ if (event.section !== "computeChannel" || !["Settled", "TimedOutRefunded"].includes(event.method) || event.data[0].toHex() !== hex4(channelId) || !row.phase.isApplyExtrinsic) continue;
13945
+ const index = row.phase.asApplyExtrinsic.toNumber();
13946
+ if (!events.some((other) => other.phase.isApplyExtrinsic && other.phase.asApplyExtrinsic.toNumber() === index && other.event.section === "system" && other.event.method === "ExtrinsicSuccess")) continue;
13947
+ if (event.method === "TimedOutRefunded") return `refunded at finalized ${blockHash.toHex()}`;
13948
+ if (BigInt(event.data[3].toString()) !== aggregateGn || !event.data[4].isTrue) {
13949
+ return `settlement mismatch at finalized ${blockHash.toHex()}`;
13950
+ }
13951
+ const paid = BigInt(event.data[1].toString());
13952
+ if (paid <= 0n) return `settlement has zero miner payment at finalized ${blockHash.toHex()}`;
13953
+ const block = await bounded(api.rpc.chain.getBlock(blockHash));
13954
+ return `settled ${block.block.extrinsics[index].hash.toHex()} minerPaid=${paid} at finalized ${blockHash.toHex()}`;
13955
+ }
13956
+ next++;
13957
+ }
13958
+ await bounded(delay(Math.min(1e3, Math.max(1, deadline - Date.now()))));
13959
+ }
13960
+ } catch {
13961
+ }
13962
+ return "settlement unknown (no finalized outcome before probe deadline; do not resubmit)";
13963
+ }
13964
+ async function settleProbe(api, channelId, aggregateGn, deadline, allowance2, submit, forceClose) {
13965
+ try {
13966
+ await allowance2();
13967
+ } catch (error) {
13968
+ try {
13969
+ await forceClose();
13970
+ } catch {
13971
+ }
13972
+ return `cooperative settlement not submitted; force close attempted (${reason2(error)})`;
13973
+ }
13974
+ return reconcileProbeClose(api, channelId, aggregateGn, deadline, submit);
13975
+ }
13004
13976
  async function renewalMinerInfo(api, miner, fetchImpl = fetch) {
13005
13977
  const head = await api.rpc.chain.getFinalizedHead();
13006
13978
  const at = await api.at(head);
13007
13979
  const registered = await at.query.minerStaking.miners(miner.minerAddress);
13008
13980
  const active = registered?.isSome && registered.unwrap().isActive;
13009
13981
  if (!(active?.isTrue ?? active === true)) throw new Error("miner is not registered and active at finality");
13010
- const encoded = await api.rpc.state.call("TrustRootApi_accepted_calibration_cap", `0x80${hex3(decodeAddress2(miner.minerAddress)).slice(2)}`, head);
13982
+ const encoded = await api.rpc.state.call("TrustRootApi_accepted_calibration_cap", `0x80${hex4(decodeAddress3(miner.minerAddress)).slice(2)}`, head);
13011
13983
  const cap = encoded.toU8a(true);
13012
13984
  if (cap.length !== 9 || cap[0] !== 1 || new DataView(cap.buffer, cap.byteOffset + 1, 8).getBigUint64(0, true) <= 0n) throw new Error("miner has no finalized calibrated capacity");
13013
13985
  const identity = {
@@ -13021,13 +13993,13 @@ async function renewalMinerInfo(api, miner, fetchImpl = fetch) {
13021
13993
  await anonymous.body?.cancel();
13022
13994
  if (anonymous.status !== 401) throw new Error("miner /session/info does not enforce authentication");
13023
13995
  }
13024
- const info = miner.channelSigned ? await computeChannel6.fetchProvenSessionInfo(miner.endpoint, {
13996
+ const info = miner.channelSigned ? await computeChannel8.fetchProvenSessionInfo(miner.endpoint, {
13025
13997
  genesisHash: api.genesisHash.toU8a(),
13026
- account: decodeAddress2(miner.minerAddress),
13998
+ account: decodeAddress3(miner.minerAddress),
13027
13999
  minerAddress: miner.minerAddress,
13028
14000
  enclaveKey: bytes3(miner.enclaveKey),
13029
14001
  modelIdentity: identity
13030
- }, fetchImpl, AbortSignal.timeout(3e4)) : await computeChannel6.fetchMinerSessionInfo(
14002
+ }, fetchImpl, AbortSignal.timeout(3e4)) : await computeChannel8.fetchMinerSessionInfo(
13031
14003
  miner.endpoint,
13032
14004
  { Authorization: `Bearer ${miner.authToken}` },
13033
14005
  AbortSignal.timeout(3e4),
@@ -13037,13 +14009,13 @@ async function renewalMinerInfo(api, miner, fetchImpl = fetch) {
13037
14009
  const wire = {
13038
14010
  ...info,
13039
14011
  is_ready: info.isReady,
13040
- enclaveKey: hex3(info.enclaveKey),
13041
- modelHash: hex3(info.modelHash),
14012
+ enclaveKey: hex4(info.enclaveKey),
14013
+ modelHash: hex4(info.modelHash),
13042
14014
  modelIdentity: {
13043
14015
  ...info.modelIdentity,
13044
- modelHash: hex3(info.modelIdentity.modelHash),
13045
- decodePolicyHash: hex3(info.modelIdentity.decodePolicyHash),
13046
- measuredRoot: hex3(info.modelIdentity.measuredRoot)
14016
+ modelHash: hex4(info.modelIdentity.modelHash),
14017
+ decodePolicyHash: hex4(info.modelIdentity.decodePolicyHash),
14018
+ measuredRoot: hex4(info.modelIdentity.measuredRoot)
13047
14019
  }
13048
14020
  };
13049
14021
  const snapshot = await readChainSnapshot(api, wire, head.toHex());
@@ -13072,7 +14044,7 @@ async function probeToolsConformance(input) {
13072
14044
  let requestHeaders;
13073
14045
  const pinned = miner.modelIdentity;
13074
14046
  const advertised = info.modelIdentity;
13075
- if (info.minerAddress !== miner.minerAddress || hex3(info.enclaveKey) !== miner.enclaveKey.toLowerCase() || !advertised || advertised.requestedSlug !== pinned.requestedSlug || advertised.registryModel !== pinned.registryModel || advertised.registryVersion !== pinned.registryVersion || hex3(advertised.modelHash) !== pinned.modelHash || advertised.precision !== pinned.precision || hex3(advertised.decodePolicyHash) !== pinned.decodePolicyHash || hex3(advertised.measuredRoot) !== pinned.measuredRoot || advertised.backendModelName !== pinned.backendModelName) {
14047
+ if (info.minerAddress !== miner.minerAddress || hex4(info.enclaveKey) !== miner.enclaveKey.toLowerCase() || !advertised || advertised.requestedSlug !== pinned.requestedSlug || advertised.registryModel !== pinned.registryModel || advertised.registryVersion !== pinned.registryVersion || hex4(advertised.modelHash) !== pinned.modelHash || advertised.precision !== pinned.precision || hex4(advertised.decodePolicyHash) !== pinned.decodePolicyHash || hex4(advertised.measuredRoot) !== pinned.measuredRoot || advertised.backendModelName !== pinned.backendModelName) {
13076
14048
  throw new Error("miner no longer advertises its allowlisted identity");
13077
14049
  }
13078
14050
  let version;
@@ -13089,7 +14061,7 @@ async function probeToolsConformance(input) {
13089
14061
  const maxSettlementTurns = Number(api.consts.computeChannel.maxSettlementTurns.toString());
13090
14062
  const frames = [];
13091
14063
  let sequence = 0;
13092
- const transport = new computeChannel6.HttpSessionTransport(miner.endpoint, PROBE_TIMEOUT_MS, {
14064
+ const transport = new computeChannel8.HttpSessionTransport(miner.endpoint, PROBE_TIMEOUT_MS, {
13093
14065
  headers,
13094
14066
  fetchImpl,
13095
14067
  requestHeaders: (method, path5, body) => requestHeaders?.(method, path5, body) ?? {},
@@ -13098,10 +14070,10 @@ async function probeToolsConformance(input) {
13098
14070
  if (frame.type === "chunk") frames.push({ kind: frame.kind, data: bytes3(frame.data) });
13099
14071
  }
13100
14072
  });
13101
- const agentKey = computeChannel6.SessionKey.generate();
14073
+ const agentKey = computeChannel8.SessionKey.generate();
13102
14074
  const identity = { ...pinned, modelHash: bytes3(pinned.modelHash), decodePolicyHash: bytes3(pinned.decodePolicyHash), measuredRoot: bytes3(pinned.measuredRoot) };
13103
14075
  input.assertAdmission?.();
13104
- const session = await computeChannel6.ChannelSession.open({
14076
+ const session = await computeChannel8.ChannelSession.open({
13105
14077
  chain,
13106
14078
  transport,
13107
14079
  enclaveKey: info.enclaveKey,
@@ -13121,13 +14093,13 @@ async function probeToolsConformance(input) {
13121
14093
  maxDuration: BigInt(miner.maxDurationBlocks),
13122
14094
  reattestEvery: 1,
13123
14095
  escrow: input.escrow,
13124
- nonce: BigInt(hex3(randomAsU8a(8)))
14096
+ nonce: BigInt(hex4(randomAsU8a(8)))
13125
14097
  });
13126
14098
  transport.bind(session.channelId);
13127
14099
  let verdict;
13128
14100
  let receipt;
13129
14101
  try {
13130
- if (miner.channelSigned) requestHeaders = await computeChannel6.channelSessionAuth(
14102
+ if (miner.channelSigned) requestHeaders = await computeChannel8.channelSessionAuth(
13131
14103
  miner.endpoint,
13132
14104
  session.channelId,
13133
14105
  agentKey,
@@ -13135,7 +14107,7 @@ async function probeToolsConformance(input) {
13135
14107
  fetchImpl,
13136
14108
  AbortSignal.timeout(3e4)
13137
14109
  );
13138
- await computeChannel6.notifyMinerOpen(miner.endpoint, session.channelId, agentKey.publicKey, headers, AbortSignal.timeout(3e4), requestHeaders, fetchImpl);
14110
+ await computeChannel8.notifyMinerOpen(miner.endpoint, session.channelId, agentKey.publicKey, headers, AbortSignal.timeout(3e4), requestHeaders, fetchImpl);
13139
14111
  receipt = await session.chatTurn({
13140
14112
  version,
13141
14113
  model: record.openrouter_slug,
@@ -13144,7 +14116,7 @@ async function probeToolsConformance(input) {
13144
14116
  tool_choice: "auto",
13145
14117
  max_tokens: 192
13146
14118
  }, (candidate) => {
13147
- computeChannel6.reconstructSignedTurnOutput(frames, candidate);
14119
+ computeChannel8.reconstructSignedTurnOutput(frames, candidate);
13148
14120
  verdict = assertToolsConformance(signedToolCalls(frames), candidate.finishReason);
13149
14121
  });
13150
14122
  } catch (error) {
@@ -13154,29 +14126,27 @@ async function probeToolsConformance(input) {
13154
14126
  }
13155
14127
  throw error;
13156
14128
  }
13157
- let settlement = "force-settlement posted; finalize it with `flop channel finalize`";
13158
- try {
13159
- await waitForSettlementAllowance(api, session.channelId, receipt.gN, deadline);
13160
- const settled = await session.closeCooperative(async (claim) => {
13161
- const wire = await computeChannel6.notifyMinerClose(miner.endpoint, claim, headers, AbortSignal.timeout(3e4), requestHeaders, fetchImpl);
13162
- return wire ? {
14129
+ const settlement = await settleProbe(
14130
+ api,
14131
+ session.channelId,
14132
+ receipt.gN,
14133
+ deadline,
14134
+ () => waitForSettlementAllowance(api, session.channelId, receipt.gN, deadline),
14135
+ () => session.closeCooperative(async (claim) => {
14136
+ const wire = await computeChannel8.notifyMinerClose(miner.endpoint, claim, headers, AbortSignal.timeout(3e4), requestHeaders, fetchImpl);
14137
+ if (!wire) throw new Error("miner did not report a settlement");
14138
+ return {
13163
14139
  channelId: bytes3(wire.channelId),
13164
14140
  minerPaid: BigInt(wire.minerPaid),
13165
14141
  burned: BigInt(wire.burned),
13166
14142
  aggregateGn: BigInt(wire.aggregateGn),
13167
14143
  cooperative: wire.cooperative,
13168
14144
  txHash: wire.txHash
13169
- } : null;
13170
- });
13171
- if ("minerPaid" in settled && settled.cooperative) settlement = `settled ${settled.txHash}`;
13172
- } catch (error) {
13173
- try {
13174
- await session.close();
13175
- } catch {
13176
- }
13177
- settlement = `settlement failed (${reason(error)})`;
13178
- }
13179
- return `${verdict} in channel ${hex3(session.channelId)}; ${settlement}`;
14145
+ };
14146
+ }),
14147
+ () => session.close()
14148
+ );
14149
+ return `${verdict} in channel ${hex4(session.channelId)}; ${settlement}`;
13180
14150
  }
13181
14151
  function envJson(name) {
13182
14152
  const value = process.env[name];
@@ -13193,7 +14163,7 @@ function envSecret(name) {
13193
14163
  return value;
13194
14164
  }
13195
14165
  function gatewayRenewCmd() {
13196
- return new Command48("renew").description("Re-observe allowlisted miners, re-run feature conformance, and write the gateway evidence file").option("--allowlist-env <name>", "Direct probe mode: environment variable holding GATEWAY_SESSION_ALLOWLIST (never printed)").requiredOption("--catalog-env <name>", "Environment variable holding GATEWAY_MODEL_CATALOG").option("--catalog-pricing <mode>", "The gateway's GATEWAY_CATALOG_PRICING: operator, or chain when the catalog omits prices", "operator").option("--provider-key-env <name>", "Direct probe mode: environment variable holding the GPU.ai API key").option("--tailscale-socket <path>", "tailscaled LocalAPI socket for tailnet-hosted miners", "/var/run/tailscale/tailscaled.sock").option("--probe-signer-env <name>", "Direct probe mode: environment variable holding the dedicated funded probe signer URI").option("--probe-escrow <flop>", "Direct probe mode: FLOP escrowed for each conformance probe channel").option("--qualification-receipt <path>", "Receipt-only mode: settled owner tools-qualification evidence; never runs another probe").option("--qualification-receipt-sha256 <hash>", "SHA-256 of the immutable owner qualification receipt").requiredOption("--out <path>", "Evidence file the gateway reads as GATEWAY_EVIDENCE_FILE").option("--json", "Output as JSON").action(async (opts, command) => {
14166
+ return new Command50("renew").description("Re-observe allowlisted miners, re-run feature conformance, and write the gateway evidence file").option("--allowlist-env <name>", "Direct probe mode: environment variable holding GATEWAY_SESSION_ALLOWLIST (never printed)").requiredOption("--catalog-env <name>", "Environment variable holding GATEWAY_MODEL_CATALOG").option("--catalog-pricing <mode>", "The gateway's GATEWAY_CATALOG_PRICING: operator, or chain when the catalog omits prices", "operator").option("--provider-key-env <name>", "Direct probe mode: environment variable holding the GPU.ai API key").option("--tailscale-socket <path>", "tailscaled LocalAPI socket for tailnet-hosted miners", "/var/run/tailscale/tailscaled.sock").option("--probe-signer-env <name>", "Direct probe mode: environment variable holding the dedicated funded probe signer URI").option("--probe-escrow <flop>", "Direct probe mode: FLOP escrowed for each conformance probe channel").option("--qualification-receipt <path>", "Receipt-only mode: settled owner tools-qualification evidence; never runs another probe").option("--qualification-receipt-sha256 <hash>", "SHA-256 of the immutable owner qualification receipt").requiredOption("--out <path>", "Evidence file the gateway reads as GATEWAY_EVIDENCE_FILE").option("--json", "Output as JSON").action(async (opts, command) => {
13197
14167
  let client;
13198
14168
  try {
13199
14169
  if (opts.catalogPricing !== "operator" && opts.catalogPricing !== "chain") {
@@ -13204,7 +14174,7 @@ function gatewayRenewCmd() {
13204
14174
  throw new Error("receipt-only mode requires the receipt/hash and cannot run direct probes");
13205
14175
  }
13206
14176
  const raw = await readFile11(opts.qualificationReceipt);
13207
- if (raw.length > 8e6 || createHash6("sha256").update(raw).digest("hex") !== opts.qualificationReceiptSha256) throw new Error("qualification receipt bytes differ");
14177
+ if (raw.length > 8e6 || createHash9("sha256").update(raw).digest("hex") !== opts.qualificationReceiptSha256) throw new Error("qualification receipt bytes differ");
13208
14178
  const value = JSON.parse(raw.toString("utf8"));
13209
14179
  const records2 = parseCanonicalCatalog(envJson(opts.catalogEnv), { chainPriced: opts.catalogPricing === "chain" });
13210
14180
  const record = records2.find((item) => item.openrouter_slug === value.model);
@@ -13214,7 +14184,7 @@ function gatewayRenewCmd() {
13214
14184
  const evidence = parseGatewayEvidence(JSON.parse(await readFile11(opts.out, "utf8")));
13215
14185
  const renewed = renewConformance(record, evidence, tested);
13216
14186
  client = await createClient(getRpc(command));
13217
- await computeChannel6.ready();
14187
+ await computeChannel8.ready();
13218
14188
  const pass = await verifyOwnedToolsQualification(value, renewed, (id, channel) => readQualificationTerminal(client.api, id, channel));
13219
14189
  await writeEvidenceFile(opts.out, { schema_version: GATEWAY_EVIDENCE_VERSION, generated_at: (/* @__PURE__ */ new Date()).toISOString(), deployments: [], conformance: [pass] }, true);
13220
14190
  print({ out: opts.out, conformance: [pass], receipt_only: true, paid_probe_count: 0 }, getJson(command));
@@ -13237,9 +14207,9 @@ function gatewayRenewCmd() {
13237
14207
  const escrow = flopToPlanck(opts.probeEscrow);
13238
14208
  client = await createClient(getRpc(command));
13239
14209
  const api = client.api;
13240
- await computeChannel6.ready();
14210
+ await computeChannel8.ready();
13241
14211
  const signer = new Keyring5({ type: "sr25519" }).addFromUri(signerUri);
13242
- const chain = new computeChannel6.ComputeChannelChainClient(new computeChannel6.FlopChainSubmitter(api, signer, true));
14212
+ const chain = new computeChannel8.ComputeChannelChainClient(new computeChannel8.FlopChainSubmitter(api, signer, true));
13243
14213
  const assertAdmission = (miner) => {
13244
14214
  if (!miner.admissionExpiresAt) return;
13245
14215
  const dynamic = admissions?.read() ?? [];
@@ -13255,13 +14225,13 @@ function gatewayRenewCmd() {
13255
14225
  const info = {
13256
14226
  ...proven,
13257
14227
  is_ready: proven.isReady,
13258
- enclaveKey: hex3(proven.enclaveKey),
13259
- modelHash: hex3(proven.modelHash),
14228
+ enclaveKey: hex4(proven.enclaveKey),
14229
+ modelHash: hex4(proven.modelHash),
13260
14230
  modelIdentity: {
13261
14231
  ...proven.modelIdentity,
13262
- modelHash: hex3(proven.modelIdentity.modelHash),
13263
- decodePolicyHash: hex3(proven.modelIdentity.decodePolicyHash),
13264
- measuredRoot: hex3(proven.modelIdentity.measuredRoot)
14232
+ modelHash: hex4(proven.modelIdentity.modelHash),
14233
+ decodePolicyHash: hex4(proven.modelIdentity.decodePolicyHash),
14234
+ measuredRoot: hex4(proven.modelIdentity.measuredRoot)
13265
14235
  }
13266
14236
  };
13267
14237
  const pinned = miner.modelIdentity;
@@ -13287,9 +14257,9 @@ function gatewayRenewCmd() {
13287
14257
  }
13288
14258
 
13289
14259
  // src/release-check.ts
13290
- import { mkdir as mkdir14, readFile as readFile12, rename as rename10, writeFile as writeFile14 } from "node:fs/promises";
14260
+ import { mkdir as mkdir15, readFile as readFile12, rename as rename11, writeFile as writeFile14 } from "node:fs/promises";
13291
14261
  import { homedir as homedir5 } from "node:os";
13292
- import { dirname as dirname7, join as join10 } from "node:path";
14262
+ import { dirname as dirname9, join as join11 } from "node:path";
13293
14263
  var DAY_MS = 24 * 60 * 60 * 1e3;
13294
14264
  var READ_ONLY = /* @__PURE__ */ new Set([
13295
14265
  "health",
@@ -13333,7 +14303,7 @@ async function checkRelease(options) {
13333
14303
  if (options.offline || (options.testing ?? (process.env.NODE_ENV === "test" || !!process.env.VITEST))) return;
13334
14304
  const warn = options.warn ?? ((message) => console.error(message));
13335
14305
  const now = options.now ?? Date.now();
13336
- const cacheFile = options.cacheFile ?? join10(process.env.XDG_CACHE_HOME || join10(homedir5(), ".cache"), "flop", "releases.json");
14306
+ const cacheFile = options.cacheFile ?? join11(process.env.XDG_CACHE_HOME || join11(homedir5(), ".cache"), "flop", "releases.json");
13337
14307
  let releases;
13338
14308
  try {
13339
14309
  const cached = JSON.parse(await readFile12(cacheFile, "utf8"));
@@ -13348,10 +14318,10 @@ async function checkRelease(options) {
13348
14318
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
13349
14319
  releases = parseOperatorReleases(await response.json());
13350
14320
  try {
13351
- await mkdir14(dirname7(cacheFile), { recursive: true });
14321
+ await mkdir15(dirname9(cacheFile), { recursive: true });
13352
14322
  const temporary = `${cacheFile}.${process.pid}.tmp`;
13353
14323
  await writeFile14(temporary, JSON.stringify({ fetchedAt: now, releases }), { mode: 384 });
13354
- await rename10(temporary, cacheFile);
14324
+ await rename11(temporary, cacheFile);
13355
14325
  } catch {
13356
14326
  warn("warning: could not cache operator releases");
13357
14327
  }
@@ -13375,10 +14345,10 @@ async function checkRelease(options) {
13375
14345
  }
13376
14346
 
13377
14347
  // src/cli.ts
13378
- var program = new Command49();
14348
+ var program = new Command51();
13379
14349
  var outputMode = outputModeFromArgv(process.argv.slice(2));
13380
14350
  setOutputMode(outputMode);
13381
- program.name("flop").description("FLOP Network CLI").version(operatorVersion(JSON.parse(readFileSync2(join11(packageRoot, "package.json"), "utf8")).version)).option("--offline", "Skip the operator release check").addOption(new Option8("--network <network>", "Network profile").choices(["local", "devnet", "testnet", "mainnet"]).default("local")).option("--rpc <url>", "RPC endpoint override").option("--signer <uri>", "Signer URI (local/dev only; required outside local unless --keystore)").option("--keystore <path>", "Encrypted Polkadot JSON keystore").option("--password-env <name>", "Environment variable containing the keystore password").addOption(new Option8("--output <mode>", "Output mode").choices(["human", "json", "agent"]).default("human")).option("--json", "Output as JSON");
14351
+ program.name("flop").description("FLOP Network CLI").version(operatorVersion(JSON.parse(readFileSync2(join12(packageRoot, "package.json"), "utf8")).version)).option("--offline", "Skip the operator release check").addOption(new Option8("--network <network>", "Network profile").choices(["local", "devnet", "testnet", "mainnet"]).default("local")).option("--rpc <url>", "RPC endpoint override").option("--signer <uri>", "Signer URI (local/dev only; required outside local unless --keystore)").option("--keystore <path>", "Encrypted Polkadot JSON keystore").option("--password-env <name>", "Environment variable containing the keystore password").addOption(new Option8("--output <mode>", "Output mode").choices(["human", "json", "agent"]).default("human")).option("--json", "Output as JSON");
13382
14352
  program.addCommand(healthCmd());
13383
14353
  program.addCommand(balanceCmd());
13384
14354
  program.addCommand(gpuCmd());
@@ -13390,13 +14360,13 @@ program.addCommand(chatCmd());
13390
14360
  program.addCommand(agentCmd());
13391
14361
  program.addCommand(minerCmd());
13392
14362
  program.addCommand(validatorCmd());
13393
- var networkCmd = new Command49("network").description("Resolve and inspect network profiles");
14363
+ var networkCmd = new Command51("network").description("Resolve and inspect network profiles");
13394
14364
  networkCmd.addCommand(networkStatusCmd());
13395
14365
  program.addCommand(networkCmd);
13396
- var faucetCmd = new Command49("faucet").description("Request dev/test FLOP");
14366
+ var faucetCmd = new Command51("faucet").description("Request dev/test FLOP");
13397
14367
  faucetCmd.addCommand(faucetRequestCmd());
13398
14368
  program.addCommand(faucetCmd);
13399
- var sessionCmd = new Command49("session").description("Manage session keys");
14369
+ var sessionCmd = new Command51("session").description("Manage session keys");
13400
14370
  sessionCmd.addCommand(sessionRunCmd());
13401
14371
  sessionCmd.addCommand(sessionListCmd());
13402
14372
  sessionCmd.addCommand(sessionCreateCmd());
@@ -13404,7 +14374,7 @@ sessionCmd.addCommand(sessionInfoCmd());
13404
14374
  sessionCmd.addCommand(sessionRevokeCmd());
13405
14375
  sessionCmd.addCommand(sessionLimitsCmd());
13406
14376
  program.addCommand(sessionCmd);
13407
- var keysCmd = new Command49("keys").description("Manage keys");
14377
+ var keysCmd = new Command51("keys").description("Manage keys");
13408
14378
  keysCmd.addCommand(keysGenerateCmd());
13409
14379
  keysCmd.addCommand(keysInspectCmd());
13410
14380
  keysCmd.addCommand(keysHardwareCmd());
@@ -13412,35 +14382,36 @@ keysCmd.addCommand(keysImportCmd());
13412
14382
  keysCmd.addCommand(keysExportCmd());
13413
14383
  keysCmd.addCommand(keysClaudeProfileCmd());
13414
14384
  program.addCommand(keysCmd);
13415
- var walletCmd = new Command49("wallet").description("Manage wallets");
13416
- var agentWalletCmd = new Command49("agent").description("Agent wallet operations");
14385
+ var walletCmd = new Command51("wallet").description("Manage wallets");
14386
+ var agentWalletCmd = new Command51("agent").description("Agent wallet operations");
13417
14387
  agentWalletCmd.addCommand(walletAgentCreateCmd());
13418
14388
  walletCmd.addCommand(agentWalletCmd);
13419
14389
  program.addCommand(walletCmd);
13420
- var txCmd = new Command49("tx").description("Build, sign, and broadcast transactions");
14390
+ var txCmd = new Command51("tx").description("Build, sign, and broadcast transactions");
13421
14391
  txCmd.addCommand(txBuildCmd());
13422
14392
  txCmd.addCommand(txBroadcastCmd());
13423
14393
  program.addCommand(txCmd);
13424
14394
  program.addCommand(multisigCmd());
13425
14395
  program.addCommand(channelCmd());
13426
- var gatewayCmd = new Command49("gateway").description("Generate verified gateway configuration");
14396
+ var gatewayCmd = new Command51("gateway").description("Generate verified gateway configuration");
13427
14397
  gatewayCmd.addCommand(gatewayAllowlistCmd());
13428
14398
  gatewayCmd.addCommand(gatewayRenewCmd());
13429
14399
  program.addCommand(gatewayCmd);
13430
- var x402Cmd = new Command49("x402").description("x402 payment gateway operations");
14400
+ var x402Cmd = new Command51("x402").description("x402 payment gateway operations");
13431
14401
  x402Cmd.addCommand(x402SettleCmd());
13432
14402
  program.addCommand(x402Cmd);
13433
- var daCmd = new Command49("da").description("Sovereign DA registry (leases, deposits)");
14403
+ var daCmd = new Command51("da").description("Sovereign DA registry (leases, deposits)");
13434
14404
  daCmd.addCommand(daDepositCmd());
13435
14405
  daCmd.addCommand(daRegisterCmd());
13436
14406
  daCmd.addCommand(daTopupCmd());
13437
14407
  daCmd.addCommand(daWithdrawCmd());
13438
14408
  program.addCommand(daCmd);
13439
- var modelCmd = new Command49("model").description("Model registry operations");
14409
+ var modelCmd = new Command51("model").description("Model registry operations");
13440
14410
  modelCmd.addCommand(modelListCmd());
13441
14411
  modelCmd.addCommand(modelGetCmd());
13442
14412
  modelCmd.addCommand(modelFetchCmd());
13443
14413
  modelCmd.addCommand(modelUploadCmd());
14414
+ modelCmd.addCommand(modelMirrorsCmd());
13444
14415
  modelCmd.addCommand(modelPreflightCmd());
13445
14416
  modelCmd.addCommand(modelOnboardCmd());
13446
14417
  modelCmd.addCommand(modelBackupCmd());
@@ -13450,7 +14421,7 @@ program.hook("preAction", async (_root, action) => {
13450
14421
  const path5 = [];
13451
14422
  for (let command = action; command && command !== program; command = command.parent) path5.unshift(command.name());
13452
14423
  await checkRelease({
13453
- version: operatorVersion(JSON.parse(readFileSync2(join11(packageRoot, "package.json"), "utf8")).version),
14424
+ version: operatorVersion(JSON.parse(readFileSync2(join12(packageRoot, "package.json"), "utf8")).version),
13454
14425
  readOnly: isReadOnlyCommand(path5),
13455
14426
  offline: program.opts().offline,
13456
14427
  image: !!process.env.FLOP_IMAGE_VERSION