mlclaw 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/.agents/skills/mlclaw/SKILL.md +3 -2
- package/Dockerfile +3 -3
- package/README.md +36 -9
- package/dist/mlclaw-space-runtime.js +14 -0
- package/dist/mlclaw.mjs +616 -60
- package/entrypoint.sh +4 -4
- package/openclaw.default.json +5 -3
- package/package.json +5 -5
- package/tsconfig.json +1 -0
|
@@ -59,8 +59,9 @@ Collect or confirm:
|
|
|
59
59
|
|
|
60
60
|
- Hugging Face token access: `HF_TOKEN`, `HF_TOKEN_PATH`, `$HF_HOME/token`, or
|
|
61
61
|
`hf auth login`.
|
|
62
|
-
-
|
|
63
|
-
`MLCLAW_BROKER_HF_TOKEN`;
|
|
62
|
+
- A separate fine-grained Hugging Face token is installed as the broker-owned
|
|
63
|
+
`MLCLAW_BROKER_HF_TOKEN`; the active CLI login is used only to provision
|
|
64
|
+
resources. Legacy dedicated Router tokens remain supported for inference.
|
|
64
65
|
- Agent name, unless a Telegram bot token is supplied and the user wants the
|
|
65
66
|
name derived from the bot username.
|
|
66
67
|
- Gateway mode:
|
package/Dockerfile
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
ARG OPENCLAW_VERSION=2026.7.1
|
|
2
2
|
ARG OPENCLAW_BASE_IMAGE=ghcr.io/openclaw/openclaw:${OPENCLAW_VERSION}
|
|
3
|
-
ARG BROKERKIT_PLUGIN_VERSION=0.
|
|
4
|
-
ARG BROKERKIT_VERSION=hf-broker/v0.1
|
|
5
|
-
ARG MLCLAW_RUNTIME_IMAGE=ghcr.io/osolmaz/mlclaw:0.4.
|
|
3
|
+
ARG BROKERKIT_PLUGIN_VERSION=0.3.0
|
|
4
|
+
ARG BROKERKIT_VERSION=hf-broker/v0.3.1
|
|
5
|
+
ARG MLCLAW_RUNTIME_IMAGE=ghcr.io/osolmaz/mlclaw:0.4.2-openclaw-2026.7.1
|
|
6
6
|
|
|
7
7
|
FROM golang:1.26.5-bookworm AS hf-broker-build
|
|
8
8
|
ARG BROKERKIT_VERSION
|
package/README.md
CHANGED
|
@@ -47,12 +47,23 @@ flow, and resumes after sign-in. Non-interactive runs never install software and
|
|
|
47
47
|
still require a token up front. You never paste that token into someone else's
|
|
48
48
|
app; the bootstrapper runs locally.
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
credential
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
50
|
+
The `hf` CLI login is a provisioning credential: ML Claw uses it to create and
|
|
51
|
+
configure resources owned by your account. HF Broker uses a separate, durable
|
|
52
|
+
fine-grained credential whose required permissions are versioned by BrokerKit.
|
|
53
|
+
Interactive bootstrap opens Hugging Face's token form with those fields
|
|
54
|
+
preselected, then accepts the new token through a hidden local prompt. Creating
|
|
55
|
+
or replacing this credential never changes the active `hf` CLI login. For
|
|
56
|
+
automation, pass a `0600` file through `--broker-hf-token-file`; ML Claw does
|
|
57
|
+
not accept the token as a command-line value.
|
|
58
|
+
|
|
59
|
+
The broker owns the selected credential; OpenClaw receives only a separate
|
|
60
|
+
agent credential that can call the broker's typed, policy-checked routes. It
|
|
61
|
+
cannot read the token or use the admin-only operator API. Rerunning bootstrap
|
|
62
|
+
reuses a healthy saved broker credential without reopening the form. A missing,
|
|
63
|
+
invalid, wrong-account, or under-scoped credential must be repaired before ML
|
|
64
|
+
Claw continues; it never silently substitutes the active CLI login. Existing
|
|
65
|
+
dedicated inference tokens remain supported through `MLCLAW_ROUTER_TOKEN`,
|
|
66
|
+
`HF_ROUTER_TOKEN`, or `--router-token-file` during migration.
|
|
56
67
|
|
|
57
68
|
## Default Flow
|
|
58
69
|
|
|
@@ -254,9 +265,25 @@ It never reads secret values and never modifies bucket contents.
|
|
|
254
265
|
|
|
255
266
|
`mlclaw update` also refreshes the generated Space Dockerfile and runtime
|
|
256
267
|
metadata, so older Spaces can move to the current implementation without
|
|
257
|
-
recreating their bucket.
|
|
258
|
-
|
|
259
|
-
|
|
268
|
+
recreating their bucket. It does not promote the active Hugging Face CLI login
|
|
269
|
+
into a runtime secret. Recover or repair the dedicated broker credential first
|
|
270
|
+
when updating a deployment that predates this boundary.
|
|
271
|
+
|
|
272
|
+
Inspect or replace the dedicated credential with:
|
|
273
|
+
|
|
274
|
+
```bash
|
|
275
|
+
mlclaw credentials status mlclaw
|
|
276
|
+
mlclaw credentials repair mlclaw
|
|
277
|
+
mlclaw credentials repair mlclaw --broker-hf-token-file ~/secrets/mlclaw-broker.env
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
`status` performs a read-only verification and prints the account, BrokerKit
|
|
281
|
+
profile, verification time, and a short SHA-256 fingerprint, never the token.
|
|
282
|
+
`repair` validates the replacement before changing anything. Local running
|
|
283
|
+
gateways are replaced transactionally and rolled back on failure; stopped local
|
|
284
|
+
gateways stay stopped. Space repair uses the active CLI login only to install
|
|
285
|
+
the already-validated secret and restart the Space, restoring the previous
|
|
286
|
+
secret and local metadata if the operation fails.
|
|
260
287
|
|
|
261
288
|
## Browser Settings
|
|
262
289
|
|
|
@@ -8584,6 +8584,7 @@ function remainingUpstreamTimeout(deadline) {
|
|
|
8584
8584
|
// src/mlclaw-space-runtime/openclaw-config.ts
|
|
8585
8585
|
var BROKER_MCP_CONNECTION_TIMEOUT_MS = 1e4;
|
|
8586
8586
|
var BROKER_MCP_REQUEST_TIMEOUT_MS = 45e3;
|
|
8587
|
+
var AUTOMATIC_SESSION_RESET_DISABLED_MINUTES = 2147483647;
|
|
8587
8588
|
async function configureOpenClawGateway(config2) {
|
|
8588
8589
|
const raw2 = await fs.readFile(config2.openclawConfigPath, "utf8");
|
|
8589
8590
|
const openclawConfig = JSON.parse(raw2);
|
|
@@ -8607,6 +8608,7 @@ async function configureOpenClawGateway(config2) {
|
|
|
8607
8608
|
embedSandbox: "scripts"
|
|
8608
8609
|
};
|
|
8609
8610
|
configureOpenClawModels(openclawConfig, config2);
|
|
8611
|
+
disableAutomaticSessionResets(openclawConfig);
|
|
8610
8612
|
configureManagedMcpServers(openclawConfig, config2);
|
|
8611
8613
|
configureBrokerMcpServer(openclawConfig, config2);
|
|
8612
8614
|
configureBrokerKitPlugin(openclawConfig, config2);
|
|
@@ -8618,6 +8620,18 @@ async function configureOpenClawGateway(config2) {
|
|
|
8618
8620
|
await fs.chown(config2.openclawConfigPath, config2.openclawUid, config2.openclawGid);
|
|
8619
8621
|
}
|
|
8620
8622
|
}
|
|
8623
|
+
function disableAutomaticSessionResets(openclawConfig) {
|
|
8624
|
+
const session = object(openclawConfig, "session");
|
|
8625
|
+
session.reset = {
|
|
8626
|
+
mode: "idle",
|
|
8627
|
+
idleMinutes: AUTOMATIC_SESSION_RESET_DISABLED_MINUTES
|
|
8628
|
+
};
|
|
8629
|
+
delete session.idleMinutes;
|
|
8630
|
+
delete session.resetByType;
|
|
8631
|
+
delete session.resetByChannel;
|
|
8632
|
+
const maintenance = object(session, "maintenance");
|
|
8633
|
+
maintenance.resetArchiveRetention = false;
|
|
8634
|
+
}
|
|
8621
8635
|
function configureBrokerMcpServer(openclawConfig, config2) {
|
|
8622
8636
|
const servers = object(object(openclawConfig, "mcp"), "servers");
|
|
8623
8637
|
if (!config2.brokerAgentUrl || !config2.brokerAgentSecretFile) {
|
package/dist/mlclaw.mjs
CHANGED
|
@@ -8956,7 +8956,7 @@ import fs16 from "node:fs/promises";
|
|
|
8956
8956
|
import { realpathSync } from "node:fs";
|
|
8957
8957
|
import os8 from "node:os";
|
|
8958
8958
|
import process4 from "node:process";
|
|
8959
|
-
import { createHash as
|
|
8959
|
+
import { createHash as createHash4, randomBytes, randomUUID as randomUUID2 } from "node:crypto";
|
|
8960
8960
|
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
8961
8961
|
import { setTimeout as delay2 } from "node:timers/promises";
|
|
8962
8962
|
|
|
@@ -10056,10 +10056,10 @@ function parsePodmanConnections(raw) {
|
|
|
10056
10056
|
if (!value || typeof value !== "object") {
|
|
10057
10057
|
return [];
|
|
10058
10058
|
}
|
|
10059
|
-
const
|
|
10060
|
-
const name =
|
|
10061
|
-
const uri =
|
|
10062
|
-
const isDefault =
|
|
10059
|
+
const record2 = value;
|
|
10060
|
+
const name = record2.Name ?? record2.name;
|
|
10061
|
+
const uri = record2.URI ?? record2.uri;
|
|
10062
|
+
const isDefault = record2.Default ?? record2.default;
|
|
10063
10063
|
if (typeof name !== "string" || !name.trim()) {
|
|
10064
10064
|
return [];
|
|
10065
10065
|
}
|
|
@@ -14900,7 +14900,7 @@ var HubApi = class {
|
|
|
14900
14900
|
return new BucketClient({ bucket, accessToken: this.token, hubUrl: this.hubUrl, fetch: this.fetchImpl });
|
|
14901
14901
|
}
|
|
14902
14902
|
async whoami() {
|
|
14903
|
-
return await this.requestJson("/api/whoami-v2");
|
|
14903
|
+
return parseHubIdentity(await this.requestJson("/api/whoami-v2"));
|
|
14904
14904
|
}
|
|
14905
14905
|
async createBucket(bucketId, privateBucket = true) {
|
|
14906
14906
|
const [namespace, name] = splitRepoId(bucketId);
|
|
@@ -15285,6 +15285,55 @@ var HubApi = class {
|
|
|
15285
15285
|
return response;
|
|
15286
15286
|
}
|
|
15287
15287
|
};
|
|
15288
|
+
function parseHubIdentity(value) {
|
|
15289
|
+
const root = record(value);
|
|
15290
|
+
const name = stringValue(root.name);
|
|
15291
|
+
if (!name) {
|
|
15292
|
+
throw new Error("Hugging Face identity response omitted the account name");
|
|
15293
|
+
}
|
|
15294
|
+
const organizations = Array.isArray(root.orgs) ? root.orgs.map((entry) => stringValue(record(entry).name)).filter((entry) => Boolean(entry)) : [];
|
|
15295
|
+
const auth = record(root.auth);
|
|
15296
|
+
const accessToken = record(auth.accessToken);
|
|
15297
|
+
const fineGrained = record(accessToken.fineGrained);
|
|
15298
|
+
const authType = stringValue(auth.type);
|
|
15299
|
+
const accessTokenRole = stringValue(accessToken.role);
|
|
15300
|
+
const scoped = Array.isArray(fineGrained.scoped) ? fineGrained.scoped.map(parseFineGrainedScope).filter((entry) => Boolean(entry)) : [];
|
|
15301
|
+
const global2 = stringArray(fineGrained.global);
|
|
15302
|
+
const canReadGatedRepos = fineGrained.canReadGatedRepos === true;
|
|
15303
|
+
const parsedAccessToken = {
|
|
15304
|
+
...accessTokenRole ? { role: accessTokenRole } : {},
|
|
15305
|
+
...Object.keys(fineGrained).length > 0 ? { fineGrained: { global: global2, scoped, canReadGatedRepos } } : {}
|
|
15306
|
+
};
|
|
15307
|
+
const parsedAuth = {
|
|
15308
|
+
...authType ? { type: authType } : {},
|
|
15309
|
+
...Object.keys(parsedAccessToken).length > 0 ? { accessToken: parsedAccessToken } : {}
|
|
15310
|
+
};
|
|
15311
|
+
return {
|
|
15312
|
+
name,
|
|
15313
|
+
organizations: [...new Set(organizations)].sort(),
|
|
15314
|
+
...Object.keys(parsedAuth).length > 0 ? { auth: parsedAuth } : {}
|
|
15315
|
+
};
|
|
15316
|
+
}
|
|
15317
|
+
function parseFineGrainedScope(value) {
|
|
15318
|
+
const scope = record(value);
|
|
15319
|
+
const entity = record(scope.entity);
|
|
15320
|
+
const type = stringValue(entity.type);
|
|
15321
|
+
if (!type) return void 0;
|
|
15322
|
+
const name = stringValue(entity.name);
|
|
15323
|
+
return {
|
|
15324
|
+
entity: { type, ...name ? { name } : {} },
|
|
15325
|
+
permissions: stringArray(scope.permissions)
|
|
15326
|
+
};
|
|
15327
|
+
}
|
|
15328
|
+
function record(value) {
|
|
15329
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
15330
|
+
}
|
|
15331
|
+
function stringValue(value) {
|
|
15332
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
15333
|
+
}
|
|
15334
|
+
function stringArray(value) {
|
|
15335
|
+
return Array.isArray(value) ? [...new Set(value.filter((entry) => typeof entry === "string" && Boolean(entry.trim())))].map((entry) => entry.trim()).sort() : [];
|
|
15336
|
+
}
|
|
15288
15337
|
function splitRepoId(repoId) {
|
|
15289
15338
|
const parts = repoId.split("/");
|
|
15290
15339
|
if (parts.length !== 2 || !parts[0] || !parts[1]) {
|
|
@@ -15326,8 +15375,8 @@ import fs12 from "node:fs";
|
|
|
15326
15375
|
import path13 from "node:path";
|
|
15327
15376
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
15328
15377
|
var DEFAULT_OPENCLAW_VERSION = "2026.7.1";
|
|
15329
|
-
var DEFAULT_BROKERKIT_PLUGIN_VERSION = "0.
|
|
15330
|
-
var DEFAULT_BROKERKIT_VERSION = "hf-broker/v0.1
|
|
15378
|
+
var DEFAULT_BROKERKIT_PLUGIN_VERSION = "0.3.0";
|
|
15379
|
+
var DEFAULT_BROKERKIT_VERSION = "hf-broker/v0.3.1";
|
|
15331
15380
|
var DEFAULT_RUNTIME_IMAGE_REPOSITORY = "ghcr.io/osolmaz/mlclaw";
|
|
15332
15381
|
var PACKAGE_METADATA = readPackageMetadata();
|
|
15333
15382
|
var PACKAGE_VERSION = packageString("version", "unknown");
|
|
@@ -16178,15 +16227,15 @@ var makeIssue = (params) => {
|
|
|
16178
16227
|
message: issueData.message
|
|
16179
16228
|
};
|
|
16180
16229
|
}
|
|
16181
|
-
let
|
|
16230
|
+
let errorMessage2 = "";
|
|
16182
16231
|
const maps = errorMaps.filter((m2) => !!m2).slice().reverse();
|
|
16183
16232
|
for (const map of maps) {
|
|
16184
|
-
|
|
16233
|
+
errorMessage2 = map(fullIssue, { data, defaultError: errorMessage2 }).message;
|
|
16185
16234
|
}
|
|
16186
16235
|
return {
|
|
16187
16236
|
...issueData,
|
|
16188
16237
|
path: fullPath,
|
|
16189
|
-
message:
|
|
16238
|
+
message: errorMessage2
|
|
16190
16239
|
};
|
|
16191
16240
|
};
|
|
16192
16241
|
var EMPTY_PATH = [];
|
|
@@ -19791,6 +19840,12 @@ var manifestFields = {
|
|
|
19791
19840
|
gatewayLocation: external_exports.enum(["local", "space"]),
|
|
19792
19841
|
model: external_exports.string().min(1).max(512),
|
|
19793
19842
|
runtimeImage: external_exports.string().min(1).max(1024),
|
|
19843
|
+
brokerCredential: external_exports.object({
|
|
19844
|
+
profileId: external_exports.literal("hf-broker-complete-v1"),
|
|
19845
|
+
account: external_exports.string().min(1).max(128),
|
|
19846
|
+
fingerprintSha256: external_exports.string().regex(/^[a-f0-9]{64}$/),
|
|
19847
|
+
verifiedAt: external_exports.string().datetime()
|
|
19848
|
+
}).strict().optional(),
|
|
19794
19849
|
credentialKeySha256: external_exports.string().regex(/^[a-f0-9]{64}$/).optional(),
|
|
19795
19850
|
tailscaleMode: external_exports.enum(["off", "direct", "serve"]).optional(),
|
|
19796
19851
|
spaceVisibility: external_exports.enum(["private", "public"]).optional(),
|
|
@@ -20279,6 +20334,164 @@ function delay(ms) {
|
|
|
20279
20334
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
20280
20335
|
}
|
|
20281
20336
|
|
|
20337
|
+
// src/mlclaw/hf-broker-credential.ts
|
|
20338
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
20339
|
+
|
|
20340
|
+
// src/mlclaw/hf-broker-credential-requirements.json
|
|
20341
|
+
var hf_broker_credential_requirements_default = {
|
|
20342
|
+
version: 1,
|
|
20343
|
+
profile_id: "hf-broker-complete-v1",
|
|
20344
|
+
token_form_url: "https://huggingface.co/settings/tokens/new",
|
|
20345
|
+
token_type: "fineGrained",
|
|
20346
|
+
requires_gated_repositories: true,
|
|
20347
|
+
personal_permissions: [
|
|
20348
|
+
"collection.read",
|
|
20349
|
+
"collection.write",
|
|
20350
|
+
"discussion.write",
|
|
20351
|
+
"inference.endpoints.infer.write",
|
|
20352
|
+
"inference.endpoints.write",
|
|
20353
|
+
"inference.serverless.write",
|
|
20354
|
+
"job.write",
|
|
20355
|
+
"repo.access.read",
|
|
20356
|
+
"repo.content.read",
|
|
20357
|
+
"repo.write",
|
|
20358
|
+
"sql-console.embed.write",
|
|
20359
|
+
"user.billing.read",
|
|
20360
|
+
"user.mcp.read",
|
|
20361
|
+
"user.notifications.read",
|
|
20362
|
+
"user.notifications.write",
|
|
20363
|
+
"user.papers.write",
|
|
20364
|
+
"user.preferences.write",
|
|
20365
|
+
"user.settings.notifications.write",
|
|
20366
|
+
"user.social.likes.write",
|
|
20367
|
+
"user.webhooks.read",
|
|
20368
|
+
"user.webhooks.write"
|
|
20369
|
+
],
|
|
20370
|
+
global_permissions: [
|
|
20371
|
+
"discussion.write",
|
|
20372
|
+
"post.write"
|
|
20373
|
+
],
|
|
20374
|
+
organization_permissions: [
|
|
20375
|
+
"collection.read",
|
|
20376
|
+
"collection.write",
|
|
20377
|
+
"discussion.write",
|
|
20378
|
+
"inference.endpoints.infer.write",
|
|
20379
|
+
"inference.endpoints.write",
|
|
20380
|
+
"inference.serverless.write",
|
|
20381
|
+
"job.write",
|
|
20382
|
+
"org.auditLog.write",
|
|
20383
|
+
"org.billing.read",
|
|
20384
|
+
"org.members.read",
|
|
20385
|
+
"org.members.write",
|
|
20386
|
+
"org.networkSecurity.read",
|
|
20387
|
+
"org.networkSecurity.write",
|
|
20388
|
+
"org.read",
|
|
20389
|
+
"org.repos.read",
|
|
20390
|
+
"org.serviceAccounts.read",
|
|
20391
|
+
"org.serviceAccounts.write",
|
|
20392
|
+
"org.write",
|
|
20393
|
+
"repo.access.read",
|
|
20394
|
+
"repo.content.read",
|
|
20395
|
+
"repo.write",
|
|
20396
|
+
"resourceGroup.write",
|
|
20397
|
+
"sql-console.embed.write"
|
|
20398
|
+
]
|
|
20399
|
+
};
|
|
20400
|
+
|
|
20401
|
+
// src/mlclaw/hf-broker-credential.ts
|
|
20402
|
+
var permissionListSchema = external_exports.array(external_exports.string().min(1)).min(1).superRefine((permissions, context) => {
|
|
20403
|
+
if (permissions.some((permission) => permission !== permission.trim())) {
|
|
20404
|
+
context.addIssue({ code: external_exports.ZodIssueCode.custom, message: "permissions must not contain outer whitespace" });
|
|
20405
|
+
}
|
|
20406
|
+
if (new Set(permissions).size !== permissions.length) {
|
|
20407
|
+
context.addIssue({ code: external_exports.ZodIssueCode.custom, message: "permissions must be unique" });
|
|
20408
|
+
}
|
|
20409
|
+
if (permissions.some((permission, index) => index > 0 && permission < permissions[index - 1])) {
|
|
20410
|
+
context.addIssue({ code: external_exports.ZodIssueCode.custom, message: "permissions must be sorted" });
|
|
20411
|
+
}
|
|
20412
|
+
});
|
|
20413
|
+
var profileSchema = external_exports.object({
|
|
20414
|
+
version: external_exports.literal(1),
|
|
20415
|
+
profile_id: external_exports.literal("hf-broker-complete-v1"),
|
|
20416
|
+
token_form_url: external_exports.literal("https://huggingface.co/settings/tokens/new"),
|
|
20417
|
+
token_type: external_exports.literal("fineGrained"),
|
|
20418
|
+
requires_gated_repositories: external_exports.literal(true),
|
|
20419
|
+
personal_permissions: permissionListSchema,
|
|
20420
|
+
global_permissions: permissionListSchema,
|
|
20421
|
+
organization_permissions: permissionListSchema
|
|
20422
|
+
}).strict();
|
|
20423
|
+
var BROKER_CREDENTIAL_PROFILE = Object.freeze(profileSchema.parse(hf_broker_credential_requirements_default));
|
|
20424
|
+
var HF_TOKEN_CREATE_URL = BROKER_CREDENTIAL_PROFILE.token_form_url;
|
|
20425
|
+
var BROKER_PERSONAL_PERMISSIONS = BROKER_CREDENTIAL_PROFILE.personal_permissions;
|
|
20426
|
+
var BROKER_GLOBAL_PERMISSIONS = BROKER_CREDENTIAL_PROFILE.global_permissions;
|
|
20427
|
+
var BROKER_ORGANIZATION_PERMISSIONS = BROKER_CREDENTIAL_PROFILE.organization_permissions;
|
|
20428
|
+
function buildBrokerTokenUrl(owner, accountName) {
|
|
20429
|
+
const url = new URL(HF_TOKEN_CREATE_URL);
|
|
20430
|
+
url.searchParams.set("tokenType", BROKER_CREDENTIAL_PROFILE.token_type);
|
|
20431
|
+
for (const permission of BROKER_PERSONAL_PERMISSIONS) {
|
|
20432
|
+
url.searchParams.append("ownUserPermissions", permission);
|
|
20433
|
+
}
|
|
20434
|
+
for (const permission of BROKER_GLOBAL_PERMISSIONS) {
|
|
20435
|
+
url.searchParams.append("globalPermissions", permission);
|
|
20436
|
+
}
|
|
20437
|
+
url.searchParams.set("canReadGatedRepos", String(BROKER_CREDENTIAL_PROFILE.requires_gated_repositories));
|
|
20438
|
+
if (owner !== accountName) {
|
|
20439
|
+
url.searchParams.append("orgs", owner);
|
|
20440
|
+
for (const permission of BROKER_ORGANIZATION_PERMISSIONS) {
|
|
20441
|
+
url.searchParams.append("orgPermissions", permission);
|
|
20442
|
+
}
|
|
20443
|
+
}
|
|
20444
|
+
return url.toString();
|
|
20445
|
+
}
|
|
20446
|
+
function assessBrokerCredential(identity, owner) {
|
|
20447
|
+
const accessToken = identity.auth?.accessToken;
|
|
20448
|
+
if (accessToken?.role !== BROKER_CREDENTIAL_PROFILE.token_type) {
|
|
20449
|
+
return {
|
|
20450
|
+
status: "unsupported",
|
|
20451
|
+
reason: "HF Broker requires a dedicated fine-grained Hugging Face token"
|
|
20452
|
+
};
|
|
20453
|
+
}
|
|
20454
|
+
if (!accessToken.fineGrained) {
|
|
20455
|
+
return {
|
|
20456
|
+
status: "unsupported",
|
|
20457
|
+
reason: "Hugging Face omitted this fine-grained token's permission details"
|
|
20458
|
+
};
|
|
20459
|
+
}
|
|
20460
|
+
const personalAvailable = new Set(scopedPermissions(accessToken.fineGrained.scoped, "user", identity.name));
|
|
20461
|
+
const globalAvailable = new Set(accessToken.fineGrained.global);
|
|
20462
|
+
const missing = BROKER_PERSONAL_PERMISSIONS.filter((permission) => !personalAvailable.has(permission));
|
|
20463
|
+
missing.push(
|
|
20464
|
+
...BROKER_GLOBAL_PERMISSIONS.filter((permission) => !globalAvailable.has(permission)).map(
|
|
20465
|
+
(permission) => `global:${permission}`
|
|
20466
|
+
)
|
|
20467
|
+
);
|
|
20468
|
+
if (!accessToken.fineGrained.canReadGatedRepos) {
|
|
20469
|
+
missing.push("canReadGatedRepos");
|
|
20470
|
+
}
|
|
20471
|
+
if (owner !== identity.name) {
|
|
20472
|
+
const organizationAvailable = new Set(scopedPermissions(accessToken.fineGrained.scoped, "org", owner));
|
|
20473
|
+
missing.push(
|
|
20474
|
+
...BROKER_ORGANIZATION_PERMISSIONS.filter((permission) => !organizationAvailable.has(permission)).map(
|
|
20475
|
+
(permission) => `org:${permission}`
|
|
20476
|
+
)
|
|
20477
|
+
);
|
|
20478
|
+
}
|
|
20479
|
+
missing.sort();
|
|
20480
|
+
return missing.length === 0 ? { status: "sufficient" } : { status: "insufficient", missing };
|
|
20481
|
+
}
|
|
20482
|
+
function brokerCredentialMetadata(token, identity, verifiedAt) {
|
|
20483
|
+
return {
|
|
20484
|
+
profileId: BROKER_CREDENTIAL_PROFILE.profile_id,
|
|
20485
|
+
account: identity.name,
|
|
20486
|
+
fingerprintSha256: createHash3("sha256").update(token).digest("hex"),
|
|
20487
|
+
verifiedAt: verifiedAt.toISOString()
|
|
20488
|
+
};
|
|
20489
|
+
}
|
|
20490
|
+
function scopedPermissions(scopes, type, name) {
|
|
20491
|
+
if (!Array.isArray(scopes)) return [];
|
|
20492
|
+
return scopes.filter((scope) => scope.entity.type === type && (!scope.entity.name || scope.entity.name === name)).flatMap((scope) => scope.permissions);
|
|
20493
|
+
}
|
|
20494
|
+
|
|
20282
20495
|
// src/mlclaw/tailscale.ts
|
|
20283
20496
|
import { execFile as execFile3 } from "node:child_process";
|
|
20284
20497
|
import { promisify as promisify3 } from "node:util";
|
|
@@ -20351,7 +20564,7 @@ function parseTailscaleStatus(value) {
|
|
|
20351
20564
|
return { ready: false, reason: "Tailscale returned an invalid status" };
|
|
20352
20565
|
}
|
|
20353
20566
|
if (status.BackendState !== "Running") {
|
|
20354
|
-
return { ready: false, reason: `Tailscale is ${
|
|
20567
|
+
return { ready: false, reason: `Tailscale is ${stringValue2(status.BackendState) ?? "not running"}` };
|
|
20355
20568
|
}
|
|
20356
20569
|
const self = recordValue(status.Self);
|
|
20357
20570
|
if (self?.Online === false) {
|
|
@@ -20361,8 +20574,8 @@ function parseTailscaleStatus(value) {
|
|
|
20361
20574
|
(candidate) => typeof candidate === "string" && isTailscaleIpv4(candidate)
|
|
20362
20575
|
);
|
|
20363
20576
|
if (typeof ipv4 !== "string") return { ready: false, reason: "Tailscale IPv4 address is unavailable" };
|
|
20364
|
-
const dnsName = normalizeTailscaleDnsName(
|
|
20365
|
-
const tailnet =
|
|
20577
|
+
const dnsName = normalizeTailscaleDnsName(stringValue2(self?.DNSName));
|
|
20578
|
+
const tailnet = stringValue2(recordValue(status.CurrentTailnet)?.Name);
|
|
20366
20579
|
return { ready: true, ipv4, ...dnsName ? { dnsName } : {}, ...tailnet ? { tailnet } : {} };
|
|
20367
20580
|
}
|
|
20368
20581
|
var TailscaleApprovalRequiredError = class extends Error {
|
|
@@ -20469,7 +20682,7 @@ function commandErrorMessage(error) {
|
|
|
20469
20682
|
function recordValue(value) {
|
|
20470
20683
|
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
20471
20684
|
}
|
|
20472
|
-
function
|
|
20685
|
+
function stringValue2(value) {
|
|
20473
20686
|
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
20474
20687
|
}
|
|
20475
20688
|
function arrayValue(value) {
|
|
@@ -20541,7 +20754,7 @@ function createProgram(runtimeOverrides = {}) {
|
|
|
20541
20754
|
program2.command("bootstrap", { isDefault: true }).alias("configure").description("Create or update a Hugging Face OpenClaw deployment").option("--owner <owner>", "Hugging Face user or organization").option("--name <name>", "Agent and runtime resource base name").option("--bucket <owner/bucket>", "State bucket to create or adopt").option("--gateway <local|space>", "Where the live gateway runs").option("--telegram-token <token>", "Optional Telegram bot token").option("--telegram-token-file <path>", "File containing TELEGRAM_BOT_TOKEN=... or a raw token").option("--telegram-user-id <id>", "Allowed Telegram user ID").option("--telegram-api-root <url>", "Telegram API root override").option("--telegram-proxy <url>", "Telegram proxy URL override").option("--hardware <flavor>", "Hugging Face Space hardware flavor").option("--sleep-time <seconds>", "Space sleep timeout in seconds; -1 means never sleep", parseInteger).option("--model <model>", "OpenClaw model identifier").option("--runtime-image <image>", "ML Claw runtime image").option("--bundled-runtime", "Generate a bundled Space runtime instead of using the prebuilt ML Claw image", false).option("--public-space", "Create the Hugging Face Space as public instead of private", false).addOption(new Option("--gateway-token <token>").hideHelp()).option("--router-token <token>", "Hugging Face Router inference token for Space gateway model calls").option(
|
|
20542
20755
|
"--router-token-file <path>",
|
|
20543
20756
|
"File containing MLCLAW_ROUTER_TOKEN=..., HF_ROUTER_TOKEN=..., or a raw token"
|
|
20544
|
-
).option("--docker-context <name>", "Docker context for local gateway mode").option("--container-runtime <auto|docker|podman>", "Local container runtime", "auto").option("--local-port <port>", "Loopback port for a local gateway", parseLocalPort).option("--tailscale <off|direct|serve>", "Tailnet access mode", parseTailscaleMode).option("--tailscale-port <port>", "Tailnet listener or Serve HTTPS port", parseLocalPort).option(
|
|
20757
|
+
).option("--broker-hf-token-file <path>", "File containing MLCLAW_BROKER_HF_TOKEN=... or a raw Hugging Face token").option("--docker-context <name>", "Docker context for local gateway mode").option("--container-runtime <auto|docker|podman>", "Local container runtime", "auto").option("--local-port <port>", "Loopback port for a local gateway", parseLocalPort).option("--tailscale <off|direct|serve>", "Tailnet access mode", parseTailscaleMode).option("--tailscale-port <port>", "Tailnet listener or Serve HTTPS port", parseLocalPort).option(
|
|
20545
20758
|
"--allow-local-fallback",
|
|
20546
20759
|
"Allow non-interactive Space bootstrap to fall back to a ready local runtime",
|
|
20547
20760
|
false
|
|
@@ -20566,6 +20779,13 @@ function createProgram(runtimeOverrides = {}) {
|
|
|
20566
20779
|
const hub = runtime.hubFactory(token);
|
|
20567
20780
|
await settings(repoId, opts, hub, runtime);
|
|
20568
20781
|
});
|
|
20782
|
+
const credentials = program2.command("credentials").description("Inspect or replace trusted broker credentials");
|
|
20783
|
+
credentials.command("status").description("Verify the dedicated HF Broker credential").argument("[agent]", "Agent name; inferred when exactly one deployment exists").action(async (agent) => {
|
|
20784
|
+
await credentialsStatus(agent, runtime);
|
|
20785
|
+
});
|
|
20786
|
+
credentials.command("repair").description("Replace and apply the dedicated HF Broker credential").argument("[agent]", "Agent name; inferred when exactly one deployment exists").option("--broker-hf-token-file <path>", "File containing MLCLAW_BROKER_HF_TOKEN=... or a raw token").action(async (agent, opts) => {
|
|
20787
|
+
await credentialsRepair(agent, opts, runtime);
|
|
20788
|
+
});
|
|
20569
20789
|
const gateway = program2.command("gateway").description("Operate a ML Claw gateway");
|
|
20570
20790
|
gateway.command("start").argument("<agent>", "Agent name").option("--docker-context <name>", "Set Docker context only when the deployment has no pinned context").option("--local-port <port>", "Loopback port for the local gateway", parseLocalPort).option("--tailscale <off|direct|serve>", "Tailnet access mode", parseTailscaleMode).option("--tailscale-port <port>", "Tailnet listener or Serve HTTPS port", parseLocalPort).option("--no-pull", "Do not docker pull before starting a local gateway").option("--takeover", "Start even if another live runtime lease is present", false).action(async (agent, opts) => {
|
|
20571
20791
|
await gatewayStart(agent, opts, runtime);
|
|
@@ -20777,20 +20997,24 @@ async function bootstrap(opts, runtime) {
|
|
|
20777
20997
|
const runtimeImage = resolveRuntimeImage(opts.runtimeImage, runtime.env);
|
|
20778
20998
|
resolveSpaceRuntimeImage(opts, runtime.env);
|
|
20779
20999
|
let plan;
|
|
21000
|
+
let reviewedBrokerHfToken;
|
|
20780
21001
|
for (; ; ) {
|
|
20781
21002
|
plan = await resolveBootstrapPlan({
|
|
20782
21003
|
opts,
|
|
20783
21004
|
owner,
|
|
20784
21005
|
agentName,
|
|
20785
21006
|
hfToken,
|
|
21007
|
+
hfIdentity: me2,
|
|
20786
21008
|
model,
|
|
20787
21009
|
runtimeImage,
|
|
20788
21010
|
hub,
|
|
20789
21011
|
runtime,
|
|
21012
|
+
...reviewedBrokerHfToken ? { providedBrokerHfToken: reviewedBrokerHfToken } : {},
|
|
20790
21013
|
...requestedGatewayLocation ? { requestedGatewayLocation } : {},
|
|
20791
21014
|
...telegramToken ? { telegramToken } : {},
|
|
20792
21015
|
...telegramUserId ? { telegramUserId } : {}
|
|
20793
21016
|
});
|
|
21017
|
+
reviewedBrokerHfToken = plan.secrets.MLCLAW_BROKER_HF_TOKEN;
|
|
20794
21018
|
const alternative = await promptAlternativeBootstrapName({
|
|
20795
21019
|
plan,
|
|
20796
21020
|
explicitBucket: opts.bucket,
|
|
@@ -20866,6 +21090,8 @@ async function bootstrap(opts, runtime) {
|
|
|
20866
21090
|
owner,
|
|
20867
21091
|
agentName,
|
|
20868
21092
|
hfToken,
|
|
21093
|
+
hfIdentity: me2,
|
|
21094
|
+
brokerHfToken: activePlan.secrets.MLCLAW_BROKER_HF_TOKEN ?? hfToken,
|
|
20869
21095
|
model,
|
|
20870
21096
|
runtimeImage,
|
|
20871
21097
|
hub,
|
|
@@ -20923,7 +21149,7 @@ async function bootstrap(opts, runtime) {
|
|
|
20923
21149
|
const secretsChanged = JSON.stringify(previousSecrets) !== JSON.stringify(activePlan.secrets);
|
|
20924
21150
|
if (secretsChanged) {
|
|
20925
21151
|
await assertLease();
|
|
20926
|
-
await setSpaceGatewaySecrets(hub, activePlan.manifest.space,
|
|
21152
|
+
await setSpaceGatewaySecrets(hub, activePlan.manifest.space, activePlan.secrets, assertLease);
|
|
20927
21153
|
if (canDeleteBroadTokenSecrets({
|
|
20928
21154
|
model: activePlan.manifest.model,
|
|
20929
21155
|
routerTokenPresent: hasBrokerOrRouterTokenSecretRecord(activePlan.secrets)
|
|
@@ -21081,7 +21307,7 @@ function assertBootstrapIdentityMatches(plan, identity) {
|
|
|
21081
21307
|
}
|
|
21082
21308
|
async function reconcileManifest(params) {
|
|
21083
21309
|
const { hub, runtime } = params;
|
|
21084
|
-
const localLockKey =
|
|
21310
|
+
const localLockKey = deploymentLockKey(params.manifest);
|
|
21085
21311
|
return await withDeploymentLock(runtime.configRoot, localLockKey, async () => {
|
|
21086
21312
|
let requestedManifest = params.manifest;
|
|
21087
21313
|
const client = hub.bucket(requestedManifest.bucket);
|
|
@@ -21120,7 +21346,7 @@ async function reconcileManifest(params) {
|
|
|
21120
21346
|
const secrets = await ensureDeploymentCredentialKey(runtime, requestedManifest.agent);
|
|
21121
21347
|
requestedManifest = {
|
|
21122
21348
|
...requestedManifest,
|
|
21123
|
-
credentialKeySha256:
|
|
21349
|
+
credentialKeySha256: createHash4("sha256").update(requiredSecret(secrets, "MLCLAW_CREDENTIAL_KEY")).digest("hex")
|
|
21124
21350
|
};
|
|
21125
21351
|
}
|
|
21126
21352
|
const currentDesired = await readDesiredState(client);
|
|
@@ -21240,6 +21466,8 @@ async function resolveBootstrapPlan(params) {
|
|
|
21240
21466
|
agentName,
|
|
21241
21467
|
requestedGatewayLocation,
|
|
21242
21468
|
hfToken,
|
|
21469
|
+
hfIdentity,
|
|
21470
|
+
providedBrokerHfToken,
|
|
21243
21471
|
telegramToken,
|
|
21244
21472
|
telegramUserId,
|
|
21245
21473
|
model,
|
|
@@ -21251,6 +21479,14 @@ async function resolveBootstrapPlan(params) {
|
|
|
21251
21479
|
const now = runtime.now().toISOString();
|
|
21252
21480
|
const existingManifest = await readManifest(runtime.configRoot, agentName).catch(() => null);
|
|
21253
21481
|
const existingSecrets = await readSecretEnv(runtime.configRoot, agentName).catch(() => ({}));
|
|
21482
|
+
const brokerCredential = await resolveBrokerHfToken({
|
|
21483
|
+
opts,
|
|
21484
|
+
owner,
|
|
21485
|
+
hfIdentity,
|
|
21486
|
+
...providedBrokerHfToken ? { preferredToken: providedBrokerHfToken } : {},
|
|
21487
|
+
existingSecrets,
|
|
21488
|
+
runtime
|
|
21489
|
+
});
|
|
21254
21490
|
const sessionSecret = existingSecrets.MLCLAW_SESSION_SECRET ?? randomBytes(48).toString("base64url");
|
|
21255
21491
|
const restoredCredentialKey = existingSecrets.MLCLAW_CREDENTIAL_KEY ?? runtime.env.MLCLAW_CREDENTIAL_KEY;
|
|
21256
21492
|
if (existingManifest?.recoveredWithoutCredentialKey && !restoredCredentialKey) {
|
|
@@ -21259,7 +21495,7 @@ async function resolveBootstrapPlan(params) {
|
|
|
21259
21495
|
);
|
|
21260
21496
|
}
|
|
21261
21497
|
const credentialKey = restoredCredentialKey ?? randomBytes(32).toString("base64url");
|
|
21262
|
-
const credentialKeySha256 =
|
|
21498
|
+
const credentialKeySha256 = createHash4("sha256").update(credentialKey).digest("hex");
|
|
21263
21499
|
if (existingManifest?.credentialKeySha256 && existingManifest.credentialKeySha256 !== credentialKeySha256) {
|
|
21264
21500
|
throw new Error("MLCLAW_CREDENTIAL_KEY does not match the recovered deployment");
|
|
21265
21501
|
}
|
|
@@ -21325,6 +21561,7 @@ async function resolveBootstrapPlan(params) {
|
|
|
21325
21561
|
gatewayLocation,
|
|
21326
21562
|
model: effectiveModel,
|
|
21327
21563
|
runtimeImage: effectiveRuntimeImage,
|
|
21564
|
+
brokerCredential: brokerCredentialMetadata(brokerCredential.token, brokerCredential.identity, runtime.now()),
|
|
21328
21565
|
credentialKeySha256,
|
|
21329
21566
|
...existingManifest?.tailscaleMode ? { tailscaleMode: existingManifest.tailscaleMode } : {},
|
|
21330
21567
|
...existingManifest?.spaceVisibility ? { spaceVisibility: existingManifest.spaceVisibility } : {},
|
|
@@ -21342,7 +21579,7 @@ async function resolveBootstrapPlan(params) {
|
|
|
21342
21579
|
const effectiveTelegramProxy = opts.telegramProxy ?? existingSecrets.TELEGRAM_PROXY;
|
|
21343
21580
|
const effectiveTelegramApiRoot = opts.telegramApiRoot ?? existingSecrets.TELEGRAM_API_ROOT;
|
|
21344
21581
|
const secrets = deploymentSecrets({
|
|
21345
|
-
hfToken,
|
|
21582
|
+
hfToken: brokerCredential.token,
|
|
21346
21583
|
...effectiveTelegramToken ? { telegramToken: effectiveTelegramToken } : {},
|
|
21347
21584
|
...effectiveTelegramUserId ? { telegramUserId: effectiveTelegramUserId } : {},
|
|
21348
21585
|
sessionSecret,
|
|
@@ -21601,6 +21838,8 @@ async function resolveHostedBootstrapFallback(params) {
|
|
|
21601
21838
|
agentName: params.agentName,
|
|
21602
21839
|
requestedGatewayLocation: "local",
|
|
21603
21840
|
hfToken: params.hfToken,
|
|
21841
|
+
hfIdentity: params.hfIdentity,
|
|
21842
|
+
providedBrokerHfToken: params.brokerHfToken,
|
|
21604
21843
|
model: params.model,
|
|
21605
21844
|
runtimeImage: params.runtimeImage,
|
|
21606
21845
|
hub: params.hub,
|
|
@@ -21978,35 +22217,43 @@ async function deployLocalBootstrap(plan, opts, runtime, desiredChanged = true,
|
|
|
21978
22217
|
}
|
|
21979
22218
|
try {
|
|
21980
22219
|
if (networkAccessChanged && plan.manifest.networkAccess) {
|
|
22220
|
+
await assertLease();
|
|
21981
22221
|
await disableNetworkAccess(plan.manifest, runtime);
|
|
21982
22222
|
}
|
|
21983
22223
|
if (previousSecrets) {
|
|
22224
|
+
await assertLease();
|
|
21984
22225
|
await writeSecretEnv(runtime.configRoot, plan.agentName, previousSecrets);
|
|
21985
22226
|
} else {
|
|
22227
|
+
await assertLease();
|
|
21986
22228
|
await fs16.rm(secretEnvPath(runtime.configRoot, plan.agentName), { force: true });
|
|
21987
22229
|
}
|
|
21988
22230
|
if (previousContainer?.running && previousManifest) {
|
|
21989
|
-
await startLocalGateway({ manifest: previousManifest, runtime, pull: false, refresh: true });
|
|
22231
|
+
await startLocalGateway({ manifest: previousManifest, runtime, pull: false, refresh: true, assertLease });
|
|
21990
22232
|
runtime.stdout.log(`Previous local gateway restored: ${containerNameFor(previousManifest.agent)}`);
|
|
21991
22233
|
} else if (previousContainer && previousManifest && startupAttempted) {
|
|
21992
|
-
await startLocalGateway({ manifest: previousManifest, runtime, pull: false, refresh: true });
|
|
22234
|
+
await startLocalGateway({ manifest: previousManifest, runtime, pull: false, refresh: true, assertLease });
|
|
22235
|
+
await assertLease();
|
|
21993
22236
|
await localRunnerFor(previousManifest, runtime).stop(
|
|
21994
22237
|
containerNameFor(previousManifest.agent),
|
|
21995
22238
|
localConnectionFor(previousManifest)
|
|
21996
22239
|
);
|
|
21997
22240
|
if (previousNetworkState !== "owned") {
|
|
22241
|
+
await assertLease();
|
|
21998
22242
|
await disableNetworkAccess(previousManifest, runtime);
|
|
21999
22243
|
}
|
|
22000
22244
|
runtime.stdout.log(`Previous stopped local gateway restored: ${containerNameFor(previousManifest.agent)}`);
|
|
22001
22245
|
} else {
|
|
22002
22246
|
if (startupAttempted) {
|
|
22247
|
+
await assertLease();
|
|
22003
22248
|
await removeFailedBootstrapContainer(plan.manifest, runtime, !previousManifest);
|
|
22004
22249
|
}
|
|
22005
22250
|
}
|
|
22006
22251
|
if (previousNetworkState === "owned" && previousManifest?.networkAccess) {
|
|
22252
|
+
await assertLease();
|
|
22007
22253
|
await runtime.tailscaleRunner.ensureMapping(networkAccessMapping(previousManifest.networkAccess));
|
|
22008
22254
|
}
|
|
22009
22255
|
if (!previousManifest) {
|
|
22256
|
+
await assertLease();
|
|
22010
22257
|
await fs16.rm(manifestPath(runtime.configRoot, plan.agentName), { force: true });
|
|
22011
22258
|
}
|
|
22012
22259
|
} catch (rollbackError) {
|
|
@@ -22112,7 +22359,7 @@ async function deploySpaceGateway(params) {
|
|
|
22112
22359
|
await assertLease();
|
|
22113
22360
|
await clearSpaceGatewayDisabled(hub, manifest.space);
|
|
22114
22361
|
await assertLease();
|
|
22115
|
-
await setSpaceGatewaySecrets(hub, manifest.space,
|
|
22362
|
+
await setSpaceGatewaySecrets(hub, manifest.space, secrets, assertLease);
|
|
22116
22363
|
if (canDeleteBroadTokenSecrets({
|
|
22117
22364
|
model: manifest.model,
|
|
22118
22365
|
routerTokenPresent: hasBrokerOrRouterTokenSecretRecord(secrets)
|
|
@@ -23219,7 +23466,7 @@ async function restoreMatchingDeploymentCredentialKey(runtime, agent, expectedSh
|
|
|
23219
23466
|
const candidates = [suppliedCredentialKey, runtime.env.MLCLAW_CREDENTIAL_KEY, secrets.MLCLAW_CREDENTIAL_KEY].filter(
|
|
23220
23467
|
(value) => Boolean(value)
|
|
23221
23468
|
);
|
|
23222
|
-
const credentialKey = candidates.find((value) =>
|
|
23469
|
+
const credentialKey = candidates.find((value) => createHash4("sha256").update(value).digest("hex") === expectedSha256);
|
|
23223
23470
|
if (!credentialKey) {
|
|
23224
23471
|
throw new Error("local MLCLAW_CREDENTIAL_KEY is missing or does not match the canonical deployment identity");
|
|
23225
23472
|
}
|
|
@@ -23245,7 +23492,7 @@ async function update(repoId, opts, hub, hfToken, runtime) {
|
|
|
23245
23492
|
const runtimeImage = resolveSpaceRuntimeImage(opts, runtime.env);
|
|
23246
23493
|
const agentName = variables.get("OPENCLAW_AGENT_NAME")?.value?.trim() || repoId.split("/")[1] || "openclaw";
|
|
23247
23494
|
if (!canonicalTemplate) {
|
|
23248
|
-
await
|
|
23495
|
+
await ensureUpdateCredentials({
|
|
23249
23496
|
repoId,
|
|
23250
23497
|
agentName,
|
|
23251
23498
|
model: variables.get("OPENCLAW_MODEL")?.value ?? DEFAULT_MODEL2,
|
|
@@ -23282,39 +23529,39 @@ async function update(repoId, opts, hub, hfToken, runtime) {
|
|
|
23282
23529
|
await doctor(repoId, { fix: true }, hub, runtime);
|
|
23283
23530
|
runtime.stdout.log(`Space deployment triggered: ${repoId}`);
|
|
23284
23531
|
}
|
|
23285
|
-
async function
|
|
23286
|
-
if (!isHuggingFaceRouterModel(params.model)) {
|
|
23287
|
-
return;
|
|
23288
|
-
}
|
|
23289
|
-
const spaceSecrets = await params.hub.getSpaceSecrets(params.repoId);
|
|
23532
|
+
async function ensureUpdateCredentials(params) {
|
|
23290
23533
|
const hasExplicitOverride = params.opts.routerToken !== void 0 || params.opts.routerTokenFile !== void 0;
|
|
23291
|
-
if (hasBrokerOrRouterTokenSecretMap(spaceSecrets) && !hasExplicitOverride) {
|
|
23292
|
-
return;
|
|
23293
|
-
}
|
|
23294
23534
|
const hasManifest = await manifestExists(params.runtime.configRoot, params.agentName);
|
|
23295
|
-
|
|
23535
|
+
if (!hasManifest) {
|
|
23536
|
+
throw new Error(
|
|
23537
|
+
`cannot verify the dedicated HF Broker credential; recover the deployment and run \`mlclaw credentials repair ${params.agentName}\``
|
|
23538
|
+
);
|
|
23539
|
+
}
|
|
23540
|
+
const localManifest = await readManifest(params.runtime.configRoot, params.agentName);
|
|
23541
|
+
if (localManifest.space !== params.repoId) {
|
|
23542
|
+
throw new Error(
|
|
23543
|
+
`local deployment ${params.agentName} belongs to ${localManifest.space}, not ${params.repoId}; specify the matching deployment before updating`
|
|
23544
|
+
);
|
|
23545
|
+
}
|
|
23546
|
+
const localSecrets = await readSecretEnv(params.runtime.configRoot, params.agentName).catch(
|
|
23547
|
+
() => ({})
|
|
23548
|
+
);
|
|
23549
|
+
const brokerCredential = await verifiedStoredBrokerCredential(localManifest, params.runtime);
|
|
23296
23550
|
const routerToken = hasExplicitOverride ? await resolveRouterToken({
|
|
23297
23551
|
opts: params.opts,
|
|
23298
23552
|
runtime: params.runtime,
|
|
23299
23553
|
existingSecrets: localSecrets,
|
|
23300
23554
|
model: params.model
|
|
23301
23555
|
}) : void 0;
|
|
23302
|
-
|
|
23303
|
-
|
|
23304
|
-
|
|
23305
|
-
|
|
23306
|
-
|
|
23307
|
-
|
|
23308
|
-
|
|
23309
|
-
routerToken ?
|
|
23310
|
-
|
|
23311
|
-
);
|
|
23312
|
-
if (hasManifest) {
|
|
23313
|
-
await writeSecretEnv(params.runtime.configRoot, params.agentName, {
|
|
23314
|
-
...localSecrets,
|
|
23315
|
-
...routerToken ? { MLCLAW_ROUTER_TOKEN: routerToken } : { MLCLAW_BROKER_HF_TOKEN: brokerToken }
|
|
23316
|
-
});
|
|
23317
|
-
}
|
|
23556
|
+
await params.hub.addSpaceSecret(params.repoId, "MLCLAW_BROKER_HF_TOKEN", brokerCredential.token);
|
|
23557
|
+
if (routerToken) {
|
|
23558
|
+
await params.hub.addSpaceSecret(params.repoId, "MLCLAW_ROUTER_TOKEN", routerToken);
|
|
23559
|
+
}
|
|
23560
|
+
await writeSecretEnv(params.runtime.configRoot, params.agentName, {
|
|
23561
|
+
...localSecrets,
|
|
23562
|
+
MLCLAW_BROKER_HF_TOKEN: brokerCredential.token,
|
|
23563
|
+
...routerToken ? { MLCLAW_ROUTER_TOKEN: routerToken } : {}
|
|
23564
|
+
});
|
|
23318
23565
|
}
|
|
23319
23566
|
async function doctor(repoId, opts, hub, runtime) {
|
|
23320
23567
|
if (!repoId.includes("/") && await manifestExists(runtime.configRoot, repoId)) {
|
|
@@ -23409,14 +23656,20 @@ async function doctor(repoId, opts, hub, runtime) {
|
|
|
23409
23656
|
}
|
|
23410
23657
|
}
|
|
23411
23658
|
}
|
|
23412
|
-
if (
|
|
23413
|
-
|
|
23414
|
-
|
|
23415
|
-
|
|
23416
|
-
|
|
23417
|
-
|
|
23418
|
-
issues.push("secret MLCLAW_BROKER_HF_TOKEN is missing");
|
|
23659
|
+
if (fix) {
|
|
23660
|
+
const manifests = (await listManifests(runtime.configRoot)).filter((manifest) => manifest.space === repoId);
|
|
23661
|
+
if (manifests.length !== 1) {
|
|
23662
|
+
throw new Error(
|
|
23663
|
+
`cannot verify MLCLAW_BROKER_HF_TOKEN without one matching local deployment; recover it with \`mlclaw bootstrap --name ${repoId.split("/")[1] ?? "agent"}\``
|
|
23664
|
+
);
|
|
23419
23665
|
}
|
|
23666
|
+
const credential = await verifiedStoredBrokerCredential(manifests[0], runtime);
|
|
23667
|
+
const existed = secrets.has("MLCLAW_BROKER_HF_TOKEN");
|
|
23668
|
+
await hub.addSpaceSecret(repoId, "MLCLAW_BROKER_HF_TOKEN", credential.token);
|
|
23669
|
+
secrets.set("MLCLAW_BROKER_HF_TOKEN", { key: "MLCLAW_BROKER_HF_TOKEN" });
|
|
23670
|
+
fixed.push(`${existed ? "refreshed verified" : "set"} secret MLCLAW_BROKER_HF_TOKEN`);
|
|
23671
|
+
} else if (!secrets.has("MLCLAW_BROKER_HF_TOKEN")) {
|
|
23672
|
+
issues.push("secret MLCLAW_BROKER_HF_TOKEN is missing");
|
|
23420
23673
|
}
|
|
23421
23674
|
const staleTokenSecrets = ["HF_TOKEN", "HUGGINGFACE_HUB_TOKEN"].filter((key) => secrets.has(key));
|
|
23422
23675
|
if (staleTokenSecrets.length > 0) {
|
|
@@ -23634,14 +23887,14 @@ async function setDeploymentSecrets(hub, repoId, secrets, assertMutation = async
|
|
|
23634
23887
|
await hub.addSpaceSecret(repoId, key, value);
|
|
23635
23888
|
}
|
|
23636
23889
|
}
|
|
23637
|
-
async function setSpaceGatewaySecrets(hub, repoId,
|
|
23890
|
+
async function setSpaceGatewaySecrets(hub, repoId, secrets, assertMutation = async () => void 0) {
|
|
23638
23891
|
await setDeploymentSecrets(
|
|
23639
23892
|
hub,
|
|
23640
23893
|
repoId,
|
|
23641
23894
|
{
|
|
23642
23895
|
MLCLAW_SESSION_SECRET: requiredSecret(secrets, "MLCLAW_SESSION_SECRET"),
|
|
23643
23896
|
MLCLAW_CREDENTIAL_KEY: requiredSecret(secrets, "MLCLAW_CREDENTIAL_KEY"),
|
|
23644
|
-
MLCLAW_BROKER_HF_TOKEN:
|
|
23897
|
+
MLCLAW_BROKER_HF_TOKEN: requiredSecret(secrets, "MLCLAW_BROKER_HF_TOKEN"),
|
|
23645
23898
|
...secrets.MLCLAW_ROUTER_TOKEN ? { MLCLAW_ROUTER_TOKEN: secrets.MLCLAW_ROUTER_TOKEN } : {},
|
|
23646
23899
|
...secrets.TELEGRAM_BOT_TOKEN ? { TELEGRAM_BOT_TOKEN: secrets.TELEGRAM_BOT_TOKEN } : {},
|
|
23647
23900
|
...secrets.TELEGRAM_ALLOWED_USERS ? { TELEGRAM_ALLOWED_USERS: secrets.TELEGRAM_ALLOWED_USERS } : {},
|
|
@@ -23768,6 +24021,309 @@ async function readOptionalRouterTokenFile(file) {
|
|
|
23768
24021
|
const parsed = parseSecretEnv(raw);
|
|
23769
24022
|
return nonEmpty(parsed.MLCLAW_ROUTER_TOKEN) ?? nonEmpty(parsed.HF_ROUTER_TOKEN) ?? nonEmpty(raw);
|
|
23770
24023
|
}
|
|
24024
|
+
async function credentialsStatus(requestedAgent, runtime) {
|
|
24025
|
+
const manifest = await credentialManifest(requestedAgent, runtime);
|
|
24026
|
+
await verifiedStoredBrokerCredential(manifest, runtime);
|
|
24027
|
+
const metadata = manifest.brokerCredential;
|
|
24028
|
+
if (!metadata) throw new Error("verified HF Broker credential metadata is missing");
|
|
24029
|
+
runtime.stdout.log(`Agent: ${manifest.agent}`);
|
|
24030
|
+
runtime.stdout.log(`Status: healthy`);
|
|
24031
|
+
runtime.stdout.log(`Profile: ${metadata.profileId}`);
|
|
24032
|
+
runtime.stdout.log(`Account: ${metadata.account}`);
|
|
24033
|
+
runtime.stdout.log(`Fingerprint: ${metadata.fingerprintSha256.slice(0, 12)}`);
|
|
24034
|
+
runtime.stdout.log(`Verified: ${metadata.verifiedAt}`);
|
|
24035
|
+
}
|
|
24036
|
+
async function verifiedStoredBrokerCredential(manifest, runtime) {
|
|
24037
|
+
const secrets = await readSecretEnv(runtime.configRoot, manifest.agent);
|
|
24038
|
+
const token = nonEmpty(secrets.MLCLAW_BROKER_HF_TOKEN);
|
|
24039
|
+
if (!token) throw new Error(`deployment ${manifest.agent} has no dedicated HF Broker credential`);
|
|
24040
|
+
if (!manifest.brokerCredential) {
|
|
24041
|
+
throw new Error(
|
|
24042
|
+
`HF Broker credential metadata is missing; run \`mlclaw credentials repair ${manifest.agent}\` to complete the cutover`
|
|
24043
|
+
);
|
|
24044
|
+
}
|
|
24045
|
+
const verified = await verifyBrokerHfToken(token, manifest.owner, manifest.brokerCredential.account, runtime);
|
|
24046
|
+
const observed = brokerCredentialMetadata(token, verified.identity, runtime.now());
|
|
24047
|
+
if (observed.fingerprintSha256 !== manifest.brokerCredential.fingerprintSha256) {
|
|
24048
|
+
throw new Error(
|
|
24049
|
+
`HF Broker credential fingerprint changed; run \`mlclaw credentials repair ${manifest.agent}\` to reconcile it`
|
|
24050
|
+
);
|
|
24051
|
+
}
|
|
24052
|
+
return verified;
|
|
24053
|
+
}
|
|
24054
|
+
async function credentialsRepair(requestedAgent, opts, runtime) {
|
|
24055
|
+
const selectedManifest = await credentialManifest(requestedAgent, runtime);
|
|
24056
|
+
await withDeploymentLock(runtime.configRoot, deploymentLockKey(selectedManifest), async () => {
|
|
24057
|
+
const manifest = await readManifest(runtime.configRoot, selectedManifest.agent);
|
|
24058
|
+
const secrets = await readSecretEnv(runtime.configRoot, manifest.agent).catch(() => ({}));
|
|
24059
|
+
const oldToken = nonEmpty(secrets.MLCLAW_BROKER_HF_TOKEN);
|
|
24060
|
+
const account = await credentialRepairAccount(manifest, oldToken, runtime);
|
|
24061
|
+
const fileToken = await readOptionalBrokerHfTokenFile(opts.brokerHfTokenFile);
|
|
24062
|
+
const suppliedToken = fileToken ?? nonEmpty(runtime.env.MLCLAW_BROKER_HF_TOKEN);
|
|
24063
|
+
let replacement;
|
|
24064
|
+
if (suppliedToken) {
|
|
24065
|
+
replacement = await verifyBrokerHfToken(suppliedToken, manifest.owner, account, runtime);
|
|
24066
|
+
} else {
|
|
24067
|
+
if (!runtime.prompt.isInteractive()) {
|
|
24068
|
+
throw new Error(
|
|
24069
|
+
"credential repair requires --broker-hf-token-file, MLCLAW_BROKER_HF_TOKEN, or an interactive terminal"
|
|
24070
|
+
);
|
|
24071
|
+
}
|
|
24072
|
+
replacement = await promptForBrokerHfToken(manifest.owner, account, runtime);
|
|
24073
|
+
}
|
|
24074
|
+
const updatedManifest = {
|
|
24075
|
+
...manifest,
|
|
24076
|
+
brokerCredential: brokerCredentialMetadata(replacement.token, replacement.identity, runtime.now()),
|
|
24077
|
+
updatedAt: runtime.now().toISOString()
|
|
24078
|
+
};
|
|
24079
|
+
const updatedSecrets = { ...secrets, MLCLAW_BROKER_HF_TOKEN: replacement.token };
|
|
24080
|
+
const coordinationToken = manifest.gatewayLocation === "space" ? await runtime.readToken(runtime.env) : replacement.token;
|
|
24081
|
+
const coordinationHub = runtime.hubFactory(coordinationToken);
|
|
24082
|
+
const control = await coordinationHub.deploymentControlStore(manifest.owner, manifest.deploymentId);
|
|
24083
|
+
const operation = newOperation(manifest, runtime.now());
|
|
24084
|
+
let lease = await acquireControlLease(control, manifest, operation, runtime.now());
|
|
24085
|
+
let renewalError;
|
|
24086
|
+
let renewal = Promise.resolve();
|
|
24087
|
+
const renewalTimer = setInterval(() => {
|
|
24088
|
+
renewal = renewal.then(async () => {
|
|
24089
|
+
if (renewalError) return;
|
|
24090
|
+
try {
|
|
24091
|
+
lease = await renewControlLease(control, lease, runtime.now());
|
|
24092
|
+
} catch (error) {
|
|
24093
|
+
renewalError = error;
|
|
24094
|
+
}
|
|
24095
|
+
});
|
|
24096
|
+
}, 45e3);
|
|
24097
|
+
const assertLease = async () => {
|
|
24098
|
+
await renewal;
|
|
24099
|
+
if (renewalError) throw renewalError;
|
|
24100
|
+
await assertControlLease(control, lease, runtime.now());
|
|
24101
|
+
if (renewalError) throw renewalError;
|
|
24102
|
+
};
|
|
24103
|
+
try {
|
|
24104
|
+
await assertLease();
|
|
24105
|
+
if (manifest.gatewayLocation === "local") {
|
|
24106
|
+
await applyLocalCredentialRepair(manifest, updatedManifest, secrets, updatedSecrets, runtime, assertLease);
|
|
24107
|
+
} else {
|
|
24108
|
+
await applySpaceCredentialRepair(
|
|
24109
|
+
manifest,
|
|
24110
|
+
updatedManifest,
|
|
24111
|
+
secrets,
|
|
24112
|
+
updatedSecrets,
|
|
24113
|
+
oldToken,
|
|
24114
|
+
coordinationHub,
|
|
24115
|
+
runtime,
|
|
24116
|
+
assertLease
|
|
24117
|
+
);
|
|
24118
|
+
}
|
|
24119
|
+
await assertLease();
|
|
24120
|
+
} finally {
|
|
24121
|
+
clearInterval(renewalTimer);
|
|
24122
|
+
await renewal;
|
|
24123
|
+
await releaseControlLease(control, lease);
|
|
24124
|
+
}
|
|
24125
|
+
runtime.stdout.log(`HF Broker credential repaired: ${manifest.agent}`);
|
|
24126
|
+
});
|
|
24127
|
+
}
|
|
24128
|
+
async function credentialManifest(requestedAgent, runtime) {
|
|
24129
|
+
if (requestedAgent) return await readManifest(runtime.configRoot, requestedAgent);
|
|
24130
|
+
const manifests = await listManifests(runtime.configRoot);
|
|
24131
|
+
if (manifests.length === 1) return manifests[0];
|
|
24132
|
+
if (manifests.length === 0) throw new Error("no ML Claw deployment found");
|
|
24133
|
+
throw new Error("multiple ML Claw deployments found; specify an agent name");
|
|
24134
|
+
}
|
|
24135
|
+
async function credentialRepairAccount(manifest, oldToken, runtime) {
|
|
24136
|
+
if (manifest.brokerCredential) return manifest.brokerCredential.account;
|
|
24137
|
+
if (oldToken) {
|
|
24138
|
+
try {
|
|
24139
|
+
return (await runtime.hubFactory(oldToken).whoami()).name;
|
|
24140
|
+
} catch {
|
|
24141
|
+
}
|
|
24142
|
+
}
|
|
24143
|
+
const provisioningToken = await runtime.readToken(runtime.env);
|
|
24144
|
+
return (await runtime.hubFactory(provisioningToken).whoami()).name;
|
|
24145
|
+
}
|
|
24146
|
+
async function applyLocalCredentialRepair(previousManifest, updatedManifest, previousSecrets, updatedSecrets, runtime, assertLease) {
|
|
24147
|
+
const runner = localRunnerFor(previousManifest, runtime);
|
|
24148
|
+
const existing = await runner.inspect(containerNameFor(previousManifest.agent), localConnectionFor(previousManifest));
|
|
24149
|
+
if (!existing?.running) {
|
|
24150
|
+
await replaceLocalCredentialFiles(
|
|
24151
|
+
previousManifest,
|
|
24152
|
+
updatedManifest,
|
|
24153
|
+
previousSecrets,
|
|
24154
|
+
updatedSecrets,
|
|
24155
|
+
runtime,
|
|
24156
|
+
assertLease
|
|
24157
|
+
);
|
|
24158
|
+
return;
|
|
24159
|
+
}
|
|
24160
|
+
const bucketPrefix = await readDeploymentBucketPrefix(runtime, previousManifest.agent);
|
|
24161
|
+
await deployLocalBootstrap(
|
|
24162
|
+
{
|
|
24163
|
+
agentName: previousManifest.agent,
|
|
24164
|
+
names: namesFor(previousManifest.owner, previousManifest.agent),
|
|
24165
|
+
hasExistingManifest: true,
|
|
24166
|
+
previousManifest,
|
|
24167
|
+
gatewayLocation: "local",
|
|
24168
|
+
...bucketPrefix ? { bucketPrefix } : {},
|
|
24169
|
+
bucketPlan: { bucket: previousManifest.bucket, exists: true, objectCount: 0 },
|
|
24170
|
+
bucket: previousManifest.bucket,
|
|
24171
|
+
manifest: updatedManifest,
|
|
24172
|
+
secrets: updatedSecrets
|
|
24173
|
+
},
|
|
24174
|
+
{ pull: false },
|
|
24175
|
+
runtime,
|
|
24176
|
+
true,
|
|
24177
|
+
assertLease
|
|
24178
|
+
);
|
|
24179
|
+
}
|
|
24180
|
+
async function replaceLocalCredentialFiles(previousManifest, updatedManifest, previousSecrets, updatedSecrets, runtime, assertLease) {
|
|
24181
|
+
try {
|
|
24182
|
+
await assertLease();
|
|
24183
|
+
await writeSecretEnv(runtime.configRoot, updatedManifest.agent, updatedSecrets);
|
|
24184
|
+
await assertLease();
|
|
24185
|
+
await writeManifest(runtime.configRoot, updatedManifest);
|
|
24186
|
+
} catch (error) {
|
|
24187
|
+
await assertLease();
|
|
24188
|
+
await writeSecretEnv(runtime.configRoot, previousManifest.agent, previousSecrets);
|
|
24189
|
+
await assertLease();
|
|
24190
|
+
await writeManifest(runtime.configRoot, previousManifest);
|
|
24191
|
+
throw error;
|
|
24192
|
+
}
|
|
24193
|
+
}
|
|
24194
|
+
async function applySpaceCredentialRepair(previousManifest, updatedManifest, previousSecrets, updatedSecrets, oldToken, hub, runtime, assertLease) {
|
|
24195
|
+
try {
|
|
24196
|
+
await assertLease();
|
|
24197
|
+
await hub.addSpaceSecret(
|
|
24198
|
+
previousManifest.space,
|
|
24199
|
+
"MLCLAW_BROKER_HF_TOKEN",
|
|
24200
|
+
requiredSecret(updatedSecrets, "MLCLAW_BROKER_HF_TOKEN")
|
|
24201
|
+
);
|
|
24202
|
+
await assertLease();
|
|
24203
|
+
await hub.restartSpace(previousManifest.space, true);
|
|
24204
|
+
await replaceLocalCredentialFiles(
|
|
24205
|
+
previousManifest,
|
|
24206
|
+
updatedManifest,
|
|
24207
|
+
previousSecrets,
|
|
24208
|
+
updatedSecrets,
|
|
24209
|
+
runtime,
|
|
24210
|
+
assertLease
|
|
24211
|
+
);
|
|
24212
|
+
} catch (error) {
|
|
24213
|
+
try {
|
|
24214
|
+
await assertLease();
|
|
24215
|
+
if (oldToken) {
|
|
24216
|
+
await hub.addSpaceSecret(previousManifest.space, "MLCLAW_BROKER_HF_TOKEN", oldToken);
|
|
24217
|
+
} else {
|
|
24218
|
+
await hub.deleteSpaceSecret(previousManifest.space, "MLCLAW_BROKER_HF_TOKEN");
|
|
24219
|
+
}
|
|
24220
|
+
await assertLease();
|
|
24221
|
+
await hub.restartSpace(previousManifest.space, true);
|
|
24222
|
+
await assertLease();
|
|
24223
|
+
await writeSecretEnv(runtime.configRoot, previousManifest.agent, previousSecrets);
|
|
24224
|
+
await assertLease();
|
|
24225
|
+
await writeManifest(runtime.configRoot, previousManifest);
|
|
24226
|
+
} catch (rollbackError) {
|
|
24227
|
+
throw new Error(
|
|
24228
|
+
`credential repair failed (${errorMessage(error)}) and rollback failed (${errorMessage(rollbackError)})`
|
|
24229
|
+
);
|
|
24230
|
+
}
|
|
24231
|
+
throw error;
|
|
24232
|
+
}
|
|
24233
|
+
}
|
|
24234
|
+
function deploymentLockKey(manifest) {
|
|
24235
|
+
return createHash4("sha256").update(`${manifest.owner}\0${manifest.agent}`).digest("hex");
|
|
24236
|
+
}
|
|
24237
|
+
async function resolveBrokerHfToken(params) {
|
|
24238
|
+
const fileToken = await readOptionalBrokerHfTokenFile(params.opts.brokerHfTokenFile);
|
|
24239
|
+
const environmentToken = nonEmpty(params.runtime.env.MLCLAW_BROKER_HF_TOKEN);
|
|
24240
|
+
const configuredToken = fileToken ?? environmentToken ?? nonEmpty(params.preferredToken) ?? nonEmpty(params.existingSecrets.MLCLAW_BROKER_HF_TOKEN);
|
|
24241
|
+
if (configuredToken) {
|
|
24242
|
+
try {
|
|
24243
|
+
return await verifyBrokerHfToken(configuredToken, params.owner, params.hfIdentity.name, params.runtime);
|
|
24244
|
+
} catch (error) {
|
|
24245
|
+
if (fileToken || environmentToken || params.preferredToken) throw error;
|
|
24246
|
+
if (params.runtime.prompt.isInteractive()) {
|
|
24247
|
+
params.runtime.prompt.note(
|
|
24248
|
+
`The saved HF Broker credential could not be used (${errorMessage(error)}). It must be replaced before ML Claw can continue.`,
|
|
24249
|
+
"HF Broker credential"
|
|
24250
|
+
);
|
|
24251
|
+
} else {
|
|
24252
|
+
throw new Error(
|
|
24253
|
+
`the saved HF Broker credential is unusable (${errorMessage(error)}); run \`mlclaw credentials repair\` interactively`
|
|
24254
|
+
);
|
|
24255
|
+
}
|
|
24256
|
+
}
|
|
24257
|
+
}
|
|
24258
|
+
if (!params.runtime.prompt.isInteractive()) {
|
|
24259
|
+
throw new Error(
|
|
24260
|
+
"a dedicated HF Broker credential is required; set MLCLAW_BROKER_HF_TOKEN, pass --broker-hf-token-file, or run bootstrap interactively"
|
|
24261
|
+
);
|
|
24262
|
+
}
|
|
24263
|
+
return await promptForBrokerHfToken(params.owner, params.hfIdentity.name, params.runtime);
|
|
24264
|
+
}
|
|
24265
|
+
async function promptForBrokerHfToken(owner, account, runtime) {
|
|
24266
|
+
runtime.prompt.note(
|
|
24267
|
+
"ML Claw will open a Hugging Face token form with BrokerKit's permissions preselected. Create a dedicated token and paste it here. Your current HF CLI login will not be changed.",
|
|
24268
|
+
"HF Broker credential"
|
|
24269
|
+
);
|
|
24270
|
+
const url = buildBrokerTokenUrl(owner, account);
|
|
24271
|
+
const opened = await runtime.hfCli.openUrl(url);
|
|
24272
|
+
runtime.prompt.note(
|
|
24273
|
+
`${opened ? "The token form was opened in your browser." : "Open this token form in your browser."}
|
|
24274
|
+
|
|
24275
|
+
Name and create the token, then copy it. The URL contains permission names only; it contains no credential. The exact URL is printed below.`,
|
|
24276
|
+
"Create the broker token"
|
|
24277
|
+
);
|
|
24278
|
+
runtime.stdout.log(url);
|
|
24279
|
+
for (; ; ) {
|
|
24280
|
+
const replacement = readPromptValue(
|
|
24281
|
+
await runtime.prompt.password({ message: "Paste the new Hugging Face broker token" }),
|
|
24282
|
+
"Hugging Face broker token"
|
|
24283
|
+
);
|
|
24284
|
+
try {
|
|
24285
|
+
const verified = await verifyBrokerHfToken(replacement, owner, account, runtime);
|
|
24286
|
+
runtime.prompt.note(
|
|
24287
|
+
"The dedicated broker token was verified. It will be stored only in ML Claw's trusted broker configuration.",
|
|
24288
|
+
"HF Broker credential ready"
|
|
24289
|
+
);
|
|
24290
|
+
return verified;
|
|
24291
|
+
} catch (error) {
|
|
24292
|
+
runtime.prompt.note(errorMessage(error), "Broker token was not accepted");
|
|
24293
|
+
if (!await promptConfirm("Try another broker token?", true, runtime)) {
|
|
24294
|
+
throw new Error("a valid dedicated HF Broker credential is required");
|
|
24295
|
+
}
|
|
24296
|
+
}
|
|
24297
|
+
}
|
|
24298
|
+
}
|
|
24299
|
+
async function verifyBrokerHfToken(token, owner, expectedAccount, runtime) {
|
|
24300
|
+
const identity = await runtime.hubFactory(token).whoami();
|
|
24301
|
+
if (identity.name !== expectedAccount) {
|
|
24302
|
+
throw new Error(`broker token belongs to ${identity.name}, not ${expectedAccount}`);
|
|
24303
|
+
}
|
|
24304
|
+
const assessment = assessBrokerCredential(identity, owner);
|
|
24305
|
+
if (assessment.status !== "sufficient") {
|
|
24306
|
+
throw new Error(brokerCredentialAssessmentDetail(assessment));
|
|
24307
|
+
}
|
|
24308
|
+
return { token, identity };
|
|
24309
|
+
}
|
|
24310
|
+
async function readOptionalBrokerHfTokenFile(file) {
|
|
24311
|
+
if (!file) return void 0;
|
|
24312
|
+
const raw = await fs16.readFile(file, "utf8");
|
|
24313
|
+
const parsed = parseSecretEnv(raw);
|
|
24314
|
+
const token = nonEmpty(parsed.MLCLAW_BROKER_HF_TOKEN) ?? nonEmpty(raw);
|
|
24315
|
+
if (!token) throw new Error("HF Broker token file is empty");
|
|
24316
|
+
return token;
|
|
24317
|
+
}
|
|
24318
|
+
function brokerCredentialAssessmentDetail(assessment) {
|
|
24319
|
+
if (assessment.status === "unsupported") return assessment.reason;
|
|
24320
|
+
const shown = assessment.missing.slice(0, 8);
|
|
24321
|
+
const remaining = assessment.missing.length - shown.length;
|
|
24322
|
+
return `The HF Broker credential is missing ${assessment.missing.length} required permission${assessment.missing.length === 1 ? "" : "s"}: ${shown.join(", ")}${remaining > 0 ? `, and ${remaining} more` : ""}`;
|
|
24323
|
+
}
|
|
24324
|
+
function errorMessage(error) {
|
|
24325
|
+
return error instanceof Error ? error.message : String(error);
|
|
24326
|
+
}
|
|
23771
24327
|
function isHuggingFaceRouterModel(model) {
|
|
23772
24328
|
return model.trim().startsWith("huggingface/");
|
|
23773
24329
|
}
|
package/entrypoint.sh
CHANGED
|
@@ -15,7 +15,7 @@ PROTECTED_STATE_DIR="/var/lib/mlclaw-protected"
|
|
|
15
15
|
HF_BROKER_STATE_DIR="$PROTECTED_STATE_DIR/hf-broker"
|
|
16
16
|
|
|
17
17
|
prepare_hf_broker() {
|
|
18
|
-
local broker_token="${MLCLAW_BROKER_HF_TOKEN
|
|
18
|
+
local broker_token="${MLCLAW_BROKER_HF_TOKEN:-}"
|
|
19
19
|
if [ -z "$broker_token" ]; then
|
|
20
20
|
echo "[hf-broker] MLCLAW_BROKER_HF_TOKEN is not configured; broker disabled"
|
|
21
21
|
return
|
|
@@ -45,7 +45,7 @@ prepare_hf_broker() {
|
|
|
45
45
|
chmod 0600 "$token_file" "$agent_secret_file" "$operator_secret_file" "$broker_agent_secrets" "$broker_operator_secrets" "$operator_brokers_file"
|
|
46
46
|
|
|
47
47
|
if [ -z "${MLCLAW_STATE_MOUNT_DIR:-}" ]; then
|
|
48
|
-
STATE_HF_TOKEN="$
|
|
48
|
+
STATE_HF_TOKEN="$broker_token"
|
|
49
49
|
fi
|
|
50
50
|
|
|
51
51
|
export MLCLAW_HF_BROKER_URL="http://127.0.0.1:7863"
|
|
@@ -130,8 +130,8 @@ fi
|
|
|
130
130
|
prepare_hf_broker
|
|
131
131
|
export MLCLAW_PROTECTED_STATE_DIR="$PROTECTED_STATE_DIR"
|
|
132
132
|
export MLCLAW_OPENAI_CREDENTIAL_STORE_FILE="$PROTECTED_STATE_DIR/control/openai-api-key.enc"
|
|
133
|
-
# The
|
|
134
|
-
# OpenClaw. The token is already in
|
|
133
|
+
# The broker token and legacy token variables must not enter the control plane
|
|
134
|
+
# or OpenClaw. The broker token is already in its owned runtime file before the
|
|
135
135
|
# environment is scrubbed; local bucket state sync receives a dedicated copy
|
|
136
136
|
# only around trusted restore and supervisor execution.
|
|
137
137
|
unset MLCLAW_BROKER_HF_TOKEN MLCLAW_ROUTER_TOKEN HF_ROUTER_TOKEN HF_TOKEN HUGGINGFACE_HUB_TOKEN
|
package/openclaw.default.json
CHANGED
|
@@ -57,9 +57,11 @@
|
|
|
57
57
|
"session": {
|
|
58
58
|
"dmScope": "per-channel-peer",
|
|
59
59
|
"reset": {
|
|
60
|
-
"mode": "
|
|
61
|
-
"
|
|
62
|
-
|
|
60
|
+
"mode": "idle",
|
|
61
|
+
"idleMinutes": 2147483647
|
|
62
|
+
},
|
|
63
|
+
"maintenance": {
|
|
64
|
+
"resetArchiveRetention": false
|
|
63
65
|
}
|
|
64
66
|
},
|
|
65
67
|
"cron": {
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mlclaw",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"config": {
|
|
6
6
|
"openclawVersion": "2026.7.1",
|
|
7
|
-
"brokerkitVersion": "hf-broker/v0.1
|
|
8
|
-
"brokerkitPluginVersion": "0.
|
|
7
|
+
"brokerkitVersion": "hf-broker/v0.3.1",
|
|
8
|
+
"brokerkitPluginVersion": "0.3.0",
|
|
9
9
|
"runtimeImageRepository": "ghcr.io/osolmaz/mlclaw"
|
|
10
10
|
},
|
|
11
11
|
"repository": {
|
|
@@ -55,8 +55,8 @@
|
|
|
55
55
|
"pack:check": "node scripts/check-package.mjs",
|
|
56
56
|
"prepack": "npm run build",
|
|
57
57
|
"check:secrets": "node scripts/check-secrets.mjs",
|
|
58
|
-
"format": "prettier --check eslint.config.mjs vitest.config.ts vitest.stryker.config.ts package.json README.md src/hf-state-sync/archive.ts src/hf-state-sync/cli.ts src/hf-state-sync/paths.ts src/hf-state-sync/prepare.ts src/hf-state-sync/stage-worker.ts src/hf-state-sync/supervise.ts src/mlclaw/auth.ts src/mlclaw/cli.ts src/mlclaw/deployment-state.ts src/mlclaw/docker.ts src/mlclaw/hf-cli.ts src/mlclaw/hub-api.ts src/mlclaw/local-config.ts src/mlclaw/tailscale.ts src/mlclaw-space-runtime/delegated-brokerkit.ts src/mlclaw-space-runtime/delegated-revisions.ts src/mlclaw-space-runtime/operator-brokers.ts src/mlclaw-space-runtime/openclaw-config.ts src/mlclaw-space-runtime/app.ts src/mlclaw-space-runtime/config.ts src/mlclaw-space-runtime/local-access.ts src/mlclaw-space-runtime/openai-credentials.ts src/mlclaw-space-runtime/proxy.ts src/mlclaw-space-runtime/server.ts src/mlclaw-space-runtime/session.ts src/mlclaw-space-runtime/shell.ts src/mlclaw-control-ui/src/main.tsx src/mlclaw-control-ui/src/styles.css src/mlclaw-control-ui/src/components/ui/utils.ts src/mlclaw/git.ts test/hf-state-sync.archive.test.ts test/hf-state-sync.roundtrip.test.ts test/hf-state-sync.supervise.test.ts test/mlclaw.auth.test.ts test/mlclaw.brokerkit-agent-tools.test.ts test/mlclaw.bundle.test.ts test/mlclaw.cli.test.ts test/mlclaw.delegated-brokerkit.test.ts test/mlclaw.deployment-state.test.ts test/mlclaw.docker.test.ts test/mlclaw.generate-space.test.ts test/mlclaw.hub-api.test.ts test/mlclaw.local-config.test.ts test/mlclaw.operator-brokers.test.ts test/mlclaw.runtime-image.test.ts test/mlclaw.space-runtime.test.ts test/mlclaw.tailscale.test.ts docs/operator-brokers-config.md",
|
|
59
|
-
"format:write": "prettier --write eslint.config.mjs vitest.config.ts vitest.stryker.config.ts package.json README.md src/hf-state-sync/archive.ts src/hf-state-sync/cli.ts src/hf-state-sync/paths.ts src/hf-state-sync/prepare.ts src/hf-state-sync/stage-worker.ts src/hf-state-sync/supervise.ts src/mlclaw/auth.ts src/mlclaw/cli.ts src/mlclaw/deployment-state.ts src/mlclaw/docker.ts src/mlclaw/hf-cli.ts src/mlclaw/hub-api.ts src/mlclaw/local-config.ts src/mlclaw/tailscale.ts src/mlclaw-space-runtime/delegated-brokerkit.ts src/mlclaw-space-runtime/delegated-revisions.ts src/mlclaw-space-runtime/operator-brokers.ts src/mlclaw-space-runtime/openclaw-config.ts src/mlclaw-space-runtime/app.ts src/mlclaw-space-runtime/config.ts src/mlclaw-space-runtime/local-access.ts src/mlclaw-space-runtime/openai-credentials.ts src/mlclaw-space-runtime/proxy.ts src/mlclaw-space-runtime/server.ts src/mlclaw-space-runtime/session.ts src/mlclaw-space-runtime/shell.ts src/mlclaw-control-ui/src/main.tsx src/mlclaw-control-ui/src/styles.css src/mlclaw-control-ui/src/components/ui/utils.ts src/mlclaw/git.ts test/hf-state-sync.archive.test.ts test/hf-state-sync.roundtrip.test.ts test/hf-state-sync.supervise.test.ts test/mlclaw.auth.test.ts test/mlclaw.brokerkit-agent-tools.test.ts test/mlclaw.bundle.test.ts test/mlclaw.cli.test.ts test/mlclaw.delegated-brokerkit.test.ts test/mlclaw.deployment-state.test.ts test/mlclaw.docker.test.ts test/mlclaw.generate-space.test.ts test/mlclaw.hub-api.test.ts test/mlclaw.local-config.test.ts test/mlclaw.operator-brokers.test.ts test/mlclaw.runtime-image.test.ts test/mlclaw.space-runtime.test.ts test/mlclaw.tailscale.test.ts docs/operator-brokers-config.md",
|
|
58
|
+
"format": "prettier --check eslint.config.mjs vitest.config.ts vitest.stryker.config.ts package.json README.md src/hf-state-sync/archive.ts src/hf-state-sync/cli.ts src/hf-state-sync/paths.ts src/hf-state-sync/prepare.ts src/hf-state-sync/stage-worker.ts src/hf-state-sync/supervise.ts src/mlclaw/auth.ts src/mlclaw/cli.ts src/mlclaw/deployment-state.ts src/mlclaw/docker.ts src/mlclaw/hf-broker-credential.ts src/mlclaw/hf-cli.ts src/mlclaw/hub-api.ts src/mlclaw/local-config.ts src/mlclaw/tailscale.ts src/mlclaw-space-runtime/delegated-brokerkit.ts src/mlclaw-space-runtime/delegated-revisions.ts src/mlclaw-space-runtime/operator-brokers.ts src/mlclaw-space-runtime/openclaw-config.ts src/mlclaw-space-runtime/app.ts src/mlclaw-space-runtime/config.ts src/mlclaw-space-runtime/local-access.ts src/mlclaw-space-runtime/openai-credentials.ts src/mlclaw-space-runtime/proxy.ts src/mlclaw-space-runtime/server.ts src/mlclaw-space-runtime/session.ts src/mlclaw-space-runtime/shell.ts src/mlclaw-control-ui/src/main.tsx src/mlclaw-control-ui/src/styles.css src/mlclaw-control-ui/src/components/ui/utils.ts src/mlclaw/git.ts test/hf-state-sync.archive.test.ts test/hf-state-sync.roundtrip.test.ts test/hf-state-sync.supervise.test.ts test/mlclaw.auth.test.ts test/mlclaw.brokerkit-agent-tools.test.ts test/mlclaw.bundle.test.ts test/mlclaw.cli.test.ts test/mlclaw.delegated-brokerkit.test.ts test/mlclaw.deployment-state.test.ts test/mlclaw.docker.test.ts test/mlclaw.generate-space.test.ts test/mlclaw.hf-broker-credential.test.ts test/mlclaw.hub-api.test.ts test/mlclaw.local-config.test.ts test/mlclaw.operator-brokers.test.ts test/mlclaw.runtime-image.test.ts test/mlclaw.space-runtime.test.ts test/mlclaw.tailscale.test.ts docs/operator-brokers-config.md",
|
|
59
|
+
"format:write": "prettier --write eslint.config.mjs vitest.config.ts vitest.stryker.config.ts package.json README.md src/hf-state-sync/archive.ts src/hf-state-sync/cli.ts src/hf-state-sync/paths.ts src/hf-state-sync/prepare.ts src/hf-state-sync/stage-worker.ts src/hf-state-sync/supervise.ts src/mlclaw/auth.ts src/mlclaw/cli.ts src/mlclaw/deployment-state.ts src/mlclaw/docker.ts src/mlclaw/hf-broker-credential.ts src/mlclaw/hf-cli.ts src/mlclaw/hub-api.ts src/mlclaw/local-config.ts src/mlclaw/tailscale.ts src/mlclaw-space-runtime/delegated-brokerkit.ts src/mlclaw-space-runtime/delegated-revisions.ts src/mlclaw-space-runtime/operator-brokers.ts src/mlclaw-space-runtime/openclaw-config.ts src/mlclaw-space-runtime/app.ts src/mlclaw-space-runtime/config.ts src/mlclaw-space-runtime/local-access.ts src/mlclaw-space-runtime/openai-credentials.ts src/mlclaw-space-runtime/proxy.ts src/mlclaw-space-runtime/server.ts src/mlclaw-space-runtime/session.ts src/mlclaw-space-runtime/shell.ts src/mlclaw-control-ui/src/main.tsx src/mlclaw-control-ui/src/styles.css src/mlclaw-control-ui/src/components/ui/utils.ts src/mlclaw/git.ts test/hf-state-sync.archive.test.ts test/hf-state-sync.roundtrip.test.ts test/hf-state-sync.supervise.test.ts test/mlclaw.auth.test.ts test/mlclaw.brokerkit-agent-tools.test.ts test/mlclaw.bundle.test.ts test/mlclaw.cli.test.ts test/mlclaw.delegated-brokerkit.test.ts test/mlclaw.deployment-state.test.ts test/mlclaw.docker.test.ts test/mlclaw.generate-space.test.ts test/mlclaw.hf-broker-credential.test.ts test/mlclaw.hub-api.test.ts test/mlclaw.local-config.test.ts test/mlclaw.operator-brokers.test.ts test/mlclaw.runtime-image.test.ts test/mlclaw.space-runtime.test.ts test/mlclaw.tailscale.test.ts docs/operator-brokers-config.md",
|
|
60
60
|
"lint": "eslint --max-warnings 0 src/mlclaw-space-runtime/delegated-brokerkit.ts src/mlclaw-space-runtime/delegated-revisions.ts src/mlclaw-space-runtime/operator-brokers.ts src/mlclaw-space-runtime/openclaw-config.ts",
|
|
61
61
|
"coverage": "vitest run --coverage",
|
|
62
62
|
"dry": "slophammer-ts dry .",
|