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