mlclaw 0.2.3 → 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}`;
@@ -14916,6 +14955,7 @@ async function generateSpaceRepo(sourceDir, outDir, options = {}) {
14916
14955
  ["dist/hf-tooling-seed.js", "runtime/hf-tooling-seed.js"],
14917
14956
  ["dist/mlclaw-space-runtime.js", "runtime/mlclaw-space-runtime.js"],
14918
14957
  ["entrypoint.sh", "runtime/entrypoint.sh"],
14958
+ ["hf-broker.scope.json", "runtime/hf-broker.scope.json"],
14919
14959
  ["openclaw.default.json", "runtime/openclaw.default.json"],
14920
14960
  ["scripts/configure-huggingface-model.mjs", "runtime/scripts/configure-huggingface-model.mjs"],
14921
14961
  ["scripts/configure-telegram.mjs", "runtime/scripts/configure-telegram.mjs"],
@@ -14936,14 +14976,34 @@ function imageDockerfile(runtimeImage) {
14936
14976
  `;
14937
14977
  }
14938
14978
  function bundledDockerfile() {
14939
- 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}
14940
14999
 
14941
15000
  LABEL org.opencontainers.image.source="https://github.com/osolmaz/mlclaw"
14942
15001
  LABEL org.opencontainers.image.description="ML Claw runtime for OpenClaw on Hugging Face"
14943
15002
 
14944
15003
  USER root
14945
15004
  RUN apt-get update \\
14946
- && 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 \\
14947
15007
  && rm -rf /var/lib/apt/lists/*
14948
15008
  RUN python3 -m pip install --break-system-packages --no-cache-dir \\
14949
15009
  "huggingface_hub==1.19.0" \\
@@ -14957,10 +15017,15 @@ RUN python3 -m pip install --break-system-packages --no-cache-dir \\
14957
15017
  "uvicorn==0.49.0" \\
14958
15018
  "uv==0.11.28" \\
14959
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
14960
15023
 
14961
15024
  COPY --chown=node:node runtime/hf-state-sync.js /app/hf-state-sync.js
14962
15025
  COPY --chown=node:node runtime/hf-tooling-seed.js /app/hf-tooling-seed.js
14963
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
14964
15029
  COPY --chown=node:node runtime/openclaw.default.json /app/openclaw.default.json
14965
15030
  COPY --chown=node:node runtime/entrypoint.sh /app/entrypoint.sh
14966
15031
  COPY --chown=node:node runtime/scripts/ /app/scripts/
@@ -14970,11 +15035,12 @@ RUN chmod +x /app/entrypoint.sh
14970
15035
  ENV PORT=7860
14971
15036
  ENV MLCLAW_OPENCLAW_PORT=7861
14972
15037
  ENV OPENCLAW_GATEWAY_PORT=7861
14973
- ENV OPENCLAW_LIVE_DIR=/tmp/openclaw-live
14974
- ENV OPENCLAW_STATE_DIR=/tmp/openclaw-live/.openclaw
14975
- ENV OPENCLAW_WORKSPACE_DIR=/tmp/openclaw-live/workspace
14976
- 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
14977
15042
  ENV OPENCLAW_DISABLE_BONJOUR=1
15043
+ ENV MLCLAW_BROKERKIT_PLUGIN_PATH=/opt/openclaw-plugins/node_modules/openclaw-brokerkit
14978
15044
 
14979
15045
  EXPOSE 7860
14980
15046
 
@@ -15259,6 +15325,8 @@ var DEFAULT_LOCAL_PORT = 7860;
15259
15325
  var DEFAULT_SPACE_OPENCLAW_PORT = 7861;
15260
15326
  var LOCAL_VOLUME_MOUNT_PATH = "/tmp/mlclaw-local";
15261
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";
15262
15330
  var SPACE_HANDOFF_TIMEOUT_MS = 12e4;
15263
15331
  var SPACE_HANDOFF_POLL_MS = 5e3;
15264
15332
  var STALE_PATH_VARS = ["OPENCLAW_STATE_DIR", "OPENCLAW_WORKSPACE_DIR", "OPENCLAW_CONFIG_PATH"];
@@ -15296,10 +15364,10 @@ function createProgram(runtimeOverrides = {}) {
15296
15364
  program2.name("mlclaw").description("Deploy OpenClaw to a Hugging Face Space and private bucket").showHelpAfterError().exitOverride((err) => {
15297
15365
  throw err;
15298
15366
  });
15299
- 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) => {
15300
15368
  await bootstrap(opts, runtime);
15301
15369
  });
15302
- 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) => {
15303
15371
  const token = await runtime.readToken(runtime.env);
15304
15372
  const hub = runtime.hubFactory(token);
15305
15373
  await update(repoId, opts, hub, token, runtime);
@@ -15322,8 +15390,7 @@ function createProgram(runtimeOverrides = {}) {
15322
15390
  await gatewayStop(agent, runtime);
15323
15391
  });
15324
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) => {
15325
- await gatewayStop(agent, runtime);
15326
- await gatewayStart(agent, opts, runtime);
15393
+ await gatewayRestart(agent, opts, runtime);
15327
15394
  });
15328
15395
  gateway.command("status").argument("<agent>", "Agent name").action(async (agent) => {
15329
15396
  await gatewayStatus(agent, runtime);
@@ -15331,7 +15398,7 @@ function createProgram(runtimeOverrides = {}) {
15331
15398
  gateway.command("logs").argument("<agent>", "Agent name").option("--tail <lines>", "Number of log lines", parseInteger, 200).action(async (agent, opts) => {
15332
15399
  await gatewayLogs(agent, opts, runtime);
15333
15400
  });
15334
- 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) => {
15335
15402
  await gatewayMigrate(agent, opts, runtime);
15336
15403
  });
15337
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) => {
@@ -15404,8 +15471,9 @@ async function bootstrap(opts, runtime) {
15404
15471
  throw new Error("internal error: Space plan was not resolved");
15405
15472
  }
15406
15473
  const paidHardware = await resolveHardware({
15407
- requestedHardware: opts.hardware ?? (telegramToken ? TELEGRAM_HARDWARE : DEFAULT_HARDWARE),
15474
+ ...opts.hardware ? { requestedHardware: opts.hardware } : {},
15408
15475
  ...typeof opts.sleepTime === "number" ? { requestedSleepTime: opts.sleepTime } : telegramToken ? { requestedSleepTime: TELEGRAM_SLEEP_TIME } : {},
15476
+ defaultLabel: spacePlan.exists ? "unchanged Space hardware" : "default free CPU",
15409
15477
  requiresMessagingEgress: Boolean(telegramToken),
15410
15478
  yes: Boolean(opts.yes),
15411
15479
  runtime
@@ -15415,7 +15483,7 @@ async function bootstrap(opts, runtime) {
15415
15483
  bucketPlan,
15416
15484
  spacePlan,
15417
15485
  hasExistingManifest: plan.hasExistingManifest,
15418
- hardware: paidHardware.hardware,
15486
+ hardware: paidHardware.label,
15419
15487
  ...typeof paidHardware.sleepTime === "number" ? { sleepTime: paidHardware.sleepTime } : {},
15420
15488
  yes: Boolean(opts.yes),
15421
15489
  runtime
@@ -15435,9 +15503,9 @@ async function bootstrap(opts, runtime) {
15435
15503
  manifest,
15436
15504
  secrets,
15437
15505
  allowedUsers: me2.name,
15438
- hardware: paidHardware.hardware,
15439
15506
  spaceExists: spacePlan.exists,
15440
15507
  publicSpace: Boolean(opts.publicSpace),
15508
+ ...paidHardware.kind === "explicit" ? { hardware: paidHardware.hardware } : {},
15441
15509
  ...typeof paidHardware.sleepTime === "number" ? { sleepTime: paidHardware.sleepTime } : {},
15442
15510
  ...templateRuntimeImage ? { templateRuntimeImage } : {}
15443
15511
  });
@@ -15493,10 +15561,11 @@ function spacePublicUrl(repoId) {
15493
15561
  async function resolveBootstrapPlan(params) {
15494
15562
  const { opts, owner, agentName, requestedGatewayLocation, hfToken, telegramToken, telegramUserId, model, runtimeImage, hub, runtime } = params;
15495
15563
  const names = namesFor(owner, agentName);
15496
- const sessionSecret = randomBytes(48).toString("base64url");
15497
15564
  const now = runtime.now().toISOString();
15498
15565
  const existingManifest = await readManifest(runtime.configRoot, agentName).catch(() => null);
15499
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");
15500
15569
  const gatewayLocation = requestedGatewayLocation ?? existingManifest?.gatewayLocation ?? DEFAULT_GATEWAY_LOCATION;
15501
15570
  if (opts.dockerContext && gatewayLocation !== "local") {
15502
15571
  throw new Error("--docker-context only applies to local gateway mode");
@@ -15517,6 +15586,12 @@ async function resolveBootstrapPlan(params) {
15517
15586
  hub
15518
15587
  });
15519
15588
  const bucket = bucketPlan.bucket;
15589
+ const routerToken = await resolveRouterToken({
15590
+ opts,
15591
+ runtime,
15592
+ existingSecrets,
15593
+ model
15594
+ });
15520
15595
  const spacePlan = gatewayLocation === "space" ? {
15521
15596
  space: names.space,
15522
15597
  exists: await hub.spaceExists(names.space),
@@ -15541,6 +15616,7 @@ async function resolveBootstrapPlan(params) {
15541
15616
  ...telegramToken ? { telegramToken } : {},
15542
15617
  ...telegramUserId ? { telegramUserId } : {},
15543
15618
  sessionSecret,
15619
+ credentialKey,
15544
15620
  bucket,
15545
15621
  model,
15546
15622
  agentName,
@@ -15549,7 +15625,8 @@ async function resolveBootstrapPlan(params) {
15549
15625
  runtimeId: gatewayLocation === "local" ? manifest.localRuntimeId : spaceRuntimeId(agentName),
15550
15626
  ...bucketPrefix ? { bucketPrefix } : {},
15551
15627
  ...opts.telegramProxy ? { telegramProxy: opts.telegramProxy } : {},
15552
- ...opts.telegramApiRoot ? { telegramApiRoot: opts.telegramApiRoot } : {}
15628
+ ...opts.telegramApiRoot ? { telegramApiRoot: opts.telegramApiRoot } : {},
15629
+ ...routerToken ? { routerToken } : {}
15553
15630
  });
15554
15631
  return {
15555
15632
  agentName,
@@ -15611,6 +15688,8 @@ async function confirmBootstrapPlan(params) {
15611
15688
  if (params.spacePlan) {
15612
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}`})`);
15613
15690
  lines.push(`Hardware: ${params.hardware}`);
15691
+ lines.push(`Bucket mount: ${SPACE_STATE_MOUNT_DIR}`);
15692
+ lines.push(`Live state: ${SPACE_LIVE_DIR}`);
15614
15693
  if (typeof params.sleepTime === "number") {
15615
15694
  lines.push(`Sleep time: ${params.sleepTime}`);
15616
15695
  }
@@ -15650,6 +15729,10 @@ async function stateAdopt(agent, opts, runtime) {
15650
15729
  const hub = runtime.hubFactory(token);
15651
15730
  const secrets = await readSecretEnv(runtime.configRoot, agent);
15652
15731
  const bucketPrefix = persistedBucketPrefix(secrets);
15732
+ const bucketChanged = current.bucket !== bucket;
15733
+ if (bucketChanged && current.gatewayLocation === "local") {
15734
+ assertDedicatedRouterToken(current.model, secrets);
15735
+ }
15653
15736
  runtime.stdout.log(`Creating or adopting private bucket ${bucket}`);
15654
15737
  await hub.createBucket(bucket, true);
15655
15738
  await inspectStateBucket(hub, bucket, bucketPrefix);
@@ -15660,7 +15743,6 @@ async function stateAdopt(agent, opts, runtime) {
15660
15743
  runtimeId: runtimeIdFor(current),
15661
15744
  takeover: Boolean(opts.takeover)
15662
15745
  });
15663
- const bucketChanged = current.bucket !== bucket;
15664
15746
  if (bucketChanged) {
15665
15747
  await confirmBucketChange({
15666
15748
  message: `Adopt state bucket ${bucket} for ${agent}, replacing ${current.bucket}?`,
@@ -15711,9 +15793,16 @@ async function stateAdopt(agent, opts, runtime) {
15711
15793
  } else {
15712
15794
  await setDeploymentVariables(hub, updated.space, {
15713
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`,
15714
15799
  MLCLAW_GATEWAY_LOCATION: "space",
15715
15800
  MLCLAW_RUNTIME_ID: spaceRuntimeId(updated.agent)
15716
15801
  });
15802
+ await ensureSpaceStateVolume(hub, updated.space, bucket);
15803
+ if (canDeleteBroadTokenSecrets({ model: updated.model, routerTokenPresent: hasBrokerOrRouterTokenSecretRecord(secrets) })) {
15804
+ await deleteStaleSpaceTokenSecrets(hub, updated.space);
15805
+ }
15717
15806
  await clearSpaceGatewayDisabled(hub, updated.space);
15718
15807
  if (bucketChanged) {
15719
15808
  await hub.restartSpace(updated.space, true);
@@ -15788,8 +15877,8 @@ function parseBucketId(raw) {
15788
15877
  }
15789
15878
  function deploymentSecrets(params) {
15790
15879
  return {
15791
- HF_TOKEN: params.hfToken,
15792
- HUGGINGFACE_HUB_TOKEN: params.hfToken,
15880
+ MLCLAW_BROKER_HF_TOKEN: params.hfToken,
15881
+ ...params.routerToken ? { MLCLAW_ROUTER_TOKEN: params.routerToken } : {},
15793
15882
  OPENCLAW_HF_STATE_BUCKET: params.bucket,
15794
15883
  OPENCLAW_MODEL: params.model,
15795
15884
  OPENCLAW_AGENT_NAME: params.agentName,
@@ -15797,6 +15886,7 @@ function deploymentSecrets(params) {
15797
15886
  MLCLAW_RUNTIME_IMAGE: params.runtimeImage,
15798
15887
  MLCLAW_RUNTIME_ID: params.runtimeId,
15799
15888
  MLCLAW_SESSION_SECRET: params.sessionSecret,
15889
+ MLCLAW_CREDENTIAL_KEY: params.credentialKey,
15800
15890
  MLCLAW_OPENCLAW_PORT: String(DEFAULT_SPACE_OPENCLAW_PORT),
15801
15891
  OPENCLAW_GATEWAY_PORT: String(DEFAULT_SPACE_OPENCLAW_PORT),
15802
15892
  ...params.telegramToken ? { TELEGRAM_BOT_TOKEN: params.telegramToken } : {},
@@ -15815,10 +15905,14 @@ async function deploySpaceGateway(params) {
15815
15905
  runtime.stdout.log(params.spaceExists ? `Updating existing Space ${manifest.space}` : `Creating ${params.publicSpace ? "public" : "private"} Space ${manifest.space}`);
15816
15906
  await hub.createDockerSpace(manifest.space, {
15817
15907
  private: !params.publicSpace,
15818
- hardware: params.hardware,
15908
+ ...params.hardware && !params.spaceExists ? { hardware: params.hardware } : {},
15819
15909
  ...typeof params.sleepTime === "number" ? { sleepTimeSeconds: params.sleepTime } : {}
15820
15910
  });
15821
- 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
+ }
15822
15916
  runtime.stdout.log(params.templateRuntimeImage ? "Generating Space files from prebuilt runtime image" : "Generating bundled Space runtime files");
15823
15917
  const { templateRev } = await runtime.pushTemplateToSpace({
15824
15918
  targetRepo: manifest.space,
@@ -15828,6 +15922,9 @@ async function deploySpaceGateway(params) {
15828
15922
  const spaceRuntimeRef = params.templateRuntimeImage ?? bundledSpaceRuntimeRef(templateRev);
15829
15923
  await setDeploymentVariables(hub, manifest.space, {
15830
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`,
15831
15928
  ...secrets.OPENCLAW_HF_STATE_PREFIX ? { OPENCLAW_HF_STATE_PREFIX: secrets.OPENCLAW_HF_STATE_PREFIX } : {},
15832
15929
  MLCLAW_TEMPLATE_REV: templateRev,
15833
15930
  OPENCLAW_MODEL: manifest.model,
@@ -15841,21 +15938,30 @@ async function deploySpaceGateway(params) {
15841
15938
  MLCLAW_OPENCLAW_PORT: String(DEFAULT_SPACE_OPENCLAW_PORT),
15842
15939
  OPENCLAW_GATEWAY_PORT: String(DEFAULT_SPACE_OPENCLAW_PORT)
15843
15940
  });
15941
+ await ensureSpaceStateVolume(hub, manifest.space, manifest.bucket, { allowMissingVolumes: !params.spaceExists });
15844
15942
  await clearSpaceGatewayDisabled(hub, manifest.space);
15845
15943
  await setDeploymentSecrets(hub, manifest.space, {
15846
- HF_TOKEN: requiredSecret(secrets, "HF_TOKEN"),
15847
- HUGGINGFACE_HUB_TOKEN: requiredSecret(secrets, "HF_TOKEN"),
15848
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 } : {},
15849
15948
  ...secrets.TELEGRAM_BOT_TOKEN ? { TELEGRAM_BOT_TOKEN: secrets.TELEGRAM_BOT_TOKEN } : {},
15850
15949
  ...secrets.TELEGRAM_ALLOWED_USERS ? { TELEGRAM_ALLOWED_USERS: secrets.TELEGRAM_ALLOWED_USERS } : {},
15851
15950
  ...secrets.TELEGRAM_PROXY ? { TELEGRAM_PROXY: secrets.TELEGRAM_PROXY } : {},
15852
15951
  ...secrets.TELEGRAM_API_ROOT ? { TELEGRAM_API_ROOT: secrets.TELEGRAM_API_ROOT } : {}
15853
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
+ }
15854
15958
  await hub.restartSpace(manifest.space, true);
15855
15959
  return { runtimeImage: spaceRuntimeRef };
15856
15960
  }
15857
15961
  async function startLocalGateway(params) {
15858
15962
  const { manifest, runtime } = params;
15963
+ const secrets = await ensureDeploymentCredentialKey(runtime, manifest.agent);
15964
+ assertDedicatedRouterToken(manifest.model, secrets);
15859
15965
  const containerName = containerNameFor(manifest.agent);
15860
15966
  const volumeName = volumeNameFor(manifest.agent);
15861
15967
  const dockerContext = dockerContextFor(manifest);
@@ -15928,6 +16034,17 @@ async function gatewayStart(agent, opts, runtime) {
15928
16034
  runtime.stdout.log(`Space gateway restart requested: ${manifest.space}`);
15929
16035
  }
15930
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
+ }
15931
16048
  async function gatewayStop(agent, runtime) {
15932
16049
  const manifest = await readDeploymentManifest(runtime, agent);
15933
16050
  const bucketPrefix = await readDeploymentBucketPrefix(runtime, agent);
@@ -15995,7 +16112,7 @@ async function gatewayMigrate(agent, opts, runtime) {
15995
16112
  }
15996
16113
  const token = await runtime.readToken(runtime.env);
15997
16114
  const hub = runtime.hubFactory(token);
15998
- const secrets = await readSecretEnv(runtime.configRoot, agent);
16115
+ const secrets = await ensureDeploymentCredentialKey(runtime, agent);
15999
16116
  const bucketPrefix = persistedBucketPrefix(secrets);
16000
16117
  const updated = {
16001
16118
  ...current,
@@ -16003,10 +16120,23 @@ async function gatewayMigrate(agent, opts, runtime) {
16003
16120
  runtimeImage: resolveRuntimeImage(opts.runtimeImage ?? current.runtimeImage, runtime.env),
16004
16121
  updatedAt: runtime.now().toISOString()
16005
16122
  };
16123
+ const routerToken = await resolveRouterToken({
16124
+ opts,
16125
+ runtime,
16126
+ existingSecrets: secrets,
16127
+ model: updated.model
16128
+ });
16006
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
+ };
16007
16136
  const paidHardware = await resolveHardware({
16008
- requestedHardware: opts.hardware ?? (secrets.TELEGRAM_BOT_TOKEN ? TELEGRAM_HARDWARE : DEFAULT_HARDWARE),
16137
+ ...opts.hardware ? { requestedHardware: opts.hardware } : {},
16009
16138
  ...typeof opts.sleepTime === "number" ? { requestedSleepTime: opts.sleepTime } : secrets.TELEGRAM_BOT_TOKEN ? { requestedSleepTime: TELEGRAM_SLEEP_TIME } : {},
16139
+ defaultLabel: "unchanged Space hardware",
16010
16140
  requiresMessagingEgress: Boolean(secrets.TELEGRAM_BOT_TOKEN),
16011
16141
  yes: Boolean(opts.yes),
16012
16142
  runtime
@@ -16021,24 +16151,22 @@ async function gatewayMigrate(agent, opts, runtime) {
16021
16151
  await handoffAndStopLocalGateway({ manifest: current, hub, runtime, bucketPrefix });
16022
16152
  const me2 = await hub.whoami();
16023
16153
  const templateRuntimeImage = resolveSpaceRuntimeImage(opts, runtime.env);
16154
+ const spaceExists = await hub.spaceExists(updated.space);
16024
16155
  await deploySpaceGateway({
16025
16156
  hub,
16026
16157
  runtime,
16027
16158
  hfToken: token,
16028
16159
  manifest: updated,
16029
- secrets: {
16030
- ...secrets,
16031
- MLCLAW_GATEWAY_LOCATION: "space",
16032
- MLCLAW_RUNTIME_IMAGE: updated.runtimeImage
16033
- },
16160
+ secrets: deploymentSecrets2,
16034
16161
  allowedUsers: me2.name,
16035
- hardware: paidHardware.hardware,
16036
16162
  publicSpace: Boolean(opts.publicSpace),
16163
+ spaceExists,
16164
+ ...paidHardware.kind === "explicit" ? { hardware: paidHardware.hardware } : {},
16037
16165
  ...typeof paidHardware.sleepTime === "number" ? { sleepTime: paidHardware.sleepTime } : {},
16038
16166
  ...templateRuntimeImage ? { templateRuntimeImage } : {}
16039
16167
  });
16040
16168
  await writeSecretEnv(runtime.configRoot, agent, {
16041
- ...secrets,
16169
+ ...deploymentSecrets2,
16042
16170
  MLCLAW_GATEWAY_LOCATION: "space",
16043
16171
  MLCLAW_RUNTIME_IMAGE: updated.runtimeImage,
16044
16172
  MLCLAW_RUNTIME_ID: spaceRuntimeId(agent)
@@ -16070,6 +16198,7 @@ async function gatewayMigrate(agent, opts, runtime) {
16070
16198
  });
16071
16199
  await writeSecretEnv(runtime.configRoot, agent, {
16072
16200
  ...secrets,
16201
+ ...routerToken ? { MLCLAW_ROUTER_TOKEN: routerToken } : {},
16073
16202
  MLCLAW_GATEWAY_LOCATION: "local",
16074
16203
  MLCLAW_RUNTIME_IMAGE: updated.runtimeImage,
16075
16204
  MLCLAW_RUNTIME_ID: updated.localRuntimeId
@@ -16085,6 +16214,10 @@ async function gatewayRebind(agent, opts, runtime) {
16085
16214
  if (current.gatewayLocation !== "local") {
16086
16215
  throw new Error("Docker context rebind only applies to local gateway deployments");
16087
16216
  }
16217
+ assertDedicatedRouterToken(
16218
+ current.model,
16219
+ await readSecretEnv(runtime.configRoot, agent).catch(() => ({}))
16220
+ );
16088
16221
  const targetBinding = await resolveLocalGatewayBinding({
16089
16222
  manifest: void 0,
16090
16223
  requestedContext: targetContext,
@@ -16358,6 +16491,18 @@ function requiredSecret(secrets, key) {
16358
16491
  }
16359
16492
  return value;
16360
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
+ }
16361
16506
  function requiredOption(value, label) {
16362
16507
  if (!value) {
16363
16508
  throw new Error(`${label} is required`);
@@ -16375,6 +16520,16 @@ async function update(repoId, opts, hub, hfToken, runtime) {
16375
16520
  }
16376
16521
  const runtimeImage = resolveSpaceRuntimeImage(opts, runtime.env);
16377
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
+ }
16378
16533
  runtime.stdout.log(`Generating current Space files into ${repoId}`);
16379
16534
  const { templateRev } = await runtime.pushTemplateToSpace({
16380
16535
  targetRepo: repoId,
@@ -16393,9 +16548,50 @@ async function update(repoId, opts, hub, hfToken, runtime) {
16393
16548
  await hub.addSpaceVariable(repoId, "MLCLAW_RUNTIME_ID", spaceRuntimeId(agentName));
16394
16549
  await hub.addSpaceVariable(repoId, "MLCLAW_OPENCLAW_PORT", String(DEFAULT_SPACE_OPENCLAW_PORT));
16395
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
+ }
16396
16558
  await doctor(repoId, { fix: true }, hub, runtime);
16397
16559
  await hub.restartSpace(repoId, true);
16398
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
+ }
16399
16595
  async function doctor(repoId, opts, hub, runtime) {
16400
16596
  if (!repoId.includes("/") && await manifestExists(runtime.configRoot, repoId)) {
16401
16597
  await gatewayStatus(repoId, runtime);
@@ -16454,6 +16650,31 @@ async function doctor(repoId, opts, hub, runtime) {
16454
16650
  await hub.addSpaceVariable(repoId, "OPENCLAW_HF_STATE_BUCKET", bucket);
16455
16651
  fixed.push("set OPENCLAW_HF_STATE_BUCKET");
16456
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
+ }
16457
16678
  for (const key of STALE_PATH_VARS) {
16458
16679
  if (variables.has(key)) {
16459
16680
  if (fix) {
@@ -16464,8 +16685,30 @@ async function doctor(repoId, opts, hub, runtime) {
16464
16685
  }
16465
16686
  }
16466
16687
  }
16467
- if (!secrets.has("HF_TOKEN")) {
16468
- 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
+ }
16469
16712
  }
16470
16713
  if (!secrets.has("MLCLAW_SESSION_SECRET")) {
16471
16714
  if (fix) {
@@ -16475,6 +16718,16 @@ async function doctor(repoId, opts, hub, runtime) {
16475
16718
  issues.push("secret MLCLAW_SESSION_SECRET is missing");
16476
16719
  }
16477
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
+ }
16478
16731
  if (!variables.has("MLCLAW_TEMPLATE_REV") && !variables.has("OPENCLAW_HF_TEMPLATE_REV")) {
16479
16732
  issues.push("MLCLAW_TEMPLATE_REV is missing; updates cannot verify template lineage");
16480
16733
  }
@@ -16514,6 +16767,14 @@ async function doctor(repoId, opts, hub, runtime) {
16514
16767
  await hub.assertBucketAccessible(bucket);
16515
16768
  }
16516
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
+ }
16517
16778
  let logs = "";
16518
16779
  try {
16519
16780
  logs = await hub.fetchSpaceLogs(repoId, "run");
@@ -16614,6 +16875,79 @@ async function setDeploymentSecrets(hub, repoId, secrets) {
16614
16875
  await hub.addSpaceSecret(repoId, key, value);
16615
16876
  }
16616
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
+ }
16617
16951
  async function clearSpaceGatewayDisabled(hub, repoId) {
16618
16952
  try {
16619
16953
  await hub.deleteSpaceVariable(repoId, "MLCLAW_GATEWAY_DISABLED");
@@ -16636,6 +16970,29 @@ async function readOptionalTelegramToken(opts, runtime) {
16636
16970
  }
16637
16971
  return void 0;
16638
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
+ }
16639
16996
  async function promptAgentName(runtime) {
16640
16997
  if (!runtime.prompt.isInteractive()) {
16641
16998
  return "mlclaw";
@@ -16648,21 +17005,26 @@ async function promptAgentName(runtime) {
16648
17005
  return readPromptValue(value, "Agent name");
16649
17006
  }
16650
17007
  async function resolveHardware(params) {
16651
- const hardware = params.requestedHardware;
16652
- 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;
16653
17014
  if (params.requiresMessagingEgress && !isPaidHardware(hardware)) {
16654
17015
  throw new Error(`Telegram requires upgraded paid Space hardware today; use --hardware ${TELEGRAM_HARDWARE} or --gateway local`);
16655
17016
  }
16656
17017
  if (isPaidHardware(hardware)) {
17018
+ const paidSleepTime = params.requestedSleepTime ?? TELEGRAM_SLEEP_TIME;
16657
17019
  await confirmPaidHardware({
16658
17020
  hardware,
16659
- sleepTime,
17021
+ sleepTime: paidSleepTime,
16660
17022
  yes: params.yes,
16661
17023
  runtime: params.runtime
16662
17024
  });
16663
- return { hardware, sleepTime };
17025
+ return { kind: "explicit", hardware, label: hardware, sleepTime: paidSleepTime };
16664
17026
  }
16665
- 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 };
16666
17028
  }
16667
17029
  async function confirmPaidHardware(params) {
16668
17030
  if (params.yes) {
@@ -16746,8 +17108,11 @@ export {
16746
17108
  LOCAL_VOLUME_MOUNT_PATH,
16747
17109
  SPACE_HANDOFF_POLL_MS,
16748
17110
  SPACE_HANDOFF_TIMEOUT_MS,
17111
+ SPACE_LIVE_DIR,
17112
+ SPACE_STATE_MOUNT_DIR,
16749
17113
  TELEGRAM_HARDWARE,
16750
17114
  TELEGRAM_SLEEP_TIME,
16751
17115
  createProgram,
16752
- main
17116
+ main,
17117
+ mergeStateVolume
16753
17118
  };