mlclaw 0.3.8 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,7 +12,7 @@ import process2 from "node:process";
12
12
 
13
13
  // src/mlclaw-space-runtime/config.ts
14
14
  import { readFileSync as readFileSync2 } from "node:fs";
15
- import { randomBytes } from "node:crypto";
15
+ import { createHash, randomBytes } from "node:crypto";
16
16
 
17
17
  // src/hf-state-sync/paths.ts
18
18
  var DEFAULT_BUCKET_PREFIX = "openclaw-state";
@@ -5039,6 +5039,18 @@ function validatedBrokerPayload(value, schema, label) {
5039
5039
  return parsed.data;
5040
5040
  }
5041
5041
 
5042
+ // src/mlclaw-space-runtime/local-access.ts
5043
+ import { createHmac, timingSafeEqual } from "node:crypto";
5044
+ var LOCAL_ACCESS_CONTEXT = "mlclaw-local-access-v1";
5045
+ function deriveLocalAccessToken(sessionSecret) {
5046
+ return createHmac("sha256", sessionSecret).update(LOCAL_ACCESS_CONTEXT).digest("base64url");
5047
+ }
5048
+ function localAccessTokenMatches(candidate, expected) {
5049
+ const left = Buffer.from(candidate);
5050
+ const right = Buffer.from(expected);
5051
+ return left.length === right.length && timingSafeEqual(left, right);
5052
+ }
5053
+
5042
5054
  // src/mlclaw-space-runtime/config.ts
5043
5055
  function loadConfig(env = process.env) {
5044
5056
  const port = integer(env.PORT ?? env.MLCLAW_SPACE_PORT, 7860);
@@ -5056,13 +5068,18 @@ function loadConfig(env = process.env) {
5056
5068
  spaceCreatorUserId
5057
5069
  });
5058
5070
  const owner = ownerFromSpaceId(spaceId);
5071
+ const stateBucket = trim(env.OPENCLAW_HF_STATE_BUCKET);
5072
+ const gatewayLocation = trim(env.MLCLAW_GATEWAY_LOCATION);
5073
+ const localAccessUser = gatewayLocation === "local" ? trim(env.MLCLAW_LOCAL_ACCESS_USER) ?? ownerFromRepoId(stateBucket) : void 0;
5059
5074
  const configuredAllowedUsers = splitUsers(env.MLCLAW_ALLOWED_USERS ?? env.ALLOWED_USERS);
5060
5075
  const configuredAdmins = splitUsers(env.MLCLAW_ADMINS);
5061
- const resolvedAdmins = uniqueUsers(
5062
- configuredAdmins.length > 0 ? configuredAdmins : owner ? [owner] : configuredAllowedUsers.slice(0, 1)
5063
- );
5076
+ const resolvedAdmins = uniqueUsers([
5077
+ ...configuredAdmins.length > 0 ? configuredAdmins : owner ? [owner] : configuredAllowedUsers.slice(0, 1),
5078
+ ...localAccessUser ? [localAccessUser] : []
5079
+ ]);
5064
5080
  const allowedUsers = uniqueUsers([...configuredAllowedUsers, ...resolvedAdmins, ...owner ? [owner] : []]);
5065
5081
  const publicUrl = publicUrlFromEnv(env, port);
5082
+ const accessOrigins = accessOriginsFromEnv(env, publicUrl);
5066
5083
  const sessionSecret = trim(env.MLCLAW_SESSION_SECRET ?? env.SESSION_SECRET) ?? randomBytes(48).toString("base64url");
5067
5084
  const configuredCredentialKey = trim(env.MLCLAW_CREDENTIAL_KEY);
5068
5085
  if (mode === "app" && !configuredCredentialKey) {
@@ -5087,6 +5104,7 @@ function loadConfig(env = process.env) {
5087
5104
  openclawUid: integer(env.MLCLAW_OPENCLAW_UID, 1e3),
5088
5105
  openclawGid: integer(env.MLCLAW_OPENCLAW_GID, 1e3),
5089
5106
  publicUrl,
5107
+ accessOrigins,
5090
5108
  providerUrl: trim(env.OPENID_PROVIDER_URL) ?? "https://huggingface.co",
5091
5109
  oauthClientId: trim(env.OAUTH_CLIENT_ID),
5092
5110
  oauthClientSecret: trim(env.OAUTH_CLIENT_SECRET),
@@ -5095,6 +5113,7 @@ function loadConfig(env = process.env) {
5095
5113
  credentialKey,
5096
5114
  credentialKeyGenerated: !configuredCredentialKey,
5097
5115
  cookieSecure: env.MLCLAW_COOKIE_SECURE === "0" ? false : !publicUrl.startsWith("http://"),
5116
+ sessionCookieName: gatewayLocation === "local" ? localSessionCookieName(trim(env.MLCLAW_RUNTIME_ID) ?? publicUrl) : SESSION_COOKIE_PREFIX,
5098
5117
  spaceId,
5099
5118
  canonicalSpaceId,
5100
5119
  canonicalCreatorUserId,
@@ -5102,6 +5121,8 @@ function loadConfig(env = process.env) {
5102
5121
  allowedUsers,
5103
5122
  adminUsers: resolvedAdmins,
5104
5123
  allowAnySignedIn: env.MLCLAW_ALLOW_ANY_SIGNED_IN === "1" || env.MLCLAW_ALLOW_ANY_SIGNED_IN === "true",
5124
+ localAccessUser,
5125
+ localAccessToken: gatewayLocation === "local" && localAccessUser ? deriveLocalAccessToken(sessionSecret) : void 0,
5105
5126
  mode,
5106
5127
  hfToken: readOptionalSecret(trim(env.MLCLAW_TRUSTED_HF_TOKEN_FILE)) ?? trim(env.HF_TOKEN ?? env.HUGGINGFACE_HUB_TOKEN),
5107
5128
  routerToken: trim(env.MLCLAW_ROUTER_TOKEN ?? env.HF_ROUTER_TOKEN),
@@ -5127,10 +5148,10 @@ function loadConfig(env = process.env) {
5127
5148
  model,
5128
5149
  modelChoices: runtimeSettings2.modelChoices ?? parseModelChoicesEnv(env.MLCLAW_MODEL_CHOICES, model),
5129
5150
  routerModelsUrl: trim(env.MLCLAW_ROUTER_MODELS_URL) ?? "https://router.huggingface.co/v1/models",
5130
- stateBucket: trim(env.OPENCLAW_HF_STATE_BUCKET),
5151
+ stateBucket,
5131
5152
  stateMountDir,
5132
5153
  statePrefix,
5133
- gatewayLocation: trim(env.MLCLAW_GATEWAY_LOCATION),
5154
+ gatewayLocation,
5134
5155
  runtimeImage: trim(env.MLCLAW_RUNTIME_IMAGE),
5135
5156
  runtimeId: trim(env.MLCLAW_RUNTIME_ID),
5136
5157
  templateRev: trim(env.MLCLAW_TEMPLATE_REV),
@@ -5138,6 +5159,30 @@ function loadConfig(env = process.env) {
5138
5159
  branding: resolveBranding(env, agentName)
5139
5160
  };
5140
5161
  }
5162
+ var SESSION_COOKIE_PREFIX = "mlclaw_session";
5163
+ function localSessionCookieName(identity) {
5164
+ return `${SESSION_COOKIE_PREFIX}_${createHash("sha256").update(identity).digest("hex").slice(0, 12)}`;
5165
+ }
5166
+ function accessOriginsFromEnv(env, publicUrl) {
5167
+ const configured = (env.MLCLAW_ACCESS_ORIGINS ?? "").split(",").map((value) => value.trim()).filter(Boolean);
5168
+ if (configured.length > 8) {
5169
+ throw new Error("MLCLAW_ACCESS_ORIGINS supports at most 8 origins");
5170
+ }
5171
+ const origins = [.../* @__PURE__ */ new Set([publicUrl, ...configured.map(parseAccessOrigin)])];
5172
+ if (origins.length > 8) {
5173
+ throw new Error("MLCLAW_ACCESS_ORIGINS supports at most 8 origins including MLCLAW_PUBLIC_URL");
5174
+ }
5175
+ return origins;
5176
+ }
5177
+ function parseAccessOrigin(value) {
5178
+ const url = new URL(value);
5179
+ if (url.protocol !== "http:" && url.protocol !== "https:" || url.username || url.password || url.hostname.includes("*") || url.pathname !== "/" || url.search || url.hash) {
5180
+ throw new Error(
5181
+ "MLCLAW_ACCESS_ORIGINS entries must be HTTP origins without credentials, wildcard hosts, paths, queries, or fragments"
5182
+ );
5183
+ }
5184
+ return url.origin;
5185
+ }
5141
5186
  function readOptionalSecret(file) {
5142
5187
  if (!file) {
5143
5188
  return void 0;
@@ -5172,6 +5217,14 @@ function resolveMode(params) {
5172
5217
  return params.canonicalCreatorUserId === params.spaceCreatorUserId ? "template" : "app";
5173
5218
  }
5174
5219
  function publicUrlFromEnv(env, port) {
5220
+ const explicit = trim(env.MLCLAW_PUBLIC_URL);
5221
+ if (explicit) {
5222
+ const url = new URL(explicit);
5223
+ if (url.protocol !== "http:" && url.protocol !== "https:" || url.username || url.password || url.pathname !== "/" || url.search || url.hash) {
5224
+ throw new Error("MLCLAW_PUBLIC_URL must be one HTTP origin without credentials, path, query, or fragment");
5225
+ }
5226
+ return url.origin;
5227
+ }
5175
5228
  const host = trim(env.SPACE_HOST);
5176
5229
  if (host) {
5177
5230
  return host.startsWith("http") ? host.replace(/\/+$/, "") : `https://${host.replace(/\/+$/, "")}`;
@@ -5179,7 +5232,10 @@ function publicUrlFromEnv(env, port) {
5179
5232
  return `http://127.0.0.1:${port}`;
5180
5233
  }
5181
5234
  function ownerFromSpaceId(spaceId) {
5182
- const owner = spaceId?.split("/")[0]?.trim();
5235
+ return ownerFromRepoId(spaceId);
5236
+ }
5237
+ function ownerFromRepoId(repoId) {
5238
+ const owner = repoId?.split("/")[0]?.trim();
5183
5239
  return owner || void 0;
5184
5240
  }
5185
5241
  function integer(value, fallback) {
@@ -7322,7 +7378,7 @@ var Hono2 = class extends Hono {
7322
7378
  };
7323
7379
 
7324
7380
  // src/mlclaw-space-runtime/csrf.ts
7325
- import { createHmac, randomBytes as randomBytes2, timingSafeEqual } from "node:crypto";
7381
+ import { createHmac as createHmac2, randomBytes as randomBytes2, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
7326
7382
  var CSRF_TTL_SECONDS = 60 * 60;
7327
7383
  function createCsrfToken(params) {
7328
7384
  const now = params.now ?? Date.now();
@@ -7355,16 +7411,16 @@ function verifyCsrfToken(params) {
7355
7411
  return payload.username === params.username && typeof payload.exp === "number" && payload.exp > now && typeof payload.nonce === "string" && payload.nonce.length > 0;
7356
7412
  }
7357
7413
  function sign(value, secret) {
7358
- return createHmac("sha256", secret).update(value).digest("base64url");
7414
+ return createHmac2("sha256", secret).update(value).digest("base64url");
7359
7415
  }
7360
7416
  function signatureMatches(a, b) {
7361
7417
  const left = Buffer.from(a);
7362
7418
  const right = Buffer.from(b);
7363
- return left.length === right.length && timingSafeEqual(left, right);
7419
+ return left.length === right.length && timingSafeEqual2(left, right);
7364
7420
  }
7365
7421
 
7366
7422
  // src/mlclaw-space-runtime/delegated-brokerkit.ts
7367
- import { createHash, createHmac as createHmac2, randomBytes as randomBytes4, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
7423
+ import { createHash as createHash2, createHmac as createHmac3, randomBytes as randomBytes4, timingSafeEqual as timingSafeEqual3 } from "node:crypto";
7368
7424
 
7369
7425
  // src/mlclaw-space-runtime/delegated-revisions.ts
7370
7426
  import { randomBytes as randomBytes3 } from "node:crypto";
@@ -7449,7 +7505,7 @@ var DelegatedBrokerKit = class {
7449
7505
  this.registry = registry;
7450
7506
  this.now = now;
7451
7507
  this.sourceDeadlineMs = sourceDeadlineMs;
7452
- this.key = createHmac2("sha256", sessionSecret).update("mlclaw/brokerkit-delegated-web/v1", "utf8").digest();
7508
+ this.key = createHmac3("sha256", sessionSecret).update("mlclaw/brokerkit-delegated-web/v1", "utf8").digest();
7453
7509
  }
7454
7510
  key;
7455
7511
  handles = /* @__PURE__ */ new Map();
@@ -7666,7 +7722,7 @@ var DelegatedBrokerKit = class {
7666
7722
  this.handlesByIdentity.delete(requestIdentity(record.sourceId, record.requestId, record.revision));
7667
7723
  }
7668
7724
  sign(encoded) {
7669
- return createHmac2("sha256", this.key).update(encoded, "utf8").digest("base64url");
7725
+ return createHmac3("sha256", this.key).update(encoded, "utf8").digest("base64url");
7670
7726
  }
7671
7727
  };
7672
7728
  async function decideWithRecovery(source, requestId, action, decision) {
@@ -7740,7 +7796,7 @@ function handleExpiry(request) {
7740
7796
  return request.pending_expires_at ?? request.active_expires_at ?? "";
7741
7797
  }
7742
7798
  function decisionKey(record, action, actor) {
7743
- return createHash("sha256").update(
7799
+ return createHash2("sha256").update(
7744
7800
  ["mlclaw-brokerkit-decision-v1", record.sourceId, record.requestId, String(record.revision), action, actor].join(
7745
7801
  "\0"
7746
7802
  ),
@@ -7813,7 +7869,7 @@ function validTokenNonce(record) {
7813
7869
  function safeEqual(left, right) {
7814
7870
  const a = Buffer.from(left, "utf8");
7815
7871
  const b = Buffer.from(right, "utf8");
7816
- return a.length === b.length && timingSafeEqual2(a, b);
7872
+ return a.length === b.length && timingSafeEqual3(a, b);
7817
7873
  }
7818
7874
  function safeSourceError(error) {
7819
7875
  const code = error instanceof BrokerOperatorError ? error.code : error instanceof DelegatedBrokerKitError ? error.code : void 0;
@@ -7973,7 +8029,7 @@ import fs from "node:fs/promises";
7973
8029
  import path from "node:path";
7974
8030
 
7975
8031
  // src/mlclaw-space-runtime/mcp-integrations.ts
7976
- import { createHmac as createHmac3, timingSafeEqual as timingSafeEqual3 } from "node:crypto";
8032
+ import { createHmac as createHmac4, timingSafeEqual as timingSafeEqual4 } from "node:crypto";
7977
8033
  import http from "node:http";
7978
8034
  import { Readable } from "node:stream";
7979
8035
  var MAX_REQUEST_BYTES = 16 * 1024 * 1024;
@@ -8237,7 +8293,7 @@ var ResearchRpcError = class extends Error {
8237
8293
  }
8238
8294
  };
8239
8295
  function deriveInternalToken(secret) {
8240
- return createHmac3("sha256", secret).update("mlclaw:mcp-integrations:v1").digest("base64url");
8296
+ return createHmac4("sha256", secret).update("mlclaw:mcp-integrations:v1").digest("base64url");
8241
8297
  }
8242
8298
  function managedMcpServerConfig(config2) {
8243
8299
  const headers = { [INTERNAL_HEADER]: deriveInternalToken(config2.sessionSecret) };
@@ -8467,7 +8523,7 @@ function validInternalToken(value, expected) {
8467
8523
  }
8468
8524
  const actualBuffer = Buffer.from(value);
8469
8525
  const expectedBuffer = Buffer.from(expected);
8470
- return actualBuffer.length === expectedBuffer.length && timingSafeEqual3(actualBuffer, expectedBuffer);
8526
+ return actualBuffer.length === expectedBuffer.length && timingSafeEqual4(actualBuffer, expectedBuffer);
8471
8527
  }
8472
8528
  function requestHeader(headers, name) {
8473
8529
  const value = headers[name];
@@ -8528,6 +8584,7 @@ function remainingUpstreamTimeout(deadline) {
8528
8584
  // src/mlclaw-space-runtime/openclaw-config.ts
8529
8585
  var BROKER_MCP_CONNECTION_TIMEOUT_MS = 1e4;
8530
8586
  var BROKER_MCP_REQUEST_TIMEOUT_MS = 45e3;
8587
+ var AUTOMATIC_SESSION_RESET_DISABLED_MINUTES = 2147483647;
8531
8588
  async function configureOpenClawGateway(config2) {
8532
8589
  const raw2 = await fs.readFile(config2.openclawConfigPath, "utf8");
8533
8590
  const openclawConfig = JSON.parse(raw2);
@@ -8547,10 +8604,11 @@ async function configureOpenClawGateway(config2) {
8547
8604
  gateway.controlUi = {
8548
8605
  ...typeof gateway.controlUi === "object" && gateway.controlUi ? gateway.controlUi : {},
8549
8606
  dangerouslyDisableDeviceAuth: true,
8550
- allowedOrigins: [config2.publicUrl],
8607
+ allowedOrigins: config2.accessOrigins,
8551
8608
  embedSandbox: "scripts"
8552
8609
  };
8553
8610
  configureOpenClawModels(openclawConfig, config2);
8611
+ disableAutomaticSessionResets(openclawConfig);
8554
8612
  configureManagedMcpServers(openclawConfig, config2);
8555
8613
  configureBrokerMcpServer(openclawConfig, config2);
8556
8614
  configureBrokerKitPlugin(openclawConfig, config2);
@@ -8562,6 +8620,18 @@ async function configureOpenClawGateway(config2) {
8562
8620
  await fs.chown(config2.openclawConfigPath, config2.openclawUid, config2.openclawGid);
8563
8621
  }
8564
8622
  }
8623
+ function disableAutomaticSessionResets(openclawConfig) {
8624
+ const session = object(openclawConfig, "session");
8625
+ session.reset = {
8626
+ mode: "idle",
8627
+ idleMinutes: AUTOMATIC_SESSION_RESET_DISABLED_MINUTES
8628
+ };
8629
+ delete session.idleMinutes;
8630
+ delete session.resetByType;
8631
+ delete session.resetByChannel;
8632
+ const maintenance = object(session, "maintenance");
8633
+ maintenance.resetArchiveRetention = false;
8634
+ }
8565
8635
  function configureBrokerMcpServer(openclawConfig, config2) {
8566
8636
  const servers = object(object(openclawConfig, "mcp"), "servers");
8567
8637
  if (!config2.brokerAgentUrl || !config2.brokerAgentSecretFile) {
@@ -8860,7 +8930,9 @@ function validateOpenAiApiKey(value) {
8860
8930
 
8861
8931
  // src/mlclaw-space-runtime/pages.ts
8862
8932
  function templatePage(config2) {
8863
- return page("ML Claw", `
8933
+ return page(
8934
+ "ML Claw",
8935
+ `
8864
8936
  <main>
8865
8937
  <img src="/assets/mlclaw.svg" alt="ML Claw" class="logo">
8866
8938
  <h1>ML Claw</h1>
@@ -8880,30 +8952,85 @@ function templatePage(config2) {
8880
8952
  <p class="muted">Manual duplication is for development or advanced setup only.</p>
8881
8953
  <p class="muted">Source Space: ${escapeHtml(config2.spaceId ?? config2.canonicalSpaceId)}</p>
8882
8954
  </main>
8883
- `);
8955
+ `
8956
+ );
8884
8957
  }
8885
8958
  function loginPage(config2, message, next = "/") {
8886
8959
  const oauthReady = Boolean(config2.oauthClientId && config2.oauthClientSecret);
8887
8960
  const loginPath = next === "/" ? "/oauth/login" : `/oauth/login?next=${encodeURIComponent(next)}`;
8888
8961
  const loginHref = new URL(loginPath, config2.publicUrl).toString();
8889
- return page(`${config2.branding.name} Login`, `
8962
+ return page(
8963
+ `${config2.branding.name} Login`,
8964
+ `
8890
8965
  <main>
8891
8966
  <img src="/assets/hf-logo.svg" alt="Hugging Face" class="logo">
8892
8967
  <h1>${escapeHtml(config2.branding.name)}</h1>
8893
8968
  ${message ? `<p class="notice">${escapeHtml(message)}</p>` : ""}
8894
8969
  ${oauthReady ? `<a class="button" href="${escapeHtml(loginHref)}" target="_blank" rel="noopener">Sign in with Hugging Face</a>` : `<p class="notice">Hugging Face OAuth is not configured for this Space. Update the Space README metadata to include <code>hf_oauth: true</code>, then rebuild.</p>`}
8895
8970
  </main>
8896
- `);
8971
+ `
8972
+ );
8973
+ }
8974
+ function localLoginPage(config2) {
8975
+ return page(
8976
+ `${config2.branding.name} Local Access`,
8977
+ `
8978
+ <main>
8979
+ <img src="/assets/mlclaw.svg" alt="ML Claw" class="logo">
8980
+ <h1>${escapeHtml(config2.branding.name)}</h1>
8981
+ <p class="muted">Use the private access link printed by the ML Claw CLI.</p>
8982
+ <form id="local-login-form">
8983
+ <label for="local-access-token">Local access code</label>
8984
+ <input id="local-access-token" name="token" type="password" autocomplete="off" required>
8985
+ <button class="button" type="submit">Open gateway</button>
8986
+ </form>
8987
+ <p id="local-login-status" class="notice" role="status"></p>
8988
+ </main>
8989
+ <script>
8990
+ const form = document.getElementById("local-login-form");
8991
+ const input = document.getElementById("local-access-token");
8992
+ const status = document.getElementById("local-login-status");
8993
+ const submit = async () => {
8994
+ status.textContent = "Signing in...";
8995
+ const response = await fetch("/mlclaw/api/local-session", {
8996
+ method: "POST",
8997
+ credentials: "same-origin",
8998
+ headers: { "content-type": "application/json" },
8999
+ body: JSON.stringify({ token: input.value }),
9000
+ });
9001
+ input.value = "";
9002
+ if (response.ok) {
9003
+ location.replace("/");
9004
+ return;
9005
+ }
9006
+ status.textContent = "The local access link is invalid. Run mlclaw gateway status again.";
9007
+ };
9008
+ form.addEventListener("submit", (event) => {
9009
+ event.preventDefault();
9010
+ submit().catch(() => { status.textContent = "The local gateway could not be reached."; });
9011
+ });
9012
+ const token = location.hash.slice(1);
9013
+ if (token) {
9014
+ history.replaceState(null, "", location.pathname);
9015
+ input.value = token;
9016
+ submit().catch(() => { status.textContent = "The local gateway could not be reached."; });
9017
+ }
9018
+ </script>
9019
+ `
9020
+ );
8897
9021
  }
8898
9022
  function unauthorizedPage(username) {
8899
- return page("ML Claw Access", `
9023
+ return page(
9024
+ "ML Claw Access",
9025
+ `
8900
9026
  <main>
8901
9027
  <h1>Access not allowed</h1>
8902
9028
  <p>The signed-in Hugging Face account <strong>${escapeHtml(username)}</strong> is not allowed to operate this Space.</p>
8903
9029
  <p class="muted">Set <code>MLCLAW_ALLOWED_USERS</code> to a comma-separated list of usernames, then restart the Space.</p>
8904
9030
  <a class="button secondary" href="/mlclaw/logout">Sign out</a>
8905
9031
  </main>
8906
- `);
9032
+ `
9033
+ );
8907
9034
  }
8908
9035
  function page(title, body) {
8909
9036
  return `<!doctype html>
@@ -9120,7 +9247,7 @@ function positiveNumber2(value) {
9120
9247
  }
9121
9248
 
9122
9249
  // src/mlclaw-space-runtime/cookies.ts
9123
- import { createHmac as createHmac4, randomBytes as randomBytes6, timingSafeEqual as timingSafeEqual4 } from "node:crypto";
9250
+ import { createHmac as createHmac5, randomBytes as randomBytes6, timingSafeEqual as timingSafeEqual5 } from "node:crypto";
9124
9251
  function createSignedCookie(options, payload) {
9125
9252
  const body = Buffer.from(JSON.stringify({
9126
9253
  ...payload,
@@ -9172,12 +9299,12 @@ function randomState() {
9172
9299
  return randomBytes6(24).toString("base64url");
9173
9300
  }
9174
9301
  function sign2(value, secret) {
9175
- return createHmac4("sha256", secret).update(value).digest("base64url");
9302
+ return createHmac5("sha256", secret).update(value).digest("base64url");
9176
9303
  }
9177
9304
  function signatureMatches2(a, b) {
9178
9305
  const left = Buffer.from(a);
9179
9306
  const right = Buffer.from(b);
9180
- return left.length === right.length && timingSafeEqual4(left, right);
9307
+ return left.length === right.length && timingSafeEqual5(left, right);
9181
9308
  }
9182
9309
  function parseCookies(header) {
9183
9310
  const cookies = /* @__PURE__ */ new Map();
@@ -9220,33 +9347,39 @@ var STATE_COOKIE = "mlclaw_oauth";
9220
9347
  var SESSION_TTL_SECONDS = 60 * 60 * 24 * 30;
9221
9348
  var STATE_TTL_SECONDS = 60 * 10;
9222
9349
  function createSessionCookie(params) {
9223
- return createSignedCookie({
9224
- name: SESSION_COOKIE,
9225
- secret: params.sessionSecret,
9226
- maxAgeSeconds: SESSION_TTL_SECONDS,
9227
- secure: params.secure
9228
- }, { username: params.username });
9350
+ return createSignedCookie(
9351
+ {
9352
+ name: params.cookieName ?? SESSION_COOKIE,
9353
+ secret: params.sessionSecret,
9354
+ maxAgeSeconds: SESSION_TTL_SECONDS,
9355
+ secure: params.secure
9356
+ },
9357
+ { username: params.username }
9358
+ );
9229
9359
  }
9230
9360
  function createOauthStateCookie(params) {
9231
9361
  const state = params.state ?? randomState();
9232
9362
  return {
9233
9363
  state,
9234
- cookie: createSignedCookie({
9235
- name: STATE_COOKIE,
9236
- secret: params.sessionSecret,
9237
- maxAgeSeconds: STATE_TTL_SECONDS,
9238
- secure: params.secure
9239
- }, { state, next: normalizeNext(params.next), intent: params.intent ?? "login" })
9364
+ cookie: createSignedCookie(
9365
+ {
9366
+ name: STATE_COOKIE,
9367
+ secret: params.sessionSecret,
9368
+ maxAgeSeconds: STATE_TTL_SECONDS,
9369
+ secure: params.secure
9370
+ },
9371
+ { state, next: normalizeNext(params.next), intent: params.intent ?? "login" }
9372
+ )
9240
9373
  };
9241
9374
  }
9242
- function clearSessionCookie(secure) {
9243
- return clearCookie(SESSION_COOKIE, secure);
9375
+ function clearSessionCookie(secure, cookieName = SESSION_COOKIE) {
9376
+ return clearCookie(cookieName, secure);
9244
9377
  }
9245
9378
  function clearOauthStateCookie(secure) {
9246
9379
  return clearCookie(STATE_COOKIE, secure);
9247
9380
  }
9248
- function readSession(cookieHeader, sessionSecret) {
9249
- return verifySignedCookie(cookieHeader, SESSION_COOKIE, sessionSecret);
9381
+ function readSession(cookieHeader, sessionSecret, cookieName = SESSION_COOKIE) {
9382
+ return verifySignedCookie(cookieHeader, cookieName, sessionSecret);
9250
9383
  }
9251
9384
  function readOauthState(cookieHeader, sessionSecret) {
9252
9385
  return verifySignedCookie(cookieHeader, STATE_COOKIE, sessionSecret);
@@ -9564,6 +9697,7 @@ function createSpaceRuntimeApp(config2, controls) {
9564
9697
  const allowBrokerKitSummary = fixedWindowRateLimit(12, 6e4);
9565
9698
  const allowDelegatedEvents = fixedWindowRateLimit(60, 6e4);
9566
9699
  const allowSummaryEvents = fixedWindowRateLimit(60, 6e4);
9700
+ const allowLocalLogin = fixedWindowRateLimit(10, 6e4);
9567
9701
  const openAiCredentials = new OpenAiCredentialStore(config2.openaiCredentialStoreFile, config2.credentialKey);
9568
9702
  app.get("/health", (c) => health(c, config2, controls));
9569
9703
  app.get("/healthz", (c) => health(c, config2, controls));
@@ -9599,10 +9733,58 @@ function createSpaceRuntimeApp(config2, controls) {
9599
9733
  );
9600
9734
  app.get("/oauth/login", (c) => handleOauthLogin(c, config2));
9601
9735
  app.get("/oauth/callback", (c) => handleOauthCallback(c, config2, controls));
9736
+ app.get("/mlclaw/local-login", (c) => {
9737
+ if (!config2.localAccessUser || !config2.localAccessToken || config2.gatewayLocation !== "local") {
9738
+ return c.text("not found\n", 404);
9739
+ }
9740
+ c.header(
9741
+ "content-security-policy",
9742
+ "default-src 'self'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'self'; img-src 'self' data:; base-uri 'none'; frame-ancestors 'none'; form-action 'self'"
9743
+ );
9744
+ c.header("cache-control", "no-store");
9745
+ c.header("referrer-policy", "no-referrer");
9746
+ return c.html(localLoginPage(config2));
9747
+ });
9748
+ app.post("/mlclaw/api/local-session", async (c) => {
9749
+ if (!config2.localAccessUser || !config2.localAccessToken || config2.gatewayLocation !== "local") {
9750
+ return c.json({ ok: false, error: "not found" }, 404);
9751
+ }
9752
+ const origin = c.req.header("origin");
9753
+ if (!origin || !config2.accessOrigins.includes(origin) || !allowLocalLogin(origin)) {
9754
+ return c.json({ ok: false, error: "access denied" }, 403);
9755
+ }
9756
+ const contentLength = Number.parseInt(c.req.header("content-length") ?? "0", 10);
9757
+ if (contentLength > 512 || !(c.req.header("content-type") ?? "").startsWith("application/json")) {
9758
+ return c.json({ ok: false, error: "invalid request" }, 400);
9759
+ }
9760
+ const text = await c.req.text();
9761
+ if (text.length > 512) {
9762
+ return c.json({ ok: false, error: "invalid request" }, 400);
9763
+ }
9764
+ let token;
9765
+ try {
9766
+ token = JSON.parse(text).token;
9767
+ } catch {
9768
+ return c.json({ ok: false, error: "invalid request" }, 400);
9769
+ }
9770
+ if (typeof token !== "string" || !localAccessTokenMatches(token, config2.localAccessToken)) {
9771
+ return c.json({ ok: false, error: "access denied" }, 403);
9772
+ }
9773
+ c.header(
9774
+ "set-cookie",
9775
+ createSessionCookie({
9776
+ username: config2.localAccessUser,
9777
+ sessionSecret: config2.sessionSecret,
9778
+ secure: origin.startsWith("https://"),
9779
+ cookieName: config2.sessionCookieName
9780
+ })
9781
+ );
9782
+ return c.json({ ok: true });
9783
+ });
9602
9784
  app.get("/login", (c) => c.html(loginPage(config2, void 0, normalizeNext(c.req.query("next") ?? "/"))));
9603
- app.get("/logout", (c) => logoutResponse(config2, false));
9604
- app.get("/mlclaw/logout", (c) => logoutResponse(config2, false));
9605
- app.post("/mlclaw/api/logout", (c) => logoutResponse(config2, true));
9785
+ app.get("/logout", (c) => logoutResponse(c, config2, false));
9786
+ app.get("/mlclaw/logout", (c) => logoutResponse(c, config2, false));
9787
+ app.post("/mlclaw/api/logout", (c) => logoutResponse(c, config2, true));
9606
9788
  app.get("/mlclaw/assets/*", async (c) => {
9607
9789
  const relative = c.req.path.slice("/mlclaw/assets/".length);
9608
9790
  const safe = safeRelativePath(relative);
@@ -9903,7 +10085,7 @@ function handleOauthLogin(c, config2) {
9903
10085
  if (!config2.oauthClientId || !config2.oauthClientSecret) {
9904
10086
  return c.html(loginPage(config2, "Hugging Face OAuth is not configured.", next));
9905
10087
  }
9906
- const session = readSession(c.req.header("cookie"), config2.sessionSecret);
10088
+ const session = readSession(c.req.header("cookie"), config2.sessionSecret, config2.sessionCookieName);
9907
10089
  const integrationsRequested = c.req.query("intent") === "integrations";
9908
10090
  const intent = integrationsRequested && session && isAdmin(config2, session.username) ? "integrations" : "login";
9909
10091
  const { state, cookie } = createOauthStateCookie({
@@ -9948,7 +10130,7 @@ async function handleOauthCallback(c, config2, controls) {
9948
10130
  return c.html(loginPage(config2, "Hugging Face sign-in failed. Try again."), 401);
9949
10131
  }
9950
10132
  if (stateCookie.intent === "integrations") {
9951
- const session = readSession(c.req.header("cookie"), config2.sessionSecret);
10133
+ const session = readSession(c.req.header("cookie"), config2.sessionSecret, config2.sessionCookieName);
9952
10134
  if (!session || !isAdmin(config2, session.username) || session.username !== identity.username) {
9953
10135
  return c.html(loginPage(config2, "Integration authorization requires the signed-in ML Claw administrator."), 403);
9954
10136
  }
@@ -9971,7 +10153,8 @@ async function handleOauthCallback(c, config2, controls) {
9971
10153
  createSessionCookie({
9972
10154
  username: identity.username,
9973
10155
  sessionSecret: config2.sessionSecret,
9974
- secure: config2.cookieSecure
10156
+ secure: config2.cookieSecure,
10157
+ cookieName: config2.sessionCookieName
9975
10158
  })
9976
10159
  );
9977
10160
  headers.append("set-cookie", clearOauthStateCookie(config2.cookieSecure));
@@ -10042,9 +10225,11 @@ function trustedBrokerKitHeaders(mode, origin) {
10042
10225
  if (asset) headers.set("access-control-allow-origin", "null");
10043
10226
  return headers;
10044
10227
  }
10045
- function logoutResponse(config2, json) {
10228
+ function logoutResponse(c, config2, json) {
10046
10229
  const headers = new Headers();
10047
- headers.append("set-cookie", clearSessionCookie(config2.cookieSecure));
10230
+ const origin = c.req.header("origin");
10231
+ const secure = origin && config2.accessOrigins.includes(origin) ? origin.startsWith("https://") : config2.cookieSecure;
10232
+ headers.append("set-cookie", clearSessionCookie(secure, config2.sessionCookieName));
10048
10233
  if (json) {
10049
10234
  headers.set("content-type", "application/json; charset=utf-8");
10050
10235
  return new Response(`${JSON.stringify({ ok: true })}
@@ -10054,7 +10239,7 @@ function logoutResponse(config2, json) {
10054
10239
  return new Response(null, { status: 302, headers });
10055
10240
  }
10056
10241
  function requireAllowed(c, config2) {
10057
- const session = readSession(c.req.header("cookie"), config2.sessionSecret);
10242
+ const session = readSession(c.req.header("cookie"), config2.sessionSecret, config2.sessionCookieName);
10058
10243
  if (!session) {
10059
10244
  return unauthenticated(c, config2);
10060
10245
  }
@@ -10160,7 +10345,10 @@ function unauthenticated(c, config2) {
10160
10345
  return c.json({ ok: false, error: "authentication required" }, 401);
10161
10346
  }
10162
10347
  if (isBrowserNavigation(c)) {
10163
- return c.redirect(`/login?next=${encodeURIComponent(next)}`, 302);
10348
+ return c.redirect(
10349
+ config2.gatewayLocation === "local" ? "/mlclaw/local-login" : `/login?next=${encodeURIComponent(next)}`,
10350
+ 302
10351
+ );
10164
10352
  }
10165
10353
  return c.html(loginPage(config2, void 0, next), 401);
10166
10354
  }
@@ -10739,7 +10927,7 @@ async function proxyHttp(req, res, config2, identity) {
10739
10927
  delete headers["accept-encoding"];
10740
10928
  delete headers["Accept-Encoding"];
10741
10929
  }
10742
- addTrustedProxyHeaders(headers, config2, identity);
10930
+ addTrustedProxyHeaders(headers, config2, identity, requestAccessOrigin(req, config2));
10743
10931
  const upstream = http2.request(
10744
10932
  {
10745
10933
  host: config2.openclawHost,
@@ -10797,7 +10985,7 @@ function proxyWebSocket(req, socket, head, config2, identity) {
10797
10985
  headers.host = `${config2.openclawHost}:${config2.openclawPort}`;
10798
10986
  headers.connection = "Upgrade";
10799
10987
  headers.upgrade = req.headers.upgrade ?? "websocket";
10800
- addTrustedProxyHeaders(headers, config2, identity);
10988
+ addTrustedProxyHeaders(headers, config2, identity, requestAccessOrigin(req, config2));
10801
10989
  upstream.write(`${req.method ?? "GET"} ${req.url ?? "/"} HTTP/${req.httpVersion}\r
10802
10990
  `);
10803
10991
  for (const [key, value] of Object.entries(headers)) {
@@ -10841,19 +11029,26 @@ function sanitizeHeaders(headers) {
10841
11029
  if (HOP_BY_HOP_HEADERS.has(lower)) {
10842
11030
  continue;
10843
11031
  }
10844
- if (lower.startsWith("x-forwarded-") || lower.startsWith("x-openclaw-") || lower === "authorization") {
11032
+ if (lower.startsWith("x-forwarded-") || lower.startsWith("x-openclaw-") || lower.startsWith("tailscale-") || lower === "authorization") {
10845
11033
  continue;
10846
11034
  }
10847
11035
  out[key] = value;
10848
11036
  }
10849
11037
  return out;
10850
11038
  }
10851
- function addTrustedProxyHeaders(headers, config2, identity) {
11039
+ function addTrustedProxyHeaders(headers, config2, identity, accessOrigin) {
10852
11040
  headers["x-forwarded-user"] = identity.username;
10853
- headers["x-forwarded-proto"] = config2.publicUrl.startsWith("https://") ? "https" : "http";
10854
- headers["x-forwarded-host"] = new URL(config2.publicUrl).host;
11041
+ headers["x-forwarded-proto"] = accessOrigin.startsWith("https://") ? "https" : "http";
11042
+ headers["x-forwarded-host"] = new URL(accessOrigin).host;
10855
11043
  headers["x-openclaw-scopes"] = resolveControlUiScopes(config2, identity).join(",");
10856
11044
  }
11045
+ function requestAccessOrigin(req, config2) {
11046
+ const host = req.headers.host?.trim().toLowerCase();
11047
+ if (!host) {
11048
+ return config2.publicUrl;
11049
+ }
11050
+ return config2.accessOrigins.find((origin) => new URL(origin).host.toLowerCase() === host) ?? config2.publicUrl;
11051
+ }
10857
11052
  function resolveControlUiScopes(config2, identity) {
10858
11053
  return config2.adminUsers.includes(identity.username) ? ADMIN_CONTROL_UI_SCOPES : USER_CONTROL_UI_SCOPES;
10859
11054
  }
@@ -10934,7 +11129,7 @@ var SpaceRuntimeServer = class {
10934
11129
  server2.on("upgrade", (req, socket, head) => {
10935
11130
  const netSocket = socket;
10936
11131
  try {
10937
- const session = readSession(req.headers.cookie, this.config.sessionSecret);
11132
+ const session = readSession(req.headers.cookie, this.config.sessionSecret, this.config.sessionCookieName);
10938
11133
  if (!session || !this.isAllowed(session.username)) {
10939
11134
  rejectWebSocket(netSocket);
10940
11135
  return;
@@ -11011,7 +11206,7 @@ var SpaceRuntimeServer = class {
11011
11206
  res.off("close", abortRequest);
11012
11207
  }
11013
11208
  }
11014
- const session = readSession(req.headers.cookie, this.config.sessionSecret);
11209
+ const session = readSession(req.headers.cookie, this.config.sessionSecret, this.config.sessionCookieName);
11015
11210
  if (!session) {
11016
11211
  this.sendUnauthenticated(req, res, url);
11017
11212
  return;
@@ -11091,6 +11286,10 @@ var SpaceRuntimeServer = class {
11091
11286
  }
11092
11287
  sendUnauthenticated(req, res, url) {
11093
11288
  const next = normalizeNext(`${url.pathname}${url.search}`);
11289
+ if (this.config.gatewayLocation === "local" && isBrowserNavigation2(req)) {
11290
+ this.sendRedirect(res, "/mlclaw/local-login");
11291
+ return;
11292
+ }
11094
11293
  if (url.pathname === "/" && (req.method === "GET" || req.method === "HEAD")) {
11095
11294
  this.sendHtml(res, loginPage(this.config, void 0, next));
11096
11295
  return;