@flop-labs/cli 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -724,14 +724,14 @@ var init_task_runner = __esm({
724
724
  }
725
725
  }
726
726
  withTimeout(promise, timeoutMs) {
727
- return new Promise((resolve9, reject) => {
727
+ return new Promise((resolve10, 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
- resolve9(result);
734
+ resolve10(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((resolve9) => setTimeout(resolve9, ms));
742
+ return new Promise((resolve10) => setTimeout(resolve10, ms));
743
743
  }
744
744
  };
745
745
  }
@@ -904,7 +904,7 @@ var init_worker = __esm({
904
904
  return this.identity;
905
905
  }
906
906
  sleep(ms) {
907
- return new Promise((resolve9) => setTimeout(resolve9, ms));
907
+ return new Promise((resolve10) => setTimeout(resolve10, 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((resolve9, reject) => {
915
+ return new Promise((resolve10, 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) resolve9(status.asFinalized.toString());
918
+ else if (status.isInBlock || status.isFinalized) resolve10(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((resolve9) => setTimeout(resolve9, pollIntervalMs));
1222
+ await new Promise((resolve10) => setTimeout(resolve10, pollIntervalMs));
1223
1223
  }
1224
1224
  throw new AgentError(
1225
1225
  "T002" /* TASK_TIMEOUT */,
@@ -1878,7 +1878,7 @@ async function waitForBalance(api, address, minBalance, timeoutMs = 6e4) {
1878
1878
  }
1879
1879
  } catch {
1880
1880
  }
1881
- await new Promise((resolve9) => setTimeout(resolve9, pollInterval));
1881
+ await new Promise((resolve10) => setTimeout(resolve10, pollInterval));
1882
1882
  }
1883
1883
  return false;
1884
1884
  }
@@ -4941,9 +4941,9 @@ var init_src = __esm({
4941
4941
  });
4942
4942
 
4943
4943
  // src/cli.ts
4944
- import { Command as Command48, Option as Option7 } from "commander";
4944
+ import { Command as Command49, Option as Option8 } from "commander";
4945
4945
  import { readFileSync as readFileSync2 } from "node:fs";
4946
- import { join as join10 } from "node:path";
4946
+ import { join as join11 } from "node:path";
4947
4947
 
4948
4948
  // src/output.ts
4949
4949
  var activeOutputMode = "human";
@@ -5019,9 +5019,12 @@ function table(headers, rows) {
5019
5019
  }
5020
5020
  function inferError(error) {
5021
5021
  const message = error instanceof Error ? error.message : String(error);
5022
- if (/ECONNREFUSED|WebSocket|disconnected|connect.*(failed|error)|network error/i.test(message)) {
5022
+ if (/ECONNREFUSED|WebSocket|disconnected|RPC unreachable|connect.*(failed|error)|network error/i.test(message)) {
5023
5023
  return { code: "RPC_UNREACHABLE", message, retryable: true };
5024
5024
  }
5025
+ if (error && typeof error === "object" && "code" in error && typeof error.code === "string") {
5026
+ return { code: error.code, message, retryable: false };
5027
+ }
5025
5028
  if (/insufficient custody|shards confirmed/i.test(message)) {
5026
5029
  return { code: "DA_CUSTODY_FAILED", message, retryable: true };
5027
5030
  }
@@ -5119,13 +5122,14 @@ async function createClient(rpcUrl, signer) {
5119
5122
  return FlopClient7.create(opts);
5120
5123
  }
5121
5124
  async function createReadOnlyClient(rpcUrl) {
5122
- const [{ ApiPromise: ApiPromise2, HttpProvider: HttpProvider2, WsProvider: WsProvider2 }, { FlopClient: FlopClient7 }] = await Promise.all([
5123
- import("@polkadot/api"),
5124
- import("@flop-labs/sdk")
5125
- ]);
5126
- const provider = rpcUrl.startsWith("ws") ? new WsProvider2(rpcUrl) : new HttpProvider2(rpcUrl);
5127
- const api = await ApiPromise2.create({ provider, noInitWarn: true });
5128
- return FlopClient7.fromApiReadOnly(api);
5125
+ const { connectFlopApi, FlopClient: FlopClient7 } = await import("@flop-labs/sdk");
5126
+ const api = await connectFlopApi(rpcUrl);
5127
+ try {
5128
+ return FlopClient7.fromApiReadOnly(api);
5129
+ } catch (error) {
5130
+ await api.disconnect();
5131
+ throw error;
5132
+ }
5129
5133
  }
5130
5134
 
5131
5135
  // src/opts.ts
@@ -5208,21 +5212,26 @@ function parsePositiveInt(value, name) {
5208
5212
  // src/commands/balance.ts
5209
5213
  function balanceCmd() {
5210
5214
  return new Command("balance").description("Check account balance").argument("<address>", "Account address (SS58)").option("--json", "Output as JSON").action(async (address, opts, command) => {
5215
+ let client;
5211
5216
  try {
5212
5217
  const rpc = getRpc(command);
5213
5218
  const json = getJson(command);
5214
- const client = await createClient(rpc);
5219
+ client = await createClient(rpc);
5215
5220
  const info = await client.api.query.system.account(address);
5221
+ const free = BigInt(info.data.free.toString());
5222
+ const reserved = BigInt(info.data.reserved.toString());
5216
5223
  const result = {
5217
5224
  address,
5218
5225
  free: info.data.free.toString(),
5219
5226
  reserved: info.data.reserved.toString(),
5220
- total: info.data.free.toString()
5227
+ total: (free + reserved).toString(),
5228
+ frozen: info.data.frozen.toString()
5221
5229
  };
5222
5230
  print(result, json);
5223
- await client.disconnect();
5224
5231
  } catch (e) {
5225
5232
  commandError(e);
5233
+ } finally {
5234
+ await client?.disconnect();
5226
5235
  }
5227
5236
  });
5228
5237
  }
@@ -5588,7 +5597,15 @@ function sessionRunCmd() {
5588
5597
  completion_tokens: result.receipts[0].completionTokens?.toString()
5589
5598
  }, getJson(command));
5590
5599
  } catch (error) {
5591
- commandError(error, { next: walletPath ? `wallet saved at ${walletPath}; inspect the channel before retrying` : "check miner endpoint, registry identity, signer balance and channel state" });
5600
+ const channelId = error && typeof error === "object" && "channelId" in error && error.channelId instanceof Uint8Array ? `0x${Buffer.from(error.channelId).toString("hex")}` : null;
5601
+ printError({
5602
+ code: "SESSION_FAILED",
5603
+ message: error instanceof Error ? error.message : String(error),
5604
+ channel: channelId,
5605
+ recovery: channelId ? "inspect finalized channel and terminal events before retrying" : "no channel reported",
5606
+ ...walletPath ? { wallet: walletPath } : {}
5607
+ }, "json");
5608
+ process.exitCode = 1;
5592
5609
  } finally {
5593
5610
  await client?.disconnect();
5594
5611
  }
@@ -5599,10 +5616,11 @@ function sessionRunCmd() {
5599
5616
  import { Command as Command11 } from "commander";
5600
5617
  function walletAgentCreateCmd() {
5601
5618
  return new Command11("create").description("Create an agent wallet").argument("<agent>", "Agent address (SS58)").option("--daily-cap <cap>", "Daily cap in FLOP", "50").option("--tx-limit <limit>", "Per-tx limit in FLOP", "5").option("--json", "Output as JSON").action(async (agent, opts, command) => {
5619
+ let client;
5602
5620
  try {
5603
5621
  const rpc = getRpc(command);
5604
5622
  const { createAgentWallet: createAgentWallet2 } = await import("@flop-labs/sdk");
5605
- const client = await createClient(rpc, getSigner(command));
5623
+ client = await createClient(rpc, getSigner(command));
5606
5624
  const flopToRaw3 = (flop) => BigInt(Math.floor(parseFloat(flop) * 1e18));
5607
5625
  const txHash = await createAgentWallet2(client.api, client.signer, {
5608
5626
  agent,
@@ -5618,9 +5636,10 @@ function walletAgentCreateCmd() {
5618
5636
  },
5619
5637
  getJson(command)
5620
5638
  );
5621
- await client.disconnect();
5622
5639
  } catch (e) {
5623
- commandError(e, { next: "check signer balance and agent address; retry" });
5640
+ commandError(e, { next: "inspect finalized agentWallet.wallets for this agent before retrying creation" });
5641
+ } finally {
5642
+ await client?.disconnect();
5624
5643
  }
5625
5644
  });
5626
5645
  }
@@ -6000,6 +6019,7 @@ import { constants, accessSync, existsSync } from "node:fs";
6000
6019
  import http from "node:http";
6001
6020
  import https from "node:https";
6002
6021
  import path from "node:path";
6022
+ import { watchTransaction } from "@flop-labs/sdk";
6003
6023
  function printRolePlan(plan, output) {
6004
6024
  const mode = resolveOutputMode(output);
6005
6025
  if (mode === "agent") {
@@ -6010,7 +6030,8 @@ function printRolePlan(plan, output) {
6010
6030
  steps: plan.steps.map(({ phase, command, mutates }) => ({ phase, command, mutates })),
6011
6031
  ...extended.config_path ? { config_path: extended.config_path } : {},
6012
6032
  ...extended.mcp_config ? { mcp_config: extended.mcp_config } : {},
6013
- ...plan.gpu_inventory ? { gpu_inventory: plan.gpu_inventory } : {}
6033
+ ...plan.gpu_inventory ? { gpu_inventory: plan.gpu_inventory } : {},
6034
+ ...plan.role === "validator" ? { live_minimum_stake: plan.live_minimum_stake ?? null } : {}
6014
6035
  }, mode);
6015
6036
  return;
6016
6037
  }
@@ -6020,6 +6041,7 @@ function printRolePlan(plan, output) {
6020
6041
  }
6021
6042
  console.log(`${plan.role} onboarding (${plan.mode})`);
6022
6043
  console.log(plan.summary);
6044
+ if (plan.role === "validator") console.log(`Live minimum stake: ${plan.live_minimum_stake === void 0 ? "not queried (--offline)" : `${plan.live_minimum_stake} FLOP`}`);
6023
6045
  for (const [index, step] of plan.steps.entries()) {
6024
6046
  console.log(` ${index + 1}. ${step.phase}: ${step.command}`);
6025
6047
  console.log(` ${step.purpose}${step.mutates ? " [changes state]" : ""}`);
@@ -6105,12 +6127,12 @@ function pathCheck(id, target, required = true) {
6105
6127
  };
6106
6128
  }
6107
6129
  async function probeHttpUrl(id, url, required = true, timeoutMs = 3e3) {
6108
- return new Promise((resolve9) => {
6130
+ return new Promise((resolve10) => {
6109
6131
  let parsed;
6110
6132
  try {
6111
6133
  parsed = new URL(url);
6112
6134
  } catch {
6113
- resolve9({
6135
+ resolve10({
6114
6136
  id,
6115
6137
  status: "fail",
6116
6138
  required,
@@ -6120,10 +6142,10 @@ async function probeHttpUrl(id, url, required = true, timeoutMs = 3e3) {
6120
6142
  return;
6121
6143
  }
6122
6144
  const transport = parsed.protocol === "https:" ? https : http;
6123
- const request2 = transport.request(parsed, { method: "GET", timeout: timeoutMs }, (response) => {
6145
+ const request3 = transport.request(parsed, { method: "GET", timeout: timeoutMs }, (response) => {
6124
6146
  response.resume();
6125
6147
  const reachable = (response.statusCode ?? 500) < 500;
6126
- resolve9({
6148
+ resolve10({
6127
6149
  id,
6128
6150
  status: reachable ? "pass" : required ? "fail" : "warn",
6129
6151
  required,
@@ -6131,9 +6153,9 @@ async function probeHttpUrl(id, url, required = true, timeoutMs = 3e3) {
6131
6153
  ...!reachable ? { next: `check ${url}; retry doctor` } : {}
6132
6154
  });
6133
6155
  });
6134
- request2.on("timeout", () => request2.destroy(new Error("timeout")));
6135
- request2.on("error", (error) => {
6136
- resolve9({
6156
+ request3.on("timeout", () => request3.destroy(new Error("timeout")));
6157
+ request3.on("error", (error) => {
6158
+ resolve10({
6137
6159
  id,
6138
6160
  status: required ? "fail" : "warn",
6139
6161
  required,
@@ -6141,7 +6163,7 @@ async function probeHttpUrl(id, url, required = true, timeoutMs = 3e3) {
6141
6163
  next: `check ${url}; retry doctor`
6142
6164
  });
6143
6165
  });
6144
- request2.end();
6166
+ request3.end();
6145
6167
  });
6146
6168
  }
6147
6169
  async function probeRpc(api, rpc) {
@@ -6167,25 +6189,8 @@ async function probeRpc(api, rpc) {
6167
6189
  }
6168
6190
  }
6169
6191
  async function submitExtrinsic(api, signer, extrinsic) {
6170
- return new Promise((resolve9, reject) => {
6171
- let unsubscribe;
6172
- extrinsic.signAndSend(signer, (result) => {
6173
- if (!result.status?.isInBlock && !result.status?.isFinalized) return;
6174
- unsubscribe?.();
6175
- if (result.dispatchError) {
6176
- if (result.dispatchError.isModule) {
6177
- const decoded = api.registry.findMetaError(result.dispatchError.asModule);
6178
- reject(new Error(`${decoded.section}.${decoded.name}`));
6179
- } else {
6180
- reject(new Error(result.dispatchError.toString()));
6181
- }
6182
- return;
6183
- }
6184
- resolve9((result.txHash ?? extrinsic.hash).toHex());
6185
- }).then((fn) => {
6186
- unsubscribe = fn;
6187
- }).catch(reject);
6188
- });
6192
+ const result = await watchTransaction(extrinsic, signer, { api });
6193
+ return (result.txHash ?? extrinsic.hash).toHex();
6189
6194
  }
6190
6195
  function unwrapStorage(raw) {
6191
6196
  if (!raw || raw.isNone) return null;
@@ -6194,7 +6199,7 @@ function unwrapStorage(raw) {
6194
6199
 
6195
6200
  // src/commands/launch.ts
6196
6201
  function checkGateway(url) {
6197
- return new Promise((resolve9) => {
6202
+ return new Promise((resolve10) => {
6198
6203
  try {
6199
6204
  const parsedUrl = new URL(url);
6200
6205
  const req = http2.request(
@@ -6206,17 +6211,17 @@ function checkGateway(url) {
6206
6211
  timeout: 1e3
6207
6212
  },
6208
6213
  (res) => {
6209
- resolve9(res.statusCode === 200);
6214
+ resolve10(res.statusCode === 200);
6210
6215
  }
6211
6216
  );
6212
- req.on("error", () => resolve9(false));
6217
+ req.on("error", () => resolve10(false));
6213
6218
  req.on("timeout", () => {
6214
6219
  req.destroy();
6215
- resolve9(false);
6220
+ resolve10(false);
6216
6221
  });
6217
6222
  req.end();
6218
6223
  } catch {
6219
- resolve9(false);
6224
+ resolve10(false);
6220
6225
  }
6221
6226
  });
6222
6227
  }
@@ -6524,7 +6529,7 @@ async function withCredentialLock(path5, action) {
6524
6529
  continue;
6525
6530
  }
6526
6531
  if (Date.now() >= deadline) throw new Error(`FLOP credential lock is busy at ${lock}`);
6527
- await new Promise((resolve9) => setTimeout(resolve9, 100));
6532
+ await new Promise((resolve10) => setTimeout(resolve10, 100));
6528
6533
  }
6529
6534
  }
6530
6535
  try {
@@ -6584,6 +6589,8 @@ function validateWalletEnrollmentChallenge(message, address, gateway, genesis, n
6584
6589
  const issued = Date.parse(issuedText), expires = Date.parse(expiresText);
6585
6590
  if (!Number.isFinite(issued) || !Number.isFinite(expires) || lines[6] !== `Issued At: ${new Date(issued).toISOString()}` || lines[7] !== `Expiration Time: ${new Date(expires).toISOString()}` || issued > now || expires <= now || expires <= issued || expires - issued > 5 * 6e4) throw invalid();
6586
6591
  }
6592
+ var ENROLLMENT_CHALLENGE_TIMEOUT_MS = 3e4;
6593
+ var ENROLLMENT_SUBMIT_TIMEOUT_MS = 12e4;
6587
6594
  function walletEnrollment(options) {
6588
6595
  return async () => {
6589
6596
  if (options.purpose) await hermesEnrollmentOffer(options.gateway);
@@ -6600,12 +6607,12 @@ function walletEnrollment(options) {
6600
6607
  const deadline = Date.now() + (options.fundTimeoutMs ?? 12e4);
6601
6608
  while (await options.balance(pair.address) < minBalance) {
6602
6609
  if (Date.now() >= deadline) throw new Error("devnet faucet grant did not finalize in time; retry later");
6603
- await new Promise((resolve9) => setTimeout(resolve9, options.pollMs ?? 3e3));
6610
+ await new Promise((resolve10) => setTimeout(resolve10, options.pollMs ?? 3e3));
6604
6611
  }
6605
6612
  }
6606
6613
  const challenge = await fetch(
6607
6614
  `${options.gateway}/v1/enroll/wallet/challenge?address=${encodeURIComponent(pair.address)}${options.purpose ? `&purpose=${options.purpose}` : ""}`,
6608
- { redirect: "error", signal: AbortSignal.timeout(options.purpose ? 3e4 : 1e4) }
6615
+ { redirect: "error", signal: AbortSignal.timeout(ENROLLMENT_CHALLENGE_TIMEOUT_MS) }
6609
6616
  );
6610
6617
  if (!challenge.ok) throw await failure(challenge, "devnet enrollment challenge");
6611
6618
  const { message } = await challenge.json();
@@ -6616,7 +6623,7 @@ function walletEnrollment(options) {
6616
6623
  headers: { "content-type": "application/json" },
6617
6624
  body: JSON.stringify({ address: pair.address, message, signature }),
6618
6625
  redirect: "error",
6619
- signal: AbortSignal.timeout(options.purpose ? 3e4 : 1e4)
6626
+ signal: AbortSignal.timeout(ENROLLMENT_SUBMIT_TIMEOUT_MS)
6620
6627
  });
6621
6628
  };
6622
6629
  }
@@ -7007,14 +7014,14 @@ var CORS_HEADERS = {
7007
7014
  "Access-Control-Allow-Headers": "Authorization, Content-Type"
7008
7015
  };
7009
7016
  function readBody(req) {
7010
- return new Promise((resolve9) => {
7017
+ return new Promise((resolve10) => {
7011
7018
  let data = "";
7012
7019
  req.on("data", (c) => data += c);
7013
7020
  req.on("end", () => {
7014
7021
  try {
7015
- resolve9(data ? JSON.parse(data) : {});
7022
+ resolve10(data ? JSON.parse(data) : {});
7016
7023
  } catch {
7017
- resolve9({});
7024
+ resolve10({});
7018
7025
  }
7019
7026
  });
7020
7027
  });
@@ -7251,7 +7258,7 @@ async function startProxyServer(o) {
7251
7258
  embeddingModel: o.embeddingModel,
7252
7259
  embed: createLocalEmbedder(o.embeddingModel)
7253
7260
  });
7254
- await new Promise((resolve9) => server.listen(o.port, o.host, () => resolve9()));
7261
+ await new Promise((resolve10) => server.listen(o.port, o.host, () => resolve10()));
7255
7262
  const addr = server.address();
7256
7263
  const url = `http://${o.host}:${addr.port}`;
7257
7264
  const models = await listModels();
@@ -7388,9 +7395,9 @@ function launchExternal2(command, args, envOverrides) {
7388
7395
  function runHermesConfig(launcher, commands) {
7389
7396
  return commands.reduce(
7390
7397
  (p, args) => p.then(
7391
- () => new Promise((resolve9, reject) => {
7398
+ () => new Promise((resolve10, reject) => {
7392
7399
  const c = spawnLauncher(launcher, args, { stdio: "inherit" });
7393
- c.on("exit", (code) => code === 0 ? resolve9() : reject(new Error(`hermes ${args.join(" ")} exited ${code}`)));
7400
+ c.on("exit", (code) => code === 0 ? resolve10() : reject(new Error(`hermes ${args.join(" ")} exited ${code}`)));
7394
7401
  c.on("error", reject);
7395
7402
  })
7396
7403
  ),
@@ -7625,6 +7632,7 @@ import { basename as basename2, join as join6, resolve as resolve8 } from "node:
7625
7632
  import { resolve as resolve5, relative, sep } from "node:path";
7626
7633
  import { mkdir as mkdir7, readFile as readFile4, readdir, lstat as lstat3, writeFile as writeFile7 } from "node:fs/promises";
7627
7634
  import { execFile as execFile3, spawn as spawn4 } from "node:child_process";
7635
+ import { pipeline } from "node:stream/promises";
7628
7636
  import { promisify as promisify3 } from "node:util";
7629
7637
  function modelToOutput(model) {
7630
7638
  return {
@@ -7747,6 +7755,9 @@ async function daCommit(executable, blobFile) {
7747
7755
  const { stdout: stdout2 } = await run2(executable, ["commit", "--blob-file", blobFile, "--json"], {
7748
7756
  maxBuffer: 1024 * 1024
7749
7757
  });
7758
+ return parseDaLayout(stdout2);
7759
+ }
7760
+ function parseDaLayout(stdout2) {
7750
7761
  const line = lastJsonLine(stdout2);
7751
7762
  if (!line) throw new Error("da-validator commit returned no JSON result");
7752
7763
  const result = JSON.parse(line);
@@ -7760,6 +7771,62 @@ async function daCommit(executable, blobFile) {
7760
7771
  chunks: result.chunks
7761
7772
  };
7762
7773
  }
7774
+ async function runWithBlobOnStdin(executable, args, env, body) {
7775
+ const child = spawn4(executable, args, { stdio: ["pipe", "pipe", "inherit"], env });
7776
+ let stdout2 = "";
7777
+ child.stdout.setEncoding("utf8");
7778
+ child.stdout.on("data", (part) => {
7779
+ stdout2 += part;
7780
+ });
7781
+ let streamError;
7782
+ const fed = pipeline(body, child.stdin).catch((error) => {
7783
+ if (error.code === "EPIPE") return;
7784
+ streamError = error;
7785
+ child.kill("SIGKILL");
7786
+ });
7787
+ const code = await new Promise((resolveExit, rejectExit) => {
7788
+ child.on("error", rejectExit);
7789
+ child.on("close", resolveExit);
7790
+ });
7791
+ await fed;
7792
+ if (streamError) throw streamError;
7793
+ return { code, stdout: stdout2 };
7794
+ }
7795
+ async function daCommitStream(executable, blob) {
7796
+ const { code, stdout: stdout2 } = await runWithBlobOnStdin(
7797
+ executable,
7798
+ ["commit", "--blob-stdin", "--size", String(blob.sizeBytes), "--manifest-out", blob.manifestFile, "--json"],
7799
+ process.env,
7800
+ blob.open()
7801
+ );
7802
+ if (code !== 0) throw new Error(`da-validator commit failed (exit ${code}); see its log above`);
7803
+ return parseDaLayout(stdout2);
7804
+ }
7805
+ async function submitDaBlobStream(executable, rpc, blob, publisherKey) {
7806
+ const env = { ...process.env };
7807
+ if (publisherKey) env.DA_PUBLISHER_KEY = publisherKey;
7808
+ else delete env.DA_PUBLISHER_KEY;
7809
+ const { code, stdout: stdout2 } = await runWithBlobOnStdin(
7810
+ executable,
7811
+ [
7812
+ "submit",
7813
+ "--chain-rpc",
7814
+ rpc,
7815
+ "--blob-stdin",
7816
+ "--size",
7817
+ String(blob.sizeBytes),
7818
+ "--manifest-file",
7819
+ blob.manifestFile,
7820
+ "--json"
7821
+ ],
7822
+ env,
7823
+ blob.open()
7824
+ );
7825
+ if (code !== 0 && !lastJsonLine(stdout2)) {
7826
+ throw new Error(`da-validator submit failed (exit ${code}); see its log above`);
7827
+ }
7828
+ return parseCustodyResult(stdout2);
7829
+ }
7763
7830
  function parseCustodyResult(stdout2) {
7764
7831
  const line = lastJsonLine(stdout2);
7765
7832
  if (!line) throw new Error("da-validator returned no JSON custody result");
@@ -9284,7 +9351,9 @@ function modelOnboardCmd() {
9284
9351
  if (opts.approve && !state.approved) throw new Error("approval is not present in finalized registry state");
9285
9352
  if (metadataOnly && !state.weightsRef) {
9286
9353
  process.stderr.write(
9287
- `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)); upload it with --download from a box with the disk
9354
+ `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}` : ""}
9356
+ 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.
9288
9357
  `
9289
9358
  );
9290
9359
  }
@@ -9484,10 +9553,167 @@ function modelPreflightCmd() {
9484
9553
  });
9485
9554
  }
9486
9555
 
9556
+ // 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";
9560
+ var PRECISIONS3 = ["FP32", "FP16", "BF16", "INT8", "INT4", "Mixed"];
9561
+ async function finalizedLeasedRef2(api, commitment, size) {
9562
+ const hash2 = await api.rpc.chain.getFinalizedHead();
9563
+ const [at, header] = await Promise.all([api.at(hash2), api.rpc.chain.getHeader(hash2)]);
9564
+ return existingLeasedRef({ query: at.query, rpc: { chain: { getHeader: async () => header } } }, commitment, size);
9565
+ }
9566
+ var REGISTRATION_WAIT_POLLS = 30;
9567
+ var REGISTRATION_POLL_MS = 2e3;
9568
+ async function awaitFinalizedLeasedRef(api, commitment, size) {
9569
+ for (let poll = 0; ; poll++) {
9570
+ const ref = await finalizedLeasedRef2(api, commitment, size);
9571
+ if (ref || poll >= REGISTRATION_WAIT_POLLS) return ref;
9572
+ await new Promise((wake) => setTimeout(wake, REGISTRATION_POLL_MS));
9573
+ }
9574
+ }
9575
+ function registeredHubRepo(mirrors) {
9576
+ const repos = [...new Set(mirrors.filter((m) => m.kind === "huggingface").map((m) => m.repo))];
9577
+ if (repos.length > 1) {
9578
+ throw new Error(
9579
+ `model advertises ${repos.length} Hugging Face mirrors (${repos.join(", ")}); name the one to back up with --source`
9580
+ );
9581
+ }
9582
+ return repos[0];
9583
+ }
9584
+ function modelBackupCmd() {
9585
+ return new Command33("backup").description(
9586
+ "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) => {
9588
+ let client;
9589
+ let workDir;
9590
+ try {
9591
+ if (opts.revision && !/^[0-9a-f]{40}$/.test(opts.revision)) throw new Error("--revision must be an immutable 40-hex commit");
9592
+ const sdk = await import("@flop-labs/sdk");
9593
+ const rpc = getRpc(command);
9594
+ const precision = opts.precision;
9595
+ client = await createClient(rpc, getSigner(command));
9596
+ const api = client.api;
9597
+ const registered = await new sdk.OnChainModelRegistry(api).getModel(modelHash);
9598
+ if (!registered) throw new Error(`model not found: ${modelHash}`);
9599
+ const expected = await sdk.readMeasuredRoot(api, modelHash, precision);
9600
+ if (!expected) throw new Error(`no measured root for ${modelHash} at ${opts.precision}`);
9601
+ const hub = { revision: opts.revision, token: opts.token ?? process.env.HF_TOKEN };
9602
+ const repo = opts.source ? opts.source.startsWith("hf:") ? opts.source.slice(3) : opts.source : registeredHubRepo(await sdk.readModelMirrors(api, modelHash));
9603
+ if (!repo) {
9604
+ throw new Error(
9605
+ `model ${modelHash} advertises no Hugging Face mirror; pass --source <owner/model>, or back it up from disk with \`flop model upload --model-hash <hash> --path <dir>\``
9606
+ );
9607
+ }
9608
+ const preflight = await sdk.preflightHuggingFace(repo, hub);
9609
+ if (sdk.normalizeRoot(preflight.contentRoot) !== sdk.normalizeRoot(expected)) {
9610
+ throw new Error(
9611
+ `${repo} at ${preflight.revision} has content_root 0x${preflight.contentRoot}, not the registered root ${expected}. A verity-v1 model's root cannot be checked from hub metadata, and a repo that moved needs the registered --revision; back such a model up from disk with \`flop model upload --model-hash ${modelHash} --path <dir>\``
9612
+ );
9613
+ }
9614
+ const imageSize = sdk.packedImageSize(preflight.files);
9615
+ if (imageSize !== preflight.imageSize) throw new Error("packed image size disagrees with the preflight table");
9616
+ const tableBytes = sdk.encodeModelFileTable("0".repeat(64), preflight.files).length;
9617
+ const deposit = await sdk.da.leaseDeposit(api, imageSize + tableBytes);
9618
+ const plan = {
9619
+ model_hash: modelHash,
9620
+ precision: opts.precision,
9621
+ repo,
9622
+ revision: preflight.revision,
9623
+ content_root: `0x${preflight.contentRoot}`,
9624
+ scheme: "content-v1",
9625
+ files: preflight.files.length,
9626
+ image_size: imageSize,
9627
+ da_chunks: preflight.daChunks,
9628
+ da_deposit_plancks: deposit.toString(),
9629
+ publisher: client.signer.address
9630
+ };
9631
+ if (opts.dryRun) {
9632
+ print({ ...plan, dry_run: true, next: "rerun without --dry-run to transfer and register" }, getJson(command));
9633
+ return;
9634
+ }
9635
+ const publisherKey = await sr25519KeyHex(getSigner(command));
9636
+ const modelDir = resolve9(opts.modelDir);
9637
+ workDir = join8(resolve9(modelDir, ".."), `.flop-backup-${modelHash.replace(/^0x/, "").slice(0, 16)}`);
9638
+ await mkdir12(workDir, { recursive: true, mode: 448 });
9639
+ 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 });
9642
+ process.stderr.write(
9643
+ `backing up ${repo}@${preflight.revision} (${preflight.files.length} files, image ${imageSize} bytes, ${preflight.daChunks} DA chunk(s)) as ${modelHash}
9644
+ `
9645
+ );
9646
+ await sdk.fetchHuggingFaceToDir(repo, modelDir, {
9647
+ ...hub,
9648
+ revision: preflight.revision,
9649
+ files: preflight.files,
9650
+ resumeState
9651
+ });
9652
+ const manifestFile = join8(workDir, "layout.bin");
9653
+ const image = {
9654
+ sizeBytes: imageSize,
9655
+ manifestFile,
9656
+ open: () => sdk.packedImageStream(modelDir, preflight.files)
9657
+ };
9658
+ const imageLayout = await daCommitStream(opts.daValidator, image);
9659
+ if (imageLayout.sizeBytes !== imageSize) throw new Error("streamed DA commitment covers the wrong image size");
9660
+ const tablePath = join8(workDir, "files.json");
9661
+ await writeFile10(tablePath, sdk.encodeModelFileTable(imageLayout.commitment, preflight.files), { mode: 384 });
9662
+ const tableLayout = await daCommit(opts.daValidator, tablePath);
9663
+ const refs = [];
9664
+ for (const blob of [
9665
+ { commitment: imageLayout.commitment, sizeBytes: imageLayout.sizeBytes, stream: image },
9666
+ { commitment: tableLayout.commitment, sizeBytes: tableLayout.sizeBytes, stream: void 0 }
9667
+ ]) {
9668
+ let ref = await finalizedLeasedRef2(api, blob.commitment, blob.sizeBytes);
9669
+ let registerTx;
9670
+ if (!ref) {
9671
+ const reg = await sdk.da.registerBlob(api, client.signer, {
9672
+ commitment: blob.commitment,
9673
+ sizeBytes: blob.sizeBytes
9674
+ });
9675
+ registerTx = reg.txHash;
9676
+ ref = await awaitFinalizedLeasedRef(api, blob.commitment, blob.sizeBytes);
9677
+ }
9678
+ if (!ref) {
9679
+ throw new Error(
9680
+ `DA registration for ${blob.commitment} did not finalize within ${REGISTRATION_WAIT_POLLS * REGISTRATION_POLL_MS / 1e3}s; rerun the same command once it has`
9681
+ );
9682
+ }
9683
+ const mine = sameAccount(ref.publisher, client.signer.address);
9684
+ const custody = blob.stream ? await submitDaBlobStream(opts.daValidator, rpc, blob.stream, mine ? publisherKey : void 0) : await submitDaBlob(opts.daValidator, rpc, tablePath, mine ? publisherKey : void 0);
9685
+ process.stderr.write(`${custodySummary(custody)}
9686
+ `);
9687
+ refs.push({ ref, custody, registerTx });
9688
+ }
9689
+ const [weights, table2] = refs;
9690
+ print({
9691
+ ...plan,
9692
+ weights_ref: weights.ref.commitment,
9693
+ weights_register_tx: weights.registerTx,
9694
+ weights_custody_shards: weights.custody.confirmedShards,
9695
+ table_ref: table2.ref.commitment,
9696
+ table_register_tx: table2.registerTx,
9697
+ // The binding is the model publisher's own transaction: this box holds no
9698
+ // 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\``,
9700
+ recovery: `flop da withdraw --commitment ${weights.ref.commitment} (and ${table2.ref.commitment}) refunds both deposits while nothing pins them`
9701
+ }, getJson(command));
9702
+ } catch (error) {
9703
+ commandError(error, {
9704
+ 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
+ });
9706
+ } finally {
9707
+ if (workDir) await rm4(workDir, { recursive: true, force: true });
9708
+ await client?.disconnect();
9709
+ }
9710
+ });
9711
+ }
9712
+
9487
9713
  // src/commands/model-list.ts
9488
- import { Command as Command33 } from "commander";
9714
+ import { Command as Command34 } from "commander";
9489
9715
  function modelListCmd() {
9490
- return new Command33("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) => {
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) => {
9491
9717
  let client;
9492
9718
  try {
9493
9719
  const sdk = await import("@flop-labs/sdk");
@@ -9517,15 +9743,15 @@ function modelListCmd() {
9517
9743
  }
9518
9744
 
9519
9745
  // src/commands/model-get.ts
9520
- import { Command as Command34 } from "commander";
9746
+ import { Command as Command35 } from "commander";
9521
9747
  function modelGetCmd() {
9522
- return new Command34("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) => {
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) => {
9523
9749
  let client;
9524
9750
  try {
9525
9751
  const sdk = await import("@flop-labs/sdk");
9526
9752
  client = await createClient(getRpc(command));
9527
9753
  const registry = new sdk.OnChainModelRegistry(client.api);
9528
- const model = await registry.getModel(identifier) ?? await registry.getModelByName(identifier);
9754
+ const model = /^0x[0-9a-f]{64}$/i.test(identifier) ? await registry.getModel(identifier) : await registry.getModelByName(identifier);
9529
9755
  if (!model) throw new Error(`model not found: ${identifier}`);
9530
9756
  const precision = opts.precision;
9531
9757
  const [measuredRoot, sources] = await Promise.all([
@@ -9550,16 +9776,16 @@ function modelGetCmd() {
9550
9776
  }
9551
9777
 
9552
9778
  // src/commands/model-fetch.ts
9553
- import { Command as Command35, Option as Option3 } from "commander";
9554
- var PRECISIONS3 = ["FP32", "FP16", "BF16", "INT8", "INT4", "Mixed"];
9779
+ import { Command as Command36, Option as Option4 } from "commander";
9780
+ var PRECISIONS4 = ["FP32", "FP16", "BF16", "INT8", "INT4", "Mixed"];
9555
9781
  function modelFetchCmd() {
9556
- return new Command35("fetch").description("Fetch model weights and accept only a registered dm-verity root").argument("<identifier>", "Registered model hash or exact name").addOption(new Option3("--precision <precision>", "Weights precision").choices(PRECISIONS3).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) => {
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) => {
9557
9783
  let client;
9558
9784
  try {
9559
9785
  const sdk = await import("@flop-labs/sdk");
9560
9786
  client = await createClient(getRpc(command));
9561
9787
  const registry = new sdk.OnChainModelRegistry(client.api);
9562
- const model = await registry.getModel(identifier) ?? await registry.getModelByName(identifier);
9788
+ const model = /^0x[0-9a-f]{64}$/i.test(identifier) ? await registry.getModel(identifier) : await registry.getModelByName(identifier);
9563
9789
  if (!model) throw new Error(`model not found: ${identifier}`);
9564
9790
  const expected = await sdk.readMeasuredRoot(client.api, model.hash, opts.precision);
9565
9791
  if (!expected) {
@@ -9616,7 +9842,7 @@ function modelFetchCmd() {
9616
9842
 
9617
9843
  // src/commands/agent.ts
9618
9844
  import path2 from "node:path";
9619
- import { Command as Command36, Option as Option4 } from "commander";
9845
+ import { Command as Command37, Option as Option5 } from "commander";
9620
9846
  import { getFlopNetworkProfile as getFlopNetworkProfile3 } from "@flop-labs/sdk";
9621
9847
  var DEFAULT_MCP_URL = "https://mcp.flop.network/mcp";
9622
9848
  var DEVNET_MCP_URL = "https://mcp.flop.network/mcp";
@@ -9704,14 +9930,14 @@ function buildAgentInitPlan(client, mode, mcpUrl2 = DEFAULT_MCP_URL) {
9704
9930
  };
9705
9931
  }
9706
9932
  function agentInitCmd() {
9707
- return new Command36("init").description("Print a safe agent onboarding plan and MCP configuration").addOption(new Option4("--client <client>", "Agent client").choices([
9933
+ return new Command37("init").description("Print a safe agent onboarding plan and MCP configuration").addOption(new Option5("--client <client>", "Agent client").choices([
9708
9934
  "claude-code",
9709
9935
  "codex",
9710
9936
  "cursor",
9711
9937
  "windsurf",
9712
9938
  "zed",
9713
9939
  "generic"
9714
- ]).default("generic")).addOption(new Option4("--mode <mode>", "MCP mode").choices(["remote", "local"]).default("remote")).option("--mcp-url <url>", "Hosted MCP URL").option("--json", "Output as JSON").action((opts, command) => {
9940
+ ]).default("generic")).addOption(new Option5("--mode <mode>", "MCP mode").choices(["remote", "local"]).default("remote")).option("--mcp-url <url>", "Hosted MCP URL").option("--json", "Output as JSON").action((opts, command) => {
9715
9941
  const plan = buildAgentInitPlan(opts.client, opts.mode, mcpUrl(command, opts.mcpUrl));
9716
9942
  const output = getOutputMode(command);
9717
9943
  printRolePlan(plan, output);
@@ -9722,7 +9948,7 @@ function agentInitCmd() {
9722
9948
  });
9723
9949
  }
9724
9950
  function agentDoctorCmd() {
9725
- return new Command36("doctor").description("Check agent MCP, gateway, and optional chain connectivity").addOption(new Option4("--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) => {
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) => {
9726
9952
  try {
9727
9953
  const checks = [executableCheck("node")];
9728
9954
  if (opts.mode === "remote") {
@@ -9749,7 +9975,7 @@ function agentDoctorCmd() {
9749
9975
  });
9750
9976
  }
9751
9977
  function agentCredentialCmd() {
9752
- return new Command36("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) => {
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) => {
9753
9979
  try {
9754
9980
  if (getNetwork(command) !== "devnet") {
9755
9981
  throw new Error("agent credential requires --network devnet");
@@ -9782,20 +10008,20 @@ function agentCredentialCmd() {
9782
10008
  });
9783
10009
  }
9784
10010
  function agentCmd() {
9785
- const command = new Command36("agent").description("Onboard and operate FLOP-backed agents");
10011
+ const command = new Command37("agent").description("Onboard and operate FLOP-backed agents");
9786
10012
  command.addCommand(agentInitCmd());
9787
10013
  command.addCommand(agentDoctorCmd());
9788
10014
  command.addCommand(agentCredentialCmd());
9789
10015
  command.addCommand(launchCmd());
9790
10016
  command.addCommand(proxyCmd());
9791
10017
  command.addCommand(hermesCmd());
9792
- const keys = new Command36("keys").description("Manage agent keys");
10018
+ const keys = new Command37("keys").description("Manage agent keys");
9793
10019
  keys.addCommand(keysGenerateCmd());
9794
10020
  command.addCommand(keys);
9795
- const wallet = new Command36("wallet").description("Manage agent wallets");
10021
+ const wallet = new Command37("wallet").description("Manage agent wallets");
9796
10022
  wallet.addCommand(walletAgentCreateCmd());
9797
10023
  command.addCommand(wallet);
9798
- const session = new Command36("session").description("Manage agent session keys");
10024
+ const session = new Command37("session").description("Manage agent session keys");
9799
10025
  session.addCommand(sessionCreateCmd());
9800
10026
  session.addCommand(sessionInfoCmd());
9801
10027
  session.addCommand(sessionRevokeCmd());
@@ -9807,7 +10033,7 @@ function agentCmd() {
9807
10033
  // src/commands/miner.ts
9808
10034
  import { readFileSync } from "node:fs";
9809
10035
  import path3 from "node:path";
9810
- import { Command as Command37, Option as Option5 } from "commander";
10036
+ import { Command as Command38, Option as Option6 } from "commander";
9811
10037
  import { formatFlop as formatFlop2 } from "@flop-labs/sdk";
9812
10038
 
9813
10039
  // src/gpu-inventory-remark.ts
@@ -9968,6 +10194,12 @@ function buildMinerInitPlan(options) {
9968
10194
  summary: mode === "tdx" ? "Prepare the measured TDX guest, attest it, then activate the on-chain miner separately." : mode === "cloud" ? "Provision a non-attested inference backend, then connect miner-worker explicitly." : "Start a local measured-manifest simulation before using real stake or hardware.",
9969
10195
  gpu_inventory: gpu,
9970
10196
  steps: [
10197
+ {
10198
+ phase: "devnet host",
10199
+ command: "sudo ./install.sh <machine-name>",
10200
+ purpose: "On an Ubuntu 24.04 GPU host, use the operator-issued pack and https://flop.network/skills/flop-miner-devnet/SKILL.md. The pack pins the registered model; the following generic steps apply to other setups.",
10201
+ mutates: true
10202
+ },
9971
10203
  {
9972
10204
  phase: "inventory",
9973
10205
  command: "flop gpu --json",
@@ -10019,12 +10251,12 @@ function buildMinerInitPlan(options) {
10019
10251
  };
10020
10252
  }
10021
10253
  function minerInitCmd() {
10022
- return new Command37("init").description("Print a phased miner onboarding plan").addOption(new Option5("--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(
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(
10023
10255
  "--gpu <SKU[:COUNT]>",
10024
10256
  "Expected GPU inventory; repeat for mixed-SKU hosts",
10025
10257
  collectGpu,
10026
10258
  []
10027
- ).option("--model <id>", "Inference model", "Qwen/Qwen2.5-7B-Instruct").option("--json", "Output as JSON").action((opts, command) => {
10259
+ ).option("--model <id>", "Inference model", "Qwen/Qwen2.5-0.5B-Instruct").option("--json", "Output as JSON").action((opts, command) => {
10028
10260
  const instance = Number(opts.instance);
10029
10261
  if (!Number.isInteger(instance) || instance < 0) {
10030
10262
  commandError(new Error("--instance must be a non-negative integer"));
@@ -10062,7 +10294,7 @@ async function gpuCheck(mode) {
10062
10294
  }
10063
10295
  }
10064
10296
  function minerDoctorCmd() {
10065
- return new Command37("doctor").description("Check miner runtime, hardware, chain, and serving prerequisites").addOption(new Option5("--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) => {
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) => {
10066
10298
  try {
10067
10299
  const checks = [];
10068
10300
  if (opts.mode === "cloud") {
@@ -10099,7 +10331,7 @@ function minerDoctorCmd() {
10099
10331
  });
10100
10332
  }
10101
10333
  function minerStatusCmd() {
10102
- return new Command37("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(
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(
10103
10335
  "--expect-gpu <SKU[:COUNT]>",
10104
10336
  "Expected inventory; repeat to check attested state",
10105
10337
  collectGpu,
@@ -10149,7 +10381,7 @@ function minerStatusCmd() {
10149
10381
  });
10150
10382
  }
10151
10383
  function minerSkusCmd() {
10152
- return new Command37("skus").description("List GPU SKUs with active governance calibration ceilings").option("--json", "Output as JSON").action(async (_opts, command) => {
10384
+ return new Command38("skus").description("List GPU SKUs with active governance calibration ceilings").option("--json", "Output as JSON").action(async (_opts, command) => {
10153
10385
  let client;
10154
10386
  try {
10155
10387
  const rpc = getRpc(command);
@@ -10173,7 +10405,7 @@ function minerSkusCmd() {
10173
10405
  });
10174
10406
  }
10175
10407
  function minerRegisterCmd() {
10176
- return new Command37("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) => {
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) => {
10177
10409
  let client;
10178
10410
  try {
10179
10411
  const commission = Number(opts.commission);
@@ -10200,7 +10432,7 @@ function minerRegisterCmd() {
10200
10432
  });
10201
10433
  }
10202
10434
  function minerDeclareGpuCmd() {
10203
- return new Command37("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) => {
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) => {
10204
10436
  let client;
10205
10437
  try {
10206
10438
  if ([opts.nvidiaSmi, opts.detect, opts.retract].filter(Boolean).length !== 1) {
@@ -10236,7 +10468,7 @@ function minerDeclareGpuCmd() {
10236
10468
  });
10237
10469
  }
10238
10470
  function minerCmd() {
10239
- const command = new Command37("miner").description("Onboard and operate inference miners");
10471
+ const command = new Command38("miner").description("Onboard and operate inference miners");
10240
10472
  command.addCommand(minerInitCmd());
10241
10473
  command.addCommand(minerDoctorCmd());
10242
10474
  command.addCommand(minerStatusCmd());
@@ -10249,7 +10481,7 @@ function minerCmd() {
10249
10481
 
10250
10482
  // src/commands/validator.ts
10251
10483
  import path4 from "node:path";
10252
- import { Command as Command38, Option as Option6 } from "commander";
10484
+ import { Command as Command39, Option as Option7 } from "commander";
10253
10485
  import { formatFlop as formatFlop3 } from "@flop-labs/sdk";
10254
10486
  function buildValidatorInitPlan(mode, minimumStake) {
10255
10487
  const bringUp = mode === "local" ? "just run-node" : "infra/vultr/create-vm.sh <args>; infra/vultr/deploy-core.sh --vm-ip <TAILSCALE-IP>";
@@ -10299,11 +10531,11 @@ function buildValidatorInitPlan(mode, minimumStake) {
10299
10531
  };
10300
10532
  }
10301
10533
  function validatorInitCmd() {
10302
- return new Command38("init").description("Print a validator onboarding plan with the live minimum stake").addOption(new Option6("--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) => {
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) => {
10303
10535
  let client;
10304
10536
  try {
10305
10537
  let minimumStake;
10306
- if (!opts.offline) {
10538
+ if (!getCommandOption(command, "offline", false)) {
10307
10539
  const rpc = getRpc(command);
10308
10540
  client = await createClient(rpc);
10309
10541
  const { getMinimumStake } = await import("@flop-labs/sdk");
@@ -10318,7 +10550,7 @@ function validatorInitCmd() {
10318
10550
  });
10319
10551
  }
10320
10552
  function validatorDoctorCmd() {
10321
- return new Command38("doctor").description("Check validator node and on-chain eligibility prerequisites").addOption(new Option6("--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) => {
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) => {
10322
10554
  let client;
10323
10555
  try {
10324
10556
  const checks = [
@@ -10389,7 +10621,7 @@ function validatorDoctorCmd() {
10389
10621
  });
10390
10622
  }
10391
10623
  function validatorStatusCmd() {
10392
- return new Command38("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) => {
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) => {
10393
10625
  let client;
10394
10626
  try {
10395
10627
  const rpc = getRpc(command);
@@ -10434,7 +10666,7 @@ function validatorStatusCmd() {
10434
10666
  });
10435
10667
  }
10436
10668
  function validatorRegisterCmd() {
10437
- return new Command38("register").description("Register the signer as a validator, bonding the effective minimum").option("--json", "Output as JSON").action(async (opts, command) => {
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) => {
10438
10670
  let client;
10439
10671
  try {
10440
10672
  const rpc = getRpc(command);
@@ -10464,7 +10696,7 @@ function validatorRegisterCmd() {
10464
10696
  });
10465
10697
  }
10466
10698
  function validatorCommissionCmd() {
10467
- return new Command38("commission").description("Set validator commission").argument("<percent>", "Commission percent (0-100)").option("--json", "Output as JSON").action(async (percent, opts, command) => {
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) => {
10468
10700
  let client;
10469
10701
  try {
10470
10702
  const value = Number(percent);
@@ -10484,7 +10716,7 @@ function validatorCommissionCmd() {
10484
10716
  });
10485
10717
  }
10486
10718
  function validatorLifecycleCmd(name, description, sdkMethod) {
10487
- return new Command38(name).description(description).option("--json", "Output as JSON").action(async (opts, command) => {
10719
+ return new Command39(name).description(description).option("--json", "Output as JSON").action(async (opts, command) => {
10488
10720
  let client;
10489
10721
  try {
10490
10722
  const rpc = getRpc(command);
@@ -10500,7 +10732,7 @@ function validatorLifecycleCmd(name, description, sdkMethod) {
10500
10732
  });
10501
10733
  }
10502
10734
  function validatorCmd() {
10503
- const command = new Command38("validator").description("Onboard and operate validator nodes");
10735
+ const command = new Command39("validator").description("Onboard and operate validator nodes");
10504
10736
  command.addCommand(validatorInitCmd());
10505
10737
  command.addCommand(validatorDoctorCmd());
10506
10738
  command.addCommand(validatorStatusCmd());
@@ -10513,13 +10745,12 @@ function validatorCmd() {
10513
10745
  }
10514
10746
 
10515
10747
  // src/commands/network-status.ts
10516
- import { Command as Command39 } from "commander";
10748
+ import { Command as Command40 } from "commander";
10517
10749
 
10518
10750
  // src/network.ts
10519
10751
  async function probeRpcChain(network, rpc) {
10520
- const { ApiPromise: ApiPromise2, HttpProvider: HttpProvider2, WsProvider: WsProvider2 } = await import("@polkadot/api");
10521
- const provider = rpc.startsWith("ws") ? new WsProvider2(rpc) : new HttpProvider2(rpc);
10522
- const api = await ApiPromise2.create({ provider, noInitWarn: true });
10752
+ const { connectFlopApi } = await import("@flop-labs/sdk");
10753
+ const api = await connectFlopApi(rpc);
10523
10754
  try {
10524
10755
  const chain = (await api.rpc.system.chain()).toString();
10525
10756
  return {
@@ -10539,9 +10770,16 @@ async function probeRpcChain(network, rpc) {
10539
10770
  // src/commands/network-status.ts
10540
10771
  function networkStatusCmd(probeRpc2 = probeRpcChain, probeFaucet = async (endpoint) => {
10541
10772
  const { getFaucetChainInfo } = await import("@flop-labs/sdk");
10542
- return getFaucetChainInfo({ endpoint });
10773
+ const identity = await getFaucetChainInfo({ endpoint });
10774
+ try {
10775
+ const response = await fetch(`${endpoint.replace(/\/$/, "")}/health`, { signal: AbortSignal.timeout(15e3) });
10776
+ const health = await response.json();
10777
+ return { ...identity, ready: typeof health.ready === "boolean" ? health.ready : null, dependencies: health.dependencies };
10778
+ } catch (error) {
10779
+ return { ...identity, ready: null, readinessError: error instanceof Error ? error.message : String(error) };
10780
+ }
10543
10781
  }) {
10544
- return new Command39("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) => {
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) => {
10545
10783
  const resolved = resolveNetwork(command);
10546
10784
  let rpcInfo = null;
10547
10785
  let rpcError;
@@ -10582,6 +10820,9 @@ function networkStatusCmd(probeRpc2 = probeRpcChain, probeFaucet = async (endpoi
10582
10820
  faucet: resolved.profile.faucet ? {
10583
10821
  url: resolved.profile.faucet,
10584
10822
  reachable: opts.offline ? null : faucetInfo !== null,
10823
+ ready: faucetInfo?.ready ?? null,
10824
+ dependencies: faucetInfo?.dependencies,
10825
+ readiness_error: faucetInfo?.readinessError,
10585
10826
  network: faucetInfo?.network,
10586
10827
  genesis_hash: faucetInfo?.genesisHash,
10587
10828
  matches_rpc: rpcInfo && faucetInfo ? rpcInfo.genesisHash.toLowerCase() === faucetInfo.genesisHash.toLowerCase() : null,
@@ -10592,9 +10833,9 @@ function networkStatusCmd(probeRpc2 = probeRpcChain, probeFaucet = async (endpoi
10592
10833
  }
10593
10834
 
10594
10835
  // src/commands/faucet-request.ts
10595
- import { Command as Command40 } from "commander";
10836
+ import { Command as Command41 } from "commander";
10596
10837
  function faucetRequestCmd(probeRpc2 = probeRpcChain) {
10597
- return new Command40("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) => {
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) => {
10598
10839
  try {
10599
10840
  const resolved = resolveNetwork(command);
10600
10841
  const endpoint = opts.endpoint ?? resolved.profile.faucet;
@@ -10647,15 +10888,15 @@ function faucetRequestCmd(probeRpc2 = probeRpcChain) {
10647
10888
  }
10648
10889
 
10649
10890
  // src/commands/chat.ts
10650
- import { Command as Command41 } from "commander";
10891
+ import { Command as Command42 } from "commander";
10651
10892
  import { randomUUID as randomUUID4 } from "node:crypto";
10652
10893
  import { homedir as homedir4 } from "node:os";
10653
- import { join as join8 } from "node:path";
10894
+ import { join as join9 } from "node:path";
10654
10895
  import { createInterface } from "node:readline/promises";
10655
10896
  import { stdin, stdout } from "node:process";
10656
10897
 
10657
10898
  // src/conversations.ts
10658
- import { mkdir as mkdir12, readFile as readFile8, rename as rename8, writeFile as writeFile10 } from "node:fs/promises";
10899
+ import { mkdir as mkdir13, readFile as readFile8, rename as rename8, writeFile as writeFile11 } from "node:fs/promises";
10659
10900
  import { dirname as dirname6 } from "node:path";
10660
10901
  var MAX_CONVERSATIONS = 100;
10661
10902
  var MAX_CONVERSATION_MESSAGES = 100;
@@ -10675,9 +10916,9 @@ var ConversationStore = class {
10675
10916
  }
10676
10917
  }
10677
10918
  async write(data) {
10678
- await mkdir12(dirname6(this.path), { recursive: true, mode: 448 });
10919
+ await mkdir13(dirname6(this.path), { recursive: true, mode: 448 });
10679
10920
  const temp = `${this.path}.${process.pid}.tmp`;
10680
- await writeFile10(temp, `${JSON.stringify(data, null, 2)}
10921
+ await writeFile11(temp, `${JSON.stringify(data, null, 2)}
10681
10922
  `, { mode: 384 });
10682
10923
  await rename8(temp, this.path);
10683
10924
  }
@@ -10714,7 +10955,7 @@ function conversationPrompt(messages) {
10714
10955
 
10715
10956
  // src/commands/chat.ts
10716
10957
  function chatCmd() {
10717
- return new Command41("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>", "Registered model ID", "llama-3-70b-instruct").option("--session-owner <address>", "Use --signer as this owner's session delegate").option("--max-flop <n>", "Per-turn FLOP cap", "5").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", join8(homedir4(), ".flop", "conversations.json")).option("--json", "Output as JSON").action(async (prompt, opts, command) => {
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) => {
10718
10959
  const store = new ConversationStore(opts.store);
10719
10960
  let agent;
10720
10961
  try {
@@ -10747,19 +10988,28 @@ function chatCmd() {
10747
10988
  const now = (/* @__PURE__ */ new Date()).toISOString();
10748
10989
  const conversation = existing ?? {
10749
10990
  id: randomUUID4(),
10750
- model: opts.model,
10991
+ model: opts.model ?? "",
10751
10992
  sessionOwner: opts.sessionOwner,
10752
10993
  createdAt: now,
10753
10994
  updatedAt: now,
10754
10995
  messages: []
10755
10996
  };
10756
- if (!opts.skipModelCheck) {
10997
+ if (!conversation.model) throw new Error("Supply --model with a hash or exact registered name from `flop model list`");
10998
+ {
10757
10999
  const sdk = await import("@flop-labs/sdk");
10758
11000
  const check = await createClient(getRpc(command));
10759
11001
  try {
10760
- const registry = new sdk.OnChainModelRegistry(check.api);
10761
- const model = await registry.getModel(conversation.model) ?? await registry.getModelByName(conversation.model);
10762
- if (!model) throw new Error(`model not found: ${conversation.model}`);
11002
+ if (!opts.skipModelCheck) {
11003
+ const registry = new sdk.OnChainModelRegistry(check.api);
11004
+ const model = /^0x[0-9a-f]{64}$/i.test(conversation.model) ? await registry.getModel(conversation.model) : await registry.getModelByName(conversation.model);
11005
+ if (!model) throw new Error(`model not found: ${conversation.model}; use a hash or exact name from flop model list`);
11006
+ conversation.model = model.hash;
11007
+ }
11008
+ if (!opts.teeRequirements) throw new Error("HTLC chat requires --tee-requirements from a registered EconomicCritical profile; check the network's attestation prerequisites before funding or signing");
11009
+ if (/^0x[0-9a-f]{200}$/i.test(opts.teeRequirements) && /^0x[0-9a-f]{64}$/i.test(conversation.model) && opts.teeRequirements.slice(74, 138).toLowerCase() !== conversation.model.slice(2).toLowerCase()) {
11010
+ throw new Error("HTLC model commitment must match the resolved --model hash");
11011
+ }
11012
+ await sdk.assertHtlcAppraisalProfile(check.api, opts.teeRequirements);
10763
11013
  } finally {
10764
11014
  await check.disconnect();
10765
11015
  }
@@ -10779,7 +11029,8 @@ function chatCmd() {
10779
11029
  conversation.messages.push({ role: "user", content: input, at: (/* @__PURE__ */ new Date()).toISOString() });
10780
11030
  const result = await agent.inference(conversationPrompt(conversation.messages), {
10781
11031
  model: conversation.model,
10782
- maxCost: maxFlop
11032
+ maxCost: maxFlop,
11033
+ teeRequirements: { appraisal: opts.teeRequirements }
10783
11034
  });
10784
11035
  conversation.messages.push({
10785
11036
  role: "assistant",
@@ -10829,8 +11080,8 @@ function chatCmd() {
10829
11080
  }
10830
11081
 
10831
11082
  // src/commands/tx-build.ts
10832
- import { Command as Command42 } from "commander";
10833
- import { writeFile as writeFile11 } from "node:fs/promises";
11083
+ import { Command as Command43 } from "commander";
11084
+ import { writeFile as writeFile12 } from "node:fs/promises";
10834
11085
  import { execFile as execFile5 } from "node:child_process";
10835
11086
  import { promisify as promisify5 } from "node:util";
10836
11087
 
@@ -10896,7 +11147,7 @@ function assertEnvelopeChain(api, envelope) {
10896
11147
 
10897
11148
  // src/commands/tx-build.ts
10898
11149
  function txBuildCmd() {
10899
- return new Command42("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) => {
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) => {
10900
11151
  let client;
10901
11152
  try {
10902
11153
  const resolved = resolveNetwork(command);
@@ -10909,7 +11160,7 @@ function txBuildCmd() {
10909
11160
  opts.call
10910
11161
  );
10911
11162
  if (opts.out) {
10912
- await writeFile11(opts.out, `${JSON.stringify(envelope, null, 2)}
11163
+ await writeFile12(opts.out, `${JSON.stringify(envelope, null, 2)}
10913
11164
  `, { mode: 384 });
10914
11165
  }
10915
11166
  if (opts.qr) {
@@ -10941,10 +11192,10 @@ function txBuildCmd() {
10941
11192
  }
10942
11193
 
10943
11194
  // src/commands/tx-broadcast.ts
10944
- import { Command as Command43 } from "commander";
11195
+ import { Command as Command44 } from "commander";
10945
11196
  import { readFile as readFile9 } from "node:fs/promises";
10946
11197
  function txBroadcastCmd() {
10947
- return new Command43("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) => {
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) => {
10948
11199
  let client;
10949
11200
  try {
10950
11201
  if (!/^0x[0-9a-f]+$/i.test(opts.signature)) throw new Error("--signature must be 0x hex");
@@ -10980,7 +11231,7 @@ function txBroadcastCmd() {
10980
11231
  }
10981
11232
 
10982
11233
  // src/commands/multisig.ts
10983
- import { Command as Command44 } from "commander";
11234
+ import { Command as Command45 } from "commander";
10984
11235
  import { createKeyMulti, encodeAddress, sortAddresses } from "@polkadot/util-crypto";
10985
11236
  import { blake2AsHex } from "@polkadot/util-crypto";
10986
11237
  function otherSignatories(signer, values) {
@@ -11001,8 +11252,8 @@ function timepoint(opts) {
11001
11252
  return { height: parsePositiveInt(opts.height, "--height"), index };
11002
11253
  }
11003
11254
  function multisigCmd() {
11004
- const command = new Command44("multisig").description("Participate in Substrate multisig transactions");
11005
- command.addCommand(new Command44("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) => {
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) => {
11006
11257
  try {
11007
11258
  const threshold = parsePositiveInt(opts.threshold, "--threshold");
11008
11259
  const sorted = sortAddresses(opts.signatory, 42);
@@ -11017,7 +11268,7 @@ function multisigCmd() {
11017
11268
  commandError(error);
11018
11269
  }
11019
11270
  }));
11020
- command.addCommand(new Command44("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) => {
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) => {
11021
11272
  let client;
11022
11273
  try {
11023
11274
  client = await createClient(getRpc(cmd), getSigner(cmd));
@@ -11040,7 +11291,7 @@ function multisigCmd() {
11040
11291
  await client?.disconnect();
11041
11292
  }
11042
11293
  }));
11043
- command.addCommand(new Command44("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) => {
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) => {
11044
11295
  let client;
11045
11296
  try {
11046
11297
  client = await createClient(getRpc(cmd), getSigner(cmd));
@@ -11059,7 +11310,7 @@ function multisigCmd() {
11059
11310
  await client?.disconnect();
11060
11311
  }
11061
11312
  }));
11062
- command.addCommand(new Command44("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) => {
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) => {
11063
11314
  let client;
11064
11315
  try {
11065
11316
  client = await createClient(getRpc(cmd), getSigner(cmd));
@@ -11076,7 +11327,7 @@ function multisigCmd() {
11076
11327
  await client?.disconnect();
11077
11328
  }
11078
11329
  }));
11079
- command.addCommand(new Command44("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) => {
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) => {
11080
11331
  let client;
11081
11332
  try {
11082
11333
  client = await createClient(getRpc(cmd));
@@ -11096,10 +11347,10 @@ function multisigCmd() {
11096
11347
  }
11097
11348
 
11098
11349
  // src/commands/channel.ts
11099
- import { Command as Command45 } from "commander";
11350
+ import { Command as Command46 } from "commander";
11100
11351
  function channelCmd() {
11101
- const command = new Command45("channel").description("Inspect and finalize compute channels");
11102
- command.addCommand(new Command45("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) => {
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) => {
11103
11354
  let client;
11104
11355
  try {
11105
11356
  client = await createClient(getRpc(cmd));
@@ -11127,7 +11378,7 @@ function channelCmd() {
11127
11378
  await client?.disconnect();
11128
11379
  }
11129
11380
  }));
11130
- command.addCommand(new Command45("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) => {
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) => {
11131
11382
  let client;
11132
11383
  try {
11133
11384
  client = await createClient(getRpc(cmd), getSigner(cmd));
@@ -11573,8 +11824,8 @@ function generateGatewayConfig(input) {
11573
11824
  }
11574
11825
 
11575
11826
  // src/commands/gateway-allowlist.ts
11576
- import { Command as Command46 } from "commander";
11577
- var PRECISIONS4 = ["FP32", "FP16", "BF16", "INT8", "INT4", "Mixed"];
11827
+ import { Command as Command47 } from "commander";
11828
+ var PRECISIONS5 = ["FP32", "FP16", "BF16", "INT8", "INT4", "Mixed"];
11578
11829
  function field(value, camel, snake) {
11579
11830
  return value?.[camel] ?? value?.[snake];
11580
11831
  }
@@ -11601,18 +11852,19 @@ function verificationStatus(model, modelJson) {
11601
11852
  }
11602
11853
  async function fetchMinerSessionInfo(endpoint, authToken, signal) {
11603
11854
  const infoUrl = `${endpoint.replace(/\/+$/, "")}/session/info`;
11604
- const anonymous = await fetch(infoUrl, { signal });
11855
+ const anonymous = await fetch(infoUrl, { signal, redirect: "error" });
11605
11856
  await anonymous.body?.cancel();
11606
11857
  if (anonymous.status !== 401) throw new Error("miner /session/info does not enforce bearer authentication");
11607
- const response = await fetch(infoUrl, { headers: { Authorization: `Bearer ${authToken}` }, signal });
11858
+ const response = await fetch(infoUrl, { redirect: "error", headers: { Authorization: `Bearer ${authToken}` }, signal });
11608
11859
  if (!response.ok) throw new Error(`miner /session/info returned HTTP ${response.status}`);
11609
- return await response.json();
11860
+ const { computeChannel: computeChannel7 } = await import("@flop-labs/sdk");
11861
+ return JSON.parse(await computeChannel7.readMinerResponse(response, 16 * 1024));
11610
11862
  }
11611
- async function readChainSnapshot(api, miner) {
11863
+ async function readChainSnapshot(api, miner, pinnedHead) {
11612
11864
  const identity = miner.modelIdentity;
11613
- const precision = PRECISIONS4[identity.precision];
11865
+ const precision = PRECISIONS5[identity.precision];
11614
11866
  if (!precision) throw new Error(`miner precision is invalid: ${identity.precision}`);
11615
- const finalizedHead = await api.rpc.chain.getFinalizedHead();
11867
+ const finalizedHead = pinnedHead ?? await api.rpc.chain.getFinalizedHead();
11616
11868
  const at = await api.at(finalizedHead);
11617
11869
  const modelRegistry = at.query.modelRegistry;
11618
11870
  const hwCalibration = at.query.hwCalibration;
@@ -11661,7 +11913,7 @@ async function readChainSnapshot(api, miner) {
11661
11913
  };
11662
11914
  }
11663
11915
  function gatewayAllowlistCmd() {
11664
- return new Command46("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) => {
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) => {
11665
11917
  let client;
11666
11918
  try {
11667
11919
  const authToken = process.env[opts.authTokenEnv];
@@ -11687,9 +11939,9 @@ function gatewayAllowlistCmd() {
11687
11939
  }
11688
11940
 
11689
11941
  // src/commands/gateway-renew.ts
11690
- import { open as open3, readFile as readFile11, rename as rename9, rm as rm4, writeFile as writeFile12 } from "node:fs/promises";
11691
- import { createHash as createHash5 } from "node:crypto";
11692
- import { request } from "node:http";
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";
11944
+ import { request as request2 } from "node:http";
11693
11945
  import { setTimeout as delay } from "node:timers/promises";
11694
11946
  import { Keyring as Keyring5 } from "@polkadot/keyring";
11695
11947
  import { decodeAddress as decodeAddress2, randomAsU8a } from "@polkadot/util-crypto";
@@ -11885,8 +12137,284 @@ function qualificationSignedToolCalls(frames) {
11885
12137
  });
11886
12138
  }
11887
12139
 
12140
+ // ../gateway-core/src/miner-admissions.ts
12141
+ import { closeSync, fstatSync, openSync, readSync, statSync } from "node:fs";
12142
+ import { createHash as createHash5 } from "node:crypto";
12143
+ var MAX_ADMITTED_MINERS = 256;
12144
+ var MAX_FILE_BYTES = 512 * 1024;
12145
+ var HASH4 = /^0x[0-9a-f]{64}$/;
12146
+ var minerSlot = (miner) => `${miner.minerAddress}:${miner.modelIdentity.requestedSlug}`;
12147
+ var staticMinerSha256 = (miner) => createHash5("sha256").update(JSON.stringify(miner)).digest("hex");
12148
+ var sameModel = (a, b) => Object.keys(a.modelIdentity).every((key) => a.modelIdentity[key] === b.modelIdentity[key]);
12149
+ var MinerRouting = class {
12150
+ constructor(statics, path5) {
12151
+ this.statics = statics;
12152
+ this.path = path5;
12153
+ }
12154
+ statics;
12155
+ path;
12156
+ stamp;
12157
+ entries = [];
12158
+ error;
12159
+ revision = 0;
12160
+ read(dynamic, now = Date.now()) {
12161
+ if (this.path) {
12162
+ let stamp;
12163
+ try {
12164
+ const stat = statSync(this.path);
12165
+ stamp = `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeMs}`;
12166
+ } catch {
12167
+ stamp = "missing";
12168
+ }
12169
+ if (stamp !== this.stamp) {
12170
+ this.stamp = stamp;
12171
+ this.revision++;
12172
+ this.entries = [];
12173
+ this.error = void 0;
12174
+ let fd;
12175
+ try {
12176
+ fd = openSync(this.path, "r");
12177
+ const stat = fstatSync(fd);
12178
+ if (!stat.isFile() || stat.size > MAX_FILE_BYTES) throw new Error();
12179
+ const buffer = Buffer.alloc(MAX_FILE_BYTES + 1);
12180
+ const size = readSync(fd, buffer, 0, buffer.length, 0);
12181
+ if (size > MAX_FILE_BYTES) throw new Error();
12182
+ const value = JSON.parse(buffer.subarray(0, size).toString("utf8"));
12183
+ if (value.schema !== "flop-static-miner-handoffs/v1" || !Array.isArray(value.entries) || value.entries.length > MAX_ADMITTED_MINERS) throw new Error();
12184
+ const slots2 = /* @__PURE__ */ new Set();
12185
+ for (const entry of value.entries) {
12186
+ if (!entry || typeof entry.minerAddress !== "string" || !entry.minerAddress || typeof entry.requestedSlug !== "string" || !entry.requestedSlug || !/^[0-9a-f]{64}$/.test(entry.staticSha256)) throw new Error();
12187
+ const url = new URL(entry.endpoint);
12188
+ if (url.protocol !== "https:" || url.username || url.password || url.pathname !== "/" || url.search || url.hash) throw new Error();
12189
+ const slot = `${entry.minerAddress}:${entry.requestedSlug}`;
12190
+ if (slots2.has(slot)) throw new Error();
12191
+ slots2.add(slot);
12192
+ }
12193
+ this.entries = value.entries;
12194
+ } catch {
12195
+ this.error = new Error("static miner handoffs file is missing or invalid; new routing is disabled");
12196
+ } finally {
12197
+ if (fd !== void 0) closeSync(fd);
12198
+ }
12199
+ }
12200
+ if (this.error) throw this.error;
12201
+ }
12202
+ const handed = /* @__PURE__ */ new Map();
12203
+ for (const entry of this.entries) {
12204
+ const slot = `${entry.minerAddress}:${entry.requestedSlug}`;
12205
+ const sources = this.statics.filter((miner) => minerSlot(miner) === slot);
12206
+ const source = sources[0];
12207
+ if (sources.length !== 1 || source.onDemand || source.admissionExpiresAt || staticMinerSha256(source) !== entry.staticSha256 || source.endpoint === entry.endpoint || !Number.isFinite(source.maxBudgetFlop) || source.maxBudgetFlop <= 0 || !Number.isSafeInteger(source.maxDurationBlocks) || source.maxDurationBlocks < 1) throw new Error("static miner handoff source differs; new routing is disabled");
12208
+ const target = dynamic.find((miner) => minerSlot(miner) === slot && miner.endpoint === entry.endpoint && miner.enclaveKey === source.enclaveKey && sameModel(source, miner) && miner.channelSigned && !!miner.admissionExpiresAt && Date.parse(miner.admissionExpiresAt) > now);
12209
+ handed.set(slot, target ? {
12210
+ ...target,
12211
+ maxBudgetFlop: Math.min(source.maxBudgetFlop, target.maxBudgetFlop),
12212
+ maxDurationBlocks: Math.min(source.maxDurationBlocks, target.maxDurationBlocks)
12213
+ } : void 0);
12214
+ }
12215
+ const slots = new Set(this.statics.map(minerSlot));
12216
+ const selected = [];
12217
+ for (const miner of this.statics) {
12218
+ if (!handed.has(minerSlot(miner))) selected.push(miner);
12219
+ else {
12220
+ const target = handed.get(minerSlot(miner));
12221
+ if (target) selected.push(target);
12222
+ }
12223
+ }
12224
+ return [...selected, ...dynamic.filter((miner) => !slots.has(minerSlot(miner)))];
12225
+ }
12226
+ };
12227
+ var MinerAdmissions = class {
12228
+ constructor(path5, policy) {
12229
+ this.path = path5;
12230
+ this.policy = policy;
12231
+ }
12232
+ path;
12233
+ policy;
12234
+ stamp;
12235
+ entries = [];
12236
+ revision = 0;
12237
+ read(now = Date.now()) {
12238
+ let stamp;
12239
+ try {
12240
+ const stat = statSync(this.path);
12241
+ stamp = `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeMs}`;
12242
+ } catch {
12243
+ stamp = "missing";
12244
+ }
12245
+ if (stamp !== this.stamp) {
12246
+ this.stamp = stamp;
12247
+ this.revision++;
12248
+ this.entries = [];
12249
+ let fd;
12250
+ try {
12251
+ 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");
12254
+ const buffer = Buffer.alloc(MAX_FILE_BYTES + 1);
12255
+ const size = readSync(fd, buffer, 0, buffer.length, 0);
12256
+ if (size > MAX_FILE_BYTES) throw new Error("admissions file too large");
12257
+ const file = JSON.parse(buffer.subarray(0, size).toString("utf8"));
12258
+ 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
+ const slots = /* @__PURE__ */ new Set();
12260
+ const entries = file.entries.map((entry) => {
12261
+ const id = entry.modelIdentity;
12262
+ const url = new URL(entry.endpoint);
12263
+ const accepted = Date.parse(entry.adAcceptedAt), expiry = Date.parse(entry.admissionExpiresAt);
12264
+ 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);
12266
+ const slot = minerSlot(entry);
12267
+ if (slots.has(slot)) throw new Error("duplicate admission slot");
12268
+ slots.add(slot);
12269
+ return {
12270
+ endpoint: entry.endpoint,
12271
+ authToken: "",
12272
+ channelSigned: true,
12273
+ minerAddress: entry.minerAddress,
12274
+ enclaveKey: entry.enclaveKey,
12275
+ modelIdentity: id,
12276
+ ...this.policy,
12277
+ admissionExpiresAt: entry.admissionExpiresAt,
12278
+ deployment: {
12279
+ countryCode: entry.observation.countryCode ?? "ZZ",
12280
+ source: "mailbox-observed:non-regional",
12281
+ observedAt: entry.adAcceptedAt,
12282
+ expiresAt: entry.admissionExpiresAt
12283
+ }
12284
+ };
12285
+ });
12286
+ this.entries = entries;
12287
+ } catch {
12288
+ } finally {
12289
+ if (fd !== void 0) closeSync(fd);
12290
+ }
12291
+ }
12292
+ return this.entries.filter((entry) => Date.parse(entry.admissionExpiresAt) > now);
12293
+ }
12294
+ };
12295
+
12296
+ // ../gateway-core/src/miner-peer.ts
12297
+ import { BlockList, isIP } from "node:net";
12298
+ import { resolve4 as resolve42, resolve6 as resolve62 } from "node:dns/promises";
12299
+ import { request } from "node:https";
12300
+ import { Readable } from "node:stream";
12301
+ var NOT_PUBLIC = new BlockList();
12302
+ for (const [net, bits] of [
12303
+ ["0.0.0.0", 8],
12304
+ ["10.0.0.0", 8],
12305
+ ["100.64.0.0", 10],
12306
+ ["127.0.0.0", 8],
12307
+ ["169.254.0.0", 16],
12308
+ ["172.16.0.0", 12],
12309
+ ["192.0.0.0", 24],
12310
+ ["192.0.2.0", 24],
12311
+ ["192.168.0.0", 16],
12312
+ ["198.18.0.0", 15],
12313
+ ["198.51.100.0", 24],
12314
+ ["203.0.113.0", 24],
12315
+ ["224.0.0.0", 4],
12316
+ ["240.0.0.0", 4]
12317
+ ]) NOT_PUBLIC.addSubnet(net, bits, "ipv4");
12318
+ for (const [net, bits] of [
12319
+ // No ::ffff:0:0/96 rule: BlockList also checks IPv4 addresses against IPv4-mapped rules, so it
12320
+ // would match every IPv4 address. A mapped address is judged by its IPv4 part instead.
12321
+ ["::", 128],
12322
+ ["::1", 128],
12323
+ ["64:ff9b::", 96],
12324
+ ["100::", 64],
12325
+ ["2001:db8::", 32],
12326
+ ["fc00::", 7],
12327
+ ["fe80::", 10],
12328
+ ["ff00::", 8]
12329
+ ]) NOT_PUBLIC.addSubnet(net, bits, "ipv6");
12330
+ function isPublicAddress(ip) {
12331
+ const family = isIP(ip);
12332
+ if (family === 0) return false;
12333
+ if (family === 6) {
12334
+ const canonical = new URL(`http://[${ip}]`).hostname.slice(1, -1);
12335
+ const mapped = /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/.exec(canonical);
12336
+ if (mapped) {
12337
+ const [high, low] = [parseInt(mapped[1], 16), parseInt(mapped[2], 16)];
12338
+ return isPublicAddress(`${high >> 8}.${high & 255}.${low >> 8}.${low & 255}`);
12339
+ }
12340
+ }
12341
+ return !NOT_PUBLIC.check(ip, family === 4 ? "ipv4" : "ipv6");
12342
+ }
12343
+ var EndpointRefusal = class extends Error {
12344
+ };
12345
+ async function resolveDnsAddresses(host, v4 = resolve42, v6 = resolve62) {
12346
+ for (let attempt = 0; ; attempt++) {
12347
+ const results = await Promise.allSettled([v4(host), v6(host)]);
12348
+ const errors = results.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
12349
+ const code = (error) => error?.code;
12350
+ const fatal = errors.find((error) => code(error) !== "ENODATA" && code(error) !== "ENOTFOUND");
12351
+ if (fatal) throw fatal;
12352
+ const notFound = errors.find((error) => code(error) === "ENOTFOUND");
12353
+ if (notFound) {
12354
+ if (attempt === 0) continue;
12355
+ throw notFound;
12356
+ }
12357
+ return results.flatMap((result, index) => result.status === "fulfilled" ? result.value.map((address) => ({ address, family: index === 0 ? 4 : 6 })) : []);
12358
+ }
12359
+ }
12360
+ var systemResolve = resolveDnsAddresses;
12361
+ async function resolvePublic(host, resolve10 = systemResolve) {
12362
+ const family = isIP(host);
12363
+ const addresses = family ? [{ address: host, family }] : await resolve10(host);
12364
+ if (addresses.length === 0 || !addresses.every((a) => isPublicAddress(a.address))) {
12365
+ throw new EndpointRefusal(`${host} does not resolve to public addresses only`);
12366
+ }
12367
+ return addresses[0];
12368
+ }
12369
+ function minerPeerFetch(endpoint, dynamic, before = () => {
12370
+ }, resolve10 = systemResolve, send2 = request) {
12371
+ return async (input, init = {}) => {
12372
+ before();
12373
+ const url = new URL(String(input));
12374
+ if (url.origin !== new URL(endpoint).origin) throw new EndpointRefusal("miner request escaped its origin");
12375
+ if (!dynamic) return fetch(input, { ...init, redirect: "error" });
12376
+ if (url.protocol !== "https:" || url.username || url.password) throw new EndpointRefusal("dynamic miner requires HTTPS");
12377
+ const signal = init.signal ?? AbortSignal.timeout(3e4);
12378
+ const pinned = await new Promise((accept, reject) => {
12379
+ const abort = () => reject(signal.reason);
12380
+ signal.addEventListener("abort", abort, { once: true });
12381
+ resolvePublic(url.hostname.replace(/^\[|\]$/g, ""), resolve10).then(accept, reject).finally(() => signal.removeEventListener("abort", abort));
12382
+ if (signal.aborted) abort();
12383
+ });
12384
+ before();
12385
+ signal.throwIfAborted();
12386
+ return new Promise((accept, reject) => {
12387
+ const req = send2(url, {
12388
+ method: init.method ?? "GET",
12389
+ headers: Object.fromEntries(new Headers(init.headers).entries()),
12390
+ signal,
12391
+ agent: false,
12392
+ lookup: (_host, options, callback) => {
12393
+ if (options.all) callback(null, [pinned]);
12394
+ else callback(null, pinned.address, pinned.family);
12395
+ }
12396
+ }, (res) => {
12397
+ const status = res.statusCode;
12398
+ if (status >= 300 && status < 400) {
12399
+ res.destroy();
12400
+ req.destroy();
12401
+ reject(new EndpointRefusal("miner redirect refused"));
12402
+ return;
12403
+ }
12404
+ const headers = new Headers();
12405
+ for (const [name, value] of Object.entries(res.headers)) {
12406
+ if (value !== void 0) headers.set(name, Array.isArray(value) ? value.join(", ") : value);
12407
+ }
12408
+ accept(new Response([204, 205].includes(status) ? null : Readable.toWeb(res), { status, headers }));
12409
+ });
12410
+ req.on("error", (error) => reject(signal.aborted ? signal.reason : error));
12411
+ req.end(init.body);
12412
+ });
12413
+ };
12414
+ }
12415
+
11888
12416
  // src/commands/gateway-renew.ts
11889
- import { Command as Command47 } from "commander";
12417
+ import { Command as Command48 } from "commander";
11890
12418
 
11891
12419
  // src/commands/gateway-tools-qualification.ts
11892
12420
  import { decodeAddress } from "@polkadot/util-crypto";
@@ -12150,8 +12678,8 @@ function tailnetNodeRef(source) {
12150
12678
  return match ? { host: match[1], egress: match[2], asn: match[3] } : null;
12151
12679
  }
12152
12680
  function readTailscaleStatus(socketPath) {
12153
- return new Promise((resolve9, reject) => {
12154
- const req = request({
12681
+ return new Promise((resolve10, reject) => {
12682
+ const req = request2({
12155
12683
  socketPath,
12156
12684
  path: "/localapi/v0/status",
12157
12685
  headers: { Host: "local-tailscaled.sock", "Sec-Tailscale": "localapi" },
@@ -12165,7 +12693,7 @@ function readTailscaleStatus(socketPath) {
12165
12693
  res.on("end", () => {
12166
12694
  if (res.statusCode !== 200) return reject(new Error(`tailscaled status returned HTTP ${res.statusCode}`));
12167
12695
  try {
12168
- resolve9(JSON.parse(body));
12696
+ resolve10(JSON.parse(body));
12169
12697
  } catch {
12170
12698
  reject(new Error("tailscaled status is not JSON"));
12171
12699
  }
@@ -12253,6 +12781,12 @@ async function renewGatewayEvidence(miners, records, observe) {
12253
12781
  const node = tailnetNodeRef(declared.source);
12254
12782
  let provider;
12255
12783
  let session;
12784
+ if (miner.admissionExpiresAt) {
12785
+ await observe.miner(miner);
12786
+ if (Date.parse(miner.admissionExpiresAt) <= observe.now()) throw new Error("miner admission expired");
12787
+ observed.push(miner);
12788
+ continue;
12789
+ }
12256
12790
  if (node) {
12257
12791
  session = await observe.miner(miner);
12258
12792
  provider = await observe.tailnet(node);
@@ -12373,12 +12907,12 @@ async function writeEvidenceFile(path5, evidence, appendQualification = false) {
12373
12907
  evidence = { ...prior, generated_at: evidence.generated_at, conformance: [...conformance, ...latest.values()] };
12374
12908
  }
12375
12909
  parseGatewayEvidence(evidence);
12376
- await writeFile12(temporary, `${JSON.stringify(evidence, null, 2)}
12910
+ await writeFile13(temporary, `${JSON.stringify(evidence, null, 2)}
12377
12911
  `, { mode: 420 });
12378
12912
  await rename9(temporary, path5);
12379
12913
  } finally {
12380
12914
  await lock.close();
12381
- await rm4(`${path5}.lock`);
12915
+ await rm5(`${path5}.lock`);
12382
12916
  }
12383
12917
  }
12384
12918
  async function finishRenewal(report, out, output) {
@@ -12467,11 +13001,75 @@ async function waitForSettlementAllowance(api, channelId, aggregateGn, deadline)
12467
13001
  }
12468
13002
  throw new Error("settlement allowance did not arrive before the probe deadline");
12469
13003
  }
13004
+ async function renewalMinerInfo(api, miner, fetchImpl = fetch) {
13005
+ const head = await api.rpc.chain.getFinalizedHead();
13006
+ const at = await api.at(head);
13007
+ const registered = await at.query.minerStaking.miners(miner.minerAddress);
13008
+ const active = registered?.isSome && registered.unwrap().isActive;
13009
+ 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);
13011
+ const cap = encoded.toU8a(true);
13012
+ 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
+ const identity = {
13014
+ ...miner.modelIdentity,
13015
+ modelHash: bytes3(miner.modelIdentity.modelHash),
13016
+ decodePolicyHash: bytes3(miner.modelIdentity.decodePolicyHash),
13017
+ measuredRoot: bytes3(miner.modelIdentity.measuredRoot)
13018
+ };
13019
+ if (!miner.channelSigned) {
13020
+ const anonymous = await fetchImpl(`${miner.endpoint.replace(/\/$/, "")}/session/info`, { redirect: "error", signal: AbortSignal.timeout(3e4) });
13021
+ await anonymous.body?.cancel();
13022
+ if (anonymous.status !== 401) throw new Error("miner /session/info does not enforce authentication");
13023
+ }
13024
+ const info = miner.channelSigned ? await computeChannel6.fetchProvenSessionInfo(miner.endpoint, {
13025
+ genesisHash: api.genesisHash.toU8a(),
13026
+ account: decodeAddress2(miner.minerAddress),
13027
+ minerAddress: miner.minerAddress,
13028
+ enclaveKey: bytes3(miner.enclaveKey),
13029
+ modelIdentity: identity
13030
+ }, fetchImpl, AbortSignal.timeout(3e4)) : await computeChannel6.fetchMinerSessionInfo(
13031
+ miner.endpoint,
13032
+ { Authorization: `Bearer ${miner.authToken}` },
13033
+ AbortSignal.timeout(3e4),
13034
+ fetchImpl
13035
+ );
13036
+ requireWorkerMinimum(info.image?.version, info.image?.revision);
13037
+ const wire = {
13038
+ ...info,
13039
+ is_ready: info.isReady,
13040
+ enclaveKey: hex3(info.enclaveKey),
13041
+ modelHash: hex3(info.modelHash),
13042
+ modelIdentity: {
13043
+ ...info.modelIdentity,
13044
+ modelHash: hex3(info.modelIdentity.modelHash),
13045
+ decodePolicyHash: hex3(info.modelIdentity.decodePolicyHash),
13046
+ measuredRoot: hex3(info.modelIdentity.measuredRoot)
13047
+ }
13048
+ };
13049
+ const snapshot = await readChainSnapshot(api, wire, head.toHex());
13050
+ if (miner.channelSigned && snapshot.calibration) wire.calibration = {
13051
+ ...snapshot.calibration,
13052
+ ready: true,
13053
+ state: "active",
13054
+ currentBlock: snapshot.finalizedBlock
13055
+ };
13056
+ assertMinerMatchesChain(wire, snapshot);
13057
+ if (wire.minerAddress !== miner.minerAddress || wire.enclaveKey !== miner.enclaveKey || Object.keys(miner.modelIdentity).some((field2) => wire.modelIdentity[field2] !== miner.modelIdentity[field2])) throw new Error("miner no longer advertises its allowlisted identity");
13058
+ return { ...info, calibration: wire.calibration };
13059
+ }
13060
+ function renewalAdmissions(env, chainPriced) {
13061
+ if (!env.GATEWAY_ADMISSIONS_FILE) return void 0;
13062
+ const maxBudgetFlop = Number(env.GATEWAY_ADMISSION_MAX_BUDGET_FLOP), maxDurationBlocks = Number(env.GATEWAY_ADMISSION_MAX_DURATION_BLOCKS);
13063
+ if (!chainPriced || !Number.isFinite(maxBudgetFlop) || maxBudgetFlop <= 0 || !Number.isSafeInteger(maxDurationBlocks) || maxDurationBlocks <= 0) throw new Error("dynamic admission requires chain pricing and explicit gateway budget/duration policy");
13064
+ return new MinerAdmissions(env.GATEWAY_ADMISSIONS_FILE, { maxBudgetFlop, maxDurationBlocks });
13065
+ }
12470
13066
  async function probeToolsConformance(input) {
12471
13067
  const { api, chain, miner, record } = input;
12472
13068
  const deadline = Date.now() + PROBE_TIMEOUT_MS;
12473
- const headers = { Authorization: `Bearer ${miner.authToken}` };
12474
- const info = await computeChannel6.fetchMinerSessionInfo(miner.endpoint, headers, AbortSignal.timeout(3e4));
13069
+ const fetchImpl = minerPeerFetch(miner.endpoint, !!miner.admissionExpiresAt, input.assertAdmission);
13070
+ const headers = miner.channelSigned ? {} : { Authorization: `Bearer ${miner.authToken}` };
13071
+ const info = await renewalMinerInfo(api, miner, fetchImpl);
13072
+ let requestHeaders;
12475
13073
  const pinned = miner.modelIdentity;
12476
13074
  const advertised = info.modelIdentity;
12477
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) {
@@ -12493,6 +13091,8 @@ async function probeToolsConformance(input) {
12493
13091
  let sequence = 0;
12494
13092
  const transport = new computeChannel6.HttpSessionTransport(miner.endpoint, PROBE_TIMEOUT_MS, {
12495
13093
  headers,
13094
+ fetchImpl,
13095
+ requestHeaders: (method, path5, body) => requestHeaders?.(method, path5, body) ?? {},
12496
13096
  onFrame: (frame) => {
12497
13097
  if (frame.sequence !== sequence++) throw new Error("tools probe frame sequence mismatch");
12498
13098
  if (frame.type === "chunk") frames.push({ kind: frame.kind, data: bytes3(frame.data) });
@@ -12500,6 +13100,7 @@ async function probeToolsConformance(input) {
12500
13100
  });
12501
13101
  const agentKey = computeChannel6.SessionKey.generate();
12502
13102
  const identity = { ...pinned, modelHash: bytes3(pinned.modelHash), decodePolicyHash: bytes3(pinned.decodePolicyHash), measuredRoot: bytes3(pinned.measuredRoot) };
13103
+ input.assertAdmission?.();
12503
13104
  const session = await computeChannel6.ChannelSession.open({
12504
13105
  chain,
12505
13106
  transport,
@@ -12526,7 +13127,15 @@ async function probeToolsConformance(input) {
12526
13127
  let verdict;
12527
13128
  let receipt;
12528
13129
  try {
12529
- await computeChannel6.notifyMinerOpen(miner.endpoint, session.channelId, agentKey.publicKey, headers, AbortSignal.timeout(3e4));
13130
+ if (miner.channelSigned) requestHeaders = await computeChannel6.channelSessionAuth(
13131
+ miner.endpoint,
13132
+ session.channelId,
13133
+ agentKey,
13134
+ info,
13135
+ fetchImpl,
13136
+ AbortSignal.timeout(3e4)
13137
+ );
13138
+ await computeChannel6.notifyMinerOpen(miner.endpoint, session.channelId, agentKey.publicKey, headers, AbortSignal.timeout(3e4), requestHeaders, fetchImpl);
12530
13139
  receipt = await session.chatTurn({
12531
13140
  version,
12532
13141
  model: record.openrouter_slug,
@@ -12549,7 +13158,7 @@ async function probeToolsConformance(input) {
12549
13158
  try {
12550
13159
  await waitForSettlementAllowance(api, session.channelId, receipt.gN, deadline);
12551
13160
  const settled = await session.closeCooperative(async (claim) => {
12552
- const wire = await computeChannel6.notifyMinerClose(miner.endpoint, claim, headers, AbortSignal.timeout(3e4));
13161
+ const wire = await computeChannel6.notifyMinerClose(miner.endpoint, claim, headers, AbortSignal.timeout(3e4), requestHeaders, fetchImpl);
12553
13162
  return wire ? {
12554
13163
  channelId: bytes3(wire.channelId),
12555
13164
  minerPaid: BigInt(wire.minerPaid),
@@ -12584,7 +13193,7 @@ function envSecret(name) {
12584
13193
  return value;
12585
13194
  }
12586
13195
  function gatewayRenewCmd() {
12587
- return new Command47("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) => {
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) => {
12588
13197
  let client;
12589
13198
  try {
12590
13199
  if (opts.catalogPricing !== "operator" && opts.catalogPricing !== "chain") {
@@ -12595,7 +13204,7 @@ function gatewayRenewCmd() {
12595
13204
  throw new Error("receipt-only mode requires the receipt/hash and cannot run direct probes");
12596
13205
  }
12597
13206
  const raw = await readFile11(opts.qualificationReceipt);
12598
- if (raw.length > 8e6 || createHash5("sha256").update(raw).digest("hex") !== opts.qualificationReceiptSha256) throw new Error("qualification receipt bytes differ");
13207
+ if (raw.length > 8e6 || createHash6("sha256").update(raw).digest("hex") !== opts.qualificationReceiptSha256) throw new Error("qualification receipt bytes differ");
12599
13208
  const value = JSON.parse(raw.toString("utf8"));
12600
13209
  const records2 = parseCanonicalCatalog(envJson(opts.catalogEnv), { chainPriced: opts.catalogPricing === "chain" });
12601
13210
  const record = records2.find((item) => item.openrouter_slug === value.model);
@@ -12612,7 +13221,12 @@ function gatewayRenewCmd() {
12612
13221
  return;
12613
13222
  }
12614
13223
  if (!opts.allowlistEnv || !opts.providerKeyEnv || !opts.probeSignerEnv || !opts.probeEscrow) throw new Error("direct renewal requires allowlist, provider, signer and escrow options");
12615
- const miners = envJson(opts.allowlistEnv);
13224
+ const staticMiners = envJson(opts.allowlistEnv);
13225
+ const admissions = renewalAdmissions(process.env, opts.catalogPricing === "chain");
13226
+ const handoffs = process.env.GATEWAY_STATIC_HANDOFFS_FILE?.trim();
13227
+ if (handoffs && !admissions) throw new Error("static handoffs require dynamic admissions");
13228
+ const routing = new MinerRouting(staticMiners, handoffs);
13229
+ const miners = routing.read(admissions?.read() ?? []);
12616
13230
  if (!Array.isArray(miners) || miners.length === 0) throw new Error(`${opts.allowlistEnv} must list at least one miner`);
12617
13231
  const records = parseCanonicalCatalog(envJson(opts.catalogEnv), { chainPriced: opts.catalogPricing === "chain" });
12618
13232
  const providerKey = envSecret(opts.providerKeyEnv);
@@ -12626,24 +13240,40 @@ function gatewayRenewCmd() {
12626
13240
  await computeChannel6.ready();
12627
13241
  const signer = new Keyring5({ type: "sr25519" }).addFromUri(signerUri);
12628
13242
  const chain = new computeChannel6.ComputeChannelChainClient(new computeChannel6.FlopChainSubmitter(api, signer, true));
13243
+ const assertAdmission = (miner) => {
13244
+ if (!miner.admissionExpiresAt) return;
13245
+ const dynamic = admissions?.read() ?? [];
13246
+ const matches = (item) => !!item && item.enclaveKey === miner.enclaveKey && item.endpoint === miner.endpoint && JSON.stringify(item.modelIdentity) === JSON.stringify(miner.modelIdentity);
13247
+ if (!matches(dynamic.find((item) => minerSlot(item) === minerSlot(miner))) || !matches(routing.read(dynamic).find((item) => minerSlot(item) === minerSlot(miner)))) throw new Error("miner admission expired or key revoked");
13248
+ };
12629
13249
  const report = await renewGatewayEvidence(miners, records, {
12630
13250
  now: Date.now,
12631
13251
  provider: (ref) => observeGpuAiInstance(ref, providerKey),
12632
13252
  tailnet: tailnetObserver(opts.tailscaleSocket),
12633
13253
  miner: async (miner) => {
12634
- const info = await fetchMinerSessionInfo(miner.endpoint, miner.authToken, AbortSignal.timeout(3e4));
13254
+ const proven = await renewalMinerInfo(api, miner, minerPeerFetch(miner.endpoint, !!miner.admissionExpiresAt, () => assertAdmission(miner)));
13255
+ const info = {
13256
+ ...proven,
13257
+ is_ready: proven.isReady,
13258
+ enclaveKey: hex3(proven.enclaveKey),
13259
+ modelHash: hex3(proven.modelHash),
13260
+ modelIdentity: {
13261
+ ...proven.modelIdentity,
13262
+ modelHash: hex3(proven.modelIdentity.modelHash),
13263
+ decodePolicyHash: hex3(proven.modelIdentity.decodePolicyHash),
13264
+ measuredRoot: hex3(proven.modelIdentity.measuredRoot)
13265
+ }
13266
+ };
12635
13267
  const pinned = miner.modelIdentity;
12636
13268
  const advertised = info.modelIdentity;
12637
13269
  if (info.minerAddress !== miner.minerAddress || info.enclaveKey?.toLowerCase() !== miner.enclaveKey.toLowerCase() || !advertised || Object.keys(pinned).some((key) => advertised[key] !== pinned[key])) {
12638
13270
  throw new Error("miner no longer advertises its allowlisted identity");
12639
13271
  }
12640
- const snapshot = await readChainSnapshot(api, info);
12641
- assertMinerMatchesChain(info, snapshot);
12642
- return `authenticated /session/info matches its pin and finalized block ${snapshot.finalizedBlock}`;
13272
+ return "authenticated session identity matches its pin and finalized registry/calibration";
12643
13273
  },
12644
13274
  tools: async (miner, record) => {
12645
13275
  await assertProbeSignerFunded(api, signer.address, escrow);
12646
- return probeToolsConformance({ api, chain, miner, record, escrow });
13276
+ return probeToolsConformance({ api, chain, miner, record, escrow, assertAdmission: () => assertAdmission(miner) });
12647
13277
  }
12648
13278
  });
12649
13279
  const exitCode = await finishRenewal(report, opts.out, getJson(command));
@@ -12657,9 +13287,9 @@ function gatewayRenewCmd() {
12657
13287
  }
12658
13288
 
12659
13289
  // src/release-check.ts
12660
- import { mkdir as mkdir13, readFile as readFile12, rename as rename10, writeFile as writeFile13 } from "node:fs/promises";
13290
+ import { mkdir as mkdir14, readFile as readFile12, rename as rename10, writeFile as writeFile14 } from "node:fs/promises";
12661
13291
  import { homedir as homedir5 } from "node:os";
12662
- import { dirname as dirname7, join as join9 } from "node:path";
13292
+ import { dirname as dirname7, join as join10 } from "node:path";
12663
13293
  var DAY_MS = 24 * 60 * 60 * 1e3;
12664
13294
  var READ_ONLY = /* @__PURE__ */ new Set([
12665
13295
  "health",
@@ -12703,7 +13333,7 @@ async function checkRelease(options) {
12703
13333
  if (options.offline || (options.testing ?? (process.env.NODE_ENV === "test" || !!process.env.VITEST))) return;
12704
13334
  const warn = options.warn ?? ((message) => console.error(message));
12705
13335
  const now = options.now ?? Date.now();
12706
- const cacheFile = options.cacheFile ?? join9(process.env.XDG_CACHE_HOME || join9(homedir5(), ".cache"), "flop", "releases.json");
13336
+ const cacheFile = options.cacheFile ?? join10(process.env.XDG_CACHE_HOME || join10(homedir5(), ".cache"), "flop", "releases.json");
12707
13337
  let releases;
12708
13338
  try {
12709
13339
  const cached = JSON.parse(await readFile12(cacheFile, "utf8"));
@@ -12718,9 +13348,9 @@ async function checkRelease(options) {
12718
13348
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
12719
13349
  releases = parseOperatorReleases(await response.json());
12720
13350
  try {
12721
- await mkdir13(dirname7(cacheFile), { recursive: true });
13351
+ await mkdir14(dirname7(cacheFile), { recursive: true });
12722
13352
  const temporary = `${cacheFile}.${process.pid}.tmp`;
12723
- await writeFile13(temporary, JSON.stringify({ fetchedAt: now, releases }), { mode: 384 });
13353
+ await writeFile14(temporary, JSON.stringify({ fetchedAt: now, releases }), { mode: 384 });
12724
13354
  await rename10(temporary, cacheFile);
12725
13355
  } catch {
12726
13356
  warn("warning: could not cache operator releases");
@@ -12745,10 +13375,10 @@ async function checkRelease(options) {
12745
13375
  }
12746
13376
 
12747
13377
  // src/cli.ts
12748
- var program = new Command48();
13378
+ var program = new Command49();
12749
13379
  var outputMode = outputModeFromArgv(process.argv.slice(2));
12750
13380
  setOutputMode(outputMode);
12751
- program.name("flop").description("FLOP Network CLI").version(operatorVersion(JSON.parse(readFileSync2(join10(packageRoot, "package.json"), "utf8")).version)).option("--offline", "Skip the operator release check").addOption(new Option7("--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 Option7("--output <mode>", "Output mode").choices(["human", "json", "agent"]).default("human")).option("--json", "Output as JSON");
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");
12752
13382
  program.addCommand(healthCmd());
12753
13383
  program.addCommand(balanceCmd());
12754
13384
  program.addCommand(gpuCmd());
@@ -12760,13 +13390,13 @@ program.addCommand(chatCmd());
12760
13390
  program.addCommand(agentCmd());
12761
13391
  program.addCommand(minerCmd());
12762
13392
  program.addCommand(validatorCmd());
12763
- var networkCmd = new Command48("network").description("Resolve and inspect network profiles");
13393
+ var networkCmd = new Command49("network").description("Resolve and inspect network profiles");
12764
13394
  networkCmd.addCommand(networkStatusCmd());
12765
13395
  program.addCommand(networkCmd);
12766
- var faucetCmd = new Command48("faucet").description("Request dev/test FLOP");
13396
+ var faucetCmd = new Command49("faucet").description("Request dev/test FLOP");
12767
13397
  faucetCmd.addCommand(faucetRequestCmd());
12768
13398
  program.addCommand(faucetCmd);
12769
- var sessionCmd = new Command48("session").description("Manage session keys");
13399
+ var sessionCmd = new Command49("session").description("Manage session keys");
12770
13400
  sessionCmd.addCommand(sessionRunCmd());
12771
13401
  sessionCmd.addCommand(sessionListCmd());
12772
13402
  sessionCmd.addCommand(sessionCreateCmd());
@@ -12774,7 +13404,7 @@ sessionCmd.addCommand(sessionInfoCmd());
12774
13404
  sessionCmd.addCommand(sessionRevokeCmd());
12775
13405
  sessionCmd.addCommand(sessionLimitsCmd());
12776
13406
  program.addCommand(sessionCmd);
12777
- var keysCmd = new Command48("keys").description("Manage keys");
13407
+ var keysCmd = new Command49("keys").description("Manage keys");
12778
13408
  keysCmd.addCommand(keysGenerateCmd());
12779
13409
  keysCmd.addCommand(keysInspectCmd());
12780
13410
  keysCmd.addCommand(keysHardwareCmd());
@@ -12782,44 +13412,45 @@ keysCmd.addCommand(keysImportCmd());
12782
13412
  keysCmd.addCommand(keysExportCmd());
12783
13413
  keysCmd.addCommand(keysClaudeProfileCmd());
12784
13414
  program.addCommand(keysCmd);
12785
- var walletCmd = new Command48("wallet").description("Manage wallets");
12786
- var agentWalletCmd = new Command48("agent").description("Agent wallet operations");
13415
+ var walletCmd = new Command49("wallet").description("Manage wallets");
13416
+ var agentWalletCmd = new Command49("agent").description("Agent wallet operations");
12787
13417
  agentWalletCmd.addCommand(walletAgentCreateCmd());
12788
13418
  walletCmd.addCommand(agentWalletCmd);
12789
13419
  program.addCommand(walletCmd);
12790
- var txCmd = new Command48("tx").description("Build, sign, and broadcast transactions");
13420
+ var txCmd = new Command49("tx").description("Build, sign, and broadcast transactions");
12791
13421
  txCmd.addCommand(txBuildCmd());
12792
13422
  txCmd.addCommand(txBroadcastCmd());
12793
13423
  program.addCommand(txCmd);
12794
13424
  program.addCommand(multisigCmd());
12795
13425
  program.addCommand(channelCmd());
12796
- var gatewayCmd = new Command48("gateway").description("Generate verified gateway configuration");
13426
+ var gatewayCmd = new Command49("gateway").description("Generate verified gateway configuration");
12797
13427
  gatewayCmd.addCommand(gatewayAllowlistCmd());
12798
13428
  gatewayCmd.addCommand(gatewayRenewCmd());
12799
13429
  program.addCommand(gatewayCmd);
12800
- var x402Cmd = new Command48("x402").description("x402 payment gateway operations");
13430
+ var x402Cmd = new Command49("x402").description("x402 payment gateway operations");
12801
13431
  x402Cmd.addCommand(x402SettleCmd());
12802
13432
  program.addCommand(x402Cmd);
12803
- var daCmd = new Command48("da").description("Sovereign DA registry (leases, deposits)");
13433
+ var daCmd = new Command49("da").description("Sovereign DA registry (leases, deposits)");
12804
13434
  daCmd.addCommand(daDepositCmd());
12805
13435
  daCmd.addCommand(daRegisterCmd());
12806
13436
  daCmd.addCommand(daTopupCmd());
12807
13437
  daCmd.addCommand(daWithdrawCmd());
12808
13438
  program.addCommand(daCmd);
12809
- var modelCmd = new Command48("model").description("Model registry operations");
13439
+ var modelCmd = new Command49("model").description("Model registry operations");
12810
13440
  modelCmd.addCommand(modelListCmd());
12811
13441
  modelCmd.addCommand(modelGetCmd());
12812
13442
  modelCmd.addCommand(modelFetchCmd());
12813
13443
  modelCmd.addCommand(modelUploadCmd());
12814
13444
  modelCmd.addCommand(modelPreflightCmd());
12815
13445
  modelCmd.addCommand(modelOnboardCmd());
13446
+ modelCmd.addCommand(modelBackupCmd());
12816
13447
  modelCmd.addCommand(modelAuditCmd());
12817
13448
  program.addCommand(modelCmd);
12818
13449
  program.hook("preAction", async (_root, action) => {
12819
13450
  const path5 = [];
12820
13451
  for (let command = action; command && command !== program; command = command.parent) path5.unshift(command.name());
12821
13452
  await checkRelease({
12822
- version: operatorVersion(JSON.parse(readFileSync2(join10(packageRoot, "package.json"), "utf8")).version),
13453
+ version: operatorVersion(JSON.parse(readFileSync2(join11(packageRoot, "package.json"), "utf8")).version),
12823
13454
  readOnly: isReadOnlyCommand(path5),
12824
13455
  offline: program.opts().offline,
12825
13456
  image: !!process.env.FLOP_IMAGE_VERSION