mlclaw 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mlclaw.mjs CHANGED
@@ -14629,6 +14629,20 @@ var HubApi = class {
14629
14629
  const raw = await this.requestJson(`/api/spaces/${repoId}/secrets`);
14630
14630
  return new Map(Object.entries(raw));
14631
14631
  }
14632
+ async deleteSpaceSecret(repoId, key) {
14633
+ try {
14634
+ await this.requestJson(`/api/spaces/${repoId}/secrets`, {
14635
+ method: "DELETE",
14636
+ body: JSON.stringify({ key }),
14637
+ headers: { "Content-Type": "application/json" }
14638
+ });
14639
+ } catch (err) {
14640
+ if (err instanceof HubApiError2 && err.status === 404) {
14641
+ return;
14642
+ }
14643
+ throw err;
14644
+ }
14645
+ }
14632
14646
  async restartSpace(repoId, factoryReboot = false) {
14633
14647
  await this.requestJson(`/api/spaces/${repoId}/restart`, {
14634
14648
  method: "POST",
@@ -14660,7 +14674,25 @@ var HubApi = class {
14660
14674
  });
14661
14675
  }
14662
14676
  async getSpaceRuntime(repoId) {
14663
- return await this.requestJson(`/api/spaces/${repoId}/runtime`);
14677
+ const runtime = await this.requestJson(`/api/spaces/${repoId}/runtime`);
14678
+ if (Array.isArray(runtime.volumes)) {
14679
+ return runtime;
14680
+ }
14681
+ try {
14682
+ const info = await this.requestJson(`/api/spaces/${repoId}`);
14683
+ if (Array.isArray(info.runtime?.volumes)) {
14684
+ return { ...runtime, volumes: info.runtime.volumes };
14685
+ }
14686
+ } catch {
14687
+ }
14688
+ return runtime;
14689
+ }
14690
+ async setSpaceVolumes(repoId, volumes) {
14691
+ await this.requestJson(`/api/spaces/${repoId}/volumes`, {
14692
+ method: "PUT",
14693
+ body: JSON.stringify({ volumes }),
14694
+ headers: { "Content-Type": "application/json" }
14695
+ });
14664
14696
  }
14665
14697
  async fetchSpaceLogs(repoId, kind = "run") {
14666
14698
  const url = `${this.hubUrl}/api/spaces/${repoId}/logs/${kind}`;
@@ -14804,12 +14836,19 @@ function sseDataToText(raw) {
14804
14836
  import fs11 from "node:fs";
14805
14837
  import path12 from "node:path";
14806
14838
  import { fileURLToPath as fileURLToPath2 } from "node:url";
14807
- var DEFAULT_OPENCLAW_VERSION = "2026.7.1-beta.2";
14839
+ var DEFAULT_OPENCLAW_VERSION = "2026.7.1-beta.5";
14840
+ var DEFAULT_BROKERKIT_PLUGIN_VERSION = "0.1.0";
14841
+ var DEFAULT_BROKERKIT_VERSION = "fcfc5f2304436d952b18e7c9583bb378d1952776";
14808
14842
  var DEFAULT_RUNTIME_IMAGE_REPOSITORY = "ghcr.io/osolmaz/mlclaw";
14809
14843
  var PACKAGE_METADATA = readPackageMetadata();
14810
14844
  var PACKAGE_VERSION = packageString("version", "unknown");
14811
14845
  var OPENCLAW_VERSION = packageConfigString("openclawVersion", DEFAULT_OPENCLAW_VERSION);
14812
14846
  var OPENCLAW_BASE_IMAGE = `ghcr.io/openclaw/openclaw:${OPENCLAW_VERSION}`;
14847
+ var BROKERKIT_PLUGIN_VERSION = packageConfigString(
14848
+ "brokerkitPluginVersion",
14849
+ DEFAULT_BROKERKIT_PLUGIN_VERSION
14850
+ );
14851
+ var BROKERKIT_VERSION = packageConfigString("brokerkitVersion", DEFAULT_BROKERKIT_VERSION);
14813
14852
  var RUNTIME_IMAGE_REPOSITORY = packageConfigString("runtimeImageRepository", DEFAULT_RUNTIME_IMAGE_REPOSITORY);
14814
14853
  var DEFAULT_RUNTIME_IMAGE_TAG = `${PACKAGE_VERSION}-openclaw-${OPENCLAW_VERSION}`;
14815
14854
  var DEFAULT_RUNTIME_IMAGE = `${RUNTIME_IMAGE_REPOSITORY}:${DEFAULT_RUNTIME_IMAGE_TAG}`;
@@ -14903,6 +14942,7 @@ async function currentTemplateRev(sourceDir) {
14903
14942
  async function generateSpaceRepo(sourceDir, outDir, options = {}) {
14904
14943
  const copies = [
14905
14944
  [".gitattributes", ".gitattributes"],
14945
+ ["assets/assistant-avatar.svg", "assets/assistant-avatar.svg"],
14906
14946
  ["assets/hf-logo.svg", "assets/hf-logo.svg"],
14907
14947
  ["assets/mlclaw.svg", "assets/mlclaw.svg"],
14908
14948
  ["assets/mlclaw-control-ui", "assets/mlclaw-control-ui"],
@@ -14915,6 +14955,7 @@ async function generateSpaceRepo(sourceDir, outDir, options = {}) {
14915
14955
  ["dist/hf-tooling-seed.js", "runtime/hf-tooling-seed.js"],
14916
14956
  ["dist/mlclaw-space-runtime.js", "runtime/mlclaw-space-runtime.js"],
14917
14957
  ["entrypoint.sh", "runtime/entrypoint.sh"],
14958
+ ["hf-broker.scope.json", "runtime/hf-broker.scope.json"],
14918
14959
  ["openclaw.default.json", "runtime/openclaw.default.json"],
14919
14960
  ["scripts/configure-huggingface-model.mjs", "runtime/scripts/configure-huggingface-model.mjs"],
14920
14961
  ["scripts/configure-telegram.mjs", "runtime/scripts/configure-telegram.mjs"],
@@ -14935,14 +14976,34 @@ function imageDockerfile(runtimeImage) {
14935
14976
  `;
14936
14977
  }
14937
14978
  function bundledDockerfile() {
14938
- return `FROM ${OPENCLAW_BASE_IMAGE}
14979
+ return `ARG HF_BROKER_VERSION=bb65192b4dca845289427e63e1d5fa72f64914d8
14980
+ ARG BROKERKIT_PLUGIN_VERSION=${BROKERKIT_PLUGIN_VERSION}
14981
+ ARG BROKERKIT_VERSION=${BROKERKIT_VERSION}
14982
+
14983
+ FROM golang:1.26.5-bookworm AS hf-broker-build
14984
+ ARG HF_BROKER_VERSION=bb65192b4dca845289427e63e1d5fa72f64914d8
14985
+ RUN git init /src \\
14986
+ && git -C /src fetch --depth=1 https://github.com/osolmaz/hf-broker.git "$HF_BROKER_VERSION" \\
14987
+ && git -C /src checkout --detach FETCH_HEAD \\
14988
+ && test "$(git -C /src rev-parse HEAD)" = "$HF_BROKER_VERSION" \\
14989
+ && cd /src \\
14990
+ && GOWORK=off go build -trimpath -o /out/hf-broker ./cmd/hf-broker
14991
+
14992
+ FROM node:24-bookworm-slim AS brokerkit-plugin-build
14993
+ ARG BROKERKIT_VERSION
14994
+ RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates git && rm -rf /var/lib/apt/lists/* && git init /src && git -C /src fetch --depth=1 https://github.com/osolmaz/brokerkit.git "$BROKERKIT_VERSION" && git -C /src checkout --detach FETCH_HEAD && test "$(git -C /src rev-parse HEAD)" = "$BROKERKIT_VERSION"
14995
+ WORKDIR /src
14996
+ RUN corepack enable && pnpm install --frozen-lockfile && pnpm --filter openclaw-brokerkit build && pnpm --filter openclaw-brokerkit pack --pack-destination /out
14997
+
14998
+ FROM ${OPENCLAW_BASE_IMAGE}
14939
14999
 
14940
15000
  LABEL org.opencontainers.image.source="https://github.com/osolmaz/mlclaw"
14941
15001
  LABEL org.opencontainers.image.description="ML Claw runtime for OpenClaw on Hugging Face"
14942
15002
 
14943
15003
  USER root
14944
15004
  RUN apt-get update \\
14945
- && apt-get install -y --no-install-recommends gosu python3 python3-pip python3-venv zstd \\
15005
+ && apt-get install -y --no-install-recommends ca-certificates gosu python3 python3-pip python3-venv zstd \\
15006
+ && useradd --system --home-dir /var/lib/hf-broker --create-home --shell /usr/sbin/nologin hf-broker \\
14946
15007
  && rm -rf /var/lib/apt/lists/*
14947
15008
  RUN python3 -m pip install --break-system-packages --no-cache-dir \\
14948
15009
  "huggingface_hub==1.19.0" \\
@@ -14956,10 +15017,15 @@ RUN python3 -m pip install --break-system-packages --no-cache-dir \\
14956
15017
  "uvicorn==0.49.0" \\
14957
15018
  "uv==0.11.28" \\
14958
15019
  "hf-discover==1.3.7"
15020
+ ARG BROKERKIT_PLUGIN_VERSION
15021
+ COPY --from=brokerkit-plugin-build /out/openclaw-brokerkit-\${BROKERKIT_PLUGIN_VERSION}.tgz /tmp/openclaw-brokerkit.tgz
15022
+ RUN npm install --omit=dev --omit=peer --no-audit --no-fund --prefix /opt/openclaw-plugins /tmp/openclaw-brokerkit.tgz && rm /tmp/openclaw-brokerkit.tgz && test -f /opt/openclaw-plugins/node_modules/openclaw-brokerkit/openclaw.plugin.json
14959
15023
 
14960
15024
  COPY --chown=node:node runtime/hf-state-sync.js /app/hf-state-sync.js
14961
15025
  COPY --chown=node:node runtime/hf-tooling-seed.js /app/hf-tooling-seed.js
14962
15026
  COPY --chown=node:node runtime/mlclaw-space-runtime.js /app/mlclaw-space-runtime.js
15027
+ COPY --from=hf-broker-build /out/hf-broker /usr/local/bin/hf-broker
15028
+ COPY runtime/hf-broker.scope.json /app/hf-broker.scope.json
14963
15029
  COPY --chown=node:node runtime/openclaw.default.json /app/openclaw.default.json
14964
15030
  COPY --chown=node:node runtime/entrypoint.sh /app/entrypoint.sh
14965
15031
  COPY --chown=node:node runtime/scripts/ /app/scripts/
@@ -14969,11 +15035,12 @@ RUN chmod +x /app/entrypoint.sh
14969
15035
  ENV PORT=7860
14970
15036
  ENV MLCLAW_OPENCLAW_PORT=7861
14971
15037
  ENV OPENCLAW_GATEWAY_PORT=7861
14972
- ENV OPENCLAW_LIVE_DIR=/tmp/openclaw-live
14973
- ENV OPENCLAW_STATE_DIR=/tmp/openclaw-live/.openclaw
14974
- ENV OPENCLAW_WORKSPACE_DIR=/tmp/openclaw-live/workspace
14975
- ENV OPENCLAW_CONFIG_PATH=/tmp/openclaw-live/.openclaw/openclaw.json
15038
+ ENV OPENCLAW_LIVE_DIR=/home/node/.local/share/mlclaw/live
15039
+ ENV OPENCLAW_STATE_DIR=/home/node/.local/share/mlclaw/live/.openclaw
15040
+ ENV OPENCLAW_WORKSPACE_DIR=/home/node/.local/share/mlclaw/live/workspace
15041
+ ENV OPENCLAW_CONFIG_PATH=/home/node/.local/share/mlclaw/live/.openclaw/openclaw.json
14976
15042
  ENV OPENCLAW_DISABLE_BONJOUR=1
15043
+ ENV MLCLAW_BROKERKIT_PLUGIN_PATH=/opt/openclaw-plugins/node_modules/openclaw-brokerkit
14977
15044
 
14978
15045
  EXPOSE 7860
14979
15046
 
@@ -15258,6 +15325,8 @@ var DEFAULT_LOCAL_PORT = 7860;
15258
15325
  var DEFAULT_SPACE_OPENCLAW_PORT = 7861;
15259
15326
  var LOCAL_VOLUME_MOUNT_PATH = "/tmp/mlclaw-local";
15260
15327
  var LOCAL_LIVE_DIR = `${LOCAL_VOLUME_MOUNT_PATH}/openclaw-live`;
15328
+ var SPACE_STATE_MOUNT_DIR = "/data/mlclaw-state";
15329
+ var SPACE_LIVE_DIR = "/home/node/.local/share/mlclaw/live";
15261
15330
  var SPACE_HANDOFF_TIMEOUT_MS = 12e4;
15262
15331
  var SPACE_HANDOFF_POLL_MS = 5e3;
15263
15332
  var STALE_PATH_VARS = ["OPENCLAW_STATE_DIR", "OPENCLAW_WORKSPACE_DIR", "OPENCLAW_CONFIG_PATH"];
@@ -15295,10 +15364,10 @@ function createProgram(runtimeOverrides = {}) {
15295
15364
  program2.name("mlclaw").description("Deploy OpenClaw to a Hugging Face Space and private bucket").showHelpAfterError().exitOverride((err) => {
15296
15365
  throw err;
15297
15366
  });
15298
- program2.command("bootstrap", { isDefault: true }).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", DEFAULT_MODEL).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("--docker-context <name>", "Docker context for local gateway mode").option("--no-pull", "Do not docker pull before starting a local gateway").option("--takeover", "Start even if a stale runtime lease is present", false).option("--yes", "Confirm paid hardware prompts for automation", false).action(async (opts) => {
15367
+ program2.command("bootstrap", { isDefault: true }).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", DEFAULT_MODEL).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("--router-token-file <path>", "File containing MLCLAW_ROUTER_TOKEN=..., HF_ROUTER_TOKEN=..., or a raw token").option("--docker-context <name>", "Docker context for local gateway mode").option("--no-pull", "Do not docker pull before starting a local gateway").option("--takeover", "Start even if a stale runtime lease is present", false).option("--yes", "Confirm paid hardware prompts for automation", false).action(async (opts) => {
15299
15368
  await bootstrap(opts, runtime);
15300
15369
  });
15301
- program2.command("update").description("Regenerate and upload current ML Claw Space files").argument("<owner/space>", "Hugging Face Space repo ID").option("--runtime-image <image>", "Runtime image to write into the generated Space Dockerfile").option("--bundled-runtime", "Generate a bundled Space runtime instead of using the prebuilt ML Claw image", false).option("--force", "Update even if the Space does not look like ML Claw", false).action(async (repoId, opts) => {
15370
+ program2.command("update").description("Regenerate and upload current ML Claw Space files").argument("<owner/space>", "Hugging Face Space repo ID").option("--runtime-image <image>", "Runtime image to write into the generated Space Dockerfile").option("--bundled-runtime", "Generate a bundled Space runtime instead of using the prebuilt ML Claw image", false).option("--router-token <token>", "Dedicated Hugging Face Router inference token").option("--router-token-file <path>", "File containing MLCLAW_ROUTER_TOKEN=..., HF_ROUTER_TOKEN=..., or a raw token").option("--force", "Update even if the Space does not look like ML Claw", false).action(async (repoId, opts) => {
15302
15371
  const token = await runtime.readToken(runtime.env);
15303
15372
  const hub = runtime.hubFactory(token);
15304
15373
  await update(repoId, opts, hub, token, runtime);
@@ -15321,8 +15390,7 @@ function createProgram(runtimeOverrides = {}) {
15321
15390
  await gatewayStop(agent, runtime);
15322
15391
  });
15323
15392
  gateway.command("restart").argument("<agent>", "Agent name").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) => {
15324
- await gatewayStop(agent, runtime);
15325
- await gatewayStart(agent, opts, runtime);
15393
+ await gatewayRestart(agent, opts, runtime);
15326
15394
  });
15327
15395
  gateway.command("status").argument("<agent>", "Agent name").action(async (agent) => {
15328
15396
  await gatewayStatus(agent, runtime);
@@ -15330,7 +15398,7 @@ function createProgram(runtimeOverrides = {}) {
15330
15398
  gateway.command("logs").argument("<agent>", "Agent name").option("--tail <lines>", "Number of log lines", parseInteger, 200).action(async (agent, opts) => {
15331
15399
  await gatewayLogs(agent, opts, runtime);
15332
15400
  });
15333
- gateway.command("migrate").argument("<agent>", "Agent name").requiredOption("--to <local|space>", "Target gateway location").option("--hardware <flavor>", "Hugging Face Space hardware flavor").option("--sleep-time <seconds>", "Space sleep timeout in seconds; -1 means never sleep", parseInteger).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).option("--docker-context <name>", "Docker context for local gateway startup when migrating to local").option("--no-pull", "Do not docker pull before starting a local gateway").option("--takeover", "Start even if another live runtime lease is present", false).option("--yes", "Confirm paid hardware prompts for automation", false).action(async (agent, opts) => {
15401
+ gateway.command("migrate").argument("<agent>", "Agent name").requiredOption("--to <local|space>", "Target gateway location").option("--hardware <flavor>", "Hugging Face Space hardware flavor").option("--sleep-time <seconds>", "Space sleep timeout in seconds; -1 means never sleep", parseInteger).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).option("--router-token <token>", "Hugging Face Router inference token for Space gateway model calls").option("--router-token-file <path>", "File containing MLCLAW_ROUTER_TOKEN=..., HF_ROUTER_TOKEN=..., or a raw token").option("--docker-context <name>", "Docker context for local gateway startup when migrating to local").option("--no-pull", "Do not docker pull before starting a local gateway").option("--takeover", "Start even if another live runtime lease is present", false).option("--yes", "Confirm paid hardware prompts for automation", false).action(async (agent, opts) => {
15334
15402
  await gatewayMigrate(agent, opts, runtime);
15335
15403
  });
15336
15404
  gateway.command("rebind").argument("<agent>", "Agent name").requiredOption("--docker-context <name>", "Target Docker context").option("--no-pull", "Do not docker pull before starting the rebound local gateway").option("--takeover", "Rebind even if the old Docker context is unavailable", false).action(async (agent, opts) => {
@@ -15403,8 +15471,9 @@ async function bootstrap(opts, runtime) {
15403
15471
  throw new Error("internal error: Space plan was not resolved");
15404
15472
  }
15405
15473
  const paidHardware = await resolveHardware({
15406
- requestedHardware: opts.hardware ?? (telegramToken ? TELEGRAM_HARDWARE : DEFAULT_HARDWARE),
15474
+ ...opts.hardware ? { requestedHardware: opts.hardware } : {},
15407
15475
  ...typeof opts.sleepTime === "number" ? { requestedSleepTime: opts.sleepTime } : telegramToken ? { requestedSleepTime: TELEGRAM_SLEEP_TIME } : {},
15476
+ defaultLabel: spacePlan.exists ? "unchanged Space hardware" : "default free CPU",
15408
15477
  requiresMessagingEgress: Boolean(telegramToken),
15409
15478
  yes: Boolean(opts.yes),
15410
15479
  runtime
@@ -15414,7 +15483,7 @@ async function bootstrap(opts, runtime) {
15414
15483
  bucketPlan,
15415
15484
  spacePlan,
15416
15485
  hasExistingManifest: plan.hasExistingManifest,
15417
- hardware: paidHardware.hardware,
15486
+ hardware: paidHardware.label,
15418
15487
  ...typeof paidHardware.sleepTime === "number" ? { sleepTime: paidHardware.sleepTime } : {},
15419
15488
  yes: Boolean(opts.yes),
15420
15489
  runtime
@@ -15434,9 +15503,9 @@ async function bootstrap(opts, runtime) {
15434
15503
  manifest,
15435
15504
  secrets,
15436
15505
  allowedUsers: me2.name,
15437
- hardware: paidHardware.hardware,
15438
15506
  spaceExists: spacePlan.exists,
15439
15507
  publicSpace: Boolean(opts.publicSpace),
15508
+ ...paidHardware.kind === "explicit" ? { hardware: paidHardware.hardware } : {},
15440
15509
  ...typeof paidHardware.sleepTime === "number" ? { sleepTime: paidHardware.sleepTime } : {},
15441
15510
  ...templateRuntimeImage ? { templateRuntimeImage } : {}
15442
15511
  });
@@ -15492,10 +15561,11 @@ function spacePublicUrl(repoId) {
15492
15561
  async function resolveBootstrapPlan(params) {
15493
15562
  const { opts, owner, agentName, requestedGatewayLocation, hfToken, telegramToken, telegramUserId, model, runtimeImage, hub, runtime } = params;
15494
15563
  const names = namesFor(owner, agentName);
15495
- const sessionSecret = randomBytes(48).toString("base64url");
15496
15564
  const now = runtime.now().toISOString();
15497
15565
  const existingManifest = await readManifest(runtime.configRoot, agentName).catch(() => null);
15498
15566
  const existingSecrets = await readSecretEnv(runtime.configRoot, agentName).catch(() => ({}));
15567
+ const sessionSecret = existingSecrets.MLCLAW_SESSION_SECRET ?? randomBytes(48).toString("base64url");
15568
+ const credentialKey = existingSecrets.MLCLAW_CREDENTIAL_KEY ?? randomBytes(32).toString("base64url");
15499
15569
  const gatewayLocation = requestedGatewayLocation ?? existingManifest?.gatewayLocation ?? DEFAULT_GATEWAY_LOCATION;
15500
15570
  if (opts.dockerContext && gatewayLocation !== "local") {
15501
15571
  throw new Error("--docker-context only applies to local gateway mode");
@@ -15516,6 +15586,12 @@ async function resolveBootstrapPlan(params) {
15516
15586
  hub
15517
15587
  });
15518
15588
  const bucket = bucketPlan.bucket;
15589
+ const routerToken = await resolveRouterToken({
15590
+ opts,
15591
+ runtime,
15592
+ existingSecrets,
15593
+ model
15594
+ });
15519
15595
  const spacePlan = gatewayLocation === "space" ? {
15520
15596
  space: names.space,
15521
15597
  exists: await hub.spaceExists(names.space),
@@ -15540,6 +15616,7 @@ async function resolveBootstrapPlan(params) {
15540
15616
  ...telegramToken ? { telegramToken } : {},
15541
15617
  ...telegramUserId ? { telegramUserId } : {},
15542
15618
  sessionSecret,
15619
+ credentialKey,
15543
15620
  bucket,
15544
15621
  model,
15545
15622
  agentName,
@@ -15548,7 +15625,8 @@ async function resolveBootstrapPlan(params) {
15548
15625
  runtimeId: gatewayLocation === "local" ? manifest.localRuntimeId : spaceRuntimeId(agentName),
15549
15626
  ...bucketPrefix ? { bucketPrefix } : {},
15550
15627
  ...opts.telegramProxy ? { telegramProxy: opts.telegramProxy } : {},
15551
- ...opts.telegramApiRoot ? { telegramApiRoot: opts.telegramApiRoot } : {}
15628
+ ...opts.telegramApiRoot ? { telegramApiRoot: opts.telegramApiRoot } : {},
15629
+ ...routerToken ? { routerToken } : {}
15552
15630
  });
15553
15631
  return {
15554
15632
  agentName,
@@ -15610,6 +15688,8 @@ async function confirmBootstrapPlan(params) {
15610
15688
  if (params.spacePlan) {
15611
15689
  lines.push(`Space: ${params.spacePlan.space} (${params.spacePlan.exists ? "exists; files, variables, secrets, and runtime will be updated" : `will be created as ${params.spacePlan.visibility}`})`);
15612
15690
  lines.push(`Hardware: ${params.hardware}`);
15691
+ lines.push(`Bucket mount: ${SPACE_STATE_MOUNT_DIR}`);
15692
+ lines.push(`Live state: ${SPACE_LIVE_DIR}`);
15613
15693
  if (typeof params.sleepTime === "number") {
15614
15694
  lines.push(`Sleep time: ${params.sleepTime}`);
15615
15695
  }
@@ -15649,6 +15729,10 @@ async function stateAdopt(agent, opts, runtime) {
15649
15729
  const hub = runtime.hubFactory(token);
15650
15730
  const secrets = await readSecretEnv(runtime.configRoot, agent);
15651
15731
  const bucketPrefix = persistedBucketPrefix(secrets);
15732
+ const bucketChanged = current.bucket !== bucket;
15733
+ if (bucketChanged && current.gatewayLocation === "local") {
15734
+ assertDedicatedRouterToken(current.model, secrets);
15735
+ }
15652
15736
  runtime.stdout.log(`Creating or adopting private bucket ${bucket}`);
15653
15737
  await hub.createBucket(bucket, true);
15654
15738
  await inspectStateBucket(hub, bucket, bucketPrefix);
@@ -15659,7 +15743,6 @@ async function stateAdopt(agent, opts, runtime) {
15659
15743
  runtimeId: runtimeIdFor(current),
15660
15744
  takeover: Boolean(opts.takeover)
15661
15745
  });
15662
- const bucketChanged = current.bucket !== bucket;
15663
15746
  if (bucketChanged) {
15664
15747
  await confirmBucketChange({
15665
15748
  message: `Adopt state bucket ${bucket} for ${agent}, replacing ${current.bucket}?`,
@@ -15710,9 +15793,16 @@ async function stateAdopt(agent, opts, runtime) {
15710
15793
  } else {
15711
15794
  await setDeploymentVariables(hub, updated.space, {
15712
15795
  OPENCLAW_HF_STATE_BUCKET: bucket,
15796
+ MLCLAW_STATE_MOUNT_DIR: SPACE_STATE_MOUNT_DIR,
15797
+ OPENCLAW_LIVE_DIR: SPACE_LIVE_DIR,
15798
+ MLCLAW_RUNTIME_SETTINGS_FILE: `${SPACE_LIVE_DIR}/.mlclaw/settings.json`,
15713
15799
  MLCLAW_GATEWAY_LOCATION: "space",
15714
15800
  MLCLAW_RUNTIME_ID: spaceRuntimeId(updated.agent)
15715
15801
  });
15802
+ await ensureSpaceStateVolume(hub, updated.space, bucket);
15803
+ if (canDeleteBroadTokenSecrets({ model: updated.model, routerTokenPresent: hasBrokerOrRouterTokenSecretRecord(secrets) })) {
15804
+ await deleteStaleSpaceTokenSecrets(hub, updated.space);
15805
+ }
15716
15806
  await clearSpaceGatewayDisabled(hub, updated.space);
15717
15807
  if (bucketChanged) {
15718
15808
  await hub.restartSpace(updated.space, true);
@@ -15787,8 +15877,8 @@ function parseBucketId(raw) {
15787
15877
  }
15788
15878
  function deploymentSecrets(params) {
15789
15879
  return {
15790
- HF_TOKEN: params.hfToken,
15791
- HUGGINGFACE_HUB_TOKEN: params.hfToken,
15880
+ MLCLAW_BROKER_HF_TOKEN: params.hfToken,
15881
+ ...params.routerToken ? { MLCLAW_ROUTER_TOKEN: params.routerToken } : {},
15792
15882
  OPENCLAW_HF_STATE_BUCKET: params.bucket,
15793
15883
  OPENCLAW_MODEL: params.model,
15794
15884
  OPENCLAW_AGENT_NAME: params.agentName,
@@ -15796,6 +15886,7 @@ function deploymentSecrets(params) {
15796
15886
  MLCLAW_RUNTIME_IMAGE: params.runtimeImage,
15797
15887
  MLCLAW_RUNTIME_ID: params.runtimeId,
15798
15888
  MLCLAW_SESSION_SECRET: params.sessionSecret,
15889
+ MLCLAW_CREDENTIAL_KEY: params.credentialKey,
15799
15890
  MLCLAW_OPENCLAW_PORT: String(DEFAULT_SPACE_OPENCLAW_PORT),
15800
15891
  OPENCLAW_GATEWAY_PORT: String(DEFAULT_SPACE_OPENCLAW_PORT),
15801
15892
  ...params.telegramToken ? { TELEGRAM_BOT_TOKEN: params.telegramToken } : {},
@@ -15814,10 +15905,14 @@ async function deploySpaceGateway(params) {
15814
15905
  runtime.stdout.log(params.spaceExists ? `Updating existing Space ${manifest.space}` : `Creating ${params.publicSpace ? "public" : "private"} Space ${manifest.space}`);
15815
15906
  await hub.createDockerSpace(manifest.space, {
15816
15907
  private: !params.publicSpace,
15817
- hardware: params.hardware,
15908
+ ...params.hardware && !params.spaceExists ? { hardware: params.hardware } : {},
15818
15909
  ...typeof params.sleepTime === "number" ? { sleepTimeSeconds: params.sleepTime } : {}
15819
15910
  });
15820
- await hub.requestSpaceHardware(manifest.space, params.hardware, params.sleepTime);
15911
+ if (params.hardware && params.spaceExists) {
15912
+ await hub.requestSpaceHardware(manifest.space, params.hardware, params.sleepTime);
15913
+ } else if (!params.hardware && params.spaceExists && typeof params.sleepTime === "number") {
15914
+ await hub.setSpaceSleepTime(manifest.space, params.sleepTime);
15915
+ }
15821
15916
  runtime.stdout.log(params.templateRuntimeImage ? "Generating Space files from prebuilt runtime image" : "Generating bundled Space runtime files");
15822
15917
  const { templateRev } = await runtime.pushTemplateToSpace({
15823
15918
  targetRepo: manifest.space,
@@ -15827,6 +15922,9 @@ async function deploySpaceGateway(params) {
15827
15922
  const spaceRuntimeRef = params.templateRuntimeImage ?? bundledSpaceRuntimeRef(templateRev);
15828
15923
  await setDeploymentVariables(hub, manifest.space, {
15829
15924
  OPENCLAW_HF_STATE_BUCKET: manifest.bucket,
15925
+ MLCLAW_STATE_MOUNT_DIR: SPACE_STATE_MOUNT_DIR,
15926
+ OPENCLAW_LIVE_DIR: SPACE_LIVE_DIR,
15927
+ MLCLAW_RUNTIME_SETTINGS_FILE: `${SPACE_LIVE_DIR}/.mlclaw/settings.json`,
15830
15928
  ...secrets.OPENCLAW_HF_STATE_PREFIX ? { OPENCLAW_HF_STATE_PREFIX: secrets.OPENCLAW_HF_STATE_PREFIX } : {},
15831
15929
  MLCLAW_TEMPLATE_REV: templateRev,
15832
15930
  OPENCLAW_MODEL: manifest.model,
@@ -15840,21 +15938,30 @@ async function deploySpaceGateway(params) {
15840
15938
  MLCLAW_OPENCLAW_PORT: String(DEFAULT_SPACE_OPENCLAW_PORT),
15841
15939
  OPENCLAW_GATEWAY_PORT: String(DEFAULT_SPACE_OPENCLAW_PORT)
15842
15940
  });
15941
+ await ensureSpaceStateVolume(hub, manifest.space, manifest.bucket, { allowMissingVolumes: !params.spaceExists });
15843
15942
  await clearSpaceGatewayDisabled(hub, manifest.space);
15844
15943
  await setDeploymentSecrets(hub, manifest.space, {
15845
- HF_TOKEN: requiredSecret(secrets, "HF_TOKEN"),
15846
- HUGGINGFACE_HUB_TOKEN: requiredSecret(secrets, "HF_TOKEN"),
15847
15944
  MLCLAW_SESSION_SECRET: requiredSecret(secrets, "MLCLAW_SESSION_SECRET"),
15945
+ MLCLAW_CREDENTIAL_KEY: requiredSecret(secrets, "MLCLAW_CREDENTIAL_KEY"),
15946
+ MLCLAW_BROKER_HF_TOKEN: hfToken,
15947
+ ...secrets.MLCLAW_ROUTER_TOKEN ? { MLCLAW_ROUTER_TOKEN: secrets.MLCLAW_ROUTER_TOKEN } : {},
15848
15948
  ...secrets.TELEGRAM_BOT_TOKEN ? { TELEGRAM_BOT_TOKEN: secrets.TELEGRAM_BOT_TOKEN } : {},
15849
15949
  ...secrets.TELEGRAM_ALLOWED_USERS ? { TELEGRAM_ALLOWED_USERS: secrets.TELEGRAM_ALLOWED_USERS } : {},
15850
15950
  ...secrets.TELEGRAM_PROXY ? { TELEGRAM_PROXY: secrets.TELEGRAM_PROXY } : {},
15851
15951
  ...secrets.TELEGRAM_API_ROOT ? { TELEGRAM_API_ROOT: secrets.TELEGRAM_API_ROOT } : {}
15852
15952
  });
15953
+ if (canDeleteBroadTokenSecrets({ model: manifest.model, routerTokenPresent: hasBrokerOrRouterTokenSecretRecord(secrets) })) {
15954
+ await deleteStaleSpaceTokenSecrets(hub, manifest.space);
15955
+ } else {
15956
+ runtime.stdout.log("Keeping legacy broad Hub token secrets until an HF Broker or Router credential is configured");
15957
+ }
15853
15958
  await hub.restartSpace(manifest.space, true);
15854
15959
  return { runtimeImage: spaceRuntimeRef };
15855
15960
  }
15856
15961
  async function startLocalGateway(params) {
15857
15962
  const { manifest, runtime } = params;
15963
+ const secrets = await ensureDeploymentCredentialKey(runtime, manifest.agent);
15964
+ assertDedicatedRouterToken(manifest.model, secrets);
15858
15965
  const containerName = containerNameFor(manifest.agent);
15859
15966
  const volumeName = volumeNameFor(manifest.agent);
15860
15967
  const dockerContext = dockerContextFor(manifest);
@@ -15927,6 +16034,17 @@ async function gatewayStart(agent, opts, runtime) {
15927
16034
  runtime.stdout.log(`Space gateway restart requested: ${manifest.space}`);
15928
16035
  }
15929
16036
  }
16037
+ async function gatewayRestart(agent, opts, runtime) {
16038
+ const manifest = await readDeploymentManifest(runtime, agent, { requestedDockerContext: opts.dockerContext });
16039
+ if (manifest.gatewayLocation === "local") {
16040
+ assertDedicatedRouterToken(
16041
+ manifest.model,
16042
+ await readSecretEnv(runtime.configRoot, agent).catch(() => ({}))
16043
+ );
16044
+ }
16045
+ await gatewayStop(agent, runtime);
16046
+ await gatewayStart(agent, opts, runtime);
16047
+ }
15930
16048
  async function gatewayStop(agent, runtime) {
15931
16049
  const manifest = await readDeploymentManifest(runtime, agent);
15932
16050
  const bucketPrefix = await readDeploymentBucketPrefix(runtime, agent);
@@ -15994,7 +16112,7 @@ async function gatewayMigrate(agent, opts, runtime) {
15994
16112
  }
15995
16113
  const token = await runtime.readToken(runtime.env);
15996
16114
  const hub = runtime.hubFactory(token);
15997
- const secrets = await readSecretEnv(runtime.configRoot, agent);
16115
+ const secrets = await ensureDeploymentCredentialKey(runtime, agent);
15998
16116
  const bucketPrefix = persistedBucketPrefix(secrets);
15999
16117
  const updated = {
16000
16118
  ...current,
@@ -16002,10 +16120,23 @@ async function gatewayMigrate(agent, opts, runtime) {
16002
16120
  runtimeImage: resolveRuntimeImage(opts.runtimeImage ?? current.runtimeImage, runtime.env),
16003
16121
  updatedAt: runtime.now().toISOString()
16004
16122
  };
16123
+ const routerToken = await resolveRouterToken({
16124
+ opts,
16125
+ runtime,
16126
+ existingSecrets: secrets,
16127
+ model: updated.model
16128
+ });
16005
16129
  if (target === "space") {
16130
+ const deploymentSecrets2 = {
16131
+ ...secrets,
16132
+ MLCLAW_GATEWAY_LOCATION: "space",
16133
+ MLCLAW_RUNTIME_IMAGE: updated.runtimeImage,
16134
+ ...routerToken ? { MLCLAW_ROUTER_TOKEN: routerToken } : {}
16135
+ };
16006
16136
  const paidHardware = await resolveHardware({
16007
- requestedHardware: opts.hardware ?? (secrets.TELEGRAM_BOT_TOKEN ? TELEGRAM_HARDWARE : DEFAULT_HARDWARE),
16137
+ ...opts.hardware ? { requestedHardware: opts.hardware } : {},
16008
16138
  ...typeof opts.sleepTime === "number" ? { requestedSleepTime: opts.sleepTime } : secrets.TELEGRAM_BOT_TOKEN ? { requestedSleepTime: TELEGRAM_SLEEP_TIME } : {},
16139
+ defaultLabel: "unchanged Space hardware",
16009
16140
  requiresMessagingEgress: Boolean(secrets.TELEGRAM_BOT_TOKEN),
16010
16141
  yes: Boolean(opts.yes),
16011
16142
  runtime
@@ -16020,24 +16151,22 @@ async function gatewayMigrate(agent, opts, runtime) {
16020
16151
  await handoffAndStopLocalGateway({ manifest: current, hub, runtime, bucketPrefix });
16021
16152
  const me2 = await hub.whoami();
16022
16153
  const templateRuntimeImage = resolveSpaceRuntimeImage(opts, runtime.env);
16154
+ const spaceExists = await hub.spaceExists(updated.space);
16023
16155
  await deploySpaceGateway({
16024
16156
  hub,
16025
16157
  runtime,
16026
16158
  hfToken: token,
16027
16159
  manifest: updated,
16028
- secrets: {
16029
- ...secrets,
16030
- MLCLAW_GATEWAY_LOCATION: "space",
16031
- MLCLAW_RUNTIME_IMAGE: updated.runtimeImage
16032
- },
16160
+ secrets: deploymentSecrets2,
16033
16161
  allowedUsers: me2.name,
16034
- hardware: paidHardware.hardware,
16035
16162
  publicSpace: Boolean(opts.publicSpace),
16163
+ spaceExists,
16164
+ ...paidHardware.kind === "explicit" ? { hardware: paidHardware.hardware } : {},
16036
16165
  ...typeof paidHardware.sleepTime === "number" ? { sleepTime: paidHardware.sleepTime } : {},
16037
16166
  ...templateRuntimeImage ? { templateRuntimeImage } : {}
16038
16167
  });
16039
16168
  await writeSecretEnv(runtime.configRoot, agent, {
16040
- ...secrets,
16169
+ ...deploymentSecrets2,
16041
16170
  MLCLAW_GATEWAY_LOCATION: "space",
16042
16171
  MLCLAW_RUNTIME_IMAGE: updated.runtimeImage,
16043
16172
  MLCLAW_RUNTIME_ID: spaceRuntimeId(agent)
@@ -16069,6 +16198,7 @@ async function gatewayMigrate(agent, opts, runtime) {
16069
16198
  });
16070
16199
  await writeSecretEnv(runtime.configRoot, agent, {
16071
16200
  ...secrets,
16201
+ ...routerToken ? { MLCLAW_ROUTER_TOKEN: routerToken } : {},
16072
16202
  MLCLAW_GATEWAY_LOCATION: "local",
16073
16203
  MLCLAW_RUNTIME_IMAGE: updated.runtimeImage,
16074
16204
  MLCLAW_RUNTIME_ID: updated.localRuntimeId
@@ -16084,6 +16214,10 @@ async function gatewayRebind(agent, opts, runtime) {
16084
16214
  if (current.gatewayLocation !== "local") {
16085
16215
  throw new Error("Docker context rebind only applies to local gateway deployments");
16086
16216
  }
16217
+ assertDedicatedRouterToken(
16218
+ current.model,
16219
+ await readSecretEnv(runtime.configRoot, agent).catch(() => ({}))
16220
+ );
16087
16221
  const targetBinding = await resolveLocalGatewayBinding({
16088
16222
  manifest: void 0,
16089
16223
  requestedContext: targetContext,
@@ -16357,6 +16491,18 @@ function requiredSecret(secrets, key) {
16357
16491
  }
16358
16492
  return value;
16359
16493
  }
16494
+ async function ensureDeploymentCredentialKey(runtime, agent, existing) {
16495
+ const secrets = existing ?? await readSecretEnv(runtime.configRoot, agent).catch(() => ({}));
16496
+ if (secrets.MLCLAW_CREDENTIAL_KEY) {
16497
+ return secrets;
16498
+ }
16499
+ const updated = {
16500
+ ...secrets,
16501
+ MLCLAW_CREDENTIAL_KEY: randomBytes(32).toString("base64url")
16502
+ };
16503
+ await writeSecretEnv(runtime.configRoot, agent, updated);
16504
+ return updated;
16505
+ }
16360
16506
  function requiredOption(value, label) {
16361
16507
  if (!value) {
16362
16508
  throw new Error(`${label} is required`);
@@ -16374,6 +16520,16 @@ async function update(repoId, opts, hub, hfToken, runtime) {
16374
16520
  }
16375
16521
  const runtimeImage = resolveSpaceRuntimeImage(opts, runtime.env);
16376
16522
  const agentName = variables.get("OPENCLAW_AGENT_NAME")?.value?.trim() || repoId.split("/")[1] || "openclaw";
16523
+ if (!canonicalTemplate) {
16524
+ await ensureUpdateRouterToken({
16525
+ repoId,
16526
+ agentName,
16527
+ model: variables.get("OPENCLAW_MODEL")?.value ?? DEFAULT_MODEL,
16528
+ opts,
16529
+ hub,
16530
+ runtime
16531
+ });
16532
+ }
16377
16533
  runtime.stdout.log(`Generating current Space files into ${repoId}`);
16378
16534
  const { templateRev } = await runtime.pushTemplateToSpace({
16379
16535
  targetRepo: repoId,
@@ -16392,9 +16548,50 @@ async function update(repoId, opts, hub, hfToken, runtime) {
16392
16548
  await hub.addSpaceVariable(repoId, "MLCLAW_RUNTIME_ID", spaceRuntimeId(agentName));
16393
16549
  await hub.addSpaceVariable(repoId, "MLCLAW_OPENCLAW_PORT", String(DEFAULT_SPACE_OPENCLAW_PORT));
16394
16550
  await hub.addSpaceVariable(repoId, "OPENCLAW_GATEWAY_PORT", String(DEFAULT_SPACE_OPENCLAW_PORT));
16551
+ const bucket = variables.get("OPENCLAW_HF_STATE_BUCKET")?.value;
16552
+ if (bucket) {
16553
+ await hub.addSpaceVariable(repoId, "MLCLAW_STATE_MOUNT_DIR", SPACE_STATE_MOUNT_DIR);
16554
+ await hub.addSpaceVariable(repoId, "OPENCLAW_LIVE_DIR", SPACE_LIVE_DIR);
16555
+ await hub.addSpaceVariable(repoId, "MLCLAW_RUNTIME_SETTINGS_FILE", `${SPACE_LIVE_DIR}/.mlclaw/settings.json`);
16556
+ await ensureSpaceStateVolume(hub, repoId, bucket);
16557
+ }
16395
16558
  await doctor(repoId, { fix: true }, hub, runtime);
16396
16559
  await hub.restartSpace(repoId, true);
16397
16560
  }
16561
+ async function ensureUpdateRouterToken(params) {
16562
+ if (!isHuggingFaceRouterModel(params.model)) {
16563
+ return;
16564
+ }
16565
+ const spaceSecrets = await params.hub.getSpaceSecrets(params.repoId);
16566
+ const hasExplicitOverride = params.opts.routerToken !== void 0 || params.opts.routerTokenFile !== void 0;
16567
+ if (hasBrokerOrRouterTokenSecretMap(spaceSecrets) && !hasExplicitOverride) {
16568
+ return;
16569
+ }
16570
+ const hasManifest = await manifestExists(params.runtime.configRoot, params.agentName);
16571
+ const localSecrets = hasManifest ? await readSecretEnv(params.runtime.configRoot, params.agentName).catch(() => ({})) : {};
16572
+ const routerToken = hasExplicitOverride ? await resolveRouterToken({
16573
+ opts: params.opts,
16574
+ runtime: params.runtime,
16575
+ existingSecrets: localSecrets,
16576
+ model: params.model
16577
+ }) : void 0;
16578
+ const brokerToken = routerToken ? void 0 : await params.runtime.readToken(params.runtime.env);
16579
+ const credential = routerToken ?? brokerToken;
16580
+ if (!credential) {
16581
+ throw new Error("Hugging Face broker credential is unavailable");
16582
+ }
16583
+ await params.hub.addSpaceSecret(
16584
+ params.repoId,
16585
+ routerToken ? "MLCLAW_ROUTER_TOKEN" : "MLCLAW_BROKER_HF_TOKEN",
16586
+ credential
16587
+ );
16588
+ if (hasManifest) {
16589
+ await writeSecretEnv(params.runtime.configRoot, params.agentName, {
16590
+ ...localSecrets,
16591
+ ...routerToken ? { MLCLAW_ROUTER_TOKEN: routerToken } : { MLCLAW_BROKER_HF_TOKEN: brokerToken }
16592
+ });
16593
+ }
16594
+ }
16398
16595
  async function doctor(repoId, opts, hub, runtime) {
16399
16596
  if (!repoId.includes("/") && await manifestExists(runtime.configRoot, repoId)) {
16400
16597
  await gatewayStatus(repoId, runtime);
@@ -16453,6 +16650,31 @@ async function doctor(repoId, opts, hub, runtime) {
16453
16650
  await hub.addSpaceVariable(repoId, "OPENCLAW_HF_STATE_BUCKET", bucket);
16454
16651
  fixed.push("set OPENCLAW_HF_STATE_BUCKET");
16455
16652
  }
16653
+ if ((variables.get("MLCLAW_STATE_MOUNT_DIR")?.value ?? "") !== SPACE_STATE_MOUNT_DIR) {
16654
+ if (fix) {
16655
+ await hub.addSpaceVariable(repoId, "MLCLAW_STATE_MOUNT_DIR", SPACE_STATE_MOUNT_DIR);
16656
+ fixed.push("set MLCLAW_STATE_MOUNT_DIR");
16657
+ } else {
16658
+ issues.push(`MLCLAW_STATE_MOUNT_DIR is not ${SPACE_STATE_MOUNT_DIR}`);
16659
+ }
16660
+ }
16661
+ if ((variables.get("OPENCLAW_LIVE_DIR")?.value ?? "") !== SPACE_LIVE_DIR) {
16662
+ if (fix) {
16663
+ await hub.addSpaceVariable(repoId, "OPENCLAW_LIVE_DIR", SPACE_LIVE_DIR);
16664
+ fixed.push("set OPENCLAW_LIVE_DIR");
16665
+ } else {
16666
+ issues.push(`OPENCLAW_LIVE_DIR is not ${SPACE_LIVE_DIR}`);
16667
+ }
16668
+ }
16669
+ const expectedRuntimeSettingsFile = `${SPACE_LIVE_DIR}/.mlclaw/settings.json`;
16670
+ if ((variables.get("MLCLAW_RUNTIME_SETTINGS_FILE")?.value ?? "") !== expectedRuntimeSettingsFile) {
16671
+ if (fix) {
16672
+ await hub.addSpaceVariable(repoId, "MLCLAW_RUNTIME_SETTINGS_FILE", expectedRuntimeSettingsFile);
16673
+ fixed.push("set MLCLAW_RUNTIME_SETTINGS_FILE");
16674
+ } else {
16675
+ issues.push(`MLCLAW_RUNTIME_SETTINGS_FILE is not ${expectedRuntimeSettingsFile}`);
16676
+ }
16677
+ }
16456
16678
  for (const key of STALE_PATH_VARS) {
16457
16679
  if (variables.has(key)) {
16458
16680
  if (fix) {
@@ -16463,8 +16685,30 @@ async function doctor(repoId, opts, hub, runtime) {
16463
16685
  }
16464
16686
  }
16465
16687
  }
16466
- if (!secrets.has("HF_TOKEN")) {
16467
- issues.push("secret HF_TOKEN is missing");
16688
+ if (!secrets.has("MLCLAW_BROKER_HF_TOKEN")) {
16689
+ if (fix) {
16690
+ await hub.addSpaceSecret(repoId, "MLCLAW_BROKER_HF_TOKEN", await runtime.readToken(runtime.env));
16691
+ secrets.set("MLCLAW_BROKER_HF_TOKEN", { key: "MLCLAW_BROKER_HF_TOKEN" });
16692
+ fixed.push("set secret MLCLAW_BROKER_HF_TOKEN");
16693
+ } else {
16694
+ issues.push("secret MLCLAW_BROKER_HF_TOKEN is missing");
16695
+ }
16696
+ }
16697
+ const staleTokenSecrets = ["HF_TOKEN", "HUGGINGFACE_HUB_TOKEN"].filter((key) => secrets.has(key));
16698
+ if (staleTokenSecrets.length > 0) {
16699
+ const model = variables.get("OPENCLAW_MODEL")?.value ?? DEFAULT_MODEL;
16700
+ const canDelete = canDeleteBroadTokenSecrets({
16701
+ model,
16702
+ routerTokenPresent: hasBrokerOrRouterTokenSecretMap(secrets)
16703
+ });
16704
+ if (fix && canDelete) {
16705
+ await deleteStaleSpaceTokenSecrets(hub, repoId);
16706
+ fixed.push(`deleted stale secret${staleTokenSecrets.length === 1 ? "" : "s"} ${staleTokenSecrets.join(", ")}`);
16707
+ } else if (fix) {
16708
+ issues.push(`stale broad Hub token secret${staleTokenSecrets.length === 1 ? "" : "s"} present: ${staleTokenSecrets.join(", ")}; add MLCLAW_BROKER_HF_TOKEN before removing`);
16709
+ } else {
16710
+ issues.push(`stale broad Hub token secret${staleTokenSecrets.length === 1 ? "" : "s"} present: ${staleTokenSecrets.join(", ")}`);
16711
+ }
16468
16712
  }
16469
16713
  if (!secrets.has("MLCLAW_SESSION_SECRET")) {
16470
16714
  if (fix) {
@@ -16474,6 +16718,16 @@ async function doctor(repoId, opts, hub, runtime) {
16474
16718
  issues.push("secret MLCLAW_SESSION_SECRET is missing");
16475
16719
  }
16476
16720
  }
16721
+ if (!secrets.has("MLCLAW_CREDENTIAL_KEY")) {
16722
+ if (fix) {
16723
+ const agent = variables.get("OPENCLAW_AGENT_NAME")?.value?.trim() || repoId.split("/")[1] || "openclaw";
16724
+ const credentialKey = await manifestExists(runtime.configRoot, agent) ? requiredSecret(await ensureDeploymentCredentialKey(runtime, agent), "MLCLAW_CREDENTIAL_KEY") : randomBytes(32).toString("base64url");
16725
+ await hub.addSpaceSecret(repoId, "MLCLAW_CREDENTIAL_KEY", credentialKey);
16726
+ fixed.push("set secret MLCLAW_CREDENTIAL_KEY");
16727
+ } else {
16728
+ issues.push("secret MLCLAW_CREDENTIAL_KEY is missing");
16729
+ }
16730
+ }
16477
16731
  if (!variables.has("MLCLAW_TEMPLATE_REV") && !variables.has("OPENCLAW_HF_TEMPLATE_REV")) {
16478
16732
  issues.push("MLCLAW_TEMPLATE_REV is missing; updates cannot verify template lineage");
16479
16733
  }
@@ -16513,6 +16767,14 @@ async function doctor(repoId, opts, hub, runtime) {
16513
16767
  await hub.assertBucketAccessible(bucket);
16514
16768
  }
16515
16769
  const runtimeInfo = await hub.getSpaceRuntime(repoId);
16770
+ if (bucket && !hasStateVolume(runtimeInfo.volumes, bucket)) {
16771
+ if (fix) {
16772
+ await hub.setSpaceVolumes(repoId, mergeStateVolume(requireRuntimeVolumes(runtimeInfo, repoId), bucket));
16773
+ fixed.push(`mounted bucket ${bucket} at ${SPACE_STATE_MOUNT_DIR}`);
16774
+ } else {
16775
+ issues.push(`bucket ${bucket} is not mounted read-write at ${SPACE_STATE_MOUNT_DIR}`);
16776
+ }
16777
+ }
16516
16778
  let logs = "";
16517
16779
  try {
16518
16780
  logs = await hub.fetchSpaceLogs(repoId, "run");
@@ -16613,6 +16875,79 @@ async function setDeploymentSecrets(hub, repoId, secrets) {
16613
16875
  await hub.addSpaceSecret(repoId, key, value);
16614
16876
  }
16615
16877
  }
16878
+ async function deleteStaleSpaceTokenSecrets(hub, repoId) {
16879
+ await Promise.all([
16880
+ hub.deleteSpaceSecret(repoId, "HF_TOKEN"),
16881
+ hub.deleteSpaceSecret(repoId, "HUGGINGFACE_HUB_TOKEN")
16882
+ ]);
16883
+ }
16884
+ function canDeleteBroadTokenSecrets(params) {
16885
+ return params.routerTokenPresent || !isHuggingFaceRouterModel(params.model);
16886
+ }
16887
+ function hasRouterTokenSecretRecord(secrets) {
16888
+ return Boolean(secrets.MLCLAW_ROUTER_TOKEN || secrets.HF_ROUTER_TOKEN);
16889
+ }
16890
+ function hasRouterTokenSecretMap(secrets) {
16891
+ return secrets.has("MLCLAW_ROUTER_TOKEN") || secrets.has("HF_ROUTER_TOKEN");
16892
+ }
16893
+ function hasBrokerOrRouterTokenSecretRecord(secrets) {
16894
+ return Boolean(secrets.MLCLAW_BROKER_HF_TOKEN) || hasRouterTokenSecretRecord(secrets);
16895
+ }
16896
+ function hasBrokerOrRouterTokenSecretMap(secrets) {
16897
+ return secrets.has("MLCLAW_BROKER_HF_TOKEN") || hasRouterTokenSecretMap(secrets);
16898
+ }
16899
+ function assertDedicatedRouterToken(model, secrets) {
16900
+ if (isHuggingFaceRouterModel(model) && !hasBrokerOrRouterTokenSecretRecord(secrets)) {
16901
+ throw new Error(
16902
+ "Hugging Face Router models require MLCLAW_BROKER_HF_TOKEN or a dedicated inference token"
16903
+ );
16904
+ }
16905
+ }
16906
+ async function ensureSpaceStateVolume(hub, repoId, bucket, opts = {}) {
16907
+ const runtime = await hub.getSpaceRuntime(repoId);
16908
+ const volumes = Array.isArray(runtime.volumes) ? runtime.volumes : opts.allowMissingVolumes ? [] : requireRuntimeVolumes(runtime, repoId);
16909
+ await hub.setSpaceVolumes(repoId, mergeStateVolume(volumes, bucket));
16910
+ }
16911
+ function requireRuntimeVolumes(runtime, repoId) {
16912
+ if (!Array.isArray(runtime.volumes)) {
16913
+ throw new Error(`Space runtime metadata for ${repoId} did not include volumes; refusing to replace mounts`);
16914
+ }
16915
+ return runtime.volumes;
16916
+ }
16917
+ function mergeStateVolume(existing, bucket) {
16918
+ return [
16919
+ ...existing.filter((volume) => volumeMountPath(volume) !== SPACE_STATE_MOUNT_DIR).map(normalizeSpaceVolume),
16920
+ {
16921
+ type: "bucket",
16922
+ source: bucket,
16923
+ mountPath: SPACE_STATE_MOUNT_DIR,
16924
+ readOnly: false
16925
+ }
16926
+ ];
16927
+ }
16928
+ function hasStateVolume(volumes, bucket) {
16929
+ return Boolean(volumes?.some(
16930
+ (volume) => volume.type === "bucket" && volume.source === bucket && volumeMountPath(volume) === SPACE_STATE_MOUNT_DIR && volumeReadOnly(volume) !== true
16931
+ ));
16932
+ }
16933
+ function normalizeSpaceVolume(volume) {
16934
+ const normalized = { ...volume };
16935
+ const mountPath = volumeMountPath(volume);
16936
+ if (mountPath) {
16937
+ normalized.mountPath = mountPath;
16938
+ }
16939
+ const readOnly = volumeReadOnly(volume);
16940
+ if (typeof readOnly === "boolean") {
16941
+ normalized.readOnly = readOnly;
16942
+ }
16943
+ return normalized;
16944
+ }
16945
+ function volumeMountPath(volume) {
16946
+ return volume.mountPath ?? volume.mount_path;
16947
+ }
16948
+ function volumeReadOnly(volume) {
16949
+ return volume.readOnly ?? volume.read_only;
16950
+ }
16616
16951
  async function clearSpaceGatewayDisabled(hub, repoId) {
16617
16952
  try {
16618
16953
  await hub.deleteSpaceVariable(repoId, "MLCLAW_GATEWAY_DISABLED");
@@ -16635,6 +16970,29 @@ async function readOptionalTelegramToken(opts, runtime) {
16635
16970
  }
16636
16971
  return void 0;
16637
16972
  }
16973
+ async function resolveRouterToken(params) {
16974
+ const explicit = nonEmpty(params.opts.routerToken) ?? await readOptionalRouterTokenFile(params.opts.routerTokenFile);
16975
+ const direct = explicit ?? params.runtime.env.MLCLAW_ROUTER_TOKEN ?? params.runtime.env.HF_ROUTER_TOKEN ?? params.existingSecrets?.MLCLAW_ROUTER_TOKEN ?? params.existingSecrets?.HF_ROUTER_TOKEN;
16976
+ const existing = nonEmpty(direct);
16977
+ if (existing) {
16978
+ return existing;
16979
+ }
16980
+ if (!isHuggingFaceRouterModel(params.model)) {
16981
+ return void 0;
16982
+ }
16983
+ return void 0;
16984
+ }
16985
+ async function readOptionalRouterTokenFile(file) {
16986
+ if (!file) {
16987
+ return void 0;
16988
+ }
16989
+ const raw = await fs14.readFile(file, "utf8");
16990
+ const parsed = parseSecretEnv(raw);
16991
+ return nonEmpty(parsed.MLCLAW_ROUTER_TOKEN) ?? nonEmpty(parsed.HF_ROUTER_TOKEN) ?? nonEmpty(raw);
16992
+ }
16993
+ function isHuggingFaceRouterModel(model) {
16994
+ return model.trim().startsWith("huggingface/");
16995
+ }
16638
16996
  async function promptAgentName(runtime) {
16639
16997
  if (!runtime.prompt.isInteractive()) {
16640
16998
  return "mlclaw";
@@ -16647,21 +17005,26 @@ async function promptAgentName(runtime) {
16647
17005
  return readPromptValue(value, "Agent name");
16648
17006
  }
16649
17007
  async function resolveHardware(params) {
16650
- const hardware = params.requestedHardware;
16651
- const sleepTime = params.requestedSleepTime ?? TELEGRAM_SLEEP_TIME;
17008
+ const hardware = params.requestedHardware ?? (params.requiresMessagingEgress ? TELEGRAM_HARDWARE : void 0);
17009
+ if (!hardware) {
17010
+ const label = params.defaultLabel ?? "default free CPU";
17011
+ return typeof params.requestedSleepTime === "number" ? { kind: "default", label, sleepTime: params.requestedSleepTime } : { kind: "default", label };
17012
+ }
17013
+ const sleepTime = isPaidHardware(hardware) ? params.requestedSleepTime ?? TELEGRAM_SLEEP_TIME : params.requestedSleepTime;
16652
17014
  if (params.requiresMessagingEgress && !isPaidHardware(hardware)) {
16653
17015
  throw new Error(`Telegram requires upgraded paid Space hardware today; use --hardware ${TELEGRAM_HARDWARE} or --gateway local`);
16654
17016
  }
16655
17017
  if (isPaidHardware(hardware)) {
17018
+ const paidSleepTime = params.requestedSleepTime ?? TELEGRAM_SLEEP_TIME;
16656
17019
  await confirmPaidHardware({
16657
17020
  hardware,
16658
- sleepTime,
17021
+ sleepTime: paidSleepTime,
16659
17022
  yes: params.yes,
16660
17023
  runtime: params.runtime
16661
17024
  });
16662
- return { hardware, sleepTime };
17025
+ return { kind: "explicit", hardware, label: hardware, sleepTime: paidSleepTime };
16663
17026
  }
16664
- return typeof params.requestedSleepTime === "number" ? { hardware, sleepTime: params.requestedSleepTime } : { hardware };
17027
+ return typeof sleepTime === "number" ? { kind: "explicit", hardware, label: hardware, sleepTime } : { kind: "explicit", hardware, label: hardware };
16665
17028
  }
16666
17029
  async function confirmPaidHardware(params) {
16667
17030
  if (params.yes) {
@@ -16745,8 +17108,11 @@ export {
16745
17108
  LOCAL_VOLUME_MOUNT_PATH,
16746
17109
  SPACE_HANDOFF_POLL_MS,
16747
17110
  SPACE_HANDOFF_TIMEOUT_MS,
17111
+ SPACE_LIVE_DIR,
17112
+ SPACE_STATE_MOUNT_DIR,
16748
17113
  TELEGRAM_HARDWARE,
16749
17114
  TELEGRAM_SLEEP_TIME,
16750
17115
  createProgram,
16751
- main
17116
+ main,
17117
+ mergeStateVolume
16752
17118
  };