mlclaw 0.4.0 → 0.4.1
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 +2 -2
- package/README.md +36 -9
- package/dist/mlclaw-space-runtime.js +14 -0
- package/dist/mlclaw.mjs +616 -59
- package/entrypoint.sh +4 -4
- package/openclaw.default.json +5 -3
- package/package.json +4 -4
- 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
3
|
ARG BROKERKIT_PLUGIN_VERSION=0.2.1
|
|
4
|
-
ARG BROKERKIT_VERSION=hf-broker/v0.
|
|
5
|
-
ARG MLCLAW_RUNTIME_IMAGE=ghcr.io/osolmaz/mlclaw:0.4.
|
|
4
|
+
ARG BROKERKIT_VERSION=hf-broker/v0.2.0
|
|
5
|
+
ARG MLCLAW_RUNTIME_IMAGE=ghcr.io/osolmaz/mlclaw:0.4.1-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]) {
|
|
@@ -15327,7 +15376,7 @@ import path13 from "node:path";
|
|
|
15327
15376
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
15328
15377
|
var DEFAULT_OPENCLAW_VERSION = "2026.7.1";
|
|
15329
15378
|
var DEFAULT_BROKERKIT_PLUGIN_VERSION = "0.2.1";
|
|
15330
|
-
var DEFAULT_BROKERKIT_VERSION = "hf-broker/v0.
|
|
15379
|
+
var DEFAULT_BROKERKIT_VERSION = "hf-broker/v0.2.0";
|
|
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,165 @@ 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
|
+
"resourceGroup.write",
|
|
20359
|
+
"sql-console.embed.write",
|
|
20360
|
+
"user.billing.read",
|
|
20361
|
+
"user.mcp.read",
|
|
20362
|
+
"user.notifications.read",
|
|
20363
|
+
"user.notifications.write",
|
|
20364
|
+
"user.papers.write",
|
|
20365
|
+
"user.preferences.write",
|
|
20366
|
+
"user.settings.notifications.write",
|
|
20367
|
+
"user.social.likes.write",
|
|
20368
|
+
"user.webhooks.read",
|
|
20369
|
+
"user.webhooks.write"
|
|
20370
|
+
],
|
|
20371
|
+
global_permissions: [
|
|
20372
|
+
"discussion.write",
|
|
20373
|
+
"post.write"
|
|
20374
|
+
],
|
|
20375
|
+
organization_permissions: [
|
|
20376
|
+
"collection.read",
|
|
20377
|
+
"collection.write",
|
|
20378
|
+
"discussion.write",
|
|
20379
|
+
"inference.endpoints.infer.write",
|
|
20380
|
+
"inference.endpoints.write",
|
|
20381
|
+
"inference.serverless.write",
|
|
20382
|
+
"job.write",
|
|
20383
|
+
"org.auditLog.write",
|
|
20384
|
+
"org.billing.read",
|
|
20385
|
+
"org.members.read",
|
|
20386
|
+
"org.members.write",
|
|
20387
|
+
"org.networkSecurity.read",
|
|
20388
|
+
"org.networkSecurity.write",
|
|
20389
|
+
"org.read",
|
|
20390
|
+
"org.repos.read",
|
|
20391
|
+
"org.serviceAccounts.read",
|
|
20392
|
+
"org.serviceAccounts.write",
|
|
20393
|
+
"org.write",
|
|
20394
|
+
"repo.access.read",
|
|
20395
|
+
"repo.content.read",
|
|
20396
|
+
"repo.write",
|
|
20397
|
+
"resourceGroup.write",
|
|
20398
|
+
"sql-console.embed.write"
|
|
20399
|
+
]
|
|
20400
|
+
};
|
|
20401
|
+
|
|
20402
|
+
// src/mlclaw/hf-broker-credential.ts
|
|
20403
|
+
var permissionListSchema = external_exports.array(external_exports.string().min(1)).min(1).superRefine((permissions, context) => {
|
|
20404
|
+
if (permissions.some((permission) => permission !== permission.trim())) {
|
|
20405
|
+
context.addIssue({ code: external_exports.ZodIssueCode.custom, message: "permissions must not contain outer whitespace" });
|
|
20406
|
+
}
|
|
20407
|
+
if (new Set(permissions).size !== permissions.length) {
|
|
20408
|
+
context.addIssue({ code: external_exports.ZodIssueCode.custom, message: "permissions must be unique" });
|
|
20409
|
+
}
|
|
20410
|
+
if (permissions.some((permission, index) => index > 0 && permission < permissions[index - 1])) {
|
|
20411
|
+
context.addIssue({ code: external_exports.ZodIssueCode.custom, message: "permissions must be sorted" });
|
|
20412
|
+
}
|
|
20413
|
+
});
|
|
20414
|
+
var profileSchema = external_exports.object({
|
|
20415
|
+
version: external_exports.literal(1),
|
|
20416
|
+
profile_id: external_exports.literal("hf-broker-complete-v1"),
|
|
20417
|
+
token_form_url: external_exports.literal("https://huggingface.co/settings/tokens/new"),
|
|
20418
|
+
token_type: external_exports.literal("fineGrained"),
|
|
20419
|
+
requires_gated_repositories: external_exports.literal(true),
|
|
20420
|
+
personal_permissions: permissionListSchema,
|
|
20421
|
+
global_permissions: permissionListSchema,
|
|
20422
|
+
organization_permissions: permissionListSchema
|
|
20423
|
+
}).strict();
|
|
20424
|
+
var BROKER_CREDENTIAL_PROFILE = Object.freeze(profileSchema.parse(hf_broker_credential_requirements_default));
|
|
20425
|
+
var HF_TOKEN_CREATE_URL = BROKER_CREDENTIAL_PROFILE.token_form_url;
|
|
20426
|
+
var BROKER_PERSONAL_PERMISSIONS = BROKER_CREDENTIAL_PROFILE.personal_permissions;
|
|
20427
|
+
var BROKER_GLOBAL_PERMISSIONS = BROKER_CREDENTIAL_PROFILE.global_permissions;
|
|
20428
|
+
var BROKER_ORGANIZATION_PERMISSIONS = BROKER_CREDENTIAL_PROFILE.organization_permissions;
|
|
20429
|
+
function buildBrokerTokenUrl(owner, accountName) {
|
|
20430
|
+
const url = new URL(HF_TOKEN_CREATE_URL);
|
|
20431
|
+
url.searchParams.set("tokenType", BROKER_CREDENTIAL_PROFILE.token_type);
|
|
20432
|
+
for (const permission of BROKER_PERSONAL_PERMISSIONS) {
|
|
20433
|
+
url.searchParams.append("ownUserPermissions", permission);
|
|
20434
|
+
}
|
|
20435
|
+
for (const permission of BROKER_GLOBAL_PERMISSIONS) {
|
|
20436
|
+
url.searchParams.append("globalPermissions", permission);
|
|
20437
|
+
}
|
|
20438
|
+
url.searchParams.set("canReadGatedRepos", String(BROKER_CREDENTIAL_PROFILE.requires_gated_repositories));
|
|
20439
|
+
if (owner !== accountName) {
|
|
20440
|
+
url.searchParams.append("orgs", owner);
|
|
20441
|
+
for (const permission of BROKER_ORGANIZATION_PERMISSIONS) {
|
|
20442
|
+
url.searchParams.append("orgPermissions", permission);
|
|
20443
|
+
}
|
|
20444
|
+
}
|
|
20445
|
+
return url.toString();
|
|
20446
|
+
}
|
|
20447
|
+
function assessBrokerCredential(identity, owner) {
|
|
20448
|
+
const accessToken = identity.auth?.accessToken;
|
|
20449
|
+
if (accessToken?.role !== BROKER_CREDENTIAL_PROFILE.token_type) {
|
|
20450
|
+
return {
|
|
20451
|
+
status: "unsupported",
|
|
20452
|
+
reason: "HF Broker requires a dedicated fine-grained Hugging Face token"
|
|
20453
|
+
};
|
|
20454
|
+
}
|
|
20455
|
+
if (!accessToken.fineGrained) {
|
|
20456
|
+
return {
|
|
20457
|
+
status: "unsupported",
|
|
20458
|
+
reason: "Hugging Face omitted this fine-grained token's permission details"
|
|
20459
|
+
};
|
|
20460
|
+
}
|
|
20461
|
+
const personalAvailable = new Set(scopedPermissions(accessToken.fineGrained.scoped, "user", identity.name));
|
|
20462
|
+
const globalAvailable = new Set(accessToken.fineGrained.global);
|
|
20463
|
+
const missing = BROKER_PERSONAL_PERMISSIONS.filter((permission) => !personalAvailable.has(permission));
|
|
20464
|
+
missing.push(
|
|
20465
|
+
...BROKER_GLOBAL_PERMISSIONS.filter((permission) => !globalAvailable.has(permission)).map(
|
|
20466
|
+
(permission) => `global:${permission}`
|
|
20467
|
+
)
|
|
20468
|
+
);
|
|
20469
|
+
if (!accessToken.fineGrained.canReadGatedRepos) {
|
|
20470
|
+
missing.push("canReadGatedRepos");
|
|
20471
|
+
}
|
|
20472
|
+
if (owner !== identity.name) {
|
|
20473
|
+
const organizationAvailable = new Set(scopedPermissions(accessToken.fineGrained.scoped, "org", owner));
|
|
20474
|
+
missing.push(
|
|
20475
|
+
...BROKER_ORGANIZATION_PERMISSIONS.filter((permission) => !organizationAvailable.has(permission)).map(
|
|
20476
|
+
(permission) => `org:${permission}`
|
|
20477
|
+
)
|
|
20478
|
+
);
|
|
20479
|
+
}
|
|
20480
|
+
missing.sort();
|
|
20481
|
+
return missing.length === 0 ? { status: "sufficient" } : { status: "insufficient", missing };
|
|
20482
|
+
}
|
|
20483
|
+
function brokerCredentialMetadata(token, identity, verifiedAt) {
|
|
20484
|
+
return {
|
|
20485
|
+
profileId: BROKER_CREDENTIAL_PROFILE.profile_id,
|
|
20486
|
+
account: identity.name,
|
|
20487
|
+
fingerprintSha256: createHash3("sha256").update(token).digest("hex"),
|
|
20488
|
+
verifiedAt: verifiedAt.toISOString()
|
|
20489
|
+
};
|
|
20490
|
+
}
|
|
20491
|
+
function scopedPermissions(scopes, type, name) {
|
|
20492
|
+
if (!Array.isArray(scopes)) return [];
|
|
20493
|
+
return scopes.filter((scope) => scope.entity.type === type && (!scope.entity.name || scope.entity.name === name)).flatMap((scope) => scope.permissions);
|
|
20494
|
+
}
|
|
20495
|
+
|
|
20282
20496
|
// src/mlclaw/tailscale.ts
|
|
20283
20497
|
import { execFile as execFile3 } from "node:child_process";
|
|
20284
20498
|
import { promisify as promisify3 } from "node:util";
|
|
@@ -20351,7 +20565,7 @@ function parseTailscaleStatus(value) {
|
|
|
20351
20565
|
return { ready: false, reason: "Tailscale returned an invalid status" };
|
|
20352
20566
|
}
|
|
20353
20567
|
if (status.BackendState !== "Running") {
|
|
20354
|
-
return { ready: false, reason: `Tailscale is ${
|
|
20568
|
+
return { ready: false, reason: `Tailscale is ${stringValue2(status.BackendState) ?? "not running"}` };
|
|
20355
20569
|
}
|
|
20356
20570
|
const self = recordValue(status.Self);
|
|
20357
20571
|
if (self?.Online === false) {
|
|
@@ -20361,8 +20575,8 @@ function parseTailscaleStatus(value) {
|
|
|
20361
20575
|
(candidate) => typeof candidate === "string" && isTailscaleIpv4(candidate)
|
|
20362
20576
|
);
|
|
20363
20577
|
if (typeof ipv4 !== "string") return { ready: false, reason: "Tailscale IPv4 address is unavailable" };
|
|
20364
|
-
const dnsName = normalizeTailscaleDnsName(
|
|
20365
|
-
const tailnet =
|
|
20578
|
+
const dnsName = normalizeTailscaleDnsName(stringValue2(self?.DNSName));
|
|
20579
|
+
const tailnet = stringValue2(recordValue(status.CurrentTailnet)?.Name);
|
|
20366
20580
|
return { ready: true, ipv4, ...dnsName ? { dnsName } : {}, ...tailnet ? { tailnet } : {} };
|
|
20367
20581
|
}
|
|
20368
20582
|
var TailscaleApprovalRequiredError = class extends Error {
|
|
@@ -20469,7 +20683,7 @@ function commandErrorMessage(error) {
|
|
|
20469
20683
|
function recordValue(value) {
|
|
20470
20684
|
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
20471
20685
|
}
|
|
20472
|
-
function
|
|
20686
|
+
function stringValue2(value) {
|
|
20473
20687
|
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
20474
20688
|
}
|
|
20475
20689
|
function arrayValue(value) {
|
|
@@ -20541,7 +20755,7 @@ function createProgram(runtimeOverrides = {}) {
|
|
|
20541
20755
|
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
20756
|
"--router-token-file <path>",
|
|
20543
20757
|
"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(
|
|
20758
|
+
).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
20759
|
"--allow-local-fallback",
|
|
20546
20760
|
"Allow non-interactive Space bootstrap to fall back to a ready local runtime",
|
|
20547
20761
|
false
|
|
@@ -20566,6 +20780,13 @@ function createProgram(runtimeOverrides = {}) {
|
|
|
20566
20780
|
const hub = runtime.hubFactory(token);
|
|
20567
20781
|
await settings(repoId, opts, hub, runtime);
|
|
20568
20782
|
});
|
|
20783
|
+
const credentials = program2.command("credentials").description("Inspect or replace trusted broker credentials");
|
|
20784
|
+
credentials.command("status").description("Verify the dedicated HF Broker credential").argument("[agent]", "Agent name; inferred when exactly one deployment exists").action(async (agent) => {
|
|
20785
|
+
await credentialsStatus(agent, runtime);
|
|
20786
|
+
});
|
|
20787
|
+
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) => {
|
|
20788
|
+
await credentialsRepair(agent, opts, runtime);
|
|
20789
|
+
});
|
|
20569
20790
|
const gateway = program2.command("gateway").description("Operate a ML Claw gateway");
|
|
20570
20791
|
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
20792
|
await gatewayStart(agent, opts, runtime);
|
|
@@ -20777,20 +20998,24 @@ async function bootstrap(opts, runtime) {
|
|
|
20777
20998
|
const runtimeImage = resolveRuntimeImage(opts.runtimeImage, runtime.env);
|
|
20778
20999
|
resolveSpaceRuntimeImage(opts, runtime.env);
|
|
20779
21000
|
let plan;
|
|
21001
|
+
let reviewedBrokerHfToken;
|
|
20780
21002
|
for (; ; ) {
|
|
20781
21003
|
plan = await resolveBootstrapPlan({
|
|
20782
21004
|
opts,
|
|
20783
21005
|
owner,
|
|
20784
21006
|
agentName,
|
|
20785
21007
|
hfToken,
|
|
21008
|
+
hfIdentity: me2,
|
|
20786
21009
|
model,
|
|
20787
21010
|
runtimeImage,
|
|
20788
21011
|
hub,
|
|
20789
21012
|
runtime,
|
|
21013
|
+
...reviewedBrokerHfToken ? { providedBrokerHfToken: reviewedBrokerHfToken } : {},
|
|
20790
21014
|
...requestedGatewayLocation ? { requestedGatewayLocation } : {},
|
|
20791
21015
|
...telegramToken ? { telegramToken } : {},
|
|
20792
21016
|
...telegramUserId ? { telegramUserId } : {}
|
|
20793
21017
|
});
|
|
21018
|
+
reviewedBrokerHfToken = plan.secrets.MLCLAW_BROKER_HF_TOKEN;
|
|
20794
21019
|
const alternative = await promptAlternativeBootstrapName({
|
|
20795
21020
|
plan,
|
|
20796
21021
|
explicitBucket: opts.bucket,
|
|
@@ -20866,6 +21091,8 @@ async function bootstrap(opts, runtime) {
|
|
|
20866
21091
|
owner,
|
|
20867
21092
|
agentName,
|
|
20868
21093
|
hfToken,
|
|
21094
|
+
hfIdentity: me2,
|
|
21095
|
+
brokerHfToken: activePlan.secrets.MLCLAW_BROKER_HF_TOKEN ?? hfToken,
|
|
20869
21096
|
model,
|
|
20870
21097
|
runtimeImage,
|
|
20871
21098
|
hub,
|
|
@@ -20923,7 +21150,7 @@ async function bootstrap(opts, runtime) {
|
|
|
20923
21150
|
const secretsChanged = JSON.stringify(previousSecrets) !== JSON.stringify(activePlan.secrets);
|
|
20924
21151
|
if (secretsChanged) {
|
|
20925
21152
|
await assertLease();
|
|
20926
|
-
await setSpaceGatewaySecrets(hub, activePlan.manifest.space,
|
|
21153
|
+
await setSpaceGatewaySecrets(hub, activePlan.manifest.space, activePlan.secrets, assertLease);
|
|
20927
21154
|
if (canDeleteBroadTokenSecrets({
|
|
20928
21155
|
model: activePlan.manifest.model,
|
|
20929
21156
|
routerTokenPresent: hasBrokerOrRouterTokenSecretRecord(activePlan.secrets)
|
|
@@ -21081,7 +21308,7 @@ function assertBootstrapIdentityMatches(plan, identity) {
|
|
|
21081
21308
|
}
|
|
21082
21309
|
async function reconcileManifest(params) {
|
|
21083
21310
|
const { hub, runtime } = params;
|
|
21084
|
-
const localLockKey =
|
|
21311
|
+
const localLockKey = deploymentLockKey(params.manifest);
|
|
21085
21312
|
return await withDeploymentLock(runtime.configRoot, localLockKey, async () => {
|
|
21086
21313
|
let requestedManifest = params.manifest;
|
|
21087
21314
|
const client = hub.bucket(requestedManifest.bucket);
|
|
@@ -21120,7 +21347,7 @@ async function reconcileManifest(params) {
|
|
|
21120
21347
|
const secrets = await ensureDeploymentCredentialKey(runtime, requestedManifest.agent);
|
|
21121
21348
|
requestedManifest = {
|
|
21122
21349
|
...requestedManifest,
|
|
21123
|
-
credentialKeySha256:
|
|
21350
|
+
credentialKeySha256: createHash4("sha256").update(requiredSecret(secrets, "MLCLAW_CREDENTIAL_KEY")).digest("hex")
|
|
21124
21351
|
};
|
|
21125
21352
|
}
|
|
21126
21353
|
const currentDesired = await readDesiredState(client);
|
|
@@ -21240,6 +21467,8 @@ async function resolveBootstrapPlan(params) {
|
|
|
21240
21467
|
agentName,
|
|
21241
21468
|
requestedGatewayLocation,
|
|
21242
21469
|
hfToken,
|
|
21470
|
+
hfIdentity,
|
|
21471
|
+
providedBrokerHfToken,
|
|
21243
21472
|
telegramToken,
|
|
21244
21473
|
telegramUserId,
|
|
21245
21474
|
model,
|
|
@@ -21251,6 +21480,14 @@ async function resolveBootstrapPlan(params) {
|
|
|
21251
21480
|
const now = runtime.now().toISOString();
|
|
21252
21481
|
const existingManifest = await readManifest(runtime.configRoot, agentName).catch(() => null);
|
|
21253
21482
|
const existingSecrets = await readSecretEnv(runtime.configRoot, agentName).catch(() => ({}));
|
|
21483
|
+
const brokerCredential = await resolveBrokerHfToken({
|
|
21484
|
+
opts,
|
|
21485
|
+
owner,
|
|
21486
|
+
hfIdentity,
|
|
21487
|
+
...providedBrokerHfToken ? { preferredToken: providedBrokerHfToken } : {},
|
|
21488
|
+
existingSecrets,
|
|
21489
|
+
runtime
|
|
21490
|
+
});
|
|
21254
21491
|
const sessionSecret = existingSecrets.MLCLAW_SESSION_SECRET ?? randomBytes(48).toString("base64url");
|
|
21255
21492
|
const restoredCredentialKey = existingSecrets.MLCLAW_CREDENTIAL_KEY ?? runtime.env.MLCLAW_CREDENTIAL_KEY;
|
|
21256
21493
|
if (existingManifest?.recoveredWithoutCredentialKey && !restoredCredentialKey) {
|
|
@@ -21259,7 +21496,7 @@ async function resolveBootstrapPlan(params) {
|
|
|
21259
21496
|
);
|
|
21260
21497
|
}
|
|
21261
21498
|
const credentialKey = restoredCredentialKey ?? randomBytes(32).toString("base64url");
|
|
21262
|
-
const credentialKeySha256 =
|
|
21499
|
+
const credentialKeySha256 = createHash4("sha256").update(credentialKey).digest("hex");
|
|
21263
21500
|
if (existingManifest?.credentialKeySha256 && existingManifest.credentialKeySha256 !== credentialKeySha256) {
|
|
21264
21501
|
throw new Error("MLCLAW_CREDENTIAL_KEY does not match the recovered deployment");
|
|
21265
21502
|
}
|
|
@@ -21325,6 +21562,7 @@ async function resolveBootstrapPlan(params) {
|
|
|
21325
21562
|
gatewayLocation,
|
|
21326
21563
|
model: effectiveModel,
|
|
21327
21564
|
runtimeImage: effectiveRuntimeImage,
|
|
21565
|
+
brokerCredential: brokerCredentialMetadata(brokerCredential.token, brokerCredential.identity, runtime.now()),
|
|
21328
21566
|
credentialKeySha256,
|
|
21329
21567
|
...existingManifest?.tailscaleMode ? { tailscaleMode: existingManifest.tailscaleMode } : {},
|
|
21330
21568
|
...existingManifest?.spaceVisibility ? { spaceVisibility: existingManifest.spaceVisibility } : {},
|
|
@@ -21342,7 +21580,7 @@ async function resolveBootstrapPlan(params) {
|
|
|
21342
21580
|
const effectiveTelegramProxy = opts.telegramProxy ?? existingSecrets.TELEGRAM_PROXY;
|
|
21343
21581
|
const effectiveTelegramApiRoot = opts.telegramApiRoot ?? existingSecrets.TELEGRAM_API_ROOT;
|
|
21344
21582
|
const secrets = deploymentSecrets({
|
|
21345
|
-
hfToken,
|
|
21583
|
+
hfToken: brokerCredential.token,
|
|
21346
21584
|
...effectiveTelegramToken ? { telegramToken: effectiveTelegramToken } : {},
|
|
21347
21585
|
...effectiveTelegramUserId ? { telegramUserId: effectiveTelegramUserId } : {},
|
|
21348
21586
|
sessionSecret,
|
|
@@ -21601,6 +21839,8 @@ async function resolveHostedBootstrapFallback(params) {
|
|
|
21601
21839
|
agentName: params.agentName,
|
|
21602
21840
|
requestedGatewayLocation: "local",
|
|
21603
21841
|
hfToken: params.hfToken,
|
|
21842
|
+
hfIdentity: params.hfIdentity,
|
|
21843
|
+
providedBrokerHfToken: params.brokerHfToken,
|
|
21604
21844
|
model: params.model,
|
|
21605
21845
|
runtimeImage: params.runtimeImage,
|
|
21606
21846
|
hub: params.hub,
|
|
@@ -21978,35 +22218,43 @@ async function deployLocalBootstrap(plan, opts, runtime, desiredChanged = true,
|
|
|
21978
22218
|
}
|
|
21979
22219
|
try {
|
|
21980
22220
|
if (networkAccessChanged && plan.manifest.networkAccess) {
|
|
22221
|
+
await assertLease();
|
|
21981
22222
|
await disableNetworkAccess(plan.manifest, runtime);
|
|
21982
22223
|
}
|
|
21983
22224
|
if (previousSecrets) {
|
|
22225
|
+
await assertLease();
|
|
21984
22226
|
await writeSecretEnv(runtime.configRoot, plan.agentName, previousSecrets);
|
|
21985
22227
|
} else {
|
|
22228
|
+
await assertLease();
|
|
21986
22229
|
await fs16.rm(secretEnvPath(runtime.configRoot, plan.agentName), { force: true });
|
|
21987
22230
|
}
|
|
21988
22231
|
if (previousContainer?.running && previousManifest) {
|
|
21989
|
-
await startLocalGateway({ manifest: previousManifest, runtime, pull: false, refresh: true });
|
|
22232
|
+
await startLocalGateway({ manifest: previousManifest, runtime, pull: false, refresh: true, assertLease });
|
|
21990
22233
|
runtime.stdout.log(`Previous local gateway restored: ${containerNameFor(previousManifest.agent)}`);
|
|
21991
22234
|
} else if (previousContainer && previousManifest && startupAttempted) {
|
|
21992
|
-
await startLocalGateway({ manifest: previousManifest, runtime, pull: false, refresh: true });
|
|
22235
|
+
await startLocalGateway({ manifest: previousManifest, runtime, pull: false, refresh: true, assertLease });
|
|
22236
|
+
await assertLease();
|
|
21993
22237
|
await localRunnerFor(previousManifest, runtime).stop(
|
|
21994
22238
|
containerNameFor(previousManifest.agent),
|
|
21995
22239
|
localConnectionFor(previousManifest)
|
|
21996
22240
|
);
|
|
21997
22241
|
if (previousNetworkState !== "owned") {
|
|
22242
|
+
await assertLease();
|
|
21998
22243
|
await disableNetworkAccess(previousManifest, runtime);
|
|
21999
22244
|
}
|
|
22000
22245
|
runtime.stdout.log(`Previous stopped local gateway restored: ${containerNameFor(previousManifest.agent)}`);
|
|
22001
22246
|
} else {
|
|
22002
22247
|
if (startupAttempted) {
|
|
22248
|
+
await assertLease();
|
|
22003
22249
|
await removeFailedBootstrapContainer(plan.manifest, runtime, !previousManifest);
|
|
22004
22250
|
}
|
|
22005
22251
|
}
|
|
22006
22252
|
if (previousNetworkState === "owned" && previousManifest?.networkAccess) {
|
|
22253
|
+
await assertLease();
|
|
22007
22254
|
await runtime.tailscaleRunner.ensureMapping(networkAccessMapping(previousManifest.networkAccess));
|
|
22008
22255
|
}
|
|
22009
22256
|
if (!previousManifest) {
|
|
22257
|
+
await assertLease();
|
|
22010
22258
|
await fs16.rm(manifestPath(runtime.configRoot, plan.agentName), { force: true });
|
|
22011
22259
|
}
|
|
22012
22260
|
} catch (rollbackError) {
|
|
@@ -22112,7 +22360,7 @@ async function deploySpaceGateway(params) {
|
|
|
22112
22360
|
await assertLease();
|
|
22113
22361
|
await clearSpaceGatewayDisabled(hub, manifest.space);
|
|
22114
22362
|
await assertLease();
|
|
22115
|
-
await setSpaceGatewaySecrets(hub, manifest.space,
|
|
22363
|
+
await setSpaceGatewaySecrets(hub, manifest.space, secrets, assertLease);
|
|
22116
22364
|
if (canDeleteBroadTokenSecrets({
|
|
22117
22365
|
model: manifest.model,
|
|
22118
22366
|
routerTokenPresent: hasBrokerOrRouterTokenSecretRecord(secrets)
|
|
@@ -23219,7 +23467,7 @@ async function restoreMatchingDeploymentCredentialKey(runtime, agent, expectedSh
|
|
|
23219
23467
|
const candidates = [suppliedCredentialKey, runtime.env.MLCLAW_CREDENTIAL_KEY, secrets.MLCLAW_CREDENTIAL_KEY].filter(
|
|
23220
23468
|
(value) => Boolean(value)
|
|
23221
23469
|
);
|
|
23222
|
-
const credentialKey = candidates.find((value) =>
|
|
23470
|
+
const credentialKey = candidates.find((value) => createHash4("sha256").update(value).digest("hex") === expectedSha256);
|
|
23223
23471
|
if (!credentialKey) {
|
|
23224
23472
|
throw new Error("local MLCLAW_CREDENTIAL_KEY is missing or does not match the canonical deployment identity");
|
|
23225
23473
|
}
|
|
@@ -23245,7 +23493,7 @@ async function update(repoId, opts, hub, hfToken, runtime) {
|
|
|
23245
23493
|
const runtimeImage = resolveSpaceRuntimeImage(opts, runtime.env);
|
|
23246
23494
|
const agentName = variables.get("OPENCLAW_AGENT_NAME")?.value?.trim() || repoId.split("/")[1] || "openclaw";
|
|
23247
23495
|
if (!canonicalTemplate) {
|
|
23248
|
-
await
|
|
23496
|
+
await ensureUpdateCredentials({
|
|
23249
23497
|
repoId,
|
|
23250
23498
|
agentName,
|
|
23251
23499
|
model: variables.get("OPENCLAW_MODEL")?.value ?? DEFAULT_MODEL2,
|
|
@@ -23282,39 +23530,39 @@ async function update(repoId, opts, hub, hfToken, runtime) {
|
|
|
23282
23530
|
await doctor(repoId, { fix: true }, hub, runtime);
|
|
23283
23531
|
runtime.stdout.log(`Space deployment triggered: ${repoId}`);
|
|
23284
23532
|
}
|
|
23285
|
-
async function
|
|
23286
|
-
if (!isHuggingFaceRouterModel(params.model)) {
|
|
23287
|
-
return;
|
|
23288
|
-
}
|
|
23289
|
-
const spaceSecrets = await params.hub.getSpaceSecrets(params.repoId);
|
|
23533
|
+
async function ensureUpdateCredentials(params) {
|
|
23290
23534
|
const hasExplicitOverride = params.opts.routerToken !== void 0 || params.opts.routerTokenFile !== void 0;
|
|
23291
|
-
if (hasBrokerOrRouterTokenSecretMap(spaceSecrets) && !hasExplicitOverride) {
|
|
23292
|
-
return;
|
|
23293
|
-
}
|
|
23294
23535
|
const hasManifest = await manifestExists(params.runtime.configRoot, params.agentName);
|
|
23295
|
-
|
|
23536
|
+
if (!hasManifest) {
|
|
23537
|
+
throw new Error(
|
|
23538
|
+
`cannot verify the dedicated HF Broker credential; recover the deployment and run \`mlclaw credentials repair ${params.agentName}\``
|
|
23539
|
+
);
|
|
23540
|
+
}
|
|
23541
|
+
const localManifest = await readManifest(params.runtime.configRoot, params.agentName);
|
|
23542
|
+
if (localManifest.space !== params.repoId) {
|
|
23543
|
+
throw new Error(
|
|
23544
|
+
`local deployment ${params.agentName} belongs to ${localManifest.space}, not ${params.repoId}; specify the matching deployment before updating`
|
|
23545
|
+
);
|
|
23546
|
+
}
|
|
23547
|
+
const localSecrets = await readSecretEnv(params.runtime.configRoot, params.agentName).catch(
|
|
23548
|
+
() => ({})
|
|
23549
|
+
);
|
|
23550
|
+
const brokerCredential = await verifiedStoredBrokerCredential(localManifest, params.runtime);
|
|
23296
23551
|
const routerToken = hasExplicitOverride ? await resolveRouterToken({
|
|
23297
23552
|
opts: params.opts,
|
|
23298
23553
|
runtime: params.runtime,
|
|
23299
23554
|
existingSecrets: localSecrets,
|
|
23300
23555
|
model: params.model
|
|
23301
23556
|
}) : 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
|
-
}
|
|
23557
|
+
await params.hub.addSpaceSecret(params.repoId, "MLCLAW_BROKER_HF_TOKEN", brokerCredential.token);
|
|
23558
|
+
if (routerToken) {
|
|
23559
|
+
await params.hub.addSpaceSecret(params.repoId, "MLCLAW_ROUTER_TOKEN", routerToken);
|
|
23560
|
+
}
|
|
23561
|
+
await writeSecretEnv(params.runtime.configRoot, params.agentName, {
|
|
23562
|
+
...localSecrets,
|
|
23563
|
+
MLCLAW_BROKER_HF_TOKEN: brokerCredential.token,
|
|
23564
|
+
...routerToken ? { MLCLAW_ROUTER_TOKEN: routerToken } : {}
|
|
23565
|
+
});
|
|
23318
23566
|
}
|
|
23319
23567
|
async function doctor(repoId, opts, hub, runtime) {
|
|
23320
23568
|
if (!repoId.includes("/") && await manifestExists(runtime.configRoot, repoId)) {
|
|
@@ -23409,14 +23657,20 @@ async function doctor(repoId, opts, hub, runtime) {
|
|
|
23409
23657
|
}
|
|
23410
23658
|
}
|
|
23411
23659
|
}
|
|
23412
|
-
if (
|
|
23413
|
-
|
|
23414
|
-
|
|
23415
|
-
|
|
23416
|
-
|
|
23417
|
-
|
|
23418
|
-
issues.push("secret MLCLAW_BROKER_HF_TOKEN is missing");
|
|
23660
|
+
if (fix) {
|
|
23661
|
+
const manifests = (await listManifests(runtime.configRoot)).filter((manifest) => manifest.space === repoId);
|
|
23662
|
+
if (manifests.length !== 1) {
|
|
23663
|
+
throw new Error(
|
|
23664
|
+
`cannot verify MLCLAW_BROKER_HF_TOKEN without one matching local deployment; recover it with \`mlclaw bootstrap --name ${repoId.split("/")[1] ?? "agent"}\``
|
|
23665
|
+
);
|
|
23419
23666
|
}
|
|
23667
|
+
const credential = await verifiedStoredBrokerCredential(manifests[0], runtime);
|
|
23668
|
+
const existed = secrets.has("MLCLAW_BROKER_HF_TOKEN");
|
|
23669
|
+
await hub.addSpaceSecret(repoId, "MLCLAW_BROKER_HF_TOKEN", credential.token);
|
|
23670
|
+
secrets.set("MLCLAW_BROKER_HF_TOKEN", { key: "MLCLAW_BROKER_HF_TOKEN" });
|
|
23671
|
+
fixed.push(`${existed ? "refreshed verified" : "set"} secret MLCLAW_BROKER_HF_TOKEN`);
|
|
23672
|
+
} else if (!secrets.has("MLCLAW_BROKER_HF_TOKEN")) {
|
|
23673
|
+
issues.push("secret MLCLAW_BROKER_HF_TOKEN is missing");
|
|
23420
23674
|
}
|
|
23421
23675
|
const staleTokenSecrets = ["HF_TOKEN", "HUGGINGFACE_HUB_TOKEN"].filter((key) => secrets.has(key));
|
|
23422
23676
|
if (staleTokenSecrets.length > 0) {
|
|
@@ -23634,14 +23888,14 @@ async function setDeploymentSecrets(hub, repoId, secrets, assertMutation = async
|
|
|
23634
23888
|
await hub.addSpaceSecret(repoId, key, value);
|
|
23635
23889
|
}
|
|
23636
23890
|
}
|
|
23637
|
-
async function setSpaceGatewaySecrets(hub, repoId,
|
|
23891
|
+
async function setSpaceGatewaySecrets(hub, repoId, secrets, assertMutation = async () => void 0) {
|
|
23638
23892
|
await setDeploymentSecrets(
|
|
23639
23893
|
hub,
|
|
23640
23894
|
repoId,
|
|
23641
23895
|
{
|
|
23642
23896
|
MLCLAW_SESSION_SECRET: requiredSecret(secrets, "MLCLAW_SESSION_SECRET"),
|
|
23643
23897
|
MLCLAW_CREDENTIAL_KEY: requiredSecret(secrets, "MLCLAW_CREDENTIAL_KEY"),
|
|
23644
|
-
MLCLAW_BROKER_HF_TOKEN:
|
|
23898
|
+
MLCLAW_BROKER_HF_TOKEN: requiredSecret(secrets, "MLCLAW_BROKER_HF_TOKEN"),
|
|
23645
23899
|
...secrets.MLCLAW_ROUTER_TOKEN ? { MLCLAW_ROUTER_TOKEN: secrets.MLCLAW_ROUTER_TOKEN } : {},
|
|
23646
23900
|
...secrets.TELEGRAM_BOT_TOKEN ? { TELEGRAM_BOT_TOKEN: secrets.TELEGRAM_BOT_TOKEN } : {},
|
|
23647
23901
|
...secrets.TELEGRAM_ALLOWED_USERS ? { TELEGRAM_ALLOWED_USERS: secrets.TELEGRAM_ALLOWED_USERS } : {},
|
|
@@ -23768,6 +24022,309 @@ async function readOptionalRouterTokenFile(file) {
|
|
|
23768
24022
|
const parsed = parseSecretEnv(raw);
|
|
23769
24023
|
return nonEmpty(parsed.MLCLAW_ROUTER_TOKEN) ?? nonEmpty(parsed.HF_ROUTER_TOKEN) ?? nonEmpty(raw);
|
|
23770
24024
|
}
|
|
24025
|
+
async function credentialsStatus(requestedAgent, runtime) {
|
|
24026
|
+
const manifest = await credentialManifest(requestedAgent, runtime);
|
|
24027
|
+
await verifiedStoredBrokerCredential(manifest, runtime);
|
|
24028
|
+
const metadata = manifest.brokerCredential;
|
|
24029
|
+
if (!metadata) throw new Error("verified HF Broker credential metadata is missing");
|
|
24030
|
+
runtime.stdout.log(`Agent: ${manifest.agent}`);
|
|
24031
|
+
runtime.stdout.log(`Status: healthy`);
|
|
24032
|
+
runtime.stdout.log(`Profile: ${metadata.profileId}`);
|
|
24033
|
+
runtime.stdout.log(`Account: ${metadata.account}`);
|
|
24034
|
+
runtime.stdout.log(`Fingerprint: ${metadata.fingerprintSha256.slice(0, 12)}`);
|
|
24035
|
+
runtime.stdout.log(`Verified: ${metadata.verifiedAt}`);
|
|
24036
|
+
}
|
|
24037
|
+
async function verifiedStoredBrokerCredential(manifest, runtime) {
|
|
24038
|
+
const secrets = await readSecretEnv(runtime.configRoot, manifest.agent);
|
|
24039
|
+
const token = nonEmpty(secrets.MLCLAW_BROKER_HF_TOKEN);
|
|
24040
|
+
if (!token) throw new Error(`deployment ${manifest.agent} has no dedicated HF Broker credential`);
|
|
24041
|
+
if (!manifest.brokerCredential) {
|
|
24042
|
+
throw new Error(
|
|
24043
|
+
`HF Broker credential metadata is missing; run \`mlclaw credentials repair ${manifest.agent}\` to complete the cutover`
|
|
24044
|
+
);
|
|
24045
|
+
}
|
|
24046
|
+
const verified = await verifyBrokerHfToken(token, manifest.owner, manifest.brokerCredential.account, runtime);
|
|
24047
|
+
const observed = brokerCredentialMetadata(token, verified.identity, runtime.now());
|
|
24048
|
+
if (observed.fingerprintSha256 !== manifest.brokerCredential.fingerprintSha256) {
|
|
24049
|
+
throw new Error(
|
|
24050
|
+
`HF Broker credential fingerprint changed; run \`mlclaw credentials repair ${manifest.agent}\` to reconcile it`
|
|
24051
|
+
);
|
|
24052
|
+
}
|
|
24053
|
+
return verified;
|
|
24054
|
+
}
|
|
24055
|
+
async function credentialsRepair(requestedAgent, opts, runtime) {
|
|
24056
|
+
const selectedManifest = await credentialManifest(requestedAgent, runtime);
|
|
24057
|
+
await withDeploymentLock(runtime.configRoot, deploymentLockKey(selectedManifest), async () => {
|
|
24058
|
+
const manifest = await readManifest(runtime.configRoot, selectedManifest.agent);
|
|
24059
|
+
const secrets = await readSecretEnv(runtime.configRoot, manifest.agent).catch(() => ({}));
|
|
24060
|
+
const oldToken = nonEmpty(secrets.MLCLAW_BROKER_HF_TOKEN);
|
|
24061
|
+
const account = await credentialRepairAccount(manifest, oldToken, runtime);
|
|
24062
|
+
const fileToken = await readOptionalBrokerHfTokenFile(opts.brokerHfTokenFile);
|
|
24063
|
+
const suppliedToken = fileToken ?? nonEmpty(runtime.env.MLCLAW_BROKER_HF_TOKEN);
|
|
24064
|
+
let replacement;
|
|
24065
|
+
if (suppliedToken) {
|
|
24066
|
+
replacement = await verifyBrokerHfToken(suppliedToken, manifest.owner, account, runtime);
|
|
24067
|
+
} else {
|
|
24068
|
+
if (!runtime.prompt.isInteractive()) {
|
|
24069
|
+
throw new Error(
|
|
24070
|
+
"credential repair requires --broker-hf-token-file, MLCLAW_BROKER_HF_TOKEN, or an interactive terminal"
|
|
24071
|
+
);
|
|
24072
|
+
}
|
|
24073
|
+
replacement = await promptForBrokerHfToken(manifest.owner, account, runtime);
|
|
24074
|
+
}
|
|
24075
|
+
const updatedManifest = {
|
|
24076
|
+
...manifest,
|
|
24077
|
+
brokerCredential: brokerCredentialMetadata(replacement.token, replacement.identity, runtime.now()),
|
|
24078
|
+
updatedAt: runtime.now().toISOString()
|
|
24079
|
+
};
|
|
24080
|
+
const updatedSecrets = { ...secrets, MLCLAW_BROKER_HF_TOKEN: replacement.token };
|
|
24081
|
+
const coordinationToken = manifest.gatewayLocation === "space" ? await runtime.readToken(runtime.env) : replacement.token;
|
|
24082
|
+
const coordinationHub = runtime.hubFactory(coordinationToken);
|
|
24083
|
+
const control = await coordinationHub.deploymentControlStore(manifest.owner, manifest.deploymentId);
|
|
24084
|
+
const operation = newOperation(manifest, runtime.now());
|
|
24085
|
+
let lease = await acquireControlLease(control, manifest, operation, runtime.now());
|
|
24086
|
+
let renewalError;
|
|
24087
|
+
let renewal = Promise.resolve();
|
|
24088
|
+
const renewalTimer = setInterval(() => {
|
|
24089
|
+
renewal = renewal.then(async () => {
|
|
24090
|
+
if (renewalError) return;
|
|
24091
|
+
try {
|
|
24092
|
+
lease = await renewControlLease(control, lease, runtime.now());
|
|
24093
|
+
} catch (error) {
|
|
24094
|
+
renewalError = error;
|
|
24095
|
+
}
|
|
24096
|
+
});
|
|
24097
|
+
}, 45e3);
|
|
24098
|
+
const assertLease = async () => {
|
|
24099
|
+
await renewal;
|
|
24100
|
+
if (renewalError) throw renewalError;
|
|
24101
|
+
await assertControlLease(control, lease, runtime.now());
|
|
24102
|
+
if (renewalError) throw renewalError;
|
|
24103
|
+
};
|
|
24104
|
+
try {
|
|
24105
|
+
await assertLease();
|
|
24106
|
+
if (manifest.gatewayLocation === "local") {
|
|
24107
|
+
await applyLocalCredentialRepair(manifest, updatedManifest, secrets, updatedSecrets, runtime, assertLease);
|
|
24108
|
+
} else {
|
|
24109
|
+
await applySpaceCredentialRepair(
|
|
24110
|
+
manifest,
|
|
24111
|
+
updatedManifest,
|
|
24112
|
+
secrets,
|
|
24113
|
+
updatedSecrets,
|
|
24114
|
+
oldToken,
|
|
24115
|
+
coordinationHub,
|
|
24116
|
+
runtime,
|
|
24117
|
+
assertLease
|
|
24118
|
+
);
|
|
24119
|
+
}
|
|
24120
|
+
await assertLease();
|
|
24121
|
+
} finally {
|
|
24122
|
+
clearInterval(renewalTimer);
|
|
24123
|
+
await renewal;
|
|
24124
|
+
await releaseControlLease(control, lease);
|
|
24125
|
+
}
|
|
24126
|
+
runtime.stdout.log(`HF Broker credential repaired: ${manifest.agent}`);
|
|
24127
|
+
});
|
|
24128
|
+
}
|
|
24129
|
+
async function credentialManifest(requestedAgent, runtime) {
|
|
24130
|
+
if (requestedAgent) return await readManifest(runtime.configRoot, requestedAgent);
|
|
24131
|
+
const manifests = await listManifests(runtime.configRoot);
|
|
24132
|
+
if (manifests.length === 1) return manifests[0];
|
|
24133
|
+
if (manifests.length === 0) throw new Error("no ML Claw deployment found");
|
|
24134
|
+
throw new Error("multiple ML Claw deployments found; specify an agent name");
|
|
24135
|
+
}
|
|
24136
|
+
async function credentialRepairAccount(manifest, oldToken, runtime) {
|
|
24137
|
+
if (manifest.brokerCredential) return manifest.brokerCredential.account;
|
|
24138
|
+
if (oldToken) {
|
|
24139
|
+
try {
|
|
24140
|
+
return (await runtime.hubFactory(oldToken).whoami()).name;
|
|
24141
|
+
} catch {
|
|
24142
|
+
}
|
|
24143
|
+
}
|
|
24144
|
+
const provisioningToken = await runtime.readToken(runtime.env);
|
|
24145
|
+
return (await runtime.hubFactory(provisioningToken).whoami()).name;
|
|
24146
|
+
}
|
|
24147
|
+
async function applyLocalCredentialRepair(previousManifest, updatedManifest, previousSecrets, updatedSecrets, runtime, assertLease) {
|
|
24148
|
+
const runner = localRunnerFor(previousManifest, runtime);
|
|
24149
|
+
const existing = await runner.inspect(containerNameFor(previousManifest.agent), localConnectionFor(previousManifest));
|
|
24150
|
+
if (!existing?.running) {
|
|
24151
|
+
await replaceLocalCredentialFiles(
|
|
24152
|
+
previousManifest,
|
|
24153
|
+
updatedManifest,
|
|
24154
|
+
previousSecrets,
|
|
24155
|
+
updatedSecrets,
|
|
24156
|
+
runtime,
|
|
24157
|
+
assertLease
|
|
24158
|
+
);
|
|
24159
|
+
return;
|
|
24160
|
+
}
|
|
24161
|
+
const bucketPrefix = await readDeploymentBucketPrefix(runtime, previousManifest.agent);
|
|
24162
|
+
await deployLocalBootstrap(
|
|
24163
|
+
{
|
|
24164
|
+
agentName: previousManifest.agent,
|
|
24165
|
+
names: namesFor(previousManifest.owner, previousManifest.agent),
|
|
24166
|
+
hasExistingManifest: true,
|
|
24167
|
+
previousManifest,
|
|
24168
|
+
gatewayLocation: "local",
|
|
24169
|
+
...bucketPrefix ? { bucketPrefix } : {},
|
|
24170
|
+
bucketPlan: { bucket: previousManifest.bucket, exists: true, objectCount: 0 },
|
|
24171
|
+
bucket: previousManifest.bucket,
|
|
24172
|
+
manifest: updatedManifest,
|
|
24173
|
+
secrets: updatedSecrets
|
|
24174
|
+
},
|
|
24175
|
+
{ pull: false },
|
|
24176
|
+
runtime,
|
|
24177
|
+
true,
|
|
24178
|
+
assertLease
|
|
24179
|
+
);
|
|
24180
|
+
}
|
|
24181
|
+
async function replaceLocalCredentialFiles(previousManifest, updatedManifest, previousSecrets, updatedSecrets, runtime, assertLease) {
|
|
24182
|
+
try {
|
|
24183
|
+
await assertLease();
|
|
24184
|
+
await writeSecretEnv(runtime.configRoot, updatedManifest.agent, updatedSecrets);
|
|
24185
|
+
await assertLease();
|
|
24186
|
+
await writeManifest(runtime.configRoot, updatedManifest);
|
|
24187
|
+
} catch (error) {
|
|
24188
|
+
await assertLease();
|
|
24189
|
+
await writeSecretEnv(runtime.configRoot, previousManifest.agent, previousSecrets);
|
|
24190
|
+
await assertLease();
|
|
24191
|
+
await writeManifest(runtime.configRoot, previousManifest);
|
|
24192
|
+
throw error;
|
|
24193
|
+
}
|
|
24194
|
+
}
|
|
24195
|
+
async function applySpaceCredentialRepair(previousManifest, updatedManifest, previousSecrets, updatedSecrets, oldToken, hub, runtime, assertLease) {
|
|
24196
|
+
try {
|
|
24197
|
+
await assertLease();
|
|
24198
|
+
await hub.addSpaceSecret(
|
|
24199
|
+
previousManifest.space,
|
|
24200
|
+
"MLCLAW_BROKER_HF_TOKEN",
|
|
24201
|
+
requiredSecret(updatedSecrets, "MLCLAW_BROKER_HF_TOKEN")
|
|
24202
|
+
);
|
|
24203
|
+
await assertLease();
|
|
24204
|
+
await hub.restartSpace(previousManifest.space, true);
|
|
24205
|
+
await replaceLocalCredentialFiles(
|
|
24206
|
+
previousManifest,
|
|
24207
|
+
updatedManifest,
|
|
24208
|
+
previousSecrets,
|
|
24209
|
+
updatedSecrets,
|
|
24210
|
+
runtime,
|
|
24211
|
+
assertLease
|
|
24212
|
+
);
|
|
24213
|
+
} catch (error) {
|
|
24214
|
+
try {
|
|
24215
|
+
await assertLease();
|
|
24216
|
+
if (oldToken) {
|
|
24217
|
+
await hub.addSpaceSecret(previousManifest.space, "MLCLAW_BROKER_HF_TOKEN", oldToken);
|
|
24218
|
+
} else {
|
|
24219
|
+
await hub.deleteSpaceSecret(previousManifest.space, "MLCLAW_BROKER_HF_TOKEN");
|
|
24220
|
+
}
|
|
24221
|
+
await assertLease();
|
|
24222
|
+
await hub.restartSpace(previousManifest.space, true);
|
|
24223
|
+
await assertLease();
|
|
24224
|
+
await writeSecretEnv(runtime.configRoot, previousManifest.agent, previousSecrets);
|
|
24225
|
+
await assertLease();
|
|
24226
|
+
await writeManifest(runtime.configRoot, previousManifest);
|
|
24227
|
+
} catch (rollbackError) {
|
|
24228
|
+
throw new Error(
|
|
24229
|
+
`credential repair failed (${errorMessage(error)}) and rollback failed (${errorMessage(rollbackError)})`
|
|
24230
|
+
);
|
|
24231
|
+
}
|
|
24232
|
+
throw error;
|
|
24233
|
+
}
|
|
24234
|
+
}
|
|
24235
|
+
function deploymentLockKey(manifest) {
|
|
24236
|
+
return createHash4("sha256").update(`${manifest.owner}\0${manifest.agent}`).digest("hex");
|
|
24237
|
+
}
|
|
24238
|
+
async function resolveBrokerHfToken(params) {
|
|
24239
|
+
const fileToken = await readOptionalBrokerHfTokenFile(params.opts.brokerHfTokenFile);
|
|
24240
|
+
const environmentToken = nonEmpty(params.runtime.env.MLCLAW_BROKER_HF_TOKEN);
|
|
24241
|
+
const configuredToken = fileToken ?? environmentToken ?? nonEmpty(params.preferredToken) ?? nonEmpty(params.existingSecrets.MLCLAW_BROKER_HF_TOKEN);
|
|
24242
|
+
if (configuredToken) {
|
|
24243
|
+
try {
|
|
24244
|
+
return await verifyBrokerHfToken(configuredToken, params.owner, params.hfIdentity.name, params.runtime);
|
|
24245
|
+
} catch (error) {
|
|
24246
|
+
if (fileToken || environmentToken || params.preferredToken) throw error;
|
|
24247
|
+
if (params.runtime.prompt.isInteractive()) {
|
|
24248
|
+
params.runtime.prompt.note(
|
|
24249
|
+
`The saved HF Broker credential could not be used (${errorMessage(error)}). It must be replaced before ML Claw can continue.`,
|
|
24250
|
+
"HF Broker credential"
|
|
24251
|
+
);
|
|
24252
|
+
} else {
|
|
24253
|
+
throw new Error(
|
|
24254
|
+
`the saved HF Broker credential is unusable (${errorMessage(error)}); run \`mlclaw credentials repair\` interactively`
|
|
24255
|
+
);
|
|
24256
|
+
}
|
|
24257
|
+
}
|
|
24258
|
+
}
|
|
24259
|
+
if (!params.runtime.prompt.isInteractive()) {
|
|
24260
|
+
throw new Error(
|
|
24261
|
+
"a dedicated HF Broker credential is required; set MLCLAW_BROKER_HF_TOKEN, pass --broker-hf-token-file, or run bootstrap interactively"
|
|
24262
|
+
);
|
|
24263
|
+
}
|
|
24264
|
+
return await promptForBrokerHfToken(params.owner, params.hfIdentity.name, params.runtime);
|
|
24265
|
+
}
|
|
24266
|
+
async function promptForBrokerHfToken(owner, account, runtime) {
|
|
24267
|
+
runtime.prompt.note(
|
|
24268
|
+
"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.",
|
|
24269
|
+
"HF Broker credential"
|
|
24270
|
+
);
|
|
24271
|
+
const url = buildBrokerTokenUrl(owner, account);
|
|
24272
|
+
const opened = await runtime.hfCli.openUrl(url);
|
|
24273
|
+
runtime.prompt.note(
|
|
24274
|
+
`${opened ? "The token form was opened in your browser." : "Open this token form in your browser."}
|
|
24275
|
+
|
|
24276
|
+
Name and create the token, then copy it. The URL contains permission names only; it contains no credential. The exact URL is printed below.`,
|
|
24277
|
+
"Create the broker token"
|
|
24278
|
+
);
|
|
24279
|
+
runtime.stdout.log(url);
|
|
24280
|
+
for (; ; ) {
|
|
24281
|
+
const replacement = readPromptValue(
|
|
24282
|
+
await runtime.prompt.password({ message: "Paste the new Hugging Face broker token" }),
|
|
24283
|
+
"Hugging Face broker token"
|
|
24284
|
+
);
|
|
24285
|
+
try {
|
|
24286
|
+
const verified = await verifyBrokerHfToken(replacement, owner, account, runtime);
|
|
24287
|
+
runtime.prompt.note(
|
|
24288
|
+
"The dedicated broker token was verified. It will be stored only in ML Claw's trusted broker configuration.",
|
|
24289
|
+
"HF Broker credential ready"
|
|
24290
|
+
);
|
|
24291
|
+
return verified;
|
|
24292
|
+
} catch (error) {
|
|
24293
|
+
runtime.prompt.note(errorMessage(error), "Broker token was not accepted");
|
|
24294
|
+
if (!await promptConfirm("Try another broker token?", true, runtime)) {
|
|
24295
|
+
throw new Error("a valid dedicated HF Broker credential is required");
|
|
24296
|
+
}
|
|
24297
|
+
}
|
|
24298
|
+
}
|
|
24299
|
+
}
|
|
24300
|
+
async function verifyBrokerHfToken(token, owner, expectedAccount, runtime) {
|
|
24301
|
+
const identity = await runtime.hubFactory(token).whoami();
|
|
24302
|
+
if (identity.name !== expectedAccount) {
|
|
24303
|
+
throw new Error(`broker token belongs to ${identity.name}, not ${expectedAccount}`);
|
|
24304
|
+
}
|
|
24305
|
+
const assessment = assessBrokerCredential(identity, owner);
|
|
24306
|
+
if (assessment.status !== "sufficient") {
|
|
24307
|
+
throw new Error(brokerCredentialAssessmentDetail(assessment));
|
|
24308
|
+
}
|
|
24309
|
+
return { token, identity };
|
|
24310
|
+
}
|
|
24311
|
+
async function readOptionalBrokerHfTokenFile(file) {
|
|
24312
|
+
if (!file) return void 0;
|
|
24313
|
+
const raw = await fs16.readFile(file, "utf8");
|
|
24314
|
+
const parsed = parseSecretEnv(raw);
|
|
24315
|
+
const token = nonEmpty(parsed.MLCLAW_BROKER_HF_TOKEN) ?? nonEmpty(raw);
|
|
24316
|
+
if (!token) throw new Error("HF Broker token file is empty");
|
|
24317
|
+
return token;
|
|
24318
|
+
}
|
|
24319
|
+
function brokerCredentialAssessmentDetail(assessment) {
|
|
24320
|
+
if (assessment.status === "unsupported") return assessment.reason;
|
|
24321
|
+
const shown = assessment.missing.slice(0, 8);
|
|
24322
|
+
const remaining = assessment.missing.length - shown.length;
|
|
24323
|
+
return `The HF Broker credential is missing ${assessment.missing.length} required permission${assessment.missing.length === 1 ? "" : "s"}: ${shown.join(", ")}${remaining > 0 ? `, and ${remaining} more` : ""}`;
|
|
24324
|
+
}
|
|
24325
|
+
function errorMessage(error) {
|
|
24326
|
+
return error instanceof Error ? error.message : String(error);
|
|
24327
|
+
}
|
|
23771
24328
|
function isHuggingFaceRouterModel(model) {
|
|
23772
24329
|
return model.trim().startsWith("huggingface/");
|
|
23773
24330
|
}
|
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,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mlclaw",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"config": {
|
|
6
6
|
"openclawVersion": "2026.7.1",
|
|
7
|
-
"brokerkitVersion": "hf-broker/v0.
|
|
7
|
+
"brokerkitVersion": "hf-broker/v0.2.0",
|
|
8
8
|
"brokerkitPluginVersion": "0.2.1",
|
|
9
9
|
"runtimeImageRepository": "ghcr.io/osolmaz/mlclaw"
|
|
10
10
|
},
|
|
@@ -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 .",
|