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