@flop-labs/cli 0.4.2 → 0.4.4
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 +18 -0
- package/dist/cli.js +145 -63
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,24 @@ and this package follows [Semantic Versioning](https://semver.org/spec/v2.0.0.ht
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.4] - 2026-11-10
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Gateway tools probes now reconcile ambiguous cooperative closes against finalized
|
|
15
|
+
channel events without submitting a second close.
|
|
16
|
+
- Model backup keeps download receipts beside `--model-dir` unless `--state-dir`
|
|
17
|
+
selects another location on the same filesystem; metadata-only onboarding now
|
|
18
|
+
passes the registered precision to the suggested backup command.
|
|
19
|
+
|
|
20
|
+
## [0.4.3] - 2026-11-09
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- Package with SDK 0.5.2 as the minimum compatible SDK, including canonical session
|
|
25
|
+
pallet aliases. The 30-second challenge and 120-second enrollment deadlines
|
|
26
|
+
shipped in 0.4.2 are unchanged; packed offline regressions cover those paths.
|
|
27
|
+
|
|
10
28
|
## [0.4.2] - 2026-11-09
|
|
11
29
|
|
|
12
30
|
### Added
|
package/dist/cli.js
CHANGED
|
@@ -6462,15 +6462,15 @@ function ownerOnly(mode) {
|
|
|
6462
6462
|
}
|
|
6463
6463
|
async function privateDirectory(path5) {
|
|
6464
6464
|
await mkdir5(path5, { recursive: true, mode: 448 });
|
|
6465
|
-
const
|
|
6466
|
-
if (!
|
|
6465
|
+
const stat2 = await lstat(path5);
|
|
6466
|
+
if (!stat2.isDirectory() || !ownerOnly(stat2.mode) || process.getuid && stat2.uid !== process.getuid()) {
|
|
6467
6467
|
throw new Error("FLOP credential directory must be owned by this user with mode 0700");
|
|
6468
6468
|
}
|
|
6469
6469
|
}
|
|
6470
6470
|
async function cachedCredential(path5, gateway) {
|
|
6471
6471
|
try {
|
|
6472
|
-
const
|
|
6473
|
-
if (!
|
|
6472
|
+
const stat2 = await lstat(path5);
|
|
6473
|
+
if (!stat2.isFile() || !ownerOnly(stat2.mode) || process.getuid && stat2.uid !== process.getuid()) {
|
|
6474
6474
|
throw new Error("FLOP credential cache must be an owner-only regular file (mode 0600)");
|
|
6475
6475
|
}
|
|
6476
6476
|
const cached = JSON.parse(await readFile3(path5, "utf8"));
|
|
@@ -6551,8 +6551,8 @@ async function enrollmentWallet(path5) {
|
|
|
6551
6551
|
await cryptoWaitReady4();
|
|
6552
6552
|
const keyring = new Keyring4({ type: "sr25519", ss58Format: 42 });
|
|
6553
6553
|
try {
|
|
6554
|
-
const
|
|
6555
|
-
if (!
|
|
6554
|
+
const stat2 = await lstat(path5);
|
|
6555
|
+
if (!stat2.isFile() || !ownerOnly(stat2.mode) || process.getuid && stat2.uid !== process.getuid()) {
|
|
6556
6556
|
throw new Error("FLOP devnet wallet must be an owner-only regular file (mode 0600)");
|
|
6557
6557
|
}
|
|
6558
6558
|
const stored = JSON.parse(await readFile3(path5, "utf8"));
|
|
@@ -6760,8 +6760,8 @@ async function savePublicProfile(model, key) {
|
|
|
6760
6760
|
const path5 = publicHermesProfilePath(model, key);
|
|
6761
6761
|
for (const directory of [join2(homedir2(), ".flop"), publicHermesProfileRoot(), path5]) {
|
|
6762
6762
|
await mkdir6(directory, { recursive: true, mode: 448 });
|
|
6763
|
-
const
|
|
6764
|
-
if (!
|
|
6763
|
+
const stat2 = await lstat2(directory);
|
|
6764
|
+
if (!stat2.isDirectory() || process.getuid && stat2.uid !== process.getuid() || process.platform !== "win32" && (stat2.mode & 63) !== 0) {
|
|
6765
6765
|
throw new Error("FLOP Hermes profile and parent directories must be owner-only regular directories");
|
|
6766
6766
|
}
|
|
6767
6767
|
}
|
|
@@ -7707,12 +7707,12 @@ async function writeModelFiles(outputDir, files) {
|
|
|
7707
7707
|
}
|
|
7708
7708
|
async function readLocalModelFiles(path5) {
|
|
7709
7709
|
const root = resolve5(path5);
|
|
7710
|
-
const
|
|
7711
|
-
if (
|
|
7712
|
-
if (
|
|
7710
|
+
const stat2 = await lstat3(root);
|
|
7711
|
+
if (stat2.isSymbolicLink()) throw new Error(`model source must not be a symlink: ${path5}`);
|
|
7712
|
+
if (stat2.isFile()) {
|
|
7713
7713
|
return [{ path: root.split(sep).pop() ?? "weights.bin", bytes: await readFile4(root) }];
|
|
7714
7714
|
}
|
|
7715
|
-
if (!
|
|
7715
|
+
if (!stat2.isDirectory()) throw new Error(`model source is not a file or directory: ${path5}`);
|
|
7716
7716
|
const files = [];
|
|
7717
7717
|
async function walk(dir) {
|
|
7718
7718
|
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
@@ -7899,8 +7899,8 @@ function onboardingDirectory(binding, override) {
|
|
|
7899
7899
|
return override ? resolve6(override) : join3(process.env.XDG_STATE_HOME ?? join3(homedir3(), ".local/state"), "flop/model-onboarding", digest(JSON.stringify(binding)));
|
|
7900
7900
|
}
|
|
7901
7901
|
async function hashFile(path5) {
|
|
7902
|
-
const
|
|
7903
|
-
if (!
|
|
7902
|
+
const stat2 = await lstat4(path5);
|
|
7903
|
+
if (!stat2.isFile() || stat2.isSymbolicLink()) throw new Error("onboarding artifact must be a regular file");
|
|
7904
7904
|
const hash2 = createHash2("sha256");
|
|
7905
7905
|
for await (const chunk of createReadStream(path5)) hash2.update(chunk);
|
|
7906
7906
|
return hash2.digest("hex");
|
|
@@ -8077,8 +8077,8 @@ var privateJson = (value) => JSON.stringify(
|
|
|
8077
8077
|
(_k, v) => typeof v === "bigint" ? v.toString() : v instanceof Uint8Array ? hex(v) : v
|
|
8078
8078
|
);
|
|
8079
8079
|
async function readPrivateJson(path5) {
|
|
8080
|
-
const
|
|
8081
|
-
if (!
|
|
8080
|
+
const stat2 = await lstat5(path5);
|
|
8081
|
+
if (!stat2.isFile() || stat2.isSymbolicLink() || (stat2.mode & 63) !== 0)
|
|
8082
8082
|
throw new Error("replay files must be private regular files (0600)");
|
|
8083
8083
|
return JSON.parse(await readFile6(path5, "utf8"));
|
|
8084
8084
|
}
|
|
@@ -8467,6 +8467,9 @@ function activeHybridProfile(profile) {
|
|
|
8467
8467
|
const image = optionValue(profile.imageId);
|
|
8468
8468
|
return image !== null && codecHash(image) !== `0x${"00".repeat(32)}` && profile.epoch.toString() !== "0";
|
|
8469
8469
|
}
|
|
8470
|
+
function modelBackupHandoff(modelHash, precision, revision) {
|
|
8471
|
+
return `flop model backup ${modelHash} --model-dir <path> --precision ${precision}${revision ? ` --revision ${revision}` : ""}`;
|
|
8472
|
+
}
|
|
8470
8473
|
function normalizePrecision(raw) {
|
|
8471
8474
|
const upper = raw.trim().toUpperCase();
|
|
8472
8475
|
const precision = PRECISIONS.find((candidate) => candidate.toUpperCase() === upper);
|
|
@@ -8632,10 +8635,10 @@ async function exists(path5) {
|
|
|
8632
8635
|
async function materializeSource(raw, outputDir, token, sourceRevision, resumeState, files) {
|
|
8633
8636
|
const local = await exists(raw);
|
|
8634
8637
|
if (local) {
|
|
8635
|
-
const
|
|
8636
|
-
if (
|
|
8637
|
-
const modelDir =
|
|
8638
|
-
if (!
|
|
8638
|
+
const stat2 = await lstat6(raw);
|
|
8639
|
+
if (stat2.isSymbolicLink()) throw new Error(`model source must not be a symlink: ${raw}`);
|
|
8640
|
+
const modelDir = stat2.isDirectory() ? resolve7(raw) : outputDir;
|
|
8641
|
+
if (!stat2.isDirectory()) {
|
|
8639
8642
|
await writeModelFiles(outputDir, await readLocalModelFiles(raw));
|
|
8640
8643
|
}
|
|
8641
8644
|
const name = basename(resolve7(raw));
|
|
@@ -9352,7 +9355,7 @@ function modelOnboardCmd() {
|
|
|
9352
9355
|
if (metadataOnly && !state.weightsRef) {
|
|
9353
9356
|
process.stderr.write(
|
|
9354
9357
|
`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
|
-
|
|
9358
|
+
${modelBackupHandoff(identity.modelHash, precision, preflight?.revision)}
|
|
9356
9359
|
then bind the DataRef it prints. Rerunning this command with --download packs the image from disk instead, which needs about twice the checkpoint's size.
|
|
9357
9360
|
`
|
|
9358
9361
|
);
|
|
@@ -9407,9 +9410,9 @@ function modelUploadCmd() {
|
|
|
9407
9410
|
imagePath = join6(tempDir, "weights.img");
|
|
9408
9411
|
if (opts.path) {
|
|
9409
9412
|
let modelDir = resolve8(opts.path);
|
|
9410
|
-
const
|
|
9411
|
-
if (
|
|
9412
|
-
if (
|
|
9413
|
+
const stat2 = await lstat7(modelDir);
|
|
9414
|
+
if (stat2.isSymbolicLink()) throw new Error(`model source must not be a symlink: ${opts.path}`);
|
|
9415
|
+
if (stat2.isFile()) {
|
|
9413
9416
|
modelDir = join6(tempDir, "model");
|
|
9414
9417
|
await mkdir11(modelDir);
|
|
9415
9418
|
await copyFile(resolve8(opts.path), join6(modelDir, basename2(opts.path)));
|
|
@@ -9555,8 +9558,9 @@ function modelPreflightCmd() {
|
|
|
9555
9558
|
|
|
9556
9559
|
// src/commands/model-backup.ts
|
|
9557
9560
|
import { Command as Command33, Option as Option3 } from "commander";
|
|
9558
|
-
import {
|
|
9559
|
-
import {
|
|
9561
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
9562
|
+
import { mkdir as mkdir12, rm as rm4, stat, writeFile as writeFile10 } from "node:fs/promises";
|
|
9563
|
+
import { dirname as dirname6, join as join8, resolve as resolve9 } from "node:path";
|
|
9560
9564
|
var PRECISIONS3 = ["FP32", "FP16", "BF16", "INT8", "INT4", "Mixed"];
|
|
9561
9565
|
async function finalizedLeasedRef2(api, commitment, size) {
|
|
9562
9566
|
const hash2 = await api.rpc.chain.getFinalizedHead();
|
|
@@ -9572,6 +9576,17 @@ async function awaitFinalizedLeasedRef(api, commitment, size) {
|
|
|
9572
9576
|
await new Promise((wake) => setTimeout(wake, REGISTRATION_POLL_MS));
|
|
9573
9577
|
}
|
|
9574
9578
|
}
|
|
9579
|
+
function downloadStateDir(binding, override) {
|
|
9580
|
+
if (override) return resolve9(override);
|
|
9581
|
+
const key = createHash4("sha256").update(JSON.stringify(binding)).digest("hex");
|
|
9582
|
+
return join8(dirname6(binding.modelDir), `.flop-backup-state-${key.slice(0, 16)}`);
|
|
9583
|
+
}
|
|
9584
|
+
async function assertSameFilesystem(stateDir, modelDir) {
|
|
9585
|
+
if ((await stat(stateDir)).dev === (await stat(modelDir)).dev) return;
|
|
9586
|
+
throw new Error(
|
|
9587
|
+
`download state ${stateDir} is not on the same filesystem as ${modelDir}; pass a --state-dir on that filesystem outside the checkpoint tree (if --model-dir is a mount point, use a subdirectory of it)`
|
|
9588
|
+
);
|
|
9589
|
+
}
|
|
9575
9590
|
function registeredHubRepo(mirrors) {
|
|
9576
9591
|
const repos = [...new Set(mirrors.filter((m) => m.kind === "huggingface").map((m) => m.repo))];
|
|
9577
9592
|
if (repos.length > 1) {
|
|
@@ -9584,7 +9599,7 @@ function registeredHubRepo(mirrors) {
|
|
|
9584
9599
|
function modelBackupCmd() {
|
|
9585
9600
|
return new Command33("backup").description(
|
|
9586
9601
|
"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
|
|
9602
|
+
).argument("<model_hash>", "Registered model hash (0x-hex)").requiredOption("--model-dir <path>", "Directory the checkpoint streams into (needs room for it)").option("--source <hf-id>", "Hugging Face id (owner/model or hf:owner/model); the model's registered mirror when omitted").option("--revision <sha>", "Immutable 40-hex commit; main is resolved to one when omitted").addOption(new Option3("--precision <precision>", "Measured-root precision").choices(PRECISIONS3).default("FP16")).option("--da-validator <path>", "da-validator executable", "da-validator").option("--token <token>", "Hugging Face bearer token (prefer HF_TOKEN)").option("--state-dir <path>", "Download resume receipts, on the checkpoint's filesystem but outside its tree (defaults to beside --model-dir)").option("--dry-run", "Print the plan and the DA deposit it would lock, and stop before any transfer").option("--json", "Output as JSON").action(async (modelHash, opts, command) => {
|
|
9588
9603
|
let client;
|
|
9589
9604
|
let workDir;
|
|
9590
9605
|
try {
|
|
@@ -9637,10 +9652,12 @@ function modelBackupCmd() {
|
|
|
9637
9652
|
workDir = join8(resolve9(modelDir, ".."), `.flop-backup-${modelHash.replace(/^0x/, "").slice(0, 16)}`);
|
|
9638
9653
|
await mkdir12(workDir, { recursive: true, mode: 448 });
|
|
9639
9654
|
await mkdir12(modelDir, { recursive: true });
|
|
9640
|
-
const
|
|
9641
|
-
|
|
9655
|
+
const stateDir = downloadStateDir({ modelHash, modelDir, repo, revision: preflight.revision }, opts.stateDir);
|
|
9656
|
+
await mkdir12(stateDir, { recursive: true, mode: 448 });
|
|
9657
|
+
await assertSameFilesystem(stateDir, modelDir);
|
|
9658
|
+
const resumeState = join8(stateDir, "download.json");
|
|
9642
9659
|
process.stderr.write(
|
|
9643
|
-
`backing up ${repo}@${preflight.revision} (${preflight.files.length} files, image ${imageSize} bytes, ${preflight.daChunks} DA chunk(s)) as ${modelHash}
|
|
9660
|
+
`backing up ${repo}@${preflight.revision} (${preflight.files.length} files, image ${imageSize} bytes, ${preflight.daChunks} DA chunk(s)) as ${modelHash}; download receipts in ${stateDir}
|
|
9644
9661
|
`
|
|
9645
9662
|
);
|
|
9646
9663
|
await sdk.fetchHuggingFaceToDir(repo, modelDir, {
|
|
@@ -10897,7 +10914,7 @@ import { stdin, stdout } from "node:process";
|
|
|
10897
10914
|
|
|
10898
10915
|
// src/conversations.ts
|
|
10899
10916
|
import { mkdir as mkdir13, readFile as readFile8, rename as rename8, writeFile as writeFile11 } from "node:fs/promises";
|
|
10900
|
-
import { dirname as
|
|
10917
|
+
import { dirname as dirname7 } from "node:path";
|
|
10901
10918
|
var MAX_CONVERSATIONS = 100;
|
|
10902
10919
|
var MAX_CONVERSATION_MESSAGES = 100;
|
|
10903
10920
|
var MAX_MESSAGE_CHARS = 16384;
|
|
@@ -10916,7 +10933,7 @@ var ConversationStore = class {
|
|
|
10916
10933
|
}
|
|
10917
10934
|
}
|
|
10918
10935
|
async write(data) {
|
|
10919
|
-
await mkdir13(
|
|
10936
|
+
await mkdir13(dirname7(this.path), { recursive: true, mode: 448 });
|
|
10920
10937
|
const temp = `${this.path}.${process.pid}.tmp`;
|
|
10921
10938
|
await writeFile11(temp, `${JSON.stringify(data, null, 2)}
|
|
10922
10939
|
`, { mode: 384 });
|
|
@@ -11940,7 +11957,7 @@ function gatewayAllowlistCmd() {
|
|
|
11940
11957
|
|
|
11941
11958
|
// src/commands/gateway-renew.ts
|
|
11942
11959
|
import { open as open3, readFile as readFile11, rename as rename9, rm as rm5, writeFile as writeFile13 } from "node:fs/promises";
|
|
11943
|
-
import { createHash as
|
|
11960
|
+
import { createHash as createHash7 } from "node:crypto";
|
|
11944
11961
|
import { request as request2 } from "node:http";
|
|
11945
11962
|
import { setTimeout as delay } from "node:timers/promises";
|
|
11946
11963
|
import { Keyring as Keyring5 } from "@polkadot/keyring";
|
|
@@ -12037,11 +12054,11 @@ function renewConformance(record, evidence, now = Date.now()) {
|
|
|
12037
12054
|
}
|
|
12038
12055
|
|
|
12039
12056
|
// ../gateway-core/src/tools-qualification.ts
|
|
12040
|
-
import { createHash as
|
|
12057
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
12041
12058
|
import { isDeepStrictEqual } from "node:util";
|
|
12042
12059
|
var OWNER_TOOLS_QUALIFICATION = "flop.owner-tools-qualification.v1";
|
|
12043
12060
|
function qualificationHash(value) {
|
|
12044
|
-
return
|
|
12061
|
+
return createHash5("sha256").update(JSON.stringify(value)).digest("hex");
|
|
12045
12062
|
}
|
|
12046
12063
|
function toolsQualificationEnvelope(model, version, scope) {
|
|
12047
12064
|
return {
|
|
@@ -12139,12 +12156,12 @@ function qualificationSignedToolCalls(frames) {
|
|
|
12139
12156
|
|
|
12140
12157
|
// ../gateway-core/src/miner-admissions.ts
|
|
12141
12158
|
import { closeSync, fstatSync, openSync, readSync, statSync } from "node:fs";
|
|
12142
|
-
import { createHash as
|
|
12159
|
+
import { createHash as createHash6 } from "node:crypto";
|
|
12143
12160
|
var MAX_ADMITTED_MINERS = 256;
|
|
12144
12161
|
var MAX_FILE_BYTES = 512 * 1024;
|
|
12145
12162
|
var HASH4 = /^0x[0-9a-f]{64}$/;
|
|
12146
12163
|
var minerSlot = (miner) => `${miner.minerAddress}:${miner.modelIdentity.requestedSlug}`;
|
|
12147
|
-
var staticMinerSha256 = (miner) =>
|
|
12164
|
+
var staticMinerSha256 = (miner) => createHash6("sha256").update(JSON.stringify(miner)).digest("hex");
|
|
12148
12165
|
var sameModel = (a, b) => Object.keys(a.modelIdentity).every((key) => a.modelIdentity[key] === b.modelIdentity[key]);
|
|
12149
12166
|
var MinerRouting = class {
|
|
12150
12167
|
constructor(statics, path5) {
|
|
@@ -12161,8 +12178,8 @@ var MinerRouting = class {
|
|
|
12161
12178
|
if (this.path) {
|
|
12162
12179
|
let stamp;
|
|
12163
12180
|
try {
|
|
12164
|
-
const
|
|
12165
|
-
stamp = `${
|
|
12181
|
+
const stat2 = statSync(this.path);
|
|
12182
|
+
stamp = `${stat2.dev}:${stat2.ino}:${stat2.size}:${stat2.mtimeMs}`;
|
|
12166
12183
|
} catch {
|
|
12167
12184
|
stamp = "missing";
|
|
12168
12185
|
}
|
|
@@ -12174,8 +12191,8 @@ var MinerRouting = class {
|
|
|
12174
12191
|
let fd;
|
|
12175
12192
|
try {
|
|
12176
12193
|
fd = openSync(this.path, "r");
|
|
12177
|
-
const
|
|
12178
|
-
if (!
|
|
12194
|
+
const stat2 = fstatSync(fd);
|
|
12195
|
+
if (!stat2.isFile() || stat2.size > MAX_FILE_BYTES) throw new Error();
|
|
12179
12196
|
const buffer = Buffer.alloc(MAX_FILE_BYTES + 1);
|
|
12180
12197
|
const size = readSync(fd, buffer, 0, buffer.length, 0);
|
|
12181
12198
|
if (size > MAX_FILE_BYTES) throw new Error();
|
|
@@ -12237,8 +12254,8 @@ var MinerAdmissions = class {
|
|
|
12237
12254
|
read(now = Date.now()) {
|
|
12238
12255
|
let stamp;
|
|
12239
12256
|
try {
|
|
12240
|
-
const
|
|
12241
|
-
stamp = `${
|
|
12257
|
+
const stat2 = statSync(this.path);
|
|
12258
|
+
stamp = `${stat2.dev}:${stat2.ino}:${stat2.size}:${stat2.mtimeMs}`;
|
|
12242
12259
|
} catch {
|
|
12243
12260
|
stamp = "missing";
|
|
12244
12261
|
}
|
|
@@ -12249,8 +12266,8 @@ var MinerAdmissions = class {
|
|
|
12249
12266
|
let fd;
|
|
12250
12267
|
try {
|
|
12251
12268
|
fd = openSync(this.path, "r");
|
|
12252
|
-
const
|
|
12253
|
-
if (!
|
|
12269
|
+
const stat2 = fstatSync(fd);
|
|
12270
|
+
if (!stat2.isFile() || stat2.size > MAX_FILE_BYTES) throw new Error("admissions file too large");
|
|
12254
12271
|
const buffer = Buffer.alloc(MAX_FILE_BYTES + 1);
|
|
12255
12272
|
const size = readSync(fd, buffer, 0, buffer.length, 0);
|
|
12256
12273
|
if (size > MAX_FILE_BYTES) throw new Error("admissions file too large");
|
|
@@ -13001,6 +13018,73 @@ async function waitForSettlementAllowance(api, channelId, aggregateGn, deadline)
|
|
|
13001
13018
|
}
|
|
13002
13019
|
throw new Error("settlement allowance did not arrive before the probe deadline");
|
|
13003
13020
|
}
|
|
13021
|
+
async function reconcileProbeClose(api, channelId, aggregateGn, deadline, submit) {
|
|
13022
|
+
const bounded = async (operation) => {
|
|
13023
|
+
const remaining = deadline - Date.now();
|
|
13024
|
+
if (remaining <= 0) throw new Error("probe deadline elapsed");
|
|
13025
|
+
let timer;
|
|
13026
|
+
try {
|
|
13027
|
+
return await Promise.race([operation, new Promise((_, reject) => {
|
|
13028
|
+
timer = setTimeout(() => reject(new Error("probe deadline elapsed")), remaining);
|
|
13029
|
+
})]);
|
|
13030
|
+
} finally {
|
|
13031
|
+
if (timer) clearTimeout(timer);
|
|
13032
|
+
}
|
|
13033
|
+
};
|
|
13034
|
+
let next;
|
|
13035
|
+
try {
|
|
13036
|
+
const head = await bounded(api.rpc.chain.getFinalizedHead());
|
|
13037
|
+
next = BigInt((await bounded(api.rpc.chain.getHeader(head))).number.toString());
|
|
13038
|
+
} catch {
|
|
13039
|
+
return "settlement unknown (finalized start unavailable; do not resubmit)";
|
|
13040
|
+
}
|
|
13041
|
+
if (Date.now() >= deadline) return "settlement not submitted (probe deadline elapsed)";
|
|
13042
|
+
try {
|
|
13043
|
+
await bounded(submit());
|
|
13044
|
+
} catch {
|
|
13045
|
+
}
|
|
13046
|
+
try {
|
|
13047
|
+
while (Date.now() < deadline) {
|
|
13048
|
+
const head = await bounded(api.rpc.chain.getFinalizedHead());
|
|
13049
|
+
const height = BigInt((await bounded(api.rpc.chain.getHeader(head))).number.toString());
|
|
13050
|
+
while (next <= height) {
|
|
13051
|
+
const blockHash = await bounded(api.rpc.chain.getBlockHash(next.toString()));
|
|
13052
|
+
const at = await bounded(api.at(blockHash));
|
|
13053
|
+
const events = [...await bounded(at.query.system.events())];
|
|
13054
|
+
for (const row of events) {
|
|
13055
|
+
const event = row.event;
|
|
13056
|
+
if (event.section !== "computeChannel" || !["Settled", "TimedOutRefunded"].includes(event.method) || event.data[0].toHex() !== hex3(channelId) || !row.phase.isApplyExtrinsic) continue;
|
|
13057
|
+
const index = row.phase.asApplyExtrinsic.toNumber();
|
|
13058
|
+
if (!events.some((other) => other.phase.isApplyExtrinsic && other.phase.asApplyExtrinsic.toNumber() === index && other.event.section === "system" && other.event.method === "ExtrinsicSuccess")) continue;
|
|
13059
|
+
if (event.method === "TimedOutRefunded") return `refunded at finalized ${blockHash.toHex()}`;
|
|
13060
|
+
if (BigInt(event.data[3].toString()) !== aggregateGn || !event.data[4].isTrue) {
|
|
13061
|
+
return `settlement mismatch at finalized ${blockHash.toHex()}`;
|
|
13062
|
+
}
|
|
13063
|
+
const paid = BigInt(event.data[1].toString());
|
|
13064
|
+
if (paid <= 0n) return `settlement has zero miner payment at finalized ${blockHash.toHex()}`;
|
|
13065
|
+
const block = await bounded(api.rpc.chain.getBlock(blockHash));
|
|
13066
|
+
return `settled ${block.block.extrinsics[index].hash.toHex()} minerPaid=${paid} at finalized ${blockHash.toHex()}`;
|
|
13067
|
+
}
|
|
13068
|
+
next++;
|
|
13069
|
+
}
|
|
13070
|
+
await bounded(delay(Math.min(1e3, Math.max(1, deadline - Date.now()))));
|
|
13071
|
+
}
|
|
13072
|
+
} catch {
|
|
13073
|
+
}
|
|
13074
|
+
return "settlement unknown (no finalized outcome before probe deadline; do not resubmit)";
|
|
13075
|
+
}
|
|
13076
|
+
async function settleProbe(api, channelId, aggregateGn, deadline, allowance2, submit, forceClose) {
|
|
13077
|
+
try {
|
|
13078
|
+
await allowance2();
|
|
13079
|
+
} catch (error) {
|
|
13080
|
+
try {
|
|
13081
|
+
await forceClose();
|
|
13082
|
+
} catch {
|
|
13083
|
+
}
|
|
13084
|
+
return `cooperative settlement not submitted; force close attempted (${reason(error)})`;
|
|
13085
|
+
}
|
|
13086
|
+
return reconcileProbeClose(api, channelId, aggregateGn, deadline, submit);
|
|
13087
|
+
}
|
|
13004
13088
|
async function renewalMinerInfo(api, miner, fetchImpl = fetch) {
|
|
13005
13089
|
const head = await api.rpc.chain.getFinalizedHead();
|
|
13006
13090
|
const at = await api.at(head);
|
|
@@ -13154,28 +13238,26 @@ async function probeToolsConformance(input) {
|
|
|
13154
13238
|
}
|
|
13155
13239
|
throw error;
|
|
13156
13240
|
}
|
|
13157
|
-
|
|
13158
|
-
|
|
13159
|
-
|
|
13160
|
-
|
|
13241
|
+
const settlement = await settleProbe(
|
|
13242
|
+
api,
|
|
13243
|
+
session.channelId,
|
|
13244
|
+
receipt.gN,
|
|
13245
|
+
deadline,
|
|
13246
|
+
() => waitForSettlementAllowance(api, session.channelId, receipt.gN, deadline),
|
|
13247
|
+
() => session.closeCooperative(async (claim) => {
|
|
13161
13248
|
const wire = await computeChannel6.notifyMinerClose(miner.endpoint, claim, headers, AbortSignal.timeout(3e4), requestHeaders, fetchImpl);
|
|
13162
|
-
|
|
13249
|
+
if (!wire) throw new Error("miner did not report a settlement");
|
|
13250
|
+
return {
|
|
13163
13251
|
channelId: bytes3(wire.channelId),
|
|
13164
13252
|
minerPaid: BigInt(wire.minerPaid),
|
|
13165
13253
|
burned: BigInt(wire.burned),
|
|
13166
13254
|
aggregateGn: BigInt(wire.aggregateGn),
|
|
13167
13255
|
cooperative: wire.cooperative,
|
|
13168
13256
|
txHash: wire.txHash
|
|
13169
|
-
}
|
|
13170
|
-
})
|
|
13171
|
-
|
|
13172
|
-
|
|
13173
|
-
try {
|
|
13174
|
-
await session.close();
|
|
13175
|
-
} catch {
|
|
13176
|
-
}
|
|
13177
|
-
settlement = `settlement failed (${reason(error)})`;
|
|
13178
|
-
}
|
|
13257
|
+
};
|
|
13258
|
+
}),
|
|
13259
|
+
() => session.close()
|
|
13260
|
+
);
|
|
13179
13261
|
return `${verdict} in channel ${hex3(session.channelId)}; ${settlement}`;
|
|
13180
13262
|
}
|
|
13181
13263
|
function envJson(name) {
|
|
@@ -13204,7 +13286,7 @@ function gatewayRenewCmd() {
|
|
|
13204
13286
|
throw new Error("receipt-only mode requires the receipt/hash and cannot run direct probes");
|
|
13205
13287
|
}
|
|
13206
13288
|
const raw = await readFile11(opts.qualificationReceipt);
|
|
13207
|
-
if (raw.length > 8e6 ||
|
|
13289
|
+
if (raw.length > 8e6 || createHash7("sha256").update(raw).digest("hex") !== opts.qualificationReceiptSha256) throw new Error("qualification receipt bytes differ");
|
|
13208
13290
|
const value = JSON.parse(raw.toString("utf8"));
|
|
13209
13291
|
const records2 = parseCanonicalCatalog(envJson(opts.catalogEnv), { chainPriced: opts.catalogPricing === "chain" });
|
|
13210
13292
|
const record = records2.find((item) => item.openrouter_slug === value.model);
|
|
@@ -13289,7 +13371,7 @@ function gatewayRenewCmd() {
|
|
|
13289
13371
|
// src/release-check.ts
|
|
13290
13372
|
import { mkdir as mkdir14, readFile as readFile12, rename as rename10, writeFile as writeFile14 } from "node:fs/promises";
|
|
13291
13373
|
import { homedir as homedir5 } from "node:os";
|
|
13292
|
-
import { dirname as
|
|
13374
|
+
import { dirname as dirname8, join as join10 } from "node:path";
|
|
13293
13375
|
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
13294
13376
|
var READ_ONLY = /* @__PURE__ */ new Set([
|
|
13295
13377
|
"health",
|
|
@@ -13348,7 +13430,7 @@ async function checkRelease(options) {
|
|
|
13348
13430
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
13349
13431
|
releases = parseOperatorReleases(await response.json());
|
|
13350
13432
|
try {
|
|
13351
|
-
await mkdir14(
|
|
13433
|
+
await mkdir14(dirname8(cacheFile), { recursive: true });
|
|
13352
13434
|
const temporary = `${cacheFile}.${process.pid}.tmp`;
|
|
13353
13435
|
await writeFile14(temporary, JSON.stringify({ fetchedAt: now, releases }), { mode: 384 });
|
|
13354
13436
|
await rename10(temporary, cacheFile);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flop-labs/cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
4
4
|
"description": "FLOP Network CLI — interact with FLOP from the terminal",
|
|
5
5
|
"license": "MIT OR Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@flop-labs/client": "^0.2.2",
|
|
26
|
-
"@flop-labs/sdk": "^0.5.
|
|
26
|
+
"@flop-labs/sdk": "^0.5.2",
|
|
27
27
|
"@polkadot/api": "^17.0.1",
|
|
28
28
|
"@polkadot/keyring": "^14.0.3",
|
|
29
29
|
"@polkadot/util-crypto": "^14.0.3",
|